goke 6.8.0 → 6.9.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/__test__/index.test.js +124 -0
- package/dist/__test__/just-bash.test.js +19 -0
- package/dist/__test__/types.test-d.js +37 -0
- package/dist/goke.d.ts +23 -0
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +8 -24
- package/dist/just-bash.d.ts +1 -1
- package/dist/just-bash.d.ts.map +1 -1
- package/dist/just-bash.js +80 -15
- package/package.json +1 -1
- package/src/__test__/index.test.ts +166 -0
- package/src/__test__/just-bash.test.ts +24 -0
- package/src/__test__/types.test-d.ts +41 -0
- package/src/goke.ts +35 -1
- package/src/just-bash.ts +92 -18
- package/README.md +0 -1254
|
@@ -389,4 +389,28 @@ describe('createJustBashCommand', () => {
|
|
|
389
389
|
exitCode: 7,
|
|
390
390
|
})
|
|
391
391
|
})
|
|
392
|
+
|
|
393
|
+
test('truncates captured output to just-bash maxOutputSize and appends a notice', async () => {
|
|
394
|
+
const { Bash } = await import('just-bash')
|
|
395
|
+
const cli = gokeTestable('parent')
|
|
396
|
+
|
|
397
|
+
cli
|
|
398
|
+
.command('spam-stderr', 'Write too much stderr')
|
|
399
|
+
.action((options, { console }) => {
|
|
400
|
+
console.error('x'.repeat(60))
|
|
401
|
+
console.error('y'.repeat(60))
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const bash = new Bash({
|
|
405
|
+
executionLimits: { maxOutputSize: 80 },
|
|
406
|
+
customCommands: [await cli.createJustBashCommand()],
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const result = await bash.exec('parent spam-stderr')
|
|
410
|
+
|
|
411
|
+
expect(result.exitCode).toBe(0)
|
|
412
|
+
expect(result.stdout).toBe('')
|
|
413
|
+
expect(result.stderr.length).toBeLessThanOrEqual(80)
|
|
414
|
+
expect(result.stderr).toBe(`${'x'.repeat(60)}\n[output truncated]\n`)
|
|
415
|
+
})
|
|
392
416
|
})
|
|
@@ -481,6 +481,47 @@ describe('type-level: README TypeScript examples', () => {
|
|
|
481
481
|
})
|
|
482
482
|
})
|
|
483
483
|
|
|
484
|
+
test('use() with sub-CLI preserves parent middleware typing', () => {
|
|
485
|
+
const sub = goke()
|
|
486
|
+
sub
|
|
487
|
+
.command('deploy', 'Deploy the app')
|
|
488
|
+
.option('--force', z.boolean())
|
|
489
|
+
.action((options) => {
|
|
490
|
+
// Sub-CLI command's action sees its own options
|
|
491
|
+
expectTypeOf(options.force).toEqualTypeOf<boolean | undefined>()
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
goke('test')
|
|
495
|
+
.option('--verbose', z.boolean().default(false).describe('Verbose'))
|
|
496
|
+
.use(sub)
|
|
497
|
+
.use((options) => {
|
|
498
|
+
// Parent middleware still sees parent's accumulated options after .use(subCli)
|
|
499
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean>()
|
|
500
|
+
})
|
|
501
|
+
.command('build', 'Build')
|
|
502
|
+
.option('--target <target>', z.string())
|
|
503
|
+
.action((options) => {
|
|
504
|
+
// Parent's inline command still sees global options
|
|
505
|
+
expectTypeOf(options.verbose).toEqualTypeOf<boolean>()
|
|
506
|
+
expectTypeOf(options.target).toEqualTypeOf<string>()
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('use() with sub-CLI does not leak sub-CLI types to parent', () => {
|
|
511
|
+
const sub = goke()
|
|
512
|
+
.option('--sub-only <val>', z.string())
|
|
513
|
+
sub.command('sub-cmd', 'Sub command').action(() => {})
|
|
514
|
+
|
|
515
|
+
goke('test')
|
|
516
|
+
.option('--parent-only <val>', z.number())
|
|
517
|
+
.use(sub)
|
|
518
|
+
.use((options) => {
|
|
519
|
+
expectTypeOf(options.parentOnly).toEqualTypeOf<number>()
|
|
520
|
+
// @ts-expect-error subOnly is not declared on the parent
|
|
521
|
+
options.subOnly
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
484
525
|
test('README global options and middleware example stays typed end-to-end', () => {
|
|
485
526
|
// `z.boolean().default(false)` and `z.string().default(...)` are
|
|
486
527
|
// effectively required at runtime: the default applies when the flag is
|
package/src/goke.ts
CHANGED
|
@@ -1381,14 +1381,48 @@ class Goke<Opts = {}> extends EventEmitter {
|
|
|
1381
1381
|
* }
|
|
1382
1382
|
* })
|
|
1383
1383
|
* ```
|
|
1384
|
+
*
|
|
1385
|
+
* Alternatively, pass another `Goke` instance to compose commands from
|
|
1386
|
+
* separate files. All commands defined on the sub-CLI are merged into
|
|
1387
|
+
* this CLI. Middlewares and global options from the sub-CLI are NOT
|
|
1388
|
+
* copied; only commands are composed.
|
|
1389
|
+
*
|
|
1390
|
+
* @example
|
|
1391
|
+
* ```ts
|
|
1392
|
+
* // selfhost.ts
|
|
1393
|
+
* export const selfhostCli = goke()
|
|
1394
|
+
* selfhostCli
|
|
1395
|
+
* .command('selfhost', 'Set up on your own workspace')
|
|
1396
|
+
* .option('-t, --token [token]', 'Admin token')
|
|
1397
|
+
* .action((options) => { ... })
|
|
1398
|
+
*
|
|
1399
|
+
* // main.ts
|
|
1400
|
+
* import { selfhostCli } from './selfhost.js'
|
|
1401
|
+
* goke('mycli')
|
|
1402
|
+
* .use(selfhostCli)
|
|
1403
|
+
* .help()
|
|
1404
|
+
* .parse(process.argv)
|
|
1405
|
+
* ```
|
|
1384
1406
|
*/
|
|
1407
|
+
use(subCli: Goke<any>): this
|
|
1385
1408
|
use(
|
|
1386
1409
|
callback: (
|
|
1387
1410
|
options: Opts & DoubleDashOptions,
|
|
1388
1411
|
context: GokeExecutionContext,
|
|
1389
1412
|
) => void | Promise<void>,
|
|
1413
|
+
): this
|
|
1414
|
+
use(
|
|
1415
|
+
callbackOrCli:
|
|
1416
|
+
| Goke<any>
|
|
1417
|
+
| ((options: Opts & DoubleDashOptions, context: GokeExecutionContext) => void | Promise<void>),
|
|
1390
1418
|
): this {
|
|
1391
|
-
|
|
1419
|
+
if (callbackOrCli instanceof Goke) {
|
|
1420
|
+
for (const command of callbackOrCli.commands) {
|
|
1421
|
+
this.commands.push(cloneCommandInto(command, this))
|
|
1422
|
+
}
|
|
1423
|
+
return this
|
|
1424
|
+
}
|
|
1425
|
+
this.middlewares.push({ action: callbackOrCli })
|
|
1392
1426
|
return this
|
|
1393
1427
|
}
|
|
1394
1428
|
|
package/src/just-bash.ts
CHANGED
|
@@ -26,17 +26,90 @@ interface JustBashCommand {
|
|
|
26
26
|
execute(args: string[], context?: JustBashExecutionContext): Promise<JustBashExecResult>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
type JustBashExecutionContext = Pick<CommandContext, 'cwd' | 'env' | 'fs' | 'stdin'>
|
|
30
|
-
|
|
29
|
+
type JustBashExecutionContext = Pick<CommandContext, 'cwd' | 'env' | 'fs' | 'stdin' | 'limits'>
|
|
30
|
+
|
|
31
|
+
const TRUNCATION_MESSAGE = '\n[output truncated]\n'
|
|
32
|
+
|
|
33
|
+
function createTextCaptureStreams(maxLength?: number): {
|
|
34
|
+
stdout: GokeOutputStream
|
|
35
|
+
stderr: GokeOutputStream
|
|
36
|
+
getResult(): { stdout: string; stderr: string }
|
|
37
|
+
} {
|
|
38
|
+
const stdoutChunks: string[] = []
|
|
39
|
+
const stderrChunks: string[] = []
|
|
40
|
+
const limit = maxLength != null && maxLength > 0 ? maxLength : Number.POSITIVE_INFINITY
|
|
41
|
+
let totalLength = 0
|
|
42
|
+
let stdoutTruncated = false
|
|
43
|
+
let stderrTruncated = false
|
|
44
|
+
|
|
45
|
+
const createStream = (stream: 'stdout' | 'stderr'): GokeOutputStream => ({
|
|
46
|
+
write(data: string) {
|
|
47
|
+
if (totalLength >= limit) {
|
|
48
|
+
if (stream === 'stdout') {
|
|
49
|
+
stdoutTruncated = true
|
|
50
|
+
} else {
|
|
51
|
+
stderrTruncated = true
|
|
52
|
+
}
|
|
53
|
+
return
|
|
54
|
+
}
|
|
31
55
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
const remaining = limit - totalLength
|
|
57
|
+
const text = data.length <= remaining ? data : data.slice(0, remaining)
|
|
58
|
+
if (stream === 'stdout') {
|
|
59
|
+
stdoutChunks.push(text)
|
|
60
|
+
stdoutTruncated ||= text.length !== data.length
|
|
61
|
+
} else {
|
|
62
|
+
stderrChunks.push(text)
|
|
63
|
+
stderrTruncated ||= text.length !== data.length
|
|
64
|
+
}
|
|
65
|
+
totalLength += text.length
|
|
37
66
|
},
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const trimEnd = (value: string, count: number) => value.slice(0, Math.max(0, value.length - count))
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
stdout: createStream('stdout'),
|
|
73
|
+
stderr: createStream('stderr'),
|
|
74
|
+
getResult() {
|
|
75
|
+
let stdout = stdoutChunks.join('')
|
|
76
|
+
let stderr = stderrChunks.join('')
|
|
77
|
+
|
|
78
|
+
if (!stdoutTruncated && !stderrTruncated) {
|
|
79
|
+
return { stdout, stderr }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const target = stderrTruncated ? 'stderr' : 'stdout'
|
|
83
|
+
const message = limit === Number.POSITIVE_INFINITY
|
|
84
|
+
? TRUNCATION_MESSAGE
|
|
85
|
+
: TRUNCATION_MESSAGE.slice(0, Math.min(TRUNCATION_MESSAGE.length, limit))
|
|
86
|
+
|
|
87
|
+
let overflow = stdout.length + stderr.length + message.length - limit
|
|
88
|
+
if (Number.isFinite(limit) && overflow > 0) {
|
|
89
|
+
if (target === 'stderr') {
|
|
90
|
+
const stderrTrim = Math.min(overflow, stderr.length)
|
|
91
|
+
stderr = trimEnd(stderr, stderrTrim)
|
|
92
|
+
overflow -= stderrTrim
|
|
93
|
+
if (overflow > 0) {
|
|
94
|
+
stdout = trimEnd(stdout, overflow)
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const stdoutTrim = Math.min(overflow, stdout.length)
|
|
98
|
+
stdout = trimEnd(stdout, stdoutTrim)
|
|
99
|
+
overflow -= stdoutTrim
|
|
100
|
+
if (overflow > 0) {
|
|
101
|
+
stderr = trimEnd(stderr, overflow)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (target === 'stderr') {
|
|
107
|
+
stderr += message
|
|
108
|
+
} else {
|
|
109
|
+
stdout += message
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { stdout, stderr }
|
|
40
113
|
},
|
|
41
114
|
}
|
|
42
115
|
}
|
|
@@ -55,7 +128,7 @@ const getEncoding = (options?: GokeFsEncodingOption) => {
|
|
|
55
128
|
return options.encoding
|
|
56
129
|
}
|
|
57
130
|
|
|
58
|
-
const toJustBashEncoding = (encoding?: BufferEncoding | null):
|
|
131
|
+
const toJustBashEncoding = (encoding?: BufferEncoding | null): 'utf8' | 'utf-8' | 'ascii' | 'binary' | 'base64' | 'hex' | 'latin1' | null | undefined => {
|
|
59
132
|
if (encoding == null) {
|
|
60
133
|
return encoding
|
|
61
134
|
}
|
|
@@ -232,16 +305,15 @@ export function createJustBashCommand(
|
|
|
232
305
|
name,
|
|
233
306
|
trusted: true,
|
|
234
307
|
async execute(args: string[], context?: JustBashExecutionContext) {
|
|
235
|
-
const
|
|
236
|
-
const stderr = createTextCaptureStream()
|
|
308
|
+
const output = createTextCaptureStreams(context?.limits?.maxOutputSize)
|
|
237
309
|
const argv = ['node', name, ...args]
|
|
238
310
|
const cloned = cli.clone({
|
|
239
311
|
cwd: context?.cwd,
|
|
240
312
|
env: context ? createJustBashEnvProxy(context.env) : cli.env,
|
|
241
313
|
fs: context ? createJustBashFs(context.fs, context.cwd) : cli.fs,
|
|
242
314
|
stdin: context?.stdin,
|
|
243
|
-
stdout,
|
|
244
|
-
stderr,
|
|
315
|
+
stdout: output.stdout,
|
|
316
|
+
stderr: output.stderr,
|
|
245
317
|
argv,
|
|
246
318
|
exit: (code) => {
|
|
247
319
|
throw new GokeProcessExit(code)
|
|
@@ -253,16 +325,18 @@ export function createJustBashCommand(
|
|
|
253
325
|
try {
|
|
254
326
|
cloned.parse(argv, { run: false })
|
|
255
327
|
await cloned.runMatchedCommand()
|
|
328
|
+
const result = output.getResult()
|
|
256
329
|
return {
|
|
257
|
-
stdout: stdout
|
|
258
|
-
stderr: stderr
|
|
330
|
+
stdout: result.stdout,
|
|
331
|
+
stderr: result.stderr,
|
|
259
332
|
exitCode: 0,
|
|
260
333
|
}
|
|
261
334
|
} catch (error) {
|
|
262
335
|
if (error instanceof GokeProcessExit) {
|
|
336
|
+
const result = output.getResult()
|
|
263
337
|
return {
|
|
264
|
-
stdout: stdout
|
|
265
|
-
stderr: stderr
|
|
338
|
+
stdout: result.stdout,
|
|
339
|
+
stderr: result.stderr,
|
|
266
340
|
exitCode: error.code,
|
|
267
341
|
}
|
|
268
342
|
}
|