goke 6.2.3 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -1
- package/dist/__test__/index.test.js +178 -0
- package/dist/__test__/types.test-d.js +40 -0
- package/dist/goke.d.ts +34 -4
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +65 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__test__/index.test.ts +235 -0
- package/src/__test__/types.test-d.ts +47 -1
- package/src/goke.ts +83 -14
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Super light-weight**: No dependency, just a single file.
|
|
14
|
-
- **Easy to learn**. There are only
|
|
14
|
+
- **Easy to learn**. There are only 5 APIs you need to learn for building simple CLIs: `cli.option` `cli.use` `cli.version` `cli.help` `cli.parse`.
|
|
15
15
|
- **Yet so powerful**. Enable features like default command, git-like subcommands, validation for required arguments and options, variadic arguments, dot-nested options, automated help message generation and so on.
|
|
16
16
|
- **Space-separated subcommands**: Support multi-word commands like `mcp login`, `git remote add`.
|
|
17
17
|
- **Schema-based type coercion**: Use Zod, Valibot, ArkType, or plain JSON Schema for automatic type coercion and TypeScript type inference. Description and default values are extracted from the schema automatically.
|
|
18
|
+
- **Type-safe middleware**: Register `.use()` callbacks that run before commands with full type inference from global options.
|
|
18
19
|
- **Developer friendly**. Written in TypeScript.
|
|
19
20
|
|
|
20
21
|
## Install
|
|
@@ -346,6 +347,73 @@ deploy logs abc123 --follow # subcommand with args + options
|
|
|
346
347
|
deploy --help # shows all commands
|
|
347
348
|
```
|
|
348
349
|
|
|
350
|
+
### Global Options and Middleware
|
|
351
|
+
|
|
352
|
+
Global options are defined on the CLI instance and apply to all commands. Use `.use()` to register middleware that runs before any command action — useful for reacting to global options like setting up logging, initializing state, or configuring services.
|
|
353
|
+
|
|
354
|
+
Middleware runs in registration order, after option parsing and validation, but before the matched command's `.action()` callback.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { goke } from 'goke'
|
|
358
|
+
import { z } from 'zod'
|
|
359
|
+
|
|
360
|
+
const cli = goke('mycli')
|
|
361
|
+
|
|
362
|
+
cli
|
|
363
|
+
.option('--verbose', z.boolean().default(false).describe('Enable verbose logging'))
|
|
364
|
+
.option('--api-url [url]', z.string().default('https://api.example.com').describe('API base URL'))
|
|
365
|
+
.use((options) => {
|
|
366
|
+
// options.verbose and options.apiUrl are fully typed here
|
|
367
|
+
if (options.verbose) {
|
|
368
|
+
process.env.LOG_LEVEL = 'debug'
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
cli
|
|
373
|
+
.command('deploy <env>', 'Deploy to an environment')
|
|
374
|
+
.option('--dry-run', 'Preview without deploying')
|
|
375
|
+
.action((env, options) => {
|
|
376
|
+
// options includes both command options (dryRun) and global options (verbose, apiUrl)
|
|
377
|
+
console.log(`Deploying to ${env} via ${options.apiUrl}`)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
cli
|
|
381
|
+
.command('status', 'Show deployment status')
|
|
382
|
+
.action((options) => {
|
|
383
|
+
console.log('Checking status...')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
cli.help()
|
|
387
|
+
cli.parse()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Type safety is positional — each `.use()` callback only sees options declared before it in the chain:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
cli
|
|
394
|
+
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
395
|
+
.use((options) => {
|
|
396
|
+
options.verbose // boolean — typed
|
|
397
|
+
options.port // TypeScript error — not declared yet
|
|
398
|
+
})
|
|
399
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
400
|
+
.use((options) => {
|
|
401
|
+
options.verbose // boolean — still visible
|
|
402
|
+
options.port // number — now visible
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Middleware supports async functions. If any middleware is async, the remaining middleware and command action are chained as promises:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
cli
|
|
410
|
+
.option('--token <token>', z.string().describe('API token'))
|
|
411
|
+
.use(async (options) => {
|
|
412
|
+
const client = await connectToApi(options.token)
|
|
413
|
+
globalState.client = client
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
349
417
|
### Command-specific Options
|
|
350
418
|
|
|
351
419
|
You can attach options to a command.
|
|
@@ -630,6 +698,12 @@ Add a global option. The second argument is either:
|
|
|
630
698
|
- A **string** used as the description text
|
|
631
699
|
- A **Standard Schema** (e.g. `z.number().describe('Port')`) — description and default are extracted from the schema automatically
|
|
632
700
|
|
|
701
|
+
#### cli.use(callback)
|
|
702
|
+
|
|
703
|
+
- Type: `(callback: (options: Opts) => void | Promise<void>) => CLI`
|
|
704
|
+
|
|
705
|
+
Register a middleware function that runs before the matched command action. Middleware runs in registration order, after option parsing and validation. The callback receives the parsed global options, typed according to all `.option()` calls that precede the `.use()` in the chain.
|
|
706
|
+
|
|
633
707
|
#### cli.parse(argv?)
|
|
634
708
|
|
|
635
709
|
- Type: `(argv = process.argv) => ParsedArgv`
|
|
@@ -1467,3 +1467,181 @@ describe('helpText()', () => {
|
|
|
1467
1467
|
expect(text).toContain('--coverage');
|
|
1468
1468
|
});
|
|
1469
1469
|
});
|
|
1470
|
+
describe('middleware', () => {
|
|
1471
|
+
test('middleware runs before command action', () => {
|
|
1472
|
+
const cli = goke('mycli');
|
|
1473
|
+
const order = [];
|
|
1474
|
+
cli
|
|
1475
|
+
.option('--verbose', 'Verbose')
|
|
1476
|
+
.use(() => {
|
|
1477
|
+
order.push('middleware');
|
|
1478
|
+
});
|
|
1479
|
+
cli
|
|
1480
|
+
.command('build', 'Build')
|
|
1481
|
+
.action(() => {
|
|
1482
|
+
order.push('action');
|
|
1483
|
+
});
|
|
1484
|
+
cli.parse(['node', 'bin', 'build'], { run: true });
|
|
1485
|
+
expect(order).toEqual(['middleware', 'action']);
|
|
1486
|
+
});
|
|
1487
|
+
test('multiple middleware run in registration order', () => {
|
|
1488
|
+
const cli = goke('mycli');
|
|
1489
|
+
const order = [];
|
|
1490
|
+
cli
|
|
1491
|
+
.use(() => { order.push('mw1'); })
|
|
1492
|
+
.use(() => { order.push('mw2'); })
|
|
1493
|
+
.use(() => { order.push('mw3'); });
|
|
1494
|
+
cli
|
|
1495
|
+
.command('deploy', 'Deploy')
|
|
1496
|
+
.action(() => { order.push('action'); });
|
|
1497
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1498
|
+
expect(order).toEqual(['mw1', 'mw2', 'mw3', 'action']);
|
|
1499
|
+
});
|
|
1500
|
+
test('middleware receives parsed global options', () => {
|
|
1501
|
+
const cli = goke('mycli');
|
|
1502
|
+
let received = null;
|
|
1503
|
+
cli
|
|
1504
|
+
.option('--verbose', 'Verbose')
|
|
1505
|
+
.use((options) => {
|
|
1506
|
+
received = { ...options };
|
|
1507
|
+
});
|
|
1508
|
+
cli
|
|
1509
|
+
.command('build', 'Build')
|
|
1510
|
+
.action(() => { });
|
|
1511
|
+
cli.parse(['node', 'bin', 'build', '--verbose'], { run: true });
|
|
1512
|
+
expect(received.verbose).toBe(true);
|
|
1513
|
+
});
|
|
1514
|
+
test('middleware receives schema-coerced global options', () => {
|
|
1515
|
+
const cli = goke('mycli');
|
|
1516
|
+
let received = null;
|
|
1517
|
+
cli
|
|
1518
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
1519
|
+
.use((options) => {
|
|
1520
|
+
received = { ...options };
|
|
1521
|
+
});
|
|
1522
|
+
cli
|
|
1523
|
+
.command('serve', 'Serve')
|
|
1524
|
+
.action(() => { });
|
|
1525
|
+
cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true });
|
|
1526
|
+
expect(received.port).toBe(3000);
|
|
1527
|
+
expect(typeof received.port).toBe('number');
|
|
1528
|
+
});
|
|
1529
|
+
test('async middleware awaited before command action', async () => {
|
|
1530
|
+
const cli = goke('mycli');
|
|
1531
|
+
const order = [];
|
|
1532
|
+
cli.use(async () => {
|
|
1533
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1534
|
+
order.push('async-mw');
|
|
1535
|
+
});
|
|
1536
|
+
cli
|
|
1537
|
+
.command('run', 'Run')
|
|
1538
|
+
.action(() => { order.push('action'); });
|
|
1539
|
+
cli.parse(['node', 'bin', 'run'], { run: true });
|
|
1540
|
+
// Wait for async chain to complete
|
|
1541
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1542
|
+
expect(order).toEqual(['async-mw', 'action']);
|
|
1543
|
+
});
|
|
1544
|
+
test('async middleware error is caught and formatted', async () => {
|
|
1545
|
+
const stderr = createTestOutputStream();
|
|
1546
|
+
let exitCode;
|
|
1547
|
+
const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code; } });
|
|
1548
|
+
cli.use(async () => {
|
|
1549
|
+
throw new Error('middleware failed');
|
|
1550
|
+
});
|
|
1551
|
+
cli
|
|
1552
|
+
.command('deploy', 'Deploy')
|
|
1553
|
+
.action(() => { });
|
|
1554
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1555
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1556
|
+
expect(exitCode).toBe(1);
|
|
1557
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware failed"`);
|
|
1558
|
+
});
|
|
1559
|
+
test('middleware does not run with { run: false }', () => {
|
|
1560
|
+
const cli = goke('mycli');
|
|
1561
|
+
let middlewareCalled = false;
|
|
1562
|
+
cli.use(() => { middlewareCalled = true; });
|
|
1563
|
+
cli
|
|
1564
|
+
.command('build', 'Build')
|
|
1565
|
+
.action(() => { });
|
|
1566
|
+
cli.parse(['node', 'bin', 'build'], { run: false });
|
|
1567
|
+
expect(middlewareCalled).toBe(false);
|
|
1568
|
+
});
|
|
1569
|
+
test('middleware does not run for help', () => {
|
|
1570
|
+
const stdout = createTestOutputStream();
|
|
1571
|
+
const cli = goke('mycli', { stdout });
|
|
1572
|
+
let middlewareCalled = false;
|
|
1573
|
+
cli.use(() => { middlewareCalled = true; });
|
|
1574
|
+
cli.help();
|
|
1575
|
+
cli
|
|
1576
|
+
.command('build', 'Build')
|
|
1577
|
+
.action(() => { });
|
|
1578
|
+
cli.parse(['node', 'bin', '--help'], { run: true });
|
|
1579
|
+
expect(middlewareCalled).toBe(false);
|
|
1580
|
+
});
|
|
1581
|
+
test('middleware does not run when no command matched', () => {
|
|
1582
|
+
const stdout = createTestOutputStream();
|
|
1583
|
+
const cli = goke('mycli', { stdout });
|
|
1584
|
+
let middlewareCalled = false;
|
|
1585
|
+
cli.use(() => { middlewareCalled = true; });
|
|
1586
|
+
cli.help();
|
|
1587
|
+
cli
|
|
1588
|
+
.command('build', 'Build')
|
|
1589
|
+
.action(() => { });
|
|
1590
|
+
cli.parse(['node', 'bin', 'nonexistent'], { run: true });
|
|
1591
|
+
expect(middlewareCalled).toBe(false);
|
|
1592
|
+
});
|
|
1593
|
+
test('middleware runs for default command', () => {
|
|
1594
|
+
const cli = goke('mycli');
|
|
1595
|
+
const order = [];
|
|
1596
|
+
cli.use(() => { order.push('mw'); });
|
|
1597
|
+
cli
|
|
1598
|
+
.command('', 'Default')
|
|
1599
|
+
.action(() => { order.push('action'); });
|
|
1600
|
+
cli.parse(['node', 'bin'], { run: true });
|
|
1601
|
+
expect(order).toEqual(['mw', 'action']);
|
|
1602
|
+
});
|
|
1603
|
+
test('sync middleware error is caught and formatted', () => {
|
|
1604
|
+
const stderr = createTestOutputStream();
|
|
1605
|
+
let exitCode;
|
|
1606
|
+
const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code; } });
|
|
1607
|
+
cli.use(() => {
|
|
1608
|
+
throw new Error('middleware exploded');
|
|
1609
|
+
});
|
|
1610
|
+
cli
|
|
1611
|
+
.command('deploy', 'Deploy')
|
|
1612
|
+
.action(() => { });
|
|
1613
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true });
|
|
1614
|
+
expect(exitCode).toBe(1);
|
|
1615
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware exploded"`);
|
|
1616
|
+
});
|
|
1617
|
+
test('sync middleware error short-circuits command action', () => {
|
|
1618
|
+
const stderr = createTestOutputStream();
|
|
1619
|
+
const cli = goke('mycli', { stderr, exit: () => { } });
|
|
1620
|
+
let actionCalled = false;
|
|
1621
|
+
cli.use(() => {
|
|
1622
|
+
throw new Error('abort');
|
|
1623
|
+
});
|
|
1624
|
+
cli
|
|
1625
|
+
.command('build', 'Build')
|
|
1626
|
+
.action(() => { actionCalled = true; });
|
|
1627
|
+
cli.parse(['node', 'bin', 'build'], { run: true });
|
|
1628
|
+
expect(actionCalled).toBe(false);
|
|
1629
|
+
});
|
|
1630
|
+
test('mixed sync and async middleware chain correctly', async () => {
|
|
1631
|
+
const cli = goke('mycli');
|
|
1632
|
+
const order = [];
|
|
1633
|
+
cli
|
|
1634
|
+
.use(() => { order.push('sync1'); })
|
|
1635
|
+
.use(async () => {
|
|
1636
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1637
|
+
order.push('async');
|
|
1638
|
+
})
|
|
1639
|
+
.use(() => { order.push('sync2'); });
|
|
1640
|
+
cli
|
|
1641
|
+
.command('run', 'Run')
|
|
1642
|
+
.action(() => { order.push('action'); });
|
|
1643
|
+
cli.parse(['node', 'bin', 'run'], { run: true });
|
|
1644
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1645
|
+
expect(order).toEqual(['sync1', 'async', 'sync2', 'action']);
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* These use expectTypeOf from vitest for compile-time type assertions.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, test, expectTypeOf } from 'vitest';
|
|
9
|
+
import goke from '../index.js';
|
|
9
10
|
describe('type-level: ExtractOptionName', () => {
|
|
10
11
|
test('extracts name from --name <value>', () => {
|
|
11
12
|
expectTypeOf().toEqualTypeOf();
|
|
@@ -65,3 +66,42 @@ describe('type-level: CamelCase', () => {
|
|
|
65
66
|
expectTypeOf().toEqualTypeOf();
|
|
66
67
|
});
|
|
67
68
|
});
|
|
69
|
+
describe('type-level: middleware use() callback inference', () => {
|
|
70
|
+
test('use() callback receives accumulated option types', () => {
|
|
71
|
+
const schema1 = {};
|
|
72
|
+
const schema2 = {};
|
|
73
|
+
goke('test')
|
|
74
|
+
.option('--port <port>', schema1)
|
|
75
|
+
.option('--host <host>', schema2)
|
|
76
|
+
.use((options) => {
|
|
77
|
+
expectTypeOf(options.port).toEqualTypeOf();
|
|
78
|
+
expectTypeOf(options.host).toEqualTypeOf();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test('use() only sees options declared before it', () => {
|
|
82
|
+
const schema1 = {};
|
|
83
|
+
const schema2 = {};
|
|
84
|
+
goke('test')
|
|
85
|
+
.option('--verbose', schema1)
|
|
86
|
+
.use((options) => {
|
|
87
|
+
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
88
|
+
// @ts-expect-error port is not declared yet
|
|
89
|
+
options.port;
|
|
90
|
+
})
|
|
91
|
+
.option('--port <port>', schema2)
|
|
92
|
+
.use((options) => {
|
|
93
|
+
// Now both are visible
|
|
94
|
+
expectTypeOf(options.verbose).toEqualTypeOf();
|
|
95
|
+
expectTypeOf(options.port).toEqualTypeOf();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
test('accessing a non-existent option is a type error', () => {
|
|
99
|
+
const schema = {};
|
|
100
|
+
goke('test')
|
|
101
|
+
.option('--port <port>', schema)
|
|
102
|
+
.use((options) => {
|
|
103
|
+
// @ts-expect-error nonExistent was never defined
|
|
104
|
+
options.nonExistent;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
package/dist/goke.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ declare class Command {
|
|
|
90
90
|
rawName: string;
|
|
91
91
|
description: string;
|
|
92
92
|
config: CommandConfig;
|
|
93
|
-
cli: Goke
|
|
93
|
+
cli: Goke<any>;
|
|
94
94
|
options: Option[];
|
|
95
95
|
aliasNames: string[];
|
|
96
96
|
name: string;
|
|
@@ -101,7 +101,7 @@ declare class Command {
|
|
|
101
101
|
examples: CommandExample[];
|
|
102
102
|
helpCallback?: HelpCallback;
|
|
103
103
|
globalCommand?: GlobalCommand;
|
|
104
|
-
constructor(rawName: string, description: string, config: CommandConfig | undefined, cli: Goke);
|
|
104
|
+
constructor(rawName: string, description: string, config: CommandConfig | undefined, cli: Goke<any>);
|
|
105
105
|
usage(text: string): this;
|
|
106
106
|
allowUnknownOptions(): this;
|
|
107
107
|
ignoreOptionDefaultValue(): this;
|
|
@@ -160,7 +160,7 @@ declare class Command {
|
|
|
160
160
|
checkOptionValue(): void;
|
|
161
161
|
}
|
|
162
162
|
declare class GlobalCommand extends Command {
|
|
163
|
-
constructor(cli: Goke);
|
|
163
|
+
constructor(cli: Goke<any>);
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
166
|
* Output stream interface, modeled after Node's process.stdout / process.stderr.
|
|
@@ -210,11 +210,15 @@ interface ParsedArgv {
|
|
|
210
210
|
[k: string]: any;
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
-
declare class Goke extends EventEmitter {
|
|
213
|
+
declare class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
214
214
|
#private;
|
|
215
215
|
/** The program name to display in help and version message */
|
|
216
216
|
name: string;
|
|
217
217
|
commands: Command[];
|
|
218
|
+
/** Middleware functions that run before the matched command action, in registration order */
|
|
219
|
+
middlewares: Array<{
|
|
220
|
+
action: (options: any) => void | Promise<void>;
|
|
221
|
+
}>;
|
|
218
222
|
globalCommand: GlobalCommand;
|
|
219
223
|
matchedCommand?: Command;
|
|
220
224
|
matchedCommandName?: string;
|
|
@@ -261,8 +265,34 @@ declare class Goke extends EventEmitter {
|
|
|
261
265
|
* Add a global CLI option.
|
|
262
266
|
*
|
|
263
267
|
* Which is also applied to sub-commands.
|
|
268
|
+
*
|
|
269
|
+
* When a StandardJSONSchemaV1 schema is provided, the return type is narrowed
|
|
270
|
+
* to include the inferred option type — enabling type-safe `.use()` callbacks.
|
|
264
271
|
*/
|
|
272
|
+
option<RawName extends string, S extends StandardJSONSchemaV1>(rawName: RawName, schema: S): Goke<Opts & OptionEntry<RawName, S>>;
|
|
265
273
|
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this;
|
|
274
|
+
/**
|
|
275
|
+
* Register a middleware function that runs before the matched command action.
|
|
276
|
+
*
|
|
277
|
+
* Middleware runs in registration order, after option parsing and validation,
|
|
278
|
+
* but before the command's `.action()` callback. Useful for reacting to global
|
|
279
|
+
* options (e.g. setting up logging, initializing state).
|
|
280
|
+
*
|
|
281
|
+
* The callback receives the parsed options object, typed according to all
|
|
282
|
+
* `.option()` calls that precede this `.use()` in the chain.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* cli
|
|
287
|
+
* .option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
288
|
+
* .use((options) => {
|
|
289
|
+
* if (options.verbose) {
|
|
290
|
+
* process.env.LOG_LEVEL = 'debug'
|
|
291
|
+
* }
|
|
292
|
+
* })
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
use(callback: (options: Opts) => void | Promise<void>): this;
|
|
266
296
|
/**
|
|
267
297
|
* Show help message when `-h, --help` flags appear.
|
|
268
298
|
*
|
package/dist/goke.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"goke.d.ts","sourceRoot":"","sources":["../src/goke.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAIrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"goke.d.ts","sourceRoot":"","sources":["../src/goke.ts"],"names":[],"mappings":"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"}
|
package/dist/goke.js
CHANGED
|
@@ -160,6 +160,9 @@ const getFileName = (input) => {
|
|
|
160
160
|
const m = /([^\\\/]+)$/.exec(input);
|
|
161
161
|
return m ? m[1] : '';
|
|
162
162
|
};
|
|
163
|
+
const isPromiseLike = (value) => value != null
|
|
164
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
165
|
+
&& typeof value.then === 'function';
|
|
163
166
|
const camelcaseOptionName = (name) => {
|
|
164
167
|
// Camelcase the option name
|
|
165
168
|
// Don't camelcase anything after the dot `.`
|
|
@@ -582,6 +585,8 @@ class Goke extends EventEmitter {
|
|
|
582
585
|
/** The program name to display in help and version message */
|
|
583
586
|
name;
|
|
584
587
|
commands;
|
|
588
|
+
/** Middleware functions that run before the matched command action, in registration order */
|
|
589
|
+
middlewares;
|
|
585
590
|
globalCommand;
|
|
586
591
|
matchedCommand;
|
|
587
592
|
matchedCommandName;
|
|
@@ -618,6 +623,7 @@ class Goke extends EventEmitter {
|
|
|
618
623
|
super();
|
|
619
624
|
this.name = name;
|
|
620
625
|
this.commands = [];
|
|
626
|
+
this.middlewares = [];
|
|
621
627
|
this.rawArgs = [];
|
|
622
628
|
this.args = [];
|
|
623
629
|
this.options = {};
|
|
@@ -648,13 +654,33 @@ class Goke extends EventEmitter {
|
|
|
648
654
|
this.commands.push(command);
|
|
649
655
|
return command;
|
|
650
656
|
}
|
|
657
|
+
option(rawName, descriptionOrSchema) {
|
|
658
|
+
this.globalCommand.option(rawName, descriptionOrSchema);
|
|
659
|
+
return this;
|
|
660
|
+
}
|
|
651
661
|
/**
|
|
652
|
-
*
|
|
662
|
+
* Register a middleware function that runs before the matched command action.
|
|
663
|
+
*
|
|
664
|
+
* Middleware runs in registration order, after option parsing and validation,
|
|
665
|
+
* but before the command's `.action()` callback. Useful for reacting to global
|
|
666
|
+
* options (e.g. setting up logging, initializing state).
|
|
667
|
+
*
|
|
668
|
+
* The callback receives the parsed options object, typed according to all
|
|
669
|
+
* `.option()` calls that precede this `.use()` in the chain.
|
|
653
670
|
*
|
|
654
|
-
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```ts
|
|
673
|
+
* cli
|
|
674
|
+
* .option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
675
|
+
* .use((options) => {
|
|
676
|
+
* if (options.verbose) {
|
|
677
|
+
* process.env.LOG_LEVEL = 'debug'
|
|
678
|
+
* }
|
|
679
|
+
* })
|
|
680
|
+
* ```
|
|
655
681
|
*/
|
|
656
|
-
|
|
657
|
-
this.
|
|
682
|
+
use(callback) {
|
|
683
|
+
this.middlewares.push({ action: callback });
|
|
658
684
|
return this;
|
|
659
685
|
}
|
|
660
686
|
/**
|
|
@@ -1029,18 +1055,43 @@ class Goke extends EventEmitter {
|
|
|
1029
1055
|
}
|
|
1030
1056
|
});
|
|
1031
1057
|
actionArgs.push(options);
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1058
|
+
const executeAction = () => command.commandAction.apply(this, actionArgs);
|
|
1059
|
+
const handleAsyncError = (err) => {
|
|
1060
|
+
if (err instanceof Error) {
|
|
1061
|
+
this.handleCliError(err);
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`);
|
|
1065
|
+
}
|
|
1066
|
+
this.exit(1);
|
|
1067
|
+
};
|
|
1068
|
+
// Run middleware in registration order, then the command action.
|
|
1069
|
+
// If any middleware returns a promise, the rest of the chain
|
|
1070
|
+
// (remaining middleware + command action) becomes async.
|
|
1071
|
+
let asyncChain = null;
|
|
1072
|
+
for (const mw of this.middlewares) {
|
|
1073
|
+
if (asyncChain) {
|
|
1074
|
+
asyncChain = asyncChain.then(() => mw.action(options));
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
try {
|
|
1078
|
+
const mwResult = mw.action(options);
|
|
1079
|
+
if (isPromiseLike(mwResult)) {
|
|
1080
|
+
asyncChain = mwResult;
|
|
1081
|
+
}
|
|
1038
1082
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
handleAsyncError(err);
|
|
1085
|
+
return;
|
|
1041
1086
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const result = asyncChain
|
|
1090
|
+
? asyncChain.then(executeAction)
|
|
1091
|
+
: executeAction();
|
|
1092
|
+
// If the result is a promise, catch async errors
|
|
1093
|
+
if (isPromiseLike(result)) {
|
|
1094
|
+
result.catch(handleAsyncError);
|
|
1044
1095
|
}
|
|
1045
1096
|
return result;
|
|
1046
1097
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "./goke.js";
|
|
|
5
5
|
* @param name The program name to display in help and version message
|
|
6
6
|
* @param options Configuration for stdout, stderr, and argv
|
|
7
7
|
*/
|
|
8
|
-
declare const goke: (name?: string, options?: GokeOptions) => Goke
|
|
8
|
+
declare const goke: (name?: string, options?: GokeOptions) => Goke<{}>;
|
|
9
9
|
export default goke;
|
|
10
10
|
export { goke, Goke, Command };
|
|
11
11
|
export { createConsole } from "./goke.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;GAGG;AACH,QAAA,MAAM,IAAI,GAAI,aAAS,EAAE,UAAU,WAAW,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;GAGG;AACH,QAAA,MAAM,IAAI,GAAI,aAAS,EAAE,UAAU,WAAW,aAA4B,CAAA;AAE1E,eAAe,IAAI,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC3E,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA"}
|
package/package.json
CHANGED
|
@@ -1874,3 +1874,238 @@ describe('helpText()', () => {
|
|
|
1874
1874
|
expect(text).toContain('--coverage')
|
|
1875
1875
|
})
|
|
1876
1876
|
})
|
|
1877
|
+
|
|
1878
|
+
describe('middleware', () => {
|
|
1879
|
+
test('middleware runs before command action', () => {
|
|
1880
|
+
const cli = goke('mycli')
|
|
1881
|
+
const order: string[] = []
|
|
1882
|
+
|
|
1883
|
+
cli
|
|
1884
|
+
.option('--verbose', 'Verbose')
|
|
1885
|
+
.use(() => {
|
|
1886
|
+
order.push('middleware')
|
|
1887
|
+
})
|
|
1888
|
+
|
|
1889
|
+
cli
|
|
1890
|
+
.command('build', 'Build')
|
|
1891
|
+
.action(() => {
|
|
1892
|
+
order.push('action')
|
|
1893
|
+
})
|
|
1894
|
+
|
|
1895
|
+
cli.parse(['node', 'bin', 'build'], { run: true })
|
|
1896
|
+
expect(order).toEqual(['middleware', 'action'])
|
|
1897
|
+
})
|
|
1898
|
+
|
|
1899
|
+
test('multiple middleware run in registration order', () => {
|
|
1900
|
+
const cli = goke('mycli')
|
|
1901
|
+
const order: string[] = []
|
|
1902
|
+
|
|
1903
|
+
cli
|
|
1904
|
+
.use(() => { order.push('mw1') })
|
|
1905
|
+
.use(() => { order.push('mw2') })
|
|
1906
|
+
.use(() => { order.push('mw3') })
|
|
1907
|
+
|
|
1908
|
+
cli
|
|
1909
|
+
.command('deploy', 'Deploy')
|
|
1910
|
+
.action(() => { order.push('action') })
|
|
1911
|
+
|
|
1912
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true })
|
|
1913
|
+
expect(order).toEqual(['mw1', 'mw2', 'mw3', 'action'])
|
|
1914
|
+
})
|
|
1915
|
+
|
|
1916
|
+
test('middleware receives parsed global options', () => {
|
|
1917
|
+
const cli = goke('mycli')
|
|
1918
|
+
let received: any = null
|
|
1919
|
+
|
|
1920
|
+
cli
|
|
1921
|
+
.option('--verbose', 'Verbose')
|
|
1922
|
+
.use((options) => {
|
|
1923
|
+
received = { ...options }
|
|
1924
|
+
})
|
|
1925
|
+
|
|
1926
|
+
cli
|
|
1927
|
+
.command('build', 'Build')
|
|
1928
|
+
.action(() => {})
|
|
1929
|
+
|
|
1930
|
+
cli.parse(['node', 'bin', 'build', '--verbose'], { run: true })
|
|
1931
|
+
expect(received.verbose).toBe(true)
|
|
1932
|
+
})
|
|
1933
|
+
|
|
1934
|
+
test('middleware receives schema-coerced global options', () => {
|
|
1935
|
+
const cli = goke('mycli')
|
|
1936
|
+
let received: any = null
|
|
1937
|
+
|
|
1938
|
+
cli
|
|
1939
|
+
.option('--port <port>', z.number().describe('Port'))
|
|
1940
|
+
.use((options) => {
|
|
1941
|
+
received = { ...options }
|
|
1942
|
+
})
|
|
1943
|
+
|
|
1944
|
+
cli
|
|
1945
|
+
.command('serve', 'Serve')
|
|
1946
|
+
.action(() => {})
|
|
1947
|
+
|
|
1948
|
+
cli.parse(['node', 'bin', 'serve', '--port', '3000'], { run: true })
|
|
1949
|
+
expect(received.port).toBe(3000)
|
|
1950
|
+
expect(typeof received.port).toBe('number')
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
test('async middleware awaited before command action', async () => {
|
|
1954
|
+
const cli = goke('mycli')
|
|
1955
|
+
const order: string[] = []
|
|
1956
|
+
|
|
1957
|
+
cli.use(async () => {
|
|
1958
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1959
|
+
order.push('async-mw')
|
|
1960
|
+
})
|
|
1961
|
+
|
|
1962
|
+
cli
|
|
1963
|
+
.command('run', 'Run')
|
|
1964
|
+
.action(() => { order.push('action') })
|
|
1965
|
+
|
|
1966
|
+
cli.parse(['node', 'bin', 'run'], { run: true })
|
|
1967
|
+
|
|
1968
|
+
// Wait for async chain to complete
|
|
1969
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
1970
|
+
expect(order).toEqual(['async-mw', 'action'])
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
test('async middleware error is caught and formatted', async () => {
|
|
1974
|
+
const stderr = createTestOutputStream()
|
|
1975
|
+
let exitCode: number | undefined
|
|
1976
|
+
const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code } })
|
|
1977
|
+
|
|
1978
|
+
cli.use(async () => {
|
|
1979
|
+
throw new Error('middleware failed')
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
cli
|
|
1983
|
+
.command('deploy', 'Deploy')
|
|
1984
|
+
.action(() => {})
|
|
1985
|
+
|
|
1986
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true })
|
|
1987
|
+
|
|
1988
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1989
|
+
expect(exitCode).toBe(1)
|
|
1990
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware failed"`)
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
test('middleware does not run with { run: false }', () => {
|
|
1994
|
+
const cli = goke('mycli')
|
|
1995
|
+
let middlewareCalled = false
|
|
1996
|
+
|
|
1997
|
+
cli.use(() => { middlewareCalled = true })
|
|
1998
|
+
|
|
1999
|
+
cli
|
|
2000
|
+
.command('build', 'Build')
|
|
2001
|
+
.action(() => {})
|
|
2002
|
+
|
|
2003
|
+
cli.parse(['node', 'bin', 'build'], { run: false })
|
|
2004
|
+
expect(middlewareCalled).toBe(false)
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
test('middleware does not run for help', () => {
|
|
2008
|
+
const stdout = createTestOutputStream()
|
|
2009
|
+
const cli = goke('mycli', { stdout })
|
|
2010
|
+
let middlewareCalled = false
|
|
2011
|
+
|
|
2012
|
+
cli.use(() => { middlewareCalled = true })
|
|
2013
|
+
cli.help()
|
|
2014
|
+
|
|
2015
|
+
cli
|
|
2016
|
+
.command('build', 'Build')
|
|
2017
|
+
.action(() => {})
|
|
2018
|
+
|
|
2019
|
+
cli.parse(['node', 'bin', '--help'], { run: true })
|
|
2020
|
+
expect(middlewareCalled).toBe(false)
|
|
2021
|
+
})
|
|
2022
|
+
|
|
2023
|
+
test('middleware does not run when no command matched', () => {
|
|
2024
|
+
const stdout = createTestOutputStream()
|
|
2025
|
+
const cli = goke('mycli', { stdout })
|
|
2026
|
+
let middlewareCalled = false
|
|
2027
|
+
|
|
2028
|
+
cli.use(() => { middlewareCalled = true })
|
|
2029
|
+
cli.help()
|
|
2030
|
+
|
|
2031
|
+
cli
|
|
2032
|
+
.command('build', 'Build')
|
|
2033
|
+
.action(() => {})
|
|
2034
|
+
|
|
2035
|
+
cli.parse(['node', 'bin', 'nonexistent'], { run: true })
|
|
2036
|
+
expect(middlewareCalled).toBe(false)
|
|
2037
|
+
})
|
|
2038
|
+
|
|
2039
|
+
test('middleware runs for default command', () => {
|
|
2040
|
+
const cli = goke('mycli')
|
|
2041
|
+
const order: string[] = []
|
|
2042
|
+
|
|
2043
|
+
cli.use(() => { order.push('mw') })
|
|
2044
|
+
|
|
2045
|
+
cli
|
|
2046
|
+
.command('', 'Default')
|
|
2047
|
+
.action(() => { order.push('action') })
|
|
2048
|
+
|
|
2049
|
+
cli.parse(['node', 'bin'], { run: true })
|
|
2050
|
+
expect(order).toEqual(['mw', 'action'])
|
|
2051
|
+
})
|
|
2052
|
+
|
|
2053
|
+
test('sync middleware error is caught and formatted', () => {
|
|
2054
|
+
const stderr = createTestOutputStream()
|
|
2055
|
+
let exitCode: number | undefined
|
|
2056
|
+
const cli = goke('mycli', { stderr, exit: (code) => { exitCode = code } })
|
|
2057
|
+
|
|
2058
|
+
cli.use(() => {
|
|
2059
|
+
throw new Error('middleware exploded')
|
|
2060
|
+
})
|
|
2061
|
+
|
|
2062
|
+
cli
|
|
2063
|
+
.command('deploy', 'Deploy')
|
|
2064
|
+
.action(() => {})
|
|
2065
|
+
|
|
2066
|
+
cli.parse(['node', 'bin', 'deploy'], { run: true })
|
|
2067
|
+
|
|
2068
|
+
expect(exitCode).toBe(1)
|
|
2069
|
+
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: middleware exploded"`)
|
|
2070
|
+
})
|
|
2071
|
+
|
|
2072
|
+
test('sync middleware error short-circuits command action', () => {
|
|
2073
|
+
const stderr = createTestOutputStream()
|
|
2074
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
2075
|
+
let actionCalled = false
|
|
2076
|
+
|
|
2077
|
+
cli.use(() => {
|
|
2078
|
+
throw new Error('abort')
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
cli
|
|
2082
|
+
.command('build', 'Build')
|
|
2083
|
+
.action(() => { actionCalled = true })
|
|
2084
|
+
|
|
2085
|
+
cli.parse(['node', 'bin', 'build'], { run: true })
|
|
2086
|
+
|
|
2087
|
+
expect(actionCalled).toBe(false)
|
|
2088
|
+
})
|
|
2089
|
+
|
|
2090
|
+
test('mixed sync and async middleware chain correctly', async () => {
|
|
2091
|
+
const cli = goke('mycli')
|
|
2092
|
+
const order: string[] = []
|
|
2093
|
+
|
|
2094
|
+
cli
|
|
2095
|
+
.use(() => { order.push('sync1') })
|
|
2096
|
+
.use(async () => {
|
|
2097
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
2098
|
+
order.push('async')
|
|
2099
|
+
})
|
|
2100
|
+
.use(() => { order.push('sync2') })
|
|
2101
|
+
|
|
2102
|
+
cli
|
|
2103
|
+
.command('run', 'Run')
|
|
2104
|
+
.action(() => { order.push('action') })
|
|
2105
|
+
|
|
2106
|
+
cli.parse(['node', 'bin', 'run'], { run: true })
|
|
2107
|
+
|
|
2108
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
2109
|
+
expect(order).toEqual(['sync1', 'async', 'sync2', 'action'])
|
|
2110
|
+
})
|
|
2111
|
+
})
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* These use expectTypeOf from vitest for compile-time type assertions.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, test, expectTypeOf } from 'vitest'
|
|
9
|
-
import type { StandardTypedV1 } from '../coerce.js'
|
|
9
|
+
import type { StandardTypedV1, StandardJSONSchemaV1 } from '../coerce.js'
|
|
10
|
+
import goke from '../index.js'
|
|
10
11
|
|
|
11
12
|
// ─── Import type helpers from Command.ts ───
|
|
12
13
|
// We can't import the private types directly, so we reconstruct them here
|
|
@@ -109,3 +110,48 @@ describe('type-level: CamelCase', () => {
|
|
|
109
110
|
expectTypeOf<CamelCase<'a-b-c'>>().toEqualTypeOf<'aBC'>()
|
|
110
111
|
})
|
|
111
112
|
})
|
|
113
|
+
|
|
114
|
+
describe('type-level: middleware use() callback inference', () => {
|
|
115
|
+
test('use() callback receives accumulated option types', () => {
|
|
116
|
+
const schema1 = {} as StandardJSONSchemaV1<unknown, number>
|
|
117
|
+
const schema2 = {} as StandardJSONSchemaV1<unknown, string>
|
|
118
|
+
|
|
119
|
+
goke('test')
|
|
120
|
+
.option('--port <port>', schema1)
|
|
121
|
+
.option('--host <host>', schema2)
|
|
122
|
+
.use((options) => {
|
|
123
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
124
|
+
expectTypeOf(options.host).toEqualTypeOf<string>()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('use() only sees options declared before it', () => {
|
|
129
|
+
const schema1 = {} as StandardJSONSchemaV1<unknown, boolean>
|
|
130
|
+
const schema2 = {} as StandardJSONSchemaV1<unknown, number>
|
|
131
|
+
|
|
132
|
+
goke('test')
|
|
133
|
+
.option('--verbose', schema1)
|
|
134
|
+
.use((options) => {
|
|
135
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
136
|
+
// @ts-expect-error port is not declared yet
|
|
137
|
+
options.port
|
|
138
|
+
})
|
|
139
|
+
.option('--port <port>', schema2)
|
|
140
|
+
.use((options) => {
|
|
141
|
+
// Now both are visible
|
|
142
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
|
|
143
|
+
expectTypeOf(options.port).toEqualTypeOf<number>()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('accessing a non-existent option is a type error', () => {
|
|
148
|
+
const schema = {} as StandardJSONSchemaV1<unknown, number>
|
|
149
|
+
|
|
150
|
+
goke('test')
|
|
151
|
+
.option('--port <port>', schema)
|
|
152
|
+
.use((options) => {
|
|
153
|
+
// @ts-expect-error nonExistent was never defined
|
|
154
|
+
options.nonExistent
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
package/src/goke.ts
CHANGED
|
@@ -206,6 +206,11 @@ const getFileName = (input: string) => {
|
|
|
206
206
|
return m ? m[1] : ''
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
|
|
210
|
+
value != null
|
|
211
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
212
|
+
&& typeof (value as any).then === 'function'
|
|
213
|
+
|
|
209
214
|
const camelcaseOptionName = (name: string) => {
|
|
210
215
|
// Camelcase the option name
|
|
211
216
|
// Don't camelcase anything after the dot `.`
|
|
@@ -373,7 +378,7 @@ class Command {
|
|
|
373
378
|
public rawName: string,
|
|
374
379
|
public description: string,
|
|
375
380
|
public config: CommandConfig = {},
|
|
376
|
-
public cli: Goke
|
|
381
|
+
public cli: Goke<any>
|
|
377
382
|
) {
|
|
378
383
|
this.options = []
|
|
379
384
|
this.aliasNames = []
|
|
@@ -750,7 +755,7 @@ class Command {
|
|
|
750
755
|
}
|
|
751
756
|
|
|
752
757
|
class GlobalCommand extends Command {
|
|
753
|
-
constructor(cli: Goke) {
|
|
758
|
+
constructor(cli: Goke<any>) {
|
|
754
759
|
super('@@global@@', '', {}, cli)
|
|
755
760
|
}
|
|
756
761
|
}
|
|
@@ -841,10 +846,12 @@ interface ParsedArgv {
|
|
|
841
846
|
}
|
|
842
847
|
}
|
|
843
848
|
|
|
844
|
-
class Goke extends EventEmitter {
|
|
849
|
+
class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
|
|
845
850
|
/** The program name to display in help and version message */
|
|
846
851
|
name: string
|
|
847
852
|
commands: Command[]
|
|
853
|
+
/** Middleware functions that run before the matched command action, in registration order */
|
|
854
|
+
middlewares: Array<{ action: (options: any) => void | Promise<void> }>
|
|
848
855
|
globalCommand: GlobalCommand
|
|
849
856
|
matchedCommand?: Command
|
|
850
857
|
matchedCommandName?: string
|
|
@@ -885,6 +892,7 @@ class Goke extends EventEmitter {
|
|
|
885
892
|
super()
|
|
886
893
|
this.name = name
|
|
887
894
|
this.commands = []
|
|
895
|
+
this.middlewares = []
|
|
888
896
|
this.rawArgs = []
|
|
889
897
|
this.args = []
|
|
890
898
|
this.options = {}
|
|
@@ -922,12 +930,46 @@ class Goke extends EventEmitter {
|
|
|
922
930
|
* Add a global CLI option.
|
|
923
931
|
*
|
|
924
932
|
* Which is also applied to sub-commands.
|
|
933
|
+
*
|
|
934
|
+
* When a StandardJSONSchemaV1 schema is provided, the return type is narrowed
|
|
935
|
+
* to include the inferred option type — enabling type-safe `.use()` callbacks.
|
|
925
936
|
*/
|
|
926
|
-
option
|
|
937
|
+
option<
|
|
938
|
+
RawName extends string,
|
|
939
|
+
S extends StandardJSONSchemaV1
|
|
940
|
+
>(rawName: RawName, schema: S): Goke<Opts & OptionEntry<RawName, S>>
|
|
941
|
+
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this
|
|
942
|
+
option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
|
|
927
943
|
this.globalCommand.option(rawName, descriptionOrSchema as any)
|
|
928
944
|
return this
|
|
929
945
|
}
|
|
930
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Register a middleware function that runs before the matched command action.
|
|
949
|
+
*
|
|
950
|
+
* Middleware runs in registration order, after option parsing and validation,
|
|
951
|
+
* but before the command's `.action()` callback. Useful for reacting to global
|
|
952
|
+
* options (e.g. setting up logging, initializing state).
|
|
953
|
+
*
|
|
954
|
+
* The callback receives the parsed options object, typed according to all
|
|
955
|
+
* `.option()` calls that precede this `.use()` in the chain.
|
|
956
|
+
*
|
|
957
|
+
* @example
|
|
958
|
+
* ```ts
|
|
959
|
+
* cli
|
|
960
|
+
* .option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
961
|
+
* .use((options) => {
|
|
962
|
+
* if (options.verbose) {
|
|
963
|
+
* process.env.LOG_LEVEL = 'debug'
|
|
964
|
+
* }
|
|
965
|
+
* })
|
|
966
|
+
* ```
|
|
967
|
+
*/
|
|
968
|
+
use(callback: (options: Opts) => void | Promise<void>): this {
|
|
969
|
+
this.middlewares.push({ action: callback })
|
|
970
|
+
return this
|
|
971
|
+
}
|
|
972
|
+
|
|
931
973
|
/**
|
|
932
974
|
* Show help message when `-h, --help` flags appear.
|
|
933
975
|
*
|
|
@@ -1353,18 +1395,45 @@ class Goke extends EventEmitter {
|
|
|
1353
1395
|
})
|
|
1354
1396
|
actionArgs.push(options)
|
|
1355
1397
|
|
|
1356
|
-
const
|
|
1398
|
+
const executeAction = () => command.commandAction!.apply(this, actionArgs)
|
|
1357
1399
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1400
|
+
const handleAsyncError = (err: unknown) => {
|
|
1401
|
+
if (err instanceof Error) {
|
|
1402
|
+
this.handleCliError(err)
|
|
1403
|
+
} else {
|
|
1404
|
+
this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`)
|
|
1405
|
+
}
|
|
1406
|
+
this.exit(1)
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Run middleware in registration order, then the command action.
|
|
1410
|
+
// If any middleware returns a promise, the rest of the chain
|
|
1411
|
+
// (remaining middleware + command action) becomes async.
|
|
1412
|
+
let asyncChain: Promise<any> | null = null
|
|
1413
|
+
|
|
1414
|
+
for (const mw of this.middlewares) {
|
|
1415
|
+
if (asyncChain) {
|
|
1416
|
+
asyncChain = asyncChain.then(() => mw.action(options))
|
|
1417
|
+
} else {
|
|
1418
|
+
try {
|
|
1419
|
+
const mwResult = mw.action(options)
|
|
1420
|
+
if (isPromiseLike(mwResult)) {
|
|
1421
|
+
asyncChain = mwResult as Promise<any>
|
|
1422
|
+
}
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
handleAsyncError(err)
|
|
1425
|
+
return
|
|
1365
1426
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const result = asyncChain
|
|
1431
|
+
? asyncChain.then(executeAction)
|
|
1432
|
+
: executeAction()
|
|
1433
|
+
|
|
1434
|
+
// If the result is a promise, catch async errors
|
|
1435
|
+
if (isPromiseLike(result)) {
|
|
1436
|
+
(result as Promise<any>).catch(handleAsyncError)
|
|
1368
1437
|
}
|
|
1369
1438
|
|
|
1370
1439
|
return result
|