@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/README.md CHANGED
@@ -1,85 +1,99 @@
1
- # 🧩 Script.js 🧩
1
+ # Script.js
2
2
 
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.
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
- [![Stars](https://img.shields.io/github/stars/gutenye/script.js?style=social)](https://github.com/gutenye/script.js) [![NPM Version](https://img.shields.io/npm/v/@gutenye/script.js)](https://www.npmjs.com/package/@gutenye/script.js) [![License](https://img.shields.io/github/license/gutenye/script.js?color=blue)](https://github.com/gutenye/script.js/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue)](https://github.com/gutenye/script.js#-contribute)
7
+ [![Stars](https://img.shields.io/github/stars/gutenye/script.js?style=social)](https://github.com/gutenye/script.js) [![NPM Version](https://img.shields.io/npm/v/@gutenye/script.js)](https://www.npmjs.com/package/@gutenye/script.js) [![License](https://img.shields.io/github/license/gutenye/script.js?color=blue)](https://github.com/gutenye/script.js/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue)](https://github.com/gutenye/script.js#contribute)
8
8
 
9
- ## 🌟 Features
9
+ ## Features
10
10
 
11
- - **Fast to write**: The most important factor in script writing is **speed**. With Script.js, you can quickly open the editor, write a few lines of code, and run it instantly. Thanks to global commands, you don’t need to worry about import statements, and more. Writing a script is enjoyable and efficient.
12
- - **Fun to run**: A major benefit of using a script is **autocompletion**. Script.js includes built-in autocompletion support. Running a script is fun and seamless.
13
- - **JavaScript Power**: Writing complex scripts in JavaScript and access the entire JavaScript ecosystem.
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-generate command help documentation for better user experience.
17
- - **Fast Execution**: Fast to execute your scripts.
15
+ - **Help Documentation**: Auto-generated command help with argument validation.
16
+ - **Fast Execution**: Powered by Bun for fast startup and execution.
18
17
 
19
- ## πŸš€ Getting Started
18
+ ## Getting Started
20
19
 
21
- ### 1️⃣ Install
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️⃣ Write Your First Script
28
+ ### 2. Write Your First Script
32
29
 
33
- Create a file named `hello.js` and add the following code:
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
- .name('hello.js')
40
- .enableCompletion()
41
-
42
- app.command('greetings [files...]')
43
- .completion({
44
- positionalany: ['$files'],
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️⃣ Run the script
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.js # Make the script executable
60
- ./hello.js # First run to create completion file
61
- ./hello.js <Tab> # Use Tab key for autocompletion
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
- ## πŸ“– Documentation
74
+ ## Documentation
65
75
 
66
- - [Create Commands](./docs/Create%20Commands.md): Create commands and subcommands
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
- ## πŸ™‡ Thanks
81
+ ## v2
71
82
 
72
- - [Bun](https://github.com/oven-sh/bun): Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
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
- ## 🀝 Contribute
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
- We love contributions! Whether you’re fixing bugs, adding features, or improving documentation, your involvement makes this project better.
94
+ ## Contribute
81
95
 
82
- **How to Contribute:**
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,14 +1,34 @@
1
1
  {
2
2
  "name": "@gutenye/script.js",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Write shell scripts in JavaScript",
5
- "keywords": ["shell", "script", "bash", "exec", "spawn", "zx", "bunshell"],
5
+ "keywords": [
6
+ "shell",
7
+ "script",
8
+ "bash",
9
+ "exec",
10
+ "spawn",
11
+ "zx",
12
+ "bunshell"
13
+ ],
6
14
  "license": "MIT",
7
15
  "repository": "github:gutenye/script.js",
8
16
  "type": "module",
9
- "files": ["src", "tsconfig.json", "build", "!**/__tests__"],
17
+ "exports": {
18
+ "bun": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "tsconfig.json",
24
+ "build",
25
+ "!**/__tests__"
26
+ ],
10
27
  "bin": {
11
- "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"
12
32
  },
13
33
  "scripts": {
14
34
  "test": "bun test",
@@ -19,26 +39,16 @@
19
39
  "access": "public"
20
40
  },
21
41
  "dependencies": {
22
- "@gutenye/commander-completion-carapace": "^1.0.9",
23
- "chalk": "^5.3.0",
24
- "commander": "^12.1.0",
25
- "csv-parse": "^5.5.6",
26
- "lodash-es": "^4.17.21",
27
- "table": "^6.8.2",
28
- "tiny-invariant": "^1.3.3",
29
- "yaml": "^2.6.0",
30
- "zx": "^8.2.1"
42
+ "yaml": "^2.6.0"
31
43
  },
32
44
  "peerDependencies": {
33
45
  "typescript": "^5.7.2"
34
46
  },
35
47
  "devDependencies": {
36
- "@biomejs/biome": "^1.9.4",
48
+ "@biomejs/biome": "^2.4.4",
37
49
  "@semantic-release/changelog": "^6.0.3",
38
50
  "@semantic-release/git": "^10.0.1",
39
51
  "@types/bun": "latest",
40
- "@types/lodash-es": "^4.17.12",
41
- "conventional-changelog-conventionalcommits": "^8.0.0",
42
- "tsc-alias": "^1.8.10"
52
+ "conventional-changelog-conventionalcommits": "^8.0.0"
43
53
  }
44
54
  }
@@ -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
+ ```