rsc-universal 0.1.2 โ†’ 0.1.3

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/README.md CHANGED
@@ -65,23 +65,29 @@ assistant proposes new skills on its own from then on.
65
65
 
66
66
  ```
67
67
  $ rsc
68
- Hi ๐Ÿ‘‹ What do you want to do?
69
- > a web store with a database, and keep the books
70
-
71
- Here's what I've lined up for you:
72
- โ€ข Your store (fast, ready for Google) โ†’ nextjs, design, seo-geo
73
- โ€ข Store your data reliably โ†’ postgresdb
74
- โ€ข Charge customers and invoice them โ†’ stripe, invoicing
75
- โ€ข Keep the accounts โ†’ bookkeeping, finance-ops
76
- โ€ข Put it online โ†’ vercel
77
- Shall I set it up? (yes / no) > yes
78
-
79
- โœ… Done. Open your editor and start asking for things in your own words.
80
- ๐Ÿ’ก When a task needs something more, I'll offer it.
68
+ ๐Ÿ‘‹ rsc โ€” the skill catalog for your assistant.
69
+
70
+ What do you want to do? โ†‘โ†“ move ยท enter select
71
+ โฏ Base install โ€” the essentials (orient + suggest + harness + init)
72
+ Base + Spec-Driven Development โ€” specify โ†’ plan โ†’ implement โ†’ ship
73
+ Pick skills by hand, by area
74
+ Describe my project and let rsc choose
75
+ ```
76
+
77
+ Pick **by area** and you get a checkbox list โ€” **โ†‘โ†“ to move, space to toggle,
78
+ enter to confirm**:
79
+
80
+ ```
81
+ Languages: โ†‘โ†“ move ยท space toggle ยท a all ยท enter confirm
82
+ โฏ โ—‰ typescript
83
+ โ—ฏ python
84
+ โ—‰ go
85
+ โ—ฏ rust
81
86
  ```
82
87
 
83
- It detects your stack from the repo, maps your words to outcomes, installs the
84
- matching skills, then suggests what usually comes next.
88
+ It auto-detects your IDE and stack, installs only what you chose, then prints the
89
+ exact next steps for **Claude Code / Cursor / Codex / Gemini** โ€” and from there
90
+ keeps proposing the skills a task needs.
85
91
 
86
92
  ---
87
93
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.2",
2
+ "version": "0.1.3",
3
3
  "counts": {
4
4
  "skills": 231
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rsc-universal",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/lib/ui.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createInterface } from 'node:readline/promises';
1
+ import { createInterface, emitKeypressEvents } from 'node:readline';
2
2
  import { stdin, stdout } from 'node:process';
3
3
 
4
4
  export async function ask(question) {
@@ -13,12 +13,110 @@ export function say(...lines) {
13
13
  }
14
14
 
15
15
  export function yes(s) {
16
- return /^(s|si|sรญ|y|yes|ok|vale|dale)/i.test(s.trim());
16
+ return /^(s|si|sรญ|y|yes|ok|sure|yeah)/i.test(s.trim());
17
17
  }
18
18
 
19
- // Numbered single-choice menu. options: [{ key, label }]. Accepts the number or
20
- // the key typed verbatim. Returns the chosen key, or null if unrecognized.
19
+ // ---------------------------------------------------------------------------
20
+ // Interactive arrow-key / space TUI (zero dependencies).
21
+ // Falls back to a typed-number prompt when there is no interactive TTY
22
+ // (CI, pipes, dumb terminals) so scripts and tests never hang.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function interactive() {
26
+ return Boolean(stdin.isTTY && stdout.isTTY);
27
+ }
28
+
29
+ const C = {
30
+ dim: (s) => `\x1b[2m${s}\x1b[22m`,
31
+ bold: (s) => `\x1b[1m${s}\x1b[22m`,
32
+ cyan: (s) => `\x1b[36m${s}\x1b[39m`,
33
+ green: (s) => `\x1b[32m${s}\x1b[39m`,
34
+ };
35
+
36
+ // Repaint a fixed block of lines in place (cursor ends just below the block).
37
+ function makePainter() {
38
+ let height = 0;
39
+ return (lines) => {
40
+ if (height) stdout.write(`\x1b[${height}A`);
41
+ stdout.write(lines.map((l) => `\x1b[2K${l}`).join('\n') + '\n');
42
+ height = lines.length;
43
+ };
44
+ }
45
+
46
+ function captureKeys(onKey) {
47
+ emitKeypressEvents(stdin);
48
+ if (stdin.isTTY) stdin.setRawMode(true);
49
+ stdin.resume();
50
+ const handler = (str, key) => onKey(str, key || {});
51
+ stdin.on('keypress', handler);
52
+ return () => {
53
+ stdin.off('keypress', handler);
54
+ if (stdin.isTTY) stdin.setRawMode(false);
55
+ stdin.pause();
56
+ };
57
+ }
58
+
59
+ // Single choice: โ†‘โ†“ to move, Enter to pick. options: [{ key, label }].
60
+ function tuiSelect(question, options) {
61
+ return new Promise((resolve) => {
62
+ let i = 0;
63
+ const paint = makePainter();
64
+ const render = () => paint([
65
+ C.bold(question),
66
+ C.dim(' โ†‘โ†“ move ยท enter select'),
67
+ ...options.map((o, idx) =>
68
+ idx === i ? `${C.cyan('โฏ')} ${C.cyan(o.label)}` : ` ${o.label}`),
69
+ ]);
70
+ render();
71
+ const stop = captureKeys((str, key) => {
72
+ if (key.name === 'up' || key.name === 'k') { i = (i - 1 + options.length) % options.length; render(); }
73
+ else if (key.name === 'down' || key.name === 'j') { i = (i + 1) % options.length; render(); }
74
+ else if (key.name === 'return') { stop(); resolve(options[i].key); }
75
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { stop(); stdout.write('\n'); process.exit(130); }
76
+ });
77
+ });
78
+ }
79
+
80
+ // Multi-select: โ†‘โ†“ move, space toggle, a = all, enter confirm. Viewport scrolls.
81
+ function tuiChecklist(title, items) {
82
+ return new Promise((resolve) => {
83
+ const VISIBLE = Math.min(items.length, 12);
84
+ const sel = new Set();
85
+ let cur = 0; let top = 0;
86
+ const paint = makePainter();
87
+ const render = () => {
88
+ if (cur < top) top = cur;
89
+ if (cur >= top + VISIBLE) top = cur - VISIBLE + 1;
90
+ const rows = [];
91
+ for (let r = top; r < top + VISIBLE; r++) {
92
+ const it = items[r];
93
+ const box = sel.has(r) ? C.green('โ—‰') : 'โ—ฏ';
94
+ const line = `${box} ${it.label}`;
95
+ rows.push(r === cur ? `${C.cyan('โฏ')} ${C.cyan(line)}` : ` ${line}`);
96
+ }
97
+ const more = items.length > VISIBLE ? C.dim(` (${cur + 1}/${items.length})`) : '';
98
+ paint([
99
+ C.bold(title),
100
+ C.dim(' โ†‘โ†“ move ยท space toggle ยท a all ยท enter confirm') + more,
101
+ ...rows,
102
+ ]);
103
+ };
104
+ render();
105
+ const stop = captureKeys((str, key) => {
106
+ if (key.name === 'up' || key.name === 'k') { cur = (cur - 1 + items.length) % items.length; render(); }
107
+ else if (key.name === 'down' || key.name === 'j') { cur = (cur + 1) % items.length; render(); }
108
+ else if (key.name === 'space') { sel.has(cur) ? sel.delete(cur) : sel.add(cur); render(); }
109
+ else if (str === 'a') { if (sel.size === items.length) sel.clear(); else items.forEach((_, idx) => sel.add(idx)); render(); }
110
+ else if (key.name === 'return') { stop(); resolve([...sel].sort((x, y) => x - y).map((idx) => items[idx].id)); }
111
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { stop(); stdout.write('\n'); process.exit(130); }
112
+ });
113
+ });
114
+ }
115
+
116
+ // Public: single-choice menu. options: [{ key, label }]. Returns the chosen key.
21
117
  export async function select(question, options) {
118
+ if (interactive()) return tuiSelect(question, options);
119
+ // Fallback: typed number.
22
120
  say(question);
23
121
  options.forEach((o, i) => say(` ${i + 1}) ${o.label}`));
24
122
  const a = (await ask('> ')).toLowerCase();
@@ -28,19 +126,18 @@ export async function select(question, options) {
28
126
  return byKey ? byKey.key : null;
29
127
  }
30
128
 
31
- // Numbered multi-select. items: array of strings. Accepts comma-separated
32
- // numbers (e.g. "1,3,4"), "todo"/"all", or empty for none. Returns the subset.
129
+ // Public: multi-select. items: array of strings or { id, label }. Returns ids.
33
130
  export async function pickFrom(title, items) {
131
+ const norm = items.map((it) => (typeof it === 'string' ? { id: it, label: it } : it));
132
+ if (interactive()) return tuiChecklist(title, norm);
133
+ // Fallback: comma-separated numbers.
34
134
  say(`\n${title}`);
35
- items.forEach((id, i) => say(` ${String(i + 1).padStart(2)}) ${id}`));
135
+ norm.forEach((it, i) => say(` ${String(i + 1).padStart(2)}) ${it.label}`));
36
136
  say(' Comma-separated numbers (e.g. 1,3,4), "all", or Enter for none.');
37
137
  const a = (await ask('> ')).trim();
38
138
  if (!a) return [];
39
- if (/^(todo|todas|all)$/i.test(a)) return [...items];
40
- const picked = a
41
- .split(',')
42
- .map((s) => parseInt(s.trim(), 10))
43
- .filter((x) => x >= 1 && x <= items.length)
44
- .map((x) => items[x - 1]);
45
- return [...new Set(picked)];
139
+ if (/^(all|todo|todas)$/i.test(a)) return norm.map((it) => it.id);
140
+ return [...new Set(
141
+ a.split(',').map((s) => parseInt(s.trim(), 10)).filter((x) => x >= 1 && x <= norm.length).map((x) => norm[x - 1].id),
142
+ )];
46
143
  }