summon-cli 0.1.0 → 0.3.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 +15 -7
- package/bin/cli.mjs +77 -27
- package/package.json +1 -1
- package/src/app.mjs +37 -15
- package/src/config.mjs +2 -1
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
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.2.0).
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -21,14 +21,22 @@ Move with arrows or `j/k` or `1-9`. `Enter` launches, `Esc` quits.
|
|
|
21
21
|
|
|
22
22
|
## Commands
|
|
23
23
|
|
|
24
|
-
- `summon` open the picker
|
|
25
|
-
- `summon menu` always open the picker
|
|
24
|
+
- `summon` open the picker
|
|
26
25
|
- `summon reorder` set the order
|
|
27
|
-
- `summon default <tool>`
|
|
28
|
-
- `summon alias <name>`
|
|
26
|
+
- `summon default <tool>` start the cursor on it (`off` clears, no arg = pick)
|
|
27
|
+
- `summon alias <name>` install a second shell command name for the launcher
|
|
29
28
|
- `summon help`
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
## Flags
|
|
31
|
+
|
|
32
|
+
- `--add <cmd> [label]` add a custom CLI to the menu. Example:
|
|
33
|
+
```sh
|
|
34
|
+
summon --add grok "Grok Build"
|
|
35
|
+
```
|
|
36
|
+
Adds an entry that runs `grok` and shows as `Grok Build` with hint `Custom provider`.
|
|
37
|
+
- `--remove <id>` remove a custom CLI from the menu.
|
|
38
|
+
- `--no-logo` / `--logo` toggle the side logo (remembered).
|
|
39
|
+
- Args after `--` go to the launched tool.
|
|
32
40
|
|
|
33
41
|
## Config
|
|
34
42
|
|
package/bin/cli.mjs
CHANGED
|
@@ -23,11 +23,18 @@ const args = rawArgs.filter(arg => !arg.startsWith('-'));
|
|
|
23
23
|
const command = args[0];
|
|
24
24
|
|
|
25
25
|
const config = loadConfig();
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
let logo = config.logo;
|
|
27
|
+
if (flags.has('--no-logo')) {
|
|
28
|
+
logo = false;
|
|
29
|
+
saveConfig({logo: false});
|
|
30
|
+
} else if (flags.has('--logo')) {
|
|
31
|
+
logo = true;
|
|
32
|
+
saveConfig({logo: true});
|
|
33
|
+
}
|
|
34
|
+
const items = orderTools(config.order, config.customTools);
|
|
28
35
|
|
|
29
36
|
if (process.env.CLI_LEVEL_SNAPSHOT === '1') {
|
|
30
|
-
process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0)));
|
|
37
|
+
process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0), items));
|
|
31
38
|
process.exit(0);
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -36,6 +43,16 @@ if (flags.has('--help') || flags.has('-h') || command === 'help') {
|
|
|
36
43
|
process.exit(0);
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
if (flags.has('--add')) {
|
|
47
|
+
runAdd(args[0], args[1]);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (flags.has('--remove') || flags.has('--rm')) {
|
|
52
|
+
runRemove(args[0]);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
const canRenderTui = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== 'dumb');
|
|
40
57
|
|
|
41
58
|
switch (command) {
|
|
@@ -48,29 +65,17 @@ switch (command) {
|
|
|
48
65
|
case 'alias':
|
|
49
66
|
runAlias(args[1]);
|
|
50
67
|
break;
|
|
51
|
-
case 'menu':
|
|
52
|
-
await runMenu({forceMenu: true});
|
|
53
|
-
break;
|
|
54
68
|
case undefined:
|
|
55
|
-
await runMenu(
|
|
69
|
+
await runMenu();
|
|
56
70
|
break;
|
|
57
71
|
default:
|
|
58
72
|
process.stderr.write(`${PROG}: unknown command '${command}'. Try '${PROG} --help'.\n`);
|
|
59
73
|
process.exit(2);
|
|
60
74
|
}
|
|
61
75
|
|
|
62
|
-
async function runMenu(
|
|
63
|
-
if (!forceMenu && config.default) {
|
|
64
|
-
const tool = tools.find(item => item.id === config.default);
|
|
65
|
-
if (tool && commandExists(tool.command)) {
|
|
66
|
-
process.exitCode = await runCommand(tool.command, forwardedArgs);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
process.stderr.write(`${PROG}: default '${config.default}' unavailable, opening menu.\n`);
|
|
70
|
-
}
|
|
71
|
-
|
|
76
|
+
async function runMenu() {
|
|
72
77
|
if (!canRenderTui) {
|
|
73
|
-
process.stdout.write(renderSnapshot(0));
|
|
78
|
+
process.stdout.write(renderSnapshot(0, items));
|
|
74
79
|
process.stderr.write(`${PROG}: interactive terminal required for selection.\n`);
|
|
75
80
|
process.exit(2);
|
|
76
81
|
}
|
|
@@ -91,6 +96,7 @@ async function runReorder() {
|
|
|
91
96
|
const order = await new Promise(resolve => {
|
|
92
97
|
let instance;
|
|
93
98
|
instance = render(React.createElement(ReorderApp, {
|
|
99
|
+
items,
|
|
94
100
|
onCancel: () => {
|
|
95
101
|
instance.unmount();
|
|
96
102
|
resolve(null);
|
|
@@ -114,7 +120,7 @@ async function runReorder() {
|
|
|
114
120
|
async function runDefault(target) {
|
|
115
121
|
if (target === 'off' || target === 'none') {
|
|
116
122
|
saveConfig({default: null});
|
|
117
|
-
process.stdout.write(`Default cleared.
|
|
123
|
+
process.stdout.write(`Default cleared. The cursor starts on the first tool.\n`);
|
|
118
124
|
return;
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -125,7 +131,7 @@ async function runDefault(target) {
|
|
|
125
131
|
process.exit(2);
|
|
126
132
|
}
|
|
127
133
|
saveConfig({default: tool.id});
|
|
128
|
-
process.stdout.write(`Default set to ${tool.label}.
|
|
134
|
+
process.stdout.write(`Default set to ${tool.label}. The menu now opens with the cursor on it.\n`);
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -137,7 +143,49 @@ async function runDefault(target) {
|
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
145
|
saveConfig({default: selected.id});
|
|
140
|
-
process.stdout.write(`Default set to ${selected.label}.
|
|
146
|
+
process.stdout.write(`Default set to ${selected.label}. The menu now opens with the cursor on it.\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runAdd(name, label) {
|
|
150
|
+
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
151
|
+
process.stderr.write(`${PROG}: usage: ${PROG} --add <command> [label]\n`);
|
|
152
|
+
process.exit(2);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const id = name.toLowerCase();
|
|
156
|
+
if (tools.some(t => t.id === id)) {
|
|
157
|
+
process.stderr.write(`${PROG}: '${id}' is a built-in tool already.\n`);
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const current = loadConfig().customTools || [];
|
|
162
|
+
if (current.some(t => t.id === id)) {
|
|
163
|
+
process.stderr.write(`${PROG}: '${id}' is already in your list.\n`);
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const labelText = label || (name.charAt(0).toUpperCase() + name.slice(1));
|
|
168
|
+
const next = [...current, {id, label: labelText, command: name, hint: 'Custom provider'}];
|
|
169
|
+
saveConfig({customTools: next});
|
|
170
|
+
process.stdout.write(`Added '${labelText}' (command: ${name}) to the menu.\n`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runRemove(name) {
|
|
174
|
+
if (!name) {
|
|
175
|
+
process.stderr.write(`${PROG}: usage: ${PROG} --remove <id>\n`);
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const id = name.toLowerCase();
|
|
180
|
+
const current = loadConfig().customTools || [];
|
|
181
|
+
const next = current.filter(t => t.id !== id);
|
|
182
|
+
if (next.length === current.length) {
|
|
183
|
+
process.stderr.write(`${PROG}: no custom tool '${id}'. List: ${current.map(t => t.id).join(', ') || '(none)'}.\n`);
|
|
184
|
+
process.exit(2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
saveConfig({customTools: next});
|
|
188
|
+
process.stdout.write(`Removed '${id}'.\n`);
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
function runAlias(name) {
|
|
@@ -165,6 +213,7 @@ function chooseTool() {
|
|
|
165
213
|
instance = render(React.createElement(App, {
|
|
166
214
|
items,
|
|
167
215
|
logo,
|
|
216
|
+
initialId: config.default,
|
|
168
217
|
onCancel: () => {
|
|
169
218
|
instance.unmount();
|
|
170
219
|
resolve(null);
|
|
@@ -239,16 +288,17 @@ function printHelp() {
|
|
|
239
288
|
Summon your AI CLI. A terminal launcher.
|
|
240
289
|
|
|
241
290
|
Commands:
|
|
242
|
-
(none) Open the picker
|
|
243
|
-
menu Always open the picker (ignore the default)
|
|
291
|
+
(none) Open the picker
|
|
244
292
|
reorder Set the order tools appear in
|
|
245
|
-
default [tool]
|
|
246
|
-
|
|
247
|
-
alias <name> Install a second command name for this launcher
|
|
293
|
+
default [tool] Start the cursor on <tool>; no tool = pick one; 'off' clears
|
|
294
|
+
alias <name> Install a second shell command name for this launcher
|
|
248
295
|
help Show this help
|
|
249
296
|
|
|
250
297
|
Options:
|
|
251
|
-
--
|
|
298
|
+
--add <cmd> [lbl] Add a custom CLI to the menu (e.g. --add grok "Grok Build")
|
|
299
|
+
--remove <id> Remove a custom CLI from the menu
|
|
300
|
+
--no-logo Hide the side logo and remember it
|
|
301
|
+
--logo Show the side logo and remember it
|
|
252
302
|
|
|
253
303
|
Anything after -- is passed to the launched tool, e.g. ${PROG} -- --version
|
|
254
304
|
Config: ${configLocation()}
|
package/package.json
CHANGED
package/src/app.mjs
CHANGED
|
@@ -149,10 +149,13 @@ function LogoPanel({tool}) {
|
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export function App({items = tools, logo = false, onSelect, onCancel}) {
|
|
152
|
+
export function App({items = tools, logo = false, initialId = null, onSelect, onCancel}) {
|
|
153
153
|
const {exit} = useApp();
|
|
154
154
|
const {stdout} = useStdout();
|
|
155
|
-
const [active, setActive] = useState(
|
|
155
|
+
const [active, setActive] = useState(() => {
|
|
156
|
+
const idx = items.findIndex(tool => tool.id === initialId);
|
|
157
|
+
return idx >= 0 ? idx : 0;
|
|
158
|
+
});
|
|
156
159
|
const [tick, setTick] = useState(0);
|
|
157
160
|
const [spin, setSpin] = useState(0);
|
|
158
161
|
const [flash, setFlash] = useState(0);
|
|
@@ -246,15 +249,15 @@ export function App({items = tools, logo = false, onSelect, onCancel}) {
|
|
|
246
249
|
);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
export function ReorderApp({onDone, onCancel}) {
|
|
252
|
+
export function ReorderApp({items = tools, onDone, onCancel}) {
|
|
250
253
|
const {exit} = useApp();
|
|
251
254
|
const [order, setOrder] = useState([]);
|
|
252
255
|
const [active, setActive] = useState(0);
|
|
253
256
|
|
|
254
257
|
const seekUnpicked = (from, dir, taken) => {
|
|
255
|
-
for (let step = 0; step <=
|
|
256
|
-
const index = (from + dir * step +
|
|
257
|
-
if (!taken.has(
|
|
258
|
+
for (let step = 0; step <= items.length; step += 1) {
|
|
259
|
+
const index = (from + dir * step + items.length * (step + 1)) % items.length;
|
|
260
|
+
if (!taken.has(items[index].id)) {
|
|
258
261
|
return index;
|
|
259
262
|
}
|
|
260
263
|
}
|
|
@@ -287,13 +290,13 @@ export function ReorderApp({onDone, onCancel}) {
|
|
|
287
290
|
}
|
|
288
291
|
|
|
289
292
|
if (key.return) {
|
|
290
|
-
const id =
|
|
293
|
+
const id = items[active].id;
|
|
291
294
|
if (taken.has(id)) {
|
|
292
295
|
return;
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
const next = [...order, id];
|
|
296
|
-
if (next.length ===
|
|
299
|
+
if (next.length === items.length) {
|
|
297
300
|
exit();
|
|
298
301
|
onDone(next);
|
|
299
302
|
return;
|
|
@@ -311,7 +314,7 @@ export function ReorderApp({onDone, onCancel}) {
|
|
|
311
314
|
React.createElement(Text, {color: dimGray}, ' pick first to last')
|
|
312
315
|
),
|
|
313
316
|
React.createElement(Box, {flexDirection: 'column', gap: 0},
|
|
314
|
-
|
|
317
|
+
items.map((tool, index) => {
|
|
315
318
|
const pos = order.indexOf(tool.id);
|
|
316
319
|
const picked = pos >= 0;
|
|
317
320
|
const isActive = index === active && !picked;
|
|
@@ -506,9 +509,10 @@ function renderPlainLine(tool, index, activeIndex) {
|
|
|
506
509
|
return `${arrow} ${label.padEnd(16)} ${tool.hint}`;
|
|
507
510
|
}
|
|
508
511
|
|
|
509
|
-
export function renderSnapshot(activeIndex = 0) {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
+
export function renderSnapshot(activeIndex = 0, items = null) {
|
|
513
|
+
const list = items || tools;
|
|
514
|
+
const bounded = Math.max(0, Math.min(list.length - 1, activeIndex));
|
|
515
|
+
return `${list.map((tool, index) => renderPlainLine(tool, index, bounded)).join('\n')}\n`;
|
|
512
516
|
}
|
|
513
517
|
|
|
514
518
|
function commandExists(command) {
|
|
@@ -542,8 +546,26 @@ function nextAvailable(current, availability, items) {
|
|
|
542
546
|
}
|
|
543
547
|
|
|
544
548
|
// Reorder canonical tools by an array of ids; unknown ids ignored, missing appended.
|
|
545
|
-
export function
|
|
546
|
-
|
|
549
|
+
export function buildCustomTool({id, label, command, hint, palette} = {}) {
|
|
550
|
+
if (!id) return null;
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
label: label || id,
|
|
554
|
+
command: command || id,
|
|
555
|
+
hint: hint || 'Custom provider',
|
|
556
|
+
palette: palette && palette.length ? palette : ['#9aa0a6', '#cdcdcd']
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function orderTools(order = [], customTools = []) {
|
|
561
|
+
const all = [...tools];
|
|
562
|
+
for (const raw of customTools) {
|
|
563
|
+
const tool = buildCustomTool(raw);
|
|
564
|
+
if (!tool) continue;
|
|
565
|
+
if (all.some(t => t.id === tool.id)) continue;
|
|
566
|
+
all.push(tool);
|
|
567
|
+
}
|
|
568
|
+
const byId = new Map(all.map(tool => [tool.id, tool]));
|
|
547
569
|
const result = [];
|
|
548
570
|
for (const id of order) {
|
|
549
571
|
if (byId.has(id)) {
|
|
@@ -551,7 +573,7 @@ export function orderTools(order = []) {
|
|
|
551
573
|
byId.delete(id);
|
|
552
574
|
}
|
|
553
575
|
}
|
|
554
|
-
for (const tool of
|
|
576
|
+
for (const tool of all) {
|
|
555
577
|
if (byId.has(tool.id)) {
|
|
556
578
|
result.push(tool);
|
|
557
579
|
}
|