goke 6.3.2 → 6.4.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/dist/goke.js CHANGED
@@ -9,10 +9,11 @@
9
9
  * - createConsole: factory for console-like objects from output streams
10
10
  * - Utility functions: string helpers, bracket parsing, dot-prop access
11
11
  */
12
- import { EventEmitter } from 'events';
13
12
  import pc from 'picocolors';
14
13
  import mri from "./mri.js";
15
14
  import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js";
15
+ import { createJustBashCommand as createJustBashCommandBridge } from './just-bash.js';
16
+ import { EventEmitter, openInBrowser, process } from '#runtime';
16
17
  // ─── Node.js platform constants ───
17
18
  const processArgs = process.argv;
18
19
  const platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
@@ -238,6 +239,9 @@ class Option {
238
239
  this.isBoolean = true;
239
240
  }
240
241
  }
242
+ clone() {
243
+ return new Option(this.rawName, this.schema ?? this.description);
244
+ }
241
245
  }
242
246
  class Command {
243
247
  rawName;
@@ -551,6 +555,29 @@ class GlobalCommand extends Command {
551
555
  super('@@global@@', '', {}, cli);
552
556
  }
553
557
  }
558
+ const cloneCommandInto = (source, cli) => {
559
+ const target = source instanceof GlobalCommand
560
+ ? new GlobalCommand(cli)
561
+ : new Command(source.rawName, source.description, { ...source.config }, cli);
562
+ target.aliasNames = [...source.aliasNames];
563
+ target.usageText = source.usageText;
564
+ target.versionNumber = source.versionNumber;
565
+ target.examples = [...source.examples];
566
+ target.helpCallback = source.helpCallback;
567
+ target.commandAction = source.commandAction;
568
+ target._hidden = source._hidden;
569
+ target.options = source.options.map((option) => option.clone());
570
+ target.globalCommand = cli.globalCommand;
571
+ return target;
572
+ };
573
+ class GokeProcessExit extends Error {
574
+ code;
575
+ constructor(code) {
576
+ super(`process.exit(${code})`);
577
+ this.name = 'GokeProcessExit';
578
+ this.code = code;
579
+ }
580
+ }
554
581
  /**
555
582
  * Creates a console-like object that writes to the given output streams.
556
583
  *
@@ -566,6 +593,12 @@ function createConsole(stdout, stderr) {
566
593
  error(...args) {
567
594
  stderr.write(args.map(String).join(' ') + '\n');
568
595
  },
596
+ warn(...args) {
597
+ stderr.write(args.map(String).join(' ') + '\n');
598
+ },
599
+ info(...args) {
600
+ stdout.write(args.map(String).join(' ') + '\n');
601
+ },
569
602
  };
570
603
  }
571
604
  // ─── Error formatting ───
@@ -641,6 +674,46 @@ class Goke extends EventEmitter {
641
674
  this.globalCommand = new GlobalCommand(this);
642
675
  this.globalCommand.usage('<command> [options]');
643
676
  }
677
+ clone(options) {
678
+ const cloned = new Goke(this.name, {
679
+ stdout: options?.stdout ?? this.stdout,
680
+ stderr: options?.stderr ?? this.stderr,
681
+ argv: options?.argv ?? this.#defaultArgv,
682
+ columns: options?.columns ?? this.columns,
683
+ exit: options?.exit ?? this.exit,
684
+ });
685
+ cloned.showHelpOnExit = this.showHelpOnExit;
686
+ cloned.showVersionOnExit = this.showVersionOnExit;
687
+ cloned.globalCommand = cloneCommandInto(this.globalCommand, cloned);
688
+ cloned.commands = this.commands.map((command) => cloneCommandInto(command, cloned));
689
+ for (const command of cloned.commands) {
690
+ command.globalCommand = cloned.globalCommand;
691
+ }
692
+ cloned.middlewares = this.middlewares.map((middleware) => ({ action: middleware.action }));
693
+ for (const eventName of this.eventNames()) {
694
+ for (const listener of this.listeners(eventName)) {
695
+ cloned.on(eventName, listener);
696
+ }
697
+ }
698
+ return cloned;
699
+ }
700
+ createExecutionContext(argv = this.rawArgs) {
701
+ return {
702
+ console: this.console,
703
+ process: {
704
+ argv,
705
+ stdout: this.stdout,
706
+ stderr: this.stderr,
707
+ exit: (code) => {
708
+ this.exit(code);
709
+ throw new GokeProcessExit(code);
710
+ },
711
+ },
712
+ };
713
+ }
714
+ async createJustBashCommand(options) {
715
+ return createJustBashCommandBridge(this, options);
716
+ }
644
717
  /**
645
718
  * Add a global usage text.
646
719
  *
@@ -671,15 +744,16 @@ class Goke extends EventEmitter {
671
744
  * options (e.g. setting up logging, initializing state).
672
745
  *
673
746
  * The callback receives the parsed options object, typed according to all
674
- * `.option()` calls that precede this `.use()` in the chain.
747
+ * `.option()` calls that precede this `.use()` in the chain, plus an injected
748
+ * execution context with `{ console, process }` for portable output and exits.
675
749
  *
676
750
  * @example
677
751
  * ```ts
678
752
  * cli
679
753
  * .option('--verbose', z.boolean().default(false).describe('Verbose'))
680
- * .use((options) => {
754
+ * .use((options, { console }) => {
681
755
  * if (options.verbose) {
682
- * process.env.LOG_LEVEL = 'debug'
756
+ * console.log('verbose mode enabled')
683
757
  * }
684
758
  * })
685
759
  * ```
@@ -1036,6 +1110,7 @@ class Goke extends EventEmitter {
1036
1110
  }
1037
1111
  runMatchedCommand() {
1038
1112
  const { args, options, matchedCommand: command } = this;
1113
+ const executionContext = this.createExecutionContext();
1039
1114
  if (!command || !command.commandAction)
1040
1115
  return;
1041
1116
  try {
@@ -1060,6 +1135,7 @@ class Goke extends EventEmitter {
1060
1135
  }
1061
1136
  });
1062
1137
  actionArgs.push(options);
1138
+ actionArgs.push(executionContext);
1063
1139
  const executeAction = () => command.commandAction.apply(this, actionArgs);
1064
1140
  const handleAsyncError = (err) => {
1065
1141
  if (err instanceof Error) {
@@ -1076,58 +1152,49 @@ class Goke extends EventEmitter {
1076
1152
  let asyncChain = null;
1077
1153
  for (const mw of this.middlewares) {
1078
1154
  if (asyncChain) {
1079
- asyncChain = asyncChain.then(() => mw.action(options));
1155
+ asyncChain = asyncChain.then(() => mw.action(options, executionContext));
1080
1156
  }
1081
1157
  else {
1082
1158
  try {
1083
- const mwResult = mw.action(options);
1159
+ const mwResult = mw.action(options, executionContext);
1084
1160
  if (isPromiseLike(mwResult)) {
1085
1161
  asyncChain = mwResult;
1086
1162
  }
1087
1163
  }
1088
1164
  catch (err) {
1165
+ if (err instanceof GokeProcessExit) {
1166
+ throw err;
1167
+ }
1089
1168
  handleAsyncError(err);
1090
1169
  return;
1091
1170
  }
1092
1171
  }
1093
1172
  }
1094
- const result = asyncChain
1095
- ? asyncChain.then(executeAction)
1096
- : executeAction();
1097
- // If the result is a promise, catch async errors
1098
- if (isPromiseLike(result)) {
1099
- result.catch(handleAsyncError);
1100
- }
1101
- return result;
1102
- }
1103
- }
1104
- // ─── openInBrowser ───
1105
- /**
1106
- * Open a URL in the default browser.
1107
- * In non-TTY environments (CI, piped output, agents), prints the URL to stdout instead.
1108
- */
1109
- function openInBrowser(url) {
1110
- if (!process.stdout.isTTY) {
1111
- console.error(url);
1112
- return;
1113
- }
1114
- const { execSync } = require('child_process');
1115
- const platform = process.platform;
1116
- try {
1117
- if (platform === 'darwin') {
1118
- execSync(`open ${JSON.stringify(url)}`, { stdio: 'ignore' });
1173
+ const catchAsyncError = (err) => {
1174
+ if (err instanceof GokeProcessExit) {
1175
+ throw err;
1176
+ }
1177
+ handleAsyncError(err);
1178
+ };
1179
+ if (asyncChain) {
1180
+ return asyncChain
1181
+ .then(executeAction)
1182
+ .catch(catchAsyncError);
1119
1183
  }
1120
- else if (platform === 'win32') {
1121
- execSync(`start "" ${JSON.stringify(url)}`, { stdio: 'ignore' });
1184
+ try {
1185
+ const result = executeAction();
1186
+ return isPromiseLike(result)
1187
+ ? result.catch(catchAsyncError)
1188
+ : result;
1122
1189
  }
1123
- else {
1124
- execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' });
1190
+ catch (err) {
1191
+ if (err instanceof GokeProcessExit) {
1192
+ throw err;
1193
+ }
1194
+ handleAsyncError(err);
1195
+ return;
1125
1196
  }
1126
1197
  }
1127
- catch {
1128
- // fallback: print the URL if open fails
1129
- console.error(url);
1130
- }
1131
1198
  }
1132
- export { createConsole, Command, openInBrowser };
1199
+ export { createConsole, Command, GokeProcessExit, openInBrowser };
1133
1200
  export default Goke;
package/dist/index.d.ts CHANGED
@@ -8,8 +8,8 @@ import { Command } from "./goke.js";
8
8
  declare const goke: (name?: string, options?: GokeOptions) => Goke<{}>;
9
9
  export default goke;
10
10
  export { goke, Goke, Command };
11
- export { createConsole, openInBrowser } from "./goke.js";
12
- export type { GokeOutputStream, GokeConsole, GokeOptions } from "./goke.js";
11
+ export { createConsole, GokeProcessExit, openInBrowser } from "./goke.js";
12
+ export type { GokeOutputStream, GokeConsole, GokeExecutionContext, GokeOptions, GokeProcess } from "./goke.js";
13
13
  export type { StandardTypedV1, StandardJSONSchemaV1, JsonSchema } from "./coerce.js";
14
14
  export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -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,aAA4B,CAAA;AAE1E,eAAe,IAAI,CAAA;AACnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACxD,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,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC9G,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/dist/index.js CHANGED
@@ -7,5 +7,5 @@ import { Command } from "./goke.js";
7
7
  const goke = (name = '', options) => new Goke(name, options);
8
8
  export default goke;
9
9
  export { goke, Goke, Command };
10
- export { createConsole, openInBrowser } from "./goke.js";
10
+ export { createConsole, GokeProcessExit, openInBrowser } from "./goke.js";
11
11
  export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Runtime adapter that exposes a goke CLI as a JustBash-compatible custom command.
3
+ *
4
+ * Structural types here are based on JustBash source definitions:
5
+ * - https://github.com/vercel-labs/just-bash/blob/main/src/custom-commands.ts
6
+ * - https://github.com/vercel-labs/just-bash/blob/main/src/types.ts
7
+ */
8
+ import Goke from './goke.js';
9
+ interface JustBashExecResult {
10
+ stdout: string;
11
+ stderr: string;
12
+ exitCode: number;
13
+ }
14
+ interface JustBashCommand {
15
+ name: string;
16
+ trusted: true;
17
+ execute(args: string[]): Promise<JustBashExecResult>;
18
+ }
19
+ export declare function createJustBashCommand(cli: Goke<any>, options?: {
20
+ name?: string;
21
+ }): JustBashCommand;
22
+ export type { JustBashCommand, JustBashExecResult };
23
+ //# sourceMappingURL=just-bash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"just-bash.d.ts","sourceRoot":"","sources":["../src/just-bash.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,IAAyB,MAAM,WAAW,CAAA;AAGjD,UAAU,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,IAAI,CAAA;IACb,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACrD;AAcD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1B,eAAe,CAiDjB;AAED,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Runtime adapter that exposes a goke CLI as a JustBash-compatible custom command.
3
+ *
4
+ * Structural types here are based on JustBash source definitions:
5
+ * - https://github.com/vercel-labs/just-bash/blob/main/src/custom-commands.ts
6
+ * - https://github.com/vercel-labs/just-bash/blob/main/src/types.ts
7
+ */
8
+ import { GokeProcessExit } from './goke.js';
9
+ function createTextCaptureStream() {
10
+ const chunks = [];
11
+ return {
12
+ get text() {
13
+ return chunks.join('');
14
+ },
15
+ write(data) {
16
+ chunks.push(data);
17
+ },
18
+ };
19
+ }
20
+ export function createJustBashCommand(cli, options) {
21
+ const name = options?.name ?? cli.name;
22
+ if (!name) {
23
+ throw new Error('createJustBashCommand() requires the CLI to have a name');
24
+ }
25
+ if (name.split(/\s+/).length > 1) {
26
+ throw new Error('JustBash custom command names must be a single token');
27
+ }
28
+ return {
29
+ name,
30
+ trusted: true,
31
+ async execute(args) {
32
+ const stdout = createTextCaptureStream();
33
+ const stderr = createTextCaptureStream();
34
+ const argv = ['node', name, ...args];
35
+ const cloned = cli.clone({
36
+ stdout,
37
+ stderr,
38
+ argv,
39
+ exit: (code) => {
40
+ throw new GokeProcessExit(code);
41
+ },
42
+ });
43
+ cloned.name = name;
44
+ try {
45
+ cloned.parse(argv, { run: false });
46
+ await cloned.runMatchedCommand();
47
+ return {
48
+ stdout: stdout.text,
49
+ stderr: stderr.text,
50
+ exitCode: 0,
51
+ };
52
+ }
53
+ catch (error) {
54
+ if (error instanceof GokeProcessExit) {
55
+ return {
56
+ stdout: stdout.text,
57
+ stderr: stderr.text,
58
+ exitCode: error.code,
59
+ };
60
+ }
61
+ throw error;
62
+ }
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Browser-safe runtime stubs for goke core.
3
+ */
4
+ type Listener = (...args: any[]) => void;
5
+ declare class EventEmitter {
6
+ #private;
7
+ on(eventName: string | symbol, listener: Listener): this;
8
+ emit(eventName: string | symbol, ...args: any[]): boolean;
9
+ eventNames(): (string | symbol)[];
10
+ listeners(eventName: string | symbol): Listener[];
11
+ }
12
+ declare const process: {
13
+ argv: string[];
14
+ arch: string;
15
+ platform: string;
16
+ version: string;
17
+ stdout: {
18
+ columns: number;
19
+ isTTY: boolean;
20
+ write(_data: string): void;
21
+ };
22
+ stderr: {
23
+ columns: number;
24
+ isTTY: boolean;
25
+ write(_data: string): void;
26
+ };
27
+ exit(code: number): never;
28
+ };
29
+ declare function openInBrowser(_url: string): void;
30
+ export { EventEmitter, openInBrowser, process };
31
+ //# sourceMappingURL=runtime-browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-browser.d.ts","sourceRoot":"","sources":["../src/runtime-browser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,QAAQ,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;AAExC,cAAM,YAAY;;IAGhB,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ;IAOjD,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAS/C,UAAU;IAIV,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAGrC;AAQD,QAAA,MAAM,OAAO;UACC,MAAM,EAAE;;;;;;;qBAJP,MAAM;;;;;qBAAN,MAAM;;eAUR,MAAM,GAAG,KAAK;CAK1B,CAAA;AAED,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEzC;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Browser-safe runtime stubs for goke core.
3
+ */
4
+ class EventEmitter {
5
+ #listeners = new Map();
6
+ on(eventName, listener) {
7
+ const listeners = this.#listeners.get(eventName) ?? [];
8
+ listeners.push(listener);
9
+ this.#listeners.set(eventName, listeners);
10
+ return this;
11
+ }
12
+ emit(eventName, ...args) {
13
+ const listeners = this.#listeners.get(eventName);
14
+ if (!listeners || listeners.length === 0)
15
+ return false;
16
+ for (const listener of listeners) {
17
+ listener(...args);
18
+ }
19
+ return true;
20
+ }
21
+ eventNames() {
22
+ return [...this.#listeners.keys()];
23
+ }
24
+ listeners(eventName) {
25
+ return [...(this.#listeners.get(eventName) ?? [])];
26
+ }
27
+ }
28
+ const createOutputStream = () => ({
29
+ columns: Number.POSITIVE_INFINITY,
30
+ isTTY: false,
31
+ write(_data) { },
32
+ });
33
+ const process = {
34
+ argv: [],
35
+ arch: 'browser',
36
+ platform: 'browser',
37
+ version: 'browser',
38
+ stdout: createOutputStream(),
39
+ stderr: createOutputStream(),
40
+ exit(code) {
41
+ throw new Error(`process.exit(${code}) is not available in the browser runtime. Pass a custom exit handler to goke(...).`);
42
+ },
43
+ };
44
+ function openInBrowser(_url) {
45
+ // Browser builds should decide how to surface URLs themselves.
46
+ }
47
+ export { EventEmitter, openInBrowser, process };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Node.js runtime bindings for goke core.
3
+ */
4
+ import { EventEmitter } from 'events';
5
+ declare const process: NodeJS.Process;
6
+ declare function openInBrowser(url: string): void;
7
+ export { EventEmitter, openInBrowser, process };
8
+ //# sourceMappingURL=runtime-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-node.d.ts","sourceRoot":"","sources":["../src/runtime-node.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAErC,QAAA,MAAM,OAAO,gBAAqB,CAAA;AAElC,iBAAS,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAiBxC;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Node.js runtime bindings for goke core.
3
+ */
4
+ import { execSync } from 'child_process';
5
+ import { EventEmitter } from 'events';
6
+ const process = globalThis.process;
7
+ function openInBrowser(url) {
8
+ if (!process.stdout.isTTY) {
9
+ process.stderr.write(url + '\n');
10
+ return;
11
+ }
12
+ try {
13
+ if (process.platform === 'darwin') {
14
+ execSync(`open ${JSON.stringify(url)}`, { stdio: 'ignore' });
15
+ }
16
+ else if (process.platform === 'win32') {
17
+ execSync(`start "" ${JSON.stringify(url)}`, { stdio: 'ignore' });
18
+ }
19
+ else {
20
+ execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' });
21
+ }
22
+ }
23
+ catch {
24
+ process.stderr.write(url + '\n');
25
+ }
26
+ }
27
+ export { EventEmitter, openInBrowser, process };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goke",
3
- "version": "6.3.2",
3
+ "version": "6.4.0",
4
4
  "type": "module",
5
5
  "description": "Simple yet powerful framework for building command-line apps. Inspired by cac.",
6
6
  "repository": {
@@ -21,6 +21,14 @@
21
21
  ],
22
22
  "main": "./dist/index.js",
23
23
  "types": "./dist/index.d.ts",
24
+ "imports": {
25
+ "#runtime": {
26
+ "types": "./src/runtime-node.ts",
27
+ "development": "./src/runtime-node.ts",
28
+ "node": "./dist/runtime-node.js",
29
+ "default": "./dist/runtime-browser.js"
30
+ }
31
+ },
24
32
  "exports": {
25
33
  "./package.json": "./package.json",
26
34
  ".": {
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Tests for injected execution context, clone isolation, and the JustBash bridge.
3
+ */
4
+
5
+ import { describe, expect, test } from 'vitest'
6
+ import { z } from 'zod'
7
+ import goke from '../index.js'
8
+ import type { GokeOutputStream, GokeOptions } from '../index.js'
9
+
10
+ const ANSI_RE = /\x1B\[[0-9;]*m/g
11
+
12
+ const stripAnsi = (text: string) => text.replace(ANSI_RE, '')
13
+
14
+ function createTestOutputStream(): GokeOutputStream & { lines: string[]; readonly text: string } {
15
+ const lines: string[] = []
16
+ return {
17
+ lines,
18
+ get text() { return stripAnsi(lines.join('')) },
19
+ write(data: string) { lines.push(data) },
20
+ }
21
+ }
22
+
23
+ function gokeTestable(name = '', options?: Partial<GokeOptions>) {
24
+ return goke(name, {
25
+ ...options,
26
+ exit: () => {},
27
+ })
28
+ }
29
+
30
+ describe('injected execution context', () => {
31
+ test('command action receives injected console and process', () => {
32
+ const stdout = createTestOutputStream()
33
+ const cli = gokeTestable('mycli', { stdout })
34
+ let seenArgv: string[] | undefined
35
+
36
+ cli
37
+ .command('status', 'Show status')
38
+ .action((options, { console, process }) => {
39
+ console.log('ready')
40
+ seenArgv = process.argv
41
+ })
42
+
43
+ cli.parse(['node', 'bin', 'status'], { run: true })
44
+
45
+ expect(stdout.text).toBe('ready\n')
46
+ expect(seenArgv).toEqual(['node', 'bin', 'status'])
47
+ })
48
+
49
+ test('middleware receives injected console and process', () => {
50
+ const stdout = createTestOutputStream()
51
+ const cli = gokeTestable('mycli', { stdout })
52
+ let seenArgv: string[] | undefined
53
+
54
+ cli
55
+ .use((options, { console, process }) => {
56
+ console.log('middleware')
57
+ seenArgv = process.argv
58
+ })
59
+ .command('build', 'Build')
60
+ .action(() => {})
61
+
62
+ cli.parse(['node', 'bin', 'build'], { run: true })
63
+
64
+ expect(stdout.text).toBe('middleware\n')
65
+ expect(seenArgv).toEqual(['node', 'bin', 'build'])
66
+ })
67
+ })
68
+
69
+ describe('clone', () => {
70
+ test('clone creates isolated parse state', () => {
71
+ const cli = gokeTestable('mycli')
72
+
73
+ cli.command('build', 'Build').action(() => {})
74
+
75
+ const cloned = (cli as any).clone({ exit: () => {} })
76
+
77
+ cloned.parse(['node', 'bin', 'build'], { run: false })
78
+
79
+ expect(cloned).not.toBe(cli)
80
+ expect(cloned.matchedCommandName).toBe('build')
81
+ expect(cli.matchedCommandName).toBeUndefined()
82
+ })
83
+ })
84
+
85
+ describe('createJustBashCommand', () => {
86
+ test('runs multi-word goke subcommands through one just-bash command', async () => {
87
+ const cli = gokeTestable('parent')
88
+
89
+ cli
90
+ .command('child commandwithspaces', 'Run nested command')
91
+ .option('--name <name>', z.string().describe('Name'))
92
+ .action((options, { console }) => {
93
+ console.log(`hello ${options.name}`)
94
+ })
95
+
96
+ const customCommand = await (cli as any).createJustBashCommand()
97
+ const result = await customCommand.execute(['child', 'commandwithspaces', '--name', 'Tommy'])
98
+
99
+ expect(result).toEqual({
100
+ stdout: 'hello Tommy\n',
101
+ stderr: '',
102
+ exitCode: 0,
103
+ })
104
+ })
105
+
106
+ test('maps injected process.exit to a command exit code', async () => {
107
+ const cli = gokeTestable('parent')
108
+
109
+ cli
110
+ .command('fail', 'Exit with custom code')
111
+ .action((options, { process }) => {
112
+ process.exit(7)
113
+ })
114
+
115
+ const customCommand = await (cli as any).createJustBashCommand()
116
+ const result = await customCommand.execute(['fail'])
117
+
118
+ expect(result).toEqual({
119
+ stdout: '',
120
+ stderr: '',
121
+ exitCode: 7,
122
+ })
123
+ })
124
+ })
@@ -119,9 +119,12 @@ describe('type-level: middleware use() callback inference', () => {
119
119
  goke('test')
120
120
  .option('--port <port>', schema1)
121
121
  .option('--host <host>', schema2)
122
- .use((options) => {
122
+ .use((options, { console, process }) => {
123
123
  expectTypeOf(options.port).toEqualTypeOf<number>()
124
124
  expectTypeOf(options.host).toEqualTypeOf<string>()
125
+ expectTypeOf(process.argv).toEqualTypeOf<string[]>()
126
+ expectTypeOf(process.stdout.write).toEqualTypeOf<(data: string) => void>()
127
+ expectTypeOf(console.log).toBeFunction()
125
128
  })
126
129
  })
127
130
 
@@ -131,16 +134,18 @@ describe('type-level: middleware use() callback inference', () => {
131
134
 
132
135
  goke('test')
133
136
  .option('--verbose', schema1)
134
- .use((options) => {
137
+ .use((options, { process }) => {
135
138
  expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
139
+ expectTypeOf(process.exit).toEqualTypeOf<(code: number) => void>()
136
140
  // @ts-expect-error port is not declared yet
137
141
  options.port
138
142
  })
139
143
  .option('--port <port>', schema2)
140
- .use((options) => {
144
+ .use((options, { console }) => {
141
145
  // Now both are visible
142
146
  expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
143
147
  expectTypeOf(options.port).toEqualTypeOf<number>()
148
+ expectTypeOf(console.error).toBeFunction()
144
149
  })
145
150
  })
146
151
 
@@ -149,7 +154,8 @@ describe('type-level: middleware use() callback inference', () => {
149
154
 
150
155
  goke('test')
151
156
  .option('--port <port>', schema)
152
- .use((options) => {
157
+ .use((options, { process }) => {
158
+ expectTypeOf(process.stderr.write).toEqualTypeOf<(data: string) => void>()
153
159
  // @ts-expect-error nonExistent was never defined
154
160
  options.nonExistent
155
161
  })