Skip to content

Commit 1cbab63

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

File tree

9 files changed

+787
-30
lines changed

9 files changed

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

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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
dev() {
23+
case "$1" in
24+
off)
25+
if type -f _pkgx_dev_try_bye >/dev/null 2>&1; then
26+
rm "${datadir}$PWD/dev.pkgx.activated"
27+
_pkgx_dev_try_bye
28+
else
29+
echo "no devenv" >&2
30+
fi;;
31+
''|on)
32+
mkdir -p "${datadir}$PWD"
33+
touch "${datadir}$PWD/dev.pkgx.activated"
34+
eval "$(command dev)";;
35+
*)
36+
return 2;;
37+
esac
38+
}
39+
40+
if [ -n "$ZSH_VERSION" ] && [ $(emulate) = zsh ]; then
41+
eval 'typeset -ag chpwd_functions
42+
43+
if [[ -z "\${chpwd_functions[(r)_pkgx_chpwd_hook]+1}" ]]; then
44+
chpwd_functions=( _pkgx_chpwd_hook \${chpwd_functions[@]} )
45+
fi
46+
47+
if [ "$TERM_PROGRAM" != Apple_Terminal ]; then
48+
_pkgx_chpwd_hook
49+
fi'
50+
elif [ -n "$BASH_VERSION" ] && [ "$POSIXLY_CORRECT" != y ] ; then
51+
eval 'cd() {
52+
builtin cd "$@" || return
53+
_pkgx_chpwd_hook
54+
}
55+
_pkgx_chpwd_hook'
56+
else
57+
POSIXLY_CORRECT=y
58+
echo "pkgx: dev: warning: unsupported shell" >&2
59+
fi
60+
`.trim();
61+
}
62+
63+
function platform_data_home_default() {
64+
const home = Path.home();
65+
switch (Deno.build.os) {
66+
case "darwin":
67+
return home.join("Library/Application Support");
68+
case "windows": {
69+
const LOCALAPPDATA = Deno.env.get("LOCALAPPDATA");
70+
if (LOCALAPPDATA) {
71+
return new Path(LOCALAPPDATA);
72+
} else {
73+
return home.join("AppData/Local");
74+
}
75+
}
76+
default:
77+
return home.join(".local/share");
78+
}
79+
}

0 commit comments

Comments
 (0)