argsbarg 4.0.4 → 4.1.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.
- package/CHANGELOG.md +77 -1
- package/README.md +91 -85
- package/docs/README.md +6 -6
- package/docs/ai-skills.md +8 -5
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +9 -7
- package/docs/config-schema.md +37 -13
- package/docs/developing.md +8 -8
- package/docs/distribution-homebrew.md +103 -0
- package/docs/install.md +143 -106
- package/docs/mcp.md +23 -12
- package/docs/output-schema.md +1 -1
- package/examples/full-example/Formula/.gitkeep +0 -0
- package/examples/full-example/README.md +98 -0
- package/examples/full-example/biome.json +22 -0
- package/examples/{consumer-app → full-example}/bun.lock +2 -0
- package/examples/full-example/justfile +134 -0
- package/examples/{consumer-app → full-example}/package.json +10 -3
- package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
- package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/create-identity.ts +11 -0
- package/examples/full-example/scripts/formula-shared.ts +73 -0
- package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
- package/examples/full-example/scripts/print-identity.ts +27 -0
- package/examples/full-example/src/commands/echo/command.ts +21 -0
- package/examples/full-example/src/commands/status/command.test.ts +10 -0
- package/examples/full-example/src/commands/status/command.ts +36 -0
- package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
- package/examples/full-example/src/index.ts +10 -0
- package/examples/full-example/src/program.ts +57 -0
- package/examples/{consumer-app → full-example}/src/types.ts +1 -1
- package/examples/mcp-test.ts +8 -24
- package/examples/nested.ts +1 -3
- package/index.d.ts +81 -65
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +37 -22
- package/src/builtins/completion-group.ts +17 -15
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/dispatch.ts +25 -1
- package/src/builtins/install.ts +45 -82
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/registry.ts +2 -0
- package/src/builtins/uninstall.ts +80 -0
- package/src/capabilities.ts +5 -7
- package/src/cli-tool/cli-smoke.test.ts +19 -0
- package/src/cli-tool/create.test.ts +119 -0
- package/src/cli-tool/create.ts +380 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +82 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +149 -0
- package/src/config/bootstrap.ts +174 -66
- package/src/config/context.test.ts +22 -36
- package/src/config/context.ts +5 -4
- package/src/config/file.test.ts +66 -56
- package/src/config/file.ts +33 -25
- package/src/config/resolve.test.ts +192 -1
- package/src/config/resolve.ts +92 -13
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +4 -5
- package/src/docs/docs.test.ts +2 -1
- package/src/docs/mcp-guide.ts +7 -8
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +7 -10
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +239 -168
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +170 -90
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +20 -0
- package/src/install/paths.ts +18 -26
- package/src/install/plan.ts +40 -261
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -15
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +129 -0
- package/src/install/target-mcp-cli.ts +149 -0
- package/src/install/target-mcp-json.ts +130 -0
- package/src/install/target-plan-build.ts +67 -0
- package/src/install/target-registry.ts +57 -0
- package/src/install/target-scope.ts +253 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +129 -0
- package/src/install/targets/app.ts +60 -0
- package/src/install/targets/chatgpt-mcp.ts +12 -0
- package/src/install/targets/claude-code-mcp.ts +15 -0
- package/src/install/targets/claude-desktop-mcp.ts +12 -0
- package/src/install/targets/claude-skill.ts +16 -0
- package/src/install/targets/codex-mcp.ts +25 -0
- package/src/install/targets/codex-skill.ts +14 -0
- package/src/install/targets/configure.ts +63 -0
- package/src/install/targets/cursor-mcp.ts +15 -0
- package/src/install/targets/cursor-skill.ts +16 -0
- package/src/install/targets/index.ts +50 -0
- package/src/install/targets/openclaw-mcp.ts +25 -0
- package/src/install/targets/openclaw-skill.ts +17 -0
- package/src/install/targets/opencode-mcp.ts +101 -0
- package/src/install/targets/opencode-skill.ts +15 -0
- package/src/install/targets.test.ts +118 -0
- package/src/install/uninstall.ts +16 -152
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +14 -1
- package/src/mcp/claude.ts +11 -4
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +1 -1
- package/src/parse.test.ts +14 -2
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/prompt.ts +10 -0
- package/src/schema.ts +9 -1
- package/src/skill/generate.ts +18 -4
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +86 -7
- package/src/validate.ts +73 -9
- package/docs/templates/cursor/rules/cli-program.mdc +0 -31
- package/examples/config-app/main.ts +0 -20
- package/examples/config-app/program.ts +0 -81
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -57
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/install/binary.ts +0 -94
- package/src/install/completions.ts +0 -56
- package/src/install/update.test.ts +0 -108
- package/src/install/update.ts +0 -57
- /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
- /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/** `argsbarg create` — copy npm-shipped examples/full-example with substitutions. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
export interface CreateOptions {
|
|
9
|
+
key: string;
|
|
10
|
+
className: string;
|
|
11
|
+
tap: string;
|
|
12
|
+
homepage: string;
|
|
13
|
+
releaseRepo: string;
|
|
14
|
+
desc: string;
|
|
15
|
+
force: boolean;
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
check: boolean;
|
|
18
|
+
diff: boolean;
|
|
19
|
+
yes: boolean;
|
|
20
|
+
/** Keep file:../.. dep (in-repo template). */
|
|
21
|
+
devTemplate: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CREATE_IDENTITY_REL = "scripts/create-identity.ts";
|
|
25
|
+
|
|
26
|
+
const EXCLUDE_REL = new Set(["bun.lock", "HOMEBREW-SCAFFOLD.md", "node_modules", "dist"]);
|
|
27
|
+
|
|
28
|
+
export function packageRoot(): string {
|
|
29
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function templateDir(): string {
|
|
33
|
+
return join(packageRoot(), "examples/full-example");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isDevTemplateDir(baseDir: string): boolean {
|
|
37
|
+
return resolve(baseDir) === resolve(templateDir());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function keyToEnvPrefix(key: string): string {
|
|
41
|
+
return key
|
|
42
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
43
|
+
.replace(/^_+|_+$/g, "")
|
|
44
|
+
.toUpperCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function classNameFromKey(key: string): string {
|
|
48
|
+
return key
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tapLibraryParts(tap: string): { org: string; repo: string } {
|
|
55
|
+
const slash = tap.indexOf("/");
|
|
56
|
+
if (slash === -1) throw new Error(`Invalid tap (expected org/repo): ${tap}`);
|
|
57
|
+
return {
|
|
58
|
+
org: tap.slice(0, slash),
|
|
59
|
+
repo: tap.slice(slash + 1),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Parse `scripts/create-identity.ts` for inference and template defaults. */
|
|
64
|
+
export function parseCreateIdentityFile(
|
|
65
|
+
path: string,
|
|
66
|
+
): Partial<CreateOptions> & { envPrefix?: string } {
|
|
67
|
+
if (!existsSync(path)) return {};
|
|
68
|
+
const text = readFileSync(path, "utf8");
|
|
69
|
+
const pick = (field: string) => text.match(new RegExp(`${field}:\\s*"([^"]*)"`))?.[1];
|
|
70
|
+
return {
|
|
71
|
+
key: pick("key"),
|
|
72
|
+
className: pick("className"),
|
|
73
|
+
tap: pick("tap"),
|
|
74
|
+
homepage: pick("homepage"),
|
|
75
|
+
releaseRepo: pick("releaseRepo"),
|
|
76
|
+
desc: pick("desc"),
|
|
77
|
+
envPrefix: pick("envPrefix"),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function templateIdentity(): {
|
|
82
|
+
key: string;
|
|
83
|
+
className: string;
|
|
84
|
+
tap: string;
|
|
85
|
+
homepage: string;
|
|
86
|
+
releaseRepo: string;
|
|
87
|
+
desc: string;
|
|
88
|
+
envPrefix: string;
|
|
89
|
+
} {
|
|
90
|
+
const parsed = parseCreateIdentityFile(join(templateDir(), CREATE_IDENTITY_REL));
|
|
91
|
+
const key = parsed.key ?? "full-example";
|
|
92
|
+
return {
|
|
93
|
+
key,
|
|
94
|
+
className: parsed.className ?? classNameFromKey(key),
|
|
95
|
+
tap: parsed.tap ?? `local/${key}`,
|
|
96
|
+
homepage: parsed.homepage ?? "https://github.com/bdombro/bun-argsbarg",
|
|
97
|
+
releaseRepo: parsed.releaseRepo ?? "bdombro/bun-argsbarg",
|
|
98
|
+
desc: parsed.desc ?? "Argsbarg full example reference app",
|
|
99
|
+
envPrefix: parsed.envPrefix ?? keyToEnvPrefix(key),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function inferCreateOptions(
|
|
104
|
+
baseDir: string,
|
|
105
|
+
partial: Partial<CreateOptions>,
|
|
106
|
+
): Partial<CreateOptions> {
|
|
107
|
+
if (
|
|
108
|
+
partial.key &&
|
|
109
|
+
partial.className &&
|
|
110
|
+
partial.homepage &&
|
|
111
|
+
partial.releaseRepo &&
|
|
112
|
+
partial.desc &&
|
|
113
|
+
partial.tap
|
|
114
|
+
) {
|
|
115
|
+
return partial;
|
|
116
|
+
}
|
|
117
|
+
const fromIdentity = parseCreateIdentityFile(join(baseDir, CREATE_IDENTITY_REL));
|
|
118
|
+
return {
|
|
119
|
+
key: partial.key ?? fromIdentity.key,
|
|
120
|
+
className: partial.className ?? fromIdentity.className,
|
|
121
|
+
desc: partial.desc ?? fromIdentity.desc,
|
|
122
|
+
homepage: partial.homepage ?? fromIdentity.homepage,
|
|
123
|
+
releaseRepo: partial.releaseRepo ?? fromIdentity.releaseRepo,
|
|
124
|
+
tap: partial.tap ?? fromIdentity.tap,
|
|
125
|
+
force: partial.force,
|
|
126
|
+
dryRun: partial.dryRun,
|
|
127
|
+
check: partial.check,
|
|
128
|
+
diff: partial.diff,
|
|
129
|
+
yes: partial.yes,
|
|
130
|
+
devTemplate: partial.devTemplate,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveCreateOptions(
|
|
135
|
+
partial: Partial<CreateOptions>,
|
|
136
|
+
baseDir?: string,
|
|
137
|
+
): CreateOptions {
|
|
138
|
+
const merged = baseDir ? inferCreateOptions(baseDir, partial) : partial;
|
|
139
|
+
const tmpl = templateIdentity();
|
|
140
|
+
const key = merged.key ?? tmpl.key ?? "full-example";
|
|
141
|
+
const className = merged.className ?? classNameFromKey(key);
|
|
142
|
+
const devTemplate = merged.devTemplate ?? (baseDir ? isDevTemplateDir(baseDir) : false);
|
|
143
|
+
return {
|
|
144
|
+
key,
|
|
145
|
+
className,
|
|
146
|
+
tap: merged.tap ?? `local/${key}`,
|
|
147
|
+
homepage: merged.homepage ?? `https://github.com/${merged.releaseRepo ?? `example/${key}`}`,
|
|
148
|
+
releaseRepo: merged.releaseRepo ?? `example/${key}`,
|
|
149
|
+
desc: merged.desc ?? `${className} CLI`,
|
|
150
|
+
force: merged.force ?? false,
|
|
151
|
+
dryRun: merged.dryRun ?? false,
|
|
152
|
+
check: merged.check ?? false,
|
|
153
|
+
diff: merged.diff ?? false,
|
|
154
|
+
yes: merged.yes ?? false,
|
|
155
|
+
devTemplate,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderCreateIdentitySource(opts: CreateOptions): string {
|
|
160
|
+
const envPrefix = keyToEnvPrefix(opts.key);
|
|
161
|
+
return `/** CLI identity — substituted by \`argsbarg create\`. */
|
|
162
|
+
|
|
163
|
+
export const createIdentity = {
|
|
164
|
+
key: "${opts.key}",
|
|
165
|
+
className: "${opts.className}",
|
|
166
|
+
tap: "${opts.tap}",
|
|
167
|
+
homepage: "${opts.homepage}",
|
|
168
|
+
releaseRepo: "${opts.releaseRepo}",
|
|
169
|
+
desc: "${opts.desc}",
|
|
170
|
+
envPrefix: "${envPrefix}",
|
|
171
|
+
} as const;
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function tokenMap(opts: CreateOptions): Record<string, string> {
|
|
176
|
+
const { org: tapOrg, repo: tapRepo } = tapLibraryParts(opts.tap);
|
|
177
|
+
const envPrefix = keyToEnvPrefix(opts.key);
|
|
178
|
+
return {
|
|
179
|
+
key: opts.key,
|
|
180
|
+
className: opts.className,
|
|
181
|
+
envPrefix,
|
|
182
|
+
tap: opts.tap,
|
|
183
|
+
tapOrg,
|
|
184
|
+
tapRepo,
|
|
185
|
+
homepage: opts.homepage,
|
|
186
|
+
releaseRepo: opts.releaseRepo,
|
|
187
|
+
desc: opts.desc,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Substitute \`{key}\`-style placeholders; also replace template identity literals. */
|
|
192
|
+
export function substituteTemplateContent(content: string, opts: CreateOptions): string {
|
|
193
|
+
const tmpl = templateIdentity();
|
|
194
|
+
const tokens = tokenMap(opts);
|
|
195
|
+
const argsbargVersion = pkg.version;
|
|
196
|
+
|
|
197
|
+
const protectedSpans: string[] = [];
|
|
198
|
+
let out = content.replace(/\$\{[^}]+\}/g, (span) => {
|
|
199
|
+
const idx = protectedSpans.length;
|
|
200
|
+
protectedSpans.push(span);
|
|
201
|
+
return `@@PROTECT${idx}@@`;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
out = out.replace(
|
|
205
|
+
/(?<!\{)\{(key|className|envPrefix|tap|tapOrg|tapRepo|homepage|releaseRepo|desc)\}(?!\})/g,
|
|
206
|
+
(_, name: string) => tokens[name] ?? `{${name}}`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const literalPairs: [string, string][] = [
|
|
210
|
+
[tmpl.envPrefix, tokens.envPrefix],
|
|
211
|
+
[tmpl.tap, opts.tap],
|
|
212
|
+
[tmpl.releaseRepo, opts.releaseRepo],
|
|
213
|
+
[tmpl.homepage, opts.homepage],
|
|
214
|
+
[tmpl.desc, opts.desc],
|
|
215
|
+
[tmpl.className, opts.className],
|
|
216
|
+
[tmpl.key, opts.key],
|
|
217
|
+
[`**${tmpl.key} conventions:**`, `**${opts.key} conventions:**`],
|
|
218
|
+
["**App-specific conventions:**", `**${opts.key} conventions:**`],
|
|
219
|
+
];
|
|
220
|
+
for (const [from, to] of literalPairs) {
|
|
221
|
+
if (from && from !== to) {
|
|
222
|
+
out = out.split(from).join(to);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
out = out.replace(/@@PROTECT(\d+)@@/g, (_, idx: string) => protectedSpans[Number(idx)] ?? "");
|
|
227
|
+
|
|
228
|
+
if (!opts.devTemplate) {
|
|
229
|
+
out = out.replace(/"argsbarg":\s*"file:\.\.\/\.\."/, `"argsbarg": "^${argsbargVersion}"`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function shouldExcludeRel(rel: string): boolean {
|
|
236
|
+
const parts = rel.split("/");
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
if (EXCLUDE_REL.has(part)) return true;
|
|
239
|
+
}
|
|
240
|
+
return EXCLUDE_REL.has(rel);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function listTemplateFiles(): string[] {
|
|
244
|
+
const root = templateDir();
|
|
245
|
+
if (!existsSync(root)) return [];
|
|
246
|
+
const out: string[] = [];
|
|
247
|
+
const walk = (dir: string, prefix: string) => {
|
|
248
|
+
for (const name of readdirSync(dir)) {
|
|
249
|
+
const full = join(dir, name);
|
|
250
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
251
|
+
if (statSync(full).isDirectory()) {
|
|
252
|
+
if (!shouldExcludeRel(rel)) walk(full, rel);
|
|
253
|
+
} else if (!shouldExcludeRel(rel)) {
|
|
254
|
+
out.push(rel);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
walk(root, "");
|
|
259
|
+
return out.sort();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function renderCreateTree(opts: CreateOptions): Map<string, string> {
|
|
263
|
+
const files = new Map<string, string>();
|
|
264
|
+
for (const rel of listTemplateFiles()) {
|
|
265
|
+
if (rel === CREATE_IDENTITY_REL) {
|
|
266
|
+
files.set(rel, renderCreateIdentitySource(opts));
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const src = join(templateDir(), rel);
|
|
270
|
+
const raw = readFileSync(src, "utf8");
|
|
271
|
+
files.set(rel, substituteTemplateContent(raw, opts));
|
|
272
|
+
}
|
|
273
|
+
return files;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function applyCreate(baseDir: string, opts: CreateOptions): string[] {
|
|
277
|
+
const written: string[] = [];
|
|
278
|
+
const tree = renderCreateTree(opts);
|
|
279
|
+
for (const [rel, content] of tree) {
|
|
280
|
+
const dest = join(baseDir, rel);
|
|
281
|
+
if (existsSync(dest) && !opts.force && !opts.check && !opts.dryRun) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (opts.dryRun || opts.check) {
|
|
285
|
+
written.push(dest);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
289
|
+
writeFileSync(dest, content, "utf8");
|
|
290
|
+
written.push(dest);
|
|
291
|
+
}
|
|
292
|
+
return written;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface CreateDrift {
|
|
296
|
+
rel: string;
|
|
297
|
+
expected: string;
|
|
298
|
+
actual?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function diffCreateDetails(baseDir: string, partial: Partial<CreateOptions>): CreateDrift[] {
|
|
302
|
+
const opts = resolveCreateOptions(partial, baseDir);
|
|
303
|
+
const drifts: CreateDrift[] = [];
|
|
304
|
+
const tree = renderCreateTree(opts);
|
|
305
|
+
for (const [rel, expected] of tree) {
|
|
306
|
+
const dest = join(baseDir, rel);
|
|
307
|
+
if (!existsSync(dest)) {
|
|
308
|
+
drifts.push({ rel: `${rel} (missing)`, expected, actual: undefined });
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const actual = readFileSync(dest, "utf8");
|
|
312
|
+
if (actual !== expected) {
|
|
313
|
+
drifts.push({ rel, expected, actual });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return drifts;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function diffCreate(baseDir: string, partial: Partial<CreateOptions>): string[] {
|
|
320
|
+
return diffCreateDetails(baseDir, partial).map((d) => d.rel);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Print a short unified diff for drifted files. */
|
|
324
|
+
export function printCreateDiffs(drifts: CreateDrift[], baseDir: string): void {
|
|
325
|
+
for (const drift of drifts) {
|
|
326
|
+
if (drift.actual === undefined) {
|
|
327
|
+
process.stderr.write(`--- missing ${drift.rel}\n`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const relPath = drift.rel;
|
|
331
|
+
process.stderr.write(`--- ${relPath}\n`);
|
|
332
|
+
const expectedLines = drift.expected.split("\n");
|
|
333
|
+
const actualLines = drift.actual.split("\n");
|
|
334
|
+
const max = Math.max(expectedLines.length, actualLines.length);
|
|
335
|
+
let shown = 0;
|
|
336
|
+
for (let i = 0; i < max && shown < 12; i++) {
|
|
337
|
+
const exp = expectedLines[i];
|
|
338
|
+
const act = actualLines[i];
|
|
339
|
+
if (exp !== act) {
|
|
340
|
+
if (act !== undefined) process.stderr.write(`+ ${act}\n`);
|
|
341
|
+
if (exp !== undefined) process.stderr.write(`- ${exp}\n`);
|
|
342
|
+
shown++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (max > 12) {
|
|
346
|
+
process.stderr.write(` … (${relative(baseDir, join(baseDir, relPath))})\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface ParsedCreateArgv {
|
|
352
|
+
dir: string;
|
|
353
|
+
opts: Partial<CreateOptions>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function parseCreateArgv(rest: string[]): ParsedCreateArgv {
|
|
357
|
+
const opts: Partial<CreateOptions> = {};
|
|
358
|
+
const positional: string[] = [];
|
|
359
|
+
for (let i = 0; i < rest.length; i++) {
|
|
360
|
+
const a = rest[i];
|
|
361
|
+
if (a === "--check") opts.check = true;
|
|
362
|
+
else if (a === "--diff") opts.diff = true;
|
|
363
|
+
else if (a === "--force") opts.force = true;
|
|
364
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
365
|
+
else if (a === "--yes") opts.yes = true;
|
|
366
|
+
else if (a === "--key" && rest[i + 1]) opts.key = rest[++i];
|
|
367
|
+
else if (a === "--class-name" && rest[i + 1]) opts.className = rest[++i];
|
|
368
|
+
else if (a === "--tap" && rest[i + 1]) opts.tap = rest[++i];
|
|
369
|
+
else if (a === "--homepage" && rest[i + 1]) opts.homepage = rest[++i];
|
|
370
|
+
else if (a === "--release-repo" && rest[i + 1]) opts.releaseRepo = rest[++i];
|
|
371
|
+
else if (a === "--desc" && rest[i + 1]) opts.desc = rest[++i];
|
|
372
|
+
else if (a?.startsWith("--")) {
|
|
373
|
+
throw new Error(`Unknown option: ${a}`);
|
|
374
|
+
} else if (a) {
|
|
375
|
+
positional.push(a);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const dir = positional[0] ?? ".";
|
|
379
|
+
return { dir, opts };
|
|
380
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { resolveCapabilities } from "
|
|
5
|
-
import { CliOptionKind, type CliProgram } from "
|
|
4
|
+
import { resolveCapabilities } from "../capabilities.ts";
|
|
5
|
+
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const exampleRoot = join(import.meta.dir, "../../examples/full-example");
|
|
8
|
+
const programSource = readFileSync(join(exampleRoot, "src/program.ts"), "utf8");
|
|
8
9
|
|
|
9
|
-
/** Mirror of
|
|
10
|
+
/** Mirror of full-example/src/program.ts capability flags (in-repo types only). */
|
|
10
11
|
const sinkProgram = {
|
|
11
|
-
key: "
|
|
12
|
+
key: "full-example",
|
|
12
13
|
version: "1.0.0",
|
|
13
|
-
description: "
|
|
14
|
+
description: "Full example reference.",
|
|
14
15
|
appConfig: {
|
|
15
16
|
entries: {
|
|
16
|
-
apiToken: { description: "Token.", env: "
|
|
17
|
+
apiToken: { description: "Token.", env: "FULL_EXAMPLE_API_TOKEN" },
|
|
17
18
|
},
|
|
18
19
|
},
|
|
19
20
|
docs: {
|
|
@@ -21,9 +22,7 @@ const sinkProgram = {
|
|
|
21
22
|
topics: { readme: { text: "# readme\n" } },
|
|
22
23
|
},
|
|
23
24
|
mcpServer: { enabled: true },
|
|
24
|
-
install: {
|
|
25
|
-
updateGetLatest: async () => ({ path: "/bin/app", version: "1.0.0" }),
|
|
26
|
-
},
|
|
25
|
+
install: {},
|
|
27
26
|
commands: [
|
|
28
27
|
{
|
|
29
28
|
key: "status",
|
|
@@ -46,14 +45,17 @@ const sinkProgram = {
|
|
|
46
45
|
],
|
|
47
46
|
} satisfies CliProgram;
|
|
48
47
|
|
|
49
|
-
describe("
|
|
48
|
+
describe("full-example template", () => {
|
|
50
49
|
test("program source enables every builtin flag", () => {
|
|
51
50
|
expect(programSource).toContain("mcpServer: {");
|
|
52
51
|
expect(programSource).toContain("enabled: true");
|
|
53
52
|
expect(programSource).toContain("docs:");
|
|
54
|
-
expect(programSource).toContain("updateGetLatest");
|
|
55
53
|
expect(programSource).toContain("appConfig:");
|
|
56
|
-
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("status command defines outputSchema", () => {
|
|
57
|
+
const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
|
|
58
|
+
expect(statusSource).toContain("outputSchema:");
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
test("resolveCapabilities matches full sink shape", () => {
|
|
@@ -62,7 +64,6 @@ describe("consumer-app sink", () => {
|
|
|
62
64
|
mcp: true,
|
|
63
65
|
install: true,
|
|
64
66
|
docs: true,
|
|
65
|
-
update: true,
|
|
66
67
|
configCommands: true,
|
|
67
68
|
});
|
|
68
69
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Argsbarg package CLI (`bunx argsbarg`) — bootstrap and tooling; library API is `import from "argsbarg"`. */
|
|
3
|
+
|
|
4
|
+
import { Cli } from "../index.ts";
|
|
5
|
+
import { program } from "./program.ts";
|
|
6
|
+
|
|
7
|
+
const cli = new Cli(program);
|
|
8
|
+
await cli.run();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Post-create steps and git bootstrap for `argsbarg create`. */
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function isInsideGitWorkTree(dir: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
const proc = Bun.spawnSync(["git", "-C", dir, "rev-parse", "--show-toplevel"], {
|
|
9
|
+
stdout: "pipe",
|
|
10
|
+
stderr: "pipe",
|
|
11
|
+
});
|
|
12
|
+
return proc.exitCode === 0;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldSkipGitBootstrap(targetDir: string): boolean {
|
|
19
|
+
if (existsSync(join(targetDir, ".git"))) return true;
|
|
20
|
+
const parent = resolve(targetDir, "..");
|
|
21
|
+
if (parent === targetDir) return false;
|
|
22
|
+
if (isInsideGitWorkTree(parent) && !isInsideGitWorkTree(targetDir)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runPostCreate(targetDir: string, dryRun: boolean): Promise<void> {
|
|
29
|
+
const abs = resolve(targetDir);
|
|
30
|
+
const steps: Array<{ label: string; run: () => Promise<void> | void }> = [
|
|
31
|
+
{
|
|
32
|
+
label: "bun install",
|
|
33
|
+
run: () => {
|
|
34
|
+
if (dryRun) return;
|
|
35
|
+
const proc = Bun.spawnSync(["bun", "install"], {
|
|
36
|
+
cwd: abs,
|
|
37
|
+
stdout: "inherit",
|
|
38
|
+
stderr: "inherit",
|
|
39
|
+
});
|
|
40
|
+
if (proc.exitCode !== 0) throw new Error("bun install failed");
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "bun scripts/schemagen.ts",
|
|
45
|
+
run: () => {
|
|
46
|
+
if (dryRun) return;
|
|
47
|
+
const proc = Bun.spawnSync(["bun", "scripts/schemagen.ts"], {
|
|
48
|
+
cwd: abs,
|
|
49
|
+
stdout: "inherit",
|
|
50
|
+
stderr: "inherit",
|
|
51
|
+
});
|
|
52
|
+
if (proc.exitCode !== 0) throw new Error("schemagen failed");
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "bun test",
|
|
57
|
+
run: () => {
|
|
58
|
+
if (dryRun) return;
|
|
59
|
+
const proc = Bun.spawnSync(["bun", "test"], {
|
|
60
|
+
cwd: abs,
|
|
61
|
+
stdout: "inherit",
|
|
62
|
+
stderr: "inherit",
|
|
63
|
+
});
|
|
64
|
+
if (proc.exitCode !== 0) throw new Error("bun test failed");
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: "git init + Initial commit",
|
|
69
|
+
run: () => {
|
|
70
|
+
if (dryRun) return;
|
|
71
|
+
if (shouldSkipGitBootstrap(abs)) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
"Skipping git bootstrap (existing repo or nested in git work tree).\n",
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let proc = Bun.spawnSync(["git", "init"], {
|
|
78
|
+
cwd: abs,
|
|
79
|
+
stdout: "inherit",
|
|
80
|
+
stderr: "inherit",
|
|
81
|
+
});
|
|
82
|
+
if (proc.exitCode !== 0) throw new Error("git init failed");
|
|
83
|
+
proc = Bun.spawnSync(["git", "add", "-A"], {
|
|
84
|
+
cwd: abs,
|
|
85
|
+
stdout: "inherit",
|
|
86
|
+
stderr: "inherit",
|
|
87
|
+
});
|
|
88
|
+
if (proc.exitCode !== 0) throw new Error("git add failed");
|
|
89
|
+
proc = Bun.spawnSync(["git", "commit", "-m", "Initial commit"], {
|
|
90
|
+
cwd: abs,
|
|
91
|
+
stdout: "inherit",
|
|
92
|
+
stderr: "inherit",
|
|
93
|
+
});
|
|
94
|
+
if (proc.exitCode !== 0) throw new Error("git commit failed");
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const step of steps) {
|
|
100
|
+
process.stderr.write(`→ ${step.label}\n`);
|
|
101
|
+
await step.run();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function printPostCreatePlan(): void {
|
|
106
|
+
process.stderr.write("Post-create steps:\n");
|
|
107
|
+
process.stderr.write(" 1. bun install\n");
|
|
108
|
+
process.stderr.write(" 2. bun scripts/schemagen.ts\n");
|
|
109
|
+
process.stderr.write(" 3. bun test\n");
|
|
110
|
+
process.stderr.write(" 4. git init + Initial commit (skipped inside existing git work tree)\n");
|
|
111
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Argsbarg developer tools — bootstrap consumer CLIs via `create`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
6
|
+
import { CliOptionKind, type CliProgram } from "../index.ts";
|
|
7
|
+
import { runCreate } from "./run-create.ts";
|
|
8
|
+
|
|
9
|
+
export const program = {
|
|
10
|
+
key: "argsbarg",
|
|
11
|
+
version: pkg.version,
|
|
12
|
+
description: "Argsbarg developer tools — bootstrap CLIs from the full-example template.",
|
|
13
|
+
commands: [
|
|
14
|
+
{
|
|
15
|
+
key: "create",
|
|
16
|
+
description: "Copy the full-example template into a directory with substitutions.",
|
|
17
|
+
options: [
|
|
18
|
+
{ name: "key", description: "CLI binary name.", kind: CliOptionKind.String },
|
|
19
|
+
{
|
|
20
|
+
name: "class-name",
|
|
21
|
+
description: "Homebrew formula class name.",
|
|
22
|
+
kind: CliOptionKind.String,
|
|
23
|
+
},
|
|
24
|
+
{ name: "tap", description: "Homebrew tap (org/repo).", kind: CliOptionKind.String },
|
|
25
|
+
{ name: "homepage", description: "Formula homepage URL.", kind: CliOptionKind.String },
|
|
26
|
+
{
|
|
27
|
+
name: "release-repo",
|
|
28
|
+
description: "GitHub org/repo for release binary.",
|
|
29
|
+
kind: CliOptionKind.String,
|
|
30
|
+
},
|
|
31
|
+
{ name: "desc", description: "Formula description.", kind: CliOptionKind.String },
|
|
32
|
+
{ name: "force", description: "Overwrite existing files.", kind: CliOptionKind.Presence },
|
|
33
|
+
{
|
|
34
|
+
name: "dry-run",
|
|
35
|
+
description: "Print planned writes without changing disk.",
|
|
36
|
+
kind: CliOptionKind.Presence,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "check",
|
|
40
|
+
description: "Fail if directory drifts from template output.",
|
|
41
|
+
kind: CliOptionKind.Presence,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "diff",
|
|
45
|
+
description: "With --check, print a short diff for drifted files.",
|
|
46
|
+
kind: CliOptionKind.Presence,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "yes",
|
|
50
|
+
description: "Skip confirmation (required when stdin is not a TTY).",
|
|
51
|
+
kind: CliOptionKind.Presence,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
positionals: [
|
|
55
|
+
{
|
|
56
|
+
name: "dir",
|
|
57
|
+
description: "Target directory (default: current directory).",
|
|
58
|
+
kind: CliOptionKind.String,
|
|
59
|
+
argMin: 0,
|
|
60
|
+
argMax: 1,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
handler: async (ctx) => {
|
|
64
|
+
const code = await runCreate({
|
|
65
|
+
dir: ctx.args[0],
|
|
66
|
+
key: ctx.stringOpt("key"),
|
|
67
|
+
className: ctx.stringOpt("class-name"),
|
|
68
|
+
tap: ctx.stringOpt("tap"),
|
|
69
|
+
homepage: ctx.stringOpt("homepage"),
|
|
70
|
+
releaseRepo: ctx.stringOpt("release-repo"),
|
|
71
|
+
desc: ctx.stringOpt("desc"),
|
|
72
|
+
force: ctx.hasFlag("force"),
|
|
73
|
+
dryRun: ctx.hasFlag("dry-run"),
|
|
74
|
+
check: ctx.hasFlag("check"),
|
|
75
|
+
diff: ctx.hasFlag("diff"),
|
|
76
|
+
yes: ctx.hasFlag("yes"),
|
|
77
|
+
});
|
|
78
|
+
process.exit(code);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
} satisfies CliProgram;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Interactive prompts for `argsbarg create`. */
|
|
2
|
+
|
|
3
|
+
import { readPromptLine as readStdinLine } from "../prompt.ts";
|
|
4
|
+
|
|
5
|
+
export function readPromptLine(prompt: string): string {
|
|
6
|
+
process.stderr.write(prompt);
|
|
7
|
+
return readStdinLine().trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function promptConfirm(message: string): boolean {
|
|
11
|
+
const ans = readPromptLine(`${message} [y/N]: `);
|
|
12
|
+
return ans === "y" || ans === "Y";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function promptOptional(label: string, current?: string): string | undefined {
|
|
16
|
+
const suffix = current ? ` [${current}]` : "";
|
|
17
|
+
const ans = readPromptLine(`${label}${suffix}: `);
|
|
18
|
+
if (ans.length === 0) return current;
|
|
19
|
+
return ans;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function promptRequired(label: string, current?: string): string {
|
|
23
|
+
while (true) {
|
|
24
|
+
const value = promptOptional(label, current);
|
|
25
|
+
if (value && value.length > 0) return value;
|
|
26
|
+
process.stderr.write(" (required)\n");
|
|
27
|
+
}
|
|
28
|
+
}
|