├── fixtures ├── bun.lockb ├── yarn.lock ├── .nvmrc ├── .ruby-version ├── .node-version ├── .terraform-version ├── action.yml │ ├── empty │ │ └── action.yml │ ├── std │ │ └── action.yml │ └── not-node │ │ └── action.yml ├── python-version │ ├── broken │ │ └── .python-version │ ├── std │ │ └── .python-version │ └── commented │ │ └── .python-version ├── deno.json │ ├── arr │ │ └── deno.json │ └── std │ │ └── deno.json ├── package.json │ ├── arr │ │ └── package.json │ ├── str │ │ └── package.json │ ├── packageManager │ │ └── package.json │ ├── volta │ │ └── package.json │ ├── engines │ │ └── package.json │ └── std │ │ └── package.json ├── pkgx.yml ├── .yarnrc.yml ├── .yarnrc ├── skaffold.yaml │ ├── empty │ │ └── skaffold.yaml │ ├── manifests │ │ └── skaffold.yaml │ ├── std │ │ └── skaffold.yaml │ └── multidoc │ │ └── skaffold.yaml ├── pixi.toml ├── Gemfile ├── deno.jsonc ├── pyproject.toml │ ├── std │ │ └── pyproject.toml │ ├── poetry-yaml-fm │ │ └── pyproject.toml │ └── poetry │ │ └── pyproject.toml ├── requirements.txt ├── Cargo.toml ├── go.mod └── cdk.json ├── src ├── app-version.ts ├── shell-escape.ts ├── dump.ts ├── shellcode().ts ├── integrate.ts ├── sniff.test.ts └── sniff.ts ├── .vscode └── settings.json ├── .github └── workflows │ ├── cd.vx.yml │ └── ci.yml ├── deno.json ├── tea.yaml ├── action.yml ├── action.js ├── app.ts ├── README.md ├── LICENSE.txt └── deno.lock /fixtures/bun.lockb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/yarn.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/.nvmrc: -------------------------------------------------------------------------------- 1 | ^20 2 | -------------------------------------------------------------------------------- /fixtures/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.1 2 | -------------------------------------------------------------------------------- /fixtures/.node-version: -------------------------------------------------------------------------------- 1 | v16.16.0 2 | -------------------------------------------------------------------------------- /fixtures/.terraform-version: -------------------------------------------------------------------------------- 1 | 1.6.1 2 | -------------------------------------------------------------------------------- /fixtures/action.yml/empty/action.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/python-version/broken/.python-version: -------------------------------------------------------------------------------- 1 | broken -------------------------------------------------------------------------------- /fixtures/python-version/std/.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /src/app-version.ts: -------------------------------------------------------------------------------- 1 | export default "0.0.0+dev"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/python-version/commented/.python-version: -------------------------------------------------------------------------------- 1 | # hi 2 | 3 | 3.11 4 | -------------------------------------------------------------------------------- /fixtures/deno.json/arr/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": ["zlib.net^1.2"] 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/package.json/arr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": ["zlib.net^1.2", "deno@1.33"] 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/package.json/str/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": "zlib.net^1.2 python.org@latest" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/pkgx.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | zlib.net: ^1.2 3 | python.org: latest 4 | env: 5 | FOO: BAR 6 | -------------------------------------------------------------------------------- /fixtures/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | #--- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | #--- 7 | -------------------------------------------------------------------------------- /fixtures/.yarnrc: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | # BAR: 1 7 | # --- 8 | -------------------------------------------------------------------------------- /fixtures/skaffold.yaml/empty/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v4beta7 2 | kind: Config 3 | metadata: 4 | name: skaffold-fixture 5 | -------------------------------------------------------------------------------- /fixtures/pixi.toml: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # python.org: '@latest' 5 | # env: 6 | # FOO: BAR 7 | # --- 8 | -------------------------------------------------------------------------------- /fixtures/Gemfile: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | # --- 7 | gem 'rails', '5.0.0' 8 | gem 'sqlite3' 9 | -------------------------------------------------------------------------------- /fixtures/package.json/packageManager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "pnpm@7.33.7+sha256.d1581d46ed10f54ff0cbdd94a2373b1f070202b0fbff29f27c2ce01460427043" 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/action.yml/std/action.yml: -------------------------------------------------------------------------------- 1 | runs: 2 | using: "node16" 3 | main: "main.js" 4 | 5 | pkgx: 6 | dependencies: 7 | zlib.net^1.2 8 | env: 9 | FOO: BAR 10 | -------------------------------------------------------------------------------- /fixtures/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": { 3 | "dependencies": { 4 | "zlib.net": "^1.2" 5 | }, 6 | "env": { 7 | "FOO": "BAR" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/package.json/volta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "volta": { 3 | "node": "16.16.1", 4 | "npm": "9.7.1", 5 | "yarn": "1.22.10", 6 | "pnpm": "7.33.7" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/package.json/engines/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "~16.16.1", 4 | "npm": "~9.7.1", 5 | "yarn": "~1.22.10", 6 | "pnpm": "~7.33.7" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/pyproject.toml/std/pyproject.toml: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | # --- 7 | 8 | [project] 9 | name = "pkgx example" 10 | -------------------------------------------------------------------------------- /fixtures/requirements.txt: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | # --- 7 | tensorflow==2.3.1 8 | uvicorn==0.12.2 9 | fastapi==0.63.0 10 | -------------------------------------------------------------------------------- /fixtures/deno.json/std/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": { 3 | "dependencies": { 4 | "zlib.net": "^1.2" 5 | }, 6 | "env": { 7 | "FOO": "BAR" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/package.json/std/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgx": { 3 | "dependencies": { 4 | "zlib.net": "^1.2" 5 | }, 6 | "env": { 7 | "FOO": "BAR" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/skaffold.yaml/manifests/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v4beta7 2 | kind: Config 3 | metadata: 4 | name: skaffold-fixture 5 | manifests: 6 | kpt: [] 7 | kustomize: {} 8 | helm: 9 | releases: [] 10 | -------------------------------------------------------------------------------- /fixtures/Cargo.toml: -------------------------------------------------------------------------------- 1 | # --- 2 | # pkgx: 3 | # dependencies: 4 | # - zlib.net^1.2 5 | # env: 6 | # FOO: BAR 7 | # --- 8 | 9 | [package] 10 | name = "pkgx example" 11 | version = "0.0.1" 12 | authors = ["mxcl"] 13 | autobins = false 14 | -------------------------------------------------------------------------------- /fixtures/pyproject.toml/poetry-yaml-fm/pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject. toml 2 | # --- 3 | # pkgx: 4 | # python@3.10 5 | # --- 6 | 7 | [tool-poetry] 8 | name = "magicaitrainer" 9 | version = "0.1.0" 10 | description = 11 | authors = ["jlphilli"] 12 | readme = "README. md" 13 | -------------------------------------------------------------------------------- /fixtures/pyproject.toml/poetry/pyproject.toml: -------------------------------------------------------------------------------- 1 | # --- 2 | # dependencies: 3 | # zlib.net: ^1.2 4 | # env: 5 | # FOO: BAR 6 | # --- 7 | 8 | [build-system] 9 | requires = ["poetry-core"] 10 | build-backend = "poetry.core.masonry.api" 11 | 12 | [tool-poetry.dependencies] 13 | python = "3.11.4" 14 | -------------------------------------------------------------------------------- /src/shell-escape.ts: -------------------------------------------------------------------------------- 1 | export default function (x: string) { 2 | /// `$` because we add some env vars recursively 3 | if (!/\s/.test(x) && !/['"$><]/.test(x)) return x; 4 | if (!x.includes('"')) return `"${x}"`; 5 | if (!x.includes("'")) return `'${x}'`; 6 | x = x.replaceAll('"', '\\"'); 7 | return `"${x}"`; 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/skaffold.yaml/std/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v4beta7 2 | kind: Config 3 | metadata: 4 | name: skaffold-fixture 5 | build: 6 | local: 7 | useDockerCLI: true 8 | deploy: 9 | kubeContext: minikube 10 | kubectl: {} 11 | helm: {} 12 | kpt: {} 13 | docker: 14 | images: [] 15 | manifests: 16 | kpt: [] 17 | kustomize: {} 18 | helm: 19 | releases: [] 20 | 21 | profiles: 22 | - name: skaffold-fixture 23 | -------------------------------------------------------------------------------- /fixtures/go.mod: -------------------------------------------------------------------------------- 1 | // --- 2 | // dependencies: 3 | // zlib.net: ^1.2 4 | // testing.org: 1.2.3 5 | // env: 6 | // FOO: BAR 7 | // --- 8 | 9 | module github.com/pkgxdev/go-mod-example 10 | 11 | require ( 12 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 13 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 14 | github.com/gorilla/mux v1.6.2 15 | github.com/sirupsen/logrus v1.2.0 16 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /.github/workflows/cd.vx.yml: -------------------------------------------------------------------------------- 1 | name: cd·vx 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | concurrency: 9 | group: cd/vx/${{ github.event.release.tag_name }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | retag: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: fischerscode/tagger@v0 21 | with: 22 | prefix: v 23 | - run: | 24 | git tag -f latest 25 | git push origin latest --force 26 | -------------------------------------------------------------------------------- /fixtures/skaffold.yaml/multidoc/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v4beta7 2 | kind: Config 3 | metadata: 4 | name: skaffold-fixture 5 | build: 6 | local: 7 | useDockerCLI: true 8 | deploy: 9 | kubeContext: minikube 10 | docker: 11 | images: [] 12 | manifests: 13 | kpt: [] 14 | kustomize: {} 15 | helm: 16 | releases: [] 17 | 18 | profiles: 19 | - name: skaffold-fixture 20 | --- 21 | apiVersion: v4beta7 22 | kind: Config 23 | metadata: 24 | name: skaffold-fixture 25 | deploy: 26 | kubectl: {} 27 | helm: {} 28 | manifests: 29 | kustomize: {} 30 | 31 | profiles: 32 | - name: skaffold-fixture 33 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true 4 | }, 5 | "pkgx": "deno^2.1", 6 | "lint": { 7 | "include": ["src/", "./app.ts"], 8 | "exclude": ["**/*.test.ts", "./action.js"] 9 | }, 10 | "test": { 11 | "include": ["src/"] 12 | }, 13 | "imports": { 14 | "libpkgx": "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/mod.ts", 15 | "libpkgx/": "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/", 16 | "is-what": "https://deno.land/x/is_what@v4.1.15/src/index.ts", 17 | "outdent": "https://deno.land/x/outdent@v0.8.0/mod.ts" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/action.yml/not-node/action.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Script 2 | author: Jon Lauridsen 3 | description: Run simple Elixir scripts 4 | branding: 5 | color: orange 6 | icon: code 7 | inputs: 8 | script: 9 | description: The script to run 10 | required: true 11 | debug: 12 | description: Whether to tell the GitHub client to log details of its requests. true or false. Default is to run in debug mode when the GitHub Actions step debug logging is turned on. 13 | default: ${{ runner.debug == '1' }} 14 | outputs: 15 | result: 16 | description: The stringified return value of the script 17 | runs: 18 | using: docker 19 | image: Dockerfile 20 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | # 3 | # DO NOT REMOVE OR EDIT THIS WARNING: 4 | # 5 | # This file is auto-generated by the TEA app. It is intended to validate ownership of your repository. 6 | # DO NOT commit this file or accept any PR if you don't know what this is. 7 | # We are aware that spammers will try to use this file to try to profit off others' work. 8 | # We take this very seriously and will take action against any malicious actors. 9 | # 10 | # If you are not the owner of this repository, and someone maliciously opens a commit with this file 11 | # please report it to us at support@tea.xyz. 12 | # 13 | # A constitution without this header is invalid. 14 | --- 15 | version: 2.0.0 16 | codeOwners: 17 | - "0x060F08FE34282a3783D2544A1f0804d9FEaAb283" 18 | quorum: 1 19 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: pkgx/dev 2 | description: 3 | runs `pkgx`’s `dev` tool making the resulting packages available to this job 4 | 5 | inputs: 6 | path: 7 | description: path that should be evaluated by `dev` 8 | required: false 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: pkgxdev/setup@v4 14 | 15 | - run: | 16 | TMP="$(mktemp)" 17 | "$GITHUB_ACTION_PATH"/app.ts "$PWD" > "$TMP" 18 | echo "file=$TMP" >> $GITHUB_OUTPUT 19 | id: env 20 | working-directory: ${{ inputs.path }} 21 | shell: bash 22 | 23 | - run: | 24 | if ! command -v node >/dev/null 2>&1; then 25 | node() { 26 | pkgx node^20 "$@" 27 | } 28 | fi 29 | node "$GITHUB_ACTION_PATH"/action.js ${{ steps.env.outputs.file }} 30 | shell: bash 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | jobs: 8 | test-action: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | container: 13 | - null 14 | - ubuntu:latest 15 | container: ${{ matrix.container }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - run: "! deno" 19 | - uses: ./ 20 | - run: deno --version 21 | 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: denolib/setup-deno@v2 27 | with: 28 | deno-version: v2.x 29 | - run: deno fmt --check . 30 | - run: deno lint . 31 | - run: deno check ./app.ts 32 | 33 | test: 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest, macos-latest] 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: denolib/setup-deno@v2 41 | with: 42 | deno-version: v2.x 43 | - run: deno test --allow-read --allow-write --allow-env 44 | -------------------------------------------------------------------------------- /action.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const readline = require("readline"); 3 | 4 | const readInterface = readline.createInterface({ 5 | input: fs.createReadStream(process.argv[2]), 6 | output: process.stdout, 7 | terminal: false, 8 | }); 9 | 10 | const stripQuotes = (str) => 11 | str.startsWith('"') || str.startsWith("'") ? str.slice(1, -1) : str; 12 | 13 | const replaceEnvVars = (str) => { 14 | const value = str 15 | .replaceAll( 16 | /\$\{([a-zA-Z0-9_]+):\+:\$[a-zA-Z0-9_]+\}/g, 17 | (_, key) => ((v) => v ? `:${v}` : "")(process.env[key]), 18 | ) 19 | .replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (_, key) => process.env[key] ?? "") 20 | .replaceAll(/\$([a-zA-Z0-9_]+)/g, (_, key) => process.env[key] ?? ""); 21 | return value; 22 | }; 23 | 24 | let found = false; 25 | 26 | readInterface.on("line", (line) => { 27 | if (!found) found = line.trim() == "set -a"; 28 | if (!found) return; 29 | const match = line.match(/^([^=]+)=(.*)$/); 30 | if (match) { 31 | const [_, key, value_] = match; 32 | const value = stripQuotes(value_); 33 | if (key.trim() === "PATH") { 34 | value 35 | .replaceAll("${PATH:+:$PATH}", "") 36 | .replaceAll("$PATH", "") 37 | .replaceAll("${PATH}", "") 38 | .split(":").forEach((path) => { 39 | fs.appendFileSync(process.env["GITHUB_PATH"], `${path}\n`); 40 | }); 41 | } else { 42 | let v = replaceEnvVars(value); 43 | fs.appendFileSync(process.env["GITHUB_ENV"], `${key}=${v}\n`); 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /src/dump.ts: -------------------------------------------------------------------------------- 1 | import { Path, utils } from "libpkgx"; 2 | import sniff from "./sniff.ts"; 3 | import shell_escape from "./shell-escape.ts"; 4 | 5 | export default async function ( 6 | cwd: Path, 7 | opts: { dryrun: boolean; quiet: boolean }, 8 | ) { 9 | const snuff = await sniff(cwd); 10 | 11 | if (snuff.pkgs.length === 0 && Object.keys(snuff.env).length === 0) { 12 | console.error("no devenv detected"); 13 | Deno.exit(1); 14 | } 15 | 16 | let env = ""; 17 | const pkgspecs = snuff.pkgs.map((pkg) => `+${utils.pkg.str(pkg)}`); 18 | 19 | if (opts.dryrun) { 20 | console.log(pkgspecs.join(" ")); 21 | return; 22 | } 23 | 24 | if (snuff.pkgs.length > 0) { 25 | const cmd = new Deno.Command("pkgx", { 26 | args: ["--quiet", ...pkgspecs], 27 | stdout: "piped", 28 | env: { CLICOLOR_FORCE: "1" }, // unfortunate 29 | }).spawn(); 30 | 31 | await cmd.status; 32 | 33 | const stdout = (await cmd.output()).stdout; 34 | env = new TextDecoder().decode(stdout); 35 | } 36 | 37 | // add any additional env that we sniffed 38 | for (const [key, value] of Object.entries(snuff.env)) { 39 | env += `${key}=${shell_escape(value)}\n`; 40 | } 41 | 42 | env = env.trim(); 43 | 44 | let undo = ""; 45 | for (const envln of env.trim().split("\n")) { 46 | if (!envln) continue; 47 | 48 | const [key] = envln.split("=", 2); 49 | undo += ` if [ \\"$${key}\\" ]; then 50 | export ${key}=\\"$${key}\\" 51 | else 52 | unset ${key} 53 | fi\n`; 54 | } 55 | 56 | const bye_bye_msg = pkgspecs.map((pkgspec) => `-${pkgspec.slice(1)}`).join( 57 | " ", 58 | ); 59 | 60 | if (!opts.quiet) { 61 | console.error("%c%s", "color: green", pkgspecs.join(" ")); 62 | } 63 | 64 | console.log(` 65 | eval "_pkgx_dev_try_bye() { 66 | suffix=\\"\\\${PWD#\\"${cwd}\\"}\\" 67 | [ \\"\\$PWD\\" = \\"${cwd}\\$suffix\\" ] && return 1 68 | echo -e \\"\\033[31m${bye_bye_msg}\\033[0m\\" >&2 69 | ${undo.trim()} 70 | unset -f _pkgx_dev_try_bye 71 | }" 72 | 73 | set -a 74 | ${env} 75 | set +a`); 76 | } 77 | -------------------------------------------------------------------------------- /src/shellcode().ts: -------------------------------------------------------------------------------- 1 | import { Path } from "libpkgx"; 2 | 3 | export default function shellcode() { 4 | // find self 5 | const dev_cmd = Deno.env.get("PATH")?.split(":").map((path) => 6 | Path.abs(path)?.join("dev") 7 | ) 8 | .filter((x) => x?.isExecutableFile())[0]; 9 | 10 | if (!dev_cmd) throw new Error("couldn’t find `dev`"); 11 | 12 | return ` 13 | _pkgx_chpwd_hook() { 14 | if ! type _pkgx_dev_try_bye >/dev/null 2>&1 || _pkgx_dev_try_bye; then 15 | dir="$PWD" 16 | while [ "$dir" != / -a "$dir" != . ]; do 17 | if [ -f "${datadir()}/$dir/dev.pkgx.activated" ]; then 18 | eval "$(${dev_cmd})" "$dir" 19 | break 20 | fi 21 | dir="$(dirname "$dir")" 22 | done 23 | fi 24 | } 25 | 26 | dev() { 27 | case "$1" in 28 | off) 29 | if type -f _pkgx_dev_try_bye >/dev/null 2>&1; then 30 | dir="$PWD" 31 | while [ "$dir" != / -a "$dir" != . ]; do 32 | if [ -f "${datadir()}/$dir/dev.pkgx.activated" ]; then 33 | rm "${datadir()}/$dir/dev.pkgx.activated" 34 | break 35 | fi 36 | dir="$(dirname "$dir")" 37 | done 38 | PWD=/ _pkgx_dev_try_bye 39 | else 40 | echo "no devenv" >&2 41 | fi;; 42 | ''|on) 43 | if [ "$2" ]; then 44 | "${dev_cmd}" "$@" 45 | elif ! type -f _pkgx_dev_try_bye >/dev/null 2>&1; then 46 | mkdir -p "${datadir()}$PWD" 47 | touch "${datadir()}$PWD/dev.pkgx.activated" 48 | eval "$(${dev_cmd})" 49 | else 50 | echo "devenv already active" >&2 51 | fi;; 52 | *) 53 | "${dev_cmd}" "$@";; 54 | esac 55 | } 56 | 57 | if [ -n "$ZSH_VERSION" ] && [ $(emulate) = zsh ]; then 58 | eval 'typeset -ag chpwd_functions 59 | 60 | if [[ -z "\${chpwd_functions[(r)_pkgx_chpwd_hook]+1}" ]]; then 61 | chpwd_functions=( _pkgx_chpwd_hook \${chpwd_functions[@]} ) 62 | fi 63 | 64 | if [ "$TERM_PROGRAM" != Apple_Terminal ]; then 65 | _pkgx_chpwd_hook 66 | fi' 67 | elif [ -n "$BASH_VERSION" ] && [ "$POSIXLY_CORRECT" != y ] ; then 68 | eval 'cd() { 69 | builtin cd "$@" || return 70 | _pkgx_chpwd_hook 71 | } 72 | _pkgx_chpwd_hook' 73 | else 74 | POSIXLY_CORRECT=y 75 | echo "pkgx: dev: warning: unsupported shell" >&2 76 | fi 77 | `.trim(); 78 | } 79 | 80 | export function datadir() { 81 | return new Path( 82 | Deno.env.get("XDG_DATA_HOME")?.trim() || platform_data_home_default(), 83 | ).join("pkgx", "dev"); 84 | } 85 | 86 | function platform_data_home_default() { 87 | const home = Path.home(); 88 | switch (Deno.build.os) { 89 | case "darwin": 90 | return home.join("Library/Application Support"); 91 | case "windows": { 92 | const LOCALAPPDATA = Deno.env.get("LOCALAPPDATA"); 93 | if (LOCALAPPDATA) { 94 | return new Path(LOCALAPPDATA); 95 | } else { 96 | return home.join("AppData/Local"); 97 | } 98 | } 99 | default: 100 | return home.join(".local/share"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /fixtures/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/integrate.ts: -------------------------------------------------------------------------------- 1 | import readLines from "libpkgx/utils/read-lines.ts"; 2 | import { readAll, writeAll } from "jsr:@std/io"; 3 | import { Path, utils } from "libpkgx"; 4 | import { existsSync } from "node:fs"; 5 | const { flatmap } = utils; 6 | 7 | export default async function ( 8 | op: "install" | "uninstall", 9 | { dryrun }: { dryrun: boolean }, 10 | ) { 11 | let opd_at_least_once = false; 12 | const encode = ((e) => e.encode.bind(e))(new TextEncoder()); 13 | 14 | const fopts = { read: true, ...dryrun ? {} : { write: true, create: true } }; 15 | 16 | here: for (const [file, line] of shells()) { 17 | const fd = await Deno.open(file.string, fopts); 18 | try { 19 | let pos = 0; 20 | for await (const readline of readLines(fd)) { 21 | if (readline.trim().endsWith("# https://github.com/pkgxdev/dev")) { 22 | if (op == "install") { 23 | console.error("hook already integrated:", file); 24 | continue here; 25 | } else if (op == "uninstall") { 26 | // we have to seek because readLines is buffered and thus the seek pos is probs already at the file end 27 | await fd.seek(pos + readline.length + 1, Deno.SeekMode.Start); 28 | const rest = await readAll(fd); 29 | 30 | if (!dryrun) await fd.truncate(pos); // deno has no way I can find to truncate from the current seek position 31 | await fd.seek(pos, Deno.SeekMode.Start); 32 | if (!dryrun) await writeAll(fd, rest); 33 | 34 | opd_at_least_once = true; 35 | console.error("removed hook:", file); 36 | 37 | continue here; 38 | } 39 | } 40 | 41 | pos += readline.length + 1; // the +1 is because readLines() truncates it 42 | } 43 | 44 | if (op == "install") { 45 | const byte = new Uint8Array(1); 46 | if (pos) { 47 | await fd.seek(0, Deno.SeekMode.End); // potentially the above didn't reach the end 48 | while (true && pos > 0) { 49 | await fd.seek(-1, Deno.SeekMode.Current); 50 | await fd.read(byte); 51 | if (byte[0] != 10) break; 52 | await fd.seek(-1, Deno.SeekMode.Current); 53 | pos -= 1; 54 | } 55 | 56 | if (!dryrun) { 57 | await writeAll( 58 | fd, 59 | encode(`\n\n${line} # https://github.com/pkgxdev/dev\n`), 60 | ); 61 | } 62 | } 63 | opd_at_least_once = true; 64 | console.error(`${file} << \`${line}\``); 65 | } 66 | } finally { 67 | fd.close(); 68 | } 69 | } 70 | if (dryrun && opd_at_least_once) { 71 | console.error( 72 | "%cthis was a dry-run. %cnothing was changed.", 73 | "color: #5f5fff", 74 | "color: initial", 75 | ); 76 | } else {switch (op) { 77 | case "uninstall": 78 | if (!opd_at_least_once) { 79 | console.error("nothing to deintegrate found"); 80 | } 81 | break; 82 | case "install": 83 | if (opd_at_least_once) { 84 | console.log( 85 | "now %crestart your terminal%c for `dev` hooks to take effect", 86 | "color: #5f5fff", 87 | "color: initial", 88 | ); 89 | } 90 | }} 91 | } 92 | 93 | function shells(): [Path, string][] { 94 | const eval_ln = 95 | existsSync("/opt/homebrew/bin/dev") || existsSync("/usr/local/bin/dev") 96 | ? 'eval "$(dev --shellcode)"' 97 | : 'eval "$(pkgx --quiet dev --shellcode)"'; 98 | 99 | const zdotdir = flatmap(Deno.env.get("ZDOTDIR"), Path.abs) ?? Path.home(); 100 | const zshpair: [Path, string] = [zdotdir.join(".zshrc"), eval_ln]; 101 | 102 | const candidates: [Path, string][] = [ 103 | zshpair, 104 | [Path.home().join(".bashrc"), eval_ln], 105 | [Path.home().join(".bash_profile"), eval_ln], 106 | ]; 107 | 108 | const viable_candidates = candidates.filter(([file]) => file.exists()); 109 | 110 | if (viable_candidates.length == 0) { 111 | if (Deno.build.os == "darwin") { 112 | /// macOS has no .zshrc by default and we want mac users to get a just works experience 113 | return [zshpair]; 114 | } else { 115 | console.error("no `.shellrc` files found"); 116 | Deno.exit(1); 117 | } 118 | } 119 | 120 | return viable_candidates; 121 | } 122 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx --quiet deno^2 run -A 2 | 3 | //TODO if you step into dev-dir/subdir and type `dev` does it find the root properly? 4 | //TODO dev off uses PWD which may not be correct if in subdir (obv) 5 | 6 | import { Path, utils } from "libpkgx"; 7 | import shellcode, { datadir } from "./src/shellcode().ts"; 8 | import app_version from "./src/app-version.ts"; 9 | import integrate from "./src/integrate.ts"; 10 | import { parseArgs } from "jsr:@std/cli@^1/parse-args"; 11 | import dump from "./src/dump.ts"; 12 | import sniff from "./src/sniff.ts"; 13 | import { walk } from "jsr:@std/fs@1/walk"; 14 | 15 | const parsedArgs = parseArgs(Deno.args, { 16 | alias: { 17 | n: "dry-run", 18 | "just-print": "dry-run", 19 | recon: "dry-run", 20 | v: "version", 21 | h: "help", 22 | q: "quiet", 23 | }, 24 | collect: ["quiet"], 25 | boolean: ["help", "version", "shellcode", "quiet"], 26 | default: { 27 | "dry-run": false, 28 | }, 29 | }); 30 | 31 | if (parsedArgs.help) { 32 | const { code } = await new Deno.Command("pkgx", { 33 | args: [ 34 | "glow", 35 | "https://raw.githubusercontent.com/pkgxdev/dev/refs/heads/main/README.md", 36 | ], 37 | }).spawn().status; 38 | Deno.exit(code); 39 | } else if (parsedArgs.shellcode) { 40 | console.log(shellcode()); 41 | } else if (parsedArgs.version) { 42 | console.log(`dev ${app_version}`); 43 | } else { 44 | const subcommand = parsedArgs._[0]; 45 | const dryrun = parsedArgs["dry-run"] as boolean; 46 | const quiet = Array.isArray(parsedArgs["quiet"]) 47 | ? !!parsedArgs["quiet"].length 48 | : parsedArgs["quiet"]; 49 | 50 | switch (subcommand) { 51 | case "integrate": 52 | await integrate("install", { dryrun }); 53 | break; 54 | 55 | case "deintegrate": 56 | await integrate("uninstall", { dryrun }); 57 | break; 58 | 59 | case "status": 60 | { 61 | const cwd = Path.cwd(); 62 | if ( 63 | datadir().join(cwd.string.slice(1), "dev.pkgx.activated").isFile() 64 | ) { 65 | //FIXME probably slower than ideal 66 | const { pkgs } = await sniff(cwd); 67 | Deno.exit(pkgs.length == 0 ? 1 : 0); 68 | } else { 69 | Deno.exit(1); 70 | } 71 | } 72 | break; 73 | 74 | case "ls": 75 | for await ( 76 | const entry of walk(datadir().string, { includeDirs: false }) 77 | ) { 78 | if (entry.name === "dev.pkgx.activated") { 79 | const partial_path = new Path(entry.path).parent().relative({ 80 | to: datadir(), 81 | }); 82 | console.log(`/${partial_path}`); 83 | } 84 | } 85 | break; 86 | 87 | case undefined: 88 | if (Deno.stdout.isTerminal()) { 89 | const cwd = Path.cwd(); 90 | const { pkgs } = await sniff(cwd); 91 | if ( 92 | datadir().join(cwd.string.slice(1), "dev.pkgx.activated").isFile() 93 | ) { 94 | console.log( 95 | "%cactive", 96 | "color: green", 97 | pkgs.map(utils.pkg.str).join(" "), 98 | ); 99 | } else if (pkgs.length > 0) { 100 | console.log( 101 | "%cinactive", 102 | "color: red", 103 | pkgs.map(utils.pkg.str).join(" "), 104 | ); 105 | } else { 106 | console.log("%cno keyfiles found", "color: red"); 107 | } 108 | } else { 109 | const cwd = Path.cwd(); 110 | await dump(cwd, { dryrun, quiet }); 111 | } 112 | break; 113 | 114 | case "off": { 115 | let dir = Path.cwd(); 116 | while (dir.string != "/") { 117 | const f = datadir().join(dir.string.slice(1), "dev.pkgx.activated") 118 | .isFile(); 119 | if (f) { 120 | f.rm(); 121 | console.log("%cdeactivated", "color: green", dir.string); 122 | Deno.exit(0); 123 | } 124 | dir = dir.parent(); 125 | } 126 | console.error("%cno devenv found", "color: red"); 127 | Deno.exit(1); 128 | break; 129 | } 130 | 131 | default: { 132 | if (Deno.stdout.isTerminal()) { 133 | const cwd = Path.cwd().join(subcommand as string); 134 | const { pkgs } = await sniff(cwd); 135 | if (pkgs.length > 0) { 136 | datadir().join(cwd.string.slice(1)).mkdir("p").join( 137 | "dev.pkgx.activated", 138 | ).touch(); 139 | console.log( 140 | "%cactived", 141 | "color: green", 142 | pkgs.map(utils.pkg.str).join(" "), 143 | ); 144 | } else { 145 | console.error("%cno keyfiles found", "color: red"); 146 | Deno.exit(1); 147 | } 148 | } else { 149 | const cwd = Path.cwd().join(subcommand as string); 150 | await dump(cwd, { dryrun, quiet }); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `dev` 2 | 3 | `dev` uses `pkgx` and shellcode to automatically, install and activate the 4 | packages you need for different projects as you navigate in your shell. 5 | 6 | Ensure you are using the same versions of tools for your entire stack, during 7 | dev, across your team and in production. 8 | 9 | > [!NOTE] 10 | > Packages are installed to `~/.pkgx` and not available to your wider system 11 | > without using a tool from the `pkgx` tooling ecosystem. 12 | 13 | ## Getting Started 14 | 15 | Since `dev` v1.8.0 we integrate with `pkgm` (^0.11) and this is the recommended 16 | way to use `dev`. 17 | 18 | > [!IMPORTANT] 19 | > 20 | > Both `dev` and the packages you want to be `dev`-aware must be installed to 21 | > `/usr/local/` for this to work. (`dev` (but only `dev`) can be a `pkgm shim`). 22 | > 23 | > ```sh 24 | > sudo pkgm install dev node@22 25 | > ``` 26 | 27 | ```sh 28 | $ cd my-project 29 | $ ls 30 | package.json 31 | 32 | $ node --version 33 | command not found: node 34 | 35 | $ sudo pkgm install dev node 36 | $ node --version && which node 37 | v23.11.0 38 | /usr/local/bin/node 39 | 40 | $ cat package.json | jq .engines 41 | { 42 | "node": "^20" 43 | } 44 | 45 | $ dev . 46 | activated `~/my-project` (+node^20) 47 | 48 | $ node --version && which node 49 | v20.19.0 50 | /usr/local/bin/node 51 | 52 | $ cd .. 53 | $ node --version && which node 54 | v23.11.0 55 | /usr/local/bin/node 56 | 57 | $ cd - 58 | $ node --version && which node 59 | v20.19.0 60 | /usr/local/bin/node 61 | ``` 62 | 63 | `pkgm` installs `dev`-aware packages to `/usr/local/bin`. Provided you have 64 | `/usr/local/bin/dev` installed and you have activated `dev` in your project 65 | directories the `node` that is invoked is swapped out _when invoked_. 66 | 67 | This is the recommended way to use `dev` because it works everywhere and not 68 | just the terminal. 69 | 70 | ## `dev` via Shellcode 71 | 72 | Using `dev` via shellcode requires hooks to be installed in your shell. It is 73 | handy in that no tool needs to be installed. It is problematic in that shell 74 | hooks are more invasive and don’t work in other tools like editors. 75 | 76 | --- 77 | 78 |
79 | Using dev via shellcode… 80 | 81 | A great advantage of the shellcode is not needing to install tools you may never 82 | need again when exploring new open source projects. 83 | 84 | ```sh 85 | pkgx dev integrate 86 | ``` 87 | 88 | `dev` requires `pkgx` but at your preference: 89 | 90 | ```sh 91 | brew install pkgxdev/made/dev 92 | dev integrate 93 | ``` 94 | 95 | We support macOS & Linux, **Bash** & **Zsh**. PRs are very welcome to support 96 | more shells. 97 | 98 | > [!NOTE] 99 | > 100 | > `dev integrate` looks for and edits known `shell.rc` files adding one line: 101 | > 102 | > ```sh 103 | > eval "$(dev --shellcode)" 104 | > ``` 105 | > 106 | > If you don’t trust us (good on you), then do a dry run first: 107 | > 108 | > ```sh 109 | > pkgx dev integrate --dry-run 110 | > ``` 111 | > 112 | > If you like, preview the shellcode: `pkgx dev --shellcode`. This command only 113 | > outputs shellcode, it doesn’t modify any files or do anything else either. 114 | 115 | ### Usage 116 | 117 | ```sh 118 | $ cd my-project 119 | 120 | my-project $ ls 121 | package.json 122 | 123 | my-project $ dev 124 | +nodejs.org 125 | # ^^ installs node to ~/.pkgx/nodejs.org/v22.12.0 if not already installed 126 | 127 | my-project $ node --version 128 | v22.12.0 129 | 130 | $ which node 131 | ~/.pkgx/nodejs.org/v22.12.0/bin/node 132 | 133 | $ cd .. 134 | -nodejs.org 135 | 136 | $ node 137 | command not found: node 138 | ``` 139 | 140 | > [!TIP] 141 | > 142 | > #### Try Before You `vi` 143 | > 144 | > Modifying your `shell.rc` can be… _intimidating_. If you just want to 145 | > temporarily try `dev` out before you `:wq`—we got you: 146 | > 147 | > ```sh 148 | > $ cd my-project 149 | > $ eval "$(pkgx dev)" 150 | > +deno^2 151 | > $ deno --version 152 | > deno 2.1.1 153 | > ``` 154 | > 155 | > The devenv will only exist for the duration of your shell session. 156 | 157 |
158 | 159 | --- 160 | 161 | ## How Packages are Determined 162 | 163 | - We look at the files you have and figure out the packages you need. 164 | - Where possible we also determine the versions you need if such things can be 165 | determined by looking at configuration files. 166 | 167 | ## Specifying Versions 168 | 169 | We allow you to add YAML front matter to all files to specify versions more 170 | precisely: 171 | 172 | ```toml 173 | # --- 174 | # pkgx: 175 | # dependencies: 176 | # openssl.org: 1.1.1n 177 | # --- 178 | 179 | [package] 180 | name = "my cargo project" 181 | # snip… 182 | ``` 183 | 184 | We allow more terse expressions including eg: 185 | 186 | ```toml 187 | # --- 188 | # pkgx: 189 | # dependencies: openssl.org@1.1.1n deno^2 npm 190 | # --- 191 | ``` 192 | 193 | The major exception being json since it doesn’t support comments, in this case 194 | we read a special `pkgx` node: 195 | 196 | ```json 197 | { 198 | "pkgx": { 199 | "dependencies": { 200 | "openssl.org": "1.1.1n", 201 | "deno": "^2", 202 | "npm": null 203 | } 204 | } 205 | } 206 | ``` 207 | 208 | You can also make a `pkgx.yaml` file. 209 | 210 | ## Adding Custom Environment Variables 211 | 212 | You can add your own environment variables if you like: 213 | 214 | ```toml 215 | # --- 216 | # pkgx: 217 | # dependencies: 218 | # openssl.org: 1.1.1n 219 | # env: 220 | # MY_VAR: my-value 221 | # --- 222 | ``` 223 | 224 | > [!NOTE] 225 | > 226 | > - Adding environment variables only works via the `shellcode` route. 227 | > - The environment variable's value is sanitized, so expressions like 228 | > `MY_VAR: $(sudo rm -rf --no-preserve-root /)` will throw an error. 229 | > - We recommend `direnv` instead of this route. 230 | 231 | ## `dev` & Editors 232 | 233 | The sure fire way for things to work in editors is to use the `dev`/`pkgm` 234 | combo. Having said this most editors if opened via the Terminal will inherit 235 | that Terminal’s environment. 236 | 237 | ## GitHub Actions 238 | 239 | ```yaml 240 | - uses: pkgxdev/dev@v1 241 | ``` 242 | 243 | Our action installs needed packages (via `pkgx`) and sets up the environment the 244 | same as `dev` does in your terminal. 245 | 246 | ## Contributing 247 | 248 | We use `deno`, so either install that or—you know—type `dev`. 249 | 250 | Edit [./src/sniff.ts](src/sniff.ts) to add new dev types. 251 | -------------------------------------------------------------------------------- /src/sniff.test.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file require-await 2 | import { 3 | assert, 4 | assertEquals, 5 | assertRejects, 6 | assertThrows, 7 | } from "jsr:@std/assert"; 8 | import specimen, { _internals } from "./sniff.ts"; 9 | import * as mock from "jsr:@std/testing/mock"; 10 | import { Path, utils } from "libpkgx"; 11 | 12 | export const fixturesd = new Path(new URL(import.meta.url).pathname).parent() 13 | .parent().join("fixtures"); 14 | 15 | Deno.test("devenv.ts", async (runner) => { 16 | // const stub = mock.stub(_internals, "find", async pkg => utils.pkg.parse(pkg)) 17 | 18 | try { 19 | await runner.step("supplementable fixtures", async (test) => { 20 | // each of the files in this list must have a zlib.net^1.2 dependency and a FOO=BAR env 21 | const keyfiles = [ 22 | ["pkgx.yml"], 23 | ["deno.json/std/deno.json", "deno.land"], 24 | ["deno.json/arr/deno.json", "deno.land"], 25 | ["deno.jsonc", "deno.land"], 26 | ["package.json/std/package.json", "nodejs.org"], 27 | ["package.json/str/package.json", "nodejs.org"], 28 | ["package.json/arr/package.json", "nodejs.org"], 29 | ["Cargo.toml", "rust-lang.org"], 30 | ["Gemfile", "ruby-lang.org"], 31 | ["pyproject.toml/std/pyproject.toml", "pip.pypa.io"], 32 | ["pyproject.toml/poetry/pyproject.toml", "python-poetry.org"], 33 | ["go.mod", "go.dev"], 34 | ["requirements.txt", "pip.pypa.io"], 35 | [".yarnrc", "classic.yarnpkg.com"], 36 | ["pixi.toml", "prefix.dev"], 37 | ["action.yml/std/action.yml", "nodejs.org^16"], 38 | [".yarnrc.yml", "yarnpkg.com"], 39 | ]; 40 | 41 | for (const [keyfile, dep] of keyfiles) { 42 | await test.step(`${keyfile}`, async () => { 43 | const go = async (file: Path) => { 44 | const { env, pkgs } = await specimen(file.parent()); 45 | assert( 46 | pkgs.find((pkg) => utils.pkg.str(pkg) == "zlib.net^1.2"), 47 | "should dep zlib^1.2", 48 | ); 49 | if (dep) { 50 | assert( 51 | pkgs.find((pkg) => utils.pkg.str(pkg) == dep), 52 | `should dep ${dep}`, 53 | ); 54 | } 55 | 56 | switch (keyfile) { 57 | case "package.json/str/package.json": 58 | case "package.json/arr/package.json": 59 | case "deno.json/arr/deno.json": 60 | break; // testing the short form for deps with these files 61 | default: 62 | assertEquals(env.FOO, "BAR"); 63 | } 64 | }; 65 | 66 | const target = fixturesd.join(keyfile).cp({ into: Path.mktemp() }); 67 | await go(target); 68 | await go(Path.mktemp().join(target.basename()).ln("s", { target })); 69 | }); 70 | } 71 | }); 72 | 73 | await runner.step("fixed fixtures", async (test) => { 74 | const keyfiles = [ 75 | [ 76 | "package.json/engines/package.json", 77 | "nodejs.org~16.16.1", 78 | "npmjs.com~9.7.1", 79 | "yarnpkg.com~1.22.10", 80 | "pnpm.io~7.33.7", 81 | ], 82 | [ 83 | "package.json/packageManager/package.json", 84 | "pnpm.io@7.33.7", 85 | "nodejs.org", 86 | ], 87 | [ 88 | "package.json/volta/package.json", 89 | "nodejs.org@16.16.1", 90 | "npmjs.com@9.7.1", 91 | "yarnpkg.com@1.22.10", 92 | "pnpm.io@7.33.7", 93 | ], 94 | [".node-version", "nodejs.org@16.16.0"], 95 | [".nvmrc", "nodejs.org^20"], 96 | ["python-version/std/.python-version", "python.org~3.10"], 97 | ["python-version/commented/.python-version", "python.org~3.11"], 98 | [".ruby-version", "ruby-lang.org@3.2.1"], 99 | [".terraform-version", "terraform.io@1.6.1"], 100 | ["yarn.lock", "yarnpkg.com"], 101 | ["bun.lockb", "bun.sh>=1"], 102 | [ 103 | "pyproject.toml/poetry-yaml-fm/pyproject.toml", 104 | "pip.pypa.io", 105 | "python~3.10", 106 | ], 107 | ["cdk.json", "aws.amazon.com/cdk"], 108 | ]; 109 | 110 | for (const [keyfile, ...deps] of keyfiles) { 111 | await test.step(keyfile, async () => { 112 | const file = fixturesd.join(keyfile).cp({ into: Path.mktemp() }); 113 | const { env, pkgs } = await specimen(file.parent()); 114 | 115 | assertEquals(pkgs.length, deps.length); 116 | 117 | pkgs.forEach((pkg, i) => { 118 | assertEquals(Object.keys(env).length, 0); 119 | assertEquals(utils.pkg.str(pkg), deps[i]); 120 | }); 121 | }); 122 | } 123 | }); 124 | 125 | await runner.step("broken .python-version", async () => { 126 | const file = fixturesd.join("python-version/broken/.python-version").cp({ 127 | into: Path.mktemp(), 128 | }); 129 | const { pkgs } = await specimen(file.parent()); 130 | assertEquals(pkgs.length, 0); //NOTE this seems like dumb behavior 131 | }); 132 | 133 | await runner.step("vcs", async (test) => { 134 | const vcss = [ 135 | ["hg", "mercurial-scm.org"], 136 | ["svn", "apache.org/subversion"], 137 | ]; 138 | 139 | if (Deno.build.os !== "darwin") { 140 | vcss.push(["git", "git-scm.org"]); 141 | } 142 | 143 | for (const [vcs, dep] of vcss) { 144 | await test.step(vcs, async () => { 145 | const d = Path.mktemp().join(`.${vcs}`).mkdir(); 146 | const { env, pkgs } = await specimen(d.parent()); 147 | assertEquals(Object.keys(env).length, 0); 148 | assertEquals(utils.pkg.str(pkgs[0]), dep); 149 | }); 150 | } 151 | }); 152 | 153 | await runner.step("empty action.yml has no deps", async () => { 154 | const { pkgs } = await specimen(fixturesd.join("action.yml/empty")); 155 | assertEquals(pkgs.length, 0); 156 | 157 | const { pkgs: pkgs2 } = await specimen( 158 | fixturesd.join("action.yml/not-node"), 159 | ); 160 | assertEquals(pkgs2.length, 0); 161 | }); 162 | 163 | await runner.step("no dir error", async () => { 164 | await assertRejects(() => specimen(new Path("/a/b/c/pkgx"))); 165 | }); 166 | 167 | await runner.step("not error if no yaml fm", async () => { 168 | const f = Path.mktemp().join("pyproject.toml").touch(); 169 | await specimen(f.parent()); 170 | 171 | f.rm().write({ text: "#---\n#---" }); 172 | await specimen(f.parent()); 173 | 174 | // we don’t support invalid json just like npm won’t 175 | f.parent().join("package.json").touch(); 176 | await assertRejects(() => specimen(f.parent())); 177 | 178 | f.parent().join("package.json").rm().write({ text: "{}" }); 179 | await specimen(f.parent()); 180 | }); 181 | 182 | await runner.step("skips invalid deps node", async () => { 183 | const f = Path.mktemp().join("package.json").rm().write({ 184 | text: '{"pkgx": {"dependencies": true}}', 185 | }); 186 | const { pkgs } = await specimen(f.parent()); 187 | assertEquals(pkgs.length, 1); 188 | assertEquals(pkgs[0].project, "nodejs.org"); 189 | }); 190 | 191 | await runner.step("skffold.yaml", async () => { 192 | // test invalid skffold.yaml 193 | const f = Path.mktemp().join("skaffold.yaml").touch(); 194 | f.parent().join("skaffold.yaml").rm().write({ text: "" }); 195 | const { env, pkgs } = await specimen(f.parent()); 196 | // only skafflold.dev, no other dep expected 197 | assert( 198 | pkgs.length === 1, 199 | "invalid skaffold.yaml should not return any dep", 200 | ); 201 | 202 | const keyfiles = [ 203 | [ 204 | "skaffold.yaml/std/skaffold.yaml", 205 | "skaffold.dev", 206 | "kubernetes.io/kubectl", 207 | "helm.sh", 208 | "kpt.dev", 209 | "kubernetes.io/minikube", 210 | "docker.com/cli", 211 | "kubernetes.io/kustomize", 212 | ], 213 | [ 214 | "skaffold.yaml/empty/skaffold.yaml", 215 | "skaffold.dev", 216 | ], 217 | [ 218 | "skaffold.yaml/manifests/skaffold.yaml", 219 | "skaffold.dev", 220 | "helm.sh", 221 | "kpt.dev", 222 | "kubernetes.io/kustomize", 223 | ], 224 | [ 225 | "skaffold.yaml/multidoc/skaffold.yaml", 226 | "skaffold.dev", 227 | "kubernetes.io/kubectl", 228 | "helm.sh", 229 | "kpt.dev", 230 | "kubernetes.io/minikube", 231 | "docker.com/cli", 232 | "kubernetes.io/kustomize", 233 | ], 234 | ]; 235 | 236 | for (const [keyfile, ...deps] of keyfiles) { 237 | const file = fixturesd.join(keyfile).cp({ into: Path.mktemp() }); 238 | const { env, pkgs } = await specimen(file.parent()); 239 | assert( 240 | pkgs.length === deps.length, 241 | `dependencies length differ, required: ${deps.length}, actual: ${pkgs.length}`, 242 | ); 243 | deps.every((dep) => { 244 | assert( 245 | pkgs.find((pkg) => utils.pkg.str(pkg) == dep), 246 | "should dep " + dep, 247 | ); 248 | }); 249 | } 250 | }); 251 | } finally { 252 | // stub.restore() 253 | } 254 | 255 | await runner.step("validateDollarSignUsage", () => { 256 | assertThrows(() => _internals.validateDollarSignUsage("foo $(bar) baz")); 257 | assertThrows(() => _internals.validateDollarSignUsage("foo $123 baz")); 258 | 259 | _internals.validateDollarSignUsage("foo $bar baz"); 260 | _internals.validateDollarSignUsage("foo $BAR baz"); 261 | _internals.validateDollarSignUsage("foo $B0AR baz"); 262 | _internals.validateDollarSignUsage("foo z${FOO}s baz"); 263 | }); 264 | }); 265 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022–23 pkgx inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/sniff.ts: -------------------------------------------------------------------------------- 1 | import { hooks, PackageRequirement, Path, semver, utils } from "libpkgx"; 2 | import { validatePackageRequirement } from "libpkgx/hooks/usePantry.ts"; 3 | import { 4 | isArray, 5 | isNumber, 6 | isPlainObject, 7 | isString, 8 | PlainObject, 9 | } from "is-what"; 10 | import readLines from "libpkgx/utils/read-lines.ts"; 11 | import { parse as parseYaml } from "jsr:@std/yaml"; 12 | import * as JSONC from "jsr:@std/jsonc"; 13 | const { useMoustaches } = hooks; 14 | 15 | export default async function (dir: Path) { 16 | if (!dir.isDirectory()) { 17 | throw new Error(`not a directory: ${dir}`); 18 | } 19 | 20 | const constraint = new semver.Range("*"); 21 | let has_package_json = false; 22 | 23 | const pkgs: PackageRequirement[] = []; 24 | const env: Record = {}; 25 | 26 | for await ( 27 | const [path, { name, isFile, isSymlink, isDirectory }] of dir.ls() 28 | ) { 29 | if (isFile || isSymlink) { 30 | switch (name) { 31 | case "deno.json": 32 | case "deno.jsonc": 33 | await deno(path); 34 | break; 35 | case ".nvmrc": 36 | case ".node-version": 37 | await version_file(path, "nodejs.org"); 38 | break; 39 | case ".ruby-version": 40 | await version_file(path, "ruby-lang.org"); 41 | break; 42 | case ".python-version": 43 | await python_version(path); 44 | break; 45 | case ".terraform-version": 46 | await terraform_version(path); 47 | break; 48 | case "package.json": 49 | await package_json(path); 50 | break; 51 | case "action.yml": 52 | case "action.yaml": 53 | await github_actions(path); 54 | break; 55 | case "Cargo.toml": 56 | pkgs.push({ project: "rust-lang.org", constraint }); 57 | await read_YAML_FM(path); //TODO use dedicated TOML section in preference 58 | break; 59 | case "skaffold.yaml": 60 | pkgs.push({ project: "skaffold.dev", constraint }); 61 | await skaffold_yaml(path); 62 | break; 63 | case "go.mod": 64 | case "go.sum": 65 | pkgs.push({ project: "go.dev", constraint }); 66 | await read_YAML_FM(path); 67 | break; 68 | case "requirements.txt": 69 | case "pipfile": 70 | case "pipfile.lock": 71 | case "setup.py": 72 | pkgs.push({ project: "pip.pypa.io", constraint }); 73 | await read_YAML_FM(path); 74 | break; 75 | case "pyproject.toml": 76 | await pyproject(path); 77 | break; 78 | case "Gemfile": 79 | pkgs.push({ project: "ruby-lang.org", constraint }); 80 | await read_YAML_FM(path); 81 | break; 82 | case ".yarnrc": 83 | pkgs.push({ project: "classic.yarnpkg.com", constraint }); 84 | await read_YAML_FM(path); 85 | break; 86 | case "yarn.lock": 87 | pkgs.push({ project: "yarnpkg.com", constraint }); 88 | break; 89 | case ".yarnrc.yml": 90 | pkgs.push({ project: "yarnpkg.com", constraint }); 91 | await read_YAML_FM(path); 92 | break; 93 | case "bun.lock": 94 | case "bun.lockb": 95 | pkgs.push({ project: "bun.sh", constraint: new semver.Range(">=1") }); 96 | break; 97 | case "pnpm-lock.yaml": 98 | pkgs.push({ project: "pnpm.io", constraint }); 99 | break; 100 | case "pixi.toml": 101 | pkgs.push({ project: "prefix.dev", constraint }); 102 | await read_YAML_FM(path); 103 | break; 104 | case "pkgx.yml": 105 | case "pkgx.yaml": 106 | case ".pkgx.yml": 107 | case ".pkgx.yaml": 108 | await parse_well_formatted_node(await path.readYAML()); 109 | break; 110 | case "cdk.json": 111 | pkgs.push({ project: "aws.amazon.com/cdk", constraint }); 112 | break; 113 | case "justfile": 114 | case "Justfile": 115 | pkgs.push({ project: "just.systems", constraint }); 116 | break; 117 | case "Taskfile.yml": 118 | pkgs.push({ project: "taskfile.dev", constraint }); 119 | break; 120 | case "uv.lock": 121 | pkgs.push({ project: "astral.sh/uv", constraint }); 122 | break; 123 | } 124 | } else if (isDirectory) { 125 | switch (name) { 126 | case ".git": 127 | if (utils.host().platform != "darwin") { 128 | pkgs.push({ project: "git-scm.org", constraint }); 129 | } 130 | break; 131 | case ".hg": 132 | pkgs.push({ project: "mercurial-scm.org", constraint }); 133 | break; 134 | case ".svn": 135 | pkgs.push({ project: "apache.org/subversion", constraint }); 136 | break; 137 | } 138 | } 139 | } 140 | 141 | if ( 142 | has_package_json && !pkgs.some((pkg) => pkg.project === "bun.sh") && 143 | !pkgs.some((pkg) => pkg.project === "nodejs.org") 144 | ) { 145 | pkgs.push({ project: "nodejs.org", constraint }); 146 | } 147 | 148 | return { pkgs, env }; 149 | 150 | //---------------------------------------------- parsers 151 | async function deno(path: Path) { 152 | pkgs.push({ project: "deno.land", constraint }); 153 | const json = JSONC.parse(await path.read()); 154 | // deno-lint-ignore no-explicit-any 155 | if (isPlainObject(json) && (json as any).pkgx) { 156 | // deno-lint-ignore no-explicit-any 157 | let node = (json as any).pkgx; 158 | if (isString(node) || isArray(node)) node = { dependencies: node }; 159 | await parse_well_formatted_node(node); 160 | } 161 | } 162 | 163 | async function version_file(path: Path, project: string) { 164 | let s = (await path.read()).trim(); 165 | if (s.startsWith("v")) s = s.slice(1); // v prefix has no effect but is allowed 166 | if (s.match(/^[0-9]/)) s = `@${s}`; // bare versions are `@`ed 167 | s = `${project}${s}`; 168 | pkgs.push(utils.pkg.parse(s)); 169 | } 170 | 171 | async function python_version(path: Path) { 172 | const s = (await path.read()).trim(); 173 | const lines = s.split("\n"); 174 | for (let l of lines) { 175 | l = l.trim(); 176 | if (!l) continue; // skip empty lines 177 | if (l.startsWith("#")) continue; // skip commented lines 178 | // TODO: How to handle 'system'? 179 | // TODO: How to handle non-bare versions like pypy3.9-7.3.11, stackless-3.7.5, etc. in pyenv install --list? 180 | l = `python.org@${l}`; 181 | try { 182 | pkgs.push(utils.pkg.parse(l)); 183 | break; // only one thanks 184 | } catch { 185 | //noop pyenv sticks random shit in here 186 | } 187 | } 188 | } 189 | 190 | async function terraform_version(path: Path) { 191 | const terraform_version = (await path.read()).trim(); 192 | const package_descriptor = `terraform.io@${terraform_version}`; 193 | pkgs.push(utils.pkg.parse(package_descriptor)); 194 | } 195 | 196 | async function package_json(path: Path) { 197 | const json = JSON.parse(await path.read()); 198 | let node = json?.pkgx; 199 | if (isString(node) || isArray(node)) node = { dependencies: node }; 200 | if (!node) { 201 | if (json?.engines) { 202 | node = { 203 | dependencies: { 204 | ...(json.engines.node && { "nodejs.org": json.engines.node }), 205 | ...(json.engines.npm && { "npmjs.com": json.engines.npm }), 206 | ...(json.engines.yarn && { "yarnpkg.com": json.engines.yarn }), 207 | ...(json.engines.pnpm && { "pnpm.io": json.engines.pnpm }), 208 | }, 209 | }; 210 | } 211 | if (json?.packageManager) { // corepack 212 | // example: "pnpm@7.33.7+sha256.d1581d46ed10f54ff0cbdd94a2373b1f070202b0fbff29f27c2ce01460427043" 213 | const match = json.packageManager.match( 214 | /^(?[^@]+)@(?[^+]+)/, 215 | ); 216 | 217 | if (match) { 218 | const { pkg, version } = match.groups as { 219 | pkg: string; 220 | version: string; 221 | }; 222 | 223 | switch (pkg) { 224 | case "npm": 225 | node = { 226 | dependencies: { 227 | "npmjs.com": version, 228 | }, 229 | }; 230 | break; 231 | case "yarn": 232 | node = { 233 | dependencies: { 234 | "yarnpkg.com": version, 235 | }, 236 | }; 237 | break; 238 | case "pnpm": 239 | node = { 240 | dependencies: { 241 | "pnpm.io": version, 242 | }, 243 | }; 244 | break; 245 | } 246 | } 247 | } 248 | if (json?.volta) { 249 | node = { 250 | dependencies: { 251 | ...(json.volta.node && { "nodejs.org": json.volta.node }), 252 | ...(json.volta.npm && { "npmjs.com": json.volta.npm }), 253 | ...(json.volta.yarn && { "yarnpkg.com": json.volta.yarn }), 254 | ...(json.volta.pnpm && { "pnpm.io": json.volta.pnpm }), 255 | }, 256 | }; 257 | } 258 | } 259 | await parse_well_formatted_node(node); 260 | has_package_json = true; 261 | } 262 | 263 | async function skaffold_yaml(path: Path) { 264 | //deno-lint-ignore no-explicit-any 265 | const yamls = await path.readYAMLAll() as unknown as any[]; 266 | const lpkgs: PackageRequirement[] = []; 267 | 268 | for (const yaml of yamls) { 269 | if (!isPlainObject(yaml)) continue; 270 | 271 | if ( 272 | yaml.build?.local?.useDockerCLI?.toString() === "true" || 273 | yaml.deploy?.docker 274 | ) { 275 | lpkgs.push({ 276 | project: "docker.com/cli", 277 | constraint: new semver.Range(`*`), 278 | }); 279 | } 280 | if (yaml.deploy?.kubectl) { 281 | lpkgs.push({ 282 | project: "kubernetes.io/kubectl", 283 | constraint: new semver.Range(`*`), 284 | }); 285 | } 286 | if (yaml.deploy?.kubeContext?.match("minikube")) { 287 | lpkgs.push({ 288 | project: "kubernetes.io/minikube", 289 | constraint: new semver.Range(`*`), 290 | }); 291 | } 292 | if (yaml.deploy?.helm || yaml.manifests?.helm) { 293 | lpkgs.push({ 294 | project: "helm.sh", 295 | constraint: new semver.Range(`*`), 296 | }); 297 | } 298 | if (yaml.deploy?.kpt || yaml.manifests?.kpt) { 299 | lpkgs.push({ 300 | project: "kpt.dev", 301 | constraint: new semver.Range(`*`), 302 | }); 303 | } 304 | if (yaml.manifests?.kustomize) { 305 | lpkgs.push({ 306 | project: "kubernetes.io/kustomize", 307 | constraint: new semver.Range(`*`), 308 | }); 309 | } 310 | } 311 | 312 | const deduped = Array.from( 313 | new Map(lpkgs.map((pkg) => [pkg.project, pkg])).values(), 314 | ); 315 | pkgs.push(...deduped); 316 | } 317 | 318 | async function github_actions(path: Path) { 319 | const yaml = await path.readYAML(); 320 | if (!isPlainObject(yaml)) return; 321 | const rv = yaml.runs?.using?.match(/node(\d+)/); 322 | if (rv?.[1]) { 323 | pkgs.push({ 324 | project: "nodejs.org", 325 | constraint: new semver.Range(`^${rv?.[1]}`), 326 | }); 327 | } 328 | await parse_well_formatted_node(yaml.pkgx); 329 | } 330 | 331 | async function pyproject(path: Path) { 332 | //TODO parse the TOML lol! 333 | 334 | const content = await path.read(); 335 | if (content.includes("poetry.core.masonry.api")) { 336 | pkgs.push({ project: "python-poetry.org", constraint }); 337 | } else { 338 | //TODO other pkging systems…? 339 | pkgs.push({ project: "pip.pypa.io", constraint }); 340 | } 341 | await read_YAML_FM(path); 342 | } 343 | 344 | //---------------------------------------------- YAML FM utils 345 | 346 | async function read_YAML_FM(path: Path) { 347 | //TODO be smart with knowing the comment types 348 | // this parsing logic should be in the pantry ofc 349 | 350 | //TODO should only parse blank lines and comments before bailing 351 | // at the first non-comment line 352 | 353 | //TODO should be savvy to what comment type is acceptable! 354 | 355 | let yaml: string | undefined; 356 | const fd = await Deno.open(path.string, { read: true }); 357 | try { 358 | for await (const line of readLines(fd)) { 359 | if (yaml !== undefined) { 360 | if (/^((#|\/\/)\s*)?---(\s*\*\/)?$/.test(line.trim())) { 361 | let node = parseYaml(yaml); 362 | /// using a `pkgx` node is safer (YAML-FM is a free-for-all) but is not required 363 | if (isPlainObject(node) && node.pkgx) { 364 | node = isString(node.pkgx) || isArray(node.pkgx) 365 | ? { dependencies: node.pkgx } 366 | : node.pkgx; 367 | } 368 | return await parse_well_formatted_node(node); 369 | } 370 | yaml += line?.replace(/^(#|\/\/)/, ""); 371 | yaml += "\n"; 372 | } else if (/^((\/\*|#|\/\/)\s*)?---/.test(line.trim())) { 373 | yaml = ""; 374 | } 375 | } 376 | } finally { 377 | fd.close(); 378 | } 379 | } 380 | 381 | async function parse_well_formatted_node(obj: unknown) { 382 | if (!isPlainObject(obj)) { 383 | return; //TODO diagnostics in verbose mode, error if `pkgx` node 384 | } 385 | 386 | const yaml = await extract_well_formatted_entries(obj); 387 | 388 | for (let [k, v] of Object.entries(yaml.env)) { 389 | if (isNumber(v)) v = v.toString(); 390 | if (isString(v)) { 391 | //TODO provide diagnostics if verbose, throw if part of a `pkgx` node 392 | env[k] = fix(v); 393 | } 394 | } 395 | 396 | pkgs.push(...yaml.deps); 397 | 398 | function fix(input: string): string { 399 | const moustaches = useMoustaches(); 400 | 401 | //TODO deprecate moustaches and instead use env vars 402 | 403 | const foo = [ 404 | //FIXME ...moustaches.tokenize.host(), 405 | { from: "home", to: Path.home().string }, //TODO deprecate and use $HOME once pantry is migrated 406 | { from: "srcroot", to: dir.string }, //TODO deprecate and use $PWD once pantry is migrated 407 | ]; 408 | 409 | const out = moustaches.apply(input, foo); 410 | _internals.validateDollarSignUsage(out); 411 | return out; 412 | } 413 | } 414 | } 415 | 416 | function validateDollarSignUsage(str: string): void { 417 | let currentIndex = 0; 418 | 419 | while ((currentIndex = str.indexOf("$", currentIndex)) !== -1) { 420 | const substring = str.substring(currentIndex); 421 | 422 | // Check for ${FOO} format 423 | const isValidCurlyFormat = /^\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(substring); 424 | // Check for $FOO format 425 | const isValidDirectFormat = /^\$[A-Za-z_][A-Za-z0-9_]*/.test(substring); 426 | 427 | if (!isValidCurlyFormat && !isValidDirectFormat) { 428 | throw new Error("Invalid dollar sign usage detected."); 429 | } 430 | 431 | // Move past this $ instance 432 | currentIndex++; 433 | } 434 | } 435 | 436 | /// YAML-FM must be explicitly marked with a `dependencies` node 437 | function extract_well_formatted_entries( 438 | yaml: PlainObject, 439 | ): { deps: PackageRequirement[]; env: Record } { 440 | const deps = parse_deps(yaml.dependencies); 441 | const env = isPlainObject(yaml.env) ? yaml.env : {}; //TODO provide diagnostics if verbose, throw if part of a `pkgx` node 442 | return { deps, env }; 443 | } 444 | 445 | function parse_deps(node: unknown) { 446 | if (isString(node)) node = node.split(/\s+/).filter((x) => x); 447 | 448 | function parse(input: string) { 449 | // @latest means '*' here, we refuse to always check for newer versions 450 | // that is up to the user to initiate, however we should allow the spec since 451 | // users expect it. Maybe we should console.warn? 452 | // discussion: https://github.com/pkgxdev/pkgx/issues/797 453 | if (input.endsWith("@latest")) input = input.slice(0, -6); 454 | 455 | return utils.pkg.parse(input); 456 | } 457 | 458 | if (isArray(node)) { 459 | node = node.map(parse).reduce((acc, curr) => { 460 | acc[curr.project] = curr.constraint.toString(); 461 | return acc; 462 | }, {} as Record); 463 | } 464 | 465 | if (!isPlainObject(node)) { 466 | return []; //TODO provide diagnostics if verbose, throw if part of a `pkgx` node 467 | } 468 | 469 | return Object.entries(node) 470 | .compact(([project, constraint]) => { 471 | // see comment above in parse() about @latest 472 | if (/^@?latest$/.test(constraint)) constraint = "*"; 473 | return validatePackageRequirement(project, constraint); 474 | }); 475 | } 476 | 477 | export const _internals = { 478 | validateDollarSignUsage, 479 | }; 480 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@std/assert@*": "0.224.0", 5 | "jsr:@std/assert@0.224": "0.224.0", 6 | "jsr:@std/assert@^1.0.6": "1.0.6", 7 | "jsr:@std/bytes@^1.0.2": "1.0.2", 8 | "jsr:@std/cli@1": "1.0.11", 9 | "jsr:@std/crypto@1": "1.0.3", 10 | "jsr:@std/encoding@1": "1.0.5", 11 | "jsr:@std/flags@*": "0.224.0", 12 | "jsr:@std/fmt@0.224": "0.224.0", 13 | "jsr:@std/fs@1": "1.0.8", 14 | "jsr:@std/internal@0.224": "0.224.0", 15 | "jsr:@std/internal@^1.0.4": "1.0.4", 16 | "jsr:@std/io@*": "0.225.0", 17 | "jsr:@std/io@0.225": "0.225.0", 18 | "jsr:@std/json@1": "1.0.0", 19 | "jsr:@std/jsonc@*": "1.0.1", 20 | "jsr:@std/path@1": "1.0.8", 21 | "jsr:@std/path@^1.0.8": "1.0.8", 22 | "jsr:@std/testing@*": "1.0.3", 23 | "jsr:@std/yaml@*": "1.0.5", 24 | "jsr:@std/yaml@1": "1.0.5", 25 | "npm:@types/node@*": "22.5.4" 26 | }, 27 | "jsr": { 28 | "@std/assert@0.224.0": { 29 | "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f", 30 | "dependencies": [ 31 | "jsr:@std/fmt", 32 | "jsr:@std/internal@0.224" 33 | ] 34 | }, 35 | "@std/assert@1.0.6": { 36 | "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207", 37 | "dependencies": [ 38 | "jsr:@std/internal@^1.0.4" 39 | ] 40 | }, 41 | "@std/bytes@1.0.2": { 42 | "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" 43 | }, 44 | "@std/cli@1.0.11": { 45 | "integrity": "ec219619fdcd31bcf0d8e53bee1e2706ec9a02f70255365a094f69755dadd340" 46 | }, 47 | "@std/crypto@1.0.3": { 48 | "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" 49 | }, 50 | "@std/encoding@1.0.5": { 51 | "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" 52 | }, 53 | "@std/flags@0.224.0": { 54 | "integrity": "d40eaf58c356b2e1313c6d4e62dc28b614aad2ddae6f5ff72a969e0b1f5ad689", 55 | "dependencies": [ 56 | "jsr:@std/assert@0.224" 57 | ] 58 | }, 59 | "@std/fmt@0.224.0": { 60 | "integrity": "e20e9a2312a8b5393272c26191c0a68eda8d2c4b08b046bad1673148f1d69851" 61 | }, 62 | "@std/fs@1.0.8": { 63 | "integrity": "161c721b6f9400b8100a851b6f4061431c538b204bb76c501d02c508995cffe0", 64 | "dependencies": [ 65 | "jsr:@std/path@^1.0.8" 66 | ] 67 | }, 68 | "@std/internal@0.224.0": { 69 | "integrity": "afc50644f9cdf4495eeb80523a8f6d27226b4b36c45c7c195dfccad4b8509291", 70 | "dependencies": [ 71 | "jsr:@std/fmt" 72 | ] 73 | }, 74 | "@std/internal@1.0.4": { 75 | "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" 76 | }, 77 | "@std/io@0.225.0": { 78 | "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3", 79 | "dependencies": [ 80 | "jsr:@std/bytes" 81 | ] 82 | }, 83 | "@std/json@1.0.0": { 84 | "integrity": "985c1e544918d42e4e84072fc739ac4a19c3a5093292c99742ffcdd03fb6a268" 85 | }, 86 | "@std/jsonc@1.0.1": { 87 | "integrity": "6b36956e2a7cbb08ca5ad7fbec72e661e6217c202f348496ea88747636710dda", 88 | "dependencies": [ 89 | "jsr:@std/json" 90 | ] 91 | }, 92 | "@std/path@1.0.8": { 93 | "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" 94 | }, 95 | "@std/testing@1.0.3": { 96 | "integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42", 97 | "dependencies": [ 98 | "jsr:@std/assert@^1.0.6" 99 | ] 100 | }, 101 | "@std/yaml@1.0.5": { 102 | "integrity": "71ba3d334305ee2149391931508b2c293a8490f94a337eef3a09cade1a2a2742" 103 | } 104 | }, 105 | "npm": { 106 | "@types/node@22.5.4": { 107 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 108 | "dependencies": [ 109 | "undici-types" 110 | ] 111 | }, 112 | "undici-types@6.19.8": { 113 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 114 | } 115 | }, 116 | "remote": { 117 | "https://deno.land/std@0.196.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", 118 | "https://deno.land/std@0.196.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", 119 | "https://deno.land/std@0.196.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", 120 | "https://deno.land/std@0.196.0/flags/mod.ts": "a5ac18af6583404f21ea03771f8816669d901e0ff4374020870334d6f61d73d5", 121 | "https://deno.land/std@0.196.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 122 | "https://deno.land/std@0.196.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", 123 | "https://deno.land/std@0.196.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", 124 | "https://deno.land/std@0.196.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", 125 | "https://deno.land/std@0.196.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", 126 | "https://deno.land/std@0.196.0/path/mod.ts": "f065032a7189404fdac3ad1a1551a9ac84751d2f25c431e101787846c86c79ef", 127 | "https://deno.land/std@0.196.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", 128 | "https://deno.land/std@0.196.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", 129 | "https://deno.land/std@0.196.0/path/win32.ts": "4fca292f8d116fd6d62f243b8a61bd3d6835a9f0ede762ba5c01afe7c3c0aa12", 130 | "https://deno.land/x/is_what@v4.1.15/src/index.ts": "e55b975d532b71a0e32501ada85ae3c67993b75dc1047c6d1a2e00368b789af0", 131 | "https://deno.land/x/outdent@v0.8.0/mod.ts": "72630e680dcc36d5ae556fbff6900b12706c81a6fd592345fc98bcc0878fb3ca", 132 | "https://deno.land/x/outdent@v0.8.0/src/index.ts": "6dc3df4108d5d6fedcdb974844d321037ca81eaaa16be6073235ff3268841a22", 133 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/mod.ts": "14a69905ffad8064444c02d146008efeb6a0ddf0fe543483839af18e01684f5a", 134 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/deps.ts": "4135fc00efbacb68e3700a5498d236f2a70a5e36284954d87aa5d7becc62282d", 135 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useCache.ts": "9f3cc576fabae2caa6aedbf00ab12a59c732be1315471e5a475fef496c1e35ae", 136 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useCellar.ts": "c1e264fcb732423734f8c113fc7cb80c97befe8f13ed9d24906328bc5526c72d", 137 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useConfig.ts": "d5a02ee7a191fb4a2c3cd1721690ab6cf0338c9680847a9e9c4a6c9ea94df025", 138 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useDownload.ts": "3f9133486008146809508783b977e3480d0a43238ace27f78565fb9679aa9906", 139 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useFetch.ts": "ecf29342210b8eceed216e3bb73fcc7ea5b3ea5059686cf344ed190ca42ff682", 140 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useInventory.ts": "f459d819ab676a7e3786522d856b7670e994e4a755b0d1609b53c8b4ebe0c959", 141 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useMoustaches.ts": "e9166ddace759315782be0f570a4cd63c78e3b85592d59b75ddd33a0e401aa6b", 142 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useOffLicense.ts": "1c41ef6882512b67a47fcd1d1c0ce459906d6981a59f6be86d982594a7c26058", 143 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/usePantry.ts": "5e9f6dae8042b2b7635a1e7e4492f15b576d7a735eec7604bb824f7676d14d95", 144 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useShellEnv.ts": "ae2388d3f15d2e03435df23a8392ace21d3d4f0c83b2575a9670ab7badc389c3", 145 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useSync.ts": "ea605a0eaa43ab9988d36dd6150e16dd911c4be45b7b0f2add6b236636bd517c", 146 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/hooks/useSyncCache.ts": "30891e9d923f2c2b28f1ba220923221195b8261a4aeea18ef2676d93bd5da10d", 147 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/plumbing/hydrate.ts": "c75f151ed307532ce9c2bf62c61e6478bb1132f95a11b848e02ea2dec08c2ff3", 148 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/plumbing/install.ts": "2a4e19fae70fef7ba0be454fd5b7efed4d7d19a5141d26d3b26124ab792007ed", 149 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/plumbing/link.ts": "0ed6198de737ebeab1704d375c732c9264fb0cfa7f2aedddb90f51d100174a73", 150 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/plumbing/resolve.ts": "9425e0d201ee440a8dc011940046f0bb6d94aa29cd738e1a8c39ca86e55aad41", 151 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/plumbing/which.ts": "f918211e561e56aabf6909e06fa10fa3be06ffebd9e7cc28ce57efef4faff27d", 152 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/porcelain/install.ts": "85caffe3842ab63bf6d59c6c5c9fb93fbc95a0d5652488d93b95d865722b67b9", 153 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/porcelain/run.ts": "55cc9124dca732e2f5557a8c451daebecb109c86b2f4347fa1e433aedf35ab5a", 154 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/types.ts": "dc1a4e6458d11454282f832909838c56f786a26eed54fb8ab5675d6691ebf534", 155 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/Path.ts": "45303993a377363277e6c201160f36f1f9a5997632db03f473b618968d568e58", 156 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/error.ts": "b0d3130f5cdfc0cc8ea10f93fea0e7e97d4473ddc9bc527156b0fcf24c7b939c", 157 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/flock.ts": "5fd77f6b53c3a90888cf20a7726e9047aad2c766e4ec2fbf7cf2f916b98d99a4", 158 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/host.ts": "3b9e0d4cb05f9bde0ee8bcb0f8557b0a339f6ef56dfb1f08b2cfa63b44db91ee", 159 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/misc.ts": "a4d7944da07066e5dd2ef289af436dc7f1032aed4272811e9b19ceeed60b8491", 160 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/pkg.ts": "e737cc9a98cd6a2797668c6ef856128692290256a521cc3906bd538410925451", 161 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/read-lines.ts": "6d947ccd5f8e48701ed9c5402b6ac5144df3fce60d666f19b6506edbc36c8367", 162 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/src/utils/semver.ts": "da22a0e0cf74de792cc4d44c01ec5b767463816c8abb4b5fb86583ccda573298", 163 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/mod.ts": "7ce0a19f9cea3475cc94750ece61c20d857f1c3a279ad38cd029a3f8d9b7b03e", 164 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/src/constants.ts": "85fd27aa6e199093f25f5f437052e16fd0e0870b96ca9b24a98e04ddc8b7d006", 165 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/src/database.ts": "49569b0f279cfc3e42730002ae789a2694da74deb212e63a4b4e6640dc4d70ba", 166 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/src/ffi.ts": "ddffcee178b3e72c45be385efd8b4434f7196cafe45a0046ae68df9af307c7f3", 167 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/src/statement.ts": "2be7ffebbb72a031899dbf189972c5596aa73eabfc8a382a1bac9c5c111b0026", 168 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/vendor/sqlite3@0.10.0/src/util.ts": "19815a492dd8f4c684587238dc20066de11782137de549cd4c9709d1b548247e", 169 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/mod.ts": "14a69905ffad8064444c02d146008efeb6a0ddf0fe543483839af18e01684f5a", 170 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/deps.ts": "6941cfc0b926d256c067a0ce3546ff7bac5a043c10d64f3ab06fef99d373f49d", 171 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useCache.ts": "9f3cc576fabae2caa6aedbf00ab12a59c732be1315471e5a475fef496c1e35ae", 172 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useCellar.ts": "c1e264fcb732423734f8c113fc7cb80c97befe8f13ed9d24906328bc5526c72d", 173 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useConfig.ts": "57ec8590b6d063a98932eb8f6ebb0e520c0be4e94c228f0d93ea87d01cb5c110", 174 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useDownload.ts": "3f9133486008146809508783b977e3480d0a43238ace27f78565fb9679aa9906", 175 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useFetch.ts": "ecf29342210b8eceed216e3bb73fcc7ea5b3ea5059686cf344ed190ca42ff682", 176 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useInventory.ts": "f459d819ab676a7e3786522d856b7670e994e4a755b0d1609b53c8b4ebe0c959", 177 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useMoustaches.ts": "e9166ddace759315782be0f570a4cd63c78e3b85592d59b75ddd33a0e401aa6b", 178 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useOffLicense.ts": "1c41ef6882512b67a47fcd1d1c0ce459906d6981a59f6be86d982594a7c26058", 179 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/usePantry.ts": "113f3ac7cb6565425eebc7f1bd1ee52217f074865b46b452db79cc72d82e4d4a", 180 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useShellEnv.ts": "ae2388d3f15d2e03435df23a8392ace21d3d4f0c83b2575a9670ab7badc389c3", 181 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useSync.ts": "ea605a0eaa43ab9988d36dd6150e16dd911c4be45b7b0f2add6b236636bd517c", 182 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/hooks/useSyncCache.ts": "30891e9d923f2c2b28f1ba220923221195b8261a4aeea18ef2676d93bd5da10d", 183 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/plumbing/hydrate.ts": "c75f151ed307532ce9c2bf62c61e6478bb1132f95a11b848e02ea2dec08c2ff3", 184 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/plumbing/install.ts": "2a4e19fae70fef7ba0be454fd5b7efed4d7d19a5141d26d3b26124ab792007ed", 185 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/plumbing/link.ts": "0ed6198de737ebeab1704d375c732c9264fb0cfa7f2aedddb90f51d100174a73", 186 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/plumbing/resolve.ts": "9425e0d201ee440a8dc011940046f0bb6d94aa29cd738e1a8c39ca86e55aad41", 187 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/plumbing/which.ts": "f918211e561e56aabf6909e06fa10fa3be06ffebd9e7cc28ce57efef4faff27d", 188 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/porcelain/install.ts": "85caffe3842ab63bf6d59c6c5c9fb93fbc95a0d5652488d93b95d865722b67b9", 189 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/porcelain/run.ts": "55cc9124dca732e2f5557a8c451daebecb109c86b2f4347fa1e433aedf35ab5a", 190 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/types.ts": "dc1a4e6458d11454282f832909838c56f786a26eed54fb8ab5675d6691ebf534", 191 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/Path.ts": "3ce5a1559219adeb65f7df18e2c29c26782a614bdaf635abe1d72a2ce92d2c94", 192 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/error.ts": "b0d3130f5cdfc0cc8ea10f93fea0e7e97d4473ddc9bc527156b0fcf24c7b939c", 193 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/flock.ts": "5fd77f6b53c3a90888cf20a7726e9047aad2c766e4ec2fbf7cf2f916b98d99a4", 194 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/host.ts": "3b9e0d4cb05f9bde0ee8bcb0f8557b0a339f6ef56dfb1f08b2cfa63b44db91ee", 195 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/misc.ts": "a4d7944da07066e5dd2ef289af436dc7f1032aed4272811e9b19ceeed60b8491", 196 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/pkg.ts": "e737cc9a98cd6a2797668c6ef856128692290256a521cc3906bd538410925451", 197 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/read-lines.ts": "6d947ccd5f8e48701ed9c5402b6ac5144df3fce60d666f19b6506edbc36c8367", 198 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/src/utils/semver.ts": "84884902ec2dcc1d538960dc274a69931723d66252e0531759d2a43df2406b20", 199 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/mod.ts": "7ce0a19f9cea3475cc94750ece61c20d857f1c3a279ad38cd029a3f8d9b7b03e", 200 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/src/constants.ts": "85fd27aa6e199093f25f5f437052e16fd0e0870b96ca9b24a98e04ddc8b7d006", 201 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/src/database.ts": "49569b0f279cfc3e42730002ae789a2694da74deb212e63a4b4e6640dc4d70ba", 202 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/src/ffi.ts": "ddffcee178b3e72c45be385efd8b4434f7196cafe45a0046ae68df9af307c7f3", 203 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/src/statement.ts": "2be7ffebbb72a031899dbf189972c5596aa73eabfc8a382a1bac9c5c111b0026", 204 | "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.21.0/vendor/sqlite3@0.10.0/src/util.ts": "19815a492dd8f4c684587238dc20066de11782137de549cd4c9709d1b548247e" 205 | } 206 | } 207 | --------------------------------------------------------------------------------