argsbarg 6.2.0 → 6.3.0

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 (47) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +10 -12
  3. package/docs/README.md +5 -5
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +3 -3
  6. package/docs/cli-program.md +11 -13
  7. package/docs/configure.md +37 -15
  8. package/docs/developing.md +11 -7
  9. package/docs/distribution-homebrew.md +2 -2
  10. package/docs/mcp.md +3 -3
  11. package/docs/output-schema.md +1 -1
  12. package/examples/full-example/.cursor/hooks/run-tests-on-stop.ts +56 -0
  13. package/examples/full-example/.cursor/hooks.json +12 -0
  14. package/examples/full-example/AGENTS.md +75 -0
  15. package/examples/full-example/CLAUDE.md +1 -0
  16. package/examples/full-example/Formula/full-example.rb +1 -1
  17. package/examples/full-example/docs/README.md +1 -1
  18. package/examples/full-example/docs/cli-schema.json +6 -6
  19. package/examples/full-example/docs/cli.md +6 -6
  20. package/examples/full-example/docs/mcp.md +2 -2
  21. package/examples/full-example/docs/skill.md +2 -2
  22. package/examples/full-example/justfile +12 -12
  23. package/examples/full-example/scripts/formula-shared.ts +1 -1
  24. package/examples/full-example-json/.cursor/hooks/run-tests-on-stop.ts +56 -0
  25. package/examples/full-example-json/.cursor/hooks.json +12 -0
  26. package/examples/full-example-json/AGENTS.md +86 -0
  27. package/examples/full-example-json/CLAUDE.md +1 -0
  28. package/examples/full-example-json/Formula/full-example-json.rb +1 -1
  29. package/examples/full-example-json/docs/README.md +1 -1
  30. package/examples/full-example-json/docs/cli-schema.json +27 -27
  31. package/examples/full-example-json/docs/cli.md +27 -27
  32. package/examples/full-example-json/docs/mcp.md +2 -2
  33. package/examples/full-example-json/docs/skill.md +2 -2
  34. package/examples/full-example-json/justfile +12 -12
  35. package/examples/full-example-json/scripts/formula-shared.ts +1 -1
  36. package/index.d.ts +20 -4
  37. package/package.json +1 -1
  38. package/src/builtins/builtins.test.ts +7 -7
  39. package/src/builtins/configure-copy.ts +2 -2
  40. package/src/builtins/configure.ts +4 -4
  41. package/src/configure/configure.test.ts +85 -12
  42. package/src/configure/index.ts +45 -13
  43. package/src/core/types.ts +21 -4
  44. package/src/docs/docs.test.ts +2 -2
  45. package/src/docs/mcp-guide.ts +2 -2
  46. package/src/index.ts +1 -0
  47. package/src/skill/generate.ts +2 -2
@@ -12,7 +12,7 @@ import { configureConfigSubcommands } from "./config.ts";
12
12
  import {
13
13
  configureCommandDescription,
14
14
  configureCommandNotes,
15
- configureSyncOptionDescription,
15
+ configureRefreshOptionDescription,
16
16
  } from "./configure-copy.ts";
17
17
 
18
18
  /** Hidden fallback leaf for bare `configure` (interactive / flag modes). */
@@ -23,8 +23,8 @@ export function configureBuiltinOptions(root: CliProgram): CliOption[] {
23
23
  const caps = resolveCapabilities(root);
24
24
  const opts: CliOption[] = [
25
25
  {
26
- name: "sync",
27
- description: configureSyncOptionDescription(root, caps),
26
+ name: "refresh",
27
+ description: configureRefreshOptionDescription(root, caps),
28
28
  kind: CliOptionKind.Presence,
29
29
  },
30
30
  {
@@ -50,7 +50,7 @@ export function configureBuiltinOptions(root: CliProgram): CliOption[] {
50
50
  },
51
51
  {
52
52
  name: "yes",
53
- description: "Skip confirmation (required for --sync, --remove-all, --remove-config).",
53
+ description: "Skip confirmation (required for --refresh, --remove-all, --remove-config).",
54
54
  kind: CliOptionKind.Presence,
55
55
  shortName: "y",
56
56
  },
@@ -55,12 +55,12 @@ afterEach(() => {
55
55
  /** Tests for configure opts. */
56
56
  describe("configure opts", () => {
57
57
  test("validate rejects multiple modes", () => {
58
- const opts = parseConfigureOpts({ sync: "1", status: "1" });
58
+ const opts = parseConfigureOpts({ refresh: "1", status: "1" });
59
59
  expect(validateConfigureOpts(opts)).toContain("only one");
60
60
  });
61
61
 
62
- test("sync requires --yes", () => {
63
- const opts = parseConfigureOpts({ sync: "1" });
62
+ test("refresh requires --yes", () => {
63
+ const opts = parseConfigureOpts({ refresh: "1" });
64
64
  expect(validateConfigureOpts(opts)).toContain("--yes");
65
65
  });
66
66
 
@@ -92,12 +92,12 @@ describe("configure mutation summary", () => {
92
92
  expect(msg).toBe("Installed 1 artifact.");
93
93
  });
94
94
 
95
- test("sync mode uses synced verb and artifact count", () => {
95
+ test("refresh mode uses refreshed verb and artifact count", () => {
96
96
  const msg = formatConfigureMutationSummary(
97
97
  { paths: ["a", "b", "c"], installed: 3, removed: 0, configured: 0 },
98
- { sync: true },
98
+ { refresh: true },
99
99
  );
100
- expect(msg).toBe("Synced 3 artifacts.");
100
+ expect(msg).toBe("Refreshed 3 artifacts.");
101
101
  });
102
102
 
103
103
  test("remove-all uses removed verb", () => {
@@ -147,8 +147,8 @@ describe("detect installed", () => {
147
147
  });
148
148
  });
149
149
 
150
- /** Tests for sync plan. */
151
- describe("sync plan", () => {
150
+ /** Tests for refresh plan. */
151
+ describe("refresh plan", () => {
152
152
  test("buildUpdatePlan greenfield includes skill when enabled", () => {
153
153
  const paths = resolveInstallPaths(fixture);
154
154
  const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
@@ -229,19 +229,92 @@ describe("app config wizard", () => {
229
229
  });
230
230
  });
231
231
 
232
- describe("configure --sync bootstrap", () => {
232
+ describe("configure --refresh bootstrap", () => {
233
233
  test("creates config.json for apps without appConfig", async () => {
234
234
  const program: CliProgram = {
235
- key: "syncboot",
235
+ key: "refreshboot",
236
236
  version: "0.0.0",
237
- description: "Sync bootstrap test.",
237
+ description: "Refresh bootstrap test.",
238
238
  skill: { enabled: true },
239
239
  handler: () => {},
240
240
  configure: { enabled: true },
241
241
  };
242
242
  expect(appConfigFileExists(program)).toBe(false);
243
- const result = await new Cli(program).invoke(["configure", "--sync", "--yes"]);
243
+ const result = await new Cli(program).invoke(["configure", "--refresh", "--yes"]);
244
244
  expect(result.exitCode).toBe(0);
245
245
  expect(appConfigFileExists(program)).toBe(true);
246
246
  });
247
247
  });
248
+
249
+ describe("configure lifecycle hooks", () => {
250
+ test("afterRefresh runs after --refresh plan", async () => {
251
+ const calls: string[] = [];
252
+ const program: CliProgram = {
253
+ ...fixture,
254
+ key: "hookrefresh",
255
+ configure: {
256
+ afterRefresh: async (ctx) => {
257
+ calls.push(`after:${ctx.paths.mcpName}:dry=${ctx.dry}`);
258
+ },
259
+ },
260
+ };
261
+ const result = await new Cli(program).invoke(["configure", "--refresh", "--yes"]);
262
+ expect(result.exitCode).toBe(0);
263
+ expect(calls).toEqual([`after:hookrefresh:dry=false`]);
264
+ });
265
+
266
+ test("beforeRemoveAll runs before --remove-all plan", async () => {
267
+ const paths = resolveInstallPaths({ ...fixture, key: "hookremove" });
268
+ mkdirSync(paths.agentsSkillDir, { recursive: true });
269
+ writeFileSync(join(paths.agentsSkillDir, "SKILL.md"), "# test\n", "utf8");
270
+
271
+ const calls: string[] = [];
272
+ const program: CliProgram = {
273
+ ...fixture,
274
+ key: "hookremove",
275
+ configure: {
276
+ beforeRemoveAll: async (ctx) => {
277
+ calls.push(`before:${ctx.paths.skillDirName}`);
278
+ expect(existsSync(ctx.paths.agentsSkillDir)).toBe(true);
279
+ },
280
+ },
281
+ };
282
+ const result = await new Cli(program).invoke(["configure", "--remove-all", "--yes"]);
283
+ expect(result.exitCode).toBe(0);
284
+ expect(calls).toEqual(["before:hookremove"]);
285
+ expect(existsSync(paths.agentsSkillDir)).toBe(false);
286
+ });
287
+
288
+ test("hooks receive dry from --dry", async () => {
289
+ const calls: string[] = [];
290
+ const program: CliProgram = {
291
+ ...fixture,
292
+ key: "hookdry",
293
+ configure: {
294
+ afterRefresh: (ctx) => {
295
+ calls.push(`dry=${ctx.dry}`);
296
+ },
297
+ },
298
+ };
299
+ const result = await new Cli(program).invoke(["configure", "--refresh", "--yes", "--dry"]);
300
+ expect(result.exitCode).toBe(0);
301
+ expect(calls).toEqual(["dry=true"]);
302
+ });
303
+
304
+ test("beforeRemoveAll is not called for --remove-config", async () => {
305
+ let called = false;
306
+ const program: CliProgram = {
307
+ ...fixture,
308
+ key: "hookcfg",
309
+ appConfig: { entries: { token: { description: "Token." } } },
310
+ configure: {
311
+ beforeRemoveAll: () => {
312
+ called = true;
313
+ },
314
+ },
315
+ };
316
+ const result = await new Cli(program).invoke(["configure", "--remove-config", "--yes"]);
317
+ expect(result.exitCode).toBe(0);
318
+ expect(called).toBe(false);
319
+ });
320
+ });
@@ -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 {
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 [.agents protocol](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,
@@ -152,9 +152,9 @@ function buildSkillMd(root: CliProgram, dirName: string): string {
152
152
  "",
153
153
  "## Install location",
154
154
  "",
155
- "Install follows the [.agents protocol](https://dotagentsprotocol.com/):",
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:",