argsbarg 5.1.16 → 6.0.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 +46 -1
- package/README.md +33 -25
- package/docs/README.md +2 -1
- package/docs/api-server.md +141 -0
- package/docs/bundled-docs.md +24 -10
- package/docs/cli-program.md +17 -2
- package/docs/config-schema.md +18 -8
- package/docs/developing.md +1 -1
- package/docs/mcp.md +5 -5
- package/docs/output-schema.md +78 -47
- package/examples/full-example/README.md +15 -6
- package/examples/full-example/docs/README.md +27 -0
- package/examples/full-example/docs/api.md +511 -0
- package/examples/full-example/docs/cli-schema.json +453 -0
- package/examples/full-example/docs/http.md +81 -0
- package/examples/full-example/docs/mcp.md +159 -0
- package/examples/full-example/docs/openapi.json +222 -0
- package/examples/full-example/docs/skill.md +46 -0
- package/examples/full-example/justfile +6 -2
- package/examples/full-example/schemas/generated/app-config.json +1 -1
- package/examples/full-example/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/dev-formula.ts +1 -1
- package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +3 -3
- package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +90 -35
- package/examples/full-example/scripts/schemagen/naming.ts +17 -0
- package/examples/full-example/scripts/schemagen.ts +14 -3
- package/examples/full-example/src/commands/echo/command.ts +6 -1
- package/examples/full-example/src/commands/status/schema-types.ts +14 -0
- package/examples/full-example/src/commands/status/types.ts +1 -11
- package/examples/full-example/src/{types.ts → config/schema-types.ts} +3 -2
- package/examples/full-example/src/program.ts +3 -0
- package/examples/mcp-test.ts +13 -2
- package/examples/nested.ts +12 -3
- package/examples/servers.ts +72 -0
- package/index.d.ts +66 -7
- package/package.json +17 -17
- package/src/api/openapi.ts +117 -0
- package/src/api/result.ts +111 -0
- package/src/api/schema-deref.test.ts +99 -0
- package/src/api/schema-deref.ts +76 -0
- package/src/api/server.ts +120 -0
- package/src/api.integration.test.ts +441 -0
- package/src/builtins/api.ts +38 -0
- package/src/builtins/dispatch.ts +26 -0
- package/src/builtins/registry.ts +4 -0
- package/src/capabilities.ts +12 -1
- package/src/cli-errors.ts +3 -0
- package/src/cli-tool/full-example-capabilities.test.ts +3 -0
- package/src/cli.ts +60 -8
- package/src/config.integration.test.ts +22 -4
- package/src/context.ts +29 -1
- package/src/docs/api-guide.ts +2 -2
- package/src/docs/builtin.ts +11 -1
- package/src/docs/docs.test.ts +70 -12
- package/src/docs/http-guide.ts +132 -0
- package/src/docs/mcp-guide.ts +3 -3
- package/src/docs/mcp-resources.ts +5 -2
- package/src/docs/resolve.ts +26 -2
- package/src/docs/save.ts +5 -2
- package/src/headless/tool-call.ts +157 -0
- package/src/headless.test.ts +4 -2
- package/src/headless.ts +10 -5
- package/src/index.ts +5 -0
- package/src/mcp/result.ts +39 -34
- package/src/mcp/server.ts +18 -36
- package/src/mcp/tools.ts +14 -3
- package/src/mcp.integration.test.ts +46 -39
- package/src/parse.test.ts +16 -6
- package/src/respond.ts +48 -0
- package/src/schema.ts +1 -1
- package/src/skill/generate.ts +1 -1
- package/src/types.ts +46 -4
- package/src/validate.ts +7 -0
package/src/builtins/dispatch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ParseResult } from "../parse.ts";
|
|
|
7
7
|
import { ParseKind } from "../parse.ts";
|
|
8
8
|
import type { CliNode, CliProgram, CliRouter } from "../types.ts";
|
|
9
9
|
import { isCliLeaf } from "../types.ts";
|
|
10
|
+
import { cliBuiltinApiCommand } from "./api.ts";
|
|
10
11
|
import { completionBashScript } from "./completion-bash.ts";
|
|
11
12
|
import { completionFishScript } from "./completion-fish.ts";
|
|
12
13
|
import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
|
|
@@ -68,6 +69,20 @@ export async function dispatchBuiltin(program: CliProgram, pr: ParseResult, opts
|
|
|
68
69
|
process.exit(0);
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
if (pr.path[0] === "api") {
|
|
73
|
+
if (!caps.api) {
|
|
74
|
+
process.stderr.write(capabilityDeniedMessage("api"));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const sub = pr.path[1];
|
|
78
|
+
if (pr.path.length === 1 || sub === "serve") {
|
|
79
|
+
await new Cli(program).serveApi();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
process.stderr.write(`Unknown subcommand: api ${pr.path.slice(1).join(" ")}\n`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
if (pr.path[0] === "mcp") {
|
|
72
87
|
if (!caps.mcp) {
|
|
73
88
|
process.stderr.write(capabilityDeniedMessage("mcp"));
|
|
@@ -142,6 +157,17 @@ export function builtinInterceptRoot(
|
|
|
142
157
|
};
|
|
143
158
|
}
|
|
144
159
|
|
|
160
|
+
if (first === "api" && caps.api) {
|
|
161
|
+
return {
|
|
162
|
+
parseRoot: {
|
|
163
|
+
key: program.key,
|
|
164
|
+
description: program.description,
|
|
165
|
+
commands: [cliBuiltinApiCommand(program)],
|
|
166
|
+
},
|
|
167
|
+
isLeafCompletionIntercept: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
if (first === "mcp" && caps.mcp) {
|
|
146
172
|
return {
|
|
147
173
|
parseRoot: {
|
package/src/builtins/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CliCapabilities } from "../capabilities.ts";
|
|
2
2
|
import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
|
|
3
3
|
import type { CliNode, CliProgram } from "../types.ts";
|
|
4
|
+
import { cliBuiltinApiCommand } from "./api.ts";
|
|
4
5
|
import { cliBuiltinCompletionGroup } from "./completion-group.ts";
|
|
5
6
|
import { cliBuiltinConfigureCommand } from "./configure.ts";
|
|
6
7
|
import { cliBuiltinMcpCommand } from "./mcp.ts";
|
|
@@ -32,5 +33,8 @@ export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): Cli
|
|
|
32
33
|
if (caps.mcp) {
|
|
33
34
|
pushBuiltin(builtins, program, (p) => cliBuiltinMcpCommand(p));
|
|
34
35
|
}
|
|
36
|
+
if (caps.api) {
|
|
37
|
+
pushBuiltin(builtins, program, (p) => cliBuiltinApiCommand(p));
|
|
38
|
+
}
|
|
35
39
|
return builtins;
|
|
36
40
|
}
|
package/src/capabilities.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { CliProgram } from "./types.ts";
|
|
|
8
8
|
|
|
9
9
|
/** Platform builtins derived from program config and runtime. */
|
|
10
10
|
export interface CliCapabilities {
|
|
11
|
+
api: boolean;
|
|
11
12
|
completion: boolean;
|
|
12
13
|
mcp: boolean;
|
|
13
14
|
configure: boolean;
|
|
@@ -19,6 +20,7 @@ export interface CliCapabilities {
|
|
|
19
20
|
export function resolveCapabilities(program: CliProgram): CliCapabilities {
|
|
20
21
|
const configure = program.configure?.enabled !== false;
|
|
21
22
|
return {
|
|
23
|
+
api: program.apiServer?.enabled === true,
|
|
22
24
|
completion: program.completion?.enabled !== false,
|
|
23
25
|
mcp: program.mcpServer?.enabled === true,
|
|
24
26
|
configure,
|
|
@@ -42,6 +44,9 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
|
|
|
42
44
|
if (caps.mcp) {
|
|
43
45
|
names.push("mcp");
|
|
44
46
|
}
|
|
47
|
+
if (caps.api) {
|
|
48
|
+
names.push("api");
|
|
49
|
+
}
|
|
45
50
|
return names;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -60,13 +65,15 @@ export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities
|
|
|
60
65
|
return false;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
export type CapabilityFeature = "mcp" | "configure" | "docs" | "completion";
|
|
68
|
+
export type CapabilityFeature = "api" | "mcp" | "configure" | "docs" | "completion";
|
|
64
69
|
|
|
65
70
|
/** Stderr message when a disabled built-in is invoked from the CLI. */
|
|
66
71
|
export function capabilityDeniedMessage(feature: CapabilityFeature): string {
|
|
67
72
|
switch (feature) {
|
|
68
73
|
case "completion":
|
|
69
74
|
return "Shell completion is not available for this app.\n";
|
|
75
|
+
case "api":
|
|
76
|
+
return "HTTP API is not available for this app.\n";
|
|
70
77
|
case "mcp":
|
|
71
78
|
return "MCP is not available for this app.\n";
|
|
72
79
|
case "configure":
|
|
@@ -90,6 +97,10 @@ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): voi
|
|
|
90
97
|
process.stderr.write(capabilityDeniedMessage("mcp"));
|
|
91
98
|
process.exit(1);
|
|
92
99
|
}
|
|
100
|
+
if (first === "api" && !caps.api) {
|
|
101
|
+
process.stderr.write(capabilityDeniedMessage("api"));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
93
104
|
if (first === "configure" && !caps.configure) {
|
|
94
105
|
process.stderr.write(capabilityDeniedMessage("configure"));
|
|
95
106
|
process.exit(1);
|
package/src/cli-errors.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { CliContext } from "./context.ts";
|
|
|
7
7
|
import { cliHelpRender } from "./help.ts";
|
|
8
8
|
|
|
9
9
|
export function cliErrWithHelp(ctx: CliContext, msg: string): never {
|
|
10
|
+
if (ctx.invocation === "api" || ctx.invocation === "mcp") {
|
|
11
|
+
throw new Error(msg);
|
|
12
|
+
}
|
|
10
13
|
const color = process.stderr.isTTY;
|
|
11
14
|
const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
|
|
12
15
|
process.stderr.write(`${line}\n`);
|
|
@@ -26,6 +26,7 @@ const sinkProgram = {
|
|
|
26
26
|
topics: { readme: { text: "# readme\n" } },
|
|
27
27
|
},
|
|
28
28
|
mcpServer: { enabled: true },
|
|
29
|
+
apiServer: { enabled: true },
|
|
29
30
|
configure: {},
|
|
30
31
|
commands: [
|
|
31
32
|
{
|
|
@@ -54,6 +55,7 @@ describe("full-example template", () => {
|
|
|
54
55
|
/** Tests that program source enables every builtin flag. */
|
|
55
56
|
test("program source enables every builtin flag", () => {
|
|
56
57
|
expect(programSource).toContain("mcpServer: {");
|
|
58
|
+
expect(programSource).toContain("apiServer: {");
|
|
57
59
|
expect(programSource).toContain("enabled: true");
|
|
58
60
|
expect(programSource).toContain("docs:");
|
|
59
61
|
expect(programSource).toContain("appConfig:");
|
|
@@ -66,6 +68,7 @@ describe("full-example template", () => {
|
|
|
66
68
|
|
|
67
69
|
test("resolveCapabilities matches full sink shape", () => {
|
|
68
70
|
expect(resolveCapabilities(sinkProgram)).toEqual({
|
|
71
|
+
api: true,
|
|
69
72
|
completion: true,
|
|
70
73
|
mcp: true,
|
|
71
74
|
configure: true,
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@ Runtime entry point: validate program, cache derived state, run / invoke / MCP s
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { format } from "node:util";
|
|
6
|
+
import { apiServeHttp } from "./api/server.ts";
|
|
6
7
|
import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
|
|
7
8
|
import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
|
|
8
9
|
import {
|
|
@@ -21,7 +22,7 @@ import { bootstrapMcpEnv } from "./mcp/env.ts";
|
|
|
21
22
|
import { mcpServeStdioLoop } from "./mcp/server.ts";
|
|
22
23
|
import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
|
|
23
24
|
import { type CliSchemaExport, cliSchemaExport } from "./schema.ts";
|
|
24
|
-
import type { CliHandler, CliLeaf, CliNode, CliProgram, CliRouter } from "./types.ts";
|
|
25
|
+
import type { CliHandler, CliInvocation, CliLeaf, CliNode, CliProgram, CliRespondOptions, CliRouter } from "./types.ts";
|
|
25
26
|
import { isCliLeaf, isCliRouter } from "./types.ts";
|
|
26
27
|
import { cliValidateProgram } from "./validate.ts";
|
|
27
28
|
|
|
@@ -35,6 +36,8 @@ export interface CliInvokeResult {
|
|
|
35
36
|
stdout: string;
|
|
36
37
|
stderr: string;
|
|
37
38
|
errorMsg?: string;
|
|
39
|
+
/** Headless response payload when invocation is `api` or `mcp` and the handler succeeded. */
|
|
40
|
+
response?: CliRespondOptions;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
class CliInvokeExit extends Error {
|
|
@@ -123,7 +126,10 @@ export class Cli {
|
|
|
123
126
|
|
|
124
127
|
const ctx = new CliContext(this.program.key, pr.path, pr.args, pr.opts, this.program, "cli", snapshot);
|
|
125
128
|
try {
|
|
126
|
-
await Promise.resolve(leaf.handler(ctx));
|
|
129
|
+
const handlerResult = await Promise.resolve(leaf.handler(ctx));
|
|
130
|
+
if (handlerResult !== undefined && ctx.getResponse() === undefined) {
|
|
131
|
+
ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
|
|
132
|
+
}
|
|
127
133
|
process.exit(0);
|
|
128
134
|
} catch (err) {
|
|
129
135
|
if (err instanceof Error) {
|
|
@@ -133,7 +139,11 @@ export class Cli {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
async invoke(
|
|
142
|
+
async invoke(
|
|
143
|
+
argv: string[],
|
|
144
|
+
opts?: { invocation?: CliInvocation; toolArgs?: Record<string, unknown> },
|
|
145
|
+
): Promise<CliInvokeResult> {
|
|
146
|
+
const invocation = opts?.invocation ?? "mcp";
|
|
137
147
|
const prep = this.prepareDispatch(argv, { presentationFallback: true });
|
|
138
148
|
if ("error" in prep) {
|
|
139
149
|
if (prep.error.kind === ParseKind.Help) {
|
|
@@ -142,7 +152,7 @@ export class Cli {
|
|
|
142
152
|
exitCode: 1,
|
|
143
153
|
stdout: "",
|
|
144
154
|
stderr: "",
|
|
145
|
-
errorMsg: "Help is not available via
|
|
155
|
+
errorMsg: "Help is not available via tool calls.",
|
|
146
156
|
};
|
|
147
157
|
}
|
|
148
158
|
return {
|
|
@@ -160,7 +170,16 @@ export class Cli {
|
|
|
160
170
|
exitOnMissing: false,
|
|
161
171
|
});
|
|
162
172
|
|
|
163
|
-
const ctx = new CliContext(
|
|
173
|
+
const ctx = new CliContext(
|
|
174
|
+
this.program.key,
|
|
175
|
+
pr.path,
|
|
176
|
+
pr.args,
|
|
177
|
+
pr.opts,
|
|
178
|
+
this.program,
|
|
179
|
+
invocation,
|
|
180
|
+
snapshot,
|
|
181
|
+
opts?.toolArgs,
|
|
182
|
+
);
|
|
164
183
|
|
|
165
184
|
let stdout = "";
|
|
166
185
|
let stderr = "";
|
|
@@ -213,12 +232,30 @@ export class Cli {
|
|
|
213
232
|
});
|
|
214
233
|
}
|
|
215
234
|
|
|
216
|
-
await Promise.resolve(leaf.handler(ctx));
|
|
217
|
-
|
|
235
|
+
const handlerResult = await Promise.resolve(leaf.handler(ctx));
|
|
236
|
+
if (handlerResult !== undefined && ctx.getResponse() === undefined) {
|
|
237
|
+
ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const response = ctx.getResponse();
|
|
241
|
+
return {
|
|
242
|
+
kind: "ok",
|
|
243
|
+
exitCode: 0,
|
|
244
|
+
stdout,
|
|
245
|
+
stderr,
|
|
246
|
+
...(response ? { response } : {}),
|
|
247
|
+
};
|
|
218
248
|
} catch (err) {
|
|
219
249
|
if (err instanceof CliInvokeExit) {
|
|
220
250
|
if (err.code === 0) {
|
|
221
|
-
|
|
251
|
+
const response = ctx.getResponse();
|
|
252
|
+
return {
|
|
253
|
+
kind: "ok",
|
|
254
|
+
exitCode: 0,
|
|
255
|
+
stdout,
|
|
256
|
+
stderr,
|
|
257
|
+
...(response ? { response } : {}),
|
|
258
|
+
};
|
|
222
259
|
}
|
|
223
260
|
const msg = stderr.trim() || `Exit code ${err.code}`;
|
|
224
261
|
return { kind: "error", exitCode: err.code, stdout, stderr, errorMsg: msg };
|
|
@@ -268,6 +305,21 @@ export class Cli {
|
|
|
268
305
|
}
|
|
269
306
|
}
|
|
270
307
|
|
|
308
|
+
async serveApi(): Promise<never> {
|
|
309
|
+
try {
|
|
310
|
+
bootstrapAppConfig(this.program, { validateFile: false });
|
|
311
|
+
await apiServeHttp(this);
|
|
312
|
+
process.exit(0);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err instanceof Error) {
|
|
315
|
+
process.stderr.write(`${err.message}\n`);
|
|
316
|
+
} else {
|
|
317
|
+
process.stderr.write("HTTP API server error.\n");
|
|
318
|
+
}
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
271
323
|
private prepareDispatch(
|
|
272
324
|
argv: string[],
|
|
273
325
|
opts?: { presentationFallback?: boolean },
|
|
@@ -75,9 +75,18 @@ test("MCP program.appConfig succeeds when env present", async () => {
|
|
|
75
75
|
],
|
|
76
76
|
{ script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "sekrit" } },
|
|
77
77
|
);
|
|
78
|
-
const res = responses.get(14) as {
|
|
78
|
+
const res = responses.get(14) as {
|
|
79
|
+
result: {
|
|
80
|
+
isError: boolean;
|
|
81
|
+
structuredContent?: { content: string; contentType: string };
|
|
82
|
+
content: { text: string }[];
|
|
83
|
+
};
|
|
84
|
+
};
|
|
79
85
|
expect(res.result.isError).toBe(false);
|
|
80
|
-
expect(res.result.
|
|
86
|
+
expect(res.result.structuredContent).toEqual({
|
|
87
|
+
content: "sekrit",
|
|
88
|
+
contentType: "text/plain; charset=utf-8",
|
|
89
|
+
});
|
|
81
90
|
});
|
|
82
91
|
|
|
83
92
|
/** MCP config file loads and exports vars for tool handlers. */
|
|
@@ -100,9 +109,18 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
|
100
109
|
env: { HOME: dir, ARGS_TEST_SECRET: "present" },
|
|
101
110
|
},
|
|
102
111
|
);
|
|
103
|
-
const res = responses.get(15) as {
|
|
112
|
+
const res = responses.get(15) as {
|
|
113
|
+
result: {
|
|
114
|
+
isError: boolean;
|
|
115
|
+
structuredContent?: { content: string; contentType: string };
|
|
116
|
+
content: { text: string }[];
|
|
117
|
+
};
|
|
118
|
+
};
|
|
104
119
|
expect(res.result.isError).toBe(false);
|
|
105
|
-
expect(res.result.
|
|
120
|
+
expect(res.result.structuredContent).toEqual({
|
|
121
|
+
content: "present",
|
|
122
|
+
contentType: "text/plain; charset=utf-8",
|
|
123
|
+
});
|
|
106
124
|
rmSync(dir, { recursive: true, force: true });
|
|
107
125
|
});
|
|
108
126
|
|
package/src/context.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type { AnyAppConfigSnapshot } from "./config/context.ts";
|
|
|
11
11
|
import { EmptyAppConfigSnapshot } from "./config/context.ts";
|
|
12
12
|
import { parseCommaList, parseDate, parseDateTime, parseDurationMs } from "./formats.ts";
|
|
13
13
|
import { collectOptionDefs } from "./parse.ts";
|
|
14
|
-
import
|
|
14
|
+
import { normalizeRespondOptions, writeRespondBodyToStdout } from "./respond.ts";
|
|
15
|
+
import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram, CliRespondOptions } from "./types.ts";
|
|
15
16
|
import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
|
|
16
17
|
import { strictParseDouble } from "./utils.ts";
|
|
17
18
|
|
|
@@ -29,6 +30,10 @@ export class CliContext {
|
|
|
29
30
|
readonly opts: Record<string, string>;
|
|
30
31
|
readonly invocation: CliInvocation;
|
|
31
32
|
readonly appConfig: AnyAppConfigSnapshot;
|
|
33
|
+
/** Original flat tool arguments for API/MCP invocations (when provided). */
|
|
34
|
+
readonly toolArgs?: Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
private response?: CliRespondOptions;
|
|
32
37
|
|
|
33
38
|
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
34
39
|
constructor(
|
|
@@ -39,6 +44,7 @@ export class CliContext {
|
|
|
39
44
|
program: CliProgram,
|
|
40
45
|
invocation: CliInvocation = "cli",
|
|
41
46
|
appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(program),
|
|
47
|
+
toolArgs?: Record<string, unknown>,
|
|
42
48
|
) {
|
|
43
49
|
this.appName = appName;
|
|
44
50
|
this.commandPath = commandPath;
|
|
@@ -47,6 +53,28 @@ export class CliContext {
|
|
|
47
53
|
this.program = program;
|
|
48
54
|
this.invocation = invocation;
|
|
49
55
|
this.appConfig = appConfig;
|
|
56
|
+
this.toolArgs = toolArgs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sets the machine-readable response for API/MCP invocations, or writes to stdout in CLI mode.
|
|
61
|
+
* May only be called once per invocation.
|
|
62
|
+
*/
|
|
63
|
+
respond(opts: CliRespondOptions): void {
|
|
64
|
+
if (this.response !== undefined) {
|
|
65
|
+
throw new Error("ctx.respond() was already called for this invocation");
|
|
66
|
+
}
|
|
67
|
+
const normalized = normalizeRespondOptions(opts);
|
|
68
|
+
if (this.invocation === "cli") {
|
|
69
|
+
writeRespondBodyToStdout(normalized.body);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.response = normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Returns the respond payload set by {@link respond}, if any. */
|
|
76
|
+
getResponse(): CliRespondOptions | undefined {
|
|
77
|
+
return this.response;
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
/** Returns whether a presence flag was set (including implicit "1" for boolean options). */
|
package/src/docs/api-guide.ts
CHANGED
|
@@ -152,7 +152,7 @@ export function generateApiGuideBody(program: CliProgram): string {
|
|
|
152
152
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/** Generates markdown API reference from the same export as `docs schema`. */
|
|
155
|
+
/** Generates markdown API reference from the same export as `docs cli-schema`. */
|
|
156
156
|
export function generateApiGuide(program: CliProgram): string {
|
|
157
157
|
const schema = cliSchemaExport(program);
|
|
158
158
|
const lines: string[] = [
|
|
@@ -160,7 +160,7 @@ export function generateApiGuide(program: CliProgram): string {
|
|
|
160
160
|
"",
|
|
161
161
|
schema.description,
|
|
162
162
|
"",
|
|
163
|
-
`Machine-readable export: \`${program.key} docs schema\``,
|
|
163
|
+
`Machine-readable export: \`${program.key} docs cli-schema\``,
|
|
164
164
|
"",
|
|
165
165
|
];
|
|
166
166
|
|
package/src/docs/builtin.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
DOCS_ROUTER_DESCRIPTION,
|
|
13
13
|
docsEffectiveDefaultTopic,
|
|
14
14
|
docsEnabled,
|
|
15
|
+
docsIncludesHttpTopic,
|
|
15
16
|
docsIncludesMcpTopic,
|
|
17
|
+
docsIncludesOpenApiTopic,
|
|
16
18
|
docsTopicDescription,
|
|
17
19
|
docsUserTopicKeys,
|
|
18
20
|
printDocsTopic,
|
|
@@ -70,8 +72,16 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
|
|
|
70
72
|
leaves.push(docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."));
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
if (docsIncludesHttpTopic(program)) {
|
|
76
|
+
leaves.push(docsLeaf(program, "http", "Print HTTP API setup and tool guidance."));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (docsIncludesOpenApiTopic(program)) {
|
|
80
|
+
leaves.push(docsLeaf(program, "openapi", "Print the HTTP OpenAPI 3.1 document as JSON."));
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
leaves.push(
|
|
74
|
-
docsLeaf(program, "schema", "Print the full command tree as JSON."),
|
|
84
|
+
docsLeaf(program, "cli-schema", "Print the full CLI command tree as JSON."),
|
|
75
85
|
docsLeaf(program, "api", "Print the full command reference as markdown."),
|
|
76
86
|
docsLeaf(program, "skill", docsSkillTopicDescription(program, resolveCapabilities(program))),
|
|
77
87
|
);
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -73,14 +73,20 @@ test("docs rejects reserved topic keys", () => {
|
|
|
73
73
|
const root = docsFixture();
|
|
74
74
|
const docs = root.docs;
|
|
75
75
|
if (!docs) throw new Error("expected docs fixture");
|
|
76
|
-
docs.topics
|
|
76
|
+
docs.topics["cli-schema"] = { text: "nope" };
|
|
77
77
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
78
|
-
delete docs.topics
|
|
78
|
+
delete docs.topics["cli-schema"];
|
|
79
79
|
docs.topics.skill = { text: "nope" };
|
|
80
80
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
81
81
|
delete docs.topics.skill;
|
|
82
82
|
docs.topics.api = { text: "nope" };
|
|
83
83
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
84
|
+
delete docs.topics.api;
|
|
85
|
+
docs.topics.openapi = { text: "nope" };
|
|
86
|
+
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
87
|
+
delete docs.topics.openapi;
|
|
88
|
+
docs.topics.http = { text: "nope" };
|
|
89
|
+
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
84
90
|
});
|
|
85
91
|
|
|
86
92
|
test("docsEffectiveDefaultTopic uses first topic key", () => {
|
|
@@ -132,6 +138,46 @@ test("docs mcp absent from router when MCP disabled", async () => {
|
|
|
132
138
|
expect(result.exitCode).not.toBe(0);
|
|
133
139
|
});
|
|
134
140
|
|
|
141
|
+
test("docs http when API enabled", async () => {
|
|
142
|
+
const root = docsFixture(true);
|
|
143
|
+
root.apiServer = { enabled: true };
|
|
144
|
+
cliValidateProgram(root);
|
|
145
|
+
const result = await new Cli(root).invoke(["docs", "http"]);
|
|
146
|
+
expect(result.exitCode).toBe(0);
|
|
147
|
+
expect(result.stdout).toContain("HTTP API (myapp)");
|
|
148
|
+
expect(result.stdout).toContain("curl -s -X POST");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("docs http absent from router when API disabled", async () => {
|
|
152
|
+
const root = docsFixture(false);
|
|
153
|
+
const presentation = cliPresentationRoot(root);
|
|
154
|
+
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
155
|
+
expect(docsNode && "commands" in docsNode).toBe(true);
|
|
156
|
+
if (docsNode && "commands" in docsNode) {
|
|
157
|
+
expect(docsNode.commands.some((c) => c.key === "http")).toBe(false);
|
|
158
|
+
expect(docsNode.commands.some((c) => c.key === "openapi")).toBe(false);
|
|
159
|
+
}
|
|
160
|
+
const result = await new Cli(root).invoke(["docs", "http"]);
|
|
161
|
+
expect(result.exitCode).not.toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("docs openapi when API enabled", async () => {
|
|
165
|
+
const root = docsFixture(true);
|
|
166
|
+
root.apiServer = { enabled: true };
|
|
167
|
+
cliValidateProgram(root);
|
|
168
|
+
const result = await new Cli(root).invoke(["docs", "openapi"]);
|
|
169
|
+
expect(result.exitCode).toBe(0);
|
|
170
|
+
const doc = JSON.parse(result.stdout) as { openapi: string; paths: Record<string, unknown> };
|
|
171
|
+
expect(doc.openapi).toBe("3.1.0");
|
|
172
|
+
expect(doc.paths).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("docs openapi absent when API disabled", async () => {
|
|
176
|
+
const root = docsFixture(false);
|
|
177
|
+
const result = await new Cli(root).invoke(["docs", "openapi"]);
|
|
178
|
+
expect(result.exitCode).not.toBe(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
135
181
|
test("presentation includes docs subtree", () => {
|
|
136
182
|
const presentation = cliPresentationRoot(docsFixture());
|
|
137
183
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
@@ -139,8 +185,8 @@ test("presentation includes docs subtree", () => {
|
|
|
139
185
|
expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(true);
|
|
140
186
|
});
|
|
141
187
|
|
|
142
|
-
test("docs schema prints JSON", async () => {
|
|
143
|
-
const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
|
|
188
|
+
test("docs cli-schema prints JSON", async () => {
|
|
189
|
+
const result = await new Cli(docsFixture()).invoke(["docs", "cli-schema"]);
|
|
144
190
|
expect(result.exitCode).toBe(0);
|
|
145
191
|
const schema = JSON.parse(result.stdout);
|
|
146
192
|
expect(schema.key).toBe("myapp");
|
|
@@ -153,7 +199,7 @@ test("docs api prints markdown reference", async () => {
|
|
|
153
199
|
expect(result.stdout).toContain("# myapp — CLI API reference");
|
|
154
200
|
expect(result.stdout).toContain("## `myapp run`");
|
|
155
201
|
expect(result.stdout).toContain("Run something.");
|
|
156
|
-
expect(result.stdout).toContain("myapp docs schema");
|
|
202
|
+
expect(result.stdout).toContain("myapp docs cli-schema");
|
|
157
203
|
});
|
|
158
204
|
|
|
159
205
|
test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
@@ -195,12 +241,12 @@ test("docs skill help recommends configure", async () => {
|
|
|
195
241
|
}
|
|
196
242
|
});
|
|
197
243
|
|
|
198
|
-
test("presentation includes docs schema and skill", () => {
|
|
244
|
+
test("presentation includes docs cli-schema and skill", () => {
|
|
199
245
|
const presentation = cliPresentationRoot(docsFixture());
|
|
200
246
|
const docsNode = presentation.commands.find((c) => c.key === "docs");
|
|
201
247
|
expect(docsNode && "commands" in docsNode).toBe(true);
|
|
202
248
|
if (docsNode && "commands" in docsNode) {
|
|
203
|
-
expect(docsNode.commands.some((c) => c.key === "schema")).toBe(true);
|
|
249
|
+
expect(docsNode.commands.some((c) => c.key === "cli-schema")).toBe(true);
|
|
204
250
|
expect(docsNode.commands.some((c) => c.key === "api")).toBe(true);
|
|
205
251
|
expect(docsNode.commands.some((c) => c.key === "skill")).toBe(true);
|
|
206
252
|
}
|
|
@@ -210,7 +256,7 @@ test("completions offer docs subcommands", () => {
|
|
|
210
256
|
const bash = completionBashScript(cliPresentationRoot(docsFixture()));
|
|
211
257
|
expect(bash).toContain("docs) echo");
|
|
212
258
|
expect(bash).toContain("readme) echo");
|
|
213
|
-
expect(bash).toContain("schema) echo");
|
|
259
|
+
expect(bash).toContain("cli-schema) echo");
|
|
214
260
|
expect(bash).toContain("api) echo");
|
|
215
261
|
expect(bash).toContain("skill) echo");
|
|
216
262
|
});
|
|
@@ -255,16 +301,28 @@ test("docs skill --save keeps frontmatter first", async () => {
|
|
|
255
301
|
expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
|
|
256
302
|
});
|
|
257
303
|
|
|
258
|
-
test("docs schema --save writes JSON file", async () => {
|
|
259
|
-
const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
|
|
304
|
+
test("docs cli-schema --save writes JSON file", async () => {
|
|
305
|
+
const result = await new Cli(docsFixture()).invoke(["docs", "cli-schema", "--save"]);
|
|
260
306
|
expect(result.exitCode).toBe(0);
|
|
261
|
-
expect(result.stdout.trim()).toBe("docs/schema.json");
|
|
262
|
-
const text = readFileSync(join(workDir, "docs/schema.json"), "utf8");
|
|
307
|
+
expect(result.stdout.trim()).toBe("docs/cli-schema.json");
|
|
308
|
+
const text = readFileSync(join(workDir, "docs/cli-schema.json"), "utf8");
|
|
263
309
|
expect(text).not.toContain("Generated by");
|
|
264
310
|
const schema = JSON.parse(text);
|
|
265
311
|
expect(schema.key).toBe("myapp");
|
|
266
312
|
});
|
|
267
313
|
|
|
314
|
+
test("docs openapi --save writes JSON file", async () => {
|
|
315
|
+
const root = docsFixture(true);
|
|
316
|
+
root.apiServer = { enabled: true };
|
|
317
|
+
const result = await new Cli(root).invoke(["docs", "openapi", "--save"]);
|
|
318
|
+
expect(result.exitCode).toBe(0);
|
|
319
|
+
expect(result.stdout.trim()).toBe("docs/openapi.json");
|
|
320
|
+
const text = readFileSync(join(workDir, "docs/openapi.json"), "utf8");
|
|
321
|
+
expect(text).not.toContain("Generated by");
|
|
322
|
+
const doc = JSON.parse(text) as { openapi: string };
|
|
323
|
+
expect(doc.openapi).toBe("3.1.0");
|
|
324
|
+
});
|
|
325
|
+
|
|
268
326
|
test("saveDocsTopic returns relative path", () => {
|
|
269
327
|
const path = saveDocsTopic(docsFixture(), "api");
|
|
270
328
|
expect(path).toBe("docs/api.md");
|