spirewise 1.9.0 → 1.9.2
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 +85 -81
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -211,29 +211,21 @@ function removeFromAgent(agentKey, agent, scope, skills, onItem) {
|
|
|
211
211
|
return removed;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
const parts = steps.map((label, i) => {
|
|
221
|
-
const n = i + 1;
|
|
222
|
-
if (n < current) return `${paint(RAW.green, '✓')} ${paint(RAW.dim, label)}`; // done
|
|
223
|
-
if (n === current) return `${paint(RAW.cyan, STEP_FILLED[n])} ${paint(RAW.bold, label)}`; // active
|
|
224
|
-
return paint(RAW.dim, `${STEP_OUTLINE[n]} ${label}`); // upcoming
|
|
225
|
-
});
|
|
226
|
-
return [' ' + parts.join(paint(RAW.dim, ' ── '))];
|
|
227
|
-
}
|
|
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));
|
|
228
220
|
|
|
229
221
|
// Single, in-place updating status line (collapses the install/remove progress
|
|
230
222
|
// so the whole "done process" lives on one line instead of streaming many rows).
|
|
231
223
|
const IS_TTY = !!process.stdout.isTTY;
|
|
232
|
-
const live = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K
|
|
233
|
-
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); };
|
|
234
226
|
|
|
235
|
-
// --- Interactive
|
|
236
|
-
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 }) {
|
|
237
229
|
return new Promise((resolve) => {
|
|
238
230
|
const stdin = process.stdin, stdout = process.stdout;
|
|
239
231
|
if (!stdin.isTTY) { resolve(null); return; }
|
|
@@ -243,18 +235,10 @@ function interactiveSelect({ title, subtitle, items, multi = true, preselected =
|
|
|
243
235
|
if (multi) items.forEach((it, i) => { if (preselected.includes(it.value)) selected.add(i); });
|
|
244
236
|
else { const pi = items.findIndex((it) => preselected.includes(it.value)); if (pi >= 0) index = pi; }
|
|
245
237
|
|
|
246
|
-
const
|
|
238
|
+
const num = '#' + String(step).padStart(2, '0');
|
|
247
239
|
let lastLines = 0;
|
|
248
240
|
|
|
249
|
-
|
|
250
|
-
const w = cols(), label = ` ${text} `;
|
|
251
|
-
const side = Math.max(0, w - label.length), left = Math.floor(side / 2);
|
|
252
|
-
return paint(RAW.cyan, '═'.repeat(left) + label + '═'.repeat(side - left));
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
function render() {
|
|
256
|
-
// Windowed viewport: show at most `pageSize` rows and keep the cursor
|
|
257
|
-
// centered so long lists (e.g. skills) scroll smoothly with up/down.
|
|
241
|
+
function frame() {
|
|
258
242
|
const PAGE = Math.min(pageSize, items.length);
|
|
259
243
|
const half = Math.floor(PAGE / 2);
|
|
260
244
|
let start = index - half;
|
|
@@ -262,60 +246,75 @@ function interactiveSelect({ title, subtitle, items, multi = true, preselected =
|
|
|
262
246
|
if (start > items.length - PAGE) start = items.length - PAGE;
|
|
263
247
|
const end = start + PAGE;
|
|
264
248
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
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' : ' ')));
|
|
273
259
|
for (let i = start; i < end; i++) {
|
|
274
260
|
const it = items[i];
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
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;
|
|
279
267
|
const hint = it.hint ? paint(RAW.dim, ' ' + it.hint) : '';
|
|
280
|
-
lines.push(
|
|
268
|
+
lines.push(`${rail()} ${ptr} ${mark} ${label}${hint}`);
|
|
281
269
|
}
|
|
282
|
-
if (items.length > PAGE) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
lines.push('');
|
|
286
|
-
lines.push(paint(RAW.dim, ' ' + (multi
|
|
287
|
-
? '↑/↓ move · space toggle · a all/none · enter confirm · esc cancel'
|
|
288
|
-
: '↑/↓ 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
|
+
|
|
289
273
|
if (lastLines > 0) stdout.write(`\x1b[${lastLines}A`);
|
|
290
274
|
stdout.write('\x1b[0J' + lines.join('\n') + '\n');
|
|
291
275
|
lastLines = lines.length;
|
|
292
276
|
}
|
|
293
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
|
+
|
|
294
293
|
function cleanup() {
|
|
295
294
|
try { stdin.setRawMode(false); } catch (_) {}
|
|
296
295
|
stdin.pause();
|
|
297
296
|
stdin.removeListener('data', onData);
|
|
298
297
|
}
|
|
299
|
-
const finish = (r) => { cleanup(); resolve(r); };
|
|
300
298
|
|
|
301
299
|
function onData(s) {
|
|
302
|
-
if (s === '\x03' || s === '\x1b' || s === 'q') return
|
|
300
|
+
if (s === '\x03' || s === '\x1b' || s === 'q') { cleanup(); return resolve(null); } // ctrl-c / esc / q
|
|
303
301
|
if (s === '\r' || s === '\n') {
|
|
304
|
-
|
|
302
|
+
const val = multi ? items.filter((_, i) => selected.has(i)).map((it) => it.value) : items[index].value;
|
|
303
|
+
persist(); cleanup(); return resolve(val);
|
|
305
304
|
}
|
|
306
|
-
if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return
|
|
307
|
-
if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return
|
|
308
|
-
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(); }
|
|
309
308
|
if (multi && (s === 'a' || s === 'A')) {
|
|
310
309
|
if (selected.size === items.length) selected.clear(); else items.forEach((_, i) => selected.add(i));
|
|
311
|
-
return
|
|
310
|
+
return frame();
|
|
312
311
|
}
|
|
313
|
-
if (!multi && s === ' ')
|
|
312
|
+
if (!multi && s === ' ') { const val = items[index].value; persist(); cleanup(); return resolve(val); }
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
|
|
317
316
|
stdin.on('data', onData);
|
|
318
|
-
|
|
317
|
+
frame();
|
|
319
318
|
});
|
|
320
319
|
}
|
|
321
320
|
|
|
@@ -406,6 +405,15 @@ async function main() {
|
|
|
406
405
|
const Ving = action === 'remove' ? 'Removing' : 'Installing';
|
|
407
406
|
banner();
|
|
408
407
|
|
|
408
|
+
// Clack-style flow frame: each step hangs off the │ rail and the run closes with
|
|
409
|
+
// └. 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
|
+
};
|
|
416
|
+
|
|
409
417
|
// 1) SKILLS
|
|
410
418
|
let skills = o.skills;
|
|
411
419
|
if (skills) {
|
|
@@ -415,9 +423,9 @@ async function main() {
|
|
|
415
423
|
return id;
|
|
416
424
|
});
|
|
417
425
|
} else if (tty) {
|
|
426
|
+
ensureIntro();
|
|
418
427
|
skills = await interactiveSelect({
|
|
419
|
-
title:
|
|
420
|
-
step: 1, steps: ['Skills', 'Tools', 'Where'],
|
|
428
|
+
title: 'Choose Skills', step: 1,
|
|
421
429
|
subtitle: action === 'remove' ? 'Pick the skills to remove.' : 'Pick the skills you want.',
|
|
422
430
|
items: available.map((s) => ({ value: s, label: s, hint: skillHint(s) })),
|
|
423
431
|
multi: true, preselected: [],
|
|
@@ -431,11 +439,11 @@ async function main() {
|
|
|
431
439
|
if (agentKeys) {
|
|
432
440
|
for (const k of agentKeys) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
|
|
433
441
|
} else if (tty) {
|
|
442
|
+
ensureIntro();
|
|
434
443
|
agentKeys = await interactiveSelect({
|
|
435
|
-
title:
|
|
436
|
-
step: 2, steps: ['Skills', 'Tools', 'Where'],
|
|
444
|
+
title: 'Choose Tools', step: 2,
|
|
437
445
|
subtitle: action === 'remove' ? 'Pick which tools to remove them from.' : 'Pick which tools to add them to.',
|
|
438
|
-
items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k})
|
|
446
|
+
items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k})` })),
|
|
439
447
|
multi: true, preselected: [],
|
|
440
448
|
});
|
|
441
449
|
if (agentKeys === null) die('Cancelled.');
|
|
@@ -445,14 +453,14 @@ async function main() {
|
|
|
445
453
|
// 3) SCOPE
|
|
446
454
|
let scope = o.scope;
|
|
447
455
|
if (!scope && tty) {
|
|
456
|
+
ensureIntro();
|
|
448
457
|
scope = await interactiveSelect({
|
|
449
|
-
title: 'Where',
|
|
450
|
-
step: 3, steps: ['Skills', 'Tools', 'Where'],
|
|
458
|
+
title: 'Choose Where', step: 3,
|
|
451
459
|
subtitle: action === 'remove' ? 'Where to remove them from?' : 'Where should they go?',
|
|
452
460
|
items: [
|
|
453
|
-
{ value: 'project', label: '
|
|
454
|
-
{ value: 'global', label: '
|
|
455
|
-
{ value: 'both', label: 'Both',
|
|
461
|
+
{ value: 'project', label: 'Workspace', hint: 'just this folder' },
|
|
462
|
+
{ value: 'global', label: 'Global', hint: 'all your projects' },
|
|
463
|
+
{ value: 'both', label: 'Both', hint: 'workspace + global' },
|
|
456
464
|
],
|
|
457
465
|
multi: false, preselected: [],
|
|
458
466
|
});
|
|
@@ -462,34 +470,30 @@ async function main() {
|
|
|
462
470
|
const scopes = scope === 'both' ? ['project', 'global'] : [scope];
|
|
463
471
|
|
|
464
472
|
// ACTION
|
|
465
|
-
console.log(
|
|
473
|
+
if (framed) console.log(railLn());
|
|
466
474
|
|
|
467
475
|
const pl = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
468
|
-
const where = { project: '
|
|
476
|
+
const where = { project: 'workspace', global: 'global', both: 'workspace + global' }[scope];
|
|
477
|
+
const lead = framed ? `${rail()} ` : ' '; // hang progress/result off the rail
|
|
478
|
+
const end = framed ? `${paint(RAW.cyan, G.close)} ` : ' ';
|
|
469
479
|
|
|
470
480
|
let count = 0;
|
|
471
481
|
const totalOps = scopes.length * agentKeys.length * skills.length;
|
|
472
482
|
let seen = 0;
|
|
473
|
-
const progress = (agent
|
|
483
|
+
const progress = (agent) => {
|
|
474
484
|
seen++;
|
|
475
|
-
live(`${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, `${seen}/${totalOps}`)} ${c.dim}${agent.label}${c.reset}`);
|
|
485
|
+
live(`${lead}${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, `${seen}/${totalOps}`)} ${c.dim}${agent.label}${c.reset}`);
|
|
476
486
|
};
|
|
477
487
|
for (const sc of scopes) for (const k of agentKeys) {
|
|
478
488
|
if (action === 'remove') count += removeFromAgent(k, AGENTS[k], sc, skills, progress);
|
|
479
489
|
else { installToAgent(k, AGENTS[k], sc, skills, progress); count += skills.length; }
|
|
480
490
|
}
|
|
481
|
-
// Collapse the whole done process into one final line.
|
|
491
|
+
// Collapse the whole done process into one final line (the └ that closes the flow).
|
|
482
492
|
if (action === 'remove') {
|
|
483
|
-
liveEnd(`${count === 0 ? paint(RAW.yellow, '
|
|
493
|
+
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}`}`);
|
|
484
494
|
} else {
|
|
485
|
-
liveEnd(`${paint(RAW.green, '✓')} Added ${paint(RAW.bold, pl(skills.length, 'skill'))} to ${pl(agentKeys.length, 'tool')} ${c.dim}(${where})${c.reset}`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (action === 'remove') {
|
|
489
|
-
if (count === 0) liveEnd(paint(RAW.dim, ' nothing to remove — already clean'));
|
|
490
|
-
} else {
|
|
491
|
-
console.log('');
|
|
492
|
-
info(`Done. Open your tool and ask it to use a skill.`);
|
|
495
|
+
liveEnd(`${end}${paint(RAW.green, '✓')} Added ${paint(RAW.bold, pl(skills.length, 'skill'))} to ${pl(agentKeys.length, 'tool')} ${c.dim}(${where})${c.reset}`);
|
|
496
|
+
console.log(`${' '}${c.dim}Open your tool and ask it to use a skill.${c.reset}`);
|
|
493
497
|
}
|
|
494
498
|
console.log('');
|
|
495
499
|
}
|
package/package.json
CHANGED