Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ jobs:
- uses: denolib/setup-deno@v2
with:
deno-version: v2.x
- run: deno test --allow-read --allow-write --allow-env
- run: deno test --allow-read --allow-write --allow-env --allow-run=bash
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Public repository for developer environment activation tooling.
- `deno fmt --check .`
- `deno lint .`
- `deno check ./app.ts`
- `deno test --allow-read --allow-write --allow-env`
- `deno test --allow-read --allow-write --allow-env --allow-run=bash`

## Always Do

Expand Down
65 changes: 65 additions & 0 deletions src/shellcode().test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { assert, assertEquals } from "jsr:@std/assert";
import { Path } from "libpkgx";
import shellcode, { datadir } from "./shellcode().ts";

// Issue #51: cd-ing directly into a subdir of an already-activated devenv
// must activate the devenv (and not emit `permission denied`). This drives
// the generated shellcode through a real bash subprocess with a fake `dev`
// on PATH so we can assert how the chpwd hook invokes it.
Deno.test("chpwd hook activates when cd-ing into subdir of devenv (#51)", async () => {
const tmp = Path.mktemp();
const proj = tmp.join("proj").mkdir();
const sub = proj.join("sub").mkdir();
const xdg = tmp.join("xdg").mkdir();
const bin = tmp.join("bin").mkdir();
const log = tmp.join("dev-args.log");

// pre-activate `proj` (not `sub`) — that's the case from the bug report
xdg.join("pkgx", "dev").join(proj.string.slice(1)).mkdir("p")
.join("dev.pkgx.activated").touch();

// fake `dev` records its argv and emits a sentinel for eval to run
const fake_dev = bin.join("dev");
Deno.writeTextFileSync(
fake_dev.string,
`#!/bin/sh\nprintf '%s\\n' "$@" >> "${log}"\necho 'echo HOOK_OK'\n`,
);
Deno.chmodSync(fake_dev.string, 0o755);

const env = {
...Deno.env.toObject(),
PATH: `${bin}:${Deno.env.get("PATH") ?? ""}`,
XDG_DATA_HOME: xdg.string,
};

const proc = await new Deno.Command("bash", {
args: ["-c", `${shellcode(env)}\ncd "${sub}"`],
env,
stdout: "piped",
stderr: "piped",
}).output();

const stdout = new TextDecoder().decode(proc.stdout);
const stderr = new TextDecoder().decode(proc.stderr);
const dev_args = log.isFile()
? Deno.readTextFileSync(log.string).split("\n").filter(Boolean)
: [];

assert(
stdout.includes("HOOK_OK"),
`hook should eval dev's stdout. stdout=${stdout} stderr=${stderr}`,
);
assertEquals(stderr, "", "hook should produce no stderr");
assertEquals(
dev_args,
[proj.string],
"dev must be invoked with the activated dir, not bare",
);
});

Deno.test("datadir respects XDG_DATA_HOME", () => {
assertEquals(
datadir({ XDG_DATA_HOME: "/tmp/xdg-test" }).string,
"/tmp/xdg-test/pkgx/dev",
);
});
28 changes: 16 additions & 12 deletions src/shellcode().ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { Path } from "libpkgx";

export default function shellcode() {
type Env = Record<string, string>;

export default function shellcode(env: Env = Deno.env.toObject()) {
// find self
const dev_cmd = Deno.env.get("PATH")?.split(":").map((path) =>
const dev_cmd = env.PATH?.split(":").map((path) =>
Path.abs(path)?.join("dev")
)
.filter((x) => x?.isExecutableFile())[0];

if (!dev_cmd) throw new Error("couldn’t find `dev`");

const dd = datadir(env);

return `
_pkgx_chpwd_hook() {
if ! type _pkgx_dev_try_bye >/dev/null 2>&1 || _pkgx_dev_try_bye; then
dir="$PWD"
while [ "$dir" != / -a "$dir" != . ]; do
if [ -f "${datadir()}/$dir/dev.pkgx.activated" ]; then
eval "$(${dev_cmd})" "$dir"
if [ -f "${dd}/$dir/dev.pkgx.activated" ]; then
eval "$(${dev_cmd} "$dir")"
break
fi
dir="$(dirname "$dir")"
Expand All @@ -29,8 +33,8 @@ dev() {
if type -f _pkgx_dev_try_bye >/dev/null 2>&1; then
dir="$PWD"
while [ "$dir" != / -a "$dir" != . ]; do
if [ -f "${datadir()}/$dir/dev.pkgx.activated" ]; then
rm "${datadir()}/$dir/dev.pkgx.activated"
if [ -f "${dd}/$dir/dev.pkgx.activated" ]; then
rm "${dd}/$dir/dev.pkgx.activated"
break
fi
dir="$(dirname "$dir")"
Expand All @@ -43,8 +47,8 @@ dev() {
if [ "$2" ]; then
"${dev_cmd}" "$@"
elif ! type -f _pkgx_dev_try_bye >/dev/null 2>&1; then
mkdir -p "${datadir()}$PWD"
touch "${datadir()}$PWD/dev.pkgx.activated"
mkdir -p "${dd}$PWD"
touch "${dd}$PWD/dev.pkgx.activated"
eval "$(${dev_cmd})"
else
echo "devenv already active" >&2
Expand Down Expand Up @@ -77,19 +81,19 @@ fi
`.trim();
}

export function datadir() {
export function datadir(env: Env = Deno.env.toObject()) {
return new Path(
Deno.env.get("XDG_DATA_HOME")?.trim() || platform_data_home_default(),
env.XDG_DATA_HOME?.trim() || platform_data_home_default(env),
).join("pkgx", "dev");
}

function platform_data_home_default() {
function platform_data_home_default(env: Env) {
const home = Path.home();
switch (Deno.build.os) {
case "darwin":
return home.join("Library/Application Support");
case "windows": {
const LOCALAPPDATA = Deno.env.get("LOCALAPPDATA");
const LOCALAPPDATA = env.LOCALAPPDATA;
if (LOCALAPPDATA) {
return new Path(LOCALAPPDATA);
} else {
Expand Down
Loading