summon-cli 0.2.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 +13 -4
- package/bin/cli.mjs +56 -8
- package/package.json +1 -1
- package/src/app.mjs +32 -13
- 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
|
|
|
@@ -24,10 +24,19 @@ Move with arrows or `j/k` or `1-9`. `Enter` launches, `Esc` quits.
|
|
|
24
24
|
- `summon` open the picker
|
|
25
25
|
- `summon reorder` set the order
|
|
26
26
|
- `summon default <tool>` start the cursor on it (`off` clears, no arg = pick)
|
|
27
|
-
- `summon alias <name>`
|
|
27
|
+
- `summon alias <name>` install a second shell command name for the launcher
|
|
28
28
|
- `summon help`
|
|
29
29
|
|
|
30
|
-
Flags
|
|
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.
|
|
31
40
|
|
|
32
41
|
## Config
|
|
33
42
|
|
package/bin/cli.mjs
CHANGED
|
@@ -31,10 +31,10 @@ if (flags.has('--no-logo')) {
|
|
|
31
31
|
logo = true;
|
|
32
32
|
saveConfig({logo: true});
|
|
33
33
|
}
|
|
34
|
-
const items = orderTools(config.order);
|
|
34
|
+
const items = orderTools(config.order, config.customTools);
|
|
35
35
|
|
|
36
36
|
if (process.env.CLI_LEVEL_SNAPSHOT === '1') {
|
|
37
|
-
process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0)));
|
|
37
|
+
process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0), items));
|
|
38
38
|
process.exit(0);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -44,7 +44,12 @@ if (flags.has('--help') || flags.has('-h') || command === 'help') {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (flags.has('--add')) {
|
|
47
|
-
|
|
47
|
+
runAdd(args[0], args[1]);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (flags.has('--remove') || flags.has('--rm')) {
|
|
52
|
+
runRemove(args[0]);
|
|
48
53
|
process.exit(0);
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -70,7 +75,7 @@ switch (command) {
|
|
|
70
75
|
|
|
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);
|
|
@@ -140,9 +146,51 @@ async function runDefault(target) {
|
|
|
140
146
|
process.stdout.write(`Default set to ${selected.label}. The menu now opens with the cursor on it.\n`);
|
|
141
147
|
}
|
|
142
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`);
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
function runAlias(name) {
|
|
144
192
|
if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
145
|
-
process.stderr.write(`${PROG}: usage: ${PROG} alias <name
|
|
193
|
+
process.stderr.write(`${PROG}: usage: ${PROG} alias <name>\n`);
|
|
146
194
|
process.exit(2);
|
|
147
195
|
}
|
|
148
196
|
|
|
@@ -243,12 +291,12 @@ Commands:
|
|
|
243
291
|
(none) Open the picker
|
|
244
292
|
reorder Set the order tools appear in
|
|
245
293
|
default [tool] Start the cursor on <tool>; no tool = pick one; 'off' clears
|
|
246
|
-
alias <name> Install a second command name for this launcher
|
|
247
|
-
(shortcut: ${PROG} --add <name>)
|
|
294
|
+
alias <name> Install a second shell command name for this launcher
|
|
248
295
|
help Show this help
|
|
249
296
|
|
|
250
297
|
Options:
|
|
251
|
-
--add <
|
|
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
|
|
252
300
|
--no-logo Hide the side logo and remember it
|
|
253
301
|
--logo Show the side logo and remember it
|
|
254
302
|
|
package/package.json
CHANGED
package/src/app.mjs
CHANGED
|
@@ -249,15 +249,15 @@ export function App({items = tools, logo = false, initialId = null, onSelect, on
|
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
export function ReorderApp({onDone, onCancel}) {
|
|
252
|
+
export function ReorderApp({items = tools, onDone, onCancel}) {
|
|
253
253
|
const {exit} = useApp();
|
|
254
254
|
const [order, setOrder] = useState([]);
|
|
255
255
|
const [active, setActive] = useState(0);
|
|
256
256
|
|
|
257
257
|
const seekUnpicked = (from, dir, taken) => {
|
|
258
|
-
for (let step = 0; step <=
|
|
259
|
-
const index = (from + dir * step +
|
|
260
|
-
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)) {
|
|
261
261
|
return index;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
@@ -290,13 +290,13 @@ export function ReorderApp({onDone, onCancel}) {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
if (key.return) {
|
|
293
|
-
const id =
|
|
293
|
+
const id = items[active].id;
|
|
294
294
|
if (taken.has(id)) {
|
|
295
295
|
return;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
const next = [...order, id];
|
|
299
|
-
if (next.length ===
|
|
299
|
+
if (next.length === items.length) {
|
|
300
300
|
exit();
|
|
301
301
|
onDone(next);
|
|
302
302
|
return;
|
|
@@ -314,7 +314,7 @@ export function ReorderApp({onDone, onCancel}) {
|
|
|
314
314
|
React.createElement(Text, {color: dimGray}, ' pick first to last')
|
|
315
315
|
),
|
|
316
316
|
React.createElement(Box, {flexDirection: 'column', gap: 0},
|
|
317
|
-
|
|
317
|
+
items.map((tool, index) => {
|
|
318
318
|
const pos = order.indexOf(tool.id);
|
|
319
319
|
const picked = pos >= 0;
|
|
320
320
|
const isActive = index === active && !picked;
|
|
@@ -509,9 +509,10 @@ function renderPlainLine(tool, index, activeIndex) {
|
|
|
509
509
|
return `${arrow} ${label.padEnd(16)} ${tool.hint}`;
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
export function renderSnapshot(activeIndex = 0) {
|
|
513
|
-
const
|
|
514
|
-
|
|
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`;
|
|
515
516
|
}
|
|
516
517
|
|
|
517
518
|
function commandExists(command) {
|
|
@@ -545,8 +546,26 @@ function nextAvailable(current, availability, items) {
|
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
// Reorder canonical tools by an array of ids; unknown ids ignored, missing appended.
|
|
548
|
-
export function
|
|
549
|
-
|
|
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]));
|
|
550
569
|
const result = [];
|
|
551
570
|
for (const id of order) {
|
|
552
571
|
if (byId.has(id)) {
|
|
@@ -554,7 +573,7 @@ export function orderTools(order = []) {
|
|
|
554
573
|
byId.delete(id);
|
|
555
574
|
}
|
|
556
575
|
}
|
|
557
|
-
for (const tool of
|
|
576
|
+
for (const tool of all) {
|
|
558
577
|
if (byId.has(tool.id)) {
|
|
559
578
|
result.push(tool);
|
|
560
579
|
}
|