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 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 4 APIs you need to learn for building simple CLIs: `cli.option` `cli.version` `cli.help` `cli.parse`.
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"goke.d.ts","sourceRoot":"","sources":["../src/goke.ts"],"names":[],"mappings":"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"}
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
- * Add a global CLI option.
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
- * Which is also applied to sub-commands.
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
- option(rawName, descriptionOrSchema) {
657
- this.globalCommand.option(rawName, descriptionOrSchema);
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 result = command.commandAction.apply(this, actionArgs);
1033
- // If the action returns a promise, catch async errors
1034
- if (result && typeof result === 'object' && typeof result.catch === 'function') {
1035
- result.catch((err) => {
1036
- if (err instanceof Error) {
1037
- this.handleCliError(err);
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
- else {
1040
- this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`);
1083
+ catch (err) {
1084
+ handleAsyncError(err);
1085
+ return;
1041
1086
  }
1042
- this.exit(1);
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";
@@ -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,SAA4B,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goke",
3
- "version": "6.2.3",
3
+ "version": "6.3.0",
4
4
  "type": "module",
5
5
  "description": "Simple yet powerful framework for building command-line apps. Inspired by cac.",
6
6
  "repository": {
@@ -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(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1) {
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 result = command.commandAction.apply(this, actionArgs)
1398
+ const executeAction = () => command.commandAction!.apply(this, actionArgs)
1357
1399
 
1358
- // If the action returns a promise, catch async errors
1359
- if (result && typeof result === 'object' && typeof result.catch === 'function') {
1360
- result.catch((err: unknown) => {
1361
- if (err instanceof Error) {
1362
- this.handleCliError(err)
1363
- } else {
1364
- this.console.error(`${pc.red(pc.bold('error:'))} ${String(err)}`)
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
- this.exit(1)
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