@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
package/README.md
CHANGED
|
@@ -1,85 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Script.js
|
|
2
2
|
|
|
3
|
-
**Write shell scripts in JavaScript** and leverage the power of JavaScript
|
|
3
|
+
**Write shell scripts in JavaScript** and leverage the power of JavaScript's simplicity and flexibility to make your shell scripting easier and faster. Why struggle with the complexity of Bash or other shell scripting languages when you already know JavaScript? Script.js allows you to write and run complex shell scripts using JavaScript, saving you time and reducing errors.
|
|
4
4
|
|
|
5
5
|
**Show your β€οΈ and support by starring this project and following the author, [Guten Ye](https://github.com/gutenye)!**
|
|
6
6
|
|
|
7
|
-
[](https://github.com/gutenye/script.js) [](https://www.npmjs.com/package/@gutenye/script.js) [](https://github.com/gutenye/script.js/blob/main/LICENSE) [](https://github.com/gutenye/script.js
|
|
7
|
+
[](https://github.com/gutenye/script.js) [](https://www.npmjs.com/package/@gutenye/script.js) [](https://github.com/gutenye/script.js/blob/main/LICENSE) [](https://github.com/gutenye/script.js#contribute)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Features
|
|
10
10
|
|
|
11
|
-
- **Fast to write**:
|
|
12
|
-
- **Fun to run**:
|
|
13
|
-
- **JavaScript Power**:
|
|
14
|
-
- **Familiar Syntax**: Write your shell commands just like you would in Bash, with full support for redirection, pipes, environment variables, and more.
|
|
11
|
+
- **Fast to write**: Global commands mean no imports needed. Open the editor, write a few lines, and run it.
|
|
12
|
+
- **Fun to run**: Built-in autocompletion support via Carapace.
|
|
13
|
+
- **JavaScript Power**: Access the entire JavaScript ecosystem.
|
|
15
14
|
- **Subcommands**: Create and organize subcommands effortlessly.
|
|
16
|
-
- **Help Documentation**: Auto-
|
|
17
|
-
- **Fast Execution**:
|
|
15
|
+
- **Help Documentation**: Auto-generated command help with argument validation.
|
|
16
|
+
- **Fast Execution**: Powered by Bun for fast startup and execution.
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Getting Started
|
|
20
19
|
|
|
21
|
-
### 1
|
|
20
|
+
### 1. Install
|
|
22
21
|
|
|
23
22
|
First, make sure [Bun](https://bun.sh) and [Carapace](https://github.com/carapace-sh/carapace-bin) are installed.
|
|
24
23
|
|
|
25
|
-
To install Script.js, run:
|
|
26
|
-
|
|
27
24
|
```sh
|
|
28
25
|
npm install -g @gutenye/script.js
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
### 2
|
|
28
|
+
### 2. Write Your First Script
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
**Via script.js** β globals are set up automatically, no imports needed:
|
|
34
31
|
|
|
35
32
|
```ts
|
|
36
33
|
#!/usr/bin/env script.js
|
|
37
34
|
|
|
35
|
+
app.meta("hello");
|
|
36
|
+
|
|
37
|
+
app
|
|
38
|
+
.cmd("greetings", "Say hello")
|
|
39
|
+
.add("[...files]", "Files", ["$files"])
|
|
40
|
+
.add((files, ctx) => {
|
|
41
|
+
$`ls -l ${files}`;
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Via import** β use as a library in any Bun script:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
#!/usr/bin/env bun
|
|
49
|
+
|
|
50
|
+
import { app, $ } from "@gutenye/script.js";
|
|
51
|
+
|
|
52
|
+
app.meta("hello");
|
|
53
|
+
|
|
38
54
|
app
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.action((files, options) => {
|
|
47
|
-
$`
|
|
48
|
-
ls -l ${files}
|
|
49
|
-
echo ${files} | wc -l
|
|
50
|
-
`
|
|
51
|
-
})
|
|
55
|
+
.cmd("greetings", "Say hello")
|
|
56
|
+
.add("[...files]", "Files", ["$files"])
|
|
57
|
+
.add((files, ctx) => {
|
|
58
|
+
$`ls -l ${files}`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await app.run();
|
|
52
62
|
```
|
|
53
63
|
|
|
54
|
-
### 3
|
|
64
|
+
### 3. Run the script
|
|
55
65
|
|
|
56
66
|
You can use `<Tab>` to autocomplete arguments or options while using the script.
|
|
57
67
|
|
|
58
68
|
```sh
|
|
59
|
-
chmod +x hello
|
|
60
|
-
./hello
|
|
61
|
-
./hello
|
|
69
|
+
chmod +x hello # Make the script executable
|
|
70
|
+
./hello # First run to create completion file
|
|
71
|
+
./hello <Tab> # Use Tab key for autocompletion
|
|
62
72
|
```
|
|
63
73
|
|
|
64
|
-
##
|
|
74
|
+
## Documentation
|
|
65
75
|
|
|
66
|
-
- [Create Commands](./docs/Create%20Commands.md): Create commands and
|
|
76
|
+
- [Create Commands](./docs/Create%20Commands.md): Create commands, subcommands, and manage dependencies
|
|
67
77
|
- [Completion](./docs/Completion.md): Customize the autocompletion
|
|
68
78
|
- [Global Commands](./docs/Global%20Commands.md): List of global variables and commands
|
|
79
|
+
- [Ake](./src/ake): A task runner supports shell autocompletion
|
|
69
80
|
|
|
70
|
-
##
|
|
81
|
+
## v2
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
- [Carapace](https://github.com/carapace-sh/carapace-bin): A multi-shell completion binary
|
|
74
|
-
- [Zurk](https://github.com/webpod/zurk): A generic process spawner
|
|
75
|
-
- [Zx](https://github.com/google/zx): A tool for writing better scripts
|
|
76
|
-
- [Commander.js](https://github.com/tj/commander.js): node.js command-line interfaces made easy
|
|
83
|
+
v2 is a complete rewrite from scratch. It removes heavy dependencies, replacing them with a lightweight, custom-built CLI framework. A cleaner syntax, the smallest set of global variables, and using vanilla JavaScript as much as possible. You should be able to write scripts quickly without looking up documentation β if you know JavaScript, you know the API.
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
See [Migration from v1](./docs/Migration.md) for a full migration guide.
|
|
86
|
+
|
|
87
|
+
Looking for v1 documentation [here](https://github.com/gutenye/script.js/tree/v1)
|
|
88
|
+
|
|
89
|
+
## Thanks
|
|
90
|
+
|
|
91
|
+
- [Bun](https://github.com/oven-sh/bun): Incredibly fast JavaScript runtime, bundler, test runner, and package manager
|
|
92
|
+
- [Carapace](https://github.com/carapace-sh/carapace-bin): A multi-shell completion binary
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
## Contribute
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
We love contributions! Whether you're fixing bugs, adding features, or improving documentation, your involvement makes this project better.
|
|
83
97
|
|
|
84
98
|
1. Fork the Repository
|
|
85
99
|
2. Open a Pull Request on Github
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gutenye/script.js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Write shell scripts in JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shell",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": "github:gutenye/script.js",
|
|
16
16
|
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
"bun": "./src/index.ts",
|
|
19
|
+
"default": "./src/index.ts"
|
|
20
|
+
},
|
|
17
21
|
"files": [
|
|
18
22
|
"src",
|
|
19
23
|
"tsconfig.json",
|
|
@@ -21,7 +25,10 @@
|
|
|
21
25
|
"!**/__tests__"
|
|
22
26
|
],
|
|
23
27
|
"bin": {
|
|
24
|
-
"script.js": "src/script.ts"
|
|
28
|
+
"script.js": "src/script.ts",
|
|
29
|
+
"a": "src/ake.ts",
|
|
30
|
+
"ake": "src/ake.ts",
|
|
31
|
+
"akectl": "src/akectl.ts"
|
|
25
32
|
},
|
|
26
33
|
"scripts": {
|
|
27
34
|
"test": "bun test",
|
|
@@ -32,26 +39,16 @@
|
|
|
32
39
|
"access": "public"
|
|
33
40
|
},
|
|
34
41
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"chalk": "^5.3.0",
|
|
37
|
-
"commander": "^12.1.0",
|
|
38
|
-
"csv-parse": "^5.5.6",
|
|
39
|
-
"lodash-es": "^4.17.21",
|
|
40
|
-
"table": "^6.8.2",
|
|
41
|
-
"tiny-invariant": "^1.3.3",
|
|
42
|
-
"yaml": "^2.6.0",
|
|
43
|
-
"zx": "^8.2.1"
|
|
42
|
+
"yaml": "^2.6.0"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
46
45
|
"typescript": "^5.7.2"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@biomejs/biome": "^
|
|
48
|
+
"@biomejs/biome": "^2.4.4",
|
|
50
49
|
"@semantic-release/changelog": "^6.0.3",
|
|
51
50
|
"@semantic-release/git": "^10.0.1",
|
|
52
51
|
"@types/bun": "latest",
|
|
53
|
-
"
|
|
54
|
-
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
55
|
-
"tsc-alias": "^1.8.10"
|
|
52
|
+
"conventional-changelog-conventionalcommits": "^8.0.0"
|
|
56
53
|
}
|
|
57
54
|
}
|
package/src/Argument.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class Argument {
|
|
2
|
+
rawName: string
|
|
3
|
+
name: string
|
|
4
|
+
description: string
|
|
5
|
+
required: boolean
|
|
6
|
+
variadic: boolean
|
|
7
|
+
completion: string[] | (() => string[])
|
|
8
|
+
defaultValue: any
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
rawName: string,
|
|
12
|
+
description = '',
|
|
13
|
+
completion: string[] | (() => string[]) = [],
|
|
14
|
+
) {
|
|
15
|
+
this.rawName = rawName.trim()
|
|
16
|
+
const { name, required, variadic, defaultValue } =
|
|
17
|
+
Argument.parseName(rawName)
|
|
18
|
+
this.name = name
|
|
19
|
+
this.required = required
|
|
20
|
+
this.variadic = variadic
|
|
21
|
+
this.description = description
|
|
22
|
+
this.completion = completion
|
|
23
|
+
this.defaultValue = defaultValue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toString() {
|
|
27
|
+
return this.rawName
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static parseName(rawName: string) {
|
|
31
|
+
const trimmed = rawName.trim()
|
|
32
|
+
const required = trimmed.startsWith('<')
|
|
33
|
+
const variadic = trimmed.includes('...')
|
|
34
|
+
const inner = trimmed.replace(/[<>[\].]/g, '').trim()
|
|
35
|
+
const eqIndex = inner.indexOf('=')
|
|
36
|
+
const name = eqIndex !== -1 ? inner.slice(0, eqIndex) : inner
|
|
37
|
+
const defaultValue = eqIndex !== -1 ? inner.slice(eqIndex + 1) : undefined
|
|
38
|
+
return { name, required, variadic, defaultValue }
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/Command.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Argument } from './Argument'
|
|
2
|
+
import { installCompletion } from './completion'
|
|
3
|
+
import { Option } from './Option'
|
|
4
|
+
import { parseArgv } from './parseArgv'
|
|
5
|
+
|
|
6
|
+
export class Command {
|
|
7
|
+
name?: string
|
|
8
|
+
description?: string
|
|
9
|
+
aliases: string[] = []
|
|
10
|
+
action!: (...args: any[]) => void | Promise<void>
|
|
11
|
+
arguments: Argument[] = []
|
|
12
|
+
commands: Command[] = []
|
|
13
|
+
options: Option[] = []
|
|
14
|
+
#defaultCommand?: Command
|
|
15
|
+
#extraHelp?: string
|
|
16
|
+
|
|
17
|
+
meta(inputName: string, description = '') {
|
|
18
|
+
const { name, aliases } = this.#parseAliases(inputName)
|
|
19
|
+
this.name = name
|
|
20
|
+
this.aliases = aliases
|
|
21
|
+
this.description = description
|
|
22
|
+
return this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
help(text?: string) {
|
|
26
|
+
if (text != null) {
|
|
27
|
+
this.#extraHelp = text.trim()
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
console.log(this.helpText())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cmd(inputName?: string, description = '') {
|
|
34
|
+
const command = new Command()
|
|
35
|
+
if (inputName) {
|
|
36
|
+
const { name, aliases } = this.#parseAliases(inputName)
|
|
37
|
+
command.name = name
|
|
38
|
+
command.aliases = aliases
|
|
39
|
+
command.description = description
|
|
40
|
+
this.commands.push(command)
|
|
41
|
+
} else {
|
|
42
|
+
this.#defaultCommand = command
|
|
43
|
+
}
|
|
44
|
+
return command
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async invoke(text: string, ...args: any[]) {
|
|
48
|
+
if (args.length === 0) {
|
|
49
|
+
return this.parse(text.split(/ +/))
|
|
50
|
+
}
|
|
51
|
+
const command = this.#findCommand(text)
|
|
52
|
+
if (!command) {
|
|
53
|
+
throw new Error(`Unknown command: ${text}`)
|
|
54
|
+
}
|
|
55
|
+
return command.action?.(...args)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async runViaScriptJs() {
|
|
59
|
+
installCompletion(this, { scriptPath: Bun.argv[2] })
|
|
60
|
+
return this.parse(Bun.argv.slice(3))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// runViaBun
|
|
64
|
+
async run() {
|
|
65
|
+
installCompletion(this, { scriptPath: Bun.main })
|
|
66
|
+
return this.parse(Bun.argv.slice(2))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async parse(argv: string[]): Promise<void> {
|
|
70
|
+
const commandName = argv[0]
|
|
71
|
+
if (commandName === '-h') {
|
|
72
|
+
console.log(this.helpText())
|
|
73
|
+
return process.exit(0)
|
|
74
|
+
}
|
|
75
|
+
if (!commandName) {
|
|
76
|
+
if (this.#defaultCommand) {
|
|
77
|
+
return this.#runDefault(argv)
|
|
78
|
+
}
|
|
79
|
+
console.log(this.helpText())
|
|
80
|
+
return process.exit(0)
|
|
81
|
+
}
|
|
82
|
+
const command = this.#findCommand(commandName)
|
|
83
|
+
if (!command) {
|
|
84
|
+
if (this.#defaultCommand) {
|
|
85
|
+
return this.#runDefault(argv)
|
|
86
|
+
}
|
|
87
|
+
console.log(this.helpText())
|
|
88
|
+
console.error(`\nUnknown command: ${commandName}`)
|
|
89
|
+
return process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
const commandArgv = argv.slice(1)
|
|
92
|
+
if (command.commands.length > 0 || command.#defaultCommand) {
|
|
93
|
+
return command.parse(commandArgv)
|
|
94
|
+
}
|
|
95
|
+
if (commandArgv.includes('-h')) {
|
|
96
|
+
console.log(command.helpText())
|
|
97
|
+
return process.exit(0)
|
|
98
|
+
}
|
|
99
|
+
const { positionals, options } = parseArgv(
|
|
100
|
+
commandArgv,
|
|
101
|
+
command.arguments,
|
|
102
|
+
command.options,
|
|
103
|
+
)
|
|
104
|
+
const context: Context = { argv: commandArgv }
|
|
105
|
+
await this.#invokeAction(command, positionals, options, context)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
a = this.add.bind(this)
|
|
109
|
+
|
|
110
|
+
add(...args: any[]) {
|
|
111
|
+
if (typeof args[0] === 'function') {
|
|
112
|
+
this.action = args[0]
|
|
113
|
+
} else {
|
|
114
|
+
let [inputName, description, completionOrDefault] = args
|
|
115
|
+
|
|
116
|
+
if (typeof description !== 'string') {
|
|
117
|
+
if (completionOrDefault) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'Invalid third argument, should be a(name, description, completion) format',
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
completionOrDefault = description
|
|
123
|
+
description = ''
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const name = inputName.trim()
|
|
127
|
+
if (name.startsWith('-')) {
|
|
128
|
+
this.options.push(new Option(name, description, completionOrDefault))
|
|
129
|
+
} else {
|
|
130
|
+
this.arguments.push(
|
|
131
|
+
new Argument(name, description, completionOrDefault),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
helpText() {
|
|
140
|
+
const lines: string[] = []
|
|
141
|
+
const name = this.name || 'app'
|
|
142
|
+
if (this.commands.length > 0) {
|
|
143
|
+
lines.push(`Usage: ${name} <command>`)
|
|
144
|
+
lines.push('')
|
|
145
|
+
lines.push('Commands:')
|
|
146
|
+
const labels = this.commands.map((c) => {
|
|
147
|
+
const names = [c.name, ...c.aliases]
|
|
148
|
+
.sort((a, b) => (a?.length ?? 0) - (b?.length ?? 0))
|
|
149
|
+
.join(', ')
|
|
150
|
+
const args = c.#argsText()
|
|
151
|
+
return args ? `${names} ${args}` : names
|
|
152
|
+
})
|
|
153
|
+
const maxLen = Math.max(...labels.map((l) => l.length))
|
|
154
|
+
for (let i = 0; i < this.commands.length; i++) {
|
|
155
|
+
const padded = labels[i].padEnd(maxLen + 2)
|
|
156
|
+
lines.push(` ${padded}${this.commands[i].description || ''}`)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
const args = this.#argsText()
|
|
160
|
+
const usage = args ? `${name} ${args}` : name
|
|
161
|
+
lines.push(`Usage: ${usage}`)
|
|
162
|
+
if (this.description) {
|
|
163
|
+
lines.push('')
|
|
164
|
+
lines.push(this.description)
|
|
165
|
+
}
|
|
166
|
+
if (this.arguments.length > 0) {
|
|
167
|
+
lines.push('')
|
|
168
|
+
lines.push('Arguments:')
|
|
169
|
+
const maxLen = Math.max(...this.arguments.map((a) => a.name.length))
|
|
170
|
+
for (const arg of this.arguments) {
|
|
171
|
+
const padded = arg.name.padEnd(maxLen + 2)
|
|
172
|
+
const choices = Command.#choicesText(arg.completion)
|
|
173
|
+
const desc = [arg.description, choices].filter(Boolean).join(' ')
|
|
174
|
+
lines.push(` ${padded}${desc}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (this.options.length > 0) {
|
|
178
|
+
lines.push('')
|
|
179
|
+
lines.push('Options:')
|
|
180
|
+
const flags = this.options.map(String)
|
|
181
|
+
const maxLen = Math.max(...flags.map((f) => f.length))
|
|
182
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
183
|
+
const padded = flags[i].padEnd(maxLen + 2)
|
|
184
|
+
const choices = Command.#choicesText(this.options[i].completion)
|
|
185
|
+
const desc = [this.options[i].description, choices]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join(' ')
|
|
188
|
+
lines.push(` ${padded}${desc}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (this.#extraHelp) {
|
|
193
|
+
lines.push('')
|
|
194
|
+
lines.push(this.#extraHelp)
|
|
195
|
+
}
|
|
196
|
+
return lines.join('\n')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async #runDefault(argv: string[]) {
|
|
200
|
+
const cmd = this.#defaultCommand as Command
|
|
201
|
+
const { positionals, options } = parseArgv(argv, cmd.arguments, cmd.options)
|
|
202
|
+
const context: Context = { argv }
|
|
203
|
+
await this.#invokeAction(cmd, positionals, options, context)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #invokeAction(
|
|
207
|
+
command: Command,
|
|
208
|
+
positionals: any[],
|
|
209
|
+
options: Record<string, any>,
|
|
210
|
+
context: Context,
|
|
211
|
+
) {
|
|
212
|
+
const error =
|
|
213
|
+
Command.#validateOptions(command, options) ??
|
|
214
|
+
Command.#validateChoices(command, positionals)
|
|
215
|
+
if (error) {
|
|
216
|
+
console.log(command.helpText())
|
|
217
|
+
console.error(`\n${error}`)
|
|
218
|
+
return process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
const args = [...positionals]
|
|
221
|
+
if (command.options.length > 0) {
|
|
222
|
+
args.push(options)
|
|
223
|
+
}
|
|
224
|
+
args.push(context)
|
|
225
|
+
await command.action?.(...args)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#argsText() {
|
|
229
|
+
return this.arguments.map(String).join(' ')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static #validateOptions(
|
|
233
|
+
command: Command,
|
|
234
|
+
options: Record<string, any>,
|
|
235
|
+
): string | null {
|
|
236
|
+
for (const opt of command.options) {
|
|
237
|
+
if (opt.required && options[opt.attributeName] == null) {
|
|
238
|
+
return `Missing required value for option: ${opt}`
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static #validateChoices(command: Command, positionals: any[]): string | null {
|
|
245
|
+
for (let i = 0; i < command.arguments.length; i++) {
|
|
246
|
+
const arg = command.arguments[i]
|
|
247
|
+
const value = positionals[i]
|
|
248
|
+
if (value == null) continue
|
|
249
|
+
if (typeof arg.completion === 'function') continue
|
|
250
|
+
if (arg.completion.length === 0) continue
|
|
251
|
+
if (arg.completion.some((c) => c.startsWith('$') || /^[<[]/.test(c)))
|
|
252
|
+
continue
|
|
253
|
+
const values = arg.variadic ? value : [value]
|
|
254
|
+
for (const v of values) {
|
|
255
|
+
if (!arg.completion.includes(v)) {
|
|
256
|
+
return `Invalid value for ${arg.name}: '${v}' (expected: ${arg.completion.join(', ')})`
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static #choicesText(completion: string[] | (() => string[])): string {
|
|
264
|
+
const values = typeof completion === 'function' ? completion() : completion
|
|
265
|
+
if (values.length === 0) return ''
|
|
266
|
+
const text = values.join(', ')
|
|
267
|
+
if (text.length <= 40) return `(${text})`
|
|
268
|
+
return `(${text.slice(0, 37)}...)`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#findCommand(name: string) {
|
|
272
|
+
return this.commands.find(
|
|
273
|
+
(command) => command.name === name || command.aliases.includes(name),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#parseAliases(inputName: string) {
|
|
278
|
+
const names = inputName.split('|').map((alias) => alias.trim())
|
|
279
|
+
const [name, ...aliases] = names
|
|
280
|
+
return { name, aliases }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const app = new Command()
|
|
285
|
+
|
|
286
|
+
type Context = {
|
|
287
|
+
argv: string[]
|
|
288
|
+
}
|
package/src/Option.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export class Option {
|
|
2
|
+
rawFlags: string
|
|
3
|
+
short?: string
|
|
4
|
+
long?: string
|
|
5
|
+
description: string
|
|
6
|
+
required: boolean
|
|
7
|
+
optional: boolean
|
|
8
|
+
variadic: boolean
|
|
9
|
+
negate: boolean
|
|
10
|
+
attributeName: string
|
|
11
|
+
completion: string[] | (() => string[])
|
|
12
|
+
defaultValue: any
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
rawFlags: string,
|
|
16
|
+
description = '',
|
|
17
|
+
defaultValueOrCompletion: any = undefined,
|
|
18
|
+
) {
|
|
19
|
+
this.rawFlags = rawFlags
|
|
20
|
+
this.description = description
|
|
21
|
+
|
|
22
|
+
const parts = rawFlags.split('|').map((s) => s.trim())
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (part.startsWith('--')) {
|
|
25
|
+
this.long = part.split(/\s/)[0]
|
|
26
|
+
} else if (part.startsWith('-')) {
|
|
27
|
+
const token = part.split(/\s/)[0]
|
|
28
|
+
if (token.startsWith('--')) {
|
|
29
|
+
this.long = token
|
|
30
|
+
} else {
|
|
31
|
+
this.short = token
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.required = rawFlags.includes('<')
|
|
37
|
+
this.optional = rawFlags.includes('[')
|
|
38
|
+
this.variadic = rawFlags.includes('...')
|
|
39
|
+
this.negate = this.long?.startsWith('--no-') ?? false
|
|
40
|
+
|
|
41
|
+
if (this.long) {
|
|
42
|
+
let key = this.long.replace(/^--/, '')
|
|
43
|
+
if (this.negate) key = key.replace(/^no-/, '')
|
|
44
|
+
this.attributeName = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
45
|
+
} else if (this.short) {
|
|
46
|
+
this.attributeName = this.short.replace(/^-/, '')
|
|
47
|
+
} else {
|
|
48
|
+
this.attributeName = ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
Array.isArray(defaultValueOrCompletion) ||
|
|
53
|
+
typeof defaultValueOrCompletion === 'function'
|
|
54
|
+
) {
|
|
55
|
+
this.completion = defaultValueOrCompletion
|
|
56
|
+
this.defaultValue = undefined
|
|
57
|
+
} else {
|
|
58
|
+
this.completion = []
|
|
59
|
+
this.defaultValue = defaultValueOrCompletion
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toString() {
|
|
64
|
+
return this.rawFlags.replaceAll('|', ',')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isBoolean() {
|
|
68
|
+
return !this.required && !this.optional && !this.negate
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Ake
|
|
2
|
+
|
|
3
|
+
> A task runner supports shell autocompletion
|
|
4
|
+
|
|
5
|
+
## Start
|
|
6
|
+
|
|
7
|
+
1. Create an ake file
|
|
8
|
+
|
|
9
|
+
edit `./ake` file, and make it executable `chmod +x ake`
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
#!/usr/bin/env script.js
|
|
13
|
+
|
|
14
|
+
app.cmd('greetings')
|
|
15
|
+
.add(() => {
|
|
16
|
+
console.log('greetings')
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. Run it
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
ake greetings # find the ake file and runs it
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Supports Shell Completion
|
|
27
|
+
|
|
28
|
+
- Follow [guide](./completions) to setup
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
ake <Tab> # uses ake file's completion
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use a template
|
|
35
|
+
|
|
36
|
+
Create `~/bin.src/ake/template`
|
|
37
|
+
|
|
38
|
+
## Put ake file in another location
|
|
39
|
+
|
|
40
|
+
Doesn't touch original project files
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
akectl init remote # create in ~/bin.src/ake/<dir>
|
|
44
|
+
```
|
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()
|