argsbarg 5.1.15 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -1
- package/README.md +32 -24
- 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/configure.md +2 -2
- package/docs/developing.md +1 -1
- package/docs/mcp.md +5 -5
- package/docs/output-schema.md +3 -3
- package/examples/full-example/README.md +8 -0
- 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/scripts/dev-formula.ts +1 -1
- package/examples/full-example/src/commands/echo/command.ts +6 -1
- 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 +1 -1
- package/src/api/openapi.ts +115 -0
- package/src/api/result.ts +89 -0
- package/src/api/server.ts +120 -0
- package/src/api.integration.test.ts +358 -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-tool/full-example-capabilities.test.ts +3 -0
- package/src/cli.ts +60 -8
- package/src/config/bootstrap.test.ts +46 -0
- package/src/config/bootstrap.ts +22 -5
- package/src/config.integration.test.ts +22 -4
- package/src/configure/index.ts +1 -1
- 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/resolve.ts +26 -2
- package/src/docs/save.ts +5 -2
- package/src/headless/tool-call.ts +147 -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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolveApiListenAddress } from "../api/server.ts";
|
|
2
|
+
import { resolveCapabilities } from "../capabilities.ts";
|
|
3
|
+
import { docsEnabled } from "../docs/resolve.ts";
|
|
4
|
+
import { CliFallbackMode, type CliLeaf, type CliProgram, type CliRouter } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
/** Built-in `api` router: bare `myapp api` runs the HTTP server (via hidden `serve` fallback). */
|
|
7
|
+
export function cliBuiltinApiCommand(program: CliProgram): CliRouter {
|
|
8
|
+
const caps = resolveCapabilities(program);
|
|
9
|
+
const { hostname, port } = resolveApiListenAddress(program);
|
|
10
|
+
const lines = [
|
|
11
|
+
`HTTP tool server on http://${hostname}:${port}.`,
|
|
12
|
+
"",
|
|
13
|
+
"Endpoints: GET /health, GET /openapi.json, GET /openapi-browser, POST /tools/:name",
|
|
14
|
+
"",
|
|
15
|
+
];
|
|
16
|
+
if (caps.configure) {
|
|
17
|
+
lines.push("Configure app settings:", "", " {argsbarg:program} configure", "");
|
|
18
|
+
}
|
|
19
|
+
if (docsEnabled(program)) {
|
|
20
|
+
lines.push("Full setup guide: {argsbarg:program} docs http");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const serve: CliLeaf = {
|
|
24
|
+
key: "serve",
|
|
25
|
+
hidden: true,
|
|
26
|
+
description: "Run as an HTTP API server for tools.",
|
|
27
|
+
handler: () => {},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
key: "api",
|
|
32
|
+
description: "HTTP API server for tools.",
|
|
33
|
+
notes: lines.join("\n"),
|
|
34
|
+
fallbackCommand: "serve",
|
|
35
|
+
fallbackMode: CliFallbackMode.MissingOnly,
|
|
36
|
+
commands: [serve],
|
|
37
|
+
};
|
|
38
|
+
}
|
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);
|
|
@@ -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 },
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CliAppConfigEntry } from "../types.ts";
|
|
3
|
+
import { shouldWizardPromptConfigKey } from "./bootstrap.ts";
|
|
4
|
+
|
|
5
|
+
const requiredEntry: CliAppConfigEntry = {
|
|
6
|
+
description: "API token.",
|
|
7
|
+
required: true,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("shouldWizardPromptConfigKey", () => {
|
|
11
|
+
test("skips addressed keys by default", () => {
|
|
12
|
+
expect(
|
|
13
|
+
shouldWizardPromptConfigKey(
|
|
14
|
+
"apiToken",
|
|
15
|
+
{ apiToken: "tok", _bindings: { apiToken: "file" } },
|
|
16
|
+
requiredEntry,
|
|
17
|
+
{ apiToken: "tok" },
|
|
18
|
+
{},
|
|
19
|
+
),
|
|
20
|
+
).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rePromptAll prompts addressed keys", () => {
|
|
24
|
+
expect(
|
|
25
|
+
shouldWizardPromptConfigKey(
|
|
26
|
+
"apiToken",
|
|
27
|
+
{ apiToken: "tok", _bindings: { apiToken: "file" } },
|
|
28
|
+
requiredEntry,
|
|
29
|
+
{ apiToken: "tok" },
|
|
30
|
+
{ rePromptAll: true },
|
|
31
|
+
),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("prompts when env binding is broken", () => {
|
|
36
|
+
expect(
|
|
37
|
+
shouldWizardPromptConfigKey(
|
|
38
|
+
"apiToken",
|
|
39
|
+
{ _bindings: { apiToken: "env" } },
|
|
40
|
+
{ ...requiredEntry, env: "API_TOKEN" },
|
|
41
|
+
{},
|
|
42
|
+
{},
|
|
43
|
+
),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/config/bootstrap.ts
CHANGED
|
@@ -176,6 +176,8 @@ export interface RunInstallConfigureOpts {
|
|
|
176
176
|
context?: "standalone" | "after-install";
|
|
177
177
|
/** When false, omit the "Configuration Setup" banner (e.g. inside interactive `configure`). */
|
|
178
178
|
showHeading?: boolean;
|
|
179
|
+
/** When true, prompt every entry (Enter keeps the current value). */
|
|
180
|
+
rePromptAll?: boolean;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
/** Print the "Configuration Setup" heading before the configure prompts. */
|
|
@@ -199,6 +201,24 @@ function bindingsDiffer(a: Record<string, unknown>, b: Record<string, unknown>):
|
|
|
199
201
|
return JSON.stringify(readBindings(a)) !== JSON.stringify(readBindings(b));
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
/** Whether the configure wizard should prompt for a config key. */
|
|
205
|
+
export function shouldWizardPromptConfigKey(
|
|
206
|
+
key: string,
|
|
207
|
+
fileData: Record<string, unknown>,
|
|
208
|
+
entry: CliAppConfigEntry,
|
|
209
|
+
resolved: Record<string, unknown>,
|
|
210
|
+
opts: Pick<RunInstallConfigureOpts, "rePromptAll">,
|
|
211
|
+
): boolean {
|
|
212
|
+
const bindings = readBindings(fileData);
|
|
213
|
+
if (bindings[key] === "env" && !isPresent(resolved[key])) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (opts.rePromptAll) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return !isKeyAddressed(key, fileData, entry);
|
|
220
|
+
}
|
|
221
|
+
|
|
202
222
|
/** Ask the user for each required setting that is still empty; returns updates to save to the config file. */
|
|
203
223
|
function promptMissingRequired(program: CliProgram): Record<string, unknown> {
|
|
204
224
|
const appConfig = program.appConfig;
|
|
@@ -270,10 +290,7 @@ export function runConfigure(
|
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
for (const [key, entry] of Object.entries(program.appConfig.entries)) {
|
|
273
|
-
|
|
274
|
-
if (bindings[key] === "env" && !isPresent(resolved[key])) {
|
|
275
|
-
// Re-prompt when env binding is broken.
|
|
276
|
-
} else if (isKeyAddressed(key, existing, entry)) {
|
|
293
|
+
if (!shouldWizardPromptConfigKey(key, existing, entry, resolved, opts)) {
|
|
277
294
|
continue;
|
|
278
295
|
}
|
|
279
296
|
|
|
@@ -383,7 +400,7 @@ export function ensureAppConfig(program: CliProgram, opts: EnsureAppConfigOpts):
|
|
|
383
400
|
|
|
384
401
|
if (opts.interactive && process.stdin.isTTY) {
|
|
385
402
|
if (opts.configure) {
|
|
386
|
-
runConfigure(program, { context: "standalone" });
|
|
403
|
+
runConfigure(program, { context: "standalone", rePromptAll: true });
|
|
387
404
|
fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
|
|
388
405
|
resolved = resolveAppConfig(program, fileData, hostEnv);
|
|
389
406
|
exportConfigToEnv(program, resolved, hostEnv);
|
|
@@ -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/configure/index.ts
CHANGED
|
@@ -272,7 +272,7 @@ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): P
|
|
|
272
272
|
if (target.key === "configure") {
|
|
273
273
|
if (!appConfigHasEntries(root)) continue;
|
|
274
274
|
|
|
275
|
-
const result = runConfigure(root, { context: "standalone", showHeading: false });
|
|
275
|
+
const result = runConfigure(root, { context: "standalone", showHeading: false, rePromptAll: true });
|
|
276
276
|
if (result.changed) {
|
|
277
277
|
installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
|
|
278
278
|
recordArtifactMutation(summary, [displayAppConfigPath(root)], "configured");
|
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
|
);
|