argsbarg 6.1.10 → 6.2.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 (130) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +43 -41
  3. package/docs/README.md +3 -2
  4. package/docs/ai-skills.md +36 -23
  5. package/docs/cli-program.md +12 -11
  6. package/docs/config-schema.md +3 -3
  7. package/docs/configure.md +12 -16
  8. package/docs/developing.md +6 -6
  9. package/docs/mcp.md +21 -57
  10. package/docs/output-schema.md +2 -2
  11. package/examples/formats.ts +5 -6
  12. package/examples/full-example/README.md +7 -69
  13. package/examples/full-example/docs/cli-schema.json +1 -1659
  14. package/examples/full-example/docs/cli.md +2 -1538
  15. package/examples/full-example/docs/http.md +0 -7
  16. package/examples/full-example/docs/mcp.md +23 -59
  17. package/examples/full-example/docs/openapi.json +2 -782
  18. package/examples/full-example/docs/skill.md +18 -14
  19. package/examples/full-example/justfile +5 -17
  20. package/examples/full-example/scripts/create-identity.ts +2 -1
  21. package/examples/full-example/src/commands/status/command.test.ts +2 -2
  22. package/examples/full-example/src/commands/status/command.ts +7 -6
  23. package/examples/full-example/src/program.ts +4 -10
  24. package/examples/full-example-json/Formula/.gitkeep +0 -0
  25. package/examples/full-example-json/Formula/full-example-json.rb +35 -0
  26. package/examples/full-example-json/README.md +27 -0
  27. package/examples/full-example-json/biome.json +22 -0
  28. package/examples/full-example-json/bun.lock +48 -0
  29. package/examples/full-example-json/docs/README.md +27 -0
  30. package/examples/full-example-json/docs/cli-schema.json +2145 -0
  31. package/examples/full-example-json/docs/cli.md +1990 -0
  32. package/examples/full-example-json/docs/http.md +92 -0
  33. package/examples/full-example-json/docs/mcp.md +116 -0
  34. package/examples/full-example-json/docs/openapi.json +1246 -0
  35. package/examples/full-example-json/docs/skill.md +57 -0
  36. package/examples/full-example-json/justfile +171 -0
  37. package/examples/full-example-json/package.json +22 -0
  38. package/examples/full-example-json/scripts/create-identity.ts +12 -0
  39. package/examples/full-example-json/scripts/dev-formula.ts +97 -0
  40. package/examples/full-example-json/scripts/formula-shared.test.ts +68 -0
  41. package/examples/full-example-json/scripts/formula-shared.ts +170 -0
  42. package/examples/full-example-json/scripts/print-identity.ts +28 -0
  43. package/examples/full-example-json/scripts/release.ts +212 -0
  44. package/examples/full-example-json/src/commands/echo/command.ts +26 -0
  45. package/examples/full-example-json/src/commands/status/command.test.ts +10 -0
  46. package/examples/full-example-json/src/commands/status/command.ts +28 -0
  47. package/examples/full-example-json/src/index.ts +10 -0
  48. package/examples/full-example-json/src/program.ts +33 -0
  49. package/examples/full-example-json/src/types/md.d.ts +4 -0
  50. package/examples/full-example-json/tsconfig.json +17 -0
  51. package/examples/minimal.ts +17 -17
  52. package/examples/nested.ts +10 -10
  53. package/examples/option-required.ts +13 -13
  54. package/examples/servers.ts +10 -10
  55. package/index.d.ts +17 -43
  56. package/package.json +1 -1
  57. package/src/cli-tool/create.test.ts +44 -68
  58. package/src/cli-tool/create.ts +81 -17
  59. package/src/cli-tool/full-example-capabilities.test.ts +33 -18
  60. package/src/cli-tool/post-create.ts +31 -17
  61. package/src/cli-tool/program.ts +16 -7
  62. package/src/cli-tool/prompt.ts +27 -0
  63. package/src/cli-tool/run-create.ts +19 -7
  64. package/src/cli-tool/schemagen/schemagen.test.ts +3 -3
  65. package/src/configure/artifacts/install-validate.test.ts +20 -33
  66. package/src/configure/artifacts/paths.ts +9 -53
  67. package/src/configure/artifacts/status.test.ts +13 -16
  68. package/src/configure/artifacts/status.ts +5 -22
  69. package/src/configure/artifacts/target-base.ts +6 -15
  70. package/src/configure/artifacts/target-effective.ts +16 -54
  71. package/src/configure/artifacts/target-mcp-json.ts +2 -5
  72. package/src/configure/artifacts/target-registry.ts +0 -7
  73. package/src/configure/artifacts/target-scope.ts +7 -17
  74. package/src/configure/artifacts/target-skill.ts +6 -15
  75. package/src/configure/artifacts/target-types.ts +6 -54
  76. package/src/configure/artifacts/targets/agents-mcp.ts +11 -0
  77. package/src/configure/artifacts/targets/configure.ts +1 -5
  78. package/src/configure/artifacts/targets/index.ts +4 -44
  79. package/src/configure/artifacts/targets/skill.ts +12 -0
  80. package/src/configure/artifacts/targets.test.ts +21 -59
  81. package/src/configure/configure.test.ts +35 -46
  82. package/src/configure/index.ts +19 -19
  83. package/src/configure/prompt.ts +2 -12
  84. package/src/core/parse.test.ts +21 -32
  85. package/src/core/types.ts +18 -44
  86. package/src/core/validate.ts +28 -45
  87. package/src/docs/docs.test.ts +4 -4
  88. package/src/docs/mcp-guide.ts +41 -71
  89. package/src/docs/resolve.ts +1 -1
  90. package/src/exports/cli.ts +1 -1
  91. package/src/index.ts +1 -1
  92. package/src/skill/generate.ts +26 -45
  93. package/src/skill/install.ts +18 -38
  94. package/src/skill/naming.ts +3 -27
  95. package/src/test/integration/config.test.ts +3 -3
  96. package/src/test/integration/mcp.test.ts +4 -4
  97. package/{examples/mcp-test.ts → src/test/mcp-integration-fixture.ts} +20 -22
  98. package/src/configure/artifacts/target-mcp-cli.ts +0 -127
  99. package/src/configure/artifacts/targets/chatgpt-mcp.ts +0 -12
  100. package/src/configure/artifacts/targets/claude-code-mcp.ts +0 -15
  101. package/src/configure/artifacts/targets/claude-desktop-mcp.ts +0 -12
  102. package/src/configure/artifacts/targets/claude-skill.ts +0 -16
  103. package/src/configure/artifacts/targets/codex-mcp.ts +0 -25
  104. package/src/configure/artifacts/targets/codex-skill.ts +0 -14
  105. package/src/configure/artifacts/targets/cursor-mcp.ts +0 -15
  106. package/src/configure/artifacts/targets/cursor-skill.ts +0 -16
  107. package/src/configure/artifacts/targets/openclaw-mcp.ts +0 -25
  108. package/src/configure/artifacts/targets/openclaw-skill.ts +0 -17
  109. package/src/configure/artifacts/targets/opencode-mcp.ts +0 -96
  110. package/src/configure/artifacts/targets/opencode-skill.ts +0 -15
  111. /package/examples/{full-example → full-example-json}/src/commands/render-json/__generated__/RenderJsonInputSchema.json +0 -0
  112. /package/examples/{full-example → full-example-json}/src/commands/render-json/__generated__/index.ts +0 -0
  113. /package/examples/{full-example → full-example-json}/src/commands/render-json/command.test.ts +0 -0
  114. /package/examples/{full-example → full-example-json}/src/commands/render-json/command.ts +0 -0
  115. /package/examples/{full-example → full-example-json}/src/commands/render-json/types.ts +0 -0
  116. /package/examples/{full-example → full-example-json}/src/commands/status/__generated__/StatusJsonOutputSchema.json +0 -0
  117. /package/examples/{full-example → full-example-json}/src/commands/status/__generated__/index.ts +0 -0
  118. /package/examples/{full-example → full-example-json}/src/commands/status/types.ts +0 -0
  119. /package/examples/{full-example → full-example-json}/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +0 -0
  120. /package/examples/{full-example → full-example-json}/src/commands/workspaces/__generated__/index.ts +0 -0
  121. /package/examples/{full-example → full-example-json}/src/commands/workspaces/command.test.ts +0 -0
  122. /package/examples/{full-example → full-example-json}/src/commands/workspaces/command.ts +0 -0
  123. /package/examples/{full-example → full-example-json}/src/commands/workspaces/types.ts +0 -0
  124. /package/examples/{full-example → full-example-json}/src/db/index.test.ts +0 -0
  125. /package/examples/{full-example → full-example-json}/src/db/index.ts +0 -0
  126. /package/examples/{full-example → full-example-json}/src/db/migrate.test.ts +0 -0
  127. /package/examples/{full-example → full-example-json}/src/db/migrate.ts +0 -0
  128. /package/examples/{full-example → full-example-json}/src/db/migrations/001_workspaces.sql +0 -0
  129. /package/examples/{full-example → full-example-json}/src/db/tables/workspaces.ts +0 -0
  130. /package/examples/{full-example → full-example-json}/src/types/argsbarg.d.ts +0 -0
@@ -8,6 +8,7 @@ import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import {
10
10
  applyCreate,
11
+ type CreateOptions,
11
12
  classNameFromKey,
12
13
  diffCreate,
13
14
  diffCreateDetails,
@@ -16,25 +17,39 @@ import {
16
17
  substituteTemplateContent,
17
18
  } from "./create.ts";
18
19
 
20
+ function baseOpts(overrides: Partial<CreateOptions> = {}): CreateOptions {
21
+ return {
22
+ templateId: "cli",
23
+ key: "testapp",
24
+ className: "Testapp",
25
+ tap: "local/testapp",
26
+ homepage: "https://example.com",
27
+ releaseRepo: "example/testapp",
28
+ desc: "Test",
29
+ force: false,
30
+ dryRun: false,
31
+ check: false,
32
+ diff: false,
33
+ yes: false,
34
+ devTemplate: false,
35
+ ...overrides,
36
+ };
37
+ }
38
+
19
39
  /** Tests for argsbarg create. */
20
40
  describe("argsbarg create", () => {
21
- /** Tests that substitutes {key} tokens. */
22
41
  test("substitutes {key} tokens", () => {
23
42
  const out = substituteTemplateContent(
24
43
  "key={key} class={className} env={envPrefix}_API_TOKEN tap={tap} org={tapOrg}",
25
44
  {
26
- key: "my-cli",
27
- className: "MyCli",
28
- tap: "org/my-cli",
29
- homepage: "https://github.com/org/my-cli",
30
- releaseRepo: "org/my-cli",
31
- desc: "My CLI",
32
- force: false,
33
- dryRun: false,
34
- check: false,
35
- diff: false,
36
- yes: false,
37
- devTemplate: false,
45
+ ...baseOpts({
46
+ key: "my-cli",
47
+ className: "MyCli",
48
+ tap: "org/my-cli",
49
+ homepage: "https://github.com/org/my-cli",
50
+ releaseRepo: "org/my-cli",
51
+ desc: "My CLI",
52
+ }),
38
53
  },
39
54
  );
40
55
  expect(out).toContain("my-cli");
@@ -50,7 +65,6 @@ describe("argsbarg create", () => {
50
65
  expect(classNameFromKey("1password")).toBe("App1password");
51
66
  });
52
67
 
53
- /** ResolveCreateOptions derives identity defaults from key. */
54
68
  test("resolveCreateOptions derives identity defaults from key", () => {
55
69
  expect(resolveCreateOptions({ key: "1password", releaseRepo: "org/1password" }).className).toBe("App1password");
56
70
  expect(resolveCreateOptions({ key: "my-cli", className: "Custom", releaseRepo: "org/my-cli" }).className).toBe(
@@ -58,83 +72,45 @@ describe("argsbarg create", () => {
58
72
  );
59
73
  const opts = resolveCreateOptions({ key: "at1", releaseRepo: "bdombro/at1" });
60
74
  expect(opts.tap).toBe("bdombro/at1");
61
- expect(opts.releaseRepo).toBe("bdombro/at1");
62
- expect(opts.homepage).toBe("https://github.com/bdombro/at1");
63
- expect(opts.desc).toBe("At1 CLI");
75
+ expect(opts.templateId).toBe("cli");
64
76
  });
65
77
 
66
78
  test("resolveCreateOptions requires release repo", () => {
67
79
  expect(() => resolveCreateOptions({ key: "at1" })).toThrow(/release repo/i);
68
80
  });
69
81
 
70
- /** Tests that renderCreateTree includes justfile and create-identity. */
71
- test("renderCreateTree includes justfile and create-identity", () => {
72
- const tree = renderCreateTree({
73
- key: "testapp",
74
- className: "Testapp",
75
- tap: "local/testapp",
76
- homepage: "https://example.com",
77
- releaseRepo: "example/testapp",
78
- desc: "Test",
79
- force: false,
80
- dryRun: false,
81
- check: false,
82
- diff: false,
83
- yes: false,
84
- devTemplate: false,
85
- });
82
+ test("renderCreateTree includes justfile and create-identity for cli template", () => {
83
+ const tree = renderCreateTree(baseOpts({ key: "testapp" }));
86
84
  expect(tree.has("justfile")).toBe(true);
87
- expect(tree.has("biome.json")).toBe(true);
88
- expect(tree.has(".cursor/rules/code.mdc")).toBe(true);
89
85
  expect(tree.has("scripts/create-identity.ts")).toBe(true);
90
86
  const identity = tree.get("scripts/create-identity.ts");
91
87
  expect(identity).toContain('key: "testapp"');
92
- const formula = tree.get("scripts/formula-shared.ts");
93
- expect(formula).toContain("create-identity.ts");
88
+ expect(identity).toContain('template: "cli"');
89
+ expect(tree.has("src/commands/render-json/command.ts")).toBe(false);
90
+ });
91
+
92
+ test("renderCreateTree json template includes schemagen commands", () => {
93
+ const tree = renderCreateTree(baseOpts({ templateId: "json", key: "testapp" }));
94
+ expect(tree.has("src/commands/render-json/command.ts")).toBe(true);
95
+ expect(tree.has("src/db/index.ts")).toBe(true);
96
+ const identity = tree.get("scripts/create-identity.ts");
97
+ expect(identity).toContain('template: "json"');
94
98
  });
95
99
 
96
- /** Tests that --check detects drift. */
97
100
  test("--check detects drift", () => {
98
101
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
99
102
  try {
100
- applyCreate(dir, {
101
- key: "testapp",
102
- className: "Testapp",
103
- tap: "local/testapp",
104
- homepage: "https://example.com",
105
- releaseRepo: "example/testapp",
106
- desc: "Test",
107
- force: true,
108
- dryRun: false,
109
- check: false,
110
- diff: false,
111
- yes: false,
112
- devTemplate: false,
113
- });
114
- expect(diffCreate(dir, { key: "testapp", className: "Testapp" })).toEqual([]);
103
+ applyCreate(dir, baseOpts({ force: true }));
104
+ expect(diffCreate(dir, { key: "testapp", className: "Testapp", templateId: "cli" })).toEqual([]);
115
105
  } finally {
116
106
  rmSync(dir, { recursive: true, force: true });
117
107
  }
118
108
  });
119
109
 
120
- /** Tests that --check infers options from create-identity.ts. */
121
110
  test("--check infers options from create-identity.ts", () => {
122
111
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
123
112
  try {
124
- applyCreate(dir, {
125
- key: "testapp",
126
- className: "Testapp",
127
- tap: "local/testapp",
128
- homepage: "https://example.com",
129
- releaseRepo: "example/testapp",
130
- desc: "Test",
131
- force: true,
132
- dryRun: false,
133
- check: false,
134
- diff: false,
135
- yes: false,
136
- devTemplate: false,
137
- });
113
+ applyCreate(dir, baseOpts({ force: true }));
138
114
  expect(diffCreate(dir, { check: true })).toEqual([]);
139
115
  } finally {
140
116
  rmSync(dir, { recursive: true, force: true });
@@ -1,11 +1,48 @@
1
- /** `argsbarg create` — copy npm-shipped examples/full-example with substitutions. */
1
+ /** `argsbarg create` — copy npm-shipped example templates with substitutions. */
2
2
 
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import pkg from "../../package.json" with { type: "json" };
7
7
 
8
+ export type CreateTemplateId = "cli" | "json";
9
+
10
+ export interface CreateTemplateSpec {
11
+ id: CreateTemplateId;
12
+ dirName: string;
13
+ displayName: string;
14
+ description: string;
15
+ }
16
+
17
+ export const CREATE_TEMPLATES: CreateTemplateSpec[] = [
18
+ {
19
+ id: "cli",
20
+ dirName: "full-example",
21
+ displayName: "full-example",
22
+ description: "Production CLI with MCP, HTTP, configure, and skills. Options and flags only; no schemagen.",
23
+ },
24
+ {
25
+ id: "json",
26
+ dirName: "full-example-json",
27
+ displayName: "full-example-json",
28
+ description: "Same shell plus @sg schemagen, input/outputSchema validation, JSON HTTP leaves, and REST CRUD demo.",
29
+ },
30
+ ];
31
+
32
+ export function normalizeCreateTemplateId(value: string | undefined): CreateTemplateId {
33
+ return value === "json" ? "json" : "cli";
34
+ }
35
+
36
+ export function templateDirFor(templateId: CreateTemplateId): string {
37
+ const spec = CREATE_TEMPLATES.find((t) => t.id === templateId);
38
+ if (!spec) {
39
+ throw new Error(`Unknown create template: ${templateId}`);
40
+ }
41
+ return join(packageRoot(), "examples", spec.dirName);
42
+ }
43
+
8
44
  export interface CreateOptions {
45
+ templateId: CreateTemplateId;
9
46
  key: string;
10
47
  className: string;
11
48
  tap: string;
@@ -29,12 +66,25 @@ export function packageRoot(): string {
29
66
  return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
30
67
  }
31
68
 
32
- export function templateDir(): string {
33
- return join(packageRoot(), "examples/full-example");
69
+ export function templateDir(templateId: CreateTemplateId = "cli"): string {
70
+ return templateDirFor(templateId);
34
71
  }
35
72
 
36
- export function isDevTemplateDir(baseDir: string): boolean {
37
- return resolve(baseDir) === resolve(templateDir());
73
+ export function devTemplateIdForDir(baseDir: string): CreateTemplateId | undefined {
74
+ const resolved = resolve(baseDir);
75
+ for (const spec of CREATE_TEMPLATES) {
76
+ if (resolved === resolve(templateDirFor(spec.id))) {
77
+ return spec.id;
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ export function isDevTemplateDir(baseDir: string, templateId?: CreateTemplateId): boolean {
84
+ if (templateId) {
85
+ return resolve(baseDir) === resolve(templateDirFor(templateId));
86
+ }
87
+ return devTemplateIdForDir(baseDir) !== undefined;
38
88
  }
39
89
 
40
90
  export function keyToEnvPrefix(key: string): string {
@@ -70,10 +120,13 @@ function tapLibraryParts(tap: string): { org: string; repo: string } {
70
120
  }
71
121
 
72
122
  /** Parse `scripts/create-identity.ts` for inference and template defaults. */
73
- export function parseCreateIdentityFile(path: string): Partial<CreateOptions> & { envPrefix?: string } {
123
+ export function parseCreateIdentityFile(
124
+ path: string,
125
+ ): Partial<CreateOptions> & { envPrefix?: string; template?: string } {
74
126
  if (!existsSync(path)) return {};
75
127
  const text = readFileSync(path, "utf8");
76
128
  const pick = (field: string) => text.match(new RegExp(`${field}:\\s*"([^"]*)"`))?.[1];
129
+ const templateRaw = text.match(/template:\s*"(cli|json)"/)?.[1];
77
130
  return {
78
131
  key: pick("key"),
79
132
  className: pick("className"),
@@ -82,10 +135,13 @@ export function parseCreateIdentityFile(path: string): Partial<CreateOptions> &
82
135
  releaseRepo: pick("releaseRepo"),
83
136
  desc: pick("desc"),
84
137
  envPrefix: pick("envPrefix"),
138
+ template: templateRaw,
139
+ templateId: templateRaw ? normalizeCreateTemplateId(templateRaw) : undefined,
85
140
  };
86
141
  }
87
142
 
88
- export function templateIdentity(): {
143
+ export function templateIdentity(templateId: CreateTemplateId = "cli"): {
144
+ templateId: CreateTemplateId;
89
145
  key: string;
90
146
  className: string;
91
147
  tap: string;
@@ -94,15 +150,16 @@ export function templateIdentity(): {
94
150
  desc: string;
95
151
  envPrefix: string;
96
152
  } {
97
- const parsed = parseCreateIdentityFile(join(templateDir(), CREATE_IDENTITY_REL));
98
- const key = parsed.key ?? "full-example";
153
+ const parsed = parseCreateIdentityFile(join(templateDirFor(templateId), CREATE_IDENTITY_REL));
154
+ const key = parsed.key ?? CREATE_TEMPLATES.find((t) => t.id === templateId)?.displayName ?? "full-example";
99
155
  return {
156
+ templateId,
100
157
  key,
101
158
  className: parsed.className ?? classNameFromKey(key),
102
159
  tap: parsed.tap ?? `local/${key}`,
103
160
  homepage: parsed.homepage ?? "https://github.com/bdombro/bun-argsbarg",
104
161
  releaseRepo: parsed.releaseRepo ?? "bdombro/bun-argsbarg",
105
- desc: parsed.desc ?? "Argsbarg full example reference app",
162
+ desc: parsed.desc ?? "Argsbarg copy template",
106
163
  envPrefix: parsed.envPrefix ?? keyToEnvPrefix(key),
107
164
  };
108
165
  }
@@ -113,6 +170,7 @@ export function inferCreateOptions(baseDir: string, partial: Partial<CreateOptio
113
170
  }
114
171
  const fromIdentity = parseCreateIdentityFile(join(baseDir, CREATE_IDENTITY_REL));
115
172
  return {
173
+ templateId: partial.templateId ?? fromIdentity.templateId,
116
174
  key: partial.key ?? fromIdentity.key,
117
175
  className: partial.className ?? fromIdentity.className,
118
176
  desc: partial.desc ?? fromIdentity.desc,
@@ -136,7 +194,10 @@ function assertReleaseRepoFormat(releaseRepo: string): void {
136
194
 
137
195
  export function resolveCreateOptions(partial: Partial<CreateOptions>, baseDir?: string): CreateOptions {
138
196
  const merged = baseDir ? inferCreateOptions(baseDir, partial) : partial;
139
- const tmpl = templateIdentity();
197
+ const templateId = normalizeCreateTemplateId(
198
+ merged.templateId ?? (baseDir ? devTemplateIdForDir(baseDir) : undefined),
199
+ );
200
+ const tmpl = templateIdentity(templateId);
140
201
  const key = merged.key ?? tmpl.key ?? "full-example";
141
202
  const className = merged.className ?? classNameFromKey(key);
142
203
  const releaseRepo = merged.releaseRepo;
@@ -144,8 +205,9 @@ export function resolveCreateOptions(partial: Partial<CreateOptions>, baseDir?:
144
205
  throw new Error("GitHub release repo (org/repo) is required. Pass --release-repo or use the interactive wizard.");
145
206
  }
146
207
  assertReleaseRepoFormat(releaseRepo);
147
- const devTemplate = merged.devTemplate ?? (baseDir ? isDevTemplateDir(baseDir) : false);
208
+ const devTemplate = merged.devTemplate ?? (baseDir ? isDevTemplateDir(baseDir, templateId) : false);
148
209
  return {
210
+ templateId,
149
211
  key,
150
212
  className,
151
213
  tap: merged.tap ?? releaseRepo,
@@ -173,6 +235,7 @@ export const createIdentity = {
173
235
  releaseRepo: "${opts.releaseRepo}",
174
236
  desc: "${opts.desc}",
175
237
  envPrefix: "${envPrefix}",
238
+ template: "${opts.templateId}",
176
239
  } as const;
177
240
  `;
178
241
  }
@@ -195,7 +258,7 @@ function tokenMap(opts: CreateOptions): Record<string, string> {
195
258
 
196
259
  /** Substitute \`{key}\`-style placeholders; also replace template identity literals. */
197
260
  export function substituteTemplateContent(content: string, opts: CreateOptions): string {
198
- const tmpl = templateIdentity();
261
+ const tmpl = templateIdentity(opts.templateId);
199
262
  const tokens = tokenMap(opts);
200
263
  const argsbargVersion = pkg.version;
201
264
 
@@ -245,8 +308,8 @@ function shouldExcludeRel(rel: string): boolean {
245
308
  return EXCLUDE_REL.has(rel);
246
309
  }
247
310
 
248
- function listTemplateFiles(): string[] {
249
- const root = templateDir();
311
+ function listTemplateFiles(templateId: CreateTemplateId): string[] {
312
+ const root = templateDirFor(templateId);
250
313
  if (!existsSync(root)) return [];
251
314
  const out: string[] = [];
252
315
  const walk = (dir: string, prefix: string) => {
@@ -266,12 +329,12 @@ function listTemplateFiles(): string[] {
266
329
 
267
330
  export function renderCreateTree(opts: CreateOptions): Map<string, string> {
268
331
  const files = new Map<string, string>();
269
- for (const rel of listTemplateFiles()) {
332
+ for (const rel of listTemplateFiles(opts.templateId)) {
270
333
  if (rel === CREATE_IDENTITY_REL) {
271
334
  files.set(rel, renderCreateIdentitySource(opts));
272
335
  continue;
273
336
  }
274
- const src = join(templateDir(), rel);
337
+ const src = join(templateDirFor(opts.templateId), rel);
275
338
  const raw = readFileSync(src, "utf8");
276
339
  files.set(rel, substituteTemplateContent(raw, opts));
277
340
  }
@@ -374,6 +437,7 @@ export function parseCreateArgv(rest: string[]): ParsedCreateArgv {
374
437
  else if (a === "--homepage" && rest[i + 1]) opts.homepage = rest[++i];
375
438
  else if (a === "--release-repo" && rest[i + 1]) opts.releaseRepo = rest[++i];
376
439
  else if (a === "--desc" && rest[i + 1]) opts.desc = rest[++i];
440
+ else if (a === "--template" && rest[i + 1]) opts.templateId = normalizeCreateTemplateId(rest[++i]);
377
441
  else if (a?.startsWith("--")) {
378
442
  throw new Error(`Unknown option: ${a}`);
379
443
  } else if (a) {
@@ -8,14 +8,16 @@ import { join } from "node:path";
8
8
  import { CliOptionKind, type CliProgram } from "../core/types.ts";
9
9
  import { resolveCapabilities } from "../runtime/capabilities.ts";
10
10
 
11
- const exampleRoot = join(import.meta.dir, "../../examples/full-example");
12
- const programSource = readFileSync(join(exampleRoot, "src/program.ts"), "utf8");
11
+ const cliExampleRoot = join(import.meta.dir, "../../examples/full-example");
12
+ const jsonExampleRoot = join(import.meta.dir, "../../examples/full-example-json");
13
+ const cliProgramSource = readFileSync(join(cliExampleRoot, "src/program.ts"), "utf8");
14
+ const jsonProgramSource = readFileSync(join(jsonExampleRoot, "src/program.ts"), "utf8");
13
15
 
14
- /** Mirror of full-example/src/program.ts capability flags (in-repo types only). */
16
+ /** Mirror capability flags for copy templates (in-repo types only). */
15
17
  const sinkProgram = {
16
18
  key: "full-example",
17
19
  version: "1.0.0",
18
- description: "Full example reference.",
20
+ description: "Copy template reference.",
19
21
  docs: {
20
22
  topics: { readme: { text: "# readme\n" } },
21
23
  },
@@ -44,25 +46,22 @@ const sinkProgram = {
44
46
  ],
45
47
  } satisfies CliProgram;
46
48
 
47
- /** Tests for full-example template. */
48
- describe("full-example template", () => {
49
- /** Tests that program source enables every builtin flag. */
49
+ describe("full-example cli template", () => {
50
50
  test("program source enables every builtin flag", () => {
51
- expect(programSource).toContain("mcpServer: {");
52
- expect(programSource).toContain("httpServer: {");
53
- expect(programSource).toContain("docs:");
54
- expect(programSource).not.toMatch(/docs:\s*\{[^}]*enabled:\s*true/s);
55
- expect(programSource).not.toContain("appConfig:");
51
+ expect(cliProgramSource).toContain("mcpServer: {");
52
+ expect(cliProgramSource).toContain("httpServer: {");
53
+ expect(cliProgramSource).toContain("docs:");
54
+ expect(cliProgramSource).not.toMatch(/docs:\s*\{[^}]*enabled:\s*true/s);
55
+ expect(cliProgramSource).not.toContain("appConfig:");
56
56
  });
57
57
 
58
- test("status command defines outputSchema", () => {
59
- const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
60
- expect(statusSource).toMatch(/outputSchema[,:]/);
61
- expect(statusSource).toContain("StatusJsonOutputSchema");
62
- expect(statusSource).toContain('from "./__generated__"');
58
+ test("status command has no outputSchema", () => {
59
+ const statusSource = readFileSync(join(cliExampleRoot, "src/commands/status/command.ts"), "utf8");
60
+ expect(statusSource).not.toMatch(/outputSchema[,:]/);
61
+ expect(statusSource).not.toContain("__generated__");
63
62
  });
64
63
 
65
- test("resolveCapabilities matches full sink shape", () => {
64
+ test("resolveCapabilities matches sink shape", () => {
66
65
  expect(resolveCapabilities(sinkProgram)).toEqual({
67
66
  http: true,
68
67
  completion: true,
@@ -73,3 +72,19 @@ describe("full-example template", () => {
73
72
  });
74
73
  });
75
74
  });
75
+
76
+ describe("full-example-json template", () => {
77
+ test("program source enables every builtin flag", () => {
78
+ expect(jsonProgramSource).toContain("mcpServer: {");
79
+ expect(jsonProgramSource).toContain("httpServer: {");
80
+ expect(jsonProgramSource).toContain("docs:");
81
+ expect(jsonProgramSource).not.toContain("appConfig:");
82
+ });
83
+
84
+ test("status command defines outputSchema", () => {
85
+ const statusSource = readFileSync(join(jsonExampleRoot, "src/commands/status/command.ts"), "utf8");
86
+ expect(statusSource).toMatch(/outputSchema[,:]/);
87
+ expect(statusSource).toContain("StatusJsonOutputSchema");
88
+ expect(statusSource).toContain('from "./__generated__"');
89
+ });
90
+ });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { existsSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
+ import type { CreateTemplateId } from "./create.ts";
5
6
 
6
7
  export function isInsideGitWorkTree(dir: string): boolean {
7
8
  try {
@@ -25,7 +26,11 @@ export function shouldSkipGitBootstrap(targetDir: string): boolean {
25
26
  return false;
26
27
  }
27
28
 
28
- export async function runPostCreate(targetDir: string, dryRun: boolean): Promise<void> {
29
+ export async function runPostCreate(
30
+ targetDir: string,
31
+ dryRun: boolean,
32
+ templateId: CreateTemplateId = "cli",
33
+ ): Promise<void> {
29
34
  const abs = resolve(targetDir);
30
35
  const steps: Array<{ label: string; run: () => Promise<void> | void }> = [
31
36
  {
@@ -40,18 +45,22 @@ export async function runPostCreate(targetDir: string, dryRun: boolean): Promise
40
45
  if (proc.exitCode !== 0) throw new Error("bun install failed");
41
46
  },
42
47
  },
43
- {
44
- label: "argsbarg schemagen",
45
- run: () => {
46
- if (dryRun) return;
47
- const proc = Bun.spawnSync(["argsbarg", "schemagen"], {
48
- cwd: abs,
49
- stdout: "inherit",
50
- stderr: "inherit",
51
- });
52
- if (proc.exitCode !== 0) throw new Error("schemagen failed");
53
- },
54
- },
48
+ ...(templateId === "json"
49
+ ? [
50
+ {
51
+ label: "argsbarg schemagen",
52
+ run: () => {
53
+ if (dryRun) return;
54
+ const proc = Bun.spawnSync(["argsbarg", "schemagen"], {
55
+ cwd: abs,
56
+ stdout: "inherit",
57
+ stderr: "inherit",
58
+ });
59
+ if (proc.exitCode !== 0) throw new Error("schemagen failed");
60
+ },
61
+ },
62
+ ]
63
+ : []),
55
64
  {
56
65
  label: "bun test",
57
66
  run: () => {
@@ -100,10 +109,15 @@ export async function runPostCreate(targetDir: string, dryRun: boolean): Promise
100
109
  }
101
110
  }
102
111
 
103
- export function printPostCreatePlan(): void {
112
+ export function printPostCreatePlan(templateId: CreateTemplateId = "cli"): void {
104
113
  process.stderr.write("Post-create steps:\n");
105
114
  process.stderr.write(" 1. bun install\n");
106
- process.stderr.write(" 2. just schemagen\n");
107
- process.stderr.write(" 3. bun test\n");
108
- process.stderr.write(" 4. git init + Initial commit (skipped inside existing git work tree)\n");
115
+ if (templateId === "json") {
116
+ process.stderr.write(" 2. argsbarg schemagen\n");
117
+ process.stderr.write(" 3. bun test\n");
118
+ process.stderr.write(" 4. git init + Initial commit (skipped inside existing git work tree)\n");
119
+ } else {
120
+ process.stderr.write(" 2. bun test\n");
121
+ process.stderr.write(" 3. git init + Initial commit (skipped inside existing git work tree)\n");
122
+ }
109
123
  }
@@ -4,21 +4,22 @@ Argsbarg developer tools — bootstrap consumer CLIs via `create`.
4
4
 
5
5
  import pkg from "../../package.json" with { type: "json" };
6
6
  import { CliOptionKind, type CliProgram } from "../index.ts";
7
+ import { normalizeCreateTemplateId } from "./create.ts";
7
8
  import { runCreate } from "./run-create.ts";
8
9
  import { runSchemagenCli } from "./run-schemagen.ts";
9
10
 
10
11
  export const program = {
11
- key: "argsbarg",
12
- version: pkg.version,
13
- description: "Argsbarg developer tools — bootstrap CLIs from the full-example template.",
14
- completion: { enabled: false },
15
- configure: { enabled: false },
16
- docs: { enabled: false },
17
12
  commands: [
18
13
  {
19
14
  key: "create",
20
- description: "Copy the full-example template into a directory with substitutions.",
15
+ description: "Copy a CLI template into a directory with substitutions.",
21
16
  options: [
17
+ {
18
+ name: "template",
19
+ description: "Template: cli (default) or json (schema-first).",
20
+ kind: CliOptionKind.Enum,
21
+ choices: ["cli", "json"],
22
+ },
22
23
  { name: "key", description: "CLI binary name.", kind: CliOptionKind.String },
23
24
  {
24
25
  name: "class-name",
@@ -77,8 +78,10 @@ export const program = {
77
78
  },
78
79
  ],
79
80
  handler: async (ctx) => {
81
+ const templateRaw = ctx.stringOpt("template");
80
82
  const code = await runCreate({
81
83
  dir: ctx.args[0],
84
+ ...(templateRaw !== undefined ? { templateId: normalizeCreateTemplateId(templateRaw) } : {}),
82
85
  key: ctx.stringOpt("key"),
83
86
  className: ctx.stringOpt("class-name"),
84
87
  tap: ctx.stringOpt("tap"),
@@ -130,4 +133,10 @@ export const program = {
130
133
  },
131
134
  },
132
135
  ],
136
+ completion: { enabled: false },
137
+ configure: { enabled: false },
138
+ description: "Argsbarg developer tools — bootstrap CLIs from copy templates.",
139
+ docs: { enabled: false },
140
+ key: "argsbarg",
141
+ version: pkg.version,
133
142
  } satisfies CliProgram;
@@ -1,6 +1,7 @@
1
1
  /** Interactive prompts for `argsbarg create`. */
2
2
 
3
3
  import { readPromptLine as readStdinLine } from "../prompt.ts";
4
+ import { CREATE_TEMPLATES, type CreateTemplateId, normalizeCreateTemplateId } from "./create.ts";
4
5
 
5
6
  export function readPromptLine(prompt: string): string {
6
7
  process.stderr.write(prompt);
@@ -26,3 +27,29 @@ export function promptRequired(label: string, current?: string): string {
26
27
  process.stderr.write(" (required)\n");
27
28
  }
28
29
  }
30
+
31
+ /** A/B picker for create template (returns cli or json). */
32
+ export function promptTemplateChoice(): CreateTemplateId {
33
+ const cli = CREATE_TEMPLATES.find((t) => t.id === "cli");
34
+ const json = CREATE_TEMPLATES.find((t) => t.id === "json");
35
+ if (!cli || !json) {
36
+ throw new Error("CREATE_TEMPLATES must include cli and json entries");
37
+ }
38
+ process.stderr.write("\nSelect a template:\n\n");
39
+ process.stderr.write(`A) ${cli.displayName} — ${cli.description}\n`);
40
+ process.stderr.write(`B) ${json.displayName} — ${json.description}\n\n`);
41
+ while (true) {
42
+ const ans = readPromptLine("Choice: ").toLowerCase();
43
+ if (ans === "a") return "cli";
44
+ if (ans === "b") return "json";
45
+ process.stderr.write(" Enter A or B.\n");
46
+ }
47
+ }
48
+
49
+ export function parseTemplateChoiceInput(value: string | undefined): CreateTemplateId | undefined {
50
+ if (!value) return undefined;
51
+ const normalized = value.trim().toLowerCase();
52
+ if (normalized === "a" || normalized === "cli") return "cli";
53
+ if (normalized === "b" || normalized === "json") return "json";
54
+ return normalizeCreateTemplateId(value);
55
+ }