spirewise 1.8.0 → 1.9.1
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/bin/cli.js +91 -95
- package/install.sh +6 -6
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -211,37 +211,21 @@ function removeFromAgent(agentKey, agent, scope, skills, onItem) {
|
|
|
211
211
|
return removed;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (let i = 0; i < steps.length; i++) {
|
|
221
|
-
const n = i + 1;
|
|
222
|
-
const num = '#' + String(n).padStart(2, '0'); // #01, #02, #03
|
|
223
|
-
const state = n < current ? 'done' : n === current ? 'active' : 'pending';
|
|
224
|
-
const color = state === 'done' ? RAW.green : state === 'active' ? RAW.cyan : RAW.dim;
|
|
225
|
-
const label = state === 'active' ? paint(RAW.bold, steps[i])
|
|
226
|
-
: state === 'done' ? paint(RAW.dim, steps[i] + ' ✓')
|
|
227
|
-
: paint(RAW.dim, steps[i]);
|
|
228
|
-
// rounded square (≈10–15% corner radius via ╭╮╰╯) holding the step number
|
|
229
|
-
out.push(PAD + paint(color, '╭─────╮'));
|
|
230
|
-
out.push(PAD + paint(color, '│ ' + num + ' │') + ' ' + label);
|
|
231
|
-
out.push(PAD + paint(color, '╰─────╯'));
|
|
232
|
-
if (n < steps.length) out.push(PAD + ' ' + paint(RAW.dim, '│')); // centred connector
|
|
233
|
-
}
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
214
|
+
// Clack/Appwrite-style prompt frame: a continuous left rail (│) connects every
|
|
215
|
+
// step; each step is introduced by ◇ #NN - Title, options sit under the rail, and
|
|
216
|
+
// finished steps persist as a compact transcript line. ◆ opens the flow, └ closes it.
|
|
217
|
+
const G = { bar: '│', step: '◇', open: '◆', close: '└', done: '◇' };
|
|
218
|
+
const rail = () => paint(RAW.cyan, G.bar);
|
|
219
|
+
const railLn = (s = '') => (s ? `${paint(RAW.cyan, G.bar)} ${s}` : paint(RAW.cyan, G.bar));
|
|
236
220
|
|
|
237
221
|
// Single, in-place updating status line (collapses the install/remove progress
|
|
238
222
|
// so the whole "done process" lives on one line instead of streaming many rows).
|
|
239
223
|
const IS_TTY = !!process.stdout.isTTY;
|
|
240
|
-
const live = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K
|
|
241
|
-
const liveEnd = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K
|
|
224
|
+
const live = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K' + s); };
|
|
225
|
+
const liveEnd = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K' + s + '\n'); else console.log(s); };
|
|
242
226
|
|
|
243
|
-
// --- Interactive
|
|
244
|
-
function interactiveSelect({ title, subtitle, items, multi = true, preselected = [], pageSize =
|
|
227
|
+
// --- Interactive prompt (Clack / Appwrite style) ---------------------------
|
|
228
|
+
function interactiveSelect({ title, subtitle, items, multi = true, preselected = [], pageSize = 6, step = 1 }) {
|
|
245
229
|
return new Promise((resolve) => {
|
|
246
230
|
const stdin = process.stdin, stdout = process.stdout;
|
|
247
231
|
if (!stdin.isTTY) { resolve(null); return; }
|
|
@@ -251,18 +235,10 @@ function interactiveSelect({ title, subtitle, items, multi = true, preselected =
|
|
|
251
235
|
if (multi) items.forEach((it, i) => { if (preselected.includes(it.value)) selected.add(i); });
|
|
252
236
|
else { const pi = items.findIndex((it) => preselected.includes(it.value)); if (pi >= 0) index = pi; }
|
|
253
237
|
|
|
254
|
-
const
|
|
238
|
+
const num = '#' + String(step).padStart(2, '0');
|
|
255
239
|
let lastLines = 0;
|
|
256
240
|
|
|
257
|
-
|
|
258
|
-
const w = cols(), label = ` ${text} `;
|
|
259
|
-
const side = Math.max(0, w - label.length), left = Math.floor(side / 2);
|
|
260
|
-
return paint(RAW.cyan, '═'.repeat(left) + label + '═'.repeat(side - left));
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
function render() {
|
|
264
|
-
// Windowed viewport: show at most `pageSize` rows and keep the cursor
|
|
265
|
-
// centered so long lists (e.g. skills) scroll smoothly with up/down.
|
|
241
|
+
function frame() {
|
|
266
242
|
const PAGE = Math.min(pageSize, items.length);
|
|
267
243
|
const half = Math.floor(PAGE / 2);
|
|
268
244
|
let start = index - half;
|
|
@@ -270,60 +246,75 @@ function interactiveSelect({ title, subtitle, items, multi = true, preselected =
|
|
|
270
246
|
if (start > items.length - PAGE) start = items.length - PAGE;
|
|
271
247
|
const end = start + PAGE;
|
|
272
248
|
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
lines.push(
|
|
249
|
+
const nav = multi
|
|
250
|
+
? '↑/↓ move space select a all enter confirm'
|
|
251
|
+
: '↑/↓ move enter confirm';
|
|
252
|
+
|
|
253
|
+
const lines = [];
|
|
254
|
+
lines.push(rail());
|
|
255
|
+
lines.push(`${paint(RAW.cyan, G.step)} ${paint(RAW.bold, `${num} - ${title}`)}`);
|
|
256
|
+
if (subtitle) lines.push(railLn(paint(RAW.dim, subtitle)));
|
|
257
|
+
lines.push(railLn(paint(RAW.dim, nav)));
|
|
258
|
+
if (items.length > PAGE) lines.push(railLn(paint(RAW.dim, start > 0 ? '↑ more' : ' ')));
|
|
281
259
|
for (let i = start; i < end; i++) {
|
|
282
260
|
const it = items[i];
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
261
|
+
const on = i === index;
|
|
262
|
+
const ptr = on ? paint(RAW.cyan, '❯') : ' ';
|
|
263
|
+
const mark = multi
|
|
264
|
+
? (selected.has(i) ? paint(RAW.green, '◉') : paint(RAW.dim, '○'))
|
|
265
|
+
: (on ? paint(RAW.cyan, '●') : paint(RAW.dim, '○'));
|
|
266
|
+
const label = on ? paint(RAW.bold, it.label) : it.label;
|
|
287
267
|
const hint = it.hint ? paint(RAW.dim, ' ' + it.hint) : '';
|
|
288
|
-
lines.push(
|
|
268
|
+
lines.push(`${rail()} ${ptr} ${mark} ${label}${hint}`);
|
|
289
269
|
}
|
|
290
|
-
if (items.length > PAGE) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
lines.push('');
|
|
294
|
-
lines.push(paint(RAW.dim, ' ' + (multi
|
|
295
|
-
? '↑/↓ move · space toggle · a all/none · enter confirm · esc cancel'
|
|
296
|
-
: '↑/↓ move · enter confirm · esc cancel')));
|
|
270
|
+
if (items.length > PAGE) lines.push(railLn(paint(RAW.dim, `${end < items.length ? '↓ more' : ' '} ${index + 1}/${items.length}`)));
|
|
271
|
+
lines.push(rail());
|
|
272
|
+
|
|
297
273
|
if (lastLines > 0) stdout.write(`\x1b[${lastLines}A`);
|
|
298
274
|
stdout.write('\x1b[0J' + lines.join('\n') + '\n');
|
|
299
275
|
lastLines = lines.length;
|
|
300
276
|
}
|
|
301
277
|
|
|
278
|
+
// Replace the live frame with a compact, persisted transcript line.
|
|
279
|
+
function persist() {
|
|
280
|
+
const summary = multi
|
|
281
|
+
? (() => { const l = items.filter((_, i) => selected.has(i)).map((it) => it.label);
|
|
282
|
+
return l.length === 0 ? paint(RAW.dim, 'none') : l.length > 3 ? `${l.length} selected` : l.join(', '); })()
|
|
283
|
+
: items[index].label;
|
|
284
|
+
const out = [
|
|
285
|
+
`${paint(RAW.green, G.done)} ${paint(RAW.bold, `${num} - ${title}`)}`,
|
|
286
|
+
railLn(paint(RAW.cyan, summary)),
|
|
287
|
+
];
|
|
288
|
+
if (lastLines > 0) stdout.write(`\x1b[${lastLines}A`);
|
|
289
|
+
stdout.write('\x1b[0J' + out.join('\n') + '\n');
|
|
290
|
+
lastLines = 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
302
293
|
function cleanup() {
|
|
303
294
|
try { stdin.setRawMode(false); } catch (_) {}
|
|
304
295
|
stdin.pause();
|
|
305
296
|
stdin.removeListener('data', onData);
|
|
306
297
|
}
|
|
307
|
-
const finish = (r) => { cleanup(); resolve(r); };
|
|
308
298
|
|
|
309
299
|
function onData(s) {
|
|
310
|
-
if (s === '\x03' || s === '\x1b' || s === 'q') return
|
|
300
|
+
if (s === '\x03' || s === '\x1b' || s === 'q') { cleanup(); return resolve(null); } // ctrl-c / esc / q
|
|
311
301
|
if (s === '\r' || s === '\n') {
|
|
312
|
-
|
|
302
|
+
const val = multi ? items.filter((_, i) => selected.has(i)).map((it) => it.value) : items[index].value;
|
|
303
|
+
persist(); cleanup(); return resolve(val);
|
|
313
304
|
}
|
|
314
|
-
if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return
|
|
315
|
-
if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return
|
|
316
|
-
if (multi && s === ' ') { selected.has(index) ? selected.delete(index) : selected.add(index); return
|
|
305
|
+
if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return frame(); }
|
|
306
|
+
if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return frame(); }
|
|
307
|
+
if (multi && s === ' ') { selected.has(index) ? selected.delete(index) : selected.add(index); return frame(); }
|
|
317
308
|
if (multi && (s === 'a' || s === 'A')) {
|
|
318
309
|
if (selected.size === items.length) selected.clear(); else items.forEach((_, i) => selected.add(i));
|
|
319
|
-
return
|
|
310
|
+
return frame();
|
|
320
311
|
}
|
|
321
|
-
if (!multi && s === ' ')
|
|
312
|
+
if (!multi && s === ' ') { const val = items[index].value; persist(); cleanup(); return resolve(val); }
|
|
322
313
|
}
|
|
323
314
|
|
|
324
315
|
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
|
|
325
316
|
stdin.on('data', onData);
|
|
326
|
-
|
|
317
|
+
frame();
|
|
327
318
|
});
|
|
328
319
|
}
|
|
329
320
|
|
|
@@ -411,10 +402,19 @@ async function main() {
|
|
|
411
402
|
|
|
412
403
|
const o = parseArgs(argv);
|
|
413
404
|
const tty = process.stdin.isTTY;
|
|
414
|
-
const verb = action === 'remove' ? 'remove' : 'install';
|
|
415
405
|
const Ving = action === 'remove' ? 'Removing' : 'Installing';
|
|
416
406
|
banner();
|
|
417
407
|
|
|
408
|
+
// Clack-style flow frame: opens with ◆, each step hangs off the │ rail, and the
|
|
409
|
+
// run closes with └. Only drawn when we actually show interactive steps.
|
|
410
|
+
let framed = false;
|
|
411
|
+
const ensureIntro = () => {
|
|
412
|
+
if (framed || !tty) return;
|
|
413
|
+
framed = true;
|
|
414
|
+
console.log('');
|
|
415
|
+
console.log(`${paint(RAW.cyan, G.open)} ${paint(RAW.bold, action === 'remove' ? 'Remove skills' : 'Install skills')}`);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
418
|
// 1) SKILLS
|
|
419
419
|
let skills = o.skills;
|
|
420
420
|
if (skills) {
|
|
@@ -424,10 +424,10 @@ async function main() {
|
|
|
424
424
|
return id;
|
|
425
425
|
});
|
|
426
426
|
} else if (tty) {
|
|
427
|
+
ensureIntro();
|
|
427
428
|
skills = await interactiveSelect({
|
|
428
|
-
title:
|
|
429
|
-
|
|
430
|
-
subtitle: action === 'remove' ? 'These will be deleted from the chosen agents' : 'Copy templates to install into your agents',
|
|
429
|
+
title: 'Choose Skills', step: 1,
|
|
430
|
+
subtitle: action === 'remove' ? 'Pick the skills to remove.' : 'Pick the skills you want.',
|
|
431
431
|
items: available.map((s) => ({ value: s, label: s, hint: skillHint(s) })),
|
|
432
432
|
multi: true, preselected: [],
|
|
433
433
|
});
|
|
@@ -440,11 +440,11 @@ async function main() {
|
|
|
440
440
|
if (agentKeys) {
|
|
441
441
|
for (const k of agentKeys) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
|
|
442
442
|
} else if (tty) {
|
|
443
|
+
ensureIntro();
|
|
443
444
|
agentKeys = await interactiveSelect({
|
|
444
|
-
title:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k}) · ${a.format}` })),
|
|
445
|
+
title: 'Choose Tools', step: 2,
|
|
446
|
+
subtitle: action === 'remove' ? 'Pick which tools to remove them from.' : 'Pick which tools to add them to.',
|
|
447
|
+
items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k})` })),
|
|
448
448
|
multi: true, preselected: [],
|
|
449
449
|
});
|
|
450
450
|
if (agentKeys === null) die('Cancelled.');
|
|
@@ -454,14 +454,14 @@ async function main() {
|
|
|
454
454
|
// 3) SCOPE
|
|
455
455
|
let scope = o.scope;
|
|
456
456
|
if (!scope && tty) {
|
|
457
|
+
ensureIntro();
|
|
457
458
|
scope = await interactiveSelect({
|
|
458
|
-
title: '
|
|
459
|
-
|
|
460
|
-
subtitle: action === 'remove' ? 'Where to remove the skills from?' : 'Where should the skills live?',
|
|
459
|
+
title: 'Choose Where', step: 3,
|
|
460
|
+
subtitle: action === 'remove' ? 'Where to remove them from?' : 'Where should they go?',
|
|
461
461
|
items: [
|
|
462
|
-
{ value: 'project', label: '
|
|
463
|
-
{ value: 'global', label: '
|
|
464
|
-
{ value: 'both', label: 'Both',
|
|
462
|
+
{ value: 'project', label: 'This project', hint: 'just this folder' },
|
|
463
|
+
{ value: 'global', label: 'Everywhere', hint: 'all your projects' },
|
|
464
|
+
{ value: 'both', label: 'Both', hint: 'this project + everywhere' },
|
|
465
465
|
],
|
|
466
466
|
multi: false, preselected: [],
|
|
467
467
|
});
|
|
@@ -471,34 +471,30 @@ async function main() {
|
|
|
471
471
|
const scopes = scope === 'both' ? ['project', 'global'] : [scope];
|
|
472
472
|
|
|
473
473
|
// ACTION
|
|
474
|
-
console.log(
|
|
475
|
-
|
|
474
|
+
if (framed) console.log(railLn());
|
|
475
|
+
|
|
476
|
+
const pl = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
477
|
+
const where = { project: 'this project', global: 'everywhere', both: 'this project + everywhere' }[scope];
|
|
478
|
+
const lead = framed ? `${rail()} ` : ' '; // hang progress/result off the rail
|
|
479
|
+
const end = framed ? `${paint(RAW.cyan, G.close)} ` : ' ';
|
|
476
480
|
|
|
477
481
|
let count = 0;
|
|
478
482
|
const totalOps = scopes.length * agentKeys.length * skills.length;
|
|
479
483
|
let seen = 0;
|
|
480
|
-
const progress = (agent
|
|
484
|
+
const progress = (agent) => {
|
|
481
485
|
seen++;
|
|
482
|
-
|
|
483
|
-
live(`${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, String(seen))}/${totalOps} (${pct}%) ${c.dim}${agent.label} · ${skill}${c.reset}`);
|
|
486
|
+
live(`${lead}${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, `${seen}/${totalOps}`)} ${c.dim}${agent.label}${c.reset}`);
|
|
484
487
|
};
|
|
485
488
|
for (const sc of scopes) for (const k of agentKeys) {
|
|
486
489
|
if (action === 'remove') count += removeFromAgent(k, AGENTS[k], sc, skills, progress);
|
|
487
490
|
else { installToAgent(k, AGENTS[k], sc, skills, progress); count += skills.length; }
|
|
488
491
|
}
|
|
489
|
-
// Collapse the whole done process into one final line.
|
|
490
|
-
const scopeLabel = scopes.map((s) => (s === 'project' ? 'workspace' : s)).join(' + ');
|
|
492
|
+
// Collapse the whole done process into one final line (the └ that closes the flow).
|
|
491
493
|
if (action === 'remove') {
|
|
492
|
-
liveEnd(`${count === 0 ? paint(RAW.yellow, '
|
|
494
|
+
liveEnd(`${end}${count === 0 ? paint(RAW.yellow, 'Nothing to remove — already clean') : `${paint(RAW.green, '✓')} Removed ${paint(RAW.bold, pl(count, 'skill'))} from ${pl(agentKeys.length, 'tool')} ${c.dim}(${where})${c.reset}`}`);
|
|
493
495
|
} else {
|
|
494
|
-
liveEnd(`${paint(RAW.green, '✓')}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (action === 'remove') {
|
|
498
|
-
if (count === 0) liveEnd(paint(RAW.dim, ' nothing matched — already clean'));
|
|
499
|
-
} else {
|
|
500
|
-
console.log('');
|
|
501
|
-
info(`next: open your agent and say ${paint(RAW.bold, '"write our F6S profile copy"')}`);
|
|
496
|
+
liveEnd(`${end}${paint(RAW.green, '✓')} Added ${paint(RAW.bold, pl(skills.length, 'skill'))} to ${pl(agentKeys.length, 'tool')} ${c.dim}(${where})${c.reset}`);
|
|
497
|
+
console.log(`${' '}${c.dim}Open your tool and ask it to use a skill.${c.reset}`);
|
|
502
498
|
}
|
|
503
499
|
console.log('');
|
|
504
500
|
}
|
package/install.sh
CHANGED
|
@@ -43,7 +43,7 @@ info() { printf '%s %s\n' "$(color '1;34' '==>')" "$1"; }
|
|
|
43
43
|
ok() { printf '%s %s\n' "$(color '1;32' ' ok')" "$1"; }
|
|
44
44
|
warn() { printf '%s %s\n' "$(color '1;33' ' !')" "$1" >&2; }
|
|
45
45
|
die() { printf '%s %s\n' "$(color '1;31' 'err')" "$1" >&2; exit 1; }
|
|
46
|
-
step() { printf '%s %s
|
|
46
|
+
step() { printf '%s %s\n' "$(color '1;36' " $1.")" "$(color '1' "$2")"; }
|
|
47
47
|
substep() { :; } # per-item output collapses into the single final summary line
|
|
48
48
|
|
|
49
49
|
banner() {
|
|
@@ -255,7 +255,7 @@ fi
|
|
|
255
255
|
banner
|
|
256
256
|
|
|
257
257
|
# Step 1: skills (default all).
|
|
258
|
-
step 1 "
|
|
258
|
+
step 1 "Skills"
|
|
259
259
|
if [[ ${#SELECTED[@]} -eq 0 ]]; then
|
|
260
260
|
SELECTED=("${AVAILABLE[@]}")
|
|
261
261
|
else
|
|
@@ -269,11 +269,11 @@ fi
|
|
|
269
269
|
substep "${#SELECTED[@]} skill(s): ${SELECTED[*]}"
|
|
270
270
|
|
|
271
271
|
# Step 2: agents
|
|
272
|
-
step 2 "
|
|
272
|
+
step 2 "Tools"
|
|
273
273
|
if [[ -n "$AGENT_FILTER" ]]; then substep "agents: $AGENT_FILTER"; else substep "agents: all supported"; fi
|
|
274
274
|
|
|
275
275
|
# Step 3: scope — prompt if not given.
|
|
276
|
-
step 3 "
|
|
276
|
+
step 3 "Where"
|
|
277
277
|
if [[ -z "$SCOPE" ]]; then
|
|
278
278
|
if [[ -t 0 ]]; then
|
|
279
279
|
[[ "$MODE" == "remove" ]] && info "Where should the skills be removed from?" || info "Where should the skills be installed?"
|
|
@@ -308,8 +308,8 @@ done
|
|
|
308
308
|
|
|
309
309
|
printf '\n'
|
|
310
310
|
if [[ "$MODE" == "remove" ]]; then
|
|
311
|
-
ok "Removed $REMOVED
|
|
311
|
+
ok "Removed $REMOVED."
|
|
312
312
|
else
|
|
313
|
-
ok "
|
|
313
|
+
ok "Added ${#SELECTED[@]}. Open your tool and ask it to use a skill."
|
|
314
314
|
fi
|
|
315
315
|
|
package/package.json
CHANGED