spirewise 1.9.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.
Files changed (2) hide show
  1. package/bin/cli.js +82 -77
  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,16 @@ async function main() {
406
405
  const Ving = action === 'remove' ? 'Removing' : 'Installing';
407
406
  banner();
408
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
+
409
418
  // 1) SKILLS
410
419
  let skills = o.skills;
411
420
  if (skills) {
@@ -415,9 +424,9 @@ async function main() {
415
424
  return id;
416
425
  });
417
426
  } else if (tty) {
427
+ ensureIntro();
418
428
  skills = await interactiveSelect({
419
- title: `Skills`,
420
- step: 1, steps: ['Skills', 'Tools', 'Where'],
429
+ title: 'Choose Skills', step: 1,
421
430
  subtitle: action === 'remove' ? 'Pick the skills to remove.' : 'Pick the skills you want.',
422
431
  items: available.map((s) => ({ value: s, label: s, hint: skillHint(s) })),
423
432
  multi: true, preselected: [],
@@ -431,11 +440,11 @@ async function main() {
431
440
  if (agentKeys) {
432
441
  for (const k of agentKeys) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
433
442
  } else if (tty) {
443
+ ensureIntro();
434
444
  agentKeys = await interactiveSelect({
435
- title: `Tools`,
436
- step: 2, steps: ['Skills', 'Tools', 'Where'],
445
+ title: 'Choose Tools', step: 2,
437
446
  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}` })),
447
+ items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k})` })),
439
448
  multi: true, preselected: [],
440
449
  });
441
450
  if (agentKeys === null) die('Cancelled.');
@@ -445,9 +454,9 @@ async function main() {
445
454
  // 3) SCOPE
446
455
  let scope = o.scope;
447
456
  if (!scope && tty) {
457
+ ensureIntro();
448
458
  scope = await interactiveSelect({
449
- title: 'Where',
450
- step: 3, steps: ['Skills', 'Tools', 'Where'],
459
+ title: 'Choose Where', step: 3,
451
460
  subtitle: action === 'remove' ? 'Where to remove them from?' : 'Where should they go?',
452
461
  items: [
453
462
  { value: 'project', label: 'This project', hint: 'just this folder' },
@@ -462,34 +471,30 @@ async function main() {
462
471
  const scopes = scope === 'both' ? ['project', 'global'] : [scope];
463
472
 
464
473
  // ACTION
465
- console.log('');
474
+ if (framed) console.log(railLn());
466
475
 
467
476
  const pl = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
468
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)} ` : ' ';
469
480
 
470
481
  let count = 0;
471
482
  const totalOps = scopes.length * agentKeys.length * skills.length;
472
483
  let seen = 0;
473
- const progress = (agent, skill) => {
484
+ const progress = (agent) => {
474
485
  seen++;
475
- live(`${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, `${seen}/${totalOps}`)} ${c.dim}${agent.label}${c.reset}`);
486
+ live(`${lead}${paint(RAW.cyan, '⟳')} ${Ving}… ${paint(RAW.bold, `${seen}/${totalOps}`)} ${c.dim}${agent.label}${c.reset}`);
476
487
  };
477
488
  for (const sc of scopes) for (const k of agentKeys) {
478
489
  if (action === 'remove') count += removeFromAgent(k, AGENTS[k], sc, skills, progress);
479
490
  else { installToAgent(k, AGENTS[k], sc, skills, progress); count += skills.length; }
480
491
  }
481
- // Collapse the whole done process into one final line.
492
+ // Collapse the whole done process into one final line (the └ that closes the flow).
482
493
  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}`);
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}`}`);
484
495
  } 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.`);
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}`);
493
498
  }
494
499
  console.log('');
495
500
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spirewise",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
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"