goke 6.4.0 → 6.5.1

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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Vendored from picocolors by Alexey Raspopov (MIT license).
3
+ * Source: https://github.com/alexeyraspopov/picocolors/blob/main/picocolors.js
4
+ */
5
+ type Formatter = (input: unknown) => string;
6
+ interface PicoColors {
7
+ isColorSupported: boolean;
8
+ reset: Formatter;
9
+ bold: Formatter;
10
+ dim: Formatter;
11
+ italic: Formatter;
12
+ underline: Formatter;
13
+ inverse: Formatter;
14
+ hidden: Formatter;
15
+ strikethrough: Formatter;
16
+ black: Formatter;
17
+ red: Formatter;
18
+ green: Formatter;
19
+ yellow: Formatter;
20
+ blue: Formatter;
21
+ magenta: Formatter;
22
+ cyan: Formatter;
23
+ white: Formatter;
24
+ gray: Formatter;
25
+ bgBlack: Formatter;
26
+ bgRed: Formatter;
27
+ bgGreen: Formatter;
28
+ bgYellow: Formatter;
29
+ bgBlue: Formatter;
30
+ bgMagenta: Formatter;
31
+ bgCyan: Formatter;
32
+ bgWhite: Formatter;
33
+ blackBright: Formatter;
34
+ redBright: Formatter;
35
+ greenBright: Formatter;
36
+ yellowBright: Formatter;
37
+ blueBright: Formatter;
38
+ magentaBright: Formatter;
39
+ cyanBright: Formatter;
40
+ whiteBright: Formatter;
41
+ bgBlackBright: Formatter;
42
+ bgRedBright: Formatter;
43
+ bgGreenBright: Formatter;
44
+ bgYellowBright: Formatter;
45
+ bgBlueBright: Formatter;
46
+ bgMagentaBright: Formatter;
47
+ bgCyanBright: Formatter;
48
+ bgWhiteBright: Formatter;
49
+ }
50
+ declare const createColors: (enabled?: boolean) => PicoColors;
51
+ declare const pc: PicoColors;
52
+ export { createColors };
53
+ export type { PicoColors };
54
+ export default pc;
55
+ //# sourceMappingURL=picocolors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picocolors.d.ts","sourceRoot":"","sources":["../src/picocolors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;AAE3C,UAAU,UAAU;IAClB,gBAAgB,EAAE,OAAO,CAAA;IACzB,KAAK,EAAE,SAAS,CAAA;IAChB,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,SAAS,CAAA;IACd,MAAM,EAAE,SAAS,CAAA;IACjB,SAAS,EAAE,SAAS,CAAA;IACpB,OAAO,EAAE,SAAS,CAAA;IAClB,MAAM,EAAE,SAAS,CAAA;IACjB,aAAa,EAAE,SAAS,CAAA;IACxB,KAAK,EAAE,SAAS,CAAA;IAChB,GAAG,EAAE,SAAS,CAAA;IACd,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,SAAS,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,IAAI,EAAE,SAAS,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,SAAS,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;IACjB,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,SAAS,CAAA;IACjB,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,SAAS,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,SAAS,CAAA;IACtB,YAAY,EAAE,SAAS,CAAA;IACvB,UAAU,EAAE,SAAS,CAAA;IACrB,aAAa,EAAE,SAAS,CAAA;IACxB,UAAU,EAAE,SAAS,CAAA;IACrB,WAAW,EAAE,SAAS,CAAA;IACtB,aAAa,EAAE,SAAS,CAAA;IACxB,WAAW,EAAE,SAAS,CAAA;IACtB,aAAa,EAAE,SAAS,CAAA;IACxB,cAAc,EAAE,SAAS,CAAA;IACzB,YAAY,EAAE,SAAS,CAAA;IACvB,eAAe,EAAE,SAAS,CAAA;IAC1B,YAAY,EAAE,SAAS,CAAA;IACvB,aAAa,EAAE,SAAS,CAAA;CACzB;AAkCD,QAAA,MAAM,YAAY,GAAI,iBAA0B,KAAG,UA+ClD,CAAA;AAED,QAAA,MAAM,EAAE,YAAiB,CAAA;AAEzB,OAAO,EAAE,YAAY,EAAE,CAAA;AACvB,YAAY,EAAE,UAAU,EAAE,CAAA;AAC1B,eAAe,EAAE,CAAA"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Vendored from picocolors by Alexey Raspopov (MIT license).
3
+ * Source: https://github.com/alexeyraspopov/picocolors/blob/main/picocolors.js
4
+ */
5
+ import { process } from '#runtime';
6
+ const argv = process.argv || [];
7
+ const env = process.env || {};
8
+ const isColorSupported = !(!!env.NO_COLOR || argv.includes('--no-color'))
9
+ && (!!env.FORCE_COLOR
10
+ || argv.includes('--color')
11
+ || process.platform === 'win32'
12
+ || (process.stdout.isTTY && env.TERM !== 'dumb')
13
+ || !!env.CI);
14
+ const replaceClose = (string, close, replace, index) => {
15
+ let result = '';
16
+ let cursor = 0;
17
+ do {
18
+ result += string.substring(cursor, index) + replace;
19
+ cursor = index + close.length;
20
+ index = string.indexOf(close, cursor);
21
+ } while (~index);
22
+ return result + string.substring(cursor);
23
+ };
24
+ const formatter = (open, close, replace = open) => (input) => {
25
+ const string = String(input);
26
+ const index = string.indexOf(close, open.length);
27
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
28
+ };
29
+ const createColors = (enabled = isColorSupported) => {
30
+ const f = enabled ? formatter : () => String;
31
+ return {
32
+ isColorSupported: enabled,
33
+ reset: f('\x1b[0m', '\x1b[0m'),
34
+ bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
35
+ dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
36
+ italic: f('\x1b[3m', '\x1b[23m'),
37
+ underline: f('\x1b[4m', '\x1b[24m'),
38
+ inverse: f('\x1b[7m', '\x1b[27m'),
39
+ hidden: f('\x1b[8m', '\x1b[28m'),
40
+ strikethrough: f('\x1b[9m', '\x1b[29m'),
41
+ black: f('\x1b[30m', '\x1b[39m'),
42
+ red: f('\x1b[31m', '\x1b[39m'),
43
+ green: f('\x1b[32m', '\x1b[39m'),
44
+ yellow: f('\x1b[33m', '\x1b[39m'),
45
+ blue: f('\x1b[34m', '\x1b[39m'),
46
+ magenta: f('\x1b[35m', '\x1b[39m'),
47
+ cyan: f('\x1b[36m', '\x1b[39m'),
48
+ white: f('\x1b[37m', '\x1b[39m'),
49
+ gray: f('\x1b[90m', '\x1b[39m'),
50
+ bgBlack: f('\x1b[40m', '\x1b[49m'),
51
+ bgRed: f('\x1b[41m', '\x1b[49m'),
52
+ bgGreen: f('\x1b[42m', '\x1b[49m'),
53
+ bgYellow: f('\x1b[43m', '\x1b[49m'),
54
+ bgBlue: f('\x1b[44m', '\x1b[49m'),
55
+ bgMagenta: f('\x1b[45m', '\x1b[49m'),
56
+ bgCyan: f('\x1b[46m', '\x1b[49m'),
57
+ bgWhite: f('\x1b[47m', '\x1b[49m'),
58
+ blackBright: f('\x1b[90m', '\x1b[39m'),
59
+ redBright: f('\x1b[91m', '\x1b[39m'),
60
+ greenBright: f('\x1b[92m', '\x1b[39m'),
61
+ yellowBright: f('\x1b[93m', '\x1b[39m'),
62
+ blueBright: f('\x1b[94m', '\x1b[39m'),
63
+ magentaBright: f('\x1b[95m', '\x1b[39m'),
64
+ cyanBright: f('\x1b[96m', '\x1b[39m'),
65
+ whiteBright: f('\x1b[97m', '\x1b[39m'),
66
+ bgBlackBright: f('\x1b[100m', '\x1b[49m'),
67
+ bgRedBright: f('\x1b[101m', '\x1b[49m'),
68
+ bgGreenBright: f('\x1b[102m', '\x1b[49m'),
69
+ bgYellowBright: f('\x1b[103m', '\x1b[49m'),
70
+ bgBlueBright: f('\x1b[104m', '\x1b[49m'),
71
+ bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
72
+ bgCyanBright: f('\x1b[106m', '\x1b[49m'),
73
+ bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
74
+ };
75
+ };
76
+ const pc = createColors();
77
+ export { createColors };
78
+ export default pc;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Browser-safe runtime stubs for goke core.
3
3
  */
4
+ import type { GokeFs } from './goke-fs.js';
4
5
  type Listener = (...args: any[]) => void;
5
6
  declare class EventEmitter {
6
7
  #private;
@@ -12,6 +13,8 @@ declare class EventEmitter {
12
13
  declare const process: {
13
14
  argv: string[];
14
15
  arch: string;
16
+ cwd(): string;
17
+ env: Record<string, string | undefined>;
15
18
  platform: string;
16
19
  version: string;
17
20
  stdout: {
@@ -26,6 +29,7 @@ declare const process: {
26
29
  };
27
30
  exit(code: number): never;
28
31
  };
32
+ declare const fs: GokeFs;
29
33
  declare function openInBrowser(_url: string): void;
30
- export { EventEmitter, openInBrowser, process };
34
+ export { EventEmitter, fs, openInBrowser, process };
31
35
  //# sourceMappingURL=runtime-browser.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"runtime-browser.d.ts","sourceRoot":"","sources":["../src/runtime-browser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,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;;;SAKQ,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;;;;;;qBATjD,MAAM;;;;;qBAAN,MAAM;;eAcR,MAAM,GAAG,KAAK;CAK1B,CAAA;AAcD,QAAA,MAAM,EAAE,EAAE,MAcT,CAAA;AAED,iBAAS,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEzC;AAED,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA"}
@@ -33,6 +33,10 @@ const createOutputStream = () => ({
33
33
  const process = {
34
34
  argv: [],
35
35
  arch: 'browser',
36
+ cwd() {
37
+ return '/';
38
+ },
39
+ env: Object.create(null),
36
40
  platform: 'browser',
37
41
  version: 'browser',
38
42
  stdout: createOutputStream(),
@@ -41,7 +45,30 @@ const process = {
41
45
  throw new Error(`process.exit(${code}) is not available in the browser runtime. Pass a custom exit handler to goke(...).`);
42
46
  },
43
47
  };
48
+ function createBrowserFsError(methodName) {
49
+ return new Error(`fs.${methodName}() is not available in the browser runtime. Pass a custom fs implementation to goke(...).`);
50
+ }
51
+ function createUnsupportedFsMethod(methodName) {
52
+ return (async () => {
53
+ throw createBrowserFsError(methodName);
54
+ });
55
+ }
56
+ const fs = {
57
+ appendFile: createUnsupportedFsMethod('appendFile'),
58
+ chmod: createUnsupportedFsMethod('chmod'),
59
+ copyFile: createUnsupportedFsMethod('copyFile'),
60
+ link: createUnsupportedFsMethod('link'),
61
+ mkdir: createUnsupportedFsMethod('mkdir'),
62
+ readFile: createUnsupportedFsMethod('readFile'),
63
+ readlink: createUnsupportedFsMethod('readlink'),
64
+ realpath: createUnsupportedFsMethod('realpath'),
65
+ rename: createUnsupportedFsMethod('rename'),
66
+ rm: createUnsupportedFsMethod('rm'),
67
+ symlink: createUnsupportedFsMethod('symlink'),
68
+ utimes: createUnsupportedFsMethod('utimes'),
69
+ writeFile: createUnsupportedFsMethod('writeFile'),
70
+ };
44
71
  function openInBrowser(_url) {
45
72
  // Browser builds should decide how to surface URLs themselves.
46
73
  }
47
- export { EventEmitter, openInBrowser, process };
74
+ export { EventEmitter, fs, openInBrowser, process };
@@ -2,7 +2,9 @@
2
2
  * Node.js runtime bindings for goke core.
3
3
  */
4
4
  import { EventEmitter } from 'events';
5
+ import type { GokeFs } from './goke-fs.js';
5
6
  declare const process: NodeJS.Process;
7
+ declare const fs: GokeFs;
6
8
  declare function openInBrowser(url: string): void;
7
- export { EventEmitter, openInBrowser, process };
9
+ export { EventEmitter, fs, openInBrowser, process };
8
10
  //# sourceMappingURL=runtime-node.d.ts.map
@@ -1 +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"}
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,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,QAAA,MAAM,OAAO,gBAAqB,CAAA;AAClC,QAAA,MAAM,EAAE,EAAE,MAAe,CAAA;AAEzB,iBAAS,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAiBxC;AAED,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA"}
@@ -3,7 +3,9 @@
3
3
  */
4
4
  import { execSync } from 'child_process';
5
5
  import { EventEmitter } from 'events';
6
+ import * as nodeFs from 'node:fs/promises';
6
7
  const process = globalThis.process;
8
+ const fs = nodeFs;
7
9
  function openInBrowser(url) {
8
10
  if (!process.stdout.isTTY) {
9
11
  process.stderr.write(url + '\n');
@@ -24,4 +26,4 @@ function openInBrowser(url) {
24
26
  process.stderr.write(url + '\n');
25
27
  }
26
28
  }
27
- export { EventEmitter, openInBrowser, process };
29
+ export { EventEmitter, fs, openInBrowser, process };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goke",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
4
4
  "type": "module",
5
5
  "description": "Simple yet powerful framework for building command-line apps. Inspired by cac.",
6
6
  "repository": {
@@ -48,13 +48,12 @@
48
48
  "dist",
49
49
  "src"
50
50
  ],
51
- "dependencies": {
52
- "picocolors": "^1.1.1"
53
- },
51
+ "dependencies": {},
54
52
  "author": "remorses, egoist <0x142857@gmail.com>",
55
53
  "license": "MIT",
56
54
  "devDependencies": {
57
55
  "@types/node": "^25.2.2",
56
+ "just-bash": "^2.14.0",
58
57
  "typescript": "^5.9.3",
59
58
  "vitest": "^3.1.0",
60
59
  "zod": "^4.3.6"
@@ -3,6 +3,9 @@ import goke, { createConsole } from '../index.js'
3
3
  import type { GokeOutputStream, GokeOptions } from '../index.js'
4
4
  import { coerceBySchema } from '../coerce.js'
5
5
  import { z } from 'zod'
6
+ import { mkdtemp, readFile, rm } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
6
9
 
7
10
  const ANSI_RE = /\x1B\[[0-9;]*m/g
8
11
 
@@ -157,6 +160,96 @@ describe('error formatting', () => {
157
160
  })
158
161
  })
159
162
 
163
+ describe('injected fs', () => {
164
+ test('command actions can use the default node fs for cli storage', async () => {
165
+ const stdout = createTestOutputStream()
166
+ const cli = gokeTestable('mycli', { stdout })
167
+ const originalCwd = process.cwd()
168
+ const tempDir = await mkdtemp(join(tmpdir(), 'goke-fs-'))
169
+
170
+ try {
171
+ process.chdir(tempDir)
172
+
173
+ cli
174
+ .command('login', 'Persist login state')
175
+ .option('--token <token>', z.string().describe('Token'))
176
+ .action(async (options, { fs, console }) => {
177
+ await fs.mkdir('.mycli', { recursive: true })
178
+ await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
179
+ console.log('saved credentials')
180
+ })
181
+
182
+ cli.parse(['node', 'bin', 'login', '--token', 'abc123'], { run: false })
183
+ await cli.runMatchedCommand()
184
+
185
+ expect(stdout.text).toBe('saved credentials\n')
186
+ expect(await readFile(join(tempDir, '.mycli/auth.json'), 'utf8')).toBe('{"token":"abc123"}')
187
+ } finally {
188
+ process.chdir(originalCwd)
189
+ await rm(tempDir, { recursive: true, force: true })
190
+ }
191
+ })
192
+ })
193
+
194
+ describe('injected process context', () => {
195
+ test('command actions receive host cwd, env, and stdin defaults', async () => {
196
+ const stdout = createTestOutputStream()
197
+ const cli = gokeTestable('mycli', { stdout })
198
+ const originalCwd = process.cwd()
199
+ const originalEnv = process.env.GOKE_TEST_TOKEN
200
+ const tempDir = await mkdtemp(join(tmpdir(), 'goke-process-'))
201
+
202
+ try {
203
+ process.chdir(tempDir)
204
+ process.env.GOKE_TEST_TOKEN = 'abc123'
205
+
206
+ cli
207
+ .command('context', 'Inspect process context')
208
+ .action((options, { console, process }) => {
209
+ console.log(JSON.stringify({
210
+ cwd: process.cwd,
211
+ stdin: process.stdin,
212
+ token: process.env.GOKE_TEST_TOKEN,
213
+ }))
214
+ })
215
+
216
+ cli.parse(['node', 'bin', 'context'], { run: false })
217
+ await cli.runMatchedCommand()
218
+
219
+ expect(stdout.text).toBe(
220
+ `${JSON.stringify({ cwd: process.cwd(), stdin: '', token: 'abc123' })}\n`,
221
+ )
222
+ } finally {
223
+ process.chdir(originalCwd)
224
+ if (originalEnv === undefined) {
225
+ delete process.env.GOKE_TEST_TOKEN
226
+ } else {
227
+ process.env.GOKE_TEST_TOKEN = originalEnv
228
+ }
229
+ await rm(tempDir, { recursive: true, force: true })
230
+ }
231
+ })
232
+
233
+ test('custom injected env stays mutable inside command actions', async () => {
234
+ const stdout = createTestOutputStream()
235
+ const env: Record<string, string | undefined> = { TOKEN: 'before' }
236
+ const cli = gokeTestable('mycli', { env, stdout })
237
+
238
+ cli
239
+ .command('context', 'Mutate process env')
240
+ .action((options, { console, process }) => {
241
+ process.env.TOKEN = 'after'
242
+ console.log(process.env.TOKEN)
243
+ })
244
+
245
+ cli.parse(['node', 'bin', 'context'], { run: false })
246
+ await cli.runMatchedCommand()
247
+
248
+ expect(stdout.text).toBe('after\n')
249
+ expect(env.TOKEN).toBe('after')
250
+ })
251
+ })
252
+
160
253
  test('double dashes', () => {
161
254
  const cli = goke()
162
255
 
@@ -72,7 +72,7 @@ describe('clone', () => {
72
72
 
73
73
  cli.command('build', 'Build').action(() => {})
74
74
 
75
- const cloned = (cli as any).clone({ exit: () => {} })
75
+ const cloned = cli.clone({ exit: () => {} })
76
76
 
77
77
  cloned.parse(['node', 'bin', 'build'], { run: false })
78
78
 
@@ -93,7 +93,7 @@ describe('createJustBashCommand', () => {
93
93
  console.log(`hello ${options.name}`)
94
94
  })
95
95
 
96
- const customCommand = await (cli as any).createJustBashCommand()
96
+ const customCommand = await cli.createJustBashCommand()
97
97
  const result = await customCommand.execute(['child', 'commandwithspaces', '--name', 'Tommy'])
98
98
 
99
99
  expect(result).toEqual({
@@ -103,6 +103,174 @@ describe('createJustBashCommand', () => {
103
103
  })
104
104
  })
105
105
 
106
+ test('works through real just-bash exec with a goke custom command', async () => {
107
+ const { Bash } = await import('just-bash')
108
+ const cli = gokeTestable('parent')
109
+
110
+ cli
111
+ .command('child commandwithspaces', 'Run nested command')
112
+ .option('--name <name>', z.string().describe('Name'))
113
+ .action((options, { console }) => {
114
+ console.log(`hello ${options.name}`)
115
+ })
116
+
117
+ const bash = new Bash({
118
+ customCommands: [await cli.createJustBashCommand()],
119
+ })
120
+
121
+ const result = await bash.exec('parent child commandwithspaces --name Tommy')
122
+
123
+ expect(result.stdout).toBe('hello Tommy\n')
124
+ expect(result.stderr).toBe('')
125
+ expect(result.exitCode).toBe(0)
126
+ })
127
+
128
+ test('maps injected fs to the just-bash virtual filesystem', async () => {
129
+ const { Bash } = await import('just-bash')
130
+ const cli = gokeTestable('parent')
131
+
132
+ cli
133
+ .command('login', 'Persist login state')
134
+ .option('--token <token>', z.string().describe('Token'))
135
+ .action(async (options, { fs, console }) => {
136
+ await fs.mkdir('.mycli', { recursive: true })
137
+ await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
138
+ console.log('saved credentials')
139
+ })
140
+
141
+ const bash = new Bash({
142
+ customCommands: [await cli.createJustBashCommand()],
143
+ })
144
+
145
+ const loginResult = await bash.exec('mkdir project && cd project && parent login --token Tommy')
146
+ const catResult = await bash.exec('cd project && cat .mycli/auth.json')
147
+
148
+ expect(loginResult.stdout).toBe('saved credentials\n')
149
+ expect(loginResult.stderr).toBe('')
150
+ expect(loginResult.exitCode).toBe(0)
151
+ expect(catResult.stdout).toBe('{"token":"Tommy"}')
152
+ expect(catResult.stderr).toBe('')
153
+ expect(catResult.exitCode).toBe(0)
154
+ })
155
+
156
+ test('real just-bash exec passes the configured in-memory fs to the goke command', async () => {
157
+ const { Bash, InMemoryFs } = await import('just-bash')
158
+ const cli = gokeTestable('parent')
159
+
160
+ cli
161
+ .command('login', 'Persist login state')
162
+ .option('--token <token>', z.string().describe('Token'))
163
+ .action(async (options, { fs, console }) => {
164
+ await fs.mkdir('.mycli', { recursive: true })
165
+ await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
166
+ console.log('saved credentials')
167
+ })
168
+
169
+ const virtualFs = new InMemoryFs()
170
+ await virtualFs.mkdir('/project', { recursive: true })
171
+
172
+ const bash = new Bash({
173
+ fs: virtualFs,
174
+ cwd: '/project',
175
+ customCommands: [await cli.createJustBashCommand()],
176
+ })
177
+
178
+ const result = await bash.exec('parent login --token Tommy')
179
+
180
+ expect(result.stdout).toBe('saved credentials\n')
181
+ expect(result.stderr).toBe('')
182
+ expect(result.exitCode).toBe(0)
183
+ expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe('{"token":"Tommy"}')
184
+ })
185
+
186
+ test('real just-bash exec passes sandbox cwd, stdin, and env through process context', async () => {
187
+ const { Bash, InMemoryFs } = await import('just-bash')
188
+ const cli = gokeTestable('parent')
189
+
190
+ cli
191
+ .command('context', 'Inspect process context')
192
+ .action((options, { console, process }) => {
193
+ console.log(JSON.stringify({
194
+ cwd: process.cwd,
195
+ stdin: process.stdin,
196
+ token: process.env.TOKEN,
197
+ }))
198
+ })
199
+
200
+ const virtualFs = new InMemoryFs()
201
+ await virtualFs.mkdir('/project', { recursive: true })
202
+
203
+ const bash = new Bash({
204
+ fs: virtualFs,
205
+ cwd: '/project',
206
+ env: { TOKEN: 'Tommy' },
207
+ customCommands: [await cli.createJustBashCommand()],
208
+ })
209
+
210
+ const result = await bash.exec('parent context', { stdin: 'hello from stdin' })
211
+
212
+ expect(result.stdout).toBe(
213
+ `${JSON.stringify({ cwd: '/project', stdin: 'hello from stdin', token: 'Tommy' })}\n`,
214
+ )
215
+ expect(result.stderr).toBe('')
216
+ expect(result.exitCode).toBe(0)
217
+ })
218
+
219
+ test('explicit just-bash context exposes a mutable env object backed by the sandbox env', async () => {
220
+ const { InMemoryFs } = await import('just-bash')
221
+ const cli = gokeTestable('parent')
222
+
223
+ cli
224
+ .command('mutate-env', 'Mutate sandbox env')
225
+ .action((options, { console, process }) => {
226
+ process.env.TOKEN = 'updated'
227
+ console.log(process.env.TOKEN)
228
+ })
229
+
230
+ const virtualFs = new InMemoryFs()
231
+ await virtualFs.mkdir('/project', { recursive: true })
232
+ const env = new Map<string, string>([['TOKEN', 'before']])
233
+ const customCommand = await cli.createJustBashCommand()
234
+
235
+ const result = await customCommand.execute(
236
+ ['mutate-env'],
237
+ { fs: virtualFs, cwd: '/project', env, stdin: '' },
238
+ )
239
+
240
+ expect(result.stdout).toBe('updated\n')
241
+ expect(result.stderr).toBe('')
242
+ expect(result.exitCode).toBe(0)
243
+ expect(env.get('TOKEN')).toBe('updated')
244
+ })
245
+
246
+ test('accepts an explicit just-bash fs context when executing the custom command', async () => {
247
+ const { InMemoryFs } = await import('just-bash')
248
+ const cli = gokeTestable('parent')
249
+
250
+ cli
251
+ .command('login', 'Persist login state')
252
+ .option('--token <token>', z.string().describe('Token'))
253
+ .action(async (options, { fs, console }) => {
254
+ await fs.mkdir('.mycli', { recursive: true })
255
+ await fs.writeFile('.mycli/auth.json', JSON.stringify({ token: options.token }), 'utf8')
256
+ console.log('saved credentials')
257
+ })
258
+
259
+ const customCommand = await cli.createJustBashCommand()
260
+ const virtualFs = new InMemoryFs()
261
+ await virtualFs.mkdir('/project', { recursive: true })
262
+
263
+ const result = await customCommand.execute(
264
+ ['login', '--token', 'Tommy'],
265
+ { fs: virtualFs, cwd: '/project', env: new Map(), stdin: '' },
266
+ )
267
+
268
+ expect(result.stdout).toBe('saved credentials\n')
269
+ expect(result.stderr).toBe('')
270
+ expect(result.exitCode).toBe(0)
271
+ expect(await virtualFs.readFile('/project/.mycli/auth.json', 'utf8')).toBe('{"token":"Tommy"}')
272
+ })
273
+
106
274
  test('maps injected process.exit to a command exit code', async () => {
107
275
  const cli = gokeTestable('parent')
108
276
 
@@ -112,7 +280,7 @@ describe('createJustBashCommand', () => {
112
280
  process.exit(7)
113
281
  })
114
282
 
115
- const customCommand = await (cli as any).createJustBashCommand()
283
+ const customCommand = await cli.createJustBashCommand()
116
284
  const result = await customCommand.execute(['fail'])
117
285
 
118
286
  expect(result).toEqual({
@@ -119,10 +119,14 @@ 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, { console, process }) => {
122
+ .use((options, { console, fs, process }) => {
123
123
  expectTypeOf(options.port).toEqualTypeOf<number>()
124
124
  expectTypeOf(options.host).toEqualTypeOf<string>()
125
+ expectTypeOf(fs.mkdir).toBeFunction()
125
126
  expectTypeOf(process.argv).toEqualTypeOf<string[]>()
127
+ expectTypeOf(process.cwd).toEqualTypeOf<string>()
128
+ expectTypeOf(process.env).toEqualTypeOf<Record<string, string | undefined>>()
129
+ expectTypeOf(process.stdin).toEqualTypeOf<string>()
126
130
  expectTypeOf(process.stdout.write).toEqualTypeOf<(data: string) => void>()
127
131
  expectTypeOf(console.log).toBeFunction()
128
132
  })
@@ -134,8 +138,9 @@ describe('type-level: middleware use() callback inference', () => {
134
138
 
135
139
  goke('test')
136
140
  .option('--verbose', schema1)
137
- .use((options, { process }) => {
141
+ .use((options, { fs, process }) => {
138
142
  expectTypeOf(options.verbose).toEqualTypeOf<boolean | undefined>()
143
+ expectTypeOf(fs.writeFile).toBeFunction()
139
144
  expectTypeOf(process.exit).toEqualTypeOf<(code: number) => void>()
140
145
  // @ts-expect-error port is not declared yet
141
146
  options.port
@@ -154,7 +159,8 @@ describe('type-level: middleware use() callback inference', () => {
154
159
 
155
160
  goke('test')
156
161
  .option('--port <port>', schema)
157
- .use((options, { process }) => {
162
+ .use((options, { fs, process }) => {
163
+ expectTypeOf(fs.readFile).toBeFunction()
158
164
  expectTypeOf(process.stderr.write).toEqualTypeOf<(data: string) => void>()
159
165
  // @ts-expect-error nonExistent was never defined
160
166
  options.nonExistent
package/src/goke-fs.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Node-like filesystem types used by injected goke execution contexts.
3
+ */
4
+
5
+ import type { MakeDirectoryOptions, Mode, PathLike, RmOptions, TimeLike } from 'node:fs'
6
+
7
+ type GokeFsFileContent = string | NodeJS.ArrayBufferView
8
+ type GokeFsEncodingOption = BufferEncoding | { encoding?: BufferEncoding | null }
9
+
10
+ interface GokeFs {
11
+ appendFile(path: PathLike, data: GokeFsFileContent, options?: GokeFsEncodingOption): Promise<void>
12
+ chmod(path: PathLike, mode: Mode): Promise<void>
13
+ copyFile(src: PathLike, dest: PathLike): Promise<void>
14
+ link(existingPath: PathLike, newPath: PathLike): Promise<void>
15
+ mkdir(path: PathLike, options?: MakeDirectoryOptions): Promise<string | undefined>
16
+ readFile(path: PathLike, options?: GokeFsEncodingOption): Promise<string | Uint8Array>
17
+ readlink(path: PathLike): Promise<string>
18
+ realpath(path: PathLike): Promise<string>
19
+ rename(oldPath: PathLike, newPath: PathLike): Promise<void>
20
+ rm(path: PathLike, options?: RmOptions): Promise<void>
21
+ symlink(target: PathLike, path: PathLike): Promise<void>
22
+ utimes(path: PathLike, atime: TimeLike, mtime: TimeLike): Promise<void>
23
+ writeFile(path: PathLike, data: GokeFsFileContent, options?: GokeFsEncodingOption): Promise<void>
24
+ }
25
+
26
+ export type { GokeFs, GokeFsEncodingOption, GokeFsFileContent }