argsbarg 4.0.3 → 4.1.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 (99) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +6 -6
  3. package/docs/ai-skills.md +9 -4
  4. package/docs/bundled-docs.md +1 -0
  5. package/docs/cli-program.md +4 -4
  6. package/docs/config-schema.md +1 -4
  7. package/docs/developing.md +1 -1
  8. package/docs/install.md +160 -39
  9. package/docs/mcp.md +38 -11
  10. package/docs/templates/cursor/rules/cli-program.mdc +1 -1
  11. package/examples/config-app/program.ts +0 -3
  12. package/examples/consumer-app/README.md +1 -1
  13. package/examples/consumer-app/src/program.ts +5 -13
  14. package/examples/mcp-test.ts +14 -24
  15. package/index.d.ts +60 -5
  16. package/package.json +1 -1
  17. package/src/builtins/builtins.test.ts +20 -4
  18. package/src/builtins/config.test.ts +31 -25
  19. package/src/builtins/config.ts +4 -3
  20. package/src/builtins/install.ts +50 -57
  21. package/src/builtins/mcp.ts +1 -1
  22. package/src/capabilities.ts +4 -4
  23. package/src/cli.ts +2 -0
  24. package/src/config/bootstrap.ts +167 -66
  25. package/src/config/context.test.ts +22 -36
  26. package/src/config/context.ts +5 -4
  27. package/src/config/file.test.ts +66 -56
  28. package/src/config/file.ts +33 -25
  29. package/src/config/resolve.test.ts +25 -1
  30. package/src/config/resolve.ts +43 -8
  31. package/src/config.integration.test.ts +17 -10
  32. package/src/docs/api-guide.test.ts +1 -1
  33. package/src/docs/docs.test.ts +1 -1
  34. package/src/docs/mcp-guide.ts +15 -4
  35. package/src/docs/mcp-resources.test.ts +63 -0
  36. package/src/docs/mcp-resources.ts +68 -0
  37. package/src/hidden-mcpb.test.ts +41 -1
  38. package/src/index.ts +4 -0
  39. package/src/install/{binary.ts → app.ts} +13 -13
  40. package/src/install/bootstrap.ts +22 -0
  41. package/src/install/detect-installed.ts +2 -97
  42. package/src/install/index.ts +187 -110
  43. package/src/install/install-validate.test.ts +61 -0
  44. package/src/install/install.test.ts +138 -42
  45. package/src/install/mcp-openclaw.test.ts +40 -0
  46. package/src/install/mcp-openclaw.ts +106 -0
  47. package/src/install/normalize.ts +35 -0
  48. package/src/install/paths.ts +27 -13
  49. package/src/install/plan.ts +30 -259
  50. package/src/install/shell.ts +2 -2
  51. package/src/install/status.test.ts +85 -0
  52. package/src/install/status.ts +22 -9
  53. package/src/install/target-base.ts +93 -0
  54. package/src/install/target-detect.ts +20 -0
  55. package/src/install/target-effective.ts +131 -0
  56. package/src/install/target-mcp-cli.ts +149 -0
  57. package/src/install/target-mcp-json.ts +130 -0
  58. package/src/install/target-plan-build.ts +67 -0
  59. package/src/install/target-registry.ts +57 -0
  60. package/src/install/target-scope.ts +266 -0
  61. package/src/install/target-skill.ts +104 -0
  62. package/src/install/target-types.ts +145 -0
  63. package/src/install/targets/app.ts +69 -0
  64. package/src/install/targets/chatgpt-mcp.ts +12 -0
  65. package/src/install/targets/claude-code-mcp.ts +15 -0
  66. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  67. package/src/install/targets/claude-skill.ts +16 -0
  68. package/src/install/targets/codex-mcp.ts +25 -0
  69. package/src/install/targets/codex-skill.ts +14 -0
  70. package/src/install/targets/completions.ts +133 -0
  71. package/src/install/targets/configure.ts +59 -0
  72. package/src/install/targets/cursor-mcp.ts +15 -0
  73. package/src/install/targets/cursor-skill.ts +16 -0
  74. package/src/install/targets/index.ts +53 -0
  75. package/src/install/targets/openclaw-mcp.ts +25 -0
  76. package/src/install/targets/openclaw-skill.ts +17 -0
  77. package/src/install/targets/opencode-mcp.ts +101 -0
  78. package/src/install/targets/opencode-skill.ts +15 -0
  79. package/src/install/targets.test.ts +136 -0
  80. package/src/install/uninstall.ts +16 -152
  81. package/src/install/update.test.ts +17 -2
  82. package/src/install/update.ts +2 -5
  83. package/src/invoke.test.ts +7 -1
  84. package/src/mcp/bundle.ts +16 -4
  85. package/src/mcp/claude.test.ts +23 -4
  86. package/src/mcp/claude.ts +18 -13
  87. package/src/mcp/tools.ts +4 -1
  88. package/src/mcp/zip.test.ts +17 -0
  89. package/src/mcp/zip.ts +62 -9
  90. package/src/mcp.integration.test.ts +18 -1
  91. package/src/parse.test.ts +57 -4
  92. package/src/paths/host.ts +11 -11
  93. package/src/paths/remove-empty-dir.ts +13 -0
  94. package/src/skill/generate.ts +89 -5
  95. package/src/skill/hint.ts +5 -0
  96. package/src/skill/install.ts +33 -6
  97. package/src/skill/naming.ts +28 -0
  98. package/src/types.ts +65 -4
  99. package/src/validate.ts +79 -4
@@ -8,12 +8,11 @@ import {
8
8
  CliOptionKind,
9
9
  type CliProgram,
10
10
  } from "argsbarg";
11
+ import readmeText from "../README.md" with { type: "text" };
11
12
  import { APP_CONFIG_JSON_SCHEMA } from "../schemas/configSchemas.ts";
12
13
  import { STATUS_JSON_OUTPUT_SCHEMA } from "../schemas/outputSchemas.ts";
13
14
  import type { StatusJsonOutput } from "./commands/status/types.ts";
14
15
 
15
- const configPath = process.env.CONSUMER_APP_CONFIG_FILE;
16
-
17
16
  const configSchema = {
18
17
  apiToken: {
19
18
  description: "Create at https://example.com/settings/tokens",
@@ -38,7 +37,6 @@ export const program = {
38
37
  version: "1.0.0",
39
38
  description: "Argsbarg kitchen-sink reference — all builtins, schemagen, ctx.appConfig.",
40
39
  appConfig: {
41
- ...(configPath ? { path: configPath } : {}),
42
40
  jsonSchema: APP_CONFIG_JSON_SCHEMA,
43
41
  entries: configSchema,
44
42
  } satisfies CliAppConfig,
@@ -46,23 +44,17 @@ export const program = {
46
44
  enabled: true,
47
45
  topics: {
48
46
  readme: {
49
- text: "# consumer-app\n\nKitchen-sink argsbarg reference. Copy this layout into a new CLI repo.\n",
47
+ text: readmeText,
50
48
  },
51
49
  },
52
50
  },
53
51
  mcpServer: {
54
52
  enabled: true,
55
- resources: [
56
- {
57
- uri: "consumer-app://readme",
58
- name: "readme",
59
- description: "Bundled readme topic.",
60
- mimeType: "text/plain",
61
- load: () => "# consumer-app\n\nKitchen-sink reference.\n",
62
- },
63
- ],
53
+ mcpd: true,
54
+ claudePlugin: true,
64
55
  },
65
56
  install: {
57
+ // Defaults: app + completions + configure; agentIntegration picks skill vs MCP for --all.
66
58
  updateGetLatest: async () => ({
67
59
  path: process.execPath,
68
60
  version: "1.0.0",
@@ -5,34 +5,18 @@ MCP test fixture for subprocess integration tests only.
5
5
 
6
6
  import { Cli, CliOptionKind, type CliProgram } from "../src/index.ts";
7
7
 
8
- const configPath = process.env.ARGS_TEST_CONFIG_FILE;
9
-
10
8
  const program = {
11
9
  key: "mcp-test",
12
10
  version: "0.0.0-test",
13
11
  description: "MCP integration test fixture.",
14
- ...(configPath
15
- ? {
16
- appConfig: {
17
- path: configPath,
18
- entries: {
19
- argsTestSecret: {
20
- description: "Test secret for integration tests.",
21
- env: "ARGS_TEST_SECRET",
22
- },
23
- },
24
- },
25
- }
26
- : {
27
- appConfig: {
28
- entries: {
29
- argsTestSecret: {
30
- description: "Test secret for integration tests.",
31
- env: "ARGS_TEST_SECRET",
32
- },
33
- },
34
- },
35
- }),
12
+ appConfig: {
13
+ entries: {
14
+ argsTestSecret: {
15
+ description: "Test secret for integration tests.",
16
+ env: "ARGS_TEST_SECRET",
17
+ },
18
+ },
19
+ },
36
20
  mcpServer: {
37
21
  enabled: true,
38
22
  resources: [
@@ -45,6 +29,12 @@ const program = {
45
29
  },
46
30
  ],
47
31
  },
32
+ docs: {
33
+ enabled: true,
34
+ topics: {
35
+ readme: { text: "# MCP test readme\n" },
36
+ },
37
+ },
48
38
  commands: [
49
39
  {
50
40
  key: "echo-env",
package/index.d.ts CHANGED
@@ -22,7 +22,7 @@ declare class AppConfigSnapshot {
22
22
  require(key: string): unknown;
23
23
  set(key: string, value: unknown): void;
24
24
  read(): ResolvedConfig;
25
- /** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
25
+ /** Resolved absolute path to the app JSON config file (`~/.local/lib/<key>/config.json`). */
26
26
  get path(): string;
27
27
  /** Resolved absolute directory containing the config file. */
28
28
  get dir(): string;
@@ -194,6 +194,10 @@ export interface CliMcpBundleConfig {
194
194
  export interface CliMcpServerConfig {
195
195
  /** When `true`, enables the `mcp` built-in and MCP stdio server. */
196
196
  enabled: boolean;
197
+ /** When `true`, `mcp bundle` writes `dist/<key>.mcpb` for Claude Desktop. Default false. */
198
+ mcpd?: boolean;
199
+ /** When `true`, `mcp bundle` also writes `dist/claude-plugin/<name>.zip`. Default false. */
200
+ claudePlugin?: boolean;
197
201
  /** Resource URI for schema export (default: `<sanitized root key>://schema`). */
198
202
  schemaResourceUri?: string;
199
203
  /**
@@ -281,8 +285,6 @@ export interface CliAppConfigEntry {
281
285
  * App configuration block on the program root ({@link CliProgram.appConfig}).
282
286
  */
283
287
  export interface CliAppConfig {
284
- /** Default: `~/.config/<sanitized-key>/config` (or `%APPDATA%/<key>/config` on Windows). */
285
- path?: string;
286
288
  /** Built-in `config get` / `config set`. Default: enabled when `appConfig` is set. */
287
289
  commands?: boolean | {
288
290
  enabled?: boolean;
@@ -296,14 +298,67 @@ export interface CliAppConfig {
296
298
  export interface CliInstallConfig {
297
299
  /** When `false`, hide/disable `install` (default: enabled). */
298
300
  enabled?: boolean;
299
- /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
300
- prefix?: string;
301
+ /**
302
+ * Default agent integration for full install (`install --all`).
303
+ * - `'mcp'` when `mcpServer.enabled` (default): MCP targets in `--all`; paired skills excluded.
304
+ * - `'skill'` when MCP is off (default): skill targets in `--all`; paired MCP excluded.
305
+ * - `'both'`: install MCP and skill for the same host when both are available.
306
+ */
307
+ agentIntegration?: InstallAgentIntegration;
308
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
309
+ targets?: CliInstallTargets;
301
310
  /**
302
311
  * When set, enables `install --update` on the program root.
303
312
  * Should download or locate the latest release binary and return its path.
304
313
  */
305
314
  updateGetLatest?: CliUpdateGetLatest;
306
315
  }
316
+ /** Agent integration mode for install — MCP vs shell skill per host. */
317
+ export type InstallAgentIntegration = "mcp" | "skill" | "both";
318
+ /** Boolean or structured gate for one install artifact. */
319
+ export type InstallTargetSpec = boolean | {
320
+ /** When false, artifact is never installed (even with scoped CLI flags). Default true. */
321
+ enabled?: boolean;
322
+ /** When true, included in bare `install` / `install --all`. Default varies by key. */
323
+ includedInAll?: boolean;
324
+ };
325
+ export interface ResolvedInstallTarget {
326
+ enabled: boolean;
327
+ includedInAll: boolean;
328
+ }
329
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
330
+ export interface CliInstallTargets {
331
+ /** Copy app to `~/.local/bin/<key>`. Default includedInAll true (opt-out). */
332
+ app?: InstallTargetSpec;
333
+ /** ChatGPT desktop MCP. Default false. */
334
+ chatgptMcp?: InstallTargetSpec;
335
+ /** Claude Code MCP (`~/.claude.json`). Default false. */
336
+ claudeCodeMcp?: InstallTargetSpec;
337
+ /** Claude Desktop MCP. Default false. */
338
+ claudeDesktopMcp?: InstallTargetSpec;
339
+ /** Claude Code skill. Default false. */
340
+ claudeSkill?: InstallTargetSpec;
341
+ /** Codex MCP (`codex mcp add`). Default false. */
342
+ codexMcp?: InstallTargetSpec;
343
+ /** Codex skill. Default false. */
344
+ codexSkill?: InstallTargetSpec;
345
+ /** Shell completions for detected shells. Default includedInAll true (opt-out). */
346
+ completions?: InstallTargetSpec;
347
+ /** App config: wizard on install, file removal on uninstall. Default includedInAll true. */
348
+ configure?: InstallTargetSpec;
349
+ /** Cursor MCP. Default false. */
350
+ cursorMcp?: InstallTargetSpec;
351
+ /** Cursor skill. Default false. */
352
+ cursorSkill?: InstallTargetSpec;
353
+ /** OpenClaw MCP. Default false. */
354
+ openclawMcp?: InstallTargetSpec;
355
+ /** OpenClaw skill. Default false. */
356
+ openclawSkill?: InstallTargetSpec;
357
+ /** OpenCode MCP. Default false. */
358
+ opencodeMcp?: InstallTargetSpec;
359
+ /** OpenCode skill. Default false. */
360
+ opencodeSkill?: InstallTargetSpec;
361
+ }
307
362
  /**
308
363
  * One bundled documentation topic for the `docs` built-in (program root only).
309
364
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "4.0.3",
3
+ "version": "4.1.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -1,10 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { ParseKind, parse, postParseValidate } from "../parse.ts";
2
3
  import type { CliProgram } from "../types.ts";
3
4
  import { exportPresentationBuiltins } from "./export.ts";
4
5
  import { completionBashScript, completionFishScript, completionZshScript } from "./index.ts";
5
6
  import { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
6
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
7
- import { cliPresentationRoot } from "./presentation.ts";
8
+ import { cliParseRoot, cliPresentationRoot } from "./presentation.ts";
8
9
 
9
10
  const fixture: CliProgram = {
10
11
  key: "myapp",
@@ -23,12 +24,27 @@ const fixture: CliProgram = {
23
24
  describe("builtins help copy", () => {
24
25
  test("install command includes description and option text", () => {
25
26
  const install = cliBuiltinInstallCommand(fixture);
26
- expect(install.description).toContain("Install the binary");
27
+ expect(install.description).toContain("shell completions");
27
28
  expect(install.notes).toContain("install --all");
28
29
  const names = installBuiltinOptions(fixture).map((o) => o.name);
29
30
  expect(names).toContain("all");
30
31
  expect(names).toContain("mcp");
31
- expect(names).toContain("prefix");
32
+ expect(names.indexOf("all")).toBeLessThan(names.indexOf("mcp"));
33
+ expect(names.indexOf("mcp")).toBeLessThan(names.indexOf("status"));
34
+ expect(names.indexOf("status")).toBeLessThan(names.indexOf("uninstall"));
35
+ expect(names.indexOf("uninstall")).toBeLessThan(names.indexOf("from"));
36
+ expect(names.indexOf("from")).toBeLessThan(names.indexOf("yes"));
37
+ const yesOpt = installBuiltinOptions(fixture).find((o) => o.name === "yes");
38
+ expect(yesOpt?.shortName).toBe("y");
39
+ });
40
+
41
+ test("install -y parses as --yes", () => {
42
+ const root = cliParseRoot(fixture);
43
+ const pr = postParseValidate(root, parse(root, ["install", "-y"]));
44
+ expect(pr.kind).toBe(ParseKind.Ok);
45
+ if (pr.kind === ParseKind.Ok) {
46
+ expect(pr.opts.yes).toBe("1");
47
+ }
32
48
  });
33
49
 
34
50
  test("install omits --mcp option when mcpServer unset", () => {
@@ -143,6 +159,6 @@ describe("schema export builtins", () => {
143
159
  test("exportPresentationBuiltins includes install options", () => {
144
160
  const builtins = exportPresentationBuiltins(fixture);
145
161
  const install = builtins.find((b) => b.key === "install");
146
- expect(install?.options?.find((o) => o.name === "all")?.description).toContain("binary");
162
+ expect(install?.options?.find((o) => o.name === "all")?.description).toContain("app");
147
163
  });
148
164
  });
@@ -1,16 +1,16 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { resolveAppConfigPath, writeAppConfigFile } from "../config/file.ts";
5
6
  import { Cli, type CliProgram } from "../index.ts";
6
7
 
7
- function configFixture(configPath: string): CliProgram {
8
+ function configFixture(): CliProgram {
8
9
  return {
9
10
  key: "cfg-app",
10
11
  version: "1.0.0",
11
12
  description: "Config builtin test.",
12
13
  appConfig: {
13
- path: configPath,
14
14
  entries: {
15
15
  apiToken: { description: "Token.", env: "API_TOKEN", sensitive: true },
16
16
  port: { description: "Port.", required: false },
@@ -23,15 +23,21 @@ function configFixture(configPath: string): CliProgram {
23
23
  describe("builtins/config", () => {
24
24
  test("config get redacts sensitive values", async () => {
25
25
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
26
- const configPath = join(dir, "config");
27
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
26
+ const prevHome = process.env.HOME;
27
+ process.env.HOME = dir;
28
28
  const prev = process.env.API_TOKEN;
29
29
  delete process.env.API_TOKEN;
30
30
  try {
31
- const result = await new Cli(configFixture(configPath)).invoke(["config", "get", "apiToken"]);
31
+ const program = configFixture();
32
+ const configPath = resolveAppConfigPath(program);
33
+ mkdirSync(dirname(configPath), { recursive: true });
34
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
35
+ const result = await new Cli(program).invoke(["config", "get", "apiToken"]);
32
36
  expect(result.exitCode).toBe(0);
33
37
  expect(result.stdout.trim()).toBe("REDACTED");
34
38
  } finally {
39
+ if (prevHome === undefined) delete process.env.HOME;
40
+ else process.env.HOME = prevHome;
35
41
  if (prev !== undefined) process.env.API_TOKEN = prev;
36
42
  rmSync(dir, { recursive: true, force: true });
37
43
  }
@@ -39,20 +45,21 @@ describe("builtins/config", () => {
39
45
 
40
46
  test("config get --json redacts sensitive as { set: true }", async () => {
41
47
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
42
- const configPath = join(dir, "config");
43
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
48
+ const prevHome = process.env.HOME;
49
+ process.env.HOME = dir;
44
50
  const prev = process.env.API_TOKEN;
45
51
  delete process.env.API_TOKEN;
46
52
  try {
47
- const result = await new Cli(configFixture(configPath)).invoke([
48
- "config",
49
- "get",
50
- "apiToken",
51
- "--json",
52
- ]);
53
+ const program = configFixture();
54
+ const configPath = resolveAppConfigPath(program);
55
+ mkdirSync(dirname(configPath), { recursive: true });
56
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
57
+ const result = await new Cli(program).invoke(["config", "get", "apiToken", "--json"]);
53
58
  expect(result.exitCode).toBe(0);
54
- expect(JSON.parse(result.stdout.trim())).toEqual({ set: true });
59
+ expect(JSON.parse(result.stdout)).toEqual({ set: true });
55
60
  } finally {
61
+ if (prevHome === undefined) delete process.env.HOME;
62
+ else process.env.HOME = prevHome;
56
63
  if (prev !== undefined) process.env.API_TOKEN = prev;
57
64
  rmSync(dir, { recursive: true, force: true });
58
65
  }
@@ -60,21 +67,20 @@ describe("builtins/config", () => {
60
67
 
61
68
  test("config set writes and resolves without required exit", async () => {
62
69
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
63
- const configPath = join(dir, "config");
64
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "present" })}\n`);
70
+ const prevHome = process.env.HOME;
71
+ process.env.HOME = dir;
65
72
  const prev = process.env.API_TOKEN;
66
73
  delete process.env.API_TOKEN;
67
74
  try {
68
- const result = await new Cli(configFixture(configPath)).invoke([
69
- "config",
70
- "set",
71
- "port",
72
- "9090",
73
- ]);
75
+ const program = configFixture();
76
+ writeAppConfigFile(program, { apiToken: "seed" });
77
+ const result = await new Cli(program).invoke(["config", "set", "port", "9090"]);
74
78
  expect(result.exitCode).toBe(0);
75
- const get = await new Cli(configFixture(configPath)).invoke(["config", "get", "port"]);
79
+ const get = await new Cli(program).invoke(["config", "get", "port"]);
76
80
  expect(get.stdout.trim()).toBe("9090");
77
81
  } finally {
82
+ if (prevHome === undefined) delete process.env.HOME;
83
+ else process.env.HOME = prevHome;
78
84
  if (prev !== undefined) process.env.API_TOKEN = prev;
79
85
  rmSync(dir, { recursive: true, force: true });
80
86
  }
@@ -5,7 +5,7 @@ Built-in `config get` / `config set` commands.
5
5
  import { bootstrapAppConfig } from "../config/bootstrap.ts";
6
6
  import { configEntrySensitive, defaultConfigEntryTitle } from "../config/entry.ts";
7
7
  import { writeAppConfigFile } from "../config/file.ts";
8
- import { exportConfigToEnv, resolveAppConfig } from "../config/resolve.ts";
8
+ import { captureMappedHostEnv, exportConfigToEnv, resolveAppConfig } from "../config/resolve.ts";
9
9
  import { configPropertySchema, effectiveJsonSchema } from "../config/schema.ts";
10
10
  import { parseConfigSetValue } from "../config/validate.ts";
11
11
  import type { CliLeaf, CliOption, CliProgram, CliRouter } from "../types.ts";
@@ -128,11 +128,12 @@ function configSetRun(program: CliProgram, key: string, rawValue: string, useJso
128
128
  process.exit(1);
129
129
  }
130
130
 
131
+ const hostEnv = captureMappedHostEnv(program);
131
132
  const { fileData } = bootstrapAppConfig(program, { validateFile: true });
132
133
  const next = { ...fileData, [key]: parsed };
133
134
  writeAppConfigFile(program, next);
134
- const resolved = resolveAppConfig(program, next);
135
- exportConfigToEnv(program, resolved);
135
+ const resolved = resolveAppConfig(program, next, hostEnv);
136
+ exportConfigToEnv(program, resolved, hostEnv);
136
137
  }
137
138
 
138
139
  function configGetLeaf(program: CliProgram): CliLeaf {
@@ -6,12 +6,13 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
6
6
  const opts: CliOption[] = [
7
7
  {
8
8
  name: "all",
9
- description: "Install binary, completions, skills, and MCP config (when enabled).",
9
+ description:
10
+ "Install the default set (app, shell completions, and configuration when supported).",
10
11
  kind: CliOptionKind.Presence,
11
12
  },
12
13
  {
13
- name: "bin",
14
- description: "Copy this binary to your install directory (default ~/.local/bin).",
14
+ name: "app",
15
+ description: "Copy this app to your install directory (default ~/.local/bin).",
15
16
  kind: CliOptionKind.Presence,
16
17
  },
17
18
  {
@@ -22,40 +23,68 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
22
23
  },
23
24
  {
24
25
  name: "skill",
25
- description: "Install Cursor and Claude Code skills when ~/.cursor or ~/.claude exists.",
26
+ description:
27
+ "Install agent skills for Cursor, Claude, and other supported AI tools on this machine.",
26
28
  kind: CliOptionKind.Presence,
27
29
  },
28
- {
29
- name: "reinstall",
30
- description: "Reinstall artifacts already on disk (always includes the binary).",
30
+ ];
31
+
32
+ if (resolveCapabilities(root).mcp) {
33
+ opts.push({
34
+ name: "mcp",
35
+ description:
36
+ "Add MCP server configuration for Cursor, Claude Code, and other supported agents.",
31
37
  kind: CliOptionKind.Presence,
32
- },
33
- {
34
- name: "from",
35
- description: "Binary to copy (default: running executable). Used with --reinstall.",
36
- kind: CliOptionKind.String,
37
- },
38
+ });
39
+ }
40
+
41
+ if (root.appConfig) {
42
+ opts.push({
43
+ name: "configure",
44
+ description:
45
+ "Run the configuration wizard (install), or remove the config file (--uninstall).",
46
+ kind: CliOptionKind.Presence,
47
+ });
48
+ }
49
+
50
+ opts.push(
38
51
  {
39
52
  name: "status",
40
53
  description: "Print what is currently installed (read-only).",
41
54
  kind: CliOptionKind.Presence,
42
55
  },
56
+ {
57
+ name: "reinstall",
58
+ description: "Refresh everything already installed for this app.",
59
+ kind: CliOptionKind.Presence,
60
+ },
61
+ );
62
+
63
+ if (resolveCapabilities(root).update) {
64
+ opts.push({
65
+ name: "update",
66
+ description: "Download the latest release and refresh installed files.",
67
+ kind: CliOptionKind.Presence,
68
+ });
69
+ }
70
+
71
+ opts.push(
43
72
  {
44
73
  name: "uninstall",
45
74
  description:
46
- "Remove installed artifacts (use --all or scoped flags; skips targets not on disk).",
75
+ "Remove installed files (--all removes everything; use individual flags to remove one category).",
47
76
  kind: CliOptionKind.Presence,
48
77
  },
49
78
  {
50
- name: "prefix",
51
- description:
52
- "Install directory for the binary (default ~/.local/bin; overrides INSTALL_PREFIX).",
79
+ name: "from",
80
+ description: "App executable to copy (default: running executable). Used with --reinstall.",
53
81
  kind: CliOptionKind.String,
54
82
  },
55
83
  {
56
84
  name: "yes",
57
85
  description: "Skip the confirmation prompt.",
58
86
  kind: CliOptionKind.Presence,
87
+ shortName: "y",
59
88
  },
60
89
  {
61
90
  name: "dry",
@@ -67,43 +96,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
67
96
  description: "Print changed paths (install/update/uninstall) or status JSON on stdout.",
68
97
  kind: CliOptionKind.Presence,
69
98
  },
70
- {
71
- name: "quiet",
72
- description:
73
- "Suppress informational output (requires --yes, --json, --reinstall, or --update).",
74
- kind: CliOptionKind.Presence,
75
- },
76
- ];
77
-
78
- if (resolveCapabilities(root).mcp) {
79
- opts.splice(4, 0, {
80
- name: "mcp",
81
- description: "Add or update MCP server entries in Cursor and Claude config files.",
82
- kind: CliOptionKind.Presence,
83
- });
84
- }
85
-
86
- if (resolveCapabilities(root).update) {
87
- const statusIdx = opts.findIndex((o) => o.name === "status");
88
- opts.splice(statusIdx, 0, {
89
- name: "update",
90
- description: "Download the latest release and reinstall installed artifacts.",
91
- kind: CliOptionKind.Presence,
92
- });
93
- }
94
-
95
- if (root.appConfig) {
96
- opts.push({
97
- name: "configure",
98
- description: "Interactively write app config (~/.config/<key>/config).",
99
- kind: CliOptionKind.Presence,
100
- });
101
- opts.push({
102
- name: "config",
103
- description: "Remove app config file (with --uninstall).",
104
- kind: CliOptionKind.Presence,
105
- });
106
- }
99
+ );
107
100
 
108
101
  return opts;
109
102
  }
@@ -113,7 +106,7 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
113
106
  const app = root.key;
114
107
  const notesLines = [
115
108
  "First-time setup:",
116
- ` ${app} install --all --yes`,
109
+ ` ${app} install --yes`,
117
110
  "",
118
111
  "Refresh after upgrading:",
119
112
  ` ${app} install --reinstall`,
@@ -135,9 +128,9 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
135
128
  return {
136
129
  key: "install",
137
130
  description:
138
- "Install the binary, shell completions, agent skills, and MCP config to your user environment.",
139
- notes: notesLines.join("\n"),
131
+ "Install this app, shell completions, agent skills, and MCP config on your machine.",
140
132
  options: installBuiltinOptions(root),
133
+ notes: notesLines.join("\n"),
141
134
  handler: () => {},
142
135
  };
143
136
  }
@@ -28,7 +28,7 @@ export function cliBuiltinMcpCommand(program: CliProgram): CliRouter {
28
28
 
29
29
  const bundle: CliLeaf = {
30
30
  key: "bundle",
31
- description: "Pack Claude Desktop `.mcpb` and Claude Code plugin zip from dist/<key>.",
31
+ description: "Pack dist MCP artifacts (`.mcpb`, Claude Code plugin zip) from dist/<key>.",
32
32
  handler: () => {},
33
33
  };
34
34
 
@@ -65,13 +65,13 @@ export type CapabilityFeature = "mcp" | "install" | "docs" | "config";
65
65
  export function capabilityDeniedMessage(feature: CapabilityFeature): string {
66
66
  switch (feature) {
67
67
  case "mcp":
68
- return "MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n";
68
+ return "MCP is not available for this app.\n";
69
69
  case "install":
70
- return "install is disabled. Remove install.enabled: false from the program root.\n";
70
+ return "Install is not available for this app.\n";
71
71
  case "docs":
72
- return "docs is not enabled. Set docs: { enabled: true } on the program root.\n";
72
+ return "Documentation commands are not available for this app.\n";
73
73
  case "config":
74
- return "config commands are disabled. Set program.appConfig or enable config.commands.\n";
74
+ return "Configuration commands are not available for this app.\n";
75
75
  }
76
76
  }
77
77
 
package/src/cli.ts CHANGED
@@ -20,6 +20,7 @@ import { type AnyAppConfigSnapshot, createAppConfigSnapshot } from "./config/con
20
20
  import { effectiveJsonSchema } from "./config/schema.ts";
21
21
  import { CliContext } from "./context.ts";
22
22
  import { cliHelpRender } from "./help.ts";
23
+ import { maybeBootstrapInstallArgv } from "./install/bootstrap.ts";
23
24
  import { bootstrapMcpEnv } from "./mcp/env.ts";
24
25
  import { mcpServeStdioLoop } from "./mcp/server.ts";
25
26
  import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
@@ -94,6 +95,7 @@ export class Cli {
94
95
  }
95
96
 
96
97
  async run(argv: string[] = process.argv.slice(2)): Promise<never> {
98
+ argv = maybeBootstrapInstallArgv(argv, this.program);
97
99
  assertBuiltinAllowed(argv, this.caps);
98
100
 
99
101
  const prep = this.prepareDispatch(argv);