@gutenye/script.js 1.1.0 → 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/src/ake/ake.ts ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { exitWithError, findAkeFiles } from './shared'
4
+
5
+ async function main() {
6
+ const akeFile = await findAkeFile()
7
+ runCommand(akeFile)
8
+ }
9
+
10
+ async function findAkeFile() {
11
+ const akeFiles = await findAkeFiles()
12
+
13
+ if (akeFiles.length >= 2) {
14
+ exitWithError(
15
+ 'you have duplicated ake files, merge them first',
16
+ akeFiles.join('\n'),
17
+ )
18
+ }
19
+
20
+ const akeFile = akeFiles[0]
21
+
22
+ if (!akeFile) {
23
+ exitWithError(
24
+ 'ake file not found',
25
+ 'Use below commands to create one:\nakectl init local\nakectl init remote',
26
+ )
27
+ }
28
+
29
+ return akeFile
30
+ }
31
+
32
+ function runCommand(akeFile: string) {
33
+ const { exitCode } = Bun.spawnSync([akeFile, ...Bun.argv.slice(2)], {
34
+ stdio: ['inherit', 'inherit', 'inherit'],
35
+ })
36
+
37
+ process.exit(exitCode)
38
+ }
39
+
40
+ await main()
@@ -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,8 @@
1
+ complete --erase a
2
+ complete --command a --no-files --arguments '(_ake_complete)'
3
+
4
+ function _ake_complete
5
+ set unique_name (string replace --all '/' '_' (realpath $PWD))
6
+ set name "ake.$unique_name"
7
+ _carapace_completer $name
8
+ end
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from './Command'
2
+ import type { $ as Shell } from './spawn'
3
+
4
+ declare global {
5
+ var app: Command
6
+ var $: typeof Shell
7
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { app, Command } from './Command'
2
+ export { $ } from './spawn'
@@ -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 'zx/globals'
3
+ import path from 'node:path'
4
+ import { app } from './Command'
5
+ import { $ } from './spawn'
4
6
 
5
- // Variables
6
- globalThis.HOME = os.homedir()
7
- globalThis.CWD = process.cwd()
8
- globalThis.ENV = process.env
7
+ ;(globalThis as any).$ = $
8
+ ;(globalThis as any).app = app
9
9
 
10
- // App
11
- import { app, start } from './app'
12
- globalThis.app = app
10
+ const scriptPath = Bun.argv[2]
11
+ if (!scriptPath) {
12
+ console.error('Usage: script.js <script>')
13
+ process.exit(1)
14
+ }
13
15
 
14
- // Mixins
15
- import { mixins } from './mixins'
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
- import * as zx from 'zx'
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
- // wait for release fix: handle nullable stdout/stderr https://github.com/google/zx/commits/main/
4
- // export const $ = zx.$.sync({ stdio: 'inherit' })
5
- export const $ = zx.$({ stdio: 'inherit' })
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
- // Returns text
8
- export function $t(...args) {
9
- const result = zx.$.sync(...args).text()
10
- return result.trim()
102
+ toString() {
103
+ return this.text()
104
+ }
11
105
  }
12
106
 
13
- // Returns lines
14
- export function $l(...args) {
15
- const lines = zx.$.sync(...args).lines()
16
- // fix [''] issue
17
- if (lines.length === 1 && lines[0] === '') {
18
- return []
19
- } else {
20
- // fix ' a\n b\n' with space issue
21
- return lines.map((v) => v.trim())
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()