invoket 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/cli.ts +271 -37
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -4,12 +4,20 @@ import { Context } from "./context";
|
|
|
4
4
|
// Supported parameter types
|
|
5
5
|
type ParamType = "string" | "number" | "boolean" | "object" | "array";
|
|
6
6
|
|
|
7
|
+
// Flag metadata for a parameter
|
|
8
|
+
interface FlagMeta {
|
|
9
|
+
long: string; // e.g., "--name"
|
|
10
|
+
short?: string; // e.g., "-n"
|
|
11
|
+
aliases?: string[]; // e.g., ["--environment"]
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
// Parameter metadata extracted from TypeScript
|
|
8
15
|
interface ParamMeta {
|
|
9
16
|
name: string;
|
|
10
17
|
type: ParamType;
|
|
11
18
|
required: boolean;
|
|
12
19
|
isRest: boolean;
|
|
20
|
+
flag?: FlagMeta;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
interface TaskMeta {
|
|
@@ -17,6 +25,12 @@ interface TaskMeta {
|
|
|
17
25
|
params: ParamMeta[];
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
// Parsed CLI arguments
|
|
29
|
+
interface ParsedArgs {
|
|
30
|
+
positional: string[];
|
|
31
|
+
flags: Map<string, string | boolean>;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
interface DiscoveredTasks {
|
|
21
35
|
root: Map<string, TaskMeta>;
|
|
22
36
|
namespaced: Map<string, Map<string, TaskMeta>>; // namespace -> method -> meta
|
|
@@ -100,18 +114,57 @@ function extractMethodsFromClass(
|
|
|
100
114
|
.map((line) => line.replace(/^\s*\*?\s*/, "").trim())
|
|
101
115
|
.filter((line) => line && !line.startsWith("@"))[0] || "";
|
|
102
116
|
|
|
103
|
-
const params = parseParams(paramsStr);
|
|
117
|
+
const params = parseParams(paramsStr, jsdoc);
|
|
104
118
|
methods.set(methodName, { description, params });
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
return methods;
|
|
108
122
|
}
|
|
109
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
|
+
|
|
110
158
|
// Parse parameter string into ParamMeta array
|
|
111
|
-
function parseParams(
|
|
159
|
+
function parseParams(
|
|
160
|
+
paramsStr: string | undefined,
|
|
161
|
+
jsdoc: string = "",
|
|
162
|
+
): ParamMeta[] {
|
|
112
163
|
const params: ParamMeta[] = [];
|
|
113
164
|
if (!paramsStr) return params;
|
|
114
165
|
|
|
166
|
+
const flagAnnotations = extractFlagAnnotations(jsdoc);
|
|
167
|
+
|
|
115
168
|
// Check for rest parameter first: ...name: type
|
|
116
169
|
const restMatch = paramsStr.match(/\.\.\.(\w+)\s*:\s*(\w+\[\]|\w+)/);
|
|
117
170
|
if (restMatch) {
|
|
@@ -121,6 +174,7 @@ function parseParams(paramsStr: string | undefined): ParamMeta[] {
|
|
|
121
174
|
type: rawType.endsWith("[]") ? "array" : "string",
|
|
122
175
|
required: false,
|
|
123
176
|
isRest: true,
|
|
177
|
+
// Rest params don't get flags
|
|
124
178
|
});
|
|
125
179
|
return params;
|
|
126
180
|
}
|
|
@@ -146,7 +200,15 @@ function parseParams(paramsStr: string | undefined): ParamMeta[] {
|
|
|
146
200
|
type = "object";
|
|
147
201
|
}
|
|
148
202
|
|
|
149
|
-
|
|
203
|
+
// Build flag metadata
|
|
204
|
+
const annotation = flagAnnotations.get(name);
|
|
205
|
+
const flag: FlagMeta = {
|
|
206
|
+
long: `--${name}`,
|
|
207
|
+
short: annotation?.short,
|
|
208
|
+
aliases: annotation?.aliases,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
params.push({ name, type, required: !hasDefault, isRest: false, flag });
|
|
150
212
|
}
|
|
151
213
|
|
|
152
214
|
return params;
|
|
@@ -282,6 +344,162 @@ function coerceArg(value: string, type: ParamType): unknown {
|
|
|
282
344
|
}
|
|
283
345
|
}
|
|
284
346
|
|
|
347
|
+
// Parse CLI arguments into flags and positional args
|
|
348
|
+
function parseCliArgs(args: string[]): ParsedArgs {
|
|
349
|
+
const positional: string[] = [];
|
|
350
|
+
const flags = new Map<string, string | boolean>();
|
|
351
|
+
let stopFlagParsing = false;
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i < args.length; i++) {
|
|
354
|
+
const arg = args[i];
|
|
355
|
+
|
|
356
|
+
if (stopFlagParsing) {
|
|
357
|
+
positional.push(arg);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (arg === "--") {
|
|
362
|
+
stopFlagParsing = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --flag=value
|
|
367
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
368
|
+
const eqIdx = arg.indexOf("=");
|
|
369
|
+
const name = arg.slice(2, eqIdx);
|
|
370
|
+
const value = arg.slice(eqIdx + 1);
|
|
371
|
+
flags.set(name, value);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --no-flag (boolean negation)
|
|
376
|
+
if (arg.startsWith("--no-")) {
|
|
377
|
+
const name = arg.slice(5);
|
|
378
|
+
flags.set(name, false);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// --flag (may be boolean or need next arg)
|
|
383
|
+
if (arg.startsWith("--")) {
|
|
384
|
+
const name = arg.slice(2);
|
|
385
|
+
const nextArg = args[i + 1];
|
|
386
|
+
|
|
387
|
+
// If next arg exists and doesn't look like a flag, use it as value
|
|
388
|
+
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
389
|
+
flags.set(name, nextArg);
|
|
390
|
+
i++; // Skip next arg
|
|
391
|
+
} else {
|
|
392
|
+
flags.set(name, true); // Boolean flag
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// -f=value (short with equals)
|
|
398
|
+
if (arg.startsWith("-") && arg.length > 2 && arg.includes("=")) {
|
|
399
|
+
const eqIdx = arg.indexOf("=");
|
|
400
|
+
const name = arg.slice(1, eqIdx);
|
|
401
|
+
const value = arg.slice(eqIdx + 1);
|
|
402
|
+
flags.set(name, value);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// -f value or -f (boolean)
|
|
407
|
+
if (arg.startsWith("-") && arg.length === 2) {
|
|
408
|
+
const name = arg.slice(1);
|
|
409
|
+
const nextArg = args[i + 1];
|
|
410
|
+
|
|
411
|
+
if (nextArg !== undefined && !nextArg.startsWith("-")) {
|
|
412
|
+
flags.set(name, nextArg);
|
|
413
|
+
i++;
|
|
414
|
+
} else {
|
|
415
|
+
flags.set(name, true);
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Positional argument
|
|
421
|
+
positional.push(arg);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { positional, flags };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Resolve arguments from parsed CLI args using param metadata
|
|
428
|
+
function resolveArgs(params: ParamMeta[], parsed: ParsedArgs): unknown[] {
|
|
429
|
+
const result: unknown[] = [];
|
|
430
|
+
const usedPositional = new Set<number>();
|
|
431
|
+
|
|
432
|
+
for (const param of params) {
|
|
433
|
+
// Handle rest parameters - collect all remaining positional args
|
|
434
|
+
if (param.isRest) {
|
|
435
|
+
const remaining = parsed.positional.filter(
|
|
436
|
+
(_, i) => !usedPositional.has(i),
|
|
437
|
+
);
|
|
438
|
+
result.push(...remaining);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let value: string | boolean | undefined;
|
|
443
|
+
|
|
444
|
+
// Try to get value from flags first
|
|
445
|
+
if (param.flag) {
|
|
446
|
+
// Check long flag (without --)
|
|
447
|
+
const longName = param.flag.long.slice(2);
|
|
448
|
+
if (parsed.flags.has(longName)) {
|
|
449
|
+
value = parsed.flags.get(longName);
|
|
450
|
+
}
|
|
451
|
+
// Check short flag (without -)
|
|
452
|
+
else if (param.flag.short) {
|
|
453
|
+
const shortName = param.flag.short.slice(1);
|
|
454
|
+
if (parsed.flags.has(shortName)) {
|
|
455
|
+
value = parsed.flags.get(shortName);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Check aliases
|
|
459
|
+
if (value === undefined && param.flag.aliases) {
|
|
460
|
+
for (const alias of param.flag.aliases) {
|
|
461
|
+
const aliasName = alias.slice(2);
|
|
462
|
+
if (parsed.flags.has(aliasName)) {
|
|
463
|
+
value = parsed.flags.get(aliasName);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Fall back to positional if no flag found
|
|
471
|
+
if (value === undefined) {
|
|
472
|
+
for (let i = 0; i < parsed.positional.length; i++) {
|
|
473
|
+
if (!usedPositional.has(i)) {
|
|
474
|
+
value = parsed.positional[i];
|
|
475
|
+
usedPositional.add(i);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Handle missing values
|
|
482
|
+
if (value === undefined) {
|
|
483
|
+
if (param.required) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Missing required argument: <${param.name}> (${param.type})`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
break; // Optional param not provided, stop processing
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Coerce and add to result
|
|
492
|
+
// Boolean flags that are already boolean don't need coercion
|
|
493
|
+
if (typeof value === "boolean" && param.type === "boolean") {
|
|
494
|
+
result.push(value);
|
|
495
|
+
} else {
|
|
496
|
+
result.push(coerceArg(String(value), param.type));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
285
503
|
// Format param for help display
|
|
286
504
|
function formatParam(param: ParamMeta): string {
|
|
287
505
|
if (param.isRest) {
|
|
@@ -290,6 +508,20 @@ function formatParam(param: ParamMeta): string {
|
|
|
290
508
|
return param.required ? `<${param.name}>` : `[${param.name}]`;
|
|
291
509
|
}
|
|
292
510
|
|
|
511
|
+
// Format flag info for display
|
|
512
|
+
function formatFlagInfo(param: ParamMeta): string {
|
|
513
|
+
if (!param.flag || param.isRest) return "";
|
|
514
|
+
|
|
515
|
+
const parts: string[] = [param.flag.long];
|
|
516
|
+
if (param.flag.short) {
|
|
517
|
+
parts.push(param.flag.short);
|
|
518
|
+
}
|
|
519
|
+
if (param.flag.aliases) {
|
|
520
|
+
parts.push(...param.flag.aliases);
|
|
521
|
+
}
|
|
522
|
+
return parts.join(", ");
|
|
523
|
+
}
|
|
524
|
+
|
|
293
525
|
// Display help for a specific task
|
|
294
526
|
function showTaskHelp(command: string, meta: TaskMeta): void {
|
|
295
527
|
const paramStr = meta.params.map(formatParam).join(" ");
|
|
@@ -306,7 +538,11 @@ function showTaskHelp(command: string, meta: TaskMeta): void {
|
|
|
306
538
|
for (const param of meta.params) {
|
|
307
539
|
const reqStr = param.required ? "(required)" : "(optional)";
|
|
308
540
|
const typeStr = param.isRest ? `${param.type}...` : param.type;
|
|
309
|
-
|
|
541
|
+
const flagStr = formatFlagInfo(param);
|
|
542
|
+
const flagDisplay = flagStr ? ` ${flagStr}` : "";
|
|
543
|
+
console.log(
|
|
544
|
+
` ${param.name.padEnd(15)} ${typeStr.padEnd(10)} ${reqStr}${flagDisplay}`,
|
|
545
|
+
);
|
|
310
546
|
}
|
|
311
547
|
}
|
|
312
548
|
}
|
|
@@ -324,7 +560,23 @@ async function main() {
|
|
|
324
560
|
}
|
|
325
561
|
|
|
326
562
|
// Find tasks.ts
|
|
327
|
-
|
|
563
|
+
let tasksPath: string;
|
|
564
|
+
try {
|
|
565
|
+
tasksPath = Bun.resolveSync("./tasks.ts", process.cwd());
|
|
566
|
+
} catch {
|
|
567
|
+
console.log("No tasks.ts found. Create one to get started:\n");
|
|
568
|
+
console.log(`import { Context } from "invoket/context";
|
|
569
|
+
|
|
570
|
+
export class Tasks {
|
|
571
|
+
/** Say hello */
|
|
572
|
+
async hello(c: Context) {
|
|
573
|
+
console.log("Hello, World!");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
|
|
328
580
|
const source = await Bun.file(tasksPath).text();
|
|
329
581
|
|
|
330
582
|
// Import and instantiate Tasks class
|
|
@@ -480,43 +732,25 @@ async function main() {
|
|
|
480
732
|
return;
|
|
481
733
|
}
|
|
482
734
|
|
|
483
|
-
//
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
// If no param info (imported namespace), pass all args as strings
|
|
487
|
-
if (meta.params.length === 0 && taskArgs.length > 0) {
|
|
488
|
-
coercedArgs.push(...taskArgs);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
for (let i = 0; i < meta.params.length; i++) {
|
|
492
|
-
const param = meta.params[i];
|
|
493
|
-
|
|
494
|
-
// Handle rest parameters - collect all remaining args and spread them
|
|
495
|
-
if (param.isRest) {
|
|
496
|
-
const restArgs = taskArgs.slice(i);
|
|
497
|
-
coercedArgs.push(...restArgs);
|
|
498
|
-
break;
|
|
499
|
-
}
|
|
735
|
+
// Filter out help flags from taskArgs before parsing
|
|
736
|
+
const argsWithoutHelp = taskArgs.filter((a) => a !== "-h" && a !== "--help");
|
|
500
737
|
|
|
501
|
-
|
|
738
|
+
// Parse CLI args into flags and positional
|
|
739
|
+
const parsed = parseCliArgs(argsWithoutHelp);
|
|
502
740
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
console.error(
|
|
506
|
-
`Missing required argument: <${param.name}> (${param.type})`,
|
|
507
|
-
);
|
|
508
|
-
const paramStr = meta.params.map(formatParam).join(" ");
|
|
509
|
-
console.error(`Usage: ${command} ${paramStr}`);
|
|
510
|
-
process.exit(1);
|
|
511
|
-
}
|
|
512
|
-
// Optional param not provided, don't push (use default)
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
741
|
+
// Validate and coerce arguments
|
|
742
|
+
let coercedArgs: unknown[];
|
|
515
743
|
|
|
744
|
+
// If no param info (imported namespace), pass all args as strings
|
|
745
|
+
if (meta.params.length === 0 && argsWithoutHelp.length > 0) {
|
|
746
|
+
coercedArgs = [...parsed.positional];
|
|
747
|
+
} else {
|
|
516
748
|
try {
|
|
517
|
-
coercedArgs.
|
|
749
|
+
coercedArgs = resolveArgs(meta.params, parsed);
|
|
518
750
|
} catch (e) {
|
|
519
|
-
console.error(
|
|
751
|
+
console.error((e as Error).message);
|
|
752
|
+
const paramStr = meta.params.map(formatParam).join(" ");
|
|
753
|
+
console.error(`Usage: ${command} ${paramStr}`);
|
|
520
754
|
process.exit(1);
|
|
521
755
|
}
|
|
522
756
|
}
|