goke 6.3.2 → 6.5.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/src/goke.ts CHANGED
@@ -10,11 +10,13 @@
10
10
  * - Utility functions: string helpers, bracket parsing, dot-prop access
11
11
  */
12
12
 
13
- import { EventEmitter } from 'events'
14
13
  import pc from 'picocolors'
15
14
  import mri from "./mri.js"
16
15
  import { GokeError, coerceBySchema, extractJsonSchema, extractSchemaMetadata, isStandardSchema } from "./coerce.js"
17
16
  import type { StandardJSONSchemaV1 } from "./coerce.js"
17
+ import { createJustBashCommand as createJustBashCommandBridge } from './just-bash.js'
18
+ import type { GokeFs } from './goke-fs.js'
19
+ import { EventEmitter, fs as runtimeFs, openInBrowser, process } from '#runtime'
18
20
 
19
21
  // ─── Node.js platform constants ───
20
22
 
@@ -209,7 +211,8 @@ const getFileName = (input: string) => {
209
211
  const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
210
212
  value != null
211
213
  && (typeof value === 'object' || typeof value === 'function')
212
- && typeof (value as any).then === 'function'
214
+ && 'then' in value
215
+ && typeof value.then === 'function'
213
216
 
214
217
  const camelcaseOptionName = (name: string) => {
215
218
  // Camelcase the option name
@@ -290,6 +293,10 @@ class Option {
290
293
  this.isBoolean = true
291
294
  }
292
295
  }
296
+
297
+ clone() {
298
+ return new Option(this.rawName, this.schema ?? this.description)
299
+ }
293
300
  }
294
301
 
295
302
  // ─── Command ───
@@ -766,6 +773,24 @@ class GlobalCommand extends Command {
766
773
  }
767
774
  }
768
775
 
776
+ const cloneCommandInto = (source: Command, cli: Goke<any>) => {
777
+ const target = source instanceof GlobalCommand
778
+ ? new GlobalCommand(cli)
779
+ : new Command(source.rawName, source.description, { ...source.config }, cli)
780
+
781
+ target.aliasNames = [...source.aliasNames]
782
+ target.usageText = source.usageText
783
+ target.versionNumber = source.versionNumber
784
+ target.examples = [...source.examples]
785
+ target.helpCallback = source.helpCallback
786
+ target.commandAction = source.commandAction
787
+ target._hidden = source._hidden
788
+ target.options = source.options.map((option) => option.clone())
789
+ target.globalCommand = cli.globalCommand
790
+
791
+ return target
792
+ }
793
+
769
794
  // ─── I/O interfaces ───
770
795
 
771
796
  /**
@@ -784,12 +809,48 @@ interface GokeOutputStream {
784
809
  interface GokeConsole {
785
810
  log(...args: unknown[]): void
786
811
  error(...args: unknown[]): void
812
+ warn(...args: unknown[]): void
813
+ info(...args: unknown[]): void
814
+ }
815
+
816
+ interface GokeProcess {
817
+ argv: string[]
818
+ cwd: string
819
+ env: Record<string, string | undefined>
820
+ stdin: string
821
+ stdout: GokeOutputStream
822
+ stderr: GokeOutputStream
823
+ exit(code: number): never | void
824
+ }
825
+
826
+ interface GokeExecutionContext {
827
+ console: GokeConsole
828
+ fs: GokeFs
829
+ process: GokeProcess
830
+ }
831
+
832
+ class GokeProcessExit extends Error {
833
+ code: number
834
+
835
+ constructor(code: number) {
836
+ super(`process.exit(${code})`)
837
+ this.name = 'GokeProcessExit'
838
+ this.code = code
839
+ }
787
840
  }
788
841
 
789
842
  /**
790
843
  * Options for configuring a Goke CLI instance.
791
844
  */
792
845
  interface GokeOptions {
846
+ /** Custom cwd value exposed through the injected process context. */
847
+ cwd?: string
848
+ /** Custom environment exposed through the injected process context. */
849
+ env?: Record<string, string | undefined>
850
+ /** Custom fs implementation. Defaults to node:fs/promises in Node runtimes. */
851
+ fs?: GokeFs
852
+ /** Custom stdin content exposed through the injected process context. */
853
+ stdin?: string
793
854
  /** Custom stdout stream. Defaults to process.stdout */
794
855
  stdout?: GokeOutputStream
795
856
  /** Custom stderr stream. Defaults to process.stderr */
@@ -820,6 +881,12 @@ function createConsole(stdout: GokeOutputStream, stderr: GokeOutputStream): Goke
820
881
  error(...args: unknown[]) {
821
882
  stderr.write(args.map(String).join(' ') + '\n')
822
883
  },
884
+ warn(...args: unknown[]) {
885
+ stderr.write(args.map(String).join(' ') + '\n')
886
+ },
887
+ info(...args: unknown[]) {
888
+ stdout.write(args.map(String).join(' ') + '\n')
889
+ },
823
890
  }
824
891
  }
825
892
 
@@ -857,7 +924,7 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
857
924
  name: string
858
925
  commands: Command[]
859
926
  /** Middleware functions that run before the matched command action, in registration order */
860
- middlewares: Array<{ action: (options: any) => void | Promise<void> }>
927
+ middlewares: Array<{ action: (options: any, context: GokeExecutionContext) => void | Promise<void> }>
861
928
  globalCommand: GlobalCommand
862
929
  matchedCommand?: Command
863
930
  matchedCommandName?: string
@@ -877,6 +944,14 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
877
944
  showHelpOnExit?: boolean
878
945
  showVersionOnExit?: boolean
879
946
 
947
+ /** Working directory exposed through the injected process context. */
948
+ readonly cwd?: string
949
+ /** Environment exposed through the injected process context. */
950
+ readonly env?: Record<string, string | undefined>
951
+ /** Output stream for normal output (help, version, etc.) */
952
+ readonly fs: GokeFs
953
+ /** Standard input exposed through the injected process context. */
954
+ readonly stdin?: string
880
955
  /** Output stream for normal output (help, version, etc.) */
881
956
  readonly stdout: GokeOutputStream
882
957
  /** Output stream for error output */
@@ -902,6 +977,10 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
902
977
  this.rawArgs = []
903
978
  this.args = []
904
979
  this.options = {}
980
+ this.cwd = options?.cwd
981
+ this.env = options?.env
982
+ this.fs = options?.fs ?? runtimeFs
983
+ this.stdin = options?.stdin
905
984
  this.stdout = options?.stdout ?? process.stdout
906
985
  this.stderr = options?.stderr ?? process.stderr
907
986
  this.console = createConsole(this.stdout, this.stderr)
@@ -912,6 +991,60 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
912
991
  this.globalCommand.usage('<command> [options]')
913
992
  }
914
993
 
994
+ clone(options?: GokeOptions) {
995
+ const cloned = new Goke<Opts>(this.name, {
996
+ cwd: options?.cwd ?? this.cwd,
997
+ env: options?.env ?? this.env,
998
+ fs: options?.fs ?? this.fs,
999
+ stdin: options?.stdin ?? this.stdin,
1000
+ stdout: options?.stdout ?? this.stdout,
1001
+ stderr: options?.stderr ?? this.stderr,
1002
+ argv: options?.argv ?? this.#defaultArgv,
1003
+ columns: options?.columns ?? this.columns,
1004
+ exit: options?.exit ?? this.exit,
1005
+ })
1006
+
1007
+ cloned.showHelpOnExit = this.showHelpOnExit
1008
+ cloned.showVersionOnExit = this.showVersionOnExit
1009
+ cloned.globalCommand = cloneCommandInto(this.globalCommand, cloned) as GlobalCommand
1010
+ cloned.commands = this.commands.map((command) => cloneCommandInto(command, cloned))
1011
+ for (const command of cloned.commands) {
1012
+ command.globalCommand = cloned.globalCommand
1013
+ }
1014
+ cloned.middlewares = this.middlewares.map((middleware) => ({ action: middleware.action }))
1015
+
1016
+ for (const eventName of this.eventNames()) {
1017
+ for (const listener of this.listeners(eventName)) {
1018
+ cloned.on(eventName, listener)
1019
+ }
1020
+ }
1021
+
1022
+ return cloned
1023
+ }
1024
+
1025
+ private createExecutionContext(argv = this.rawArgs): GokeExecutionContext {
1026
+ return {
1027
+ console: this.console,
1028
+ fs: this.fs,
1029
+ process: {
1030
+ argv,
1031
+ cwd: this.cwd ?? process.cwd(),
1032
+ env: this.env ?? process.env,
1033
+ stdin: this.stdin ?? '',
1034
+ stdout: this.stdout,
1035
+ stderr: this.stderr,
1036
+ exit: (code: number) => {
1037
+ this.exit(code)
1038
+ throw new GokeProcessExit(code)
1039
+ },
1040
+ },
1041
+ }
1042
+ }
1043
+
1044
+ async createJustBashCommand(options?: { name?: string }) {
1045
+ return createJustBashCommandBridge(this, options)
1046
+ }
1047
+
915
1048
  /**
916
1049
  * Add a global usage text.
917
1050
  *
@@ -946,7 +1079,8 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
946
1079
  >(rawName: RawName, schema: S): Goke<Opts & OptionEntry<RawName, S>>
947
1080
  option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): this
948
1081
  option(rawName: string, descriptionOrSchema?: string | StandardJSONSchemaV1): any {
949
- this.globalCommand.option(rawName, descriptionOrSchema as any)
1082
+ const option = new Option(rawName, descriptionOrSchema)
1083
+ this.globalCommand.options.push(option)
950
1084
  return this
951
1085
  }
952
1086
 
@@ -958,20 +1092,21 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
958
1092
  * options (e.g. setting up logging, initializing state).
959
1093
  *
960
1094
  * The callback receives the parsed options object, typed according to all
961
- * `.option()` calls that precede this `.use()` in the chain.
1095
+ * `.option()` calls that precede this `.use()` in the chain, plus an injected
1096
+ * execution context with `{ console, process }` for portable output and exits.
962
1097
  *
963
1098
  * @example
964
1099
  * ```ts
965
1100
  * cli
966
1101
  * .option('--verbose', z.boolean().default(false).describe('Verbose'))
967
- * .use((options) => {
1102
+ * .use((options, { console }) => {
968
1103
  * if (options.verbose) {
969
- * process.env.LOG_LEVEL = 'debug'
1104
+ * console.log('verbose mode enabled')
970
1105
  * }
971
1106
  * })
972
1107
  * ```
973
1108
  */
974
- use(callback: (options: Opts) => void | Promise<void>): this {
1109
+ use(callback: (options: Opts, context: GokeExecutionContext) => void | Promise<void>): this {
975
1110
  this.middlewares.push({ action: callback })
976
1111
  return this
977
1112
  }
@@ -1376,6 +1511,7 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1376
1511
 
1377
1512
  runMatchedCommand() {
1378
1513
  const { args, options, matchedCommand: command } = this
1514
+ const executionContext = this.createExecutionContext()
1379
1515
 
1380
1516
  if (!command || !command.commandAction) return
1381
1517
 
@@ -1400,6 +1536,7 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1400
1536
  }
1401
1537
  })
1402
1538
  actionArgs.push(options)
1539
+ actionArgs.push(executionContext)
1403
1540
 
1404
1541
  const executeAction = () => command.commandAction!.apply(this, actionArgs)
1405
1542
 
@@ -1419,62 +1556,53 @@ class Goke<Opts extends Record<string, any> = {}> extends EventEmitter {
1419
1556
 
1420
1557
  for (const mw of this.middlewares) {
1421
1558
  if (asyncChain) {
1422
- asyncChain = asyncChain.then(() => mw.action(options))
1559
+ asyncChain = asyncChain.then(() => mw.action(options, executionContext))
1423
1560
  } else {
1424
1561
  try {
1425
- const mwResult = mw.action(options)
1562
+ const mwResult = mw.action(options, executionContext)
1426
1563
  if (isPromiseLike(mwResult)) {
1427
1564
  asyncChain = mwResult as Promise<any>
1428
1565
  }
1429
1566
  } catch (err) {
1567
+ if (err instanceof GokeProcessExit) {
1568
+ throw err
1569
+ }
1430
1570
  handleAsyncError(err)
1431
1571
  return
1432
1572
  }
1433
1573
  }
1434
1574
  }
1435
1575
 
1436
- const result = asyncChain
1437
- ? asyncChain.then(executeAction)
1438
- : executeAction()
1439
-
1440
- // If the result is a promise, catch async errors
1441
- if (isPromiseLike(result)) {
1442
- (result as Promise<any>).catch(handleAsyncError)
1576
+ const catchAsyncError = (err: unknown) => {
1577
+ if (err instanceof GokeProcessExit) {
1578
+ throw err
1579
+ }
1580
+ handleAsyncError(err)
1443
1581
  }
1444
1582
 
1445
- return result
1446
- }
1447
- }
1448
-
1449
- // ─── openInBrowser ───
1583
+ if (asyncChain) {
1584
+ return asyncChain
1585
+ .then(executeAction)
1586
+ .catch(catchAsyncError)
1587
+ }
1450
1588
 
1451
- /**
1452
- * Open a URL in the default browser.
1453
- * In non-TTY environments (CI, piped output, agents), prints the URL to stdout instead.
1454
- */
1455
- function openInBrowser(url: string): void {
1456
- if (!process.stdout.isTTY) {
1457
- console.error(url)
1458
- return
1459
- }
1460
- const { execSync } = require('child_process') as typeof import('child_process')
1461
- const platform = process.platform
1462
- try {
1463
- if (platform === 'darwin') {
1464
- execSync(`open ${JSON.stringify(url)}`, { stdio: 'ignore' })
1465
- } else if (platform === 'win32') {
1466
- execSync(`start "" ${JSON.stringify(url)}`, { stdio: 'ignore' })
1467
- } else {
1468
- execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: 'ignore' })
1589
+ try {
1590
+ const result = executeAction()
1591
+ return isPromiseLike(result)
1592
+ ? (result as Promise<any>).catch(catchAsyncError)
1593
+ : result
1594
+ } catch (err) {
1595
+ if (err instanceof GokeProcessExit) {
1596
+ throw err
1597
+ }
1598
+ handleAsyncError(err)
1599
+ return
1469
1600
  }
1470
- } catch {
1471
- // fallback: print the URL if open fails
1472
- console.error(url)
1473
1601
  }
1474
1602
  }
1475
1603
 
1476
1604
  // ─── Exports ───
1477
1605
 
1478
- export type { GokeOutputStream, GokeConsole, GokeOptions }
1479
- export { createConsole, Command, openInBrowser }
1606
+ export type { GokeOutputStream, GokeConsole, GokeOptions, GokeProcess, GokeExecutionContext, GokeFs }
1607
+ export { createConsole, Command, GokeProcessExit, openInBrowser }
1480
1608
  export default Goke
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ const goke = (name = '', options?: GokeOptions) => new Goke(name, options)
10
10
 
11
11
  export default goke
12
12
  export { goke, Goke, Command }
13
- export { createConsole, openInBrowser } from "./goke.js"
14
- export type { GokeOutputStream, GokeConsole, GokeOptions } from "./goke.js"
13
+ export { createConsole, GokeProcessExit, openInBrowser } from "./goke.js"
14
+ export type { GokeOutputStream, GokeConsole, GokeExecutionContext, GokeFs, GokeOptions, GokeProcess } from "./goke.js"
15
15
  export type { StandardTypedV1, StandardJSONSchemaV1, JsonSchema } from "./coerce.js"
16
16
  export { GokeError, coerceBySchema, extractJsonSchema, wrapJsonSchema, isStandardSchema, extractSchemaMetadata } from "./coerce.js"
@@ -0,0 +1,275 @@
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
+
9
+ import { Buffer } from 'node:buffer'
10
+ import { fileURLToPath } from 'node:url'
11
+ import type { PathLike } from 'node:fs'
12
+ import type { CommandContext, IFileSystem } from 'just-bash'
13
+ import Goke, { GokeProcessExit } from './goke.js'
14
+ import type { GokeOutputStream } from './goke.js'
15
+ import type { GokeFs, GokeFsEncodingOption, GokeFsFileContent } from './goke-fs.js'
16
+
17
+ interface JustBashExecResult {
18
+ stdout: string
19
+ stderr: string
20
+ exitCode: number
21
+ }
22
+
23
+ interface JustBashCommand {
24
+ name: string
25
+ trusted: true
26
+ execute(args: string[], context?: JustBashExecutionContext): Promise<JustBashExecResult>
27
+ }
28
+
29
+ type JustBashExecutionContext = Pick<CommandContext, 'cwd' | 'env' | 'fs' | 'stdin'>
30
+ type JustBashEncoding = 'utf8' | 'utf-8' | 'ascii' | 'binary' | 'base64' | 'hex' | 'latin1'
31
+
32
+ function createTextCaptureStream(): GokeOutputStream & { readonly text: string } {
33
+ const chunks: string[] = []
34
+ return {
35
+ get text() {
36
+ return chunks.join('')
37
+ },
38
+ write(data: string) {
39
+ chunks.push(data)
40
+ },
41
+ }
42
+ }
43
+
44
+ const resolveJustBashPath = (fs: IFileSystem, cwd: string, path: PathLike) => {
45
+ if (path instanceof URL) {
46
+ return fileURLToPath(path)
47
+ }
48
+ return fs.resolvePath(cwd, path.toString())
49
+ }
50
+
51
+ const getEncoding = (options?: GokeFsEncodingOption) => {
52
+ if (typeof options === 'string' || options == null) {
53
+ return options
54
+ }
55
+ return options.encoding
56
+ }
57
+
58
+ const toJustBashEncoding = (encoding?: BufferEncoding | null): JustBashEncoding | null | undefined => {
59
+ if (encoding == null) {
60
+ return encoding
61
+ }
62
+
63
+ switch (encoding) {
64
+ case 'utf8':
65
+ case 'utf-8':
66
+ case 'ascii':
67
+ case 'binary':
68
+ case 'base64':
69
+ case 'hex':
70
+ case 'latin1':
71
+ return encoding
72
+ default:
73
+ throw new Error(`Encoding ${encoding} is not supported by the JustBash fs adapter`)
74
+ }
75
+ }
76
+
77
+ const toJustBashContent = (content: GokeFsFileContent) => {
78
+ if (typeof content === 'string' || content instanceof Uint8Array) {
79
+ return content
80
+ }
81
+ return new Uint8Array(content.buffer, content.byteOffset, content.byteLength)
82
+ }
83
+
84
+ const toDate = (value: Date | string | number) => {
85
+ const date = value instanceof Date ? value : new Date(value)
86
+ if (Number.isNaN(date.getTime())) {
87
+ throw new Error(`Invalid time value: ${String(value)}`)
88
+ }
89
+ return date
90
+ }
91
+
92
+ function createJustBashEnvProxy(env: Map<string, string>): Record<string, string | undefined> {
93
+ return new Proxy(Object.create(null) as Record<string, string | undefined>, {
94
+ deleteProperty(_target, property) {
95
+ if (typeof property === 'string') {
96
+ env.delete(property)
97
+ }
98
+ return true
99
+ },
100
+ get(_target, property) {
101
+ if (typeof property !== 'string') return undefined
102
+ return env.get(property)
103
+ },
104
+ getOwnPropertyDescriptor(_target, property) {
105
+ if (typeof property !== 'string') return undefined
106
+ const value = env.get(property)
107
+ if (value === undefined) return undefined
108
+ return {
109
+ configurable: true,
110
+ enumerable: true,
111
+ value,
112
+ writable: true,
113
+ }
114
+ },
115
+ has(_target, property) {
116
+ return typeof property === 'string' && env.has(property)
117
+ },
118
+ ownKeys() {
119
+ return [...env.keys()]
120
+ },
121
+ set(_target, property, value) {
122
+ if (typeof property === 'string') {
123
+ if (value === undefined) {
124
+ env.delete(property)
125
+ } else {
126
+ env.set(property, String(value))
127
+ }
128
+ }
129
+ return true
130
+ },
131
+ })
132
+ }
133
+
134
+ function createJustBashFs(fs: IFileSystem, cwd: string): GokeFs {
135
+ const readFile: GokeFs['readFile'] = async (path, options) => {
136
+ const resolvedPath = resolveJustBashPath(fs, cwd, path)
137
+ const encoding = toJustBashEncoding(getEncoding(options))
138
+ if (encoding == null) {
139
+ return Buffer.from(await fs.readFileBuffer(resolvedPath))
140
+ }
141
+ return fs.readFile(resolvedPath, encoding)
142
+ }
143
+
144
+ const writeFile: GokeFs['writeFile'] = async (path, content, options) => {
145
+ const resolvedPath = resolveJustBashPath(fs, cwd, path)
146
+ const encoding = toJustBashEncoding(getEncoding(options)) ?? undefined
147
+ await fs.writeFile(resolvedPath, toJustBashContent(content), encoding)
148
+ }
149
+
150
+ const appendFile: GokeFs['appendFile'] = async (path, content, options) => {
151
+ const resolvedPath = resolveJustBashPath(fs, cwd, path)
152
+ const encoding = toJustBashEncoding(getEncoding(options)) ?? undefined
153
+ await fs.appendFile(resolvedPath, toJustBashContent(content), encoding)
154
+ }
155
+
156
+ const mkdir: GokeFs['mkdir'] = async (path, options) => {
157
+ await fs.mkdir(resolveJustBashPath(fs, cwd, path), { recursive: typeof options === 'object' ? options.recursive : undefined })
158
+ return undefined
159
+ }
160
+
161
+ const rm: GokeFs['rm'] = async (path, options) => {
162
+ await fs.rm(resolveJustBashPath(fs, cwd, path), {
163
+ recursive: options?.recursive,
164
+ force: options?.force,
165
+ })
166
+ }
167
+
168
+ const rename: GokeFs['rename'] = async (oldPath, newPath) => {
169
+ await fs.mv(resolveJustBashPath(fs, cwd, oldPath), resolveJustBashPath(fs, cwd, newPath))
170
+ }
171
+
172
+ const copyFile: GokeFs['copyFile'] = async (src, dest) => {
173
+ await fs.cp(resolveJustBashPath(fs, cwd, src), resolveJustBashPath(fs, cwd, dest))
174
+ }
175
+
176
+ const chmod: GokeFs['chmod'] = async (path, mode) => {
177
+ await fs.chmod(resolveJustBashPath(fs, cwd, path), Number(mode))
178
+ }
179
+
180
+ const link: GokeFs['link'] = async (existingPath, newPath) => {
181
+ await fs.link(resolveJustBashPath(fs, cwd, existingPath), resolveJustBashPath(fs, cwd, newPath))
182
+ }
183
+
184
+ const readlink: GokeFs['readlink'] = async (path) => {
185
+ return fs.readlink(resolveJustBashPath(fs, cwd, path))
186
+ }
187
+
188
+ const realpath: GokeFs['realpath'] = async (path) => {
189
+ return fs.realpath(resolveJustBashPath(fs, cwd, path))
190
+ }
191
+
192
+ const symlink: GokeFs['symlink'] = async (target, path) => {
193
+ await fs.symlink(target.toString(), resolveJustBashPath(fs, cwd, path))
194
+ }
195
+
196
+ const utimes: GokeFs['utimes'] = async (path, atime, mtime) => {
197
+ await fs.utimes(resolveJustBashPath(fs, cwd, path), toDate(atime), toDate(mtime))
198
+ }
199
+
200
+ return {
201
+ appendFile,
202
+ chmod,
203
+ copyFile,
204
+ link,
205
+ mkdir,
206
+ readFile,
207
+ readlink,
208
+ realpath,
209
+ rename,
210
+ rm,
211
+ symlink,
212
+ utimes,
213
+ writeFile,
214
+ }
215
+ }
216
+
217
+ export function createJustBashCommand(
218
+ cli: Goke<any>,
219
+ options?: { name?: string }
220
+ ): JustBashCommand {
221
+ const name = options?.name ?? cli.name
222
+
223
+ if (!name) {
224
+ throw new Error('createJustBashCommand() requires the CLI to have a name')
225
+ }
226
+
227
+ if (name.split(/\s+/).length > 1) {
228
+ throw new Error('JustBash custom command names must be a single token')
229
+ }
230
+
231
+ return {
232
+ name,
233
+ trusted: true,
234
+ async execute(args: string[], context?: JustBashExecutionContext) {
235
+ const stdout = createTextCaptureStream()
236
+ const stderr = createTextCaptureStream()
237
+ const argv = ['node', name, ...args]
238
+ const cloned = cli.clone({
239
+ cwd: context?.cwd,
240
+ env: context ? createJustBashEnvProxy(context.env) : cli.env,
241
+ fs: context ? createJustBashFs(context.fs, context.cwd) : cli.fs,
242
+ stdin: context?.stdin,
243
+ stdout,
244
+ stderr,
245
+ argv,
246
+ exit: (code) => {
247
+ throw new GokeProcessExit(code)
248
+ },
249
+ })
250
+
251
+ cloned.name = name
252
+
253
+ try {
254
+ cloned.parse(argv, { run: false })
255
+ await cloned.runMatchedCommand()
256
+ return {
257
+ stdout: stdout.text,
258
+ stderr: stderr.text,
259
+ exitCode: 0,
260
+ }
261
+ } catch (error) {
262
+ if (error instanceof GokeProcessExit) {
263
+ return {
264
+ stdout: stdout.text,
265
+ stderr: stderr.text,
266
+ exitCode: error.code,
267
+ }
268
+ }
269
+ throw error
270
+ }
271
+ },
272
+ }
273
+ }
274
+
275
+ export type { JustBashCommand, JustBashExecResult }