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.
Files changed (2) hide show
  1. package/bin/cli.js +85 -81
  2. 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
- // Clean single-line horizontal stepper (like a React UI-library Steps component):
215
- // completed steps show a check, the active step is a filled, highlighted circle,
216
- // upcoming steps are dim outline circles joined by thin connectors.
217
- const STEP_FILLED = ['', '', '', '', '', '', '❻'];
218
- const STEP_OUTLINE = ['', '①', '②', '③', '④', '⑤', '⑥'];
219
- function stepper(current, steps) {
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 ' + s); };
233
- const liveEnd = (s) => { if (IS_TTY) process.stdout.write('\r\x1b[2K ' + s + '\n'); else console.log(' ' + s); };
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 full-width selector ---------------------------------------
236
- function interactiveSelect({ title, subtitle, items, multi = true, preselected = [], pageSize = 5, step, steps }) {
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 cols = () => stdout.columns || 80;
238
+ const num = '#' + String(step).padStart(2, '0');
247
239
  let lastLines = 0;
248
240
 
249
- const bar = (text) => {
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 head = (step && steps) ? stepper(step, steps) : [bar(title)];
266
- const lines = ['', ...head, ''];
267
- if (subtitle) lines.push(paint(RAW.dim, ' ' + subtitle));
268
- if (items.length > PAGE) {
269
- const above = start > 0 ? '▲ more' : ' ';
270
- lines.push(paint(RAW.dim, ` ${above} ${index + 1}/${items.length}`));
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 cur = i === index ? paint(RAW.cyan, '❯') : ' ';
276
- const mark = multi ? (selected.has(i) ? paint(RAW.green, '') : paint(RAW.dim, '◯'))
277
- : (i === index ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'));
278
- const label = i === index ? paint(RAW.bold, it.label) : it.label;
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(` ${cur} ${mark} ${label}${hint}`);
268
+ lines.push(`${rail()} ${ptr} ${mark} ${label}${hint}`);
281
269
  }
282
- if (items.length > PAGE) {
283
- lines.push(paint(RAW.dim, ` ${end < items.length ? '▼ more' : ' '}`));
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 finish(null); // ctrl-c / esc / q
300
+ if (s === '\x03' || s === '\x1b' || s === 'q') { cleanup(); return resolve(null); } // ctrl-c / esc / q
303
301
  if (s === '\r' || s === '\n') {
304
- return finish(multi ? items.filter((_, i) => selected.has(i)).map((it) => it.value) : items[index].value);
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 render(); }
307
- if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return render(); }
308
- if (multi && s === ' ') { selected.has(index) ? selected.delete(index) : selected.add(index); return render(); }
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 render();
310
+ return frame();
312
311
  }
313
- if (!multi && s === ' ') return finish(items[index].value);
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
- render();
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: `Skills`,
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: `Tools`,
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}) · ${a.format}` })),
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: 'This project', hint: 'just this folder' },
454
- { value: 'global', label: 'Everywhere', hint: 'all your projects' },
455
- { value: 'both', label: 'Both', hint: 'this project + everywhere' },
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: 'this project', global: 'everywhere', both: 'this project + everywhere' }[scope];
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, skill) => {
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, '·') : paint(RAW.green, '✓')} Removed ${paint(RAW.bold, pl(count, 'skill'))} from ${pl(agentKeys.length, 'tool')} ${c.dim}(${where})${c.reset}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spirewise",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Installable Agent Skills for GitHub Copilot, Claude Code, and Cursor — copywriting (F6S & LinkedIn) and NVIDIA Inception tooling.",
5
5
  "bin": {
6
6
  "spirewise": "bin/cli.js"