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/README.md +188 -53
- package/dist/__test__/index.test.js +80 -0
- package/dist/__test__/just-bash.test.d.ts +5 -0
- package/dist/__test__/just-bash.test.d.ts.map +1 -0
- package/dist/__test__/just-bash.test.js +225 -0
- package/dist/__test__/types.test-d.js +16 -4
- package/dist/goke-fs.d.ts +25 -0
- package/dist/goke-fs.d.ts.map +1 -0
- package/dist/goke-fs.js +4 -0
- package/dist/goke.d.ts +52 -13
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +130 -41
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/just-bash.d.ts +25 -0
- package/dist/just-bash.d.ts.map +1 -0
- package/dist/just-bash.js +227 -0
- package/dist/runtime-browser.d.ts +35 -0
- package/dist/runtime-browser.d.ts.map +1 -0
- package/dist/runtime-browser.js +74 -0
- package/dist/runtime-node.d.ts +10 -0
- package/dist/runtime-node.d.ts.map +1 -0
- package/dist/runtime-node.js +29 -0
- package/package.json +10 -1
- package/src/__test__/index.test.ts +93 -0
- package/src/__test__/just-bash.test.ts +292 -0
- package/src/__test__/types.test-d.ts +16 -4
- package/src/goke-fs.ts +26 -0
- package/src/goke.ts +173 -45
- package/src/index.ts +2 -2
- package/src/just-bash.ts +275 -0
- package/src/runtime-browser.ts +93 -0
- package/src/runtime-node.ts +32 -0
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
|
-
&&
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1583
|
+
if (asyncChain) {
|
|
1584
|
+
return asyncChain
|
|
1585
|
+
.then(executeAction)
|
|
1586
|
+
.catch(catchAsyncError)
|
|
1587
|
+
}
|
|
1450
1588
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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"
|
package/src/just-bash.ts
ADDED
|
@@ -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 }
|