Skip to content

Commit 09ab358

Browse files
committed
The pursuit of ’app’yness
1 parent 5cc0705 commit 09ab358

File tree

9 files changed

+760
-30
lines changed

9 files changed

+760
-30
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
jobs:
8-
test:
8+
test-action:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
@@ -20,3 +20,14 @@ jobs:
2020
- run: echo $PATH
2121
- run: env
2222
- run: deno --version
23+
24+
lint:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: denolib/setup-deno@v2
29+
with:
30+
deno-version: v2.x
31+
- run: deno fmt --check .
32+
- run: deno lint .
33+
- run: deno check ./app.ts

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"deno.enable": true
3+
}

app.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env -S pkgx deno^2 run -A
2+
3+
import { Path, utils } from "libpkgx";
4+
import shellcode from "./src/shellcode().ts";
5+
import sniff from "./src/sniff.ts";
6+
import shell_escape from "./src/shell-escape.ts";
7+
8+
switch (Deno.args[0]) {
9+
case "--help":
10+
console.log("https://github.com/pkgxdev/dev");
11+
Deno.exit(0);
12+
break;
13+
case "--shellcode":
14+
console.log(shellcode());
15+
Deno.exit(0);
16+
break;
17+
}
18+
19+
const snuff = await sniff(Path.cwd());
20+
21+
const pkgspecs = snuff.pkgs.map((pkg) => `+${utils.pkg.str(pkg)}`);
22+
23+
const cmd = new Deno.Command("pkgx", {
24+
args: [...pkgspecs],
25+
stdout: "piped",
26+
env: { CLICOLOR_FORCE: "1" }, // unfortunate
27+
}).spawn();
28+
29+
await cmd.status;
30+
31+
const stdout = (await cmd.output()).stdout;
32+
let env = new TextDecoder().decode(stdout).trim();
33+
34+
// add any additional env that we sniffed
35+
for (const [key, value] of Object.entries(snuff.env)) {
36+
env += `${key}=${shell_escape(value)}\n`;
37+
}
38+
39+
//TODO moustaches aren’t being expanded
40+
41+
let undo = "";
42+
for (const envln of env.split("\n")) {
43+
const [key] = envln.split("=", 2);
44+
const value = Deno.env.get(key);
45+
if (value) {
46+
undo += ` export ${key}=${shell_escape(value)}\n`;
47+
} else {
48+
undo += ` unset ${key}\n`;
49+
}
50+
}
51+
52+
const dir = Deno.cwd();
53+
54+
console.log(`
55+
set -a
56+
${env}
57+
set +a
58+
59+
_pkgx_dev_try_bye() {
60+
suffix="\${PWD#"${dir}"}"
61+
if test "$PWD" != "${dir}$suffix"; then
62+
${undo.trim()}
63+
unset -f _pkgx_dev_try_bye
64+
return 0
65+
else
66+
return 1
67+
fi
68+
}
69+
`.trim());

deno.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true
4+
},
5+
"pkgx": "deno^2.1",
6+
"lint": {
7+
"include": ["src/", "./app.ts"],
8+
"exclude": ["**/*.test.ts"]
9+
},
10+
"test": {
11+
"include": ["src/"]
12+
},
13+
"imports": {
14+
"libpkgx": "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/mod.ts",
15+
"libpkgx/": "https://raw.githubusercontent.com/pkgxdev/libpkgx/refs/tags/v0.20.1/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+
}

deno.lock

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

parse.js

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,43 @@
1-
const fs = require('fs');
2-
const readline = require('readline');
1+
const fs = require("fs");
2+
const readline = require("readline");
33

44
const readInterface = readline.createInterface({
5-
input: fs.createReadStream(process.argv[2]),
6-
output: process.stdout,
7-
terminal: false
5+
input: fs.createReadStream(process.argv[2]),
6+
output: process.stdout,
7+
terminal: false,
88
});
99

10-
const stripQuotes = (str) => str.startsWith('"') || str.startsWith("'") ? str.slice(1, -1) : str;
10+
const stripQuotes = (str) =>
11+
str.startsWith('"') || str.startsWith("'") ? str.slice(1, -1) : str;
1112

1213
const replaceEnvVars = (str) => {
13-
const value = str
14-
.replaceAll(/\$\{([a-zA-Z0-9_]+):\+:\$[a-zA-Z0-9_]+\}/g, (_, key) => (v => v ? `:${v}` : '')(process.env[key]))
15-
.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (_, key) => process.env[key] ?? '')
16-
.replaceAll(/\$([a-zA-Z0-9_]+)/g, (_, key) => process.env[key] ?? '')
17-
console.error("FOO", str, value)
18-
return value
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+
console.error("FOO", str, value);
22+
return value;
1923
};
2024

21-
readInterface.on('line', (line) => {
22-
const match = line.match(/^export ([^=]+)=(.*)$/);
23-
if (match) {
24-
const [_, key, value_] = match;
25-
const value = stripQuotes(value_);
26-
if (key === 'PATH') {
27-
value
28-
.replaceAll('${PATH:+:$PATH}', '')
29-
.replaceAll('$PATH', '')
30-
.replaceAll('${PATH}', '')
31-
.split(':').forEach(path => {
32-
fs.appendFileSync(process.env['GITHUB_PATH'], `${path}\n`);
33-
});
34-
} else {
35-
let v = replaceEnvVars(value);
36-
fs.appendFileSync(process.env['GITHUB_ENV'], `${key}=${v}\n`);
37-
}
25+
readInterface.on("line", (line) => {
26+
const match = line.match(/^export ([^=]+)=(.*)$/);
27+
if (match) {
28+
const [_, key, value_] = match;
29+
const value = stripQuotes(value_);
30+
if (key === "PATH") {
31+
value
32+
.replaceAll("${PATH:+:$PATH}", "")
33+
.replaceAll("$PATH", "")
34+
.replaceAll("${PATH}", "")
35+
.split(":").forEach((path) => {
36+
fs.appendFileSync(process.env["GITHUB_PATH"], `${path}\n`);
37+
});
38+
} else {
39+
let v = replaceEnvVars(value);
40+
fs.appendFileSync(process.env["GITHUB_ENV"], `${key}=${v}\n`);
3841
}
42+
}
3943
});

src/shell-escape.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
}

src/shellcode().ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Path } from "libpkgx";
2+
3+
export default function shellcode() {
4+
const datadir = new Path(
5+
Deno.env.get("XDG_DATA_HOME")?.trim() || platform_data_home_default(),
6+
).join("pkgx", "dev");
7+
8+
return `
9+
_pkgx_chpwd_hook() {
10+
if ! type _pkgx_dev_try_bye >/dev/null 2>&1 || _pkgx_dev_try_bye; then
11+
dir="$PWD"
12+
while [ "$dir" != "/" ]; do
13+
if [ -f "${datadir}/$dir/dev.pkgx.activated" ]; then
14+
eval "$(command dev)"
15+
break
16+
fi
17+
dir="$(dirname "$dir")"
18+
done
19+
fi
20+
}
21+
22+
if [ -n "$ZSH_VERSION" ] && [ $(emulate) = zsh ]; then
23+
eval 'typeset -ag chpwd_functions
24+
25+
if [[ -z "\${chpwd_functions[(r)_pkgx_chpwd_hook]+1}" ]]; then
26+
chpwd_functions=( _pkgx_chpwd_hook \${chpwd_functions[@]} )
27+
fi
28+
29+
if [ "$TERM_PROGRAM" != Apple_Terminal ]; then
30+
_pkgx_chpwd_hook
31+
fi'
32+
elif [ -n "$BASH_VERSION" ] && [ "$POSIXLY_CORRECT" != y ] ; then
33+
eval 'cd() {
34+
builtin cd "$@" || return
35+
_pkgx_chpwd_hook
36+
}
37+
_pkgx_chpwd_hook'
38+
else
39+
POSIXLY_CORRECT=y
40+
echo "pkgx: dev: warning: unsupported shell" >&2
41+
fi
42+
`.trim();
43+
}
44+
45+
function platform_data_home_default() {
46+
const home = Path.home();
47+
switch (Deno.build.os) {
48+
case "darwin":
49+
return home.join("Library/Application Support");
50+
case "windows": {
51+
const LOCALAPPDATA = Deno.env.get("LOCALAPPDATA");
52+
if (LOCALAPPDATA) {
53+
return new Path(LOCALAPPDATA);
54+
} else {
55+
return home.join("AppData/Local");
56+
}
57+
}
58+
default:
59+
return home.join(".local/share");
60+
}
61+
}

0 commit comments

Comments
 (0)