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.
Files changed (3) hide show
  1. package/bin/cli.js +91 -95
  2. package/install.sh +6 -6
  3. 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
- // Vertical wizard stepper. Each step is a rounded number square (#01, #02, #03)
215
- // with a vertical connector line dropping from the centre of each square to the
216
- // next a left-rail "stepper" rendered above every wizard screen.
217
- function stepper(current, steps) {
218
- const out = [];
219
- const PAD = ' ';
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 ' + s); };
241
- 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); };
242
226
 
243
- // --- Interactive full-width selector ---------------------------------------
244
- 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 }) {
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 cols = () => stdout.columns || 80;
238
+ const num = '#' + String(step).padStart(2, '0');
255
239
  let lastLines = 0;
256
240
 
257
- const bar = (text) => {
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 head = (step && steps) ? stepper(step, steps) : [bar(title)];
274
- const lines = ['', ...head, ''];
275
- if (subtitle) lines.push(paint(RAW.dim, ' ' + subtitle));
276
- if (items.length > PAGE) {
277
- const above = start > 0 ? '▲ more' : ' ';
278
- lines.push(paint(RAW.dim, ` ${above} ${index + 1}/${items.length}`));
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 cur = i === index ? paint(RAW.cyan, '❯') : ' ';
284
- const mark = multi ? (selected.has(i) ? paint(RAW.green, '') : paint(RAW.dim, '◯'))
285
- : (i === index ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'));
286
- 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;
287
267
  const hint = it.hint ? paint(RAW.dim, ' ' + it.hint) : '';
288
- lines.push(` ${cur} ${mark} ${label}${hint}`);
268
+ lines.push(`${rail()} ${ptr} ${mark} ${label}${hint}`);
289
269
  }
290
- if (items.length > PAGE) {
291
- lines.push(paint(RAW.dim, ` ${end < items.length ? '▼ more' : ' '}`));
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 finish(null); // ctrl-c / esc / q
300
+ if (s === '\x03' || s === '\x1b' || s === 'q') { cleanup(); return resolve(null); } // ctrl-c / esc / q
311
301
  if (s === '\r' || s === '\n') {
312
- 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);
313
304
  }
314
- if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return render(); }
315
- if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return render(); }
316
- 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(); }
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 render();
310
+ return frame();
320
311
  }
321
- if (!multi && s === ' ') return finish(items[index].value);
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
- render();
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: `Select skills to ${verb}`,
429
- step: 1, steps: ['Select skills', 'Select agents', 'Select scope'],
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: `Select agents`,
445
- step: 2, steps: ['Select skills', 'Select agents', 'Select scope'],
446
- subtitle: action === 'remove' ? 'Skills are removed from each agent’s folder' : 'Each agent gets the skills in its own folder + format',
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: 'Select scope',
459
- step: 3, steps: ['Select skills', 'Select agents', 'Select scope'],
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: 'Workspace', hint: 'this folder only — the current project' },
463
- { value: 'global', label: 'Global', hint: 'your home folders — applies to all projects' },
464
- { value: 'both', label: 'Both', hint: 'workspace + global' },
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
- info(`${Ving} ${paint(RAW.bold, String(skills.length))} skill(s) ${action === 'remove' ? 'from' : 'into'} ${paint(RAW.bold, String(agentKeys.length))} agent(s) · scope ${paint(RAW.bold, scope === 'project' ? 'workspace' : scope)}`);
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, skill, removed) => {
484
+ const progress = (agent) => {
481
485
  seen++;
482
- const pct = totalOps ? Math.round((seen / totalOps) * 100) : 100;
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, '·') : paint(RAW.green, '✓')} Removed ${paint(RAW.bold, String(count))} item(s) from ${paint(RAW.bold, String(agentKeys.length))} agent(s) ${c.dim}· ${scopeLabel}${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}`}`);
493
495
  } else {
494
- liveEnd(`${paint(RAW.green, '✓')} Installed ${paint(RAW.bold, String(skills.length))} skill(s) × ${paint(RAW.bold, String(agentKeys.length))} agent(s) ${c.dim}· ${count} file(s) · ${scopeLabel}${c.reset}`);
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 %s\n' "$(color '1;36' ' >')" "$(color '1;36' "[ #$(printf '%02d' "$1") ]")" "$2"; }
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 "Scanning available skills"
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 "Selecting target agents"
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 "Choosing scope"
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 item(s)."
311
+ ok "Removed $REMOVED."
312
312
  else
313
- ok "Installed ${#SELECTED[@]} skill(s). Next: open your agent and say \"write our F6S profile copy\"."
313
+ ok "Added ${#SELECTED[@]}. Open your tool and ask it to use a skill."
314
314
  fi
315
315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spirewise",
3
- "version": "1.8.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"