├── 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 |
--------------------------------------------------------------------------------