@crouton-kit/crouter 0.2.5 → 0.3.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/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +287 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +615 -459
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -3
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/{commands/update.js → core/self-update.js} +28 -63
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- package/dist/prompts/spec.js +0 -153
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RootHelp, BranchHelp, LeafHelp, InputParam } from './help.js';
|
|
2
|
+
export interface LeafDef {
|
|
3
|
+
kind: 'leaf';
|
|
4
|
+
name: string;
|
|
5
|
+
help: LeafHelp;
|
|
6
|
+
run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
|
|
7
|
+
}
|
|
8
|
+
export interface BranchDef {
|
|
9
|
+
kind: 'branch';
|
|
10
|
+
name: string;
|
|
11
|
+
help: BranchHelp;
|
|
12
|
+
children: (LeafDef | BranchDef)[];
|
|
13
|
+
}
|
|
14
|
+
export interface RootDef {
|
|
15
|
+
kind: 'root';
|
|
16
|
+
help: RootHelp;
|
|
17
|
+
subtrees: BranchDef[];
|
|
18
|
+
}
|
|
19
|
+
export declare function defineLeaf(opts: {
|
|
20
|
+
name: string;
|
|
21
|
+
help: LeafHelp;
|
|
22
|
+
run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
|
|
23
|
+
}): LeafDef;
|
|
24
|
+
export declare function defineBranch(opts: {
|
|
25
|
+
name: string;
|
|
26
|
+
help: BranchHelp;
|
|
27
|
+
children: (LeafDef | BranchDef)[];
|
|
28
|
+
}): BranchDef;
|
|
29
|
+
export declare function defineRoot(opts: {
|
|
30
|
+
help: RootHelp;
|
|
31
|
+
subtrees: BranchDef[];
|
|
32
|
+
}): RootDef;
|
|
33
|
+
/** Parse remaining argv tokens against the leaf's InputParam schema.
|
|
34
|
+
* Returns a plain object whose keys are camelCase parameter names. */
|
|
35
|
+
export declare function parseArgv(params: InputParam[], tokens: string[]): Promise<Record<string, unknown>>;
|
|
36
|
+
export declare function runCli(root: RootDef, argv: string[]): Promise<void>;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Registration kit for the agent-first crtr CLI.
|
|
2
|
+
// Hand-rolled path walker — no commander. Rationale: the spec requires flags
|
|
3
|
+
// and positional args on input (argv model). Commander's abstraction is more
|
|
4
|
+
// complexity than a plain array walk for this interface contract.
|
|
5
|
+
// A plain array walk + a small flag parser is ~120 lines and has no surprising
|
|
6
|
+
// edge cases.
|
|
7
|
+
import { renderRoot, renderBranch, renderLeafArgv } from './help.js';
|
|
8
|
+
import { readStdinRaw, emit, handle } from './io.js';
|
|
9
|
+
import { CrtrError } from './errors.js';
|
|
10
|
+
import { ExitCode } from '../types.js';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Factory functions
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export function defineLeaf(opts) {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'leaf',
|
|
18
|
+
name: opts.name,
|
|
19
|
+
help: opts.help,
|
|
20
|
+
run: opts.run,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function defineBranch(opts) {
|
|
24
|
+
return { kind: 'branch', name: opts.name, help: opts.help, children: opts.children };
|
|
25
|
+
}
|
|
26
|
+
export function defineRoot(opts) {
|
|
27
|
+
return { kind: 'root', help: opts.help, subtrees: opts.subtrees };
|
|
28
|
+
}
|
|
29
|
+
/** Validate and return child names for an unknown-path error. */
|
|
30
|
+
function childNames(node) {
|
|
31
|
+
if (node.kind === 'root')
|
|
32
|
+
return node.subtrees.map((s) => s.name);
|
|
33
|
+
if (node.kind === 'branch')
|
|
34
|
+
return node.children.map((c) => c.name);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
/** Walk argv tokens to the deepest matched node.
|
|
38
|
+
* Returns { node, remaining } where remaining are unconsumed tokens.
|
|
39
|
+
* -h / --help tokens are NOT consumed here — the caller checks for them. */
|
|
40
|
+
function walk(root, tokens) {
|
|
41
|
+
let current = root;
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < tokens.length) {
|
|
44
|
+
const token = tokens[i];
|
|
45
|
+
// Stop consuming on -h / --help — leave them for caller to detect
|
|
46
|
+
if (token === '-h' || token === '--help')
|
|
47
|
+
break;
|
|
48
|
+
if (current.kind === 'root') {
|
|
49
|
+
const nextNode = current.subtrees.find((s) => s.name === token);
|
|
50
|
+
if (nextNode === undefined)
|
|
51
|
+
break;
|
|
52
|
+
current = nextNode;
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
else if (current.kind === 'branch') {
|
|
56
|
+
const nextNode = current.children.find((c) => c.name === token);
|
|
57
|
+
if (nextNode === undefined)
|
|
58
|
+
break;
|
|
59
|
+
current = nextNode;
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// leaf — cannot descend further
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { node: current, remaining: tokens.slice(i) };
|
|
68
|
+
}
|
|
69
|
+
function renderNode(node) {
|
|
70
|
+
if (node.kind === 'root')
|
|
71
|
+
return renderRoot(node.help);
|
|
72
|
+
if (node.kind === 'branch')
|
|
73
|
+
return renderBranch(node.help);
|
|
74
|
+
return renderLeafArgv(node.help);
|
|
75
|
+
}
|
|
76
|
+
function helpRequested(remaining) {
|
|
77
|
+
return remaining.some((t) => t === '-h' || t === '--help');
|
|
78
|
+
}
|
|
79
|
+
/** Build a structured unknown-path error. Names valid children of the deepest
|
|
80
|
+
* matched node and names the entry command per the spec. No fuzzy matching. */
|
|
81
|
+
function unknownPathError(node, bad) {
|
|
82
|
+
const valid = childNames(node);
|
|
83
|
+
const validStr = valid.length > 0 ? valid.join(', ') : '(none)';
|
|
84
|
+
const entryCmd = node.kind === 'root'
|
|
85
|
+
? 'crtr -h'
|
|
86
|
+
: node.kind === 'branch'
|
|
87
|
+
? `crtr ${node.name} -h`
|
|
88
|
+
: 'crtr -h';
|
|
89
|
+
return new CrtrError('unknown_path', `unknown subcommand: ${bad}`, ExitCode.USAGE, {
|
|
90
|
+
received: bad,
|
|
91
|
+
next: `Valid children: ${validStr}. Run \`${entryCmd}\` for the full list.`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Argv parser
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function parseArgvError(code, message, received, field, next) {
|
|
98
|
+
return new CrtrError(code, message, ExitCode.USAGE, {
|
|
99
|
+
received,
|
|
100
|
+
field,
|
|
101
|
+
next: next !== undefined ? next : 'Run the command with -h to see the parameter schema.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Convert kebab-case flag name to camelCase key. e.g. context-file → contextFile */
|
|
105
|
+
function flagNameToKey(name) {
|
|
106
|
+
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
107
|
+
}
|
|
108
|
+
/** Parse remaining argv tokens against the leaf's InputParam schema.
|
|
109
|
+
* Returns a plain object whose keys are camelCase parameter names. */
|
|
110
|
+
export async function parseArgv(params, tokens) {
|
|
111
|
+
const result = {};
|
|
112
|
+
// Index params by kind for quick lookup
|
|
113
|
+
const positionalParam = params.find((p) => p.kind === 'positional');
|
|
114
|
+
const stdinParam = params.find((p) => p.kind === 'stdin');
|
|
115
|
+
const contextFileParam = params.find((p) => p.kind === 'context-file');
|
|
116
|
+
const flagParams = params.filter((p) => p.kind === 'flag');
|
|
117
|
+
const flagsByName = new Map(flagParams.map((f) => [f.name, f]));
|
|
118
|
+
// Apply defaults for bool flags
|
|
119
|
+
for (const f of flagParams) {
|
|
120
|
+
if (f.type === 'bool') {
|
|
121
|
+
result[flagNameToKey(f.name)] = f.default !== undefined ? f.default : false;
|
|
122
|
+
}
|
|
123
|
+
else if (f.default !== undefined) {
|
|
124
|
+
result[flagNameToKey(f.name)] = f.default;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let positionalValue;
|
|
128
|
+
let forcedPositional = false; // true after bare --
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < tokens.length) {
|
|
131
|
+
const token = tokens[i];
|
|
132
|
+
if (token === '--' && !forcedPositional) {
|
|
133
|
+
forcedPositional = true;
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!forcedPositional && token.startsWith('--')) {
|
|
138
|
+
// Flag: --name or --name=value
|
|
139
|
+
const eqIdx = token.indexOf('=');
|
|
140
|
+
let flagName;
|
|
141
|
+
let inlineValue;
|
|
142
|
+
if (eqIdx !== -1) {
|
|
143
|
+
flagName = token.slice(2, eqIdx);
|
|
144
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
flagName = token.slice(2);
|
|
148
|
+
}
|
|
149
|
+
// --context-file is handled specially regardless of declared schema
|
|
150
|
+
if (flagName === 'context-file') {
|
|
151
|
+
if (contextFileParam === undefined) {
|
|
152
|
+
throw parseArgvError('unknown_flag', `unknown flag: --context-file`, '--context-file', undefined, 'This leaf does not accept --context-file.');
|
|
153
|
+
}
|
|
154
|
+
const pathVal = inlineValue !== undefined ? inlineValue : tokens[++i];
|
|
155
|
+
if (pathVal === undefined) {
|
|
156
|
+
throw parseArgvError('missing_parameter', '--context-file requires a PATH argument', '--context-file', 'context-file');
|
|
157
|
+
}
|
|
158
|
+
let fileContent;
|
|
159
|
+
try {
|
|
160
|
+
fileContent = readFileSync(pathVal, 'utf8');
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
throw parseArgvError('invalid_type', `--context-file: cannot read file: ${pathVal}`, pathVal, 'context-file', 'Provide a readable file path.');
|
|
164
|
+
}
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(fileContent);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw parseArgvError('invalid_type', `--context-file: file is not valid JSON: ${pathVal}`, pathVal, 'context-file', 'Ensure the file contains a valid JSON object.');
|
|
171
|
+
}
|
|
172
|
+
result[flagNameToKey(contextFileParam.name)] = parsed;
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const flagDef = flagsByName.get(flagName);
|
|
177
|
+
if (flagDef === undefined) {
|
|
178
|
+
throw parseArgvError('unknown_flag', `unknown flag: --${flagName}`, `--${flagName}`, undefined, 'Use --<flag-name> for declared flags only. Run -h for the schema.');
|
|
179
|
+
}
|
|
180
|
+
const key = flagNameToKey(flagName);
|
|
181
|
+
if (flagDef.type === 'bool') {
|
|
182
|
+
if (inlineValue !== undefined) {
|
|
183
|
+
throw parseArgvError('invalid_type', `boolean flag --${flagName} takes no value`, `--${flagName}=${inlineValue}`, flagName, `Use --${flagName} (presence = true) with no value.`);
|
|
184
|
+
}
|
|
185
|
+
result[key] = true;
|
|
186
|
+
i++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const rawVal = inlineValue !== undefined ? inlineValue : tokens[++i];
|
|
190
|
+
if (rawVal === undefined || rawVal.startsWith('--')) {
|
|
191
|
+
throw parseArgvError('missing_parameter', `--${flagName} requires a value`, `--${flagName}`, flagName);
|
|
192
|
+
}
|
|
193
|
+
if (flagDef.type === 'int') {
|
|
194
|
+
const n = Number(rawVal);
|
|
195
|
+
if (!Number.isInteger(n)) {
|
|
196
|
+
throw parseArgvError('invalid_type', `--${flagName} must be an integer`, rawVal, flagName, `Provide an integer value for --${flagName}.`);
|
|
197
|
+
}
|
|
198
|
+
result[key] = n;
|
|
199
|
+
}
|
|
200
|
+
else if (flagDef.type === 'enum') {
|
|
201
|
+
const choices = flagDef.choices;
|
|
202
|
+
if (choices !== undefined && !choices.includes(rawVal)) {
|
|
203
|
+
throw parseArgvError('invalid_type', `--${flagName} must be one of: ${choices.join(', ')}`, rawVal, flagName, `Retry with one of: ${choices.join(', ')}.`);
|
|
204
|
+
}
|
|
205
|
+
result[key] = rawVal;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// string or path
|
|
209
|
+
result[key] = rawVal;
|
|
210
|
+
}
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Positional (or token after --)
|
|
215
|
+
if (positionalValue !== undefined) {
|
|
216
|
+
throw parseArgvError('bad_invocation', `unexpected extra positional argument: ${token}`, tokens.join(' '), undefined, 'Use --flag for parameters; only one positional allowed.');
|
|
217
|
+
}
|
|
218
|
+
if (positionalParam === undefined) {
|
|
219
|
+
throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
|
|
220
|
+
}
|
|
221
|
+
positionalValue = token;
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
// Assign positional
|
|
225
|
+
if (positionalValue !== undefined && positionalParam !== undefined) {
|
|
226
|
+
result[flagNameToKey(positionalParam.name)] = positionalValue;
|
|
227
|
+
}
|
|
228
|
+
// Read stdin if declared
|
|
229
|
+
if (stdinParam !== undefined) {
|
|
230
|
+
const raw = await readStdinRaw();
|
|
231
|
+
if (raw.trim() === '' && stdinParam.required) {
|
|
232
|
+
throw parseArgvError('missing_parameter', `stdin is required for this leaf`, '', stdinParam.name, 'Pipe the required content on stdin.');
|
|
233
|
+
}
|
|
234
|
+
result[flagNameToKey(stdinParam.name)] = raw;
|
|
235
|
+
}
|
|
236
|
+
// Validate required params
|
|
237
|
+
for (const p of params) {
|
|
238
|
+
const key = flagNameToKey(p.kind === 'context-file' ? p.name : p.name);
|
|
239
|
+
if (p.required && (result[key] === undefined || result[key] === null)) {
|
|
240
|
+
const display = p.kind === 'positional'
|
|
241
|
+
? `positional ${p.name.toUpperCase()}`
|
|
242
|
+
: p.kind === 'flag'
|
|
243
|
+
? `--${p.name}`
|
|
244
|
+
: p.kind === 'context-file'
|
|
245
|
+
? `--context-file`
|
|
246
|
+
: 'stdin';
|
|
247
|
+
throw parseArgvError('missing_parameter', `required parameter is missing: ${display}`, undefined, p.name, `Provide ${display}. Run -h for the schema.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
export async function runCli(root, argv) {
|
|
253
|
+
// argv is process.argv — strip node binary + script path
|
|
254
|
+
const tokens = argv.slice(2);
|
|
255
|
+
// Bare root invocation or -h at root
|
|
256
|
+
if (tokens.length === 0 || (tokens.length === 1 && (tokens[0] === '-h' || tokens[0] === '--help'))) {
|
|
257
|
+
process.stdout.write(renderRoot(root.help) + '\n');
|
|
258
|
+
process.exit(ExitCode.SUCCESS);
|
|
259
|
+
}
|
|
260
|
+
const { node, remaining } = walk(root, tokens);
|
|
261
|
+
try {
|
|
262
|
+
// Help anywhere in remaining tokens → print node help and exit
|
|
263
|
+
if (helpRequested(remaining)) {
|
|
264
|
+
process.stdout.write(renderNode(node) + '\n');
|
|
265
|
+
process.exit(ExitCode.SUCCESS);
|
|
266
|
+
}
|
|
267
|
+
// Bare branch or bare root (no -h, but no leaf selected) → help surface
|
|
268
|
+
if (node.kind === 'root' || node.kind === 'branch') {
|
|
269
|
+
if (remaining.length > 0) {
|
|
270
|
+
throw unknownPathError(node, remaining[0]);
|
|
271
|
+
}
|
|
272
|
+
process.stdout.write(renderNode(node) + '\n');
|
|
273
|
+
process.exit(ExitCode.SUCCESS);
|
|
274
|
+
}
|
|
275
|
+
// Leaf dispatch — argv input model only
|
|
276
|
+
const params = node.help.params !== undefined ? node.help.params : [];
|
|
277
|
+
const input = await parseArgv(params, remaining);
|
|
278
|
+
const result = await node.run(input);
|
|
279
|
+
if (result !== undefined && result !== null) {
|
|
280
|
+
emit(result);
|
|
281
|
+
}
|
|
282
|
+
// JSONL leaves call emitLine themselves and return void
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
handle(e);
|
|
286
|
+
}
|
|
287
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -10,3 +10,6 @@ export declare function usage(message: string, details?: Record<string, unknown>
|
|
|
10
10
|
export declare function ambiguous(message: string, details?: Record<string, unknown>): CrtrError;
|
|
11
11
|
export declare function network(message: string, details?: Record<string, unknown>): CrtrError;
|
|
12
12
|
export declare function general(message: string, details?: Record<string, unknown>): CrtrError;
|
|
13
|
+
/** Thrown by stub handlers for leaves not yet wired in P3+.
|
|
14
|
+
* code='not_implemented', exitCode=GENERAL, next names the node. */
|
|
15
|
+
export declare function notImplemented(node: string): CrtrError;
|
package/dist/core/errors.js
CHANGED
|
@@ -26,3 +26,8 @@ export function network(message, details) {
|
|
|
26
26
|
export function general(message, details) {
|
|
27
27
|
return new CrtrError('error', message, ExitCode.GENERAL, details);
|
|
28
28
|
}
|
|
29
|
+
/** Thrown by stub handlers for leaves not yet wired in P3+.
|
|
30
|
+
* code='not_implemented', exitCode=GENERAL, next names the node. */
|
|
31
|
+
export function notImplemented(node) {
|
|
32
|
+
return new CrtrError('not_implemented', `${node} is not yet implemented.`, ExitCode.GENERAL, { next: 'This leaf is not yet wired.' });
|
|
33
|
+
}
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function readJson<T = unknown>(path: string): T;
|
|
|
4
4
|
export declare function readJsonIfExists<T = unknown>(path: string): T | null;
|
|
5
5
|
export declare function readTextIfExists(path: string): string | null;
|
|
6
6
|
export declare function readText(path: string): string;
|
|
7
|
+
export declare function writeText(path: string, content: string): void;
|
|
7
8
|
export declare function isDir(path: string): boolean;
|
|
8
9
|
export declare function isSymlink(path: string): boolean;
|
|
9
10
|
export declare function pathExists(path: string): boolean;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -24,6 +24,10 @@ export function readTextIfExists(path) {
|
|
|
24
24
|
export function readText(path) {
|
|
25
25
|
return readFileSync(path, 'utf8');
|
|
26
26
|
}
|
|
27
|
+
export function writeText(path, content) {
|
|
28
|
+
ensureDir(dirname(path));
|
|
29
|
+
writeFileSync(path, content, 'utf8');
|
|
30
|
+
}
|
|
27
31
|
export function isDir(path) {
|
|
28
32
|
try {
|
|
29
33
|
return statSync(path).isDirectory();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface Field {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
/** Inline semantic constraint — bounds, enum, "must reference an active plan",
|
|
6
|
+
* token caps. Lives here, never in a separate Preconditions section. */
|
|
7
|
+
constraint: string;
|
|
8
|
+
}
|
|
9
|
+
/** Positional argument — at most one per leaf. */
|
|
10
|
+
export interface PositionalParam {
|
|
11
|
+
kind: 'positional';
|
|
12
|
+
name: string;
|
|
13
|
+
/** Display hint only; always parsed as string. */
|
|
14
|
+
type?: 'string' | 'path';
|
|
15
|
+
required: boolean;
|
|
16
|
+
constraint: string;
|
|
17
|
+
}
|
|
18
|
+
/** Long-form flag (`--name`). */
|
|
19
|
+
export interface FlagParam {
|
|
20
|
+
kind: 'flag';
|
|
21
|
+
name: string;
|
|
22
|
+
/** 'bool' flags take no value — presence = true. */
|
|
23
|
+
type: 'string' | 'int' | 'bool' | 'path' | 'enum';
|
|
24
|
+
/** Required only when type is 'enum'. */
|
|
25
|
+
choices?: string[];
|
|
26
|
+
required: boolean;
|
|
27
|
+
constraint: string;
|
|
28
|
+
default?: string | number | boolean;
|
|
29
|
+
/** When true, the flag may appear multiple times; values accumulate into an
|
|
30
|
+
* array. TODO: no current leaf needs this — implement when first leaf migrates. */
|
|
31
|
+
repeatable?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Raw stdin content blob (piped text, not parsed as JSON). */
|
|
34
|
+
export interface StdinParam {
|
|
35
|
+
kind: 'stdin';
|
|
36
|
+
name: string;
|
|
37
|
+
required: boolean;
|
|
38
|
+
constraint: string;
|
|
39
|
+
}
|
|
40
|
+
/** --context-file PATH: reads and JSON-parses the file at PATH. */
|
|
41
|
+
export interface ContextFileParam {
|
|
42
|
+
kind: 'context-file';
|
|
43
|
+
name: string;
|
|
44
|
+
required: boolean;
|
|
45
|
+
constraint: string;
|
|
46
|
+
/** Optional description of the expected JSON shape. */
|
|
47
|
+
shape?: string;
|
|
48
|
+
}
|
|
49
|
+
export type InputParam = PositionalParam | FlagParam | StdinParam | ContextFileParam;
|
|
50
|
+
export interface RootHelp {
|
|
51
|
+
tagline: string;
|
|
52
|
+
/** Vocabulary block — rendered before subtrees. */
|
|
53
|
+
concepts: {
|
|
54
|
+
name: string;
|
|
55
|
+
desc: string;
|
|
56
|
+
}[];
|
|
57
|
+
subtrees: {
|
|
58
|
+
name: string;
|
|
59
|
+
desc: string;
|
|
60
|
+
useWhen: string;
|
|
61
|
+
}[];
|
|
62
|
+
globals: {
|
|
63
|
+
name: string;
|
|
64
|
+
desc: string;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
export interface BranchHelp {
|
|
68
|
+
name: string;
|
|
69
|
+
summary: string;
|
|
70
|
+
/** Local lifecycle/model line that extends the parent definition. */
|
|
71
|
+
model?: string;
|
|
72
|
+
/** Bounded runtime aggregate, e.g. "Current: 2 draft, 1 active".
|
|
73
|
+
* Renderer soft-fails to omission if this returns null or throws. */
|
|
74
|
+
dynamicState?: () => string | null;
|
|
75
|
+
children: {
|
|
76
|
+
name: string;
|
|
77
|
+
desc: string;
|
|
78
|
+
useWhen: string;
|
|
79
|
+
}[];
|
|
80
|
+
}
|
|
81
|
+
export interface LeafHelp {
|
|
82
|
+
name: string;
|
|
83
|
+
summary: string;
|
|
84
|
+
/** Optional long-form workflow prose rendered immediately after the summary
|
|
85
|
+
* line. Only plan new / spec new carry this; it precedes the schema. */
|
|
86
|
+
guide?: string;
|
|
87
|
+
params?: InputParam[];
|
|
88
|
+
/** Note appended when there is no input (replaces the Input block). */
|
|
89
|
+
inputNote?: string;
|
|
90
|
+
output: Field[];
|
|
91
|
+
outputKind: 'object' | 'jsonl';
|
|
92
|
+
/** Every persistent change the command makes to the world. For read-only
|
|
93
|
+
* leaves use exactly: ["None. Read-only."] */
|
|
94
|
+
effects: string[];
|
|
95
|
+
}
|
|
96
|
+
export declare function renderRoot(h: RootHelp): string;
|
|
97
|
+
export declare function renderBranch(h: BranchHelp): string;
|
|
98
|
+
export declare function renderLeafArgv(h: LeafHelp): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Descriptor types and renderers for the -h layer of the crtr CLI.
|
|
2
|
+
// Pure functions — no side effects, no commander, no process.exit.
|
|
3
|
+
// Rendering matches reference.md shapes exactly:
|
|
4
|
+
// root L11-32, branch L43-58, leaf L65-189.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Internal helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Return the longest string length in an array of names. */
|
|
9
|
+
function maxLen(names) {
|
|
10
|
+
let max = 0;
|
|
11
|
+
for (const n of names) {
|
|
12
|
+
if (n.length > max)
|
|
13
|
+
max = n.length;
|
|
14
|
+
}
|
|
15
|
+
return max;
|
|
16
|
+
}
|
|
17
|
+
/** Pad a string to the given width with trailing spaces. */
|
|
18
|
+
function pad(s, width) {
|
|
19
|
+
return s + ' '.repeat(Math.max(0, width - s.length));
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// renderRoot
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const IO_CONTRACT = 'I/O contract: flags and positional args on input, JSON on stdout (JSONL for streams).\n' +
|
|
25
|
+
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h.';
|
|
26
|
+
export function renderRoot(h) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push(`${h.tagline}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
// Concepts block
|
|
31
|
+
lines.push('Concepts');
|
|
32
|
+
const cNameW = maxLen(h.concepts.map((c) => c.name));
|
|
33
|
+
for (const c of h.concepts) {
|
|
34
|
+
lines.push(` ${pad(c.name, cNameW)} ${c.desc}`);
|
|
35
|
+
}
|
|
36
|
+
lines.push('');
|
|
37
|
+
// Subtrees block
|
|
38
|
+
lines.push('Subtrees');
|
|
39
|
+
const sNameW = maxLen(h.subtrees.map((s) => s.name));
|
|
40
|
+
// Align desc column so "| use when X" starts at a consistent offset
|
|
41
|
+
const sDescW = maxLen(h.subtrees.map((s) => s.desc));
|
|
42
|
+
for (const s of h.subtrees) {
|
|
43
|
+
lines.push(` ${pad(s.name, sNameW)} ${pad(s.desc, sDescW)} | use when ${s.useWhen}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push('');
|
|
46
|
+
// Globals block
|
|
47
|
+
lines.push('Globals');
|
|
48
|
+
const gNameW = maxLen(h.globals.map((g) => g.name));
|
|
49
|
+
for (const g of h.globals) {
|
|
50
|
+
lines.push(` ${pad(g.name, gNameW)} ${g.desc}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(IO_CONTRACT);
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// renderBranch
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
export function renderBranch(h) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push(`${h.name}: ${h.summary}.`);
|
|
62
|
+
if (h.model !== undefined) {
|
|
63
|
+
lines.push(h.model);
|
|
64
|
+
}
|
|
65
|
+
// Dynamic state — soft-fail to omission. Rendered as its own block,
|
|
66
|
+
// blank-line separated from the summary, so a multi-line runtime
|
|
67
|
+
// aggregate (e.g. the loaded-skills catalog) reads cleanly.
|
|
68
|
+
if (h.dynamicState !== undefined) {
|
|
69
|
+
let state = null;
|
|
70
|
+
try {
|
|
71
|
+
state = h.dynamicState();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// soft-fail: omit the block
|
|
75
|
+
}
|
|
76
|
+
if (state !== null && state !== '') {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(state);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('Branches');
|
|
83
|
+
const nameW = maxLen(h.children.map((c) => c.name));
|
|
84
|
+
const descW = maxLen(h.children.map((c) => c.desc));
|
|
85
|
+
for (const c of h.children) {
|
|
86
|
+
lines.push(` ${pad(c.name, nameW)} ${pad(c.desc, descW)} | use when ${c.useWhen}`);
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// renderLeafArgv
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/** Build the display label for a param entry (left column). */
|
|
94
|
+
function paramLabel(p) {
|
|
95
|
+
if (p.kind === 'positional')
|
|
96
|
+
return p.name.toUpperCase();
|
|
97
|
+
if (p.kind === 'stdin')
|
|
98
|
+
return 'stdin';
|
|
99
|
+
if (p.kind === 'context-file')
|
|
100
|
+
return '--context-file PATH';
|
|
101
|
+
// flag
|
|
102
|
+
const f = p;
|
|
103
|
+
if (f.type === 'bool')
|
|
104
|
+
return `--${f.name}`;
|
|
105
|
+
return `--${f.name} ${f.name.toUpperCase().replace(/-/g, '_')}`;
|
|
106
|
+
}
|
|
107
|
+
/** Build the description line for a param entry (right column). */
|
|
108
|
+
function paramDesc(p) {
|
|
109
|
+
const req = p.required ? 'required' : 'optional';
|
|
110
|
+
if (p.kind === 'positional')
|
|
111
|
+
return `positional, ${req}. ${p.constraint}`;
|
|
112
|
+
if (p.kind === 'stdin')
|
|
113
|
+
return `${req}. ${p.constraint}`;
|
|
114
|
+
if (p.kind === 'context-file') {
|
|
115
|
+
const shape = p.shape !== undefined
|
|
116
|
+
? ` Shape: ${p.shape}`
|
|
117
|
+
: '';
|
|
118
|
+
return `${req}. Path to a JSON file.${shape} ${p.constraint}`.trim();
|
|
119
|
+
}
|
|
120
|
+
// flag
|
|
121
|
+
const f = p;
|
|
122
|
+
if (f.type === 'bool')
|
|
123
|
+
return `optional boolean. Presence means true. ${f.constraint}`.trim();
|
|
124
|
+
const dflt = f.default !== undefined ? ` Default: ${String(f.default)}.` : '';
|
|
125
|
+
const choices = f.type === 'enum' && f.choices !== undefined
|
|
126
|
+
? ` One of: ${f.choices.join(', ')}.`
|
|
127
|
+
: '';
|
|
128
|
+
return `${f.type}, ${req}.${choices}${dflt} ${f.constraint}`.trim();
|
|
129
|
+
}
|
|
130
|
+
export function renderLeafArgv(h) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push(`${h.name}: ${h.summary}.`);
|
|
133
|
+
if (h.guide !== undefined) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(h.guide);
|
|
136
|
+
}
|
|
137
|
+
lines.push('');
|
|
138
|
+
const params = h.params ?? [];
|
|
139
|
+
if (params.length > 0) {
|
|
140
|
+
lines.push('Input');
|
|
141
|
+
const labels = params.map(paramLabel);
|
|
142
|
+
const colW = maxLen(labels);
|
|
143
|
+
for (let i = 0; i < params.length; i++) {
|
|
144
|
+
lines.push(` ${pad(labels[i], colW)} ${paramDesc(params[i])}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
lines.push(h.inputNote !== undefined ? h.inputNote : 'No input parameters.');
|
|
149
|
+
}
|
|
150
|
+
lines.push('');
|
|
151
|
+
const outputLabel = h.outputKind === 'jsonl' ? 'Output (stdout, JSONL)' : 'Output (stdout, JSON)';
|
|
152
|
+
lines.push(outputLabel);
|
|
153
|
+
const outNameW = maxLen(h.output.map((f) => f.name));
|
|
154
|
+
for (const f of h.output) {
|
|
155
|
+
lines.push(` ${pad(f.name, outNameW)} ${f.type}. ${f.constraint}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('Effects');
|
|
159
|
+
for (const e of h.effects) {
|
|
160
|
+
lines.push(` ${e}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CrtrError } from './errors.js';
|
|
2
|
+
import { type ExitCodeValue } from '../types.js';
|
|
3
|
+
/** Structured error payload. `error` is a stable code the agent branches on;
|
|
4
|
+
* `next` is the recovery road sign. */
|
|
5
|
+
export interface ErrorPayload {
|
|
6
|
+
error: string;
|
|
7
|
+
message: string;
|
|
8
|
+
received?: unknown;
|
|
9
|
+
field?: string;
|
|
10
|
+
next: string;
|
|
11
|
+
}
|
|
12
|
+
/** A command-level failure: surfaces as the JSON response on stdout. */
|
|
13
|
+
export declare class InputError extends CrtrError {
|
|
14
|
+
payload: ErrorPayload;
|
|
15
|
+
constructor(payload: ErrorPayload, exitCode?: ExitCodeValue);
|
|
16
|
+
}
|
|
17
|
+
/** Read raw stdin to EOF. Returns empty string when stdin is a TTY (no pipe).
|
|
18
|
+
* Called by the argv parser for leaves declaring a `stdin` parameter. */
|
|
19
|
+
export declare function readStdinRaw(): Promise<string>;
|
|
20
|
+
/** Single-shot response: one JSON object. The whole response is one value. */
|
|
21
|
+
export declare function emit(obj: Record<string, unknown>): void;
|
|
22
|
+
/** One JSONL record. Call per event in a stream; partial reads stay parseable. */
|
|
23
|
+
export declare function emitLine(obj: Record<string, unknown>): void;
|
|
24
|
+
export declare function diag(message: string): void;
|
|
25
|
+
/** Terminal error handler. Command-level failures (bad input, not-found,
|
|
26
|
+
* ambiguous) surface as the JSON response on stdout so the caller parses one
|
|
27
|
+
* contract. Runtime/internal failures go to stderr as `{error:"internal"}` —
|
|
28
|
+
* raw traces never reach the agent. Exits non-zero either way. */
|
|
29
|
+
export declare function handle(e: unknown): never;
|