argsbarg 6.3.0 → 6.3.2

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.3.2] - 2026-08-07
11
+
12
+
13
+ ## [6.3.1] - 2026-08-07
14
+
15
+
10
16
  ## [6.3.0] - 2026-08-07
11
17
 
12
18
  ### Added
@@ -14,10 +20,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
20
  - **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
21
  - **`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
22
 
23
+ ### Fixed
24
+
25
+ - **`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.
26
+
17
27
  ### Changed
18
28
 
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.
29
+ - **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.
30
+ - **Copy-template justfiles** — `install-local` depends on `uninstall` then `build` (`brew reinstall || brew install --force`). Dropped `uninstall-artifacts`, `uninstall-formula`, `uninstall-release`, and `uninstall-release-tap` (use `uninstall` + `uninstall-config`). Brew recipes prefix `HOMEBREW_NO_ASK=1`.
21
31
 
22
32
  ## [6.2.2] - 2026-08-07
23
33
 
@@ -915,7 +925,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
915
925
  - 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
926
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
917
927
 
918
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.3.0...HEAD
928
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.3.2...HEAD
929
+ [6.3.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.3.2
930
+ [6.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.3.1
919
931
  [6.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.3.0
920
932
  [6.2.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.2.2
921
933
  [6.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.2.1
package/docs/configure.md CHANGED
@@ -32,10 +32,10 @@ To remove app config only (keep skills/MCP), run `configure --remove-config --ye
32
32
 
33
33
  ```bash
34
34
  just build
35
- just install-local # same formula as production; temporary file:// URL during brew install (`just install` is an alias)
35
+ just install-local # uninstall, then build + 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: `install-local` runs `uninstall` first (keg + untap; formula hook runs `configure --remove-all`), then stages the dev formula and installs. `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
 
@@ -116,7 +116,7 @@ ArgsBarg provides an optimized workflow for developers to build, package, and te
116
116
  # 1. Build the local release binary
117
117
  just build
118
118
 
119
- # 2. Stage and install the formula locally (bypasses GitHub, uses file://)
119
+ # 2. Uninstall any existing formula/tap, stage and install locally (bypasses GitHub, uses file://)
120
120
  just install-local
121
121
 
122
122
  # 3. Swap updated binaries quickly during tight edit cycles
@@ -130,9 +130,10 @@ just uninstall
130
130
 
131
131
  To ensure you test the exact formula that will be shipped to production, `just install-local` runs:
132
132
 
133
- 1. `bun scripts/dev-formula.ts install` — Safely backs up your production formula and writes a temporary local dev formula using a `file://` URL pointing to your build directory.
134
- 2. `brew install --formula <tap>/{key}` — Installs the package locally using Homebrew.
135
- 3. `bun scripts/dev-formula.ts reset` — Automatically restores your production formula on disk.
133
+ 1. `just uninstall` — Remove any existing keg and untap (formula `uninstall` hook runs `configure --remove-all`).
134
+ 2. `bun scripts/dev-formula.ts install` — Safely backs up your production formula and writes a temporary local dev formula using a `file://` URL pointing to your build directory.
135
+ 3. `brew reinstall || brew install --force` — Installs the package locally using Homebrew.
136
+ 4. `bun scripts/dev-formula.ts reset` — Automatically restores your production formula on disk.
136
137
 
137
138
  ---
138
139
 
@@ -66,17 +66,12 @@ 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
- install-local: build
75
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
70
+ install-local: uninstall build
76
71
  mkdir -p {{tap_parent}}
77
72
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
78
73
  bun scripts/dev-formula.ts install
79
- brew install --force --formula {{tap}}/{{cli_key}}
74
+ HOMEBREW_NO_ASK=1 brew reinstall --formula {{tap}}/{{cli_key}} || HOMEBREW_NO_ASK=1 brew install --force --formula {{tap}}/{{cli_key}}
80
75
  bun scripts/dev-formula.ts reset
81
76
  @echo ""
82
77
  @echo "Next: {{cli_key}} configure"
@@ -121,10 +116,10 @@ release *ARGS:
121
116
 
122
117
  # Install release formula from tap and run formula test
123
118
  test-release:
124
- NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
119
+ HOMEBREW_NO_ASK=1 brew untap {{tap}} 2>/dev/null || true
125
120
  mkdir -p {{tap_parent}}
126
121
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
127
- NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
122
+ HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
128
123
  brew install --formula {{tap}}/{{cli_key}}
129
124
  brew test {{cli_key}}
130
125
 
@@ -132,28 +127,12 @@ test-release:
132
127
  typecheck:
133
128
  bun run tsc --noEmit
134
129
 
135
- # Undo just install-local: agent artifacts first, then formula
136
- uninstall: uninstall-artifacts uninstall-formula
137
-
138
- # Remove agent artifacts only (skills, MCP)
139
- uninstall-artifacts:
140
- just run configure --remove-all --yes
130
+ # Undo dev/Homebrew install (formula uninstall hook runs configure --remove-all)
131
+ uninstall:
132
+ @HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
133
+ @HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}}-local 2>/dev/null || true
134
+ @HOMEBREW_NO_ASK=1 brew untap {{tap}} 2>/dev/null || true
141
135
 
142
136
  # Remove app config file only
143
137
  uninstall-config:
144
138
  just run configure --remove-config --yes
145
-
146
- # Remove formula and untap
147
- uninstall-formula:
148
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
149
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}}-local 2>/dev/null || true
150
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
151
-
152
- # Remove release formula (does not untap)
153
- uninstall-release:
154
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
155
-
156
- # Remove release formula and untap
157
- uninstall-release-tap:
158
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
159
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
@@ -76,17 +76,12 @@ 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
- install-local: build
85
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
80
+ install-local: uninstall build
86
81
  mkdir -p {{tap_parent}}
87
82
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
88
83
  bun scripts/dev-formula.ts install
89
- brew install --force --formula {{tap}}/{{cli_key}}
84
+ HOMEBREW_NO_ASK=1 brew reinstall --formula {{tap}}/{{cli_key}} || HOMEBREW_NO_ASK=1 brew install --force --formula {{tap}}/{{cli_key}}
90
85
  bun scripts/dev-formula.ts reset
91
86
  @echo ""
92
87
  @echo "Next: {{cli_key}} configure"
@@ -133,10 +128,10 @@ release *ARGS:
133
128
 
134
129
  # Install release formula from tap and run formula test
135
130
  test-release:
136
- NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
131
+ HOMEBREW_NO_ASK=1 brew untap {{tap}} 2>/dev/null || true
137
132
  mkdir -p {{tap_parent}}
138
133
  ln -sfn '{{justfile_directory()}}' {{tap_path}}
139
- NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
134
+ HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
140
135
  brew install --formula {{tap}}/{{cli_key}}
141
136
  brew test {{cli_key}}
142
137
 
@@ -144,28 +139,12 @@ test-release:
144
139
  typecheck:
145
140
  bun run tsc --noEmit
146
141
 
147
- # Undo just install-local: agent artifacts first, then formula
148
- uninstall: uninstall-artifacts uninstall-formula
149
-
150
- # Remove agent artifacts only (skills, MCP)
151
- uninstall-artifacts:
152
- just run configure --remove-all --yes
142
+ # Undo dev/Homebrew install (formula uninstall hook runs configure --remove-all)
143
+ uninstall:
144
+ @HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
145
+ @HOMEBREW_NO_ASK=1 brew uninstall --formula {{tap}}/{{cli_key}}-local 2>/dev/null || true
146
+ @HOMEBREW_NO_ASK=1 brew untap {{tap}} 2>/dev/null || true
153
147
 
154
148
  # Remove app config file only
155
149
  uninstall-config:
156
150
  just run configure --remove-config --yes
157
-
158
- # Remove formula and untap
159
- uninstall-formula:
160
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
161
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}}-local 2>/dev/null || true
162
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
163
-
164
- # Remove release formula (does not untap)
165
- uninstall-release:
166
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
167
-
168
- # Remove release formula and untap
169
- uninstall-release-tap:
170
- @NONINTERACTIVE=1 brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
171
- @NONINTERACTIVE=1 brew untap {{tap}} 2>/dev/null || true
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.2",
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"], {