From 372e2bf63065aa6741c255cdea508bf4726dbc14 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Sun, 5 Nov 2023 11:43:18 -0500 Subject: [PATCH] .some(mash) --- .github/dependabot.yml | 6 + .github/scripts/build.ts | 67 +++++++++++ .github/scripts/index.ts | 162 ++++++++++++++++++++++++++ .github/scripts/trawl.ts | 67 +++++++++++ .github/workflows/ci.yml | 12 ++ .github/workflows/deploy.yml | 53 +++++++++ .gitignore | 3 + .vscode/settings.json | 5 + LICENSE.txt | 201 +++++++++++++++++++++++++++++++++ README.md | 213 +++++++++++++++++++++++++++++++++++ mash | 79 +++++++++++++ scripts/demo | 14 +++ 12 files changed, 882 insertions(+) create mode 100644 .github/dependabot.yml create mode 100755 .github/scripts/build.ts create mode 100755 .github/scripts/index.ts create mode 100755 .github/scripts/trawl.ts create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 mash create mode 100755 scripts/demo diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/scripts/build.ts b/.github/scripts/build.ts new file mode 100755 index 0000000..ecdc6d3 --- /dev/null +++ b/.github/scripts/build.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env -S pkgx deno run --allow-read --allow-write + +import * as flags from "https://deno.land/std@0.206.0/flags/mod.ts"; +import { Path } from "https://deno.land/x/libpkgx@v0.16.0/mod.ts"; +import { Script } from "./index.ts"; + +const args = flags.parse(Deno.args); +const indir = (s => Path.abs(s) ?? Path.cwd().join(s))(args['input']) +const outdir = (s => Path.abs(s) ?? Path.cwd().join(s))(args['output']) +const index_json_path = args['index-json'] + +if (!indir || !outdir || !index_json_path) { + console.error(`usage: build.ts --input --output --index-json `); + Deno.exit(64); +} + +const scripts = JSON.parse(Deno.readTextFileSync(index_json_path)).scripts as Script[] + +const categories: Record = {} +const users: Record = {} + +for (const script of scripts) { + if (script.category) { + categories[script.category] ??= [] + categories[script.category].push(script) + } + const user = script.fullname.split('/')[0] + users[user] ??= [] + users[user].push(script) +} + +// sort each entry in categories and users by the script birthtime +for (const scripts of Object.values(categories)) { + scripts.sort((a, b) => new Date(b.birthtime).getTime() - new Date(a.birthtime).getTime()); +} +for (const scripts of Object.values(users)) { + scripts.sort((a, b) => new Date(b.birthtime).getTime() - new Date(a.birthtime).getTime()); +} + +for (const category in categories) { + const d = outdir.join(category) + const scripts = categories[category].filter(({description}) => description) + d.mkdir('p').join('index.json').write({ json: { scripts }, force: true, space: 2 }) +} + +for (const user in users) { + const d = outdir.join('u', user) + const scripts = users[user].filter(({description}) => description) + d.mkdir('p').join('index.json').write({ json: { scripts }, force: true, space: 2 }) +} + +for (const script of scripts) { + console.error(script) + const [user, name] = script.fullname.split('/') + const { category } = script + const gh_slug = new URL(script.url).pathname.split('/').slice(1, 3).join('/') + const infile = indir.join(gh_slug, 'scripts', name) + + infile.cp({ into: outdir.join('u', user).mkdir('p') }) + + const leaf = infile.basename().split('-').slice(1).join('-') + if (category && !outdir.join(category, leaf).exists()) { // not already snagged + infile.cp({ to: outdir.join(category, leaf) }) + } +} + +outdir.join('u/index.json').write({ json: { users }, force: true, space: 2}) diff --git a/.github/scripts/index.ts b/.github/scripts/index.ts new file mode 100755 index 0000000..1f753fb --- /dev/null +++ b/.github/scripts/index.ts @@ -0,0 +1,162 @@ +#!/usr/bin/env -S pkgx deno run --allow-run=bash --allow-read=. + +import { join, basename, dirname } from "https://deno.land/std@0.206.0/path/mod.ts"; +import { walk, exists } from "https://deno.land/std@0.206.0/fs/mod.ts"; +import * as flags from "https://deno.land/std@0.206.0/flags/mod.ts"; + +if (import.meta.main) { + const args = flags.parse(Deno.args); + const inputdir = args['input'] + + if (!inputdir) { + console.error(`usage: index.ts --input `); + Deno.exit(1); + } + + Deno.chdir(inputdir); + + const scripts: Script[] = [] + for await (const slug of iterateGitRepos('.')) { + console.error(`iterating: ${slug}`); + scripts.push(...await get_metadata(slug)); + } + + scripts.sort((a, b) => b.birthtime.getTime() - a.birthtime.getTime()); + + const categories = (() => { + const categories: Record = {} + for (const script of scripts) { + if (script.category && script.description) { + categories[script.category] ??= 0; + categories[script.category]++; + } + } + return Object.keys(categories) + })(); + + + console.log(JSON.stringify({ scripts, categories }, null, 2)); +} + +////////////////////////////////////////////////////////////////////// lib +async function extractMarkdownSection(filePath: string, sectionTitle: string): Promise { + const data = await Deno.readTextFile(filePath); + const lines = data.split('\n'); + let capturing = false; + let sectionContent = ''; + + for (const line of lines) { + if (line.startsWith('## ')) { + if (capturing) { + break; // stop if we reach another ## section + } else if (normalize_title(line.slice(3)) == normalize_title(sectionTitle)) { + capturing = true; + } else if (line.slice(3).trim() == mash_title(sectionTitle)) { + capturing = true; + } + } else if (capturing) { + sectionContent += line + '\n'; + } + } + + return chuzzle(sectionContent); + + function normalize_title(input: string) { + return input.toLowerCase().replace(/[^a-z0-9]/g, '').trim(); + } + + function mash_title(input: string) { + const [category, ...name] = input.trim().split('-') + return `\`mash ${category} ${name.join('-')}\`` + } +} + +export interface Script { + fullname: string + birthtime: Date + description?: string + avatar: string + url: string + category?: string + README?: string + cmd: string +} + +async function* iterateGitRepos(basePath: string): AsyncIterableIterator { + for await (const entry of walk(basePath, { maxDepth: 2 })) { + if (entry.isDirectory && await exists(join(entry.path, '.git'))) { + yield entry.path; + } + } +} + +function chuzzle(ln: string): string | undefined { + const out = ln.trim() + return out || undefined; +} + +async function get_metadata(slug: string) { + + const cmdString = `git -C '${slug}' log --pretty=format:'%H %aI' --name-only --diff-filter=AR -- scripts`; + + const process = Deno.run({ + cmd: ["bash", "-c", cmdString], + stdout: "piped" + }); + + const output = new TextDecoder().decode(await process.output()); + await process.status(); + process.close(); + + const lines = chuzzle(output)?.split('\n') ?? []; + const rv: Script[] = [] + let currentCommitDate: string | undefined; + + for (let line of lines) { + line = line.trim() + + if (line.includes(' ')) { // Detect lines with commit hash and date + currentCommitDate = line.split(' ')[1]; + } else if (line && currentCommitDate) { + const filename = join(slug, line) + if (!await exists(filename)) { + // the file used to exist but has been deleted + console.warn("skipping deleted: ", filename, line) + continue + } + + console.error(line) + + const repo_metadata = JSON.parse(await Deno.readTextFile(join(slug, 'metadata.json'))) + + const README = await extractMarkdownSection(join(slug, 'README.md'), basename(filename)); + const birthtime = new Date(currentCommitDate!); + const avatar = repo_metadata.avatar + const fullname = join(dirname(slug), ...stem(filename)) + const url = repo_metadata.url +'/scripts/' + basename(filename) + const category = (([x, y]) => x?.length > 0 && y ? x : undefined)(basename(filename).split("-")) + const description = README ? extract_description(README) : undefined + const cmd = category ? `mash ${category} ${basename(filename).split('-').slice(1).join('-')}` : `mash ${fullname}` + + rv.push({ fullname, birthtime, description, avatar, url, category, README, cmd }) + } + } + + return rv; + + function stem(filename: string): string[] { + const base = basename(filename) + const parts = base.split('.') + if (parts.length == 1) { + return parts.slice(0, 1) + } else { + return parts.slice(0, -1) // no extension, but allow eg. foo.bar.js to be foo.bar + } + } +} + +function extract_description(input: string) { + const regex = /^(.*?)\n#|^.*$/ms; + const match = regex.exec(input); + return match?.[1]?.trim(); +} diff --git a/.github/scripts/trawl.ts b/.github/scripts/trawl.ts new file mode 100755 index 0000000..69675a8 --- /dev/null +++ b/.github/scripts/trawl.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env -S pkgx deno run --allow-run --allow-net --allow-env=GH_TOKEN --allow-write=. + +import * as flags from "https://deno.land/std@0.206.0/flags/mod.ts"; + +const args = flags.parse(Deno.args); +const outdir = args['out'] + +const ghToken = Deno.env.get("GH_TOKEN"); +if (!ghToken) { + console.error("error: GitHub token is required. Set the GH_TOKEN environment variable."); + Deno.exit(1) +} + +Deno.mkdirSync(outdir, { recursive: true }); + +async function cloneAllForks(user: string, repo: string) { + let page = 1; + while (true) { + const response = await fetch(`https://api.github.com/repos/${user}/${repo}/forks?page=${page}`, { + headers: { + "Authorization": `token ${ghToken}` + } + }); + + if (!response.ok) { + throw new Error(`err: ${response.statusText}`); + } + + const forks = await response.json(); + if (forks.length === 0) { + break; // No more forks + } + + for (const fork of forks) { + await clone(fork) + + Deno.writeTextFileSync(`${outdir}/${fork.full_name}/metadata.json`, JSON.stringify({ + stars: fork.stargazers_count, + license: fork.license?.spdx_id, + avatar: fork.owner.avatar_url, + url: fork.html_url + '/blob/' + fork.default_branch + }, null, 2)) + } + + page++; + } +} + +async function clone({clone_url, full_name, ...fork}: any) { + console.log(`Cloning ${clone_url}...`); + const proc = new Deno.Command("git", { args: ["-C", outdir, "clone", clone_url, full_name]}).spawn() + if (!(await proc.status).success) { + throw new Error(`err: ${await proc.status}`) + } +} + +await cloneAllForks('pkgxdev', 'mash'); + +// we have some general utility scripts here +await clone({clone_url: 'https://github.com/pkgxdev/mash.git', full_name: 'pkgxdev/mash'}); +// deploy expects this and fails otherwise +Deno.writeTextFileSync(`${outdir}/pkgxdev/mash/metadata.json`, `{ + "stars": 0, + "license": "Apache-2.0", + "avatar": "https://avatars.githubusercontent.com/u/140643783?v=4", + "url": "https://github.com/pkgxdev/mash/blob/main" +}`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..18d23ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,12 @@ +on: + pull_request: + paths: mash + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pkgxdev/setup@v2 + - run: ./mash pkgxdev/demo + - run: ./mash pkgxdev/demo # check cache route works too diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f5cc6d5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,53 @@ +on: + push: + branches: main + paths: + - .github/workflows/deploy.yml + - .github/scripts/* + pull_request: + paths: + - .github/workflows/deploy.yml + schedule: + - cron: '23 * * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + if: github.repository == 'pkgxdev/mash' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pkgxdev/setup@v2 + + - run: .github/scripts/trawl.ts --out ./build + env: + GH_TOKEN: ${{ github.token }} + + - run: | + mkdir out + .github/scripts/index.ts --input ./build > ./out/index.json + + - run: .github/scripts/build.ts --input ./build --output ./out --index-json ./out/index.json + + - uses: actions/configure-pages@v4 + - uses: actions/upload-pages-artifact@v3 + with: + path: out + + deploy: + needs: build + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + steps: + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55496b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +/out +/build diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e40716f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2ed627d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022–23 pkgx inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3eff8a --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# `mash` + +mash up millions of open source packages into monstrously powerful scripts. + +> [!CAUTION] +> +> We have not vetted any of the scripts `mash` can run and (currently) they +> can do anything they want to your computer. +> +> We fully intend to add sandboxing and user reporting, but you have found +> `mash` super early in its life so you must practice caution in your usage. +> +> All scripts can be read in advance via [mash.pkgx.sh] + +  + + +## Getting Started + +```sh +$ mash # or https://mash.pkgx.sh +# lists all script categories +``` + +You can browse script listings with the TUI or at [mash.pkgx.sh]: + +```sh +$ mash ai # or https://mash.pkgx.sh/ai/ +# lists all ai scripts +``` + +> [!NOTE] +> The above lists all user submitted scripts in the `ai` category. + +Once you’ve found a script you want to run: + +```sh +$ mash ai chat --help # or https://mash.pkgx.sh/ai/chat/ +``` + +## Installing `mash` + +`mash` uses `pkgx` for packaging primitives so you may as well use `pkgx` to +run `mash`: + +```sh +$ pkgx mash + +# or install it via pkgx: +$ pkgx install mash +$ mash +``` + +> [!TIP] +> `curl https://pkgx.sh | sh` or see https://pkgx.sh + +`mash` is a plain POSIX script. All it needs is `bash`, `curl`, and `pkgx`. +So if you like you can just download it by itself. + +> [!NOTE] +> Keeping mash so minimal isn’t a design choice. We will entertain rewriting +> it in something better for dev as it gets more complicated. + +  + + +## Contributing Scripts + +### Making your Scripts available to `mash` + +1. Fork [pkgxdev/mash] +2. Add scripts to `./scripts/` +3. Push to your fork +4. Wait an hour and then check [mash.pkgx.sh] + +> [!TIP] +> * Use any shell or scripting language you like +> * Scripts do not need to use a [`pkgx` shebang] *but we recommend it* +> * Scripts do not have to be made executable *but we recommend it* + +> [!NOTE] +> Do not create a pull request for your scripts against this repo! +> *We index the fork graph*. + +### Running Your Scripts + +`mash` operates with a “categorization by default is good” philosophy. Your +scripts must be categorized or namespaced with your user. + +Thus if you add a script named `foo` it can only be used via +`mash username/foo`. But if you add a script called `foo-bar` if will be +listed if a user types `mash foo`: + +```sh +$ mash foo + +mash foo bar # your description about `foo bar` is shown here +mash foo other-script # … +``` + +To use the script the user would type `mash foo bar` or alternatively +`mash youruser/foo-bar`. + +> [!NOTE] +> Categorized scripts occur on a first come first served basis. If you create +> a script called `foo-bar` and someone already did that then you are too late +> and users can only call your script with `mash youruser/foo-bar`. + +> [!IMPORTANT] +> `mash` will not be able to run your script until it is indexed. +> If you can see it listed at [mash.pkgx.sh] then you’re indexed. +> We index a few times an hour via the GitHub Actions committed to this repo. + +> [!NOTE] +> Updates are fetched automatically, there is no versioning at this time. + +> [!NOTE] +> Single letter categorizations are ignored, eg `./scripts/f-u` will not be +> indexed or made available to mash. If you have a particularly good single +> letter category that you want an exception made, open a discussion and let’s +> chat! + +  + + +## Anatomy of Scripts + +Thanks to [`pkgx`], `mash` scripts can be written in any scripting language +using any packages in the entire open source ecosystem. + +### The Shebang + +The shebang is where you instruct `pkgx` on what scripting language you want. +For example, if you want to write your script in `fish`: + +```sh +#!/usr/bin/env -S pkgx fish +``` + +You can also use pkgx `+pkg` syntax to add additional packages to the script’s +running environment: + +```sh +#!/usr/bin/env -S pkgx +gh +git +gum +bpb bash +``` + +pkgx knows what packages to cache (it doesn’t pollute the user system with +installs) based on the commands you want to run. There’s no figuring out +pkg names, just type what you would type to run the command. + +> https://docs.pkgx.sh/scripts + +### Documenting Your Script + +Rewrite the README in your fork so there is a `## mash category scriptname` +section. If your script is not globally categorized then you would do +`## mash username/scriptname` instead. + +* The paragraph after the `##` will be the [mash.pkgx.sh] description + * Keep it short or it’ll get truncated when we display it +* If you add a `### Usage` section we’ll list it on the web + +> [!IMPORTANT] +> If you don’t provide a description your script won’t be listed (but the +> scripts can still be run by `mash`). + +### Debugging Scripts + +You can easily test your scripts before publishing: + +```sh +$ chmod +x scripts/my-script +$ scripts/my-script +``` + +### Example Fork + +https://github.com/mxcl/mash + +  + + +## Appendix + +`mash` has no secret sauce; users can just cURL your scripts and run them +directly via `pkgx`: + +```sh +$ curl -O https://raw.githubusercontent.com/mxcl/mash/main/scripts/gh-stargazer +$ pkgx ./gh-stargazer +``` + +Even `pkgx` isn’t required, they can source the dependencies themselves and +run the script manually: + +```sh +$ bash ./stargazer +# ^^ they will need to read the script to determine deps and interpreter +``` + +Hackers can use your script without installing `pkgx` first via our cURL +one-liner. This executes the script but doesn’t install pkgx or any other +pkgs: + +```sh +sh <(curl https://pkgx.sh) mash your-script-name +``` + + +[mash.pkgx.sh]: https://mash.pkgx.sh +[pkgxdev/mash]: https://github.com/pkgxdev/mash +[`pkgx` shebang]: https://docs.pkgx.sh/scripts +[`pkgx`]: https://pkgx.sh diff --git a/mash b/mash new file mode 100755 index 0000000..bedddbc --- /dev/null +++ b/mash @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [ -n "$RUNNER_DEBUG" -a -n "$GITHUB_ACTIONS" ] || [ -n "$VERBOSE" ]; then + set -x +fi + +if ! command -v pkgx >/dev/null; then + echo "error: pkgx not found" 1>&2 + exit 1 +fi +if ! command -v curl >/dev/null; then + curl() { + pkgx curl "$@" + } +fi + +run() { + SCRIPTNAME=$1 + shift + + if [ "$(uname)" = Darwin ]; then + CACHE="${XDG_CACHE_HOME:-$HOME/Library/Caches}/mash/$SCRIPTNAME" + else + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/mash/$SCRIPTNAME" + fi + + get_etag() { + grep -i ETag "$CACHE/headers.txt" | sed -e 's/ETag: "\(.*\)"/\1/I' | tr -d '\r' + } + + if [ -f "$CACHE/headers.txt" ] && ETAG=$(get_etag); then + ETAG=(--header If-None-Match:\ $ETAG) + else + mkdir -p "$CACHE" + fi + + URL="https://pkgxdev.github.io/mash/$SCRIPTNAME" + + if curl \ + "${ETAG[@]}" \ + --silent \ + --fail \ + --show-error \ + --dump-header "$CACHE/headers.txt" \ + --output "$CACHE/script" \ + "$URL" + then + chmod +x "$CACHE/script" + exec "$CACHE/script" "$@" + elif [ -f "$CACHE/script" ]; then + echo "warn: couldn’t update check" 1>&2 + exec "$CACHE/script" "$@" + else + echo "error: $URL" 1>&2 + exit 2 + fi + +} + +if [ -z "$1" ]; then + curl -Ssf https://pkgxdev.github.io/mash/ | pkgx jq '.categories[]' --raw-output | sort | uniq + exit 0 +elif [[ "$1" == *"/"* ]]; then + # fully quaified name not a category + cmd=$1 + shift + run u/$cmd "$@" +elif [[ -z "$2" ]]; then + curl -Ssf https://pkgxdev.github.io/mash/$1/ | + pkgx jq -r '.scripts[] | [.cmd, .description] | @tsv' | + awk 'BEGIN {FS="\t"; print "| Script | Description |"; print "|-|-|"} {printf("| %s | %s |\n", $1, $2)}' | + pkgx gum format +else + cmd=$1/$2 + shift; shift + run $cmd "$@" +fi diff --git a/scripts/demo b/scripts/demo new file mode 100755 index 0000000..7ea2c0f --- /dev/null +++ b/scripts/demo @@ -0,0 +1,14 @@ +#!/bin/bash +printf " " +for b in 0 1 2 3 4 5 6 7; do printf " 4${b}m "; done +echo +for f in "" 30 31 32 33 34 35 36 37; do + for s in "" "1;"; do + printf "%4sm" "${s}${f}" + printf " \033[%sm%s\033[0m" "$s$f" "gYw " + for b in 0 1 2 3 4 5 6 7; do + printf " \033[4%s;%sm%s\033[0m" "$b" "$s$f" " gYw " + done + echo + done +done