@bomb.sh/tab 0.0.1-pre.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.2.md ADDED
@@ -0,0 +1,108 @@
1
+ > A video showcasing how pnpm autocompletions work on a test CLI command like `my-cli`
2
+
3
+ # tab
4
+
5
+ > Instant feedback for your CLI tool when hitting [TAB] in your terminal
6
+
7
+ As CLI tooling authors, if we can spare our users a second or two by not checking the documentation or writing the `-h` option, we're doing them a huge favor. The unconscious loves hitting the [TAB] key. It always expects feedback. So it feels disappointing when hitting that key in the terminal but then nothing happens. That frustration is apparent across the whole JavaScript CLI tooling ecosystem.
8
+
9
+ Autocompletions are the solution to not break the user's flow. The issue is they're not simple to add. `zsh` expects them in one way, and `bash` in another way. Then where do we provide them so the shell environment parses them? Too many headaches to ease the user's experience. Whether it's worth it or not is out of the question. Because tab is the solution to this complexity.
10
+
11
+ `my-cli.ts`:
12
+
13
+ ```typescript
14
+ import t from '@bombsh/tab';
15
+
16
+ t.name('my-cli');
17
+
18
+ t.command('start', 'start the development server');
19
+
20
+ if (process.argv[2] === 'complete') {
21
+ const [shell, ...args] = process.argv.slice(3);
22
+ if (shell === '--') {
23
+ t.parse(args);
24
+ } else {
25
+ t.setup(shell, x);
26
+ }
27
+ }
28
+ ```
29
+
30
+ This `my-cli.ts` would be equipped with all the tools required to provide autocompletions.
31
+
32
+ ```bash
33
+ node my-cli.ts complete -- "st"
34
+ ```
35
+
36
+ ```
37
+ start start the development server
38
+ :0
39
+ ```
40
+
41
+ This output was generated by the `t.parse` method to autocomplete "st" to "start".
42
+
43
+ Obviously, the user won't be running that command directly in their terminal. They'd be running something like this.
44
+
45
+ ```bash
46
+ source <(node my-cli.ts complete zsh)
47
+ ```
48
+
49
+ Now whenever the shell sees `my-cli`, it would bring the autocompletions we wrote for this CLI tool. The `node my-cli.ts complete zsh` part would output the zsh script that loads the autocompletions provided by `t.parse` which then would be executed using `source`.
50
+
51
+ The autocompletions only live through the current shell session. To set them up across all terminal sessions, the autocompletion script should be injected in the `.zshrc` file.
52
+
53
+ ```bash
54
+ my-cli complete zsh > ~/completion-for-my-cli.zsh && echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc
55
+ ```
56
+
57
+ Or
58
+
59
+ ```bash
60
+ echo 'source <(npx --offline my-cli complete zsh)' >> ~/.zshrc
61
+ ```
62
+
63
+ This is an example of autocompletions on a global CLI command that is usually installed using the `-g` flag (e.g. `npm add -g my-cli`) which is available across the computer.
64
+
65
+ ---
66
+
67
+ While working on tab, we came to the realization that most JavaScript CLIs are not global CLI commands but rather, per-project dependencies.
68
+
69
+ For instance, Vite won't be installed globally and instead it'd be always installed on a project. Here's an example usage:
70
+
71
+ ```bash
72
+ pnpm vite dev
73
+ ```
74
+
75
+ Rather than installing it globally. This example is pretty rare:
76
+
77
+ ```bash
78
+ vite dev
79
+ ```
80
+
81
+ So in this case, a computer might have hundreds of Vite instances each installed separately and potentially from different versions on different projects.
82
+
83
+ We were looking for a fluid strategy that would be able to load the autocompletions from each of these dependencies on a per-project basis.
84
+
85
+ And that made us develop our own autocompletion abstraction over npm, pnpm and yarn. This would help tab identify which binaries are available in a project and which of these binaries provide autocompletions. So the user would not have to `source` anything or inject any script in their `.zshrc`.
86
+
87
+ They'd only have to run this command once and inject it in their shell config.
88
+
89
+ ```bash
90
+ npx @bombsh/tab pnpm zsh
91
+ ```
92
+
93
+ These autocompletions on top of the normal autocompletions that these package managers provide are going to be way more powerful.
94
+
95
+ These new autocompletions on top of package managers would help us with autocompletions on commands like `pnpm vite` and other global or per-project binaries. The only requirement would be that the npm binary itself would be a tab-compatible binary.
96
+
97
+ What is a tab-compatible binary? It's a tool that provides the `complete` subcommand that was showcased above. Basically any CLI tool that uses tab for its autocompletions is a tab-compatible binary.
98
+
99
+ ```bash
100
+ pnpm my-cli complete --
101
+ ```
102
+
103
+ ```
104
+ start start the development server
105
+ :0
106
+ ```
107
+
108
+ We are planning to maintain these package manager autocompletions on our own and turn them into full-fledged autocompletions that touch on every part of our package managers.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # tab
2
+
3
+ Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool.
4
+
5
+ Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces.
6
+
7
+ ## Examples
8
+
9
+ Check out the [examples directory](./examples) for complete examples of using Tab with different command-line frameworks:
10
+
11
+ - [CAC](./examples/demo.cac.ts)
12
+ - [Citty](./examples/demo.citty.ts)
13
+ - [Commander.js](./examples/demo.commander.ts)
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { Completion, script } from '@bombsh/tab';
19
+
20
+ const name = 'my-cli';
21
+ const completion = new Completion();
22
+
23
+ completion.addCommand(
24
+ 'start',
25
+ 'Start the application',
26
+ async (previousArgs, toComplete, endsWithSpace) => {
27
+ // suggestions
28
+ return [
29
+ { value: 'dev', description: 'Start in development mode' },
30
+ { value: 'prod', description: 'Start in production mode' },
31
+ ];
32
+ }
33
+ );
34
+
35
+ completion.addOption(
36
+ 'start',
37
+ '--port',
38
+ 'Specify the port number',
39
+ async (previousArgs, toComplete, endsWithSpace) => {
40
+ return [
41
+ { value: '3000', description: 'Development port' },
42
+ { value: '8080', description: 'Production port' },
43
+ ];
44
+ }
45
+ );
46
+
47
+ // a way of getting the executable path to pass to the shell autocompletion script
48
+ function quoteIfNeeded(path: string) {
49
+ return path.includes(' ') ? `'${path}'` : path;
50
+ }
51
+ const execPath = process.execPath;
52
+ const processArgs = process.argv.slice(1);
53
+ const quotedExecPath = quoteIfNeeded(execPath);
54
+ const quotedProcessArgs = processArgs.map(quoteIfNeeded);
55
+ const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
56
+ const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`;
57
+
58
+ if (process.argv[2] === '--') {
59
+ // autocompletion logic
60
+ await completion.parse(process.argv.slice(2), 'start'); // TODO: remove "start"
61
+ } else {
62
+ // process.argv[2] can be "zsh", "bash", "fish", "powershell"
63
+ script(process.argv[2], name, x);
64
+ }
65
+ ```
66
+
67
+ Now your user can run `source <(my-cli complete zsh)` and they will get completions for the `my-cli` command using the [autocompletion server](#autocompletion-server).
68
+
69
+ ## Adapters
70
+
71
+ Since we are heavy users of tools like `cac` and `citty`, we have created adapters for both of them. Ideally, tab would be integrated internally into these tools, but for now, this is a good compromise.
72
+
73
+ ### `@bombsh/tab/cac`
74
+
75
+ ```ts
76
+ import cac from 'cac';
77
+ import tab from '@bombsh/tab/cac';
78
+
79
+ const cli = cac('my-cli');
80
+
81
+ cli.command('dev', 'Start dev server').option('--port <port>', 'Specify port');
82
+
83
+ const completion = tab(cli);
84
+
85
+ // Get the dev command completion handler
86
+ const devCommandCompletion = completion.commands.get('dev');
87
+
88
+ // Get and configure the port option completion handler
89
+ const portOptionCompletion = devCommandCompletion.options.get('--port');
90
+ portOptionCompletion.handler = async (
91
+ previousArgs,
92
+ toComplete,
93
+ endsWithSpace
94
+ ) => {
95
+ return [
96
+ { value: '3000', description: 'Development port' },
97
+ { value: '8080', description: 'Production port' },
98
+ ];
99
+ };
100
+
101
+ cli.parse();
102
+ ```
103
+
104
+ Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `my-cli dev --po`, they will get suggestions for the `--port` option. Or if they write `my-cli d` they will get suggestions for the `dev` command.
105
+
106
+ Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation.
107
+
108
+ ### `@bombsh/tab/citty`
109
+
110
+ ```ts
111
+ import citty, { defineCommand, createMain } from 'citty';
112
+ import tab from '@bombsh/tab/citty';
113
+
114
+ const main = defineCommand({
115
+ meta: {
116
+ name: 'my-cli',
117
+ description: 'My CLI tool',
118
+ },
119
+ });
120
+
121
+ const devCommand = defineCommand({
122
+ meta: {
123
+ name: 'dev',
124
+ description: 'Start dev server',
125
+ },
126
+ args: {
127
+ port: { type: 'string', description: 'Specify port' },
128
+ },
129
+ });
130
+
131
+ main.subCommands = {
132
+ dev: devCommand,
133
+ };
134
+
135
+ const completion = await tab(main);
136
+
137
+ // TODO: addHandler function to export
138
+ const devCommandCompletion = completion.commands.get('dev');
139
+
140
+ const portOptionCompletion = devCommandCompletion.options.get('--port');
141
+
142
+ portOptionCompletion.handler = async (
143
+ previousArgs,
144
+ toComplete,
145
+ endsWithSpace
146
+ ) => {
147
+ return [
148
+ { value: '3000', description: 'Development port' },
149
+ { value: '8080', description: 'Production port' },
150
+ ];
151
+ };
152
+
153
+ const cli = createMain(main);
154
+ cli();
155
+ ```
156
+
157
+ ### `@bombsh/tab/commander`
158
+
159
+ ```ts
160
+ import { Command } from 'commander';
161
+ import tab from '@bombsh/tab/commander';
162
+
163
+ const program = new Command('my-cli');
164
+ program.version('1.0.0');
165
+
166
+ // Add commands
167
+ program
168
+ .command('serve')
169
+ .description('Start the server')
170
+ .option('-p, --port <number>', 'port to use', '3000')
171
+ .option('-H, --host <host>', 'host to use', 'localhost')
172
+ .action((options) => {
173
+ console.log('Starting server...');
174
+ });
175
+
176
+ // Initialize tab completion
177
+ const completion = tab(program);
178
+
179
+ // Configure custom completions
180
+ for (const command of completion.commands.values()) {
181
+ if (command.name === 'serve') {
182
+ for (const [option, config] of command.options.entries()) {
183
+ if (option === '--port') {
184
+ config.handler = () => {
185
+ return [
186
+ { value: '3000', description: 'Default port' },
187
+ { value: '8080', description: 'Alternative port' },
188
+ ];
189
+ };
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ program.parse();
196
+ ```
197
+
198
+ ## Recipe
199
+
200
+ `source <(my-cli complete zsh)` won't be enough since the user would have to run this command each time they spin up a new shell instance.
201
+
202
+ We suggest this approach for the end user that you as a maintainer might want to push.
203
+
204
+ ```
205
+ my-cli completion zsh > ~/completion-for-my-cli.zsh
206
+ echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc
207
+ ```
208
+
209
+ For other shells:
210
+
211
+ ```bash
212
+ # Bash
213
+ my-cli complete bash > ~/.bash_completion.d/my-cli
214
+ echo 'source ~/.bash_completion.d/my-cli' >> ~/.bashrc
215
+
216
+ # Fish
217
+ my-cli complete fish > ~/.config/fish/completions/my-cli.fish
218
+
219
+ # PowerShell
220
+ my-cli complete powershell > $PROFILE.CurrentUserAllHosts
221
+ ```
222
+
223
+ ## Autocompletion Server
224
+
225
+ By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server.
226
+
227
+ ```zsh
228
+ my-cli complete -- --po
229
+ --port Specify the port number
230
+ :0
231
+ ```
232
+
233
+ The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`).
234
+
235
+ In situations like `my-cli dev --po` you'd have autocompletions! But in the case of `pnpm my-cli dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm.
236
+
237
+ Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available.
238
+
239
+ This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box.
240
+
241
+ Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support.
242
+
243
+ ## Inspiration
244
+
245
+ - git
246
+ - [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go), without cobra, tab would have took 10x longer to build
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node