@gutenye/script.js 1.0.1 → 2.0.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 +58 -44
- package/package.json +12 -15
- package/src/Argument.ts +40 -0
- package/src/Command.ts +288 -0
- package/src/Option.ts +70 -0
- package/src/ake/README.md +44 -0
- package/src/ake/ake.ts +40 -0
- package/src/ake/akectl.ts +60 -0
- package/src/ake/completions/ake.fish +8 -0
- package/src/ake/shared.ts +45 -0
- package/src/completion.ts +152 -0
- package/src/globals.d.ts +7 -0
- package/src/index.ts +2 -0
- package/src/parseArgv.ts +88 -0
- package/src/script.ts +13 -51
- package/src/spawn.ts +175 -17
- package/src/test.ts +25 -0
- package/src/utils/fs.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/app.ts +0 -25
- package/src/command.ts +0 -51
- package/src/csv.ts +0 -12
- package/src/exit.ts +0 -10
- package/src/fileSystem.ts +0 -17
- package/src/mixins.ts +0 -19
- package/src/types/global.d.ts +0 -8
- package/src/ui/index.ts +0 -1
- package/src/ui/table.ts +0 -35
- package/src/yaml.ts +0 -34
- /package/src/utils/{path.ts → nodePath.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env script.js
|
|
2
|
+
|
|
3
|
+
import { castArray } from 'lodash-es'
|
|
4
|
+
import fs from '../utils/fs'
|
|
5
|
+
import {
|
|
6
|
+
exitWithError,
|
|
7
|
+
findAkeFiles,
|
|
8
|
+
getRemoteDir,
|
|
9
|
+
STORAGE_DIR,
|
|
10
|
+
TEMPLATE_NAME,
|
|
11
|
+
} from './shared'
|
|
12
|
+
|
|
13
|
+
const NAME = 'akectl'
|
|
14
|
+
const ENV = process.env
|
|
15
|
+
|
|
16
|
+
app.meta(NAME)
|
|
17
|
+
|
|
18
|
+
app
|
|
19
|
+
.cmd('init', 'Create ake file')
|
|
20
|
+
.a('<place>', 'Place', ['local', 'remote'])
|
|
21
|
+
.a(async (place: string) => {
|
|
22
|
+
const akeFiles = await findAkeFiles()
|
|
23
|
+
if (akeFiles.length > 0) {
|
|
24
|
+
exitWithError('Already have an ake file, cannot create a new one')
|
|
25
|
+
}
|
|
26
|
+
let target = 'ake'
|
|
27
|
+
if (place === 'remote') {
|
|
28
|
+
const remoteDir = getRemoteDir()
|
|
29
|
+
await fs.mkdirp(remoteDir)
|
|
30
|
+
target = `${remoteDir}/ake`
|
|
31
|
+
}
|
|
32
|
+
const templateFile = `${STORAGE_DIR}/${TEMPLATE_NAME}`
|
|
33
|
+
if (await fs.pathExists(templateFile)) {
|
|
34
|
+
await fs.copy(templateFile, target)
|
|
35
|
+
} else {
|
|
36
|
+
await fs.writeFile(target, '')
|
|
37
|
+
await fs.chmod(target, 0o755)
|
|
38
|
+
}
|
|
39
|
+
await openEditor(target)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
app.cmd('edit', 'Edit ake file').a(async () => {
|
|
43
|
+
const akeFiles = await findAkeFiles()
|
|
44
|
+
const akeFile = akeFiles[0]
|
|
45
|
+
if (!akeFile) {
|
|
46
|
+
exitWithError('No ake file found')
|
|
47
|
+
}
|
|
48
|
+
await openEditor(akeFile)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
async function openEditor(inputPaths: string | string[]) {
|
|
52
|
+
const paths = castArray(inputPaths)
|
|
53
|
+
const editor =
|
|
54
|
+
ENV.TERM_PROGRAM === 'vscode'
|
|
55
|
+
? ENV.CURSOR_TRACE_ID
|
|
56
|
+
? 'cursor'
|
|
57
|
+
: 'code'
|
|
58
|
+
: ENV.EDITOR
|
|
59
|
+
return $`${editor} ${paths}`
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import nodeFs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import fs from '../utils/fs'
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir()
|
|
6
|
+
const CWD = process.cwd()
|
|
7
|
+
|
|
8
|
+
export const STORAGE_DIR = `${HOME}/bin.src/ake`
|
|
9
|
+
export const TEMPLATE_NAME = 'template'
|
|
10
|
+
|
|
11
|
+
export async function findAkeFiles(): Promise<string[]> {
|
|
12
|
+
const localDir = CWD
|
|
13
|
+
const remoteDir = getRemoteDir()
|
|
14
|
+
const dirsToCheck = [localDir, remoteDir]
|
|
15
|
+
|
|
16
|
+
const akeFiles = await Promise.all(
|
|
17
|
+
dirsToCheck.map(async (dir) => {
|
|
18
|
+
const akeFile = `${dir}/ake`
|
|
19
|
+
return (await fs.pathExists(akeFile)) ? akeFile : null
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return akeFiles.filter(Boolean) as string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getRemoteDir() {
|
|
27
|
+
return `${STORAGE_DIR}/${getUniqueName()}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getCompletionName() {
|
|
31
|
+
return `ake.${getUniqueName()}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getUniqueName() {
|
|
35
|
+
// use sync method to avoid using await in app.enableAkeCompletion()
|
|
36
|
+
return nodeFs.realpathSync(CWD).replaceAll('/', '_')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function exitWithError(message: string, help?: string): never {
|
|
40
|
+
console.error(`Error: ${message}`)
|
|
41
|
+
if (help) {
|
|
42
|
+
console.log(`\n${help}`)
|
|
43
|
+
}
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import nodeFs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as yaml from 'yaml'
|
|
5
|
+
import { getCompletionName } from './ake/shared'
|
|
6
|
+
import type { Command } from './Command'
|
|
7
|
+
|
|
8
|
+
export type CompletionValue = string[] | (() => string[])
|
|
9
|
+
|
|
10
|
+
export type CarapaceSpec = {
|
|
11
|
+
name: string
|
|
12
|
+
aliases?: string[]
|
|
13
|
+
description?: string
|
|
14
|
+
flags?: Record<string, string>
|
|
15
|
+
completion?: CarapaceCompletion
|
|
16
|
+
commands?: CarapaceSpec[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CarapaceCompletion = {
|
|
20
|
+
positional?: string[][]
|
|
21
|
+
positionalany?: string[]
|
|
22
|
+
flag?: Record<string, string[]>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveCompletion(completion: CompletionValue): string[] {
|
|
26
|
+
if (typeof completion === 'function') {
|
|
27
|
+
try {
|
|
28
|
+
return completion()
|
|
29
|
+
} catch {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return completion
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildSpec(command: Command): CarapaceSpec {
|
|
37
|
+
const spec: CarapaceSpec = { name: command.name as string }
|
|
38
|
+
|
|
39
|
+
if (command.description) {
|
|
40
|
+
spec.description = command.description
|
|
41
|
+
}
|
|
42
|
+
if (command.aliases.length > 0) {
|
|
43
|
+
spec.aliases = command.aliases
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const completion: CarapaceCompletion = {}
|
|
47
|
+
|
|
48
|
+
for (const arg of command.arguments) {
|
|
49
|
+
const values = resolveCompletion(arg.completion)
|
|
50
|
+
if (arg.variadic) {
|
|
51
|
+
if (values.length > 0) {
|
|
52
|
+
completion.positionalany = values
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
completion.positional = completion.positional || []
|
|
56
|
+
completion.positional.push(values)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const opt of command.options) {
|
|
61
|
+
spec.flags = spec.flags || {}
|
|
62
|
+
let flag = [opt.short, opt.long].filter(Boolean).join(', ')
|
|
63
|
+
if (opt.required) flag += '='
|
|
64
|
+
else if (opt.optional) flag += '=?'
|
|
65
|
+
spec.flags[flag] = opt.description
|
|
66
|
+
|
|
67
|
+
const values = resolveCompletion(opt.completion)
|
|
68
|
+
if (values.length > 0) {
|
|
69
|
+
completion.flag = completion.flag || {}
|
|
70
|
+
const key = opt.long?.replace(/^--/, '') || opt.attributeName
|
|
71
|
+
completion.flag[key] = values
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Object.keys(completion).length > 0) {
|
|
76
|
+
spec.completion = completion
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const sub of command.commands) {
|
|
80
|
+
spec.commands = spec.commands || []
|
|
81
|
+
spec.commands.push(buildSpec(sub))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return spec
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildSpecText(
|
|
88
|
+
command: Command,
|
|
89
|
+
): { spec: CarapaceSpec; text: string } | undefined {
|
|
90
|
+
if (!command.name) return undefined
|
|
91
|
+
|
|
92
|
+
const spec = buildSpec(command)
|
|
93
|
+
if (!(spec.commands || spec.flags || spec.completion)) return undefined
|
|
94
|
+
|
|
95
|
+
const text = yaml.stringify(spec)
|
|
96
|
+
return { spec, text }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getCarapaceSpecsDir(): string {
|
|
100
|
+
const homeDir = os.homedir()
|
|
101
|
+
switch (os.platform()) {
|
|
102
|
+
case 'darwin':
|
|
103
|
+
return path.join(homeDir, 'Library/Application Support/carapace/specs')
|
|
104
|
+
case 'win32': {
|
|
105
|
+
const localAppData =
|
|
106
|
+
process.env.LOCALAPPDATA || path.join(homeDir, 'AppData/Local')
|
|
107
|
+
return path.join(localAppData, 'carapace/specs')
|
|
108
|
+
}
|
|
109
|
+
default: {
|
|
110
|
+
const configHome =
|
|
111
|
+
process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config')
|
|
112
|
+
return path.join(configHome, 'carapace/specs')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type InstallOptions = {
|
|
118
|
+
scriptPath?: string
|
|
119
|
+
specsDir?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function installCompletion(
|
|
123
|
+
command: Command,
|
|
124
|
+
options: InstallOptions = {},
|
|
125
|
+
) {
|
|
126
|
+
try {
|
|
127
|
+
if (!command.name && options.scriptPath) {
|
|
128
|
+
const basename = path.basename(options.scriptPath)
|
|
129
|
+
if (basename === 'ake') {
|
|
130
|
+
command.name = getCompletionName()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = buildSpecText(command)
|
|
135
|
+
if (!result) return
|
|
136
|
+
|
|
137
|
+
const specsDir = options.specsDir || getCarapaceSpecsDir()
|
|
138
|
+
const filePath = path.join(specsDir, `${result.spec.name}.yaml`)
|
|
139
|
+
|
|
140
|
+
let existing: string | undefined
|
|
141
|
+
try {
|
|
142
|
+
existing = nodeFs.readFileSync(filePath, 'utf8')
|
|
143
|
+
} catch {}
|
|
144
|
+
|
|
145
|
+
if (existing === result.text) return
|
|
146
|
+
|
|
147
|
+
nodeFs.mkdirSync(specsDir, { recursive: true })
|
|
148
|
+
nodeFs.writeFileSync(filePath, result.text)
|
|
149
|
+
} catch {
|
|
150
|
+
// completion is supplementary — silently ignore errors
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/globals.d.ts
ADDED
package/src/index.ts
ADDED
package/src/parseArgv.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Argument } from './Argument'
|
|
2
|
+
import type { Option } from './Option'
|
|
3
|
+
|
|
4
|
+
export function parseArgv(
|
|
5
|
+
argv: string[],
|
|
6
|
+
registeredArgs: Argument[],
|
|
7
|
+
registeredOptions: Option[],
|
|
8
|
+
): { positionals: any[]; options: Record<string, any> } {
|
|
9
|
+
const options: Record<string, any> = {}
|
|
10
|
+
const rawPositionals: string[] = []
|
|
11
|
+
|
|
12
|
+
for (const opt of registeredOptions) {
|
|
13
|
+
if (opt.negate) {
|
|
14
|
+
options[opt.attributeName] = true
|
|
15
|
+
} else if (opt.defaultValue !== undefined) {
|
|
16
|
+
options[opt.attributeName] = opt.defaultValue
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let i = 0
|
|
21
|
+
let optionsParsing = true
|
|
22
|
+
|
|
23
|
+
while (i < argv.length) {
|
|
24
|
+
const token = argv[i]
|
|
25
|
+
|
|
26
|
+
if (token === '--') {
|
|
27
|
+
optionsParsing = false
|
|
28
|
+
i++
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (optionsParsing && token.startsWith('-') && token.length > 1) {
|
|
33
|
+
let flag = token
|
|
34
|
+
let inlineValue: string | undefined
|
|
35
|
+
const eqIndex = token.indexOf('=')
|
|
36
|
+
if (eqIndex !== -1) {
|
|
37
|
+
flag = token.slice(0, eqIndex)
|
|
38
|
+
inlineValue = token.slice(eqIndex + 1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const opt = findOption(flag, registeredOptions)
|
|
42
|
+
if (opt) {
|
|
43
|
+
if (opt.negate) {
|
|
44
|
+
options[opt.attributeName] = false
|
|
45
|
+
} else if (opt.required) {
|
|
46
|
+
const value = inlineValue ?? argv[++i]
|
|
47
|
+
options[opt.attributeName] = value
|
|
48
|
+
} else if (opt.optional) {
|
|
49
|
+
if (inlineValue !== undefined) {
|
|
50
|
+
options[opt.attributeName] = inlineValue
|
|
51
|
+
} else {
|
|
52
|
+
const next = argv[i + 1]
|
|
53
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
54
|
+
options[opt.attributeName] = next
|
|
55
|
+
i++
|
|
56
|
+
} else {
|
|
57
|
+
options[opt.attributeName] = true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
options[opt.attributeName] = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
rawPositionals.push(token)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
i++
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const positionals: any[] = []
|
|
72
|
+
let posIdx = 0
|
|
73
|
+
for (const arg of registeredArgs) {
|
|
74
|
+
if (arg.variadic) {
|
|
75
|
+
positionals.push(rawPositionals.slice(posIdx))
|
|
76
|
+
posIdx = rawPositionals.length
|
|
77
|
+
} else {
|
|
78
|
+
positionals.push(rawPositionals[posIdx] ?? arg.defaultValue)
|
|
79
|
+
posIdx++
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { positionals, options }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findOption(flag: string, options: Option[]): Option | undefined {
|
|
87
|
+
return options.find((o) => o.short === flag || o.long === flag)
|
|
88
|
+
}
|
package/src/script.ts
CHANGED
|
@@ -1,55 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
1
|
+
#!/usr/bin/env bun --env-file ''
|
|
2
2
|
|
|
3
|
-
import '
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { app } from './Command'
|
|
5
|
+
import { $ } from './spawn'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
globalThis.
|
|
7
|
-
globalThis.CWD = process.cwd()
|
|
8
|
-
globalThis.ENV = process.env
|
|
7
|
+
;(globalThis as any).$ = $
|
|
8
|
+
;(globalThis as any).app = app
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const scriptPath = Bun.argv[2]
|
|
11
|
+
if (!scriptPath) {
|
|
12
|
+
console.error('Usage: script.js <script>')
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
globalThis.mixins = mixins
|
|
17
|
-
|
|
18
|
-
// Spawn
|
|
19
|
-
import { $, $l, $t } from './spawn'
|
|
20
|
-
globalThis.$ = $
|
|
21
|
-
globalThis.$t = $t
|
|
22
|
-
globalThis.$l = $l
|
|
23
|
-
|
|
24
|
-
// Error
|
|
25
|
-
import { exitWithError } from './exit'
|
|
26
|
-
globalThis.exitWithError = exitWithError
|
|
27
|
-
|
|
28
|
-
// Filesystem
|
|
29
|
-
import { fs, path, cp, ls, mkdir, mv, rm } from './fileSystem'
|
|
30
|
-
globalThis.fs = fs
|
|
31
|
-
globalThis.path = path
|
|
32
|
-
globalThis.cp = cp
|
|
33
|
-
globalThis.mv = mv
|
|
34
|
-
globalThis.rm = rm
|
|
35
|
-
globalThis.mkdir = mkdir
|
|
36
|
-
globalThis.ls = ls
|
|
37
|
-
|
|
38
|
-
// Lodash
|
|
39
|
-
import _ from 'lodash-es'
|
|
40
|
-
globalThis._ = _
|
|
41
|
-
|
|
42
|
-
// UI
|
|
43
|
-
import * as ui from './ui'
|
|
44
|
-
globalThis.ui = ui
|
|
45
|
-
import colors from 'chalk'
|
|
46
|
-
globalThis.colors = colors
|
|
47
|
-
|
|
48
|
-
// Csv
|
|
49
|
-
import * as csv from './csv'
|
|
50
|
-
globalThis.csv = csv
|
|
51
|
-
|
|
52
|
-
import * as yaml from './yaml'
|
|
53
|
-
globalThis.yaml = yaml
|
|
54
|
-
|
|
55
|
-
start()
|
|
16
|
+
await import(path.resolve(scriptPath))
|
|
17
|
+
await app.runViaScriptJs()
|
package/src/spawn.ts
CHANGED
|
@@ -1,23 +1,181 @@
|
|
|
1
|
-
|
|
1
|
+
const defaults: {
|
|
2
|
+
cwd: string | undefined
|
|
3
|
+
env: Record<string, string> | undefined
|
|
4
|
+
} = {
|
|
5
|
+
cwd: undefined,
|
|
6
|
+
env: undefined,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ShellError extends Error {
|
|
10
|
+
exitCode: number
|
|
11
|
+
stdout: string
|
|
12
|
+
stderr: string
|
|
13
|
+
|
|
14
|
+
constructor(command: string, result: ReturnType<typeof Bun.spawnSync>) {
|
|
15
|
+
super(`Command failed with exit code ${result.exitCode}: ${command}`)
|
|
16
|
+
this.exitCode = result.exitCode
|
|
17
|
+
this.stdout = (result.stdout ?? '').toString()
|
|
18
|
+
this.stderr = (result.stderr ?? '').toString()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class ShellCommand {
|
|
23
|
+
#command: string
|
|
24
|
+
#result: ReturnType<typeof Bun.spawnSync> | undefined
|
|
25
|
+
#cwd: string | undefined
|
|
26
|
+
#env: Record<string, string> | undefined
|
|
27
|
+
|
|
28
|
+
constructor(command: string) {
|
|
29
|
+
this.#command = command
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#pipeExec() {
|
|
33
|
+
if (!this.#result) {
|
|
34
|
+
const opts: Parameters<typeof Bun.spawnSync>[1] = {
|
|
35
|
+
stdin: 'inherit',
|
|
36
|
+
stdout: 'pipe',
|
|
37
|
+
stderr: 'inherit',
|
|
38
|
+
}
|
|
39
|
+
const cwd = this.#cwd ?? defaults.cwd
|
|
40
|
+
const env = this.#env ?? defaults.env
|
|
41
|
+
if (cwd !== undefined) opts.cwd = cwd
|
|
42
|
+
if (env !== undefined) opts.env = env
|
|
43
|
+
this.#result = Bun.spawnSync(['sh', '-c', this.#command], opts)
|
|
44
|
+
}
|
|
45
|
+
return this.#result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
inheritExec() {
|
|
49
|
+
const opts: Parameters<typeof Bun.spawnSync>[1] = {
|
|
50
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
51
|
+
}
|
|
52
|
+
const cwd = this.#cwd ?? defaults.cwd
|
|
53
|
+
const env = this.#env ?? defaults.env
|
|
54
|
+
if (cwd !== undefined) opts.cwd = cwd
|
|
55
|
+
if (env !== undefined) opts.env = env
|
|
56
|
+
Bun.spawnSync(['sh', '-c', this.#command], opts)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cwd(path: string) {
|
|
60
|
+
this.#cwd = path
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
env(vars: Record<string, string>) {
|
|
65
|
+
this.#env = vars
|
|
66
|
+
return this
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get exitCode() {
|
|
70
|
+
return this.#pipeExec().exitCode
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
text() {
|
|
74
|
+
const result = this.#pipeExec()
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
throw new ShellError(this.#command, result)
|
|
77
|
+
}
|
|
78
|
+
return (result.stdout ?? '').toString().trimEnd()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
json() {
|
|
82
|
+
return JSON.parse(this.text())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines() {
|
|
86
|
+
const lines = this.text().split('\n')
|
|
87
|
+
// fix '' issue
|
|
88
|
+
if (lines.length === 1 && lines[0] === '') {
|
|
89
|
+
return []
|
|
90
|
+
} else {
|
|
91
|
+
// fix ' a\n b\n' issue, space at start
|
|
92
|
+
return lines.map((v) => v.trim())
|
|
93
|
+
}
|
|
94
|
+
}
|
|
2
95
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
96
|
+
// biome-ignore lint/suspicious/noThenProperty: thenable for await $`cmd`
|
|
97
|
+
then(resolve?: (value: undefined) => void) {
|
|
98
|
+
this.inheritExec()
|
|
99
|
+
resolve?.(undefined)
|
|
100
|
+
}
|
|
6
101
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return result.trim()
|
|
102
|
+
toString() {
|
|
103
|
+
return this.text()
|
|
104
|
+
}
|
|
11
105
|
}
|
|
12
106
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
107
|
+
const CAPTURED_PROPS = [
|
|
108
|
+
'text',
|
|
109
|
+
'json',
|
|
110
|
+
'lines',
|
|
111
|
+
'exitCode',
|
|
112
|
+
'cwd',
|
|
113
|
+
'env',
|
|
114
|
+
'then',
|
|
115
|
+
]
|
|
116
|
+
const CHAINABLE_PROPS = ['cwd', 'env']
|
|
117
|
+
|
|
118
|
+
function $tag(strings: TemplateStringsArray, ...values: any[]): ShellCommand {
|
|
119
|
+
const command = buildCommand(strings, values)
|
|
120
|
+
const output = new ShellCommand(command)
|
|
121
|
+
|
|
122
|
+
let captured = false
|
|
123
|
+
const proxy = new Proxy(output, {
|
|
124
|
+
get(target, prop, receiver) {
|
|
125
|
+
if (CAPTURED_PROPS.includes(prop as string)) {
|
|
126
|
+
captured = true
|
|
127
|
+
}
|
|
128
|
+
const value = Reflect.get(target, prop, target)
|
|
129
|
+
if (typeof value === 'function') {
|
|
130
|
+
const bound = value.bind(target)
|
|
131
|
+
if (CHAINABLE_PROPS.includes(prop as string)) {
|
|
132
|
+
return (...args: any[]) => {
|
|
133
|
+
bound(...args)
|
|
134
|
+
return receiver
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return bound
|
|
138
|
+
}
|
|
139
|
+
return value
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
queueMicrotask(() => {
|
|
144
|
+
if (!captured) {
|
|
145
|
+
output.inheritExec()
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return proxy
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
$tag.cwd = (path: string) => {
|
|
153
|
+
defaults.cwd = path
|
|
154
|
+
}
|
|
155
|
+
$tag.env = (vars: Record<string, string>) => {
|
|
156
|
+
defaults.env = vars
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { $tag as $ }
|
|
160
|
+
|
|
161
|
+
function buildCommand(strings: TemplateStringsArray, values: any[]) {
|
|
162
|
+
let result = ''
|
|
163
|
+
for (let i = 0; i < strings.length; i++) {
|
|
164
|
+
result += strings[i]
|
|
165
|
+
if (i < values.length) {
|
|
166
|
+
const value = values[i]
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
result += value.map(escapeArg).join(' ')
|
|
169
|
+
} else {
|
|
170
|
+
result += escapeArg(String(value))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
22
173
|
}
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function escapeArg(arg: string): string {
|
|
178
|
+
if (arg === '') return "''"
|
|
179
|
+
if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg
|
|
180
|
+
return `'${arg.replace(/'/g, "'\\''")}'`
|
|
23
181
|
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { app } from './Command'
|
|
4
|
+
import { $ } from './spawn'
|
|
5
|
+
|
|
6
|
+
app
|
|
7
|
+
.cmd('cmd1 | c1', 'Command 1')
|
|
8
|
+
.a('<platform>', 'Platform', [
|
|
9
|
+
'ios',
|
|
10
|
+
'android',
|
|
11
|
+
'windows',
|
|
12
|
+
'macos',
|
|
13
|
+
'linux',
|
|
14
|
+
'unknown',
|
|
15
|
+
])
|
|
16
|
+
.a('<name>', 'Name')
|
|
17
|
+
.a('-l | --long')
|
|
18
|
+
.a(async (platform: string, options: any, ctx: any) => {
|
|
19
|
+
console.log(platform, options, ctx)
|
|
20
|
+
const name = 'Mike Smith'
|
|
21
|
+
const args = ['arg 1', 'arg 2']
|
|
22
|
+
$`echo ${name} ${args}`
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await app.run()
|
package/src/utils/fs.ts
CHANGED
|
@@ -75,7 +75,7 @@ async function inputJson(input: ReadFileArgs[0], options?: ReadFileArgs[1]) {
|
|
|
75
75
|
return
|
|
76
76
|
}
|
|
77
77
|
try {
|
|
78
|
-
return JSON.parse(text)
|
|
78
|
+
return JSON.parse(text as string)
|
|
79
79
|
} catch (error) {
|
|
80
80
|
if (error instanceof Error) {
|
|
81
81
|
throw new Error(`[inputJson] ${error.message} from '${input}'`)
|
|
@@ -90,7 +90,7 @@ async function inputJson(input: ReadFileArgs[0], options?: ReadFileArgs[1]) {
|
|
|
90
90
|
async function readJson(input: ReadFileArgs[0], options?: ReadFileArgs[1]) {
|
|
91
91
|
const text = await fs.readFile(cleanPath(input), options)
|
|
92
92
|
try {
|
|
93
|
-
return JSON.parse(text)
|
|
93
|
+
return JSON.parse(text as string)
|
|
94
94
|
} catch (error) {
|
|
95
95
|
if (error instanceof Error) {
|
|
96
96
|
throw new Error(`[readJson] ${error.message} from '${input}'`)
|
package/src/utils/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as fs } from './fs'
|
|
2
|
-
export { default as
|
|
2
|
+
export { default as nodePath } from './nodePath'
|