argsbarg 6.0.2 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/README.md +7 -3
- package/docs/api-server.md +1 -1
- package/docs/cli-program.md +36 -5
- package/docs/config-schema.md +3 -3
- package/docs/output-schema.md +26 -24
- package/examples/full-example/README.md +6 -6
- package/examples/full-example/src/commands/status/types.ts +19 -1
- package/examples/full-example/src/program.ts +1 -1
- package/index.d.ts +57 -6
- package/package.json +1 -1
- package/src/cli-tool/program.ts +2 -1
- package/src/cli-tool/schemagen/cleanup.ts +3 -3
- package/src/cli-tool/schemagen/discover-schema-roots.ts +85 -15
- package/src/cli-tool/schemagen/index.ts +1 -1
- package/src/cli-tool/schemagen/names.ts +3 -3
- package/src/cli-tool/schemagen/run.ts +2 -1
- package/src/cli-tool/schemagen/schemagen.test.ts +35 -17
- package/src/cli.ts +54 -1
- package/src/context.ts +45 -44
- package/src/help.ts +2 -0
- package/src/index.ts +8 -0
- package/src/leaf-inputs.test.ts +279 -0
- package/src/leaf-inputs.ts +195 -0
- package/src/mcp/tools.ts +5 -0
- package/src/parse.ts +11 -0
- package/src/types.ts +8 -1
- package/src/validate.ts +43 -0
- package/examples/full-example/src/commands/status/schema.ts +0 -14
- /package/examples/full-example/src/config/{schema.ts → types.ts} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Discovers schema roots in
|
|
2
|
+
Discovers schema roots in types.ts files via configType / inputType / outputType exports.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import { join, relative } from "node:path";
|
|
7
|
-
import {
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { dirname, join, relative } from "node:path";
|
|
7
|
+
import { TYPES_FILE } from "./names.ts";
|
|
8
8
|
|
|
9
9
|
export type SchemaRootKind = "config" | "input" | "output";
|
|
10
10
|
|
|
@@ -13,11 +13,15 @@ export type SchemaRole = "configType" | "inputType" | "outputType";
|
|
|
13
13
|
export interface SchemaRoot {
|
|
14
14
|
kind: SchemaRootKind;
|
|
15
15
|
typeName: string;
|
|
16
|
-
/** Path relative to project root (
|
|
16
|
+
/** Path to types.ts relative to project root (anchors __generated__/ output). */
|
|
17
17
|
path: string;
|
|
18
|
+
/** Path to the file that defines `typeName` (defaults to `path`). */
|
|
19
|
+
sourcePath: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const ROLE_EXPORT_RE = /export\s+type\s+(configType|inputType|outputType)\s*=\s*(\w+)/g;
|
|
23
|
+
const HAS_ROLE_EXPORT_RE = /export\s+type\s+(configType|inputType|outputType)\s*=/;
|
|
24
|
+
const IMPORT_RE = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
|
|
21
25
|
|
|
22
26
|
const ROLE_TO_KIND: Record<SchemaRole, SchemaRootKind> = {
|
|
23
27
|
configType: "config",
|
|
@@ -25,21 +29,25 @@ const ROLE_TO_KIND: Record<SchemaRole, SchemaRootKind> = {
|
|
|
25
29
|
outputType: "output",
|
|
26
30
|
};
|
|
27
31
|
|
|
28
|
-
function
|
|
32
|
+
function listTypesManifestFiles(srcDir: string, baseDir: string, out: string[]): void {
|
|
29
33
|
for (const ent of readdirSync(srcDir)) {
|
|
30
34
|
const full = join(srcDir, ent);
|
|
31
35
|
const st = statSync(full);
|
|
32
36
|
if (st.isDirectory()) {
|
|
33
|
-
|
|
37
|
+
listTypesManifestFiles(full, baseDir, out);
|
|
34
38
|
continue;
|
|
35
39
|
}
|
|
36
|
-
if (ent
|
|
40
|
+
if (ent !== TYPES_FILE) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const text = readFileSync(full, "utf8");
|
|
44
|
+
if (HAS_ROLE_EXPORT_RE.test(text)) {
|
|
37
45
|
out.push(relative(baseDir, full));
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
/** True when `typeName` is declared in this file (not a
|
|
50
|
+
/** True when `typeName` is declared in this file (not a schemagen role alias). */
|
|
43
51
|
function isTypeDefinedInFile(text: string, typeName: string): boolean {
|
|
44
52
|
if (new RegExp(`export\\s+interface\\s+${typeName}\\b`).test(text)) {
|
|
45
53
|
return true;
|
|
@@ -50,7 +58,62 @@ function isTypeDefinedInFile(text: string, typeName: string): boolean {
|
|
|
50
58
|
return false;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
function
|
|
61
|
+
function parseLocalTypeImports(text: string): Map<string, string> {
|
|
62
|
+
const imports = new Map<string, string>();
|
|
63
|
+
for (const match of text.matchAll(IMPORT_RE)) {
|
|
64
|
+
const names = match[1];
|
|
65
|
+
const from = match[2];
|
|
66
|
+
if (!names || !from?.startsWith(".")) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const part of names.split(",")) {
|
|
70
|
+
const trimmed = part.trim();
|
|
71
|
+
const nameMatch = trimmed.match(/^(?:type\s+)?(\w+)(?:\s+as\s+(\w+))?$/);
|
|
72
|
+
if (!nameMatch?.[1]) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const localName = nameMatch[2] ?? nameMatch[1];
|
|
76
|
+
imports.set(localName, from);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return imports;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveModuleFile(manifestFile: string, specifier: string): string | null {
|
|
83
|
+
const base = join(dirname(manifestFile), specifier);
|
|
84
|
+
const candidates = [base, `${base}.ts`, join(base, "index.ts")];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
if (existsSync(candidate)) {
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve a role alias to the module that defines `typeName`, when imported from a relative path. */
|
|
94
|
+
function resolveAliasedTypeSource(
|
|
95
|
+
projectRoot: string,
|
|
96
|
+
manifestRelPath: string,
|
|
97
|
+
manifestText: string,
|
|
98
|
+
typeName: string,
|
|
99
|
+
): string | null {
|
|
100
|
+
const manifestFile = join(projectRoot, manifestRelPath);
|
|
101
|
+
const specifier = parseLocalTypeImports(manifestText).get(typeName);
|
|
102
|
+
if (!specifier) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const moduleFile = resolveModuleFile(manifestFile, specifier);
|
|
106
|
+
if (!moduleFile) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const moduleText = readFileSync(moduleFile, "utf8");
|
|
110
|
+
if (!isTypeDefinedInFile(moduleText, typeName)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return relative(projectRoot, moduleFile);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function discoverFromFile(projectRoot: string, path: string, text: string): SchemaRoot[] {
|
|
54
117
|
const rolesSeen = new Set<SchemaRole>();
|
|
55
118
|
const roots: SchemaRoot[] = [];
|
|
56
119
|
|
|
@@ -64,27 +127,34 @@ function discoverFromFile(path: string, text: string): SchemaRoot[] {
|
|
|
64
127
|
throw new Error(`${path}: duplicate export type ${role}`);
|
|
65
128
|
}
|
|
66
129
|
rolesSeen.add(role);
|
|
130
|
+
|
|
131
|
+
let sourcePath = path;
|
|
67
132
|
if (!isTypeDefinedInFile(text, typeName)) {
|
|
68
|
-
|
|
133
|
+
const resolved = resolveAliasedTypeSource(projectRoot, path, text, typeName);
|
|
134
|
+
if (!resolved) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
sourcePath = resolved;
|
|
69
138
|
}
|
|
70
|
-
|
|
139
|
+
|
|
140
|
+
roots.push({ kind: ROLE_TO_KIND[role], typeName, path, sourcePath });
|
|
71
141
|
}
|
|
72
142
|
|
|
73
143
|
return roots;
|
|
74
144
|
}
|
|
75
145
|
|
|
76
|
-
/** Find all schema roots under `srcDir` in files
|
|
146
|
+
/** Find all schema roots under `srcDir` in `types.ts` files with role exports. */
|
|
77
147
|
export function discoverSchemaRoots(projectRoot: string, srcDir = "src"): SchemaRoot[] {
|
|
78
148
|
const srcPath = join(projectRoot, srcDir);
|
|
79
149
|
const files: string[] = [];
|
|
80
|
-
|
|
150
|
+
listTypesManifestFiles(srcPath, projectRoot, files);
|
|
81
151
|
|
|
82
152
|
const roots: SchemaRoot[] = [];
|
|
83
153
|
const typeOwners = new Map<string, string>();
|
|
84
154
|
|
|
85
155
|
for (const relPath of files.sort()) {
|
|
86
156
|
const text = readFileSync(join(projectRoot, relPath), "utf8");
|
|
87
|
-
for (const root of discoverFromFile(relPath, text)) {
|
|
157
|
+
for (const root of discoverFromFile(projectRoot, relPath, text)) {
|
|
88
158
|
const prev = typeOwners.get(root.typeName);
|
|
89
159
|
if (prev) {
|
|
90
160
|
throw new Error(`${relPath}: duplicate schema root type ${root.typeName} (already declared in ${prev})`);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { discoverSchemaRoots, type SchemaRoot, type SchemaRootKind } from "./discover-schema-roots.ts";
|
|
2
|
-
export { GENERATED_DIR,
|
|
2
|
+
export { GENERATED_DIR, schemaExportName, schemaJsonBasename, TYPES_FILE } from "./names.ts";
|
|
3
3
|
export { type RunSchemagenOptions, type RunSchemagenResult, runSchemagen } from "./run.ts";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { SchemaRootKind } from "./discover-schema-roots.ts";
|
|
2
2
|
|
|
3
|
-
/** Directory name for generated schema artifacts (next to `
|
|
3
|
+
/** Directory name for generated schema artifacts (next to `types.ts`). */
|
|
4
4
|
export const GENERATED_DIR = "__generated__";
|
|
5
5
|
|
|
6
|
-
/** TypeScript
|
|
7
|
-
export const
|
|
6
|
+
/** TypeScript schemagen manifest filename under `src/`. */
|
|
7
|
+
export const TYPES_FILE = "types.ts";
|
|
8
8
|
|
|
9
9
|
/** JSON basename for a schema root kind (`outputSchema.json`, etc.). */
|
|
10
10
|
export function schemaJsonBasename(kind: SchemaRootKind): string {
|
|
@@ -36,8 +36,9 @@ function resolveTsconfig(projectRoot: string, tsconfig: string): string {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function generateJson(projectRoot: string, tsconfigPath: string, root: SchemaRoot): Record<string, unknown> {
|
|
39
|
+
const typeFile = join(projectRoot, root.sourcePath);
|
|
39
40
|
const generator = createGenerator({
|
|
40
|
-
path:
|
|
41
|
+
path: typeFile,
|
|
41
42
|
type: root.typeName,
|
|
42
43
|
tsconfig: tsconfigPath,
|
|
43
44
|
topRef: false,
|
|
@@ -39,10 +39,10 @@ function makeTempProject(): string {
|
|
|
39
39
|
return root;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
42
|
+
function writeTypes(root: string, relDir: string, body: string): void {
|
|
43
43
|
const dir = join(root, "src", relDir);
|
|
44
44
|
mkdirSync(dir, { recursive: true });
|
|
45
|
-
writeFileSync(join(dir, "
|
|
45
|
+
writeFileSync(join(dir, "types.ts"), body);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe("schemagen", () => {
|
|
@@ -62,14 +62,38 @@ describe("schemagen", () => {
|
|
|
62
62
|
expect(counts).toEqual({ configRoots: 1, inputRoots: 0, outputRoots: 1 });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test("discovers roots from types.ts with interface and role export", () => {
|
|
66
|
+
const root = makeTempProject();
|
|
67
|
+
const dir = join(root, "src/commands/demo");
|
|
68
|
+
writeTypes(
|
|
69
|
+
root,
|
|
70
|
+
"commands/demo",
|
|
71
|
+
`export interface DemoOutput { ok: boolean; }
|
|
72
|
+
|
|
73
|
+
/** Schemagen root for leaf outputSchema. */
|
|
74
|
+
export type outputType = DemoOutput;
|
|
75
|
+
`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const roots = discoverSchemaRoots(root);
|
|
79
|
+
expect(roots).toEqual([
|
|
80
|
+
{
|
|
81
|
+
kind: "output",
|
|
82
|
+
typeName: "DemoOutput",
|
|
83
|
+
path: "src/commands/demo/types.ts",
|
|
84
|
+
sourcePath: "src/commands/demo/types.ts",
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
runSchemagen({ projectRoot: root });
|
|
88
|
+
expect(existsSync(join(dir, "__generated__/outputSchema.json"))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
65
91
|
test("removes stale JSON files when a schema kind is dropped", () => {
|
|
66
92
|
const root = makeTempProject();
|
|
67
|
-
|
|
93
|
+
writeTypes(
|
|
68
94
|
root,
|
|
69
95
|
"commands/demo",
|
|
70
|
-
|
|
71
|
-
export interface DemoInput { id: string; }
|
|
72
|
-
/** Tool output. */
|
|
96
|
+
`export interface DemoInput { id: string; }
|
|
73
97
|
export interface DemoOutput { ok: boolean; }
|
|
74
98
|
export type inputType = DemoInput;
|
|
75
99
|
export type outputType = DemoOutput;
|
|
@@ -81,11 +105,10 @@ export type outputType = DemoOutput;
|
|
|
81
105
|
expect(existsSync(join(generatedDir, "inputSchema.json"))).toBe(true);
|
|
82
106
|
expect(existsSync(join(generatedDir, "outputSchema.json"))).toBe(true);
|
|
83
107
|
|
|
84
|
-
|
|
108
|
+
writeTypes(
|
|
85
109
|
root,
|
|
86
110
|
"commands/demo",
|
|
87
|
-
|
|
88
|
-
export interface DemoOutput { ok: boolean; }
|
|
111
|
+
`export interface DemoOutput { ok: boolean; }
|
|
89
112
|
export type outputType = DemoOutput;
|
|
90
113
|
`,
|
|
91
114
|
);
|
|
@@ -96,7 +119,7 @@ export type outputType = DemoOutput;
|
|
|
96
119
|
expect(existsSync(join(generatedDir, "index.ts"))).toBe(true);
|
|
97
120
|
});
|
|
98
121
|
|
|
99
|
-
test("removes orphan __generated__ when
|
|
122
|
+
test("removes orphan __generated__ when types.ts manifest is gone", () => {
|
|
100
123
|
const root = makeTempProject();
|
|
101
124
|
const generatedDir = join(root, "src/orphan/__generated__");
|
|
102
125
|
mkdirSync(generatedDir, { recursive: true });
|
|
@@ -107,14 +130,9 @@ export type outputType = DemoOutput;
|
|
|
107
130
|
expect(existsSync(generatedDir)).toBe(false);
|
|
108
131
|
});
|
|
109
132
|
|
|
110
|
-
test("removes __generated__ when
|
|
133
|
+
test("removes __generated__ when types.ts has no discoverable roots", () => {
|
|
111
134
|
const root = makeTempProject();
|
|
112
|
-
|
|
113
|
-
root,
|
|
114
|
-
"commands/empty",
|
|
115
|
-
`export type outputType = never;
|
|
116
|
-
`,
|
|
117
|
-
);
|
|
135
|
+
writeTypes(root, "commands/empty", `export type outputType = never;\n`);
|
|
118
136
|
const generatedDir = join(root, "src/commands/empty/__generated__");
|
|
119
137
|
mkdirSync(generatedDir, { recursive: true });
|
|
120
138
|
writeFileSync(join(generatedDir, "outputSchema.json"), "{}\n");
|
package/src/cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { readAppConfigFileRaw, resolveAppConfigPath } from "./config/file.ts";
|
|
|
18
18
|
import { effectiveJsonSchema } from "./config/schema.ts";
|
|
19
19
|
import { CliContext } from "./context.ts";
|
|
20
20
|
import { cliHelpRender } from "./help.ts";
|
|
21
|
+
import { LeafInputError, preloadPipableJson } from "./leaf-inputs.ts";
|
|
21
22
|
import { bootstrapMcpEnv } from "./mcp/env.ts";
|
|
22
23
|
import { mcpServeStdioLoop } from "./mcp/server.ts";
|
|
23
24
|
import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
|
|
@@ -124,14 +125,41 @@ export class Cli {
|
|
|
124
125
|
exitOnMissing: !skipRequiredConfig,
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
let preloadedJson: Record<string, unknown> = {};
|
|
128
129
|
try {
|
|
130
|
+
preloadedJson = await preloadPipableJson(this.program, pr.path, pr.opts, "cli");
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err instanceof LeafInputError) {
|
|
133
|
+
this.exitLeafInputError(err, pr.path);
|
|
134
|
+
}
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
const color = process.stderr.isTTY;
|
|
137
|
+
process.stderr.write(color ? `\u001B[31m${msg}\u001B[0m\n` : `${msg}\n`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ctx = new CliContext(
|
|
142
|
+
this.program.key,
|
|
143
|
+
pr.path,
|
|
144
|
+
pr.args,
|
|
145
|
+
pr.opts,
|
|
146
|
+
this.program,
|
|
147
|
+
"cli",
|
|
148
|
+
snapshot,
|
|
149
|
+
undefined,
|
|
150
|
+
preloadedJson,
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
this.ensureValidatedLeafInputs(ctx, leaf);
|
|
129
154
|
const handlerResult = await Promise.resolve(leaf.handler(ctx));
|
|
130
155
|
if (handlerResult !== undefined && ctx.getResponse() === undefined) {
|
|
131
156
|
ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
|
|
132
157
|
}
|
|
133
158
|
process.exit(0);
|
|
134
159
|
} catch (err) {
|
|
160
|
+
if (err instanceof LeafInputError) {
|
|
161
|
+
this.exitLeafInputError(err, pr.path);
|
|
162
|
+
}
|
|
135
163
|
if (err instanceof Error) {
|
|
136
164
|
process.stderr.write(`${err.message}\n`);
|
|
137
165
|
}
|
|
@@ -232,6 +260,7 @@ export class Cli {
|
|
|
232
260
|
});
|
|
233
261
|
}
|
|
234
262
|
|
|
263
|
+
this.ensureValidatedLeafInputs(ctx, leaf);
|
|
235
264
|
const handlerResult = await Promise.resolve(leaf.handler(ctx));
|
|
236
265
|
if (handlerResult !== undefined && ctx.getResponse() === undefined) {
|
|
237
266
|
ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
|
|
@@ -260,6 +289,15 @@ export class Cli {
|
|
|
260
289
|
const msg = stderr.trim() || `Exit code ${err.code}`;
|
|
261
290
|
return { kind: "error", exitCode: err.code, stdout, stderr, errorMsg: msg };
|
|
262
291
|
}
|
|
292
|
+
if (err instanceof LeafInputError) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "error",
|
|
295
|
+
exitCode: 1,
|
|
296
|
+
stdout,
|
|
297
|
+
stderr: `${err.message}\n`,
|
|
298
|
+
errorMsg: err.message,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
263
301
|
if (err instanceof Error) {
|
|
264
302
|
return {
|
|
265
303
|
kind: "error",
|
|
@@ -320,6 +358,21 @@ export class Cli {
|
|
|
320
358
|
}
|
|
321
359
|
}
|
|
322
360
|
|
|
361
|
+
private ensureValidatedLeafInputs(ctx: CliContext, leaf: CliLeaf): void {
|
|
362
|
+
if (leaf.inputSchema === undefined) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
ctx.inputs;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private exitLeafInputError(err: LeafInputError, helpPath: string[]): never {
|
|
369
|
+
const color = process.stderr.isTTY;
|
|
370
|
+
const msg = color ? `\u001B[31m${err.message}\u001B[0m` : err.message;
|
|
371
|
+
process.stderr.write(`${msg}\n`);
|
|
372
|
+
process.stderr.write(cliHelpRender(this.presentationRoot, helpPath, true));
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
323
376
|
private prepareDispatch(
|
|
324
377
|
argv: string[],
|
|
325
378
|
opts?: { presentationFallback?: boolean },
|
package/src/context.ts
CHANGED
|
@@ -10,14 +10,14 @@ parsed values.
|
|
|
10
10
|
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
|
-
import {
|
|
13
|
+
import { loadLeafInputs, readJsonOptionValue } from "./leaf-inputs.ts";
|
|
14
14
|
import { normalizeRespondOptions, writeRespondBodyToStdout } from "./respond.ts";
|
|
15
|
-
import type { CliInvocation, CliLeaf, CliNode,
|
|
16
|
-
import {
|
|
15
|
+
import type { CliInvocation, CliLeaf, CliNode, CliProgram, CliRespondOptions } from "./types.ts";
|
|
16
|
+
import { isCliLeaf, isCliRouter } from "./types.ts";
|
|
17
17
|
import { strictParseDouble } from "./utils.ts";
|
|
18
18
|
|
|
19
19
|
/** Coerced leaf inputs keyed by option and positional names. */
|
|
20
|
-
export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
|
|
20
|
+
export type CliLeafInputs = Record<string, boolean | number | string | string[] | unknown | undefined>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
|
|
@@ -32,8 +32,11 @@ export class CliContext {
|
|
|
32
32
|
readonly appConfig: AnyAppConfigSnapshot;
|
|
33
33
|
/** Original flat tool arguments for API/MCP invocations (when provided). */
|
|
34
34
|
readonly toolArgs?: Record<string, unknown>;
|
|
35
|
+
/** Pipable Json option values read from stdin before the handler (CLI only). */
|
|
36
|
+
readonly preloadedJson: Record<string, unknown>;
|
|
35
37
|
|
|
36
38
|
private response?: CliRespondOptions;
|
|
39
|
+
private leafInputsCache?: CliLeafInputs;
|
|
37
40
|
|
|
38
41
|
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
39
42
|
constructor(
|
|
@@ -45,6 +48,7 @@ export class CliContext {
|
|
|
45
48
|
invocation: CliInvocation = "cli",
|
|
46
49
|
appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(program),
|
|
47
50
|
toolArgs?: Record<string, unknown>,
|
|
51
|
+
preloadedJson: Record<string, unknown> = {},
|
|
48
52
|
) {
|
|
49
53
|
this.appName = appName;
|
|
50
54
|
this.commandPath = commandPath;
|
|
@@ -54,6 +58,7 @@ export class CliContext {
|
|
|
54
58
|
this.invocation = invocation;
|
|
55
59
|
this.appConfig = appConfig;
|
|
56
60
|
this.toolArgs = toolArgs;
|
|
61
|
+
this.preloadedJson = preloadedJson;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
@@ -136,54 +141,50 @@ export class CliContext {
|
|
|
136
141
|
return parseDateTime(s);
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Parsed Json option: `--name '<json>'`, preloaded piped stdin (when `pipable`), or MCP/API toolArgs.
|
|
146
|
+
* Flag wins over stdin and toolArgs.
|
|
147
|
+
*/
|
|
148
|
+
jsonOpt(name: string): unknown | undefined {
|
|
149
|
+
return readJsonOptionValue(this, name);
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
/** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
|
|
140
153
|
positional(name: string): string | string[] | undefined {
|
|
141
154
|
return this._positionalMap()[name];
|
|
142
155
|
}
|
|
143
156
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
out[opt.name] = this._readOptionValue(opt);
|
|
152
|
-
}
|
|
153
|
-
for (const p of leaf.positionals ?? []) {
|
|
154
|
-
const val = this.positional(p.name);
|
|
155
|
-
if (val === undefined) {
|
|
156
|
-
out[p.name] = undefined;
|
|
157
|
-
} else if (Array.isArray(val)) {
|
|
158
|
-
out[p.name] = val;
|
|
159
|
-
} else {
|
|
160
|
-
out[p.name] = val;
|
|
161
|
-
}
|
|
157
|
+
/**
|
|
158
|
+
* Coerced option and positional values for the current leaf.
|
|
159
|
+
* When `leaf.inputSchema` is set, argsbarg validates before the handler runs; this returns the cached result.
|
|
160
|
+
*/
|
|
161
|
+
get inputs(): CliLeafInputs {
|
|
162
|
+
if (this.leafInputsCache !== undefined) {
|
|
163
|
+
return this.leafInputsCache;
|
|
162
164
|
}
|
|
163
|
-
|
|
165
|
+
this.leafInputsCache = loadLeafInputs(this);
|
|
166
|
+
return this.leafInputsCache;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
return this.stringOpt(opt.name);
|
|
169
|
+
/**
|
|
170
|
+
* {@link inputs} cast to a schemagen or app-defined input type (consumer-asserted; not inferred from `inputSchema`).
|
|
171
|
+
*/
|
|
172
|
+
inputsAs<T = CliLeafInputs>(): T {
|
|
173
|
+
return this.inputs as T;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @deprecated Use {@link inputs} or {@link inputsAs}.
|
|
178
|
+
*/
|
|
179
|
+
readLeafInputs(): CliLeafInputs {
|
|
180
|
+
return this.inputs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs.
|
|
185
|
+
*/
|
|
186
|
+
readLeafInputsAsync(): Promise<CliLeafInputs> {
|
|
187
|
+
return Promise.resolve(this.readLeafInputs());
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
private _leafNode(): CliLeaf | undefined {
|
package/src/help.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -28,6 +28,14 @@ export {
|
|
|
28
28
|
shouldRunHeadlessWithYes,
|
|
29
29
|
wantsExplicitJson,
|
|
30
30
|
} from "./headless.ts";
|
|
31
|
+
export {
|
|
32
|
+
LeafInputError,
|
|
33
|
+
loadLeafInputs,
|
|
34
|
+
preloadPipableJson,
|
|
35
|
+
readJsonOptionValue,
|
|
36
|
+
readLeafInputs,
|
|
37
|
+
readLeafInputsAsync,
|
|
38
|
+
} from "./leaf-inputs.ts";
|
|
31
39
|
export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
|
|
32
40
|
export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
33
41
|
export type {
|