invoket 0.1.7 → 0.1.8
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/README.md +373 -101
- package/package.json +1 -1
- package/src/cli.ts +13 -594
- package/src/context.ts +12 -3
- package/src/parser.ts +560 -0
package/src/cli.ts
CHANGED
|
@@ -1,559 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Context } from "./context";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Parameter metadata extracted from TypeScript
|
|
15
|
-
interface ParamMeta {
|
|
16
|
-
name: string;
|
|
17
|
-
type: ParamType;
|
|
18
|
-
required: boolean;
|
|
19
|
-
isRest: boolean;
|
|
20
|
-
flag?: FlagMeta;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface TaskMeta {
|
|
24
|
-
description: string;
|
|
25
|
-
params: ParamMeta[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Parsed CLI arguments
|
|
29
|
-
interface ParsedArgs {
|
|
30
|
-
positional: string[];
|
|
31
|
-
flags: Map<string, string | boolean>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface DiscoveredTasks {
|
|
35
|
-
root: Map<string, TaskMeta>;
|
|
36
|
-
namespaced: Map<string, Map<string, TaskMeta>>; // namespace -> method -> meta
|
|
37
|
-
classDoc: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Extract class-level JSDoc for Tasks class
|
|
41
|
-
function extractClassDoc(source: string): string | null {
|
|
42
|
-
const match = source.match(
|
|
43
|
-
/\/\*\*\s*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*export\s+class\s+Tasks/,
|
|
44
|
-
);
|
|
45
|
-
if (!match) return null;
|
|
46
|
-
|
|
47
|
-
const lines = match[1]
|
|
48
|
-
.split("\n")
|
|
49
|
-
.map((line) => line.replace(/^\s*\*?\s*/, "").trim())
|
|
50
|
-
.filter((line) => line && !line.startsWith("@"));
|
|
51
|
-
|
|
52
|
-
return lines[0] || null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Parse command to extract namespace and method
|
|
56
|
-
function parseCommand(command: string): {
|
|
57
|
-
namespace: string | null;
|
|
58
|
-
method: string;
|
|
59
|
-
} {
|
|
60
|
-
const colonIdx = command.indexOf(":");
|
|
61
|
-
const dotIdx = command.indexOf(".");
|
|
62
|
-
|
|
63
|
-
let sepIdx = -1;
|
|
64
|
-
if (colonIdx !== -1 && dotIdx !== -1) {
|
|
65
|
-
sepIdx = Math.min(colonIdx, dotIdx);
|
|
66
|
-
} else if (colonIdx !== -1) {
|
|
67
|
-
sepIdx = colonIdx;
|
|
68
|
-
} else if (dotIdx !== -1) {
|
|
69
|
-
sepIdx = dotIdx;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (sepIdx !== -1) {
|
|
73
|
-
return {
|
|
74
|
-
namespace: command.slice(0, sepIdx),
|
|
75
|
-
method: command.slice(sepIdx + 1),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { namespace: null, method: command };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Extract methods from a class definition in source
|
|
83
|
-
function extractMethodsFromClass(
|
|
84
|
-
source: string,
|
|
85
|
-
className: string,
|
|
86
|
-
): Map<string, TaskMeta> {
|
|
87
|
-
const methods = new Map<string, TaskMeta>();
|
|
88
|
-
|
|
89
|
-
// Find the class body
|
|
90
|
-
const classPattern = new RegExp(
|
|
91
|
-
`class\\s+${className}\\s*(?:extends\\s+\\w+)?\\s*\\{([\\s\\S]*?)\\n\\}`,
|
|
92
|
-
);
|
|
93
|
-
const classMatch = source.match(classPattern);
|
|
94
|
-
if (!classMatch) return methods;
|
|
95
|
-
|
|
96
|
-
const classBody = classMatch[1];
|
|
97
|
-
|
|
98
|
-
// Match method declarations with JSDoc
|
|
99
|
-
const methodPattern =
|
|
100
|
-
/\/\*\*\s*([^*]*(?:\*(?!\/)[^*]*)*)\*\/\s*async\s+(\w+)\s*\(\s*c\s*:\s*Context\s*(?:,\s*([^)]+))?\s*\)/g;
|
|
101
|
-
|
|
102
|
-
let match;
|
|
103
|
-
while ((match = methodPattern.exec(classBody)) !== null) {
|
|
104
|
-
const [, jsdoc, methodName, paramsStr] = match;
|
|
105
|
-
|
|
106
|
-
// Skip private methods and constructor
|
|
107
|
-
if (methodName.startsWith("_") || methodName === "constructor") {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const description =
|
|
112
|
-
jsdoc
|
|
113
|
-
.split("\n")
|
|
114
|
-
.map((line) => line.replace(/^\s*\*?\s*/, "").trim())
|
|
115
|
-
.filter((line) => line && !line.startsWith("@"))[0] || "";
|
|
116
|
-
|
|
117
|
-
const params = parseParams(paramsStr, jsdoc);
|
|
118
|
-
methods.set(methodName, { description, params });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return methods;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Extract @flag annotations from JSDoc
|
|
125
|
-
function extractFlagAnnotations(
|
|
126
|
-
jsdoc: string,
|
|
127
|
-
): Map<string, { short?: string; aliases?: string[] }> {
|
|
128
|
-
const flags = new Map<string, { short?: string; aliases?: string[] }>();
|
|
129
|
-
|
|
130
|
-
// Match @flag paramName -s --alias1 --alias2
|
|
131
|
-
const flagPattern = /@flag\s+(\w+)\s+([^\n@]*)/g;
|
|
132
|
-
let match;
|
|
133
|
-
|
|
134
|
-
while ((match = flagPattern.exec(jsdoc)) !== null) {
|
|
135
|
-
const [, paramName, flagsStr] = match;
|
|
136
|
-
const parts = flagsStr.trim().split(/\s+/);
|
|
137
|
-
|
|
138
|
-
let short: string | undefined;
|
|
139
|
-
const aliases: string[] = [];
|
|
140
|
-
|
|
141
|
-
for (const part of parts) {
|
|
142
|
-
if (part.startsWith("--")) {
|
|
143
|
-
aliases.push(part);
|
|
144
|
-
} else if (part.startsWith("-") && part.length === 2) {
|
|
145
|
-
short = part;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
flags.set(paramName, {
|
|
150
|
-
short: short,
|
|
151
|
-
aliases: aliases.length > 0 ? aliases : undefined,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return flags;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Parse parameter string into ParamMeta array
|
|
159
|
-
function parseParams(
|
|
160
|
-
paramsStr: string | undefined,
|
|
161
|
-
jsdoc: string = "",
|
|
162
|
-
): ParamMeta[] {
|
|
163
|
-
const params: ParamMeta[] = [];
|
|
164
|
-
if (!paramsStr) return params;
|
|
165
|
-
|
|
166
|
-
const flagAnnotations = extractFlagAnnotations(jsdoc);
|
|
167
|
-
|
|
168
|
-
// Check for rest parameter first: ...name: type
|
|
169
|
-
const restMatch = paramsStr.match(/\.\.\.(\w+)\s*:\s*(\w+\[\]|\w+)/);
|
|
170
|
-
if (restMatch) {
|
|
171
|
-
const [, name, rawType] = restMatch;
|
|
172
|
-
params.push({
|
|
173
|
-
name,
|
|
174
|
-
type: rawType.endsWith("[]") ? "array" : "string",
|
|
175
|
-
required: false,
|
|
176
|
-
isRest: true,
|
|
177
|
-
// Rest params don't get flags
|
|
178
|
-
});
|
|
179
|
-
return params;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Updated regex to handle union types with null (e.g., string | null)
|
|
183
|
-
const paramPattern =
|
|
184
|
-
/(\w+)\s*:\s*(\w+\[\]|Record<[^>]+>|\{[^}]*\}|string|number|boolean|\w+)(?:\s*\|\s*null)?(?:\s*=\s*[^,)]+)?/g;
|
|
185
|
-
let paramMatch;
|
|
186
|
-
|
|
187
|
-
while ((paramMatch = paramPattern.exec(paramsStr)) !== null) {
|
|
188
|
-
const [fullMatch, name, rawType] = paramMatch;
|
|
189
|
-
const hasDefault = fullMatch.includes("=");
|
|
190
|
-
const isNullable = fullMatch.includes("| null");
|
|
191
|
-
|
|
192
|
-
let type: ParamType;
|
|
193
|
-
if (rawType === "string") {
|
|
194
|
-
type = "string";
|
|
195
|
-
} else if (rawType === "number") {
|
|
196
|
-
type = "number";
|
|
197
|
-
} else if (rawType === "boolean") {
|
|
198
|
-
type = "boolean";
|
|
199
|
-
} else if (rawType.endsWith("[]")) {
|
|
200
|
-
type = "array";
|
|
201
|
-
} else {
|
|
202
|
-
type = "object";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Build flag metadata
|
|
206
|
-
const annotation = flagAnnotations.get(name);
|
|
207
|
-
const flag: FlagMeta = {
|
|
208
|
-
long: `--${name}`,
|
|
209
|
-
short: annotation?.short,
|
|
210
|
-
aliases: annotation?.aliases,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
params.push({
|
|
214
|
-
name,
|
|
215
|
-
type,
|
|
216
|
-
required: !hasDefault && !isNullable,
|
|
217
|
-
isRest: false,
|
|
218
|
-
flag,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return params;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Discover all tasks including namespaced ones (source parsing only)
|
|
226
|
-
function discoverAllTasks(source: string): DiscoveredTasks {
|
|
227
|
-
const root = extractMethodsFromClass(source, "Tasks");
|
|
228
|
-
const namespaced = new Map<string, Map<string, TaskMeta>>();
|
|
229
|
-
const classDoc = extractClassDoc(source);
|
|
230
|
-
|
|
231
|
-
// Find namespace assignments in Tasks class: propertyName = new ClassName()
|
|
232
|
-
const nsPattern = /(\w+)\s*=\s*new\s+(\w+)\s*\(\s*\)/g;
|
|
233
|
-
let nsMatch;
|
|
234
|
-
|
|
235
|
-
while ((nsMatch = nsPattern.exec(source)) !== null) {
|
|
236
|
-
const [, propName, className] = nsMatch;
|
|
237
|
-
|
|
238
|
-
// Skip private namespaces
|
|
239
|
-
if (propName.startsWith("_")) continue;
|
|
240
|
-
|
|
241
|
-
const nsMethods = extractMethodsFromClass(source, className);
|
|
242
|
-
if (nsMethods.size > 0) {
|
|
243
|
-
namespaced.set(propName, nsMethods);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return { root, namespaced, classDoc };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Discover methods from runtime instance (for imported namespaces)
|
|
251
|
-
function discoverRuntimeNamespaces(
|
|
252
|
-
instance: any,
|
|
253
|
-
discovered: DiscoveredTasks,
|
|
254
|
-
): void {
|
|
255
|
-
// Find namespace properties on the instance
|
|
256
|
-
for (const propName of Object.getOwnPropertyNames(instance)) {
|
|
257
|
-
// Skip private, already discovered, or non-objects
|
|
258
|
-
if (propName.startsWith("_")) continue;
|
|
259
|
-
if (discovered.namespaced.has(propName)) continue;
|
|
260
|
-
|
|
261
|
-
const prop = instance[propName];
|
|
262
|
-
if (!prop || typeof prop !== "object" || Array.isArray(prop)) continue;
|
|
263
|
-
|
|
264
|
-
// Discover methods from this namespace at runtime
|
|
265
|
-
const methods = new Map<string, TaskMeta>();
|
|
266
|
-
let proto = Object.getPrototypeOf(prop);
|
|
267
|
-
|
|
268
|
-
while (proto && proto !== Object.prototype) {
|
|
269
|
-
for (const methodName of Object.getOwnPropertyNames(proto)) {
|
|
270
|
-
if (
|
|
271
|
-
methodName === "constructor" ||
|
|
272
|
-
methodName.startsWith("_") ||
|
|
273
|
-
typeof prop[methodName] !== "function"
|
|
274
|
-
) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// No type info for imported methods - treat args as strings
|
|
279
|
-
if (!methods.has(methodName)) {
|
|
280
|
-
methods.set(methodName, { description: "", params: [] });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
proto = Object.getPrototypeOf(proto);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (methods.size > 0) {
|
|
287
|
-
discovered.namespaced.set(propName, methods);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Parse TypeScript source to extract method signatures and types (legacy, for compatibility)
|
|
293
|
-
async function extractTaskMeta(source: string): Promise<Map<string, TaskMeta>> {
|
|
294
|
-
const { root, namespaced } = discoverAllTasks(source);
|
|
295
|
-
|
|
296
|
-
// Combine root and namespaced for backward compat
|
|
297
|
-
const all = new Map(root);
|
|
298
|
-
for (const [ns, methods] of namespaced) {
|
|
299
|
-
for (const [method, meta] of methods) {
|
|
300
|
-
all.set(method, meta); // This flattens - we'll fix in main()
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return all;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Convert CLI arg to typed value
|
|
308
|
-
function coerceArg(value: string, type: ParamType): unknown {
|
|
309
|
-
switch (type) {
|
|
310
|
-
case "number": {
|
|
311
|
-
if (value === "") {
|
|
312
|
-
throw new Error(`Expected number, got ""`);
|
|
313
|
-
}
|
|
314
|
-
const n = Number(value);
|
|
315
|
-
if (Number.isNaN(n)) {
|
|
316
|
-
throw new Error(`Expected number, got "${value}"`);
|
|
317
|
-
}
|
|
318
|
-
return n;
|
|
319
|
-
}
|
|
320
|
-
case "boolean":
|
|
321
|
-
if (value === "true" || value === "1") return true;
|
|
322
|
-
if (value === "false" || value === "0") return false;
|
|
323
|
-
throw new Error(`Expected boolean, got "${value}"`);
|
|
324
|
-
case "object":
|
|
325
|
-
case "array": {
|
|
326
|
-
try {
|
|
327
|
-
const parsed = JSON.parse(value);
|
|
328
|
-
if (type === "array" && !Array.isArray(parsed)) {
|
|
329
|
-
throw new Error(`Expected array, got ${typeof parsed}`);
|
|
330
|
-
}
|
|
331
|
-
if (
|
|
332
|
-
type === "object" &&
|
|
333
|
-
(typeof parsed !== "object" ||
|
|
334
|
-
Array.isArray(parsed) ||
|
|
335
|
-
parsed === null)
|
|
336
|
-
) {
|
|
337
|
-
throw new Error(
|
|
338
|
-
`Expected object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return parsed;
|
|
342
|
-
} catch (e) {
|
|
343
|
-
if (e instanceof SyntaxError) {
|
|
344
|
-
throw new Error(`Invalid JSON: ${e.message}`);
|
|
345
|
-
}
|
|
346
|
-
throw e;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
case "string":
|
|
350
|
-
default:
|
|
351
|
-
return value;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Parse CLI arguments into flags and positional args
|
|
356
|
-
function parseCliArgs(args: string[]): ParsedArgs {
|
|
357
|
-
const positional: string[] = [];
|
|
358
|
-
const flags = new Map<string, string | boolean>();
|
|
359
|
-
let stopFlagParsing = false;
|
|
360
|
-
|
|
361
|
-
for (let i = 0; i < args.length; i++) {
|
|
362
|
-
const arg = args[i];
|
|
363
|
-
|
|
364
|
-
if (stopFlagParsing) {
|
|
365
|
-
positional.push(arg);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (arg === "--") {
|
|
370
|
-
stopFlagParsing = true;
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// --flag=value
|
|
375
|
-
if (arg.startsWith("--") && arg.includes("=")) {
|
|
376
|
-
const eqIdx = arg.indexOf("=");
|
|
377
|
-
const name = arg.slice(2, eqIdx);
|
|
378
|
-
const value = arg.slice(eqIdx + 1);
|
|
379
|
-
flags.set(name, value);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// --no-flag (boolean negation)
|
|
384
|
-
if (arg.startsWith("--no-")) {
|
|
385
|
-
const name = arg.slice(5);
|
|
386
|
-
flags.set(name, false);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// --flag (may be boolean or need next arg)
|
|
391
|
-
if (arg.startsWith("--")) {
|
|
392
|
-
const name = arg.slice(2);
|
|
393
|
-
const nextArg = args[i + 1];
|
|
394
|
-
|
|
395
|
-
// If next arg exists and doesn't look like a flag, use it as value
|
|
396
|
-
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
397
|
-
flags.set(name, nextArg);
|
|
398
|
-
i++; // Skip next arg
|
|
399
|
-
} else {
|
|
400
|
-
flags.set(name, true); // Boolean flag
|
|
401
|
-
}
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// -f=value (short with equals)
|
|
406
|
-
if (arg.startsWith("-") && arg.length > 2 && arg.includes("=")) {
|
|
407
|
-
const eqIdx = arg.indexOf("=");
|
|
408
|
-
const name = arg.slice(1, eqIdx);
|
|
409
|
-
const value = arg.slice(eqIdx + 1);
|
|
410
|
-
flags.set(name, value);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// -f value or -f (boolean)
|
|
415
|
-
if (arg.startsWith("-") && arg.length === 2) {
|
|
416
|
-
const name = arg.slice(1);
|
|
417
|
-
const nextArg = args[i + 1];
|
|
418
|
-
|
|
419
|
-
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
420
|
-
flags.set(name, nextArg);
|
|
421
|
-
i++;
|
|
422
|
-
} else {
|
|
423
|
-
flags.set(name, true);
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Positional argument
|
|
429
|
-
positional.push(arg);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return { positional, flags };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Resolve arguments from parsed CLI args using param metadata
|
|
436
|
-
function resolveArgs(params: ParamMeta[], parsed: ParsedArgs): unknown[] {
|
|
437
|
-
const result: unknown[] = [];
|
|
438
|
-
const usedPositional = new Set<number>();
|
|
439
|
-
|
|
440
|
-
for (const param of params) {
|
|
441
|
-
// Handle rest parameters - collect all remaining positional args
|
|
442
|
-
if (param.isRest) {
|
|
443
|
-
const remaining = parsed.positional.filter(
|
|
444
|
-
(_, i) => !usedPositional.has(i),
|
|
445
|
-
);
|
|
446
|
-
result.push(...remaining);
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
let value: string | boolean | undefined;
|
|
451
|
-
|
|
452
|
-
// Try to get value from flags first
|
|
453
|
-
if (param.flag) {
|
|
454
|
-
// Check long flag (without --)
|
|
455
|
-
const longName = param.flag.long.slice(2);
|
|
456
|
-
if (parsed.flags.has(longName)) {
|
|
457
|
-
value = parsed.flags.get(longName);
|
|
458
|
-
}
|
|
459
|
-
// Check short flag (without -)
|
|
460
|
-
else if (param.flag.short) {
|
|
461
|
-
const shortName = param.flag.short.slice(1);
|
|
462
|
-
if (parsed.flags.has(shortName)) {
|
|
463
|
-
value = parsed.flags.get(shortName);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
// Check aliases
|
|
467
|
-
if (value === undefined && param.flag.aliases) {
|
|
468
|
-
for (const alias of param.flag.aliases) {
|
|
469
|
-
const aliasName = alias.slice(2);
|
|
470
|
-
if (parsed.flags.has(aliasName)) {
|
|
471
|
-
value = parsed.flags.get(aliasName);
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Fall back to positional if no flag found
|
|
479
|
-
if (value === undefined) {
|
|
480
|
-
for (let i = 0; i < parsed.positional.length; i++) {
|
|
481
|
-
if (!usedPositional.has(i)) {
|
|
482
|
-
value = parsed.positional[i];
|
|
483
|
-
usedPositional.add(i);
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Handle missing values
|
|
490
|
-
if (value === undefined) {
|
|
491
|
-
if (param.required) {
|
|
492
|
-
throw new Error(
|
|
493
|
-
`Missing required argument: <${param.name}> (${param.type})`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
break; // Optional param not provided, stop processing
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Coerce and add to result
|
|
500
|
-
// Boolean flags that are already boolean don't need coercion
|
|
501
|
-
if (typeof value === "boolean" && param.type === "boolean") {
|
|
502
|
-
result.push(value);
|
|
503
|
-
} else {
|
|
504
|
-
result.push(coerceArg(String(value), param.type));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return result;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Format param for help display
|
|
512
|
-
function formatParam(param: ParamMeta): string {
|
|
513
|
-
if (param.isRest) {
|
|
514
|
-
return `[${param.name}...]`;
|
|
515
|
-
}
|
|
516
|
-
return param.required ? `<${param.name}>` : `[${param.name}]`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Format flag info for display
|
|
520
|
-
function formatFlagInfo(param: ParamMeta): string {
|
|
521
|
-
if (!param.flag || param.isRest) return "";
|
|
522
|
-
|
|
523
|
-
const parts: string[] = [param.flag.long];
|
|
524
|
-
if (param.flag.short) {
|
|
525
|
-
parts.push(param.flag.short);
|
|
526
|
-
}
|
|
527
|
-
if (param.flag.aliases) {
|
|
528
|
-
parts.push(...param.flag.aliases);
|
|
529
|
-
}
|
|
530
|
-
return parts.join(", ");
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Display help for a specific task
|
|
534
|
-
function showTaskHelp(command: string, meta: TaskMeta): void {
|
|
535
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
536
|
-
const signature = paramStr ? `${command} ${paramStr}` : command;
|
|
537
|
-
|
|
538
|
-
console.log(`Usage: invt ${signature}\n`);
|
|
539
|
-
|
|
540
|
-
if (meta.description) {
|
|
541
|
-
console.log(`${meta.description}\n`);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (meta.params.length > 0) {
|
|
545
|
-
console.log("Arguments:");
|
|
546
|
-
for (const param of meta.params) {
|
|
547
|
-
const reqStr = param.required ? "(required)" : "(optional)";
|
|
548
|
-
const typeStr = param.isRest ? `${param.type}...` : param.type;
|
|
549
|
-
const flagStr = formatFlagInfo(param);
|
|
550
|
-
const flagDisplay = flagStr ? ` ${flagStr}` : "";
|
|
551
|
-
console.log(
|
|
552
|
-
` ${param.name.padEnd(15)} ${typeStr.padEnd(10)} ${reqStr}${flagDisplay}`,
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
discoverAllTasks,
|
|
5
|
+
discoverRuntimeNamespaces,
|
|
6
|
+
parseCommand,
|
|
7
|
+
parseCliArgs,
|
|
8
|
+
resolveArgs,
|
|
9
|
+
formatParam,
|
|
10
|
+
printTaskList,
|
|
11
|
+
showTaskHelp,
|
|
12
|
+
} from "./parser";
|
|
557
13
|
|
|
558
14
|
// Main CLI entry point
|
|
559
15
|
async function main() {
|
|
@@ -610,26 +66,7 @@ export class Tasks {
|
|
|
610
66
|
}
|
|
611
67
|
|
|
612
68
|
console.log("Available tasks:\n");
|
|
613
|
-
|
|
614
|
-
// Root tasks
|
|
615
|
-
for (const [name, meta] of discovered.root) {
|
|
616
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
617
|
-
const signature = paramStr ? `${name} ${paramStr}` : name;
|
|
618
|
-
console.log(` ${signature}`);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Namespaced tasks
|
|
622
|
-
for (const [ns, methods] of discovered.namespaced) {
|
|
623
|
-
console.log(`\n${ns}:`);
|
|
624
|
-
for (const [name, meta] of methods) {
|
|
625
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
626
|
-
const signature = paramStr
|
|
627
|
-
? `${ns}:${name} ${paramStr}`
|
|
628
|
-
: `${ns}:${name}`;
|
|
629
|
-
console.log(` ${signature}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
69
|
+
printTaskList(discovered);
|
|
633
70
|
console.log("\nUsage: invt <task> [args...]");
|
|
634
71
|
console.log(" invt <task> -h Show help for a specific task");
|
|
635
72
|
return;
|
|
@@ -638,25 +75,7 @@ export class Tasks {
|
|
|
638
75
|
// List flag
|
|
639
76
|
if (args[0] === "-l" || args[0] === "--list") {
|
|
640
77
|
console.log("Available tasks:\n");
|
|
641
|
-
|
|
642
|
-
// Root tasks
|
|
643
|
-
for (const [name, meta] of discovered.root) {
|
|
644
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
645
|
-
const signature = paramStr ? `${name} ${paramStr}` : name;
|
|
646
|
-
console.log(` ${signature}`);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Namespaced tasks
|
|
650
|
-
for (const [ns, methods] of discovered.namespaced) {
|
|
651
|
-
console.log(`\n${ns}:`);
|
|
652
|
-
for (const [name, meta] of methods) {
|
|
653
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
654
|
-
const signature = paramStr
|
|
655
|
-
? `${ns}:${name} ${paramStr}`
|
|
656
|
-
: `${ns}:${name}`;
|
|
657
|
-
console.log(` ${signature}`);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
78
|
+
printTaskList(discovered);
|
|
660
79
|
return;
|
|
661
80
|
}
|
|
662
81
|
|
|
@@ -668,7 +87,7 @@ export class Tasks {
|
|
|
668
87
|
|
|
669
88
|
const { namespace, method: methodName } = parseCommand(command);
|
|
670
89
|
|
|
671
|
-
let meta
|
|
90
|
+
let meta;
|
|
672
91
|
let method: Function | undefined;
|
|
673
92
|
let thisArg: any = instance;
|
|
674
93
|
|
package/src/context.ts
CHANGED
|
@@ -17,6 +17,16 @@ export interface RunOptions {
|
|
|
17
17
|
cwd?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export class CommandError extends Error {
|
|
21
|
+
result: RunResult;
|
|
22
|
+
|
|
23
|
+
constructor(message: string, result: RunResult) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "CommandError";
|
|
26
|
+
this.result = result;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export class Context {
|
|
21
31
|
cwd: string;
|
|
22
32
|
private options: RunOptions;
|
|
@@ -70,11 +80,10 @@ export class Context {
|
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
if (!opts.warn && runResult.failed) {
|
|
73
|
-
|
|
83
|
+
throw new CommandError(
|
|
74
84
|
`Command failed with exit code ${runResult.code}: ${command}`,
|
|
85
|
+
runResult,
|
|
75
86
|
);
|
|
76
|
-
(error as any).result = runResult;
|
|
77
|
-
throw error;
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
// When streaming, output already went to terminal; otherwise write captured output
|