summon-cli 0.3.0 → 0.4.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 +108 -19
- package/bin/cli.mjs +120 -8
- package/package.json +2 -2
- package/src/app.mjs +276 -29
- package/src/config.mjs +2 -1
package/README.md
CHANGED
|
@@ -8,43 +8,132 @@ Terminal launcher for local AI CLIs. Run `summon`, pick a tool, launch it.
|
|
|
8
8
|
|
|
9
9
|
Built-in: Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, opencode CLI. Missing tools show dimmed. Add your own with `--add`.
|
|
10
10
|
|
|
11
|
-
> Pre-release (0.
|
|
11
|
+
> Pre-release (0.4.0).
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
npm install -g summon-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## First run
|
|
20
|
+
|
|
21
|
+
The first launch walks you through a one-time setup (and keeps asking until you finish it):
|
|
22
|
+
|
|
23
|
+
```sh
|
|
17
24
|
summon
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
Two steps:
|
|
28
|
+
|
|
29
|
+
1. **Command name** — pick what you'll type to open the launcher. The default is `cli`; `summon` always keeps working. Picking `cli` (or a custom name) installs a small shim in `~/.local/bin`.
|
|
30
|
+
2. **Side logo** — show the animated logo next to the menu, or keep it compact.
|
|
31
|
+
|
|
32
|
+
Your choices are saved to the [config](#config). Re-run the wizard anytime:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
summon setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> If `~/.local/bin` isn't on your `PATH`, the wizard tells you — add it so your new command name resolves:
|
|
39
|
+
> ```sh
|
|
40
|
+
> export PATH="$HOME/.local/bin:$PATH" # add to ~/.zshrc or ~/.bashrc
|
|
41
|
+
> ```
|
|
42
|
+
|
|
43
|
+
## Daily use
|
|
44
|
+
|
|
45
|
+
Open the picker and launch a tool:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
cli # or: summon
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Move** — `↑/↓`, `j/k`, or `1`–`9`
|
|
52
|
+
- **Launch** — `Enter`
|
|
53
|
+
- **Quit** — `Esc`
|
|
54
|
+
|
|
55
|
+
Missing tools show dimmed. The side logo hides automatically on small terminals.
|
|
56
|
+
|
|
57
|
+
Pass arguments straight through to the launched tool with `--`:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
cli -- --version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Customize
|
|
21
64
|
|
|
22
|
-
|
|
65
|
+
Add your own CLI with an interactive form (one field at a time — command, name, subtitle):
|
|
23
66
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
67
|
+
```sh
|
|
68
|
+
cli add
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<img src="assets/add-form.png" alt="Filling in the add form" width="640">
|
|
73
|
+
<br>
|
|
74
|
+
<em>Fill each field, then <code>Enter</code> on the last one to save…</em>
|
|
75
|
+
<br><br>
|
|
76
|
+
<img src="assets/add-result.png" alt="The new shortcut in the picker" width="640">
|
|
77
|
+
<br>
|
|
78
|
+
<em>…and it shows up in the picker.</em>
|
|
79
|
+
</p>
|
|
80
|
+
|
|
81
|
+
Or add one in a single line:
|
|
29
82
|
|
|
30
|
-
|
|
83
|
+
```sh
|
|
84
|
+
cli --add grok "Grok Build" # runs `grok`, shows as "Grok Build"
|
|
85
|
+
cli --remove grok # remove it again
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Set the order tools appear in, or where the cursor starts:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
cli reorder # arrange the list
|
|
92
|
+
cli default claude # open with the cursor on Claude
|
|
93
|
+
cli default off # clear it
|
|
94
|
+
```
|
|
31
95
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
96
|
+
Toggle the side logo without re-running setup (remembered):
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
cli --logo
|
|
100
|
+
cli --no-logo
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Install an extra command name for the launcher at any time:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
cli alias zap # now `zap` opens it too
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Commands & flags
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
cli open the picker
|
|
113
|
+
cli reorder set the order tools appear in
|
|
114
|
+
cli default [tool] start the cursor on <tool> (off clears, no arg = pick)
|
|
115
|
+
cli alias <name> install another shell command name for the launcher
|
|
116
|
+
cli add add a custom CLI via an interactive form
|
|
117
|
+
cli setup re-run the first-run setup (command name + logo)
|
|
118
|
+
cli help show help
|
|
119
|
+
|
|
120
|
+
--add [cmd] [label] add a custom CLI (no args opens the form)
|
|
121
|
+
--remove <id> remove a custom CLI
|
|
122
|
+
--logo / --no-logo show / hide the side logo (remembered)
|
|
123
|
+
-- <args...> pass everything after -- to the launched tool
|
|
124
|
+
```
|
|
40
125
|
|
|
41
126
|
## Config
|
|
42
127
|
|
|
43
|
-
|
|
128
|
+
```text
|
|
129
|
+
~/.config/summon-cli/config.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Holds your order, default tool, custom tools, logo preference, and the setup flag. Safe to delete — the next run rebuilds it and re-runs setup.
|
|
44
133
|
|
|
45
134
|
## Requirements
|
|
46
135
|
|
|
47
|
-
Node 18+, a TrueColor terminal, the target CLIs on PATH
|
|
136
|
+
Node 18+, a TrueColor terminal, and the target CLIs on your `PATH`.
|
|
48
137
|
|
|
49
138
|
## Trademarks
|
|
50
139
|
|
package/bin/cli.mjs
CHANGED
|
@@ -8,12 +8,19 @@ import {join} from 'node:path';
|
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import {render} from 'ink';
|
|
11
|
-
import {App, ReorderApp, renderSnapshot, tools, orderTools} from '../src/app.mjs';
|
|
11
|
+
import {App, ReorderApp, SetupApp, AddApp, renderSnapshot, tools, orderTools} from '../src/app.mjs';
|
|
12
12
|
import {loadConfig, saveConfig, configLocation} from '../src/config.mjs';
|
|
13
13
|
|
|
14
14
|
const PROG = process.env.CLI_LEVEL_NAME || 'summon';
|
|
15
15
|
const SCRIPT = fileURLToPath(import.meta.url);
|
|
16
16
|
|
|
17
|
+
const ALIAS_OPTIONS = [
|
|
18
|
+
{name: 'summon', hint: 'the original name'},
|
|
19
|
+
{name: 'cli', hint: 'short', default: true},
|
|
20
|
+
{kind: 'custom', label: 'Custom…', hint: 'type your own'},
|
|
21
|
+
{kind: 'skip', label: 'Skip', hint: 'set one up later'}
|
|
22
|
+
];
|
|
23
|
+
|
|
17
24
|
const passthroughIndex = process.argv.indexOf('--');
|
|
18
25
|
const forwardedArgs = passthroughIndex === -1 ? [] : process.argv.slice(passthroughIndex + 1);
|
|
19
26
|
const rawArgs = passthroughIndex === -1 ? process.argv.slice(2) : process.argv.slice(2, passthroughIndex);
|
|
@@ -32,6 +39,7 @@ if (flags.has('--no-logo')) {
|
|
|
32
39
|
saveConfig({logo: true});
|
|
33
40
|
}
|
|
34
41
|
const items = orderTools(config.order, config.customTools);
|
|
42
|
+
const canRenderTui = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== 'dumb');
|
|
35
43
|
|
|
36
44
|
if (process.env.CLI_LEVEL_SNAPSHOT === '1') {
|
|
37
45
|
process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0), items));
|
|
@@ -44,7 +52,11 @@ if (flags.has('--help') || flags.has('-h') || command === 'help') {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
if (flags.has('--add')) {
|
|
47
|
-
|
|
55
|
+
if (args[0]) {
|
|
56
|
+
addCustomTool(args[0], args[1]);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
await runAddForm();
|
|
48
60
|
process.exit(0);
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -53,8 +65,6 @@ if (flags.has('--remove') || flags.has('--rm')) {
|
|
|
53
65
|
process.exit(0);
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
const canRenderTui = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== 'dumb');
|
|
57
|
-
|
|
58
68
|
switch (command) {
|
|
59
69
|
case 'reorder':
|
|
60
70
|
await runReorder();
|
|
@@ -65,6 +75,12 @@ switch (command) {
|
|
|
65
75
|
case 'alias':
|
|
66
76
|
runAlias(args[1]);
|
|
67
77
|
break;
|
|
78
|
+
case 'add':
|
|
79
|
+
await runAddForm();
|
|
80
|
+
break;
|
|
81
|
+
case 'setup':
|
|
82
|
+
await runSetupCommand();
|
|
83
|
+
break;
|
|
68
84
|
case undefined:
|
|
69
85
|
await runMenu();
|
|
70
86
|
break;
|
|
@@ -80,6 +96,8 @@ async function runMenu() {
|
|
|
80
96
|
process.exit(2);
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
await maybeFirstRunSetup();
|
|
100
|
+
|
|
83
101
|
clearScreen();
|
|
84
102
|
const selected = await chooseTool();
|
|
85
103
|
if (!selected) {
|
|
@@ -146,7 +164,32 @@ async function runDefault(target) {
|
|
|
146
164
|
process.stdout.write(`Default set to ${selected.label}. The menu now opens with the cursor on it.\n`);
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
function
|
|
167
|
+
async function runAddForm() {
|
|
168
|
+
requireTui('add');
|
|
169
|
+
clearScreen();
|
|
170
|
+
const result = await new Promise(resolve => {
|
|
171
|
+
let instance;
|
|
172
|
+
instance = render(React.createElement(AddApp, {
|
|
173
|
+
onSubmit: data => {
|
|
174
|
+
instance.unmount();
|
|
175
|
+
resolve(data);
|
|
176
|
+
},
|
|
177
|
+
onCancel: () => {
|
|
178
|
+
instance.unmount();
|
|
179
|
+
resolve(null);
|
|
180
|
+
}
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!result) {
|
|
185
|
+
process.stdout.write('Add cancelled.\n');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
addCustomTool(result.command, result.label, result.hint);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addCustomTool(name, label, hint) {
|
|
150
193
|
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
151
194
|
process.stderr.write(`${PROG}: usage: ${PROG} --add <command> [label]\n`);
|
|
152
195
|
process.exit(2);
|
|
@@ -165,8 +208,8 @@ function runAdd(name, label) {
|
|
|
165
208
|
}
|
|
166
209
|
|
|
167
210
|
const labelText = label || (name.charAt(0).toUpperCase() + name.slice(1));
|
|
168
|
-
const
|
|
169
|
-
saveConfig({customTools:
|
|
211
|
+
const entry = {id, label: labelText, command: name, hint: hint || 'Custom provider'};
|
|
212
|
+
saveConfig({customTools: [...current, entry]});
|
|
170
213
|
process.stdout.write(`Added '${labelText}' (command: ${name}) to the menu.\n`);
|
|
171
214
|
}
|
|
172
215
|
|
|
@@ -188,6 +231,73 @@ function runRemove(name) {
|
|
|
188
231
|
process.stdout.write(`Removed '${id}'.\n`);
|
|
189
232
|
}
|
|
190
233
|
|
|
234
|
+
function runSetup() {
|
|
235
|
+
return new Promise(resolve => {
|
|
236
|
+
let instance;
|
|
237
|
+
instance = render(React.createElement(SetupApp, {
|
|
238
|
+
aliasOptions: ALIAS_OPTIONS,
|
|
239
|
+
onComplete: result => {
|
|
240
|
+
instance.unmount();
|
|
241
|
+
resolve(result);
|
|
242
|
+
},
|
|
243
|
+
onCancel: () => {
|
|
244
|
+
instance.unmount();
|
|
245
|
+
resolve(null);
|
|
246
|
+
}
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function maybeFirstRunSetup() {
|
|
252
|
+
// Already launched through an installed alias shim — nothing to set up.
|
|
253
|
+
if (process.env.CLI_LEVEL_NAME) {
|
|
254
|
+
saveConfig({setupDone: true});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (loadConfig().setupDone) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = await runSetup();
|
|
262
|
+
if (!result) {
|
|
263
|
+
// Bailed out without finishing — leave setupDone false so we ask again next launch.
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
saveConfig({setupDone: true});
|
|
267
|
+
|
|
268
|
+
// Installing a brand-new alias prints instructions — stop before the menu so they stay visible.
|
|
269
|
+
if (applySetup(result)) {
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function runSetupCommand() {
|
|
275
|
+
requireTui('setup');
|
|
276
|
+
clearScreen();
|
|
277
|
+
const result = await runSetup();
|
|
278
|
+
if (!result) {
|
|
279
|
+
process.stdout.write('Setup cancelled.\n');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
saveConfig({setupDone: true});
|
|
283
|
+
applySetup(result);
|
|
284
|
+
process.stdout.write('Setup saved.\n');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Persist the wizard's choices. Returns true if a new alias shim was installed.
|
|
288
|
+
function applySetup(result) {
|
|
289
|
+
saveConfig({logo: result.logo});
|
|
290
|
+
logo = result.logo;
|
|
291
|
+
|
|
292
|
+
const chosen = result.alias;
|
|
293
|
+
if (chosen && chosen.name && !commandExists(chosen.name)) {
|
|
294
|
+
runAlias(chosen.name);
|
|
295
|
+
process.stdout.write(`\nStart it with: ${chosen.name}\n`);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
191
301
|
function runAlias(name) {
|
|
192
302
|
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
193
303
|
process.stderr.write(`${PROG}: usage: ${PROG} alias <name>\n`);
|
|
@@ -292,10 +402,12 @@ Commands:
|
|
|
292
402
|
reorder Set the order tools appear in
|
|
293
403
|
default [tool] Start the cursor on <tool>; no tool = pick one; 'off' clears
|
|
294
404
|
alias <name> Install a second shell command name for this launcher
|
|
405
|
+
add Add a custom CLI via an interactive form
|
|
406
|
+
setup Re-run the first-run setup (command name + logo)
|
|
295
407
|
help Show this help
|
|
296
408
|
|
|
297
409
|
Options:
|
|
298
|
-
--add
|
|
410
|
+
--add [cmd] [lbl] Add a custom CLI (no args opens the form; e.g. --add grok "Grok Build")
|
|
299
411
|
--remove <id> Remove a custom CLI from the menu
|
|
300
412
|
--no-logo Hide the side logo and remember it
|
|
301
413
|
--logo Show the side logo and remember it
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summon-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Summon your AI CLI. Terminal launcher for Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, and opencode CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"summon": "
|
|
7
|
+
"summon": "bin/cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/app.mjs
CHANGED
|
@@ -12,13 +12,6 @@ const dimGray = '#3f4348';
|
|
|
12
12
|
const white = '#ffffff';
|
|
13
13
|
|
|
14
14
|
export const tools = [
|
|
15
|
-
{
|
|
16
|
-
id: 'codex',
|
|
17
|
-
label: 'Codex',
|
|
18
|
-
command: 'codex',
|
|
19
|
-
hint: 'OpenAI',
|
|
20
|
-
palette: ['#4750d8', '#6b75f2', '#9aa2ff', '#cdd2ff', '#5b63e6', '#3d44c4']
|
|
21
|
-
},
|
|
22
15
|
{
|
|
23
16
|
id: 'claude',
|
|
24
17
|
label: 'Claude',
|
|
@@ -27,11 +20,11 @@ export const tools = [
|
|
|
27
20
|
palette: ['#ca7c5e', '#e3a888', '#b5654a']
|
|
28
21
|
},
|
|
29
22
|
{
|
|
30
|
-
id: '
|
|
31
|
-
label: '
|
|
32
|
-
command: '
|
|
33
|
-
hint: '
|
|
34
|
-
palette: ['#
|
|
23
|
+
id: 'codex',
|
|
24
|
+
label: 'Codex',
|
|
25
|
+
command: 'codex',
|
|
26
|
+
hint: 'OpenAI',
|
|
27
|
+
palette: ['#4750d8', '#6b75f2', '#9aa2ff', '#cdd2ff', '#5b63e6', '#3d44c4']
|
|
35
28
|
},
|
|
36
29
|
{
|
|
37
30
|
id: 'cursor',
|
|
@@ -40,6 +33,13 @@ export const tools = [
|
|
|
40
33
|
hint: 'Anysphere',
|
|
41
34
|
palette: ['#ffffff', '#111111']
|
|
42
35
|
},
|
|
36
|
+
{
|
|
37
|
+
id: 'antigravity',
|
|
38
|
+
label: 'Antigravity',
|
|
39
|
+
command: 'agy',
|
|
40
|
+
hint: 'Google',
|
|
41
|
+
palette: ['#2e88f5', '#3fb0a0', '#66b37f', '#f0883e', '#e15550', '#8d77c4', '#4f7ce0']
|
|
42
|
+
},
|
|
43
43
|
{
|
|
44
44
|
id: 'copilot',
|
|
45
45
|
label: 'Copilot',
|
|
@@ -85,22 +85,6 @@ const LOGO_DATA = {
|
|
|
85
85
|
[g('▄', '#6dc694'), g('▀', '#61c37d', '#62bad5'), g('▀', '#43aeab', '#47a8dc'), g(' '), g(' '), g(' '), g(' '), g('▀', '#4a80ea', '#3d89fb'), g('▀', '#6c73d8', '#4a81f0'), g('▄', '#6579e1')],
|
|
86
86
|
[g('▄', '#67b9f4'), g('▀', '#6bc7a3', '#64b6f6'), g('▀', '#64b6f6'), g(' '), g(' '), g(' '), g(' '), g(' '), g(' '), g('▀', '#3886fb'), g('▀', '#4881f4', '#3883f9'), g('▄', '#3d85fc')]
|
|
87
87
|
],
|
|
88
|
-
cursor: [
|
|
89
|
-
shade(' ▓███▓ '),
|
|
90
|
-
shade(' ▒█████████▒ '),
|
|
91
|
-
shade(' ░███████████████░ '),
|
|
92
|
-
shade(' ███████████████████ '),
|
|
93
|
-
shade('░███░ ▒█░'),
|
|
94
|
-
shade('░█████▓ ░██░'),
|
|
95
|
-
shade('░████████▒ ░███░'),
|
|
96
|
-
shade('░█████████▓ ████░'),
|
|
97
|
-
shade('░█████████▓ █████░'),
|
|
98
|
-
shade('░█████████▓ ██████░'),
|
|
99
|
-
shade(' █████████▓ ███████ '),
|
|
100
|
-
shade(' ░███████▓ ██████░ '),
|
|
101
|
-
shade(' ▒████▓▓███▒ '),
|
|
102
|
-
shade(' ▓███▓ ')
|
|
103
|
-
],
|
|
104
88
|
copilot: [
|
|
105
89
|
solid('╭─╮╭─╮', '#99dbdf'),
|
|
106
90
|
solid('╰─╯╰─╯', '#99dbdf'),
|
|
@@ -165,6 +149,7 @@ export function App({items = tools, logo = false, initialId = null, onSelect, on
|
|
|
165
149
|
}, [items]);
|
|
166
150
|
|
|
167
151
|
const width = stdout?.columns || 80;
|
|
152
|
+
const height = stdout?.rows || 24;
|
|
168
153
|
|
|
169
154
|
useEffect(() => {
|
|
170
155
|
const timer = setInterval(() => setTick(value => value + 1), 110);
|
|
@@ -221,7 +206,8 @@ export function App({items = tools, logo = false, initialId = null, onSelect, on
|
|
|
221
206
|
}
|
|
222
207
|
});
|
|
223
208
|
|
|
224
|
-
|
|
209
|
+
// Compact mode: skip the logo when the terminal can't comfortably hold it.
|
|
210
|
+
const showLogo = logo && width >= 74 && height >= LOGO_H + 6;
|
|
225
211
|
|
|
226
212
|
return (
|
|
227
213
|
React.createElement(Box, {flexDirection: 'column', paddingX: 2, paddingY: 1},
|
|
@@ -339,6 +325,267 @@ export function ReorderApp({items = tools, onDone, onCancel}) {
|
|
|
339
325
|
);
|
|
340
326
|
}
|
|
341
327
|
|
|
328
|
+
// First-run wizard: step 1 picks the command name to install, step 2 toggles the logo.
|
|
329
|
+
export function SetupApp({aliasOptions, onComplete, onCancel}) {
|
|
330
|
+
const {exit} = useApp();
|
|
331
|
+
const [phase, setPhase] = useState('alias');
|
|
332
|
+
const [active, setActive] = useState(() => {
|
|
333
|
+
const index = aliasOptions.findIndex(option => option.default);
|
|
334
|
+
return index >= 0 ? index : 0;
|
|
335
|
+
});
|
|
336
|
+
const [text, setText] = useState('');
|
|
337
|
+
const [error, setError] = useState(false);
|
|
338
|
+
const [alias, setAlias] = useState(null);
|
|
339
|
+
const [logoActive, setLogoActive] = useState(0);
|
|
340
|
+
|
|
341
|
+
useInput((input, key) => {
|
|
342
|
+
if (key.ctrl && input === 'c') {
|
|
343
|
+
exit();
|
|
344
|
+
onCancel();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (phase === 'custom') {
|
|
349
|
+
if (key.escape) {
|
|
350
|
+
setPhase('alias');
|
|
351
|
+
setText('');
|
|
352
|
+
setError(false);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (key.return) {
|
|
356
|
+
const name = text.trim();
|
|
357
|
+
if (/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
358
|
+
setAlias({name});
|
|
359
|
+
setPhase('logo');
|
|
360
|
+
} else {
|
|
361
|
+
setError(true);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (key.backspace || key.delete) {
|
|
366
|
+
setText(value => value.slice(0, -1));
|
|
367
|
+
setError(false);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (input && !key.ctrl && !key.meta) {
|
|
371
|
+
setText(value => value + input);
|
|
372
|
+
setError(false);
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (phase === 'logo') {
|
|
378
|
+
if (key.escape) {
|
|
379
|
+
setPhase('alias');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (key.upArrow || key.downArrow || input === 'j' || input === 'k') {
|
|
383
|
+
setLogoActive(value => (value + 1) % 2);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (key.return) {
|
|
387
|
+
exit();
|
|
388
|
+
onComplete({alias, logo: logoActive === 0});
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// phase === 'alias'
|
|
394
|
+
if (key.escape) {
|
|
395
|
+
setAlias({skip: true});
|
|
396
|
+
setPhase('logo');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (key.upArrow || input === 'k') {
|
|
400
|
+
setActive(value => (value - 1 + aliasOptions.length) % aliasOptions.length);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (key.downArrow || input === 'j') {
|
|
404
|
+
setActive(value => (value + 1) % aliasOptions.length);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (key.return) {
|
|
408
|
+
const option = aliasOptions[active];
|
|
409
|
+
if (option.kind === 'custom') {
|
|
410
|
+
setPhase('custom');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (option.kind === 'skip') {
|
|
414
|
+
setAlias({skip: true});
|
|
415
|
+
setPhase('logo');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
setAlias({name: option.name});
|
|
419
|
+
setPhase('logo');
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const frame = children => React.createElement(Box, {flexDirection: 'column', paddingX: 2, paddingY: 1}, children);
|
|
424
|
+
|
|
425
|
+
if (phase === 'custom') {
|
|
426
|
+
return frame([
|
|
427
|
+
React.createElement(Box, {key: 'h', marginBottom: 1},
|
|
428
|
+
React.createElement(Text, {bold: true, color: white}, 'Custom command name')
|
|
429
|
+
),
|
|
430
|
+
React.createElement(Box, {key: 'i'},
|
|
431
|
+
React.createElement(Text, {color: gray}, 'name '),
|
|
432
|
+
React.createElement(Text, {bold: true, color: white}, '› '),
|
|
433
|
+
React.createElement(Text, {color: white}, text),
|
|
434
|
+
React.createElement(Text, {color: dimGray}, '▌')
|
|
435
|
+
),
|
|
436
|
+
React.createElement(Box, {key: 'f', marginTop: 1},
|
|
437
|
+
React.createElement(Text, {color: error ? '#d96570' : dimGray},
|
|
438
|
+
error ? 'letters, digits, . _ - only' : 'enter confirm · esc back')
|
|
439
|
+
)
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (phase === 'logo') {
|
|
444
|
+
const logoOpts = [
|
|
445
|
+
{label: 'Show logo', hint: 'animated art beside the menu'},
|
|
446
|
+
{label: 'Hide logo', hint: 'compact, just the list'}
|
|
447
|
+
];
|
|
448
|
+
return frame([
|
|
449
|
+
React.createElement(Box, {key: 'h', marginBottom: 1, flexDirection: 'column'},
|
|
450
|
+
React.createElement(Text, {bold: true, color: white}, 'Side logo'),
|
|
451
|
+
React.createElement(Text, {color: dimGray}, 'step 2 of 2')
|
|
452
|
+
),
|
|
453
|
+
React.createElement(Box, {key: 'l', flexDirection: 'column'},
|
|
454
|
+
logoOpts.map((opt, index) => {
|
|
455
|
+
const isActive = index === logoActive;
|
|
456
|
+
return React.createElement(Box, {key: opt.label, height: 1},
|
|
457
|
+
React.createElement(Box, {width: 2},
|
|
458
|
+
React.createElement(Text, {bold: isActive, color: isActive ? white : dimGray}, isActive ? '›' : ' ')
|
|
459
|
+
),
|
|
460
|
+
React.createElement(Box, {width: 12},
|
|
461
|
+
React.createElement(Text, {bold: isActive, color: isActive ? white : gray}, opt.label)
|
|
462
|
+
),
|
|
463
|
+
React.createElement(Text, {color: dimGray}, opt.hint)
|
|
464
|
+
);
|
|
465
|
+
})
|
|
466
|
+
),
|
|
467
|
+
React.createElement(Box, {key: 'f', marginTop: 1},
|
|
468
|
+
React.createElement(Text, {color: dimGray}, 'enter pick · ↑/↓ move · esc back')
|
|
469
|
+
)
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// phase === 'alias'
|
|
474
|
+
return frame([
|
|
475
|
+
React.createElement(Box, {key: 'h', marginBottom: 1, flexDirection: 'column'},
|
|
476
|
+
React.createElement(Text, {bold: true, color: white}, 'Pick the command you’ll type'),
|
|
477
|
+
React.createElement(Text, {color: dimGray}, 'step 1 of 2 · installs a short name for this launcher')
|
|
478
|
+
),
|
|
479
|
+
React.createElement(Box, {key: 'o', flexDirection: 'column'},
|
|
480
|
+
aliasOptions.map((option, index) => {
|
|
481
|
+
const isActive = index === active;
|
|
482
|
+
const label = option.name || option.label;
|
|
483
|
+
return React.createElement(Box, {key: label, height: 1},
|
|
484
|
+
React.createElement(Box, {width: 2},
|
|
485
|
+
React.createElement(Text, {bold: isActive, color: isActive ? white : dimGray}, isActive ? '›' : ' ')
|
|
486
|
+
),
|
|
487
|
+
React.createElement(Box, {width: 14},
|
|
488
|
+
React.createElement(Text, {bold: isActive, color: isActive ? white : gray}, label)
|
|
489
|
+
),
|
|
490
|
+
React.createElement(Text, {color: dimGray}, option.hint || '')
|
|
491
|
+
);
|
|
492
|
+
})
|
|
493
|
+
),
|
|
494
|
+
React.createElement(Box, {key: 'f', marginTop: 1},
|
|
495
|
+
React.createElement(Text, {color: dimGray}, 'enter pick · ↑/↓ move · esc skip')
|
|
496
|
+
)
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Interactive form to add a custom shortcut: one editable field per value.
|
|
501
|
+
export function AddApp({onSubmit, onCancel}) {
|
|
502
|
+
const {exit} = useApp();
|
|
503
|
+
const fields = [
|
|
504
|
+
{key: 'command', label: 'Command', placeholder: 'binary to run, e.g. grok'},
|
|
505
|
+
{key: 'label', label: 'Name', placeholder: 'shown in the menu (optional)'},
|
|
506
|
+
{key: 'hint', label: 'Subtitle', placeholder: 'right-side text (optional)'}
|
|
507
|
+
];
|
|
508
|
+
const [values, setValues] = useState({command: '', label: '', hint: ''});
|
|
509
|
+
const [active, setActive] = useState(0);
|
|
510
|
+
const [error, setError] = useState('');
|
|
511
|
+
|
|
512
|
+
const submit = () => {
|
|
513
|
+
const command = values.command.trim();
|
|
514
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(command)) {
|
|
515
|
+
setError('command: letters, digits, . _ - only');
|
|
516
|
+
setActive(0);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
exit();
|
|
520
|
+
onSubmit({command, label: values.label.trim(), hint: values.hint.trim()});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
useInput((input, key) => {
|
|
524
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
525
|
+
exit();
|
|
526
|
+
onCancel();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (key.tab || key.downArrow) {
|
|
530
|
+
setActive(value => (value + 1) % fields.length);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (key.upArrow) {
|
|
534
|
+
setActive(value => (value - 1 + fields.length) % fields.length);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (key.return) {
|
|
538
|
+
if (active < fields.length - 1) {
|
|
539
|
+
setActive(active + 1);
|
|
540
|
+
} else {
|
|
541
|
+
submit();
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const fieldKey = fields[active].key;
|
|
546
|
+
if (key.backspace || key.delete) {
|
|
547
|
+
setValues(value => ({...value, [fieldKey]: value[fieldKey].slice(0, -1)}));
|
|
548
|
+
setError('');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (input && !key.ctrl && !key.meta) {
|
|
552
|
+
setValues(value => ({...value, [fieldKey]: value[fieldKey] + input}));
|
|
553
|
+
setError('');
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return React.createElement(Box, {flexDirection: 'column', paddingX: 2, paddingY: 1},
|
|
558
|
+
React.createElement(Box, {key: 'h', marginBottom: 1, flexDirection: 'column'},
|
|
559
|
+
React.createElement(Text, {bold: true, color: white}, 'Add a shortcut'),
|
|
560
|
+
React.createElement(Text, {color: dimGray}, 'tab/↑↓ move · enter next · enter on last saves · esc cancel')
|
|
561
|
+
),
|
|
562
|
+
React.createElement(Box, {key: 'fields', flexDirection: 'column'},
|
|
563
|
+
fields.map((field, index) => {
|
|
564
|
+
const isActive = index === active;
|
|
565
|
+
const value = values[field.key];
|
|
566
|
+
return React.createElement(Box, {key: field.key, height: 1},
|
|
567
|
+
React.createElement(Box, {width: 2},
|
|
568
|
+
React.createElement(Text, {bold: isActive, color: isActive ? white : dimGray}, isActive ? '›' : ' ')
|
|
569
|
+
),
|
|
570
|
+
React.createElement(Box, {width: 10},
|
|
571
|
+
React.createElement(Text, {color: isActive ? white : gray}, field.label)
|
|
572
|
+
),
|
|
573
|
+
isActive
|
|
574
|
+
? React.createElement(Box, null,
|
|
575
|
+
React.createElement(Text, {color: white}, value),
|
|
576
|
+
React.createElement(Text, {color: dimGray}, '▌'),
|
|
577
|
+
value ? null : React.createElement(Text, {color: dimGray}, ` ${field.placeholder}`)
|
|
578
|
+
)
|
|
579
|
+
: React.createElement(Text, {color: value ? gray : dimGray}, value || field.placeholder)
|
|
580
|
+
);
|
|
581
|
+
})
|
|
582
|
+
),
|
|
583
|
+
React.createElement(Box, {key: 'f', marginTop: 1},
|
|
584
|
+
React.createElement(Text, {color: error ? '#d96570' : dimGray}, error || 'command is required')
|
|
585
|
+
)
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
342
589
|
function Header({title, tick}) {
|
|
343
590
|
return (
|
|
344
591
|
React.createElement(Box, {marginBottom: 1},
|