argsbarg 4.1.1 → 5.0.2
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 +34 -1
- package/README.md +5 -5
- package/docs/README.md +3 -3
- package/docs/ai-skills.md +9 -9
- package/docs/bundled-docs.md +4 -4
- package/docs/cli-program.md +8 -8
- package/docs/config-schema.md +2 -2
- package/docs/configure.md +177 -0
- package/docs/developing.md +1 -1
- package/docs/distribution-homebrew.md +12 -9
- package/docs/mcp.md +9 -9
- package/examples/full-example/README.md +3 -3
- package/examples/full-example/justfile +6 -6
- package/examples/full-example/scripts/formula-shared.ts +3 -2
- package/examples/full-example/src/program.ts +3 -3
- package/examples/nested.ts +1 -1
- package/index.d.ts +23 -16
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +105 -61
- package/src/builtins/completion-group.ts +4 -6
- package/src/builtins/config.test.ts +8 -0
- package/src/builtins/configure-copy.ts +86 -0
- package/src/builtins/configure.ts +70 -0
- package/src/builtins/dispatch.ts +13 -33
- package/src/builtins/index.ts +1 -1
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/registry.ts +6 -6
- package/src/capabilities.ts +22 -13
- package/src/cli-tool/cli-smoke.test.ts +21 -3
- package/src/cli-tool/create.test.ts +36 -1
- package/src/cli-tool/create.ts +26 -4
- package/src/cli-tool/full-example-capabilities.test.ts +10 -2
- package/src/cli-tool/main.ts +0 -0
- package/src/cli-tool/program.ts +20 -5
- package/src/cli-tool/run-create.ts +8 -19
- package/src/config/bootstrap.ts +11 -9
- package/src/config/context.test.ts +8 -0
- package/src/config/file.test.ts +13 -1
- package/src/config/resolve.test.ts +18 -0
- package/src/config/resolve.ts +2 -2
- package/src/config/validate.test.ts +11 -0
- package/src/config.integration.test.ts +5 -0
- package/src/configure/configure.test.ts +170 -0
- package/src/configure/index.ts +298 -0
- package/src/configure/prompt.ts +47 -0
- package/src/docs/api-guide.test.ts +10 -0
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +32 -5
- package/src/docs/mcp-guide.ts +4 -4
- package/src/docs/mcp-resources.test.ts +10 -0
- package/src/formats.test.ts +9 -0
- package/src/headless.test.ts +10 -0
- package/src/hidden-mcpb.test.ts +22 -0
- package/src/index.ts +2 -2
- package/src/install/binary-placement.test.ts +14 -0
- package/src/install/gh-release-update.test.ts +9 -0
- package/src/install/install-validate.test.ts +15 -5
- package/src/install/mcp-codex.test.ts +7 -0
- package/src/install/mcp-openclaw.test.ts +6 -0
- package/src/install/mcp-opencode.test.ts +10 -0
- package/src/install/opts.ts +17 -0
- package/src/install/status.test.ts +9 -0
- package/src/install/target-effective.ts +8 -8
- package/src/install/target-scope.ts +11 -8
- package/src/install/targets/configure.ts +1 -1
- package/src/install/targets.test.ts +23 -4
- package/src/invoke.test.ts +15 -1
- package/src/mcp/claude.test.ts +10 -0
- package/src/mcp/env.test.ts +12 -0
- package/src/mcp/tools.ts +1 -1
- package/src/mcp/zip.test.ts +5 -0
- package/src/mcp.integration.test.ts +43 -4
- package/src/parse.test.ts +82 -13
- package/src/schema.ts +1 -9
- package/src/skill/hint.ts +2 -2
- package/src/types.ts +22 -14
- package/src/validate.ts +17 -17
- package/docs/install.md +0 -206
- package/src/builtins/install.ts +0 -106
- package/src/builtins/uninstall.ts +0 -80
- package/src/install/index.ts +0 -409
- package/src/install/install.test.ts +0 -317
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Interactive and automated `configure` command orchestration (agent artifacts and app config).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolveCapabilities } from "../capabilities.ts";
|
|
6
|
+
import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
|
|
7
|
+
import { resolveInstallPaths } from "../install/paths.ts";
|
|
8
|
+
import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
|
|
9
|
+
import {
|
|
10
|
+
installErr,
|
|
11
|
+
installInfo,
|
|
12
|
+
installOut,
|
|
13
|
+
printInstallStatus,
|
|
14
|
+
writeInteractiveInstallIntro,
|
|
15
|
+
} from "../install/status.ts";
|
|
16
|
+
import { resolveEffectiveInstallTargets } from "../install/target-effective.ts";
|
|
17
|
+
import { runTargetPreflight } from "../install/target-plan-build.ts";
|
|
18
|
+
import { INSTALL_TARGETS, installTargetForKey } from "../install/target-registry.ts";
|
|
19
|
+
import { buildDetectedSnapshot, buildTargetPlanContext } from "../install/target-scope.ts";
|
|
20
|
+
import type {
|
|
21
|
+
CliInstallArtifactKey,
|
|
22
|
+
InstallAction,
|
|
23
|
+
InstallActionKind,
|
|
24
|
+
InstallOpts,
|
|
25
|
+
TargetPlanContext,
|
|
26
|
+
UninstallAction,
|
|
27
|
+
} from "../install/target-types.ts";
|
|
28
|
+
import {
|
|
29
|
+
buildUninstallPlan,
|
|
30
|
+
skillDirFromUninstallSummary,
|
|
31
|
+
uninstallSkillDir,
|
|
32
|
+
} from "../install/uninstall.ts";
|
|
33
|
+
import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
|
|
34
|
+
import type { CliProgram } from "../types.ts";
|
|
35
|
+
import { artifactPromptLabel, promptTargetAction } from "./prompt.ts";
|
|
36
|
+
|
|
37
|
+
/** True when interactive `configure` should auto-run the app config wizard. */
|
|
38
|
+
export function appConfigHasEntries(program: CliProgram): boolean {
|
|
39
|
+
const entries = program.appConfig?.entries;
|
|
40
|
+
return !!entries && Object.keys(entries).length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parsed flags for the top-level `configure` built-in. */
|
|
44
|
+
export interface ConfigureOpts {
|
|
45
|
+
sync?: boolean;
|
|
46
|
+
removeAll?: boolean;
|
|
47
|
+
removeConfig?: boolean;
|
|
48
|
+
status?: boolean;
|
|
49
|
+
yes?: boolean;
|
|
50
|
+
dry?: boolean;
|
|
51
|
+
json?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Maps raw argv flags into {@link ConfigureOpts}. */
|
|
55
|
+
export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
|
|
56
|
+
const flag = (name: string) => raw[name] === "1";
|
|
57
|
+
return {
|
|
58
|
+
sync: flag("sync"),
|
|
59
|
+
removeAll: flag("remove-all"),
|
|
60
|
+
removeConfig: flag("remove-config"),
|
|
61
|
+
status: flag("status"),
|
|
62
|
+
yes: flag("yes"),
|
|
63
|
+
dry: flag("dry"),
|
|
64
|
+
json: flag("json"),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns an error message when configure flags are inconsistent; otherwise null. */
|
|
69
|
+
export function validateConfigureOpts(opts: ConfigureOpts): string | null {
|
|
70
|
+
const flags = [opts.sync, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
|
|
71
|
+
if (flags.length > 1) {
|
|
72
|
+
return "Use only one of --sync, --remove-all, --remove-config, or --status.";
|
|
73
|
+
}
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
opts.yes = true;
|
|
76
|
+
}
|
|
77
|
+
if ((opts.sync || opts.removeAll || opts.removeConfig) && !opts.yes) {
|
|
78
|
+
return "--yes is required with --sync, --remove-all, or --remove-config.";
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Adapts configure flags to internal install-plan option shape. */
|
|
84
|
+
function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
|
|
85
|
+
if (opts.status) {
|
|
86
|
+
return { status: true, yes: opts.yes, dry: opts.dry, json: opts.json };
|
|
87
|
+
}
|
|
88
|
+
if (opts.sync) {
|
|
89
|
+
return { reinstall: true, yes: true, dry: opts.dry, json: opts.json };
|
|
90
|
+
}
|
|
91
|
+
if (opts.removeAll) {
|
|
92
|
+
return { uninstall: true, all: true, yes: true, dry: opts.dry, json: opts.json };
|
|
93
|
+
}
|
|
94
|
+
if (opts.removeConfig) {
|
|
95
|
+
return { uninstall: true, configure: true, yes: true, dry: opts.dry, json: opts.json };
|
|
96
|
+
}
|
|
97
|
+
return { dry: opts.dry, json: opts.json };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Installs a skill target and returns changed paths. */
|
|
101
|
+
function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: InstallOpts): string[] {
|
|
102
|
+
const target = skillTargetFromActionKind(kind);
|
|
103
|
+
if (!target) return [];
|
|
104
|
+
return cliSkillInstall(root, target, {
|
|
105
|
+
global: true,
|
|
106
|
+
rimraf: true,
|
|
107
|
+
dry: opts.dry,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Runs install or uninstall actions and collects changed paths. */
|
|
112
|
+
function executePlan(
|
|
113
|
+
root: CliProgram,
|
|
114
|
+
actions: Array<InstallAction | UninstallAction>,
|
|
115
|
+
opts: InstallOpts,
|
|
116
|
+
showProgress: boolean,
|
|
117
|
+
): string[] {
|
|
118
|
+
const changed: string[] = [];
|
|
119
|
+
const paths = resolveInstallPaths(root);
|
|
120
|
+
for (const action of actions) {
|
|
121
|
+
if (showProgress) {
|
|
122
|
+
installInfo(action.message, opts);
|
|
123
|
+
}
|
|
124
|
+
if ("kind" in action && action.kind) {
|
|
125
|
+
const skillTarget = skillTargetFromActionKind(action.kind);
|
|
126
|
+
if (skillTarget) {
|
|
127
|
+
changed.push(...runSkillAction(root, action.kind as InstallActionKind, opts));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!("kind" in action) || !action.kind) {
|
|
132
|
+
const skillDir = skillDirFromUninstallSummary(action.summary, paths);
|
|
133
|
+
if (skillDir) {
|
|
134
|
+
changed.push(...uninstallSkillDir(skillDir, !!opts.dry));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
changed.push(...action.run());
|
|
139
|
+
}
|
|
140
|
+
return changed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Builds plan context limited to a single artifact key. */
|
|
144
|
+
function buildSingleTargetContext(
|
|
145
|
+
root: CliProgram,
|
|
146
|
+
paths: ReturnType<typeof resolveInstallPaths>,
|
|
147
|
+
detected: ReturnType<typeof buildDetectedSnapshot>,
|
|
148
|
+
key: CliInstallArtifactKey,
|
|
149
|
+
mode: "install" | "uninstall",
|
|
150
|
+
opts: InstallOpts,
|
|
151
|
+
): TargetPlanContext {
|
|
152
|
+
const effective = resolveEffectiveInstallTargets(root.configure, root);
|
|
153
|
+
const base = buildTargetPlanContext(root, paths, opts, detected);
|
|
154
|
+
return {
|
|
155
|
+
...base,
|
|
156
|
+
mode: mode === "install" ? "install-scoped" : "uninstall-scoped",
|
|
157
|
+
include: (k) => k === key && effective[k].enabled,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Resolves install or uninstall actions for one artifact target. */
|
|
162
|
+
function actionsForTarget(
|
|
163
|
+
root: CliProgram,
|
|
164
|
+
paths: ReturnType<typeof resolveInstallPaths>,
|
|
165
|
+
key: CliInstallArtifactKey,
|
|
166
|
+
action: "install" | "uninstall",
|
|
167
|
+
opts: InstallOpts,
|
|
168
|
+
): Array<InstallAction | UninstallAction> {
|
|
169
|
+
const detected = buildDetectedSnapshot(root, paths);
|
|
170
|
+
const target = installTargetForKey(key);
|
|
171
|
+
if (!target) return [];
|
|
172
|
+
const ctx = buildSingleTargetContext(root, paths, detected, key, action, opts);
|
|
173
|
+
if (action === "install") {
|
|
174
|
+
return target.planInstall(ctx);
|
|
175
|
+
}
|
|
176
|
+
return target.planUninstall(ctx);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Walks enabled targets with per-target prompts (TTY required). */
|
|
180
|
+
async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
|
|
181
|
+
if (!process.stdin.isTTY) {
|
|
182
|
+
throw new Error("Interactive configure requires a TTY. Use flags such as --sync --yes.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
writeInteractiveInstallIntro(root);
|
|
186
|
+
const paths = resolveInstallPaths(root);
|
|
187
|
+
const detected = buildDetectedSnapshot(root, paths);
|
|
188
|
+
const effective = resolveEffectiveInstallTargets(root.configure, root);
|
|
189
|
+
const mutationOpts: InstallOpts = { dry: opts.dry, json: opts.json };
|
|
190
|
+
const changed: string[] = [];
|
|
191
|
+
|
|
192
|
+
for (const target of INSTALL_TARGETS) {
|
|
193
|
+
if (target.key === "app") continue;
|
|
194
|
+
if (!effective[target.key].enabled) continue;
|
|
195
|
+
if (target.key !== "configure" && !target.isAvailable(root, paths)) continue;
|
|
196
|
+
if (target.key === "configure" && !root.appConfig) continue;
|
|
197
|
+
|
|
198
|
+
if (target.key === "configure") {
|
|
199
|
+
if (!appConfigHasEntries(root)) continue;
|
|
200
|
+
|
|
201
|
+
const result = runConfigure(root, { context: "standalone", showHeading: false });
|
|
202
|
+
if (result.changed) {
|
|
203
|
+
installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
|
|
204
|
+
changed.push(displayAppConfigPath(root));
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const label = artifactPromptLabel(target.key);
|
|
210
|
+
const installed = target.detectedForSnapshot(detected);
|
|
211
|
+
|
|
212
|
+
const statusHint = installed ? "installed" : "not installed";
|
|
213
|
+
process.stderr.write(`\n${label} (${statusHint})\n`);
|
|
214
|
+
const choice = promptTargetAction(label, installed);
|
|
215
|
+
if (!choice || choice === "skip") continue;
|
|
216
|
+
|
|
217
|
+
const actions = actionsForTarget(root, paths, target.key, choice, mutationOpts);
|
|
218
|
+
if (actions.length === 0) continue;
|
|
219
|
+
|
|
220
|
+
const installActions = actions.filter(
|
|
221
|
+
(a): a is InstallAction =>
|
|
222
|
+
"kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
|
|
223
|
+
);
|
|
224
|
+
if (choice === "install" && resolveCapabilities(root).mcp && installActions.length > 0) {
|
|
225
|
+
runTargetPreflight(root, paths, mutationOpts, installActions);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
changed.push(...executePlan(root, actions, mutationOpts, true));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return changed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Runs sync, remove, or status modes without per-target prompts. */
|
|
235
|
+
async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
|
|
236
|
+
const installOpts = configureToInstallOpts(opts);
|
|
237
|
+
const paths = resolveInstallPaths(root);
|
|
238
|
+
|
|
239
|
+
if (installOpts.status) {
|
|
240
|
+
printInstallStatus(root, installOpts);
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let actions: Array<InstallAction | UninstallAction>;
|
|
245
|
+
if (installOpts.uninstall) {
|
|
246
|
+
actions = buildUninstallPlan(root, paths, installOpts);
|
|
247
|
+
} else if (installOpts.reinstall) {
|
|
248
|
+
actions = buildUpdatePlan(root, paths, installOpts);
|
|
249
|
+
} else {
|
|
250
|
+
actions = buildInstallPlan(root, paths, installOpts);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const installActions = actions.filter(
|
|
254
|
+
(a): a is InstallAction => "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
|
|
255
|
+
);
|
|
256
|
+
if (!installOpts.uninstall && resolveCapabilities(root).mcp && installActions.length > 0) {
|
|
257
|
+
runTargetPreflight(root, paths, installOpts, installActions);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return executePlan(root, actions, installOpts, true);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Main configure command orchestrator. */
|
|
264
|
+
export async function cliConfigure(
|
|
265
|
+
root: CliProgram,
|
|
266
|
+
rawOpts: Record<string, string>,
|
|
267
|
+
): Promise<never> {
|
|
268
|
+
const opts = parseConfigureOpts(rawOpts);
|
|
269
|
+
const err = validateConfigureOpts(opts);
|
|
270
|
+
if (err) {
|
|
271
|
+
installErr(err);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const isInteractive = !opts.sync && !opts.removeAll && !opts.removeConfig && !opts.status;
|
|
276
|
+
|
|
277
|
+
let changed: string[] = [];
|
|
278
|
+
try {
|
|
279
|
+
changed = isInteractive
|
|
280
|
+
? await runInteractiveConfigure(root, opts)
|
|
281
|
+
: await runAutomatedConfigure(root, opts);
|
|
282
|
+
} catch (mutationErr) {
|
|
283
|
+
installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (opts.json && !opts.status) {
|
|
288
|
+
process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!opts.status && changed.length > 0) {
|
|
293
|
+
const verb = opts.removeAll || opts.removeConfig ? "Removed" : opts.sync ? "Synced" : "Updated";
|
|
294
|
+
installOut(`${verb} ${changed.length} file(s).`, configureToInstallOpts(opts));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
TTY prompts for per-target install, skip, or uninstall during interactive `configure`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readSync } from "node:fs";
|
|
6
|
+
import type { CliInstallArtifactKey } from "../install/target-types.ts";
|
|
7
|
+
|
|
8
|
+
/** Human-readable labels for each install artifact key in interactive prompts. */
|
|
9
|
+
const LABELS: Record<CliInstallArtifactKey, string> = {
|
|
10
|
+
app: "App binary",
|
|
11
|
+
cursorSkill: "Cursor skill",
|
|
12
|
+
claudeSkill: "Claude skill",
|
|
13
|
+
codexSkill: "Codex skill",
|
|
14
|
+
opencodeSkill: "OpenCode skill",
|
|
15
|
+
openclawSkill: "OpenClaw skill",
|
|
16
|
+
cursorMcp: "Cursor MCP",
|
|
17
|
+
claudeCodeMcp: "Claude Code MCP",
|
|
18
|
+
claudeDesktopMcp: "Claude Desktop MCP",
|
|
19
|
+
opencodeMcp: "OpenCode MCP",
|
|
20
|
+
codexMcp: "Codex MCP",
|
|
21
|
+
openclawMcp: "OpenClaw MCP",
|
|
22
|
+
chatgptMcp: "ChatGPT desktop MCP",
|
|
23
|
+
configure: "App config",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Returns the prompt label for an install artifact key. */
|
|
27
|
+
export function artifactPromptLabel(key: CliInstallArtifactKey): string {
|
|
28
|
+
return LABELS[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** User choice from a per-target Y/n or y/N prompt. */
|
|
32
|
+
export type TargetPromptAction = "install" | "skip" | "uninstall";
|
|
33
|
+
|
|
34
|
+
/** Prompt per target: Y/n when not installed, y/N when installed. */
|
|
35
|
+
export function promptTargetAction(label: string, installed: boolean): TargetPromptAction | null {
|
|
36
|
+
const hint = installed ? `${label} [y/N]: ` : `${label} [Y/n]: `;
|
|
37
|
+
process.stderr.write(hint);
|
|
38
|
+
const buf = Buffer.alloc(256);
|
|
39
|
+
const n = readSync(0, buf, { length: 256 });
|
|
40
|
+
const ans = buf.toString("utf8", 0, n).trim().toLowerCase();
|
|
41
|
+
if (installed) {
|
|
42
|
+
if (ans === "n") return "uninstall";
|
|
43
|
+
return "skip";
|
|
44
|
+
}
|
|
45
|
+
if (ans === "n") return "skip";
|
|
46
|
+
return "install";
|
|
47
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for docs/api-guide module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { expect, test } from "bun:test";
|
|
2
6
|
import { cliSchemaExport } from "../schema.ts";
|
|
3
7
|
import type { CliProgram } from "../types.ts";
|
|
@@ -45,6 +49,7 @@ const nestedFixture: CliProgram = {
|
|
|
45
49
|
],
|
|
46
50
|
};
|
|
47
51
|
|
|
52
|
+
/** Tests that generateApiGuideBody matches command section of full API guide. */
|
|
48
53
|
test("generateApiGuideBody matches command section of full API guide", () => {
|
|
49
54
|
const body = generateApiGuideBody(nestedFixture);
|
|
50
55
|
const full = generateApiGuide(nestedFixture);
|
|
@@ -53,6 +58,7 @@ test("generateApiGuideBody matches command section of full API guide", () => {
|
|
|
53
58
|
expect(body).not.toContain("CLI API reference");
|
|
54
59
|
});
|
|
55
60
|
|
|
61
|
+
/** Tests that generateApiGuide covers the same command keys as cliSchemaExport. */
|
|
56
62
|
test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
57
63
|
const md = generateApiGuide(nestedFixture);
|
|
58
64
|
const schema = cliSchemaExport(nestedFixture);
|
|
@@ -62,6 +68,7 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
|
|
|
62
68
|
expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
/** Tests that generateApiGuide resolves program key in install notes. */
|
|
65
72
|
test("generateApiGuide resolves program key in install notes", () => {
|
|
66
73
|
const fixture: CliProgram = {
|
|
67
74
|
key: "myapp",
|
|
@@ -75,6 +82,7 @@ test("generateApiGuide resolves program key in install notes", () => {
|
|
|
75
82
|
expect(md).not.toContain("Upgrade to latest release");
|
|
76
83
|
});
|
|
77
84
|
|
|
85
|
+
/** Tests that generateApiGuide mentions Homebrew upgrade. */
|
|
78
86
|
test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
79
87
|
const fixture: CliProgram = {
|
|
80
88
|
key: "myapp",
|
|
@@ -87,6 +95,7 @@ test("generateApiGuide mentions Homebrew upgrade", () => {
|
|
|
87
95
|
expect(md).not.toContain("install --update");
|
|
88
96
|
});
|
|
89
97
|
|
|
98
|
+
/** Tests that generateApiGuide resolves {argsbarg:program} in consumer notes. */
|
|
90
99
|
test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
|
|
91
100
|
const fixture: CliProgram = {
|
|
92
101
|
key: "myapp",
|
|
@@ -105,6 +114,7 @@ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
|
|
|
105
114
|
expect(md).toContain("Invoke `myapp run`.");
|
|
106
115
|
});
|
|
107
116
|
|
|
117
|
+
/** Tests that generateApiGuide and cliSchemaExport include leaf outputSchema. */
|
|
108
118
|
test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
|
|
109
119
|
const fixture: CliProgram = {
|
|
110
120
|
key: "myapp",
|
package/src/docs/builtin.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { docsSkillTopicDescription } from "../builtins/configure-copy.ts";
|
|
2
|
+
import { resolveCapabilities } from "../capabilities.ts";
|
|
1
3
|
import {
|
|
2
4
|
CliFallbackMode,
|
|
3
5
|
type CliLeaf,
|
|
@@ -75,11 +77,7 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
|
|
|
75
77
|
leaves.push(
|
|
76
78
|
docsLeaf(program, "schema", "Print the full command tree as JSON."),
|
|
77
79
|
docsLeaf(program, "api", "Print the full command reference as markdown."),
|
|
78
|
-
docsLeaf(
|
|
79
|
-
program,
|
|
80
|
-
"skill",
|
|
81
|
-
"Print a reference agent SKILL, use `install --skill` for optimized.",
|
|
82
|
-
),
|
|
80
|
+
docsLeaf(program, "skill", docsSkillTopicDescription(program, resolveCapabilities(program))),
|
|
83
81
|
);
|
|
84
82
|
|
|
85
83
|
return {
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for docs/docs module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
6
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -49,6 +53,7 @@ function docsFixture(mcp = true): CliProgram {
|
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/** Docs reserved when enabled. */
|
|
52
57
|
test("docs reserved when enabled", () => {
|
|
53
58
|
const root: CliProgram = {
|
|
54
59
|
...docsFixture(),
|
|
@@ -63,6 +68,7 @@ test("docs reserved when enabled", () => {
|
|
|
63
68
|
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: docs/);
|
|
64
69
|
});
|
|
65
70
|
|
|
71
|
+
/** Docs rejects reserved topic keys. */
|
|
66
72
|
test("docs rejects reserved topic keys", () => {
|
|
67
73
|
const root = docsFixture();
|
|
68
74
|
const docs = root.docs;
|
|
@@ -77,22 +83,26 @@ test("docs rejects reserved topic keys", () => {
|
|
|
77
83
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
78
84
|
});
|
|
79
85
|
|
|
86
|
+
/** DocsEffectiveDefaultTopic uses first topic key. */
|
|
80
87
|
test("docsEffectiveDefaultTopic uses first topic key", () => {
|
|
81
88
|
expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
|
|
82
89
|
});
|
|
83
90
|
|
|
91
|
+
/** Bare docs prints first topic via Cli.invoke. */
|
|
84
92
|
test("bare docs prints first topic via Cli.invoke", async () => {
|
|
85
93
|
const result = await new Cli(docsFixture()).invoke(["docs"]);
|
|
86
94
|
expect(result.exitCode).toBe(0);
|
|
87
95
|
expect(result.stdout).toContain("Hello README");
|
|
88
96
|
});
|
|
89
97
|
|
|
98
|
+
/** Docs readme prints bundled text. */
|
|
90
99
|
test("docs readme prints bundled text", async () => {
|
|
91
100
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme"]);
|
|
92
101
|
expect(result.exitCode).toBe(0);
|
|
93
102
|
expect(result.stdout).toContain("Hello README");
|
|
94
103
|
});
|
|
95
104
|
|
|
105
|
+
/** Docs defaultTopic override. */
|
|
96
106
|
test("docs defaultTopic override", async () => {
|
|
97
107
|
const root = docsFixture();
|
|
98
108
|
root.docs!.defaultTopic = "arch";
|
|
@@ -100,20 +110,23 @@ test("docs defaultTopic override", async () => {
|
|
|
100
110
|
expect(result.stdout).toContain("Architecture");
|
|
101
111
|
});
|
|
102
112
|
|
|
113
|
+
/** Docs mcp when MCP enabled. */
|
|
103
114
|
test("docs mcp when MCP enabled", async () => {
|
|
104
115
|
const result = await new Cli(docsFixture(true)).invoke(["docs", "mcp"]);
|
|
105
116
|
expect(result.exitCode).toBe(0);
|
|
106
117
|
expect(result.stdout).toContain("MCP server (myapp)");
|
|
107
118
|
expect(result.stdout).toContain("myapp mcp");
|
|
108
119
|
expect(result.stdout).toContain("claude_desktop_config.json");
|
|
109
|
-
expect(result.stdout).toContain("
|
|
120
|
+
expect(result.stdout).toContain("configure --sync --yes");
|
|
110
121
|
});
|
|
111
122
|
|
|
123
|
+
/** Docs rejects unknown subcommand. */
|
|
112
124
|
test("docs rejects unknown subcommand", async () => {
|
|
113
125
|
const result = await new Cli(docsFixture()).invoke(["docs", "all"]);
|
|
114
126
|
expect(result.exitCode).not.toBe(0);
|
|
115
127
|
});
|
|
116
128
|
|
|
129
|
+
/** Docs mcp absent from router when MCP disabled. */
|
|
117
130
|
test("docs mcp absent from router when MCP disabled", async () => {
|
|
118
131
|
const root = docsFixture(false);
|
|
119
132
|
const presentation = cliPresentationRoot(root);
|
|
@@ -126,6 +139,7 @@ test("docs mcp absent from router when MCP disabled", async () => {
|
|
|
126
139
|
expect(result.exitCode).not.toBe(0);
|
|
127
140
|
});
|
|
128
141
|
|
|
142
|
+
/** Presentation includes docs subtree. */
|
|
129
143
|
test("presentation includes docs subtree", () => {
|
|
130
144
|
const presentation = cliPresentationRoot(docsFixture());
|
|
131
145
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -135,6 +149,7 @@ test("presentation includes docs subtree", () => {
|
|
|
135
149
|
).toBe(true);
|
|
136
150
|
});
|
|
137
151
|
|
|
152
|
+
/** Docs schema prints JSON. */
|
|
138
153
|
test("docs schema prints JSON", async () => {
|
|
139
154
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
|
|
140
155
|
expect(result.exitCode).toBe(0);
|
|
@@ -143,6 +158,7 @@ test("docs schema prints JSON", async () => {
|
|
|
143
158
|
expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
|
|
144
159
|
});
|
|
145
160
|
|
|
161
|
+
/** Docs api prints markdown reference. */
|
|
146
162
|
test("docs api prints markdown reference", async () => {
|
|
147
163
|
const result = await new Cli(docsFixture()).invoke(["docs", "api"]);
|
|
148
164
|
expect(result.exitCode).toBe(0);
|
|
@@ -152,6 +168,7 @@ test("docs api prints markdown reference", async () => {
|
|
|
152
168
|
expect(result.stdout).toContain("myapp docs schema");
|
|
153
169
|
});
|
|
154
170
|
|
|
171
|
+
/** Tests that skipsRequiredAppConfigExit includes docs and config builtins. */
|
|
155
172
|
test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
156
173
|
const program = {
|
|
157
174
|
...docsFixture(),
|
|
@@ -165,6 +182,7 @@ test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
|
165
182
|
expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
|
|
166
183
|
});
|
|
167
184
|
|
|
185
|
+
/** Docs skill prints Cursor SKILL.md. */
|
|
168
186
|
test("docs skill prints Cursor SKILL.md", async () => {
|
|
169
187
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
|
|
170
188
|
expect(result.exitCode).toBe(0);
|
|
@@ -176,20 +194,22 @@ test("docs skill prints Cursor SKILL.md", async () => {
|
|
|
176
194
|
expect(result.stdout).not.toContain("mcp.json");
|
|
177
195
|
});
|
|
178
196
|
|
|
179
|
-
|
|
197
|
+
/** Docs skill help recommends configure. */
|
|
198
|
+
test("docs skill help recommends configure", async () => {
|
|
180
199
|
const presentation = cliPresentationRoot(docsFixture());
|
|
181
200
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
182
201
|
expect(docsNode && "commands" in docsNode).toBe(true);
|
|
183
202
|
if (docsNode && "commands" in docsNode) {
|
|
184
203
|
const skill = docsNode.commands.find((c) => c.key === "skill");
|
|
185
204
|
expect(skill?.description).toContain("reference agent SKILL");
|
|
186
|
-
expect(skill?.description).toContain("
|
|
205
|
+
expect(skill?.description).toContain("configure");
|
|
187
206
|
expect(skill?.notes).toBeUndefined();
|
|
188
207
|
expect(docsNode.notes).toContain("--save");
|
|
189
208
|
expect(docsNode.notes).not.toContain("install --skill");
|
|
190
209
|
}
|
|
191
210
|
});
|
|
192
211
|
|
|
212
|
+
/** Presentation includes docs schema and skill. */
|
|
193
213
|
test("presentation includes docs schema and skill", () => {
|
|
194
214
|
const presentation = cliPresentationRoot(docsFixture());
|
|
195
215
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -201,6 +221,7 @@ test("presentation includes docs schema and skill", () => {
|
|
|
201
221
|
}
|
|
202
222
|
});
|
|
203
223
|
|
|
224
|
+
/** Completions offer docs subcommands. */
|
|
204
225
|
test("completions offer docs subcommands", () => {
|
|
205
226
|
const bash = completionBashScript(cliPresentationRoot(docsFixture()));
|
|
206
227
|
expect(bash).toContain("docs) echo");
|
|
@@ -210,19 +231,21 @@ test("completions offer docs subcommands", () => {
|
|
|
210
231
|
expect(bash).toContain("skill) echo");
|
|
211
232
|
});
|
|
212
233
|
|
|
213
|
-
|
|
234
|
+
/** Tests that generateMcpGuide includes schema URI and configure sync. */
|
|
235
|
+
test("generateMcpGuide includes schema URI and configure sync", () => {
|
|
214
236
|
const guide = generateMcpGuide(docsFixture(true));
|
|
215
237
|
expect(guide).toContain("myapp://schema");
|
|
216
238
|
expect(guide).toContain("~/.cursor/mcp.json");
|
|
217
239
|
expect(guide).toContain("claude_desktop_config.json");
|
|
218
240
|
expect(guide).toContain("## Installation");
|
|
219
241
|
expect(guide).toContain("## Running directly");
|
|
220
|
-
expect(guide).toContain("
|
|
242
|
+
expect(guide).toContain("configure --sync");
|
|
221
243
|
expect(guide).toContain("brew install myapp");
|
|
222
244
|
expect(guide).toContain("OpenAI Codex");
|
|
223
245
|
expect(guide).toContain("ChatGPT");
|
|
224
246
|
});
|
|
225
247
|
|
|
248
|
+
/** Docs --save writes topic file. */
|
|
226
249
|
test("docs --save writes topic file", async () => {
|
|
227
250
|
const result = await new Cli(docsFixture()).invoke(["docs", "readme", "--save"]);
|
|
228
251
|
expect(result.exitCode).toBe(0);
|
|
@@ -232,6 +255,7 @@ test("docs --save writes topic file", async () => {
|
|
|
232
255
|
expect(text).not.toContain("Generated by");
|
|
233
256
|
});
|
|
234
257
|
|
|
258
|
+
/** Docs api --save prepends generated hint. */
|
|
235
259
|
test("docs api --save prepends generated hint", async () => {
|
|
236
260
|
const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
|
|
237
261
|
expect(result.exitCode).toBe(0);
|
|
@@ -242,6 +266,7 @@ test("docs api --save prepends generated hint", async () => {
|
|
|
242
266
|
expect(text).toContain("CLI API reference");
|
|
243
267
|
});
|
|
244
268
|
|
|
269
|
+
/** Docs skill --save keeps frontmatter first. */
|
|
245
270
|
test("docs skill --save keeps frontmatter first", async () => {
|
|
246
271
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill", "--save"]);
|
|
247
272
|
expect(result.exitCode).toBe(0);
|
|
@@ -252,6 +277,7 @@ test("docs skill --save keeps frontmatter first", async () => {
|
|
|
252
277
|
expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
|
|
253
278
|
});
|
|
254
279
|
|
|
280
|
+
/** Docs schema --save writes JSON file. */
|
|
255
281
|
test("docs schema --save writes JSON file", async () => {
|
|
256
282
|
const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
|
|
257
283
|
expect(result.exitCode).toBe(0);
|
|
@@ -262,6 +288,7 @@ test("docs schema --save writes JSON file", async () => {
|
|
|
262
288
|
expect(schema.key).toBe("myapp");
|
|
263
289
|
});
|
|
264
290
|
|
|
291
|
+
/** Tests that saveDocsTopic returns relative path. */
|
|
265
292
|
test("saveDocsTopic returns relative path", () => {
|
|
266
293
|
const path = saveDocsTopic(docsFixture(), "api");
|
|
267
294
|
expect(path).toBe("docs/api.md");
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -105,11 +105,11 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
105
105
|
"",
|
|
106
106
|
"## Installation",
|
|
107
107
|
"",
|
|
108
|
-
"### `
|
|
108
|
+
"### `configure`",
|
|
109
109
|
"",
|
|
110
110
|
];
|
|
111
111
|
|
|
112
|
-
if (caps.
|
|
112
|
+
if (caps.configure) {
|
|
113
113
|
lines.push(
|
|
114
114
|
`Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`brew install ${root.key}\`). Host configs reference the app by name.`,
|
|
115
115
|
"",
|
|
@@ -123,7 +123,7 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
123
123
|
|
|
124
124
|
lines.push(
|
|
125
125
|
"```bash",
|
|
126
|
-
`${root.key}
|
|
126
|
+
`${root.key} configure --sync --yes`,
|
|
127
127
|
"```",
|
|
128
128
|
"",
|
|
129
129
|
"Merges the server entry below into host config when each host is present:",
|
|
@@ -177,7 +177,7 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
177
177
|
if (root.appConfig?.entries && Object.keys(root.appConfig.entries).length > 0) {
|
|
178
178
|
lines.push("## Configuration", "");
|
|
179
179
|
lines.push(
|
|
180
|
-
`Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key}
|
|
180
|
+
`Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key} configure\`.`,
|
|
181
181
|
"",
|
|
182
182
|
`Default config file: \`${displayAppConfigPath(root)}\` (flat JSON keys).`,
|
|
183
183
|
"",
|