summon-cli 0.1.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/LICENSE +674 -0
- package/README.md +47 -0
- package/bin/cli.mjs +256 -0
- package/package.json +50 -0
- package/src/app.mjs +622 -0
- package/src/config.mjs +33 -0
package/src/app.mjs
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import {spawnSync} from 'node:child_process';
|
|
2
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
3
|
+
import {Box, Text, useApp, useInput, useStdout} from 'ink';
|
|
4
|
+
|
|
5
|
+
const HIDDEN_CONFIG = {
|
|
6
|
+
showHeader: false,
|
|
7
|
+
title: 'CLI-Level'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const gray = '#6f737a';
|
|
11
|
+
const dimGray = '#3f4348';
|
|
12
|
+
const white = '#ffffff';
|
|
13
|
+
|
|
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
|
+
{
|
|
23
|
+
id: 'claude',
|
|
24
|
+
label: 'Claude',
|
|
25
|
+
command: 'claude',
|
|
26
|
+
hint: 'Anthropic',
|
|
27
|
+
palette: ['#ca7c5e', '#e3a888', '#b5654a']
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'antigravity',
|
|
31
|
+
label: 'Antigravity',
|
|
32
|
+
command: 'agy',
|
|
33
|
+
hint: 'Google',
|
|
34
|
+
palette: ['#2e88f5', '#3fb0a0', '#66b37f', '#f0883e', '#e15550', '#8d77c4', '#4f7ce0']
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'cursor',
|
|
38
|
+
label: 'Cursor',
|
|
39
|
+
command: 'agent',
|
|
40
|
+
hint: 'Anysphere',
|
|
41
|
+
palette: ['#ffffff', '#111111']
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'copilot',
|
|
45
|
+
label: 'Copilot',
|
|
46
|
+
command: 'copilot',
|
|
47
|
+
hint: 'GitHub',
|
|
48
|
+
palette: ['#c88ce0', '#89bc84', '#99dbdf']
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'opencode',
|
|
52
|
+
label: 'opencode',
|
|
53
|
+
command: 'opencode',
|
|
54
|
+
hint: 'SST',
|
|
55
|
+
palette: ['#757676', '#ffffff']
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Logos lifted from each tool's own splash screen (captured via pty).
|
|
60
|
+
// Static colours; cells carry an optional bg so half-block glyphs render two tones.
|
|
61
|
+
const LOGO_W = 26;
|
|
62
|
+
const LOGO_H = 16;
|
|
63
|
+
|
|
64
|
+
const g = (ch, fg = null, bg = null) => ({ch, fg, bg});
|
|
65
|
+
const solid = (str, fg) => Array.from(str).map(ch => g(ch, ch === ' ' ? null : fg, null));
|
|
66
|
+
const SHADE = {'█': '#dcdcdc', '▓': '#b4b4b4', '▒': '#909090', '░': '#6a6a6a'};
|
|
67
|
+
const shade = str => Array.from(str).map(ch => g(ch, SHADE[ch] || null));
|
|
68
|
+
|
|
69
|
+
const LOGO_DATA = {
|
|
70
|
+
codex: [
|
|
71
|
+
solid('█▄ ', '#7e88f5'),
|
|
72
|
+
solid(' ▀█▄ ', '#7e88f5'),
|
|
73
|
+
solid(' ▄█▀ ', '#7e88f5'),
|
|
74
|
+
solid('█▀ ▄▄▄▄▄', '#7e88f5')
|
|
75
|
+
],
|
|
76
|
+
claude: [
|
|
77
|
+
solid(' ▐▛███▜▌ ', '#d77757'),
|
|
78
|
+
solid('▝▜█████▛▘', '#d77757'),
|
|
79
|
+
solid(' ▘▘ ▝▝ ', '#d77757')
|
|
80
|
+
],
|
|
81
|
+
antigravity: [
|
|
82
|
+
[g('▄', '#dbb131'), g('▀', '#f2922e', '#f6912e'), g('▀', '#f07236', '#f37337'), g('▄', '#f0583b')],
|
|
83
|
+
[g('▀', '#9ec345', '#86c64e'), g('▀', '#b5b43e', '#75b45e'), g('▀', '#e2993d', '#cc954d'), g('▀', '#f67a34', '#ef7947'), g('▀', '#f86a35', '#e16652'), g('▀', '#ef5442', '#e14f59')],
|
|
84
|
+
[g('▀', '#7cc251', '#80c654'), g('▀', '#71c25c', '#54b881'), g('▀', '#5ca98f', '#4097de'), g('▀', '#5c91b3'), g('▀', '#8373b0'), g('▀', '#746fc3', '#4a7ee4'), g('▀', '#995da8', '#706ece'), g('▀', '#9c5b97', '#8f64b4')],
|
|
85
|
+
[g('▄', '#6dc694'), g('▀', '#61c37d', '#62bad5'), g('▀', '#43aeab', '#47a8dc'), g(' '), g(' '), g(' '), g(' '), g('▀', '#4a80ea', '#3d89fb'), g('▀', '#6c73d8', '#4a81f0'), g('▄', '#6579e1')],
|
|
86
|
+
[g('▄', '#67b9f4'), g('▀', '#6bc7a3', '#64b6f6'), g('▀', '#64b6f6'), g(' '), g(' '), g(' '), g(' '), g(' '), g(' '), g('▀', '#3886fb'), g('▀', '#4881f4', '#3883f9'), g('▄', '#3d85fc')]
|
|
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
|
+
copilot: [
|
|
105
|
+
solid('╭─╮╭─╮', '#99dbdf'),
|
|
106
|
+
solid('╰─╯╰─╯', '#99dbdf'),
|
|
107
|
+
[g('█', '#c88ce0'), g(' '), g('▘', '#89bc84'), g('▝', '#89bc84'), g(' '), g('█', '#c88ce0')],
|
|
108
|
+
[g(' '), g('▔', '#c88ce0'), g('▔', '#c88ce0'), g('▔', '#c88ce0'), g('▔', '#c88ce0'), g(' ')]
|
|
109
|
+
],
|
|
110
|
+
opencode: [
|
|
111
|
+
[...solid('▀▀▀▀', '#808080'), g(' '), ...solid('▀▀▀▀', '#eeeeee')],
|
|
112
|
+
[...solid('▀ ▀', '#808080'), g(' '), ...solid('▀ ', '#eeeeee')],
|
|
113
|
+
[...solid('▀▀▀▀', '#808080'), g(' '), ...solid('▀▀▀▀', '#eeeeee')]
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function padBox(rows) {
|
|
118
|
+
const blank = n => Array.from({length: Math.max(0, n)}, () => g(' '));
|
|
119
|
+
const top = Math.floor((LOGO_H - rows.length) / 2);
|
|
120
|
+
const out = [];
|
|
121
|
+
for (let i = 0; i < LOGO_H; i += 1) {
|
|
122
|
+
const src = rows[i - top];
|
|
123
|
+
if (!src) {
|
|
124
|
+
out.push(blank(LOGO_W));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const left = Math.floor((LOGO_W - src.length) / 2);
|
|
128
|
+
out.push([...blank(left), ...src, ...blank(LOGO_W - left - src.length)]);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function LogoPanel({tool}) {
|
|
134
|
+
const rows = padBox(LOGO_DATA[tool.id] || []);
|
|
135
|
+
|
|
136
|
+
return React.createElement(Box, {flexDirection: 'column', marginLeft: 4},
|
|
137
|
+
rows.map((cells, row) => (
|
|
138
|
+
React.createElement(Box, {key: row, height: 1},
|
|
139
|
+
cells.map((cell, col) => (
|
|
140
|
+
React.createElement(Text, {
|
|
141
|
+
key: col,
|
|
142
|
+
bold: true,
|
|
143
|
+
color: cell.fg || undefined,
|
|
144
|
+
backgroundColor: cell.bg || undefined
|
|
145
|
+
}, cell.ch)
|
|
146
|
+
))
|
|
147
|
+
)
|
|
148
|
+
))
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function App({items = tools, logo = false, onSelect, onCancel}) {
|
|
153
|
+
const {exit} = useApp();
|
|
154
|
+
const {stdout} = useStdout();
|
|
155
|
+
const [active, setActive] = useState(0);
|
|
156
|
+
const [tick, setTick] = useState(0);
|
|
157
|
+
const [spin, setSpin] = useState(0);
|
|
158
|
+
const [flash, setFlash] = useState(0);
|
|
159
|
+
|
|
160
|
+
const availability = useMemo(() => {
|
|
161
|
+
return new Map(items.map(tool => [tool.id, commandExists(tool.command)]));
|
|
162
|
+
}, [items]);
|
|
163
|
+
|
|
164
|
+
const width = stdout?.columns || 80;
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const timer = setInterval(() => setTick(value => value + 1), 110);
|
|
168
|
+
return () => clearInterval(timer);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const timer = setInterval(() => setSpin(value => value + 1), 50);
|
|
173
|
+
return () => clearInterval(timer);
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
setFlash(6);
|
|
178
|
+
const timer = setInterval(() => {
|
|
179
|
+
setFlash(value => Math.max(0, value - 1));
|
|
180
|
+
}, 35);
|
|
181
|
+
|
|
182
|
+
return () => clearInterval(timer);
|
|
183
|
+
}, [active]);
|
|
184
|
+
|
|
185
|
+
useInput((input, key) => {
|
|
186
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
187
|
+
exit();
|
|
188
|
+
onCancel();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (key.upArrow || input === 'k') {
|
|
193
|
+
setActive(value => previousAvailable(value, availability, items));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (key.downArrow || input === 'j') {
|
|
198
|
+
setActive(value => nextAvailable(value, availability, items));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (/^[1-9]$/.test(input)) {
|
|
203
|
+
const index = Number(input) - 1;
|
|
204
|
+
if (index < items.length && availability.get(items[index].id)) {
|
|
205
|
+
setActive(index);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (key.return) {
|
|
211
|
+
const selected = items[active];
|
|
212
|
+
if (!availability.get(selected.id)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
exit();
|
|
217
|
+
onSelect(selected);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const showLogo = logo && width >= 74;
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
React.createElement(Box, {flexDirection: 'column', paddingX: 2, paddingY: 1},
|
|
225
|
+
HIDDEN_CONFIG.showHeader && React.createElement(Header, {title: HIDDEN_CONFIG.title, tick}),
|
|
226
|
+
React.createElement(Box, {flexDirection: 'row', alignItems: 'center'},
|
|
227
|
+
React.createElement(Box, {flexDirection: 'column', width: 40, gap: 1},
|
|
228
|
+
items.map((tool, index) => (
|
|
229
|
+
React.createElement(ToolRow, {
|
|
230
|
+
key: tool.id,
|
|
231
|
+
tool,
|
|
232
|
+
active: index === active,
|
|
233
|
+
available: availability.get(tool.id),
|
|
234
|
+
tick,
|
|
235
|
+
spin,
|
|
236
|
+
flash
|
|
237
|
+
})
|
|
238
|
+
))
|
|
239
|
+
),
|
|
240
|
+
showLogo && React.createElement(LogoPanel, {tool: items[active], spin})
|
|
241
|
+
),
|
|
242
|
+
React.createElement(Box, {marginTop: 1},
|
|
243
|
+
React.createElement(Text, {color: dimGray}, '↑/↓ or j/k move · enter open · esc quit')
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function ReorderApp({onDone, onCancel}) {
|
|
250
|
+
const {exit} = useApp();
|
|
251
|
+
const [order, setOrder] = useState([]);
|
|
252
|
+
const [active, setActive] = useState(0);
|
|
253
|
+
|
|
254
|
+
const seekUnpicked = (from, dir, taken) => {
|
|
255
|
+
for (let step = 0; step <= tools.length; step += 1) {
|
|
256
|
+
const index = (from + dir * step + tools.length * (step + 1)) % tools.length;
|
|
257
|
+
if (!taken.has(tools[index].id)) {
|
|
258
|
+
return index;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return from;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
useInput((input, key) => {
|
|
265
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
266
|
+
exit();
|
|
267
|
+
onCancel();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (input === 'r') {
|
|
272
|
+
setOrder([]);
|
|
273
|
+
setActive(0);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const taken = new Set(order);
|
|
278
|
+
|
|
279
|
+
if (key.upArrow || input === 'k') {
|
|
280
|
+
setActive(value => seekUnpicked(value - 1, -1, taken));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (key.downArrow || input === 'j') {
|
|
285
|
+
setActive(value => seekUnpicked(value + 1, 1, taken));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (key.return) {
|
|
290
|
+
const id = tools[active].id;
|
|
291
|
+
if (taken.has(id)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const next = [...order, id];
|
|
296
|
+
if (next.length === tools.length) {
|
|
297
|
+
exit();
|
|
298
|
+
onDone(next);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setOrder(next);
|
|
303
|
+
setActive(seekUnpicked(active + 1, 1, new Set(next)));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
React.createElement(Box, {flexDirection: 'column', paddingX: 2, paddingY: 1},
|
|
309
|
+
React.createElement(Box, {marginBottom: 1},
|
|
310
|
+
React.createElement(Text, {bold: true, color: white}, 'Order'),
|
|
311
|
+
React.createElement(Text, {color: dimGray}, ' pick first to last')
|
|
312
|
+
),
|
|
313
|
+
React.createElement(Box, {flexDirection: 'column', gap: 0},
|
|
314
|
+
tools.map((tool, index) => {
|
|
315
|
+
const pos = order.indexOf(tool.id);
|
|
316
|
+
const picked = pos >= 0;
|
|
317
|
+
const isActive = index === active && !picked;
|
|
318
|
+
const brand = tool.palette[0] || white;
|
|
319
|
+
return (
|
|
320
|
+
React.createElement(Box, {key: tool.id, height: 1},
|
|
321
|
+
React.createElement(Box, {width: 2},
|
|
322
|
+
React.createElement(Text, {bold: true, color: isActive ? brand : dimGray}, isActive ? '›' : ' ')
|
|
323
|
+
),
|
|
324
|
+
React.createElement(Box, {width: 3},
|
|
325
|
+
React.createElement(Text, {bold: picked, color: picked ? brand : dimGray}, picked ? String(pos + 1) : '·')
|
|
326
|
+
),
|
|
327
|
+
React.createElement(Text, {bold: picked || isActive, color: picked ? white : isActive ? white : gray}, tool.label)
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
})
|
|
331
|
+
),
|
|
332
|
+
React.createElement(Box, {marginTop: 1},
|
|
333
|
+
React.createElement(Text, {color: dimGray}, 'enter set · ↑/↓ move · r reset · esc cancel')
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function Header({title, tick}) {
|
|
340
|
+
return (
|
|
341
|
+
React.createElement(Box, {marginBottom: 1},
|
|
342
|
+
React.createElement(GradientText, {
|
|
343
|
+
text: title,
|
|
344
|
+
palette: tools[tick % tools.length].palette,
|
|
345
|
+
offset: tick
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ToolRow({tool, active, available, tick, spin, flash}) {
|
|
352
|
+
const arrow = active ? '›' : ' ';
|
|
353
|
+
const markerColor = active ? (tool.palette[0] || white) : dimGray;
|
|
354
|
+
const rowTone = available ? gray : dimGray;
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
React.createElement(Box, {height: 1},
|
|
358
|
+
React.createElement(Box, {width: 2},
|
|
359
|
+
React.createElement(Text, {bold: active, color: markerColor}, arrow)
|
|
360
|
+
),
|
|
361
|
+
React.createElement(Box, {width: 18},
|
|
362
|
+
active && available
|
|
363
|
+
? React.createElement(ActiveLabel, {tool, tick, spin, flash})
|
|
364
|
+
: React.createElement(Text, {color: rowTone}, tool.label)
|
|
365
|
+
),
|
|
366
|
+
React.createElement(Box, {width: 2},
|
|
367
|
+
active && available ? React.createElement(ActiveGlyph, {tool, tick, spin}) : React.createElement(Text, {color: dimGray}, ' ')
|
|
368
|
+
),
|
|
369
|
+
React.createElement(Text, {color: active && available ? subtle(tool, tick) : dimGray},
|
|
370
|
+
available ? tool.hint : `${tool.command} not found`
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ActiveLabel({tool, tick, spin, flash}) {
|
|
377
|
+
if (tool.id === 'codex') {
|
|
378
|
+
return React.createElement(GradientText, {
|
|
379
|
+
text: tool.label,
|
|
380
|
+
palette: tool.palette,
|
|
381
|
+
offset: tick,
|
|
382
|
+
bold: true
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (tool.id === 'claude') {
|
|
387
|
+
return React.createElement(ShimmerText, {text: tool.label, spin});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (tool.id === 'antigravity') {
|
|
391
|
+
return React.createElement(RunningLight, {text: tool.label, palette: tool.palette, spin});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (tool.id === 'cursor') {
|
|
395
|
+
const color = mix('#000000', '#ffffff', pulse(spin, 32));
|
|
396
|
+
return React.createElement(Text, {bold: true, color}, 'Cursor');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (tool.id === 'copilot') {
|
|
400
|
+
return React.createElement(CopilotText, {tick});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (tool.id === 'opencode') {
|
|
404
|
+
return React.createElement(Box,
|
|
405
|
+
null,
|
|
406
|
+
React.createElement(Text, {bold: true, color: '#808080'}, 'open'),
|
|
407
|
+
React.createElement(Text, {bold: true, color: '#eeeeee'}, 'code')
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return React.createElement(Text, {bold: true, color: flash > 0 ? white : gray}, tool.label);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function ActiveGlyph({tool, tick, spin}) {
|
|
415
|
+
if (tool.id === 'claude') {
|
|
416
|
+
const frame = ['·', '✻', '✽', '✶', '✳', '✢'][Math.floor(spin / 3) % 6];
|
|
417
|
+
return React.createElement(Text, {bold: true, color: mix('#b5654a', '#e8b79c', pulse(spin, 24))}, frame);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (tool.id === 'antigravity') {
|
|
421
|
+
const stars = ['·', '✧', '✦', '✷', '✹', '✷', '✦', '✧'];
|
|
422
|
+
const frame = stars[Math.floor(spin / 3) % stars.length];
|
|
423
|
+
const color = cycleColor(['#4285f4', '#9b72cb', '#d96570'], spin * 0.03);
|
|
424
|
+
return React.createElement(Text, {bold: true, color}, frame);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (tool.id === 'cursor') {
|
|
428
|
+
return React.createElement(Text, {inverse: tick % 8 < 4}, ' ');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (tool.id === 'copilot') {
|
|
432
|
+
return React.createElement(Text, {color: ['#c88ce0', '#89bc84', '#99dbdf', '#89bc84'][tick % 4]}, '◌');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (tool.id === 'opencode') {
|
|
436
|
+
return React.createElement(Text, {color: white}, tick % 8 < 4 ? '▌' : ' ');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return React.createElement(Text, {color: activeArrowColor(tool, tick)}, '✦');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function GradientText({text, palette, offset = 0, bold = false}) {
|
|
443
|
+
const chars = Array.from(text);
|
|
444
|
+
return React.createElement(Box,
|
|
445
|
+
null,
|
|
446
|
+
chars.map((char, index) => {
|
|
447
|
+
const color = gradientAt(palette, chars.length <= 1 ? 0 : index / (chars.length - 1), offset);
|
|
448
|
+
return React.createElement(Text, {key: `${char}-${index}`, bold, color}, char);
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function RunningLight({text, palette, spin, span = 1.4}) {
|
|
454
|
+
const chars = Array.from(text);
|
|
455
|
+
const period = chars.length + span * 3;
|
|
456
|
+
const center = (spin * 0.6) % period - span;
|
|
457
|
+
|
|
458
|
+
return React.createElement(Box,
|
|
459
|
+
null,
|
|
460
|
+
chars.map((char, index) => {
|
|
461
|
+
const base = gradientAt(palette, chars.length <= 1 ? 0 : index / (chars.length - 1), 0);
|
|
462
|
+
const light = Math.max(0, 1 - Math.abs(index - center) / span);
|
|
463
|
+
const color = mix(base, '#ffffff', light);
|
|
464
|
+
return React.createElement(Text, {key: `${char}-${index}`, bold: true, color}, char);
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function ShimmerText({text, spin, base = '#d97757', shimmer = '#ffffff', span = 2}) {
|
|
470
|
+
const chars = Array.from(text);
|
|
471
|
+
const period = chars.length + span * 2;
|
|
472
|
+
const center = (spin * 0.5) % period - span;
|
|
473
|
+
|
|
474
|
+
return React.createElement(Box,
|
|
475
|
+
null,
|
|
476
|
+
chars.map((char, index) => {
|
|
477
|
+
const weight = Math.max(0, 1 - Math.abs(index - center) / span);
|
|
478
|
+
const color = mix(base, shimmer, weight);
|
|
479
|
+
return React.createElement(Text, {key: `${char}-${index}`, bold: true, color}, char);
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function CopilotText({tick}) {
|
|
485
|
+
const chars = Array.from('Copilot');
|
|
486
|
+
const orbit = [1, 3, 5, 3][tick % 4];
|
|
487
|
+
|
|
488
|
+
return React.createElement(Box,
|
|
489
|
+
null,
|
|
490
|
+
chars.map((char, index) => {
|
|
491
|
+
const isO = char.toLowerCase() === 'o';
|
|
492
|
+
const edge = index === 0 || index === chars.length - 1;
|
|
493
|
+
const orbiting = Math.abs(index - orbit) <= 1;
|
|
494
|
+
const color = isO ? '#89bc84' : edge ? '#c88ce0' : '#99dbdf';
|
|
495
|
+
const backgroundColor = orbiting && !isO && tick % 8 < 4 ? '#143236' : undefined;
|
|
496
|
+
|
|
497
|
+
return React.createElement(Text, {key: `${char}-${index}`, bold: true, color, backgroundColor}, char);
|
|
498
|
+
})
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderPlainLine(tool, index, activeIndex) {
|
|
503
|
+
const selected = index === activeIndex;
|
|
504
|
+
const arrow = selected ? '›' : ' ';
|
|
505
|
+
const label = selected ? tool.label : tool.label;
|
|
506
|
+
return `${arrow} ${label.padEnd(16)} ${tool.hint}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function renderSnapshot(activeIndex = 0) {
|
|
510
|
+
const bounded = Math.max(0, Math.min(tools.length - 1, activeIndex));
|
|
511
|
+
return `${tools.map((tool, index) => renderPlainLine(tool, index, bounded)).join('\n')}\n`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function commandExists(command) {
|
|
515
|
+
const result = spawnSync('sh', ['-lc', `command -v ${shellQuote(command)} >/dev/null 2>&1`], {
|
|
516
|
+
stdio: 'ignore'
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return result.status === 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function previousAvailable(current, availability, items) {
|
|
523
|
+
for (let step = 1; step <= items.length; step += 1) {
|
|
524
|
+
const index = (current - step + items.length) % items.length;
|
|
525
|
+
if (availability.get(items[index].id)) {
|
|
526
|
+
return index;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return current;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function nextAvailable(current, availability, items) {
|
|
534
|
+
for (let step = 1; step <= items.length; step += 1) {
|
|
535
|
+
const index = (current + step) % items.length;
|
|
536
|
+
if (availability.get(items[index].id)) {
|
|
537
|
+
return index;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return current;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Reorder canonical tools by an array of ids; unknown ids ignored, missing appended.
|
|
545
|
+
export function orderTools(order = []) {
|
|
546
|
+
const byId = new Map(tools.map(tool => [tool.id, tool]));
|
|
547
|
+
const result = [];
|
|
548
|
+
for (const id of order) {
|
|
549
|
+
if (byId.has(id)) {
|
|
550
|
+
result.push(byId.get(id));
|
|
551
|
+
byId.delete(id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const tool of tools) {
|
|
555
|
+
if (byId.has(tool.id)) {
|
|
556
|
+
result.push(tool);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function activeArrowColor(tool, tick) {
|
|
563
|
+
return tool.palette[tick % tool.palette.length] || white;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function subtle(tool, tick) {
|
|
567
|
+
if (tool.id === 'cursor') {
|
|
568
|
+
return tick % 8 < 4 ? '#d7d7d7' : '#8b8b8b';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return mix(tool.palette[0] || gray, gray, 0.55);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function cycleColor(stops, phase) {
|
|
575
|
+
const scaled = (((phase % 1) + 1) % 1) * stops.length;
|
|
576
|
+
const left = Math.floor(scaled);
|
|
577
|
+
return mix(stops[left % stops.length], stops[(left + 1) % stops.length], scaled - left);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function gradientAt(palette, position, offset) {
|
|
581
|
+
if (palette.length === 1) {
|
|
582
|
+
return palette[0];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const animated = (position + (offset % 20) / 20) % 1;
|
|
586
|
+
const scaled = animated * (palette.length - 1);
|
|
587
|
+
const left = Math.floor(scaled);
|
|
588
|
+
const right = Math.min(palette.length - 1, left + 1);
|
|
589
|
+
return mix(palette[left], palette[right], scaled - left);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function pulse(tick, period) {
|
|
593
|
+
return (Math.sin((tick / period) * Math.PI * 2) + 1) / 2;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function mix(left, right, amount) {
|
|
597
|
+
const a = parseHex(left);
|
|
598
|
+
const b = parseHex(right);
|
|
599
|
+
const blend = channel => Math.round(a[channel] + (b[channel] - a[channel]) * amount);
|
|
600
|
+
return toHex(blend('r'), blend('g'), blend('b'));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function parseHex(value) {
|
|
604
|
+
const clean = value.replace('#', '');
|
|
605
|
+
return {
|
|
606
|
+
r: Number.parseInt(clean.slice(0, 2), 16),
|
|
607
|
+
g: Number.parseInt(clean.slice(2, 4), 16),
|
|
608
|
+
b: Number.parseInt(clean.slice(4, 6), 16)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function toHex(r, g, b) {
|
|
613
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function hex(value) {
|
|
617
|
+
return value.toString(16).padStart(2, '0');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function shellQuote(value) {
|
|
621
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
622
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {homedir} from 'node:os';
|
|
2
|
+
import {join, dirname} from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const baseDir = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
6
|
+
const configPath = join(baseDir, 'summon-cli', 'config.json');
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
order: [],
|
|
10
|
+
logo: true,
|
|
11
|
+
default: null
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return {...DEFAULTS, ...parsed};
|
|
19
|
+
} catch {
|
|
20
|
+
return {...DEFAULTS};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveConfig(patch) {
|
|
25
|
+
const next = {...loadConfig(), ...patch};
|
|
26
|
+
fs.mkdirSync(dirname(configPath), {recursive: true});
|
|
27
|
+
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function configLocation() {
|
|
32
|
+
return configPath;
|
|
33
|
+
}
|