argsbarg 6.2.2 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +2 -2
  3. package/docs/README.md +2 -2
  4. package/docs/ai-skills.md +7 -7
  5. package/docs/bundled-docs.md +2 -2
  6. package/docs/cli-program.md +4 -4
  7. package/docs/configure.md +36 -14
  8. package/docs/developing.md +7 -3
  9. package/docs/distribution-homebrew.md +2 -2
  10. package/docs/mcp.md +3 -3
  11. package/examples/full-example/.cursor/hooks/run-tests-on-stop.ts +56 -0
  12. package/examples/full-example/.cursor/hooks.json +12 -0
  13. package/examples/full-example/Formula/full-example.rb +1 -1
  14. package/examples/full-example/docs/cli-schema.json +6 -6
  15. package/examples/full-example/docs/cli.md +6 -6
  16. package/examples/full-example/docs/mcp.md +2 -2
  17. package/examples/full-example/docs/skill.md +1 -1
  18. package/examples/full-example/justfile +10 -14
  19. package/examples/full-example/scripts/formula-shared.ts +1 -1
  20. package/examples/full-example-json/.cursor/hooks/run-tests-on-stop.ts +56 -0
  21. package/examples/full-example-json/.cursor/hooks.json +12 -0
  22. package/examples/full-example-json/Formula/full-example-json.rb +1 -1
  23. package/examples/full-example-json/docs/cli-schema.json +27 -27
  24. package/examples/full-example-json/docs/cli.md +27 -27
  25. package/examples/full-example-json/docs/mcp.md +2 -2
  26. package/examples/full-example-json/docs/skill.md +1 -1
  27. package/examples/full-example-json/justfile +10 -14
  28. package/examples/full-example-json/scripts/formula-shared.ts +1 -1
  29. package/index.d.ts +25 -4
  30. package/package.json +1 -1
  31. package/src/builtins/builtins.test.ts +7 -7
  32. package/src/builtins/config.test.ts +16 -16
  33. package/src/builtins/configure-copy.ts +2 -2
  34. package/src/builtins/configure.ts +4 -4
  35. package/src/config/context.test.ts +12 -12
  36. package/src/config/file.test.ts +4 -4
  37. package/src/configure/artifacts/binary-placement.test.ts +4 -4
  38. package/src/configure/artifacts/mcp-codex.test.ts +5 -5
  39. package/src/configure/artifacts/mcp-openclaw.test.ts +5 -5
  40. package/src/configure/artifacts/mcp-opencode.test.ts +5 -5
  41. package/src/configure/artifacts/status.test.ts +5 -5
  42. package/src/configure/configure.test.ts +90 -17
  43. package/src/configure/index.ts +45 -13
  44. package/src/core/parse.test.ts +7 -7
  45. package/src/core/types.ts +21 -4
  46. package/src/docs/docs.test.ts +2 -2
  47. package/src/docs/mcp-guide.ts +2 -2
  48. package/src/index.ts +2 -0
  49. package/src/paths/host.test.ts +58 -0
  50. package/src/paths/host.ts +12 -3
  51. package/src/skill/generate.ts +1 -1
  52. package/src/test/integration/config.test.ts +6 -6
@@ -4,10 +4,10 @@ Interactive and automated `configure` command orchestration (agent artifacts and
4
4
 
5
5
  import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
6
6
  import { ensureAppConfigFile } from "../config/file.ts";
7
- import type { CliProgram } from "../core/types.ts";
7
+ import type { CliProgram, ConfigureHookContext } from "../core/types.ts";
8
8
  import { resolveCapabilities } from "../runtime/capabilities.ts";
9
9
  import { cliSkillInstall, isAgentSkillActionKind } from "../skill/install.ts";
10
- import { displayInstallPath, resolveInstallPaths } from "./artifacts/paths.ts";
10
+ import { displayInstallPath, type InstallPaths, resolveInstallPaths } from "./artifacts/paths.ts";
11
11
  import { buildInstallPlan, buildUpdatePlan } from "./artifacts/plan.ts";
12
12
  import {
13
13
  installErr,
@@ -38,7 +38,7 @@ export function appConfigHasEntries(program: CliProgram): boolean {
38
38
 
39
39
  /** Parsed flags for the top-level `configure` built-in. */
40
40
  export interface ConfigureOpts {
41
- sync?: boolean;
41
+ refresh?: boolean;
42
42
  removeAll?: boolean;
43
43
  removeConfig?: boolean;
44
44
  status?: boolean;
@@ -51,7 +51,7 @@ export interface ConfigureOpts {
51
51
  export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
52
52
  const flag = (name: string) => raw[name] === "1";
53
53
  return {
54
- sync: flag("sync"),
54
+ refresh: flag("refresh"),
55
55
  removeAll: flag("remove-all"),
56
56
  removeConfig: flag("remove-config"),
57
57
  status: flag("status"),
@@ -63,15 +63,15 @@ export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
63
63
 
64
64
  /** Returns an error message when configure flags are inconsistent; otherwise null. */
65
65
  export function validateConfigureOpts(opts: ConfigureOpts): string | null {
66
- const flags = [opts.sync, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
66
+ const flags = [opts.refresh, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
67
67
  if (flags.length > 1) {
68
- return "Use only one of --sync, --remove-all, --remove-config, or --status.";
68
+ return "Use only one of --refresh, --remove-all, --remove-config, or --status.";
69
69
  }
70
70
  if (opts.json) {
71
71
  opts.yes = true;
72
72
  }
73
- if ((opts.sync || opts.removeAll || opts.removeConfig) && !opts.yes) {
74
- return "--yes is required with --sync, --remove-all, or --remove-config.";
73
+ if ((opts.refresh || opts.removeAll || opts.removeConfig) && !opts.yes) {
74
+ return "--yes is required with --refresh, --remove-all, or --remove-config.";
75
75
  }
76
76
  return null;
77
77
  }
@@ -81,7 +81,7 @@ function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
81
81
  if (opts.status) {
82
82
  return { status: true, yes: opts.yes, dry: opts.dry, json: opts.json };
83
83
  }
84
- if (opts.sync) {
84
+ if (opts.refresh) {
85
85
  return { reinstall: true, yes: true, dry: opts.dry, json: opts.json };
86
86
  }
87
87
  if (opts.removeAll) {
@@ -141,10 +141,10 @@ export function formatConfigureMutationSummary(summary: ConfigureMutationSummary
141
141
  return n === 1 ? "Removed 1 artifact." : `Removed ${n} artifacts.`;
142
142
  }
143
143
 
144
- if (opts.sync) {
144
+ if (opts.refresh) {
145
145
  const n = summary.installed;
146
146
  if (n === 0) return null;
147
- return n === 1 ? "Synced 1 artifact." : `Synced ${n} artifacts.`;
147
+ return n === 1 ? "Refreshed 1 artifact." : `Refreshed ${n} artifacts.`;
148
148
  }
149
149
 
150
150
  const parts: string[] = [];
@@ -184,6 +184,29 @@ function runPlanAction(
184
184
  return action.run();
185
185
  }
186
186
 
187
+ function configureHookContext(root: CliProgram, paths: InstallPaths, dry: boolean): ConfigureHookContext {
188
+ return {
189
+ program: root,
190
+ dry,
191
+ paths: {
192
+ agentsSkillDir: paths.agentsSkillDir,
193
+ agentsMcpPath: paths.agentsMcpPath,
194
+ mcpName: paths.mcpName,
195
+ skillDirName: paths.skillDirName,
196
+ },
197
+ };
198
+ }
199
+
200
+ async function runConfigureLifecycleHook(
201
+ hook: ((ctx: ConfigureHookContext) => void | Promise<void>) | undefined,
202
+ root: CliProgram,
203
+ paths: InstallPaths,
204
+ dry: boolean,
205
+ ): Promise<void> {
206
+ if (!hook) return;
207
+ await hook(configureHookContext(root, paths, dry));
208
+ }
209
+
187
210
  /** Runs install or uninstall actions and collects changed paths. */
188
211
  function executePlan(
189
212
  root: CliProgram,
@@ -253,7 +276,7 @@ function actionsForTarget(
253
276
  /** Walks enabled targets with per-target prompts (TTY required). */
254
277
  async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): Promise<ConfigureMutationSummary> {
255
278
  if (!process.stdin.isTTY) {
256
- throw new Error("Interactive configure requires a TTY. Use flags such as --sync --yes.");
279
+ throw new Error("Interactive configure requires a TTY. Use flags such as --refresh --yes.");
257
280
  }
258
281
 
259
282
  writeInteractiveInstallIntro(root);
@@ -319,6 +342,10 @@ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Pro
319
342
 
320
343
  const summary = emptyMutationSummary();
321
344
 
345
+ if (opts.removeAll) {
346
+ await runConfigureLifecycleHook(root.configure?.beforeRemoveAll, root, paths, !!installOpts.dry);
347
+ }
348
+
322
349
  if (installOpts.reinstall && !installOpts.uninstall) {
323
350
  const bootstrapped = ensureAppConfigFile(root, !!installOpts.dry);
324
351
  if (bootstrapped) {
@@ -345,6 +372,11 @@ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Pro
345
372
  }
346
373
 
347
374
  mergeMutationSummary(summary, executePlan(root, actions, installOpts, true));
375
+
376
+ if (opts.refresh) {
377
+ await runConfigureLifecycleHook(root.configure?.afterRefresh, root, paths, !!installOpts.dry);
378
+ }
379
+
348
380
  return summary;
349
381
  }
350
382
 
@@ -357,7 +389,7 @@ export async function cliConfigure(root: CliProgram, rawOpts: Record<string, str
357
389
  process.exit(1);
358
390
  }
359
391
 
360
- const isInteractive = !opts.sync && !opts.removeAll && !opts.removeConfig && !opts.status;
392
+ const isInteractive = !opts.refresh && !opts.removeAll && !opts.removeConfig && !opts.status;
361
393
 
362
394
  let summary = emptyMutationSummary();
363
395
  try {
@@ -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/core/types.ts CHANGED
@@ -406,16 +406,33 @@ export interface CliCompletionConfig {
406
406
 
407
407
  /** Opt-in agent skill install to `~/.agents/skills/<key>/` (default: disabled). */
408
408
  export interface CliSkillConfig {
409
- /** When `true`, install and sync the agent skill via `configure --sync`. Default false when omitted. */
409
+ /** When `true`, install and refresh the agent skill via `configure --refresh`. Default false when omitted. */
410
410
  enabled?: boolean;
411
411
  }
412
412
 
413
+ /** Context for {@link CliConfigureConfig} lifecycle hooks. */
414
+ export interface ConfigureHookContext {
415
+ program: CliProgram;
416
+ /** When true, the configure run is `--dry` (hook should not write files). */
417
+ dry: boolean;
418
+ paths: {
419
+ agentsSkillDir: string;
420
+ agentsMcpPath: string;
421
+ mcpName: string;
422
+ skillDirName: string;
423
+ };
424
+ }
425
+
413
426
  /** @experimental */
414
427
  export interface CliConfigureConfig {
415
428
  /** When `false`, hide/disable `configure` (default: enabled). */
416
429
  enabled?: boolean;
417
- /** Per-artifact gates for configure sync and interactive wizard. See {@link resolveEffectiveInstallTargets}. */
430
+ /** Per-artifact gates for configure refresh and interactive wizard. See {@link resolveEffectiveInstallTargets}. */
418
431
  targets?: CliConfigureTargets;
432
+ /** Runs after framework artifacts are installed/refreshed (`configure --refresh`). */
433
+ afterRefresh?: (ctx: ConfigureHookContext) => void | Promise<void>;
434
+ /** Runs before framework artifacts are removed (`configure --remove-all`). */
435
+ beforeRemoveAll?: (ctx: ConfigureHookContext) => void | Promise<void>;
419
436
  }
420
437
 
421
438
  /** Boolean or structured gate for one install artifact. */
@@ -424,7 +441,7 @@ export type InstallTargetSpec =
424
441
  | {
425
442
  /** When false, artifact is never installed (even with scoped CLI flags). Default true. */
426
443
  enabled?: boolean;
427
- /** When true, included in `configure --sync`. Default varies by key. */
444
+ /** When true, included in `configure --refresh`. Default varies by key. */
428
445
  includedInAll?: boolean;
429
446
  };
430
447
 
@@ -437,7 +454,7 @@ export interface ResolvedInstallTarget {
437
454
  export interface CliConfigureTargets {
438
455
  /** App binary status only (Homebrew PATH); no self-install. */
439
456
  app?: InstallTargetSpec;
440
- /** App config: interactive wizard step in `configure`. Default not in sync. */
457
+ /** App config: interactive wizard step in `configure`. Default not in refresh. */
441
458
  configure?: InstallTargetSpec;
442
459
  }
443
460
 
@@ -153,7 +153,7 @@ test("docs mcp when MCP enabled", async () => {
153
153
  expect(result.stdout).toContain("MCP server (myapp)");
154
154
  expect(result.stdout).toContain("myapp mcp");
155
155
  expect(result.stdout).toContain("claude_desktop_config.json");
156
- expect(result.stdout).toContain("configure --sync --yes");
156
+ expect(result.stdout).toContain("configure --refresh --yes");
157
157
  });
158
158
 
159
159
  test("docs rejects unknown subcommand", async () => {
@@ -304,7 +304,7 @@ test("generateMcpGuide includes schema URI and .agents install", () => {
304
304
  expect(guide).toContain("claude_desktop_config.json");
305
305
  expect(guide).toContain("## Installation");
306
306
  expect(guide).toContain("## Running directly");
307
- expect(guide).toContain("configure --sync");
307
+ expect(guide).toContain("configure --refresh");
308
308
  expect(guide).toContain("dotagentsprotocol.com");
309
309
  expect(guide).not.toContain("OpenAI Codex");
310
310
  });
@@ -81,7 +81,7 @@ export function generateMcpGuide(root: CliProgram): string {
81
81
  "",
82
82
  "### `.agents` auto-install",
83
83
  "",
84
- "When `mcpServer.enabled` is set, `configure --sync` merges this server into `~/.agents/mcp.json` per the https://dotagentsprotocol.com.",
84
+ "When `mcpServer.enabled` is set, `configure --refresh` merges this server into `~/.agents/mcp.json` per the https://dotagentsprotocol.com.",
85
85
  "",
86
86
  ];
87
87
 
@@ -93,7 +93,7 @@ export function generateMcpGuide(root: CliProgram): string {
93
93
 
94
94
  lines.push(
95
95
  "```bash",
96
- `${root.key} configure --sync --yes`,
96
+ `${root.key} configure --refresh --yes`,
97
97
  "```",
98
98
  "",
99
99
  "Writes or updates `~/.agents/mcp.json` with a `mcpServers` entry for this app.",
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export type {
53
53
  CliRespondBody,
54
54
  CliRespondOptions,
55
55
  CliSkillConfig,
56
+ ConfigureHookContext,
56
57
  ErrorHookContext,
57
58
  InstallTargetSpec,
58
59
  InvokeFailureKind,
@@ -83,6 +84,7 @@ export type { EcsLogEvent, LogEnrichContext } from "./log/ecs.ts";
83
84
  export { ECS_VERSION, formatEcsLine } from "./log/ecs.ts";
84
85
  export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
85
86
  export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
87
+ export { userHome } from "./paths/host.ts";
86
88
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "./runtime/cli.ts";
87
89
  export { cliErrWithHelp } from "./runtime/cli-errors.ts";
88
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}. */
@@ -154,7 +154,7 @@ function buildSkillMd(root: CliProgram, dirName: string): string {
154
154
  "",
155
155
  "Install follows the https://dotagentsprotocol.com:",
156
156
  "",
157
- `- Auto-install: \`${root.key} configure --sync --yes\` when \`skill.enabled\` → \`~/.agents/skills/${dirName}/\``,
157
+ `- Auto-install: \`${root.key} configure --refresh --yes\` when \`skill.enabled\` → \`~/.agents/skills/${dirName}/\``,
158
158
  `- Cursor and most coding agents read \`~/.agents/skills/\` natively`,
159
159
  "",
160
160
  "**Claude Code (manual):** symlink or copy into Claude's skill directory:",
@@ -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"], {