argsbarg 6.3.0 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.3.1] - 2026-08-07
11
+
12
+
10
13
  ## [6.3.0] - 2026-08-07
11
14
 
12
15
  ### Added
@@ -14,10 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
17
  - **Cursor `stop` hook** — `.cursor/hooks.json` runs `just test` after agent edits to `justfile` or `*.{ts,tsx,js,jsx}` (excluding `node_modules/`, `dist/`, `.cursor/`); test failures auto-submit up to 20 follow-ups. Shipped in copy templates and argsbarg repo root (Cursor-only; not in `AGENTS.md`).
15
18
  - **`configure` lifecycle hooks** — `program.configure.afterRefresh` and `program.configure.beforeRemoveAll` for app-specific agent artifact setup/teardown around `configure --refresh` and `configure --remove-all`.
16
19
 
20
+ ### Fixed
21
+
22
+ - **`userHome()`** — resolves home from `TEST_USER_HOME` (tests) or platform defaults (`/Users/$USER`, `/home/$USER`, `USERPROFILE`). Never reads `$HOME`, fixing skills installing under Homebrew's `post_install` sandbox.
23
+
17
24
  ### Changed
18
25
 
19
- - **Breaking: `configure --sync` → `configure --refresh`** — renames the non-interactive agent-artifact refresh flag (skills, MCP, config bootstrap). `just sync-artifacts` → `just refresh-artifacts`. No `--sync` alias.
20
- - **Copy-template justfiles** — `install-local` uses `brew install --force`; uninstall/untap recipes prefix `NONINTERACTIVE=1` so agents and CI skip Homebrew confirm prompts (including post-uninstall untap). `install-production` unchanged.
26
+ - **Breaking: `configure --sync` → `configure --refresh`** — renames the non-interactive agent-artifact refresh flag (skills, MCP, config bootstrap). No `--sync` alias. Copy-template justfiles no longer ship a `refresh-artifacts` recipe; run `configure --refresh --yes` directly.
27
+ - **Copy-template justfiles** — `install-local` uses `brew reinstall || brew install --force`; uninstall/untap recipes prefix `NONINTERACTIVE=1` so agents and CI skip Homebrew confirm prompts (including post-uninstall untap). `install-production` unchanged.
21
28
 
22
29
  ## [6.2.2] - 2026-08-07
23
30
 
@@ -915,7 +922,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
915
922
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
916
923
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
917
924
 
918
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.3.0...HEAD
925
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.3.1...HEAD
926
+ [6.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.3.1
919
927
  [6.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.3.0
920
928
  [6.2.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.2.2
921
929
  [6.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.2.1
package/docs/configure.md CHANGED
@@ -35,7 +35,7 @@ just build
35
35
  just install-local # same formula as production; temporary file:// URL during brew install (`just install` is an alias)
36
36
  ```
37
37
 
38
- Dev flow matches release: formula `install` copies the binary and generates completions; `post_install` runs `<key> configure --refresh --yes` for skills/MCP. Use `just reinstall-local` to swap the binary into Cellar during tight edit cycles (skips completions and `post_install`). Use `just refresh-artifacts` to refresh agent artifacts without touching the binary.
38
+ Dev flow matches release: formula `install` copies the binary and generates completions; `post_install` runs `<key> configure --refresh --yes` for skills/MCP. Use `just reinstall-local` to swap the binary into Cellar during tight edit cycles (skips completions and `post_install`). Run `<key> configure --refresh --yes` (or `just run configure --refresh --yes`) to refresh agent artifacts without touching the binary.
39
39
 
40
40
  ## Quick reference
41
41
 
@@ -66,17 +66,13 @@ http:
66
66
  # Alias for backward compatibility
67
67
  install: install-local
68
68
 
69
- # Refresh skills and MCP without reinstalling the formula
70
- refresh-artifacts:
71
- just run configure --refresh --yes
72
-
73
69
  # Dev install: build, stage dev formula, brew install, restore release formula
74
70
  install-local: build
75
71
  @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
76
72
  mkdir -p {{tap_parent}}
77
73
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
78
74
  bun scripts/dev-formula.ts install
79
- brew install --force --formula {{tap}}/{{cli_key}}
75
+ NONINTERACTIVE=1 brew reinstall --formula {{tap}}/{{cli_key}} || NONINTERACTIVE=1 brew install --force --formula {{tap}}/{{cli_key}}
80
76
  bun scripts/dev-formula.ts reset
81
77
  @echo ""
82
78
  @echo "Next: {{cli_key}} configure"
@@ -76,17 +76,13 @@ http:
76
76
  # Alias for backward compatibility
77
77
  install: install-local
78
78
 
79
- # Refresh skills and MCP without reinstalling the formula
80
- refresh-artifacts:
81
- just run configure --refresh --yes
82
-
83
79
  # Dev install: build, stage dev formula, brew install, restore release formula
84
80
  install-local: build
85
81
  @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
86
82
  mkdir -p {{tap_parent}}
87
83
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
88
84
  bun scripts/dev-formula.ts install
89
- brew install --force --formula {{tap}}/{{cli_key}}
85
+ NONINTERACTIVE=1 brew reinstall --formula {{tap}}/{{cli_key}} || NONINTERACTIVE=1 brew install --force --formula {{tap}}/{{cli_key}}
90
86
  bun scripts/dev-formula.ts reset
91
87
  @echo ""
92
88
  @echo "Next: {{cli_key}} configure"
package/index.d.ts CHANGED
@@ -909,6 +909,11 @@ export interface PackMcpBundleOpts {
909
909
  * Requires the compiled binary to exist.
910
910
  */
911
911
  export declare function packMcpBundle(program: CliProgram, opts?: PackMcpBundleOpts): string;
912
+ /**
913
+ * Resolves the user home directory without depending on `$HOME`.
914
+ * This is helpful for when homebrew post-install hooks run with a temporary `$HOME`.
915
+ */
916
+ export declare function userHome(): string;
912
917
  /** JSON-safe command node (no handlers). */
913
918
  export interface CliSchemaExport {
914
919
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "6.3.0",
3
+ "version": "6.3.1",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -30,8 +30,8 @@ describe("builtins/config", () => {
30
30
  /** Tests that config get redacts sensitive values. */
31
31
  test("config get redacts sensitive values", async () => {
32
32
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
33
- const prevHome = process.env.HOME;
34
- process.env.HOME = dir;
33
+ const prevTestHome = process.env.TEST_USER_HOME;
34
+ process.env.TEST_USER_HOME = dir;
35
35
  const prev = process.env.API_TOKEN;
36
36
  delete process.env.API_TOKEN;
37
37
  try {
@@ -43,8 +43,8 @@ describe("builtins/config", () => {
43
43
  expect(result.exitCode).toBe(0);
44
44
  expect(result.stdout.trim()).toBe("REDACTED");
45
45
  } finally {
46
- if (prevHome === undefined) delete process.env.HOME;
47
- else process.env.HOME = prevHome;
46
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
47
+ else process.env.TEST_USER_HOME = prevTestHome;
48
48
  if (prev !== undefined) process.env.API_TOKEN = prev;
49
49
  rmSync(dir, { recursive: true, force: true });
50
50
  }
@@ -53,8 +53,8 @@ describe("builtins/config", () => {
53
53
  /** Tests that config get --json redacts sensitive as { set: true }. */
54
54
  test("config get --json redacts sensitive as { set: true }", async () => {
55
55
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
56
- const prevHome = process.env.HOME;
57
- process.env.HOME = dir;
56
+ const prevTestHome = process.env.TEST_USER_HOME;
57
+ process.env.TEST_USER_HOME = dir;
58
58
  const prev = process.env.API_TOKEN;
59
59
  delete process.env.API_TOKEN;
60
60
  try {
@@ -66,8 +66,8 @@ describe("builtins/config", () => {
66
66
  expect(result.exitCode).toBe(0);
67
67
  expect(JSON.parse(result.stdout)).toEqual({ set: true });
68
68
  } finally {
69
- if (prevHome === undefined) delete process.env.HOME;
70
- else process.env.HOME = prevHome;
69
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
70
+ else process.env.TEST_USER_HOME = prevTestHome;
71
71
  if (prev !== undefined) process.env.API_TOKEN = prev;
72
72
  rmSync(dir, { recursive: true, force: true });
73
73
  }
@@ -76,8 +76,8 @@ describe("builtins/config", () => {
76
76
  /** Tests that config set writes and resolves without required exit. */
77
77
  test("config set writes and resolves without required exit", async () => {
78
78
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
79
- const prevHome = process.env.HOME;
80
- process.env.HOME = dir;
79
+ const prevTestHome = process.env.TEST_USER_HOME;
80
+ process.env.TEST_USER_HOME = dir;
81
81
  const prev = process.env.API_TOKEN;
82
82
  delete process.env.API_TOKEN;
83
83
  try {
@@ -88,8 +88,8 @@ describe("builtins/config", () => {
88
88
  const get = await new Cli(program).invoke(["configure", "get", "port"]);
89
89
  expect(get.stdout.trim()).toBe("9090");
90
90
  } finally {
91
- if (prevHome === undefined) delete process.env.HOME;
92
- else process.env.HOME = prevHome;
91
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
92
+ else process.env.TEST_USER_HOME = prevTestHome;
93
93
  if (prev !== undefined) process.env.API_TOKEN = prev;
94
94
  rmSync(dir, { recursive: true, force: true });
95
95
  }
@@ -97,8 +97,8 @@ describe("builtins/config", () => {
97
97
 
98
98
  test("config set --from-env stores binding without literal", async () => {
99
99
  const dir = mkdtempSync(join(tmpdir(), "cfg-from-env-"));
100
- const prevHome = process.env.HOME;
101
- process.env.HOME = dir;
100
+ const prevTestHome = process.env.TEST_USER_HOME;
101
+ process.env.TEST_USER_HOME = dir;
102
102
  const prev = process.env.API_TOKEN;
103
103
  process.env.API_TOKEN = "from-env";
104
104
  try {
@@ -114,8 +114,8 @@ describe("builtins/config", () => {
114
114
  expect(onDisk.apiToken).toBeUndefined();
115
115
  expect(readBindings(onDisk).apiToken).toBe("env");
116
116
  } finally {
117
- if (prevHome === undefined) delete process.env.HOME;
118
- else process.env.HOME = prevHome;
117
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
118
+ else process.env.TEST_USER_HOME = prevTestHome;
119
119
  if (prev === undefined) delete process.env.API_TOKEN;
120
120
  else process.env.API_TOKEN = prev;
121
121
  rmSync(dir, { recursive: true, force: true });
@@ -29,8 +29,8 @@ describe("config/context", () => {
29
29
  /** Tests that AppConfigSnapshot get, require, read, set. */
30
30
  test("AppConfigSnapshot get, require, read, set", () => {
31
31
  const dir = mkdtempSync(join(tmpdir(), "ctx-test-"));
32
- const prevHome = process.env.HOME;
33
- process.env.HOME = dir;
32
+ const prevTestHome = process.env.TEST_USER_HOME;
33
+ process.env.TEST_USER_HOME = dir;
34
34
  const prevToken = process.env.API_TOKEN;
35
35
  delete process.env.API_TOKEN;
36
36
  try {
@@ -48,8 +48,8 @@ describe("config/context", () => {
48
48
  expect(ctx.get("note")).toBe("updated");
49
49
  expect(ctx.read().note).toBe("updated");
50
50
  } finally {
51
- if (prevHome === undefined) delete process.env.HOME;
52
- else process.env.HOME = prevHome;
51
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
52
+ else process.env.TEST_USER_HOME = prevTestHome;
53
53
  if (prevToken === undefined) delete process.env.API_TOKEN;
54
54
  else process.env.API_TOKEN = prevToken;
55
55
  rmSync(dir, { recursive: true, force: true });
@@ -82,8 +82,8 @@ describe("config/context", () => {
82
82
 
83
83
  test("AppConfigSnapshot unsafe read/write", () => {
84
84
  const dir = mkdtempSync(join(tmpdir(), "ctx-unsafe-"));
85
- const prevHome = process.env.HOME;
86
- process.env.HOME = dir;
85
+ const prevTestHome = process.env.TEST_USER_HOME;
86
+ process.env.TEST_USER_HOME = dir;
87
87
  const prevToken = process.env.API_TOKEN;
88
88
  delete process.env.API_TOKEN;
89
89
  try {
@@ -95,8 +95,8 @@ describe("config/context", () => {
95
95
  ctx.setUnsafe("note", "raw");
96
96
  expect(ctx.getUnsafe("note")).toBe("raw");
97
97
  } finally {
98
- if (prevHome === undefined) delete process.env.HOME;
99
- else process.env.HOME = prevHome;
98
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
99
+ else process.env.TEST_USER_HOME = prevTestHome;
100
100
  if (prevToken === undefined) delete process.env.API_TOKEN;
101
101
  else process.env.API_TOKEN = prevToken;
102
102
  rmSync(dir, { recursive: true, force: true });
@@ -105,8 +105,8 @@ describe("config/context", () => {
105
105
 
106
106
  test("EmptyAppConfigSnapshot unsafe read/write", () => {
107
107
  const dir = mkdtempSync(join(tmpdir(), "ctx-empty-unsafe-"));
108
- const prevHome = process.env.HOME;
109
- process.env.HOME = dir;
108
+ const prevTestHome = process.env.TEST_USER_HOME;
109
+ process.env.TEST_USER_HOME = dir;
110
110
  try {
111
111
  const programWithoutConfig: CliProgram = {
112
112
  key: "rawapp",
@@ -119,8 +119,8 @@ describe("config/context", () => {
119
119
  expect(empty.getUnsafe("custom")).toBe(42);
120
120
  expect(empty.readUnsafe().custom).toBe(42);
121
121
  } finally {
122
- if (prevHome === undefined) delete process.env.HOME;
123
- else process.env.HOME = prevHome;
122
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
123
+ else process.env.TEST_USER_HOME = prevTestHome;
124
124
  rmSync(dir, { recursive: true, force: true });
125
125
  }
126
126
  });
@@ -37,13 +37,13 @@ const program: CliProgram = {
37
37
 
38
38
  function withHome<T>(fn: (home: string) => T): T {
39
39
  const home = mkdtempSync(join(tmpdir(), "cfg-test-"));
40
- const prevHome = process.env.HOME;
41
- process.env.HOME = home;
40
+ const prevTestHome = process.env.TEST_USER_HOME;
41
+ process.env.TEST_USER_HOME = home;
42
42
  try {
43
43
  return fn(home);
44
44
  } finally {
45
- if (prevHome === undefined) delete process.env.HOME;
46
- else process.env.HOME = prevHome;
45
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
46
+ else process.env.TEST_USER_HOME = prevTestHome;
47
47
  rmSync(home, { recursive: true, force: true });
48
48
  }
49
49
  }
@@ -84,14 +84,14 @@ describe("isAppInstalled", () => {
84
84
  /** Tests that false when not on PATH and no local copy. */
85
85
  test("false when not on PATH and no local copy", () => {
86
86
  const home = mkdtempSync(join(tmpdir(), "argsbarg-placement-home-"));
87
- const prevHome = process.env.HOME;
88
- process.env.HOME = home;
87
+ const prevTestHome = process.env.TEST_USER_HOME;
88
+ process.env.TEST_USER_HOME = home;
89
89
  process.env.PATH = tmp;
90
90
  try {
91
91
  expect(isAppInstalled(program)).toBe(false);
92
92
  } finally {
93
- if (prevHome === undefined) delete process.env.HOME;
94
- else process.env.HOME = prevHome;
93
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
94
+ else process.env.TEST_USER_HOME = prevTestHome;
95
95
  rmSync(home, { recursive: true, force: true });
96
96
  }
97
97
  });
@@ -9,17 +9,17 @@ import { join } from "node:path";
9
9
  import { codexMcpHasServer, readCodexMcpEntry, resolveCodexConfigPath } from "./mcp-codex.ts";
10
10
 
11
11
  let home: string;
12
- let prevHome: string | undefined;
12
+ let prevTestHome: string | undefined;
13
13
 
14
14
  beforeEach(() => {
15
15
  home = mkdtempSync(join(tmpdir(), "argsbarg-codex-"));
16
- prevHome = process.env.HOME;
17
- process.env.HOME = home;
16
+ prevTestHome = process.env.TEST_USER_HOME;
17
+ process.env.TEST_USER_HOME = home;
18
18
  });
19
19
 
20
20
  afterEach(() => {
21
- if (prevHome === undefined) delete process.env.HOME;
22
- else process.env.HOME = prevHome;
21
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
22
+ else process.env.TEST_USER_HOME = prevTestHome;
23
23
  rmSync(home, { recursive: true, force: true });
24
24
  });
25
25
 
@@ -9,17 +9,17 @@ import { join } from "node:path";
9
9
  import { openclawMcpHasServer, resolveOpenclawConfigPath } from "./mcp-openclaw.ts";
10
10
 
11
11
  let home: string;
12
- let prevHome: string | undefined;
12
+ let prevTestHome: string | undefined;
13
13
 
14
14
  beforeEach(() => {
15
15
  home = mkdtempSync(join(tmpdir(), "argsbarg-openclaw-"));
16
- prevHome = process.env.HOME;
17
- process.env.HOME = home;
16
+ prevTestHome = process.env.TEST_USER_HOME;
17
+ process.env.TEST_USER_HOME = home;
18
18
  });
19
19
 
20
20
  afterEach(() => {
21
- if (prevHome === undefined) delete process.env.HOME;
22
- else process.env.HOME = prevHome;
21
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
22
+ else process.env.TEST_USER_HOME = prevTestHome;
23
23
  rmSync(home, { recursive: true, force: true });
24
24
  });
25
25
 
@@ -26,20 +26,20 @@ const fixture: CliProgram = {
26
26
  };
27
27
 
28
28
  let home: string;
29
- let prevHome: string | undefined;
29
+ let prevTestHome: string | undefined;
30
30
  let prevXdg: string | undefined;
31
31
 
32
32
  beforeEach(() => {
33
33
  home = mkdtempSync(join(tmpdir(), "argsbarg-opencode-"));
34
- prevHome = process.env.HOME;
34
+ prevTestHome = process.env.TEST_USER_HOME;
35
35
  prevXdg = process.env.XDG_CONFIG_HOME;
36
- process.env.HOME = home;
36
+ process.env.TEST_USER_HOME = home;
37
37
  delete process.env.XDG_CONFIG_HOME;
38
38
  });
39
39
 
40
40
  afterEach(() => {
41
- if (prevHome === undefined) delete process.env.HOME;
42
- else process.env.HOME = prevHome;
41
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
42
+ else process.env.TEST_USER_HOME = prevTestHome;
43
43
  if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
44
44
  else process.env.XDG_CONFIG_HOME = prevXdg;
45
45
  rmSync(home, { recursive: true, force: true });
@@ -12,17 +12,17 @@ import { printInstallStatus } from "./status.ts";
12
12
  import { resolveInstallTargetPreview } from "./target-scope.ts";
13
13
 
14
14
  let home: string;
15
- let prevHome: string | undefined;
15
+ let prevTestHome: string | undefined;
16
16
 
17
17
  beforeEach(() => {
18
18
  home = mkdtempSync(join(tmpdir(), "argsbarg-status-"));
19
- prevHome = process.env.HOME;
20
- process.env.HOME = home;
19
+ prevTestHome = process.env.TEST_USER_HOME;
20
+ process.env.TEST_USER_HOME = home;
21
21
  });
22
22
 
23
23
  afterEach(() => {
24
- if (prevHome === undefined) delete process.env.HOME;
25
- else process.env.HOME = prevHome;
24
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
25
+ else process.env.TEST_USER_HOME = prevTestHome;
26
26
  rmSync(home, { recursive: true, force: true });
27
27
  });
28
28
 
@@ -33,20 +33,20 @@ const fixture: CliProgram = {
33
33
  };
34
34
 
35
35
  let home: string;
36
- let prevHome: string | undefined;
36
+ let prevTestHome: string | undefined;
37
37
  let prevXdg: string | undefined;
38
38
 
39
39
  beforeEach(() => {
40
40
  home = mkdtempSync(join(tmpdir(), "argsbarg-configure-"));
41
- prevHome = process.env.HOME;
41
+ prevTestHome = process.env.TEST_USER_HOME;
42
42
  prevXdg = process.env.XDG_CONFIG_HOME;
43
- process.env.HOME = home;
43
+ process.env.TEST_USER_HOME = home;
44
44
  delete process.env.XDG_CONFIG_HOME;
45
45
  });
46
46
 
47
47
  afterEach(() => {
48
- if (prevHome === undefined) delete process.env.HOME;
49
- else process.env.HOME = prevHome;
48
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
49
+ else process.env.TEST_USER_HOME = prevTestHome;
50
50
  if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
51
51
  else process.env.XDG_CONFIG_HOME = prevXdg;
52
52
  rmSync(home, { recursive: true, force: true });
@@ -1212,20 +1212,20 @@ test("cliSkillInstall writes project agent skill files", () => {
1212
1212
  }
1213
1213
  });
1214
1214
 
1215
- /** CliSkillInstall global uses HOME agents skills directory. */
1216
- test("cliSkillInstall global uses HOME agents skills directory", () => {
1215
+ /** CliSkillInstall global uses TEST_USER_HOME agents skills directory. */
1216
+ test("cliSkillInstall global uses TEST_USER_HOME agents skills directory", () => {
1217
1217
  const home = mkdtempSync(join(tmpdir(), "argsbarg-home-"));
1218
- const prevHome = process.env.HOME;
1219
- process.env.HOME = home;
1218
+ const prevTestHome = process.env.TEST_USER_HOME;
1219
+ process.env.TEST_USER_HOME = home;
1220
1220
  try {
1221
1221
  const files = cliSkillInstall(nestedMcpFixture, { global: true, rimraf: true });
1222
1222
  expect(files.some((f) => f.includes(join(home, ".agents", "skills", "nested.ts")))).toBe(true);
1223
1223
  expect(existsSync(join(home, ".agents", "skills", "nested.ts", "SKILL.md"))).toBe(true);
1224
1224
  } finally {
1225
- if (prevHome === undefined) {
1226
- delete process.env.HOME;
1225
+ if (prevTestHome === undefined) {
1226
+ delete process.env.TEST_USER_HOME;
1227
1227
  } else {
1228
- process.env.HOME = prevHome;
1228
+ process.env.TEST_USER_HOME = prevTestHome;
1229
1229
  }
1230
1230
  rmSync(home, { recursive: true, force: true });
1231
1231
  }
package/src/index.ts CHANGED
@@ -84,6 +84,7 @@ export type { EcsLogEvent, LogEnrichContext } from "./log/ecs.ts";
84
84
  export { ECS_VERSION, formatEcsLine } from "./log/ecs.ts";
85
85
  export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
86
86
  export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
87
+ export { userHome } from "./paths/host.ts";
87
88
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "./runtime/cli.ts";
88
89
  export { cliErrWithHelp } from "./runtime/cli-errors.ts";
89
90
  export { isInteractiveTty } from "./utils.ts";
@@ -0,0 +1,58 @@
1
+ /*
2
+ Tests for paths/host module behavior.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
7
+ import { tmpdir, userInfo } from "node:os";
8
+ import { join } from "node:path";
9
+ import { userHome } from "./host.ts";
10
+
11
+ describe("userHome", () => {
12
+ let prevTestHome: string | undefined;
13
+
14
+ afterEach(() => {
15
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
16
+ else process.env.TEST_USER_HOME = prevTestHome;
17
+ });
18
+
19
+ test("uses TEST_USER_HOME when set", () => {
20
+ const home = mkdtempSync(join(tmpdir(), "argsbarg-test-home-"));
21
+ prevTestHome = process.env.TEST_USER_HOME;
22
+ process.env.TEST_USER_HOME = home;
23
+ try {
24
+ expect(userHome()).toBe(home);
25
+ } finally {
26
+ rmSync(home, { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ test("ignores sandboxed HOME (Homebrew post_install)", () => {
31
+ prevTestHome = process.env.TEST_USER_HOME;
32
+ delete process.env.TEST_USER_HOME;
33
+ const prevHome = process.env.HOME;
34
+ process.env.HOME = "/private/tmp/sqsp-workspaces-postinstall-20260807-fake";
35
+ try {
36
+ const resolved = userHome();
37
+ expect(resolved).not.toBe(process.env.HOME);
38
+ if (process.platform === "darwin" && process.env.USER) {
39
+ expect(resolved).toBe(`/Users/${process.env.USER}`);
40
+ expect(existsSync(resolved)).toBe(true);
41
+ } else {
42
+ expect(resolved).toBe(userInfo().homedir);
43
+ }
44
+ } finally {
45
+ if (prevHome === undefined) delete process.env.HOME;
46
+ else process.env.HOME = prevHome;
47
+ }
48
+ });
49
+
50
+ test("resolves macOS default home when TEST_USER_HOME unset", () => {
51
+ prevTestHome = process.env.TEST_USER_HOME;
52
+ delete process.env.TEST_USER_HOME;
53
+ if (process.platform !== "darwin" || !process.env.USER) return;
54
+ const expected = `/Users/${process.env.USER}`;
55
+ if (!existsSync(expected)) return;
56
+ expect(userHome()).toBe(expected);
57
+ });
58
+ });
package/src/paths/host.ts CHANGED
@@ -2,12 +2,21 @@
2
2
  Shared host path primitives for install, config, and skill modules.
3
3
  */
4
4
 
5
- import { homedir } from "node:os";
5
+ import { existsSync } from "node:fs";
6
+ import { userInfo } from "node:os";
6
7
  import { join } from "node:path";
7
8
 
8
- /** Resolves the user home directory (`$HOME` when set). */
9
+ /**
10
+ * Resolves the user home directory without depending on `$HOME`.
11
+ * This is helpful for when homebrew post-install hooks run with a temporary `$HOME`.
12
+ */
9
13
  export function userHome(): string {
10
- return process.env.HOME ?? homedir();
14
+ const user = process.env.USER ?? process.env.LOGNAME;
15
+ return (
16
+ [process.env.TEST_USER_HOME, user && `/Users/${user}`, user && `/home/${user}`, process.env.USERPROFILE].find(
17
+ (p) => p && existsSync(p),
18
+ ) ?? userInfo().homedir
19
+ );
11
20
  }
12
21
 
13
22
  /** Expands a leading `~` or `~/` in a path using {@link userHome}. */
@@ -13,8 +13,8 @@ import { mcpRequest, testProgram } from "../fixtures.ts";
13
13
  /** Tests that bootstrapAppConfig prefers host env over config file. */
14
14
  test("bootstrapAppConfig prefers host env over config file", () => {
15
15
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-env-"));
16
- const prevHome = process.env.HOME;
17
- process.env.HOME = dir;
16
+ const prevTestHome = process.env.TEST_USER_HOME;
17
+ process.env.TEST_USER_HOME = dir;
18
18
  process.env.FOO = "original";
19
19
  try {
20
20
  const p = testProgram({
@@ -36,8 +36,8 @@ test("bootstrapAppConfig prefers host env over config file", () => {
36
36
  expect(process.env.FOO).toBe("original");
37
37
  expect(process.env.BAR).toBe("bar");
38
38
  } finally {
39
- if (prevHome === undefined) delete process.env.HOME;
40
- else process.env.HOME = prevHome;
39
+ if (prevTestHome === undefined) delete process.env.TEST_USER_HOME;
40
+ else process.env.TEST_USER_HOME = prevTestHome;
41
41
  delete process.env.FOO;
42
42
  delete process.env.BAR;
43
43
  rmSync(dir, { recursive: true, force: true });
@@ -106,7 +106,7 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
106
106
  ],
107
107
  {
108
108
  script: "src/test/mcp-integration-fixture.ts",
109
- env: { HOME: dir, ARGS_TEST_SECRET: "present" },
109
+ env: { TEST_USER_HOME: dir, ARGS_TEST_SECRET: "present" },
110
110
  },
111
111
  );
112
112
  const res = responses.get(15) as {
@@ -148,7 +148,7 @@ const program = {
148
148
  await new Cli(program).run(process.argv.slice(2));
149
149
  `,
150
150
  );
151
- const env = { ...process.env, HOME: dir } as Record<string, string | undefined>;
151
+ const env = { ...process.env, TEST_USER_HOME: dir } as Record<string, string | undefined>;
152
152
  delete env.DOCS_SKIP_RUN_TOKEN;
153
153
  try {
154
154
  const proc = Bun.spawn(["bun", "run", mainPath, "docs", "cli"], {