rsc-universal 0.1.2 → 0.1.4

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
@@ -1,5 +1,14 @@
1
1
  <div align="center">
2
2
 
3
+ ```
4
+ ██████╗ ███████╗ ██████╗
5
+ ██╔══██╗██╔════╝██╔════╝
6
+ ██████╔╝███████╗██║
7
+ ██╔══██╗╚════██║██║
8
+ ██║ ██║███████║╚██████╗
9
+ ╚═╝ ╚═╝╚══════╝ ╚═════╝
10
+ ```
11
+
3
12
  # `rsc` — 231 agent skills, one CLI, zero bloat
4
13
 
5
14
  **A self-recommending skill catalog for Claude Code, Cursor, Codex & Gemini.**
@@ -65,23 +74,29 @@ assistant proposes new skills on its own from then on.
65
74
 
66
75
  ```
67
76
  $ 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.
77
+ 👋 rsc the skill catalog for your assistant.
78
+
79
+ What do you want to do? ↑↓ move · enter select
80
+ Base install the essentials (orient + suggest + harness + init)
81
+ Base + Spec-Driven Development specifyplan implement → ship
82
+ Pick skills by hand, by area
83
+ Describe my project and let rsc choose
84
+ ```
85
+
86
+ Pick **by area** and you get a checkbox list — **↑↓ to move, space to toggle,
87
+ enter to confirm**:
88
+
89
+ ```
90
+ Languages: ↑↓ move · space toggle · a all · enter confirm
91
+ ❯ ◉ typescript
92
+ ◯ python
93
+ ◉ go
94
+ ◯ rust
81
95
  ```
82
96
 
83
- It detects your stack from the repo, maps your words to outcomes, installs the
84
- matching skills, then suggests what usually comes next.
97
+ It auto-detects your IDE and stack, installs only what you chose, then prints the
98
+ exact next steps for **Claude Code / Cursor / Codex / Gemini** — and from there
99
+ keeps proposing the skills a task needs.
85
100
 
86
101
  ---
87
102
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.2",
2
+ "version": "0.1.4",
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.4",
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,126 @@ 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
+ // ASCII wordmark shown at the top of the wizard.
37
+ export function banner() {
38
+ const tty = Boolean(stdout.isTTY);
39
+ const art = [
40
+ '',
41
+ ' ██████╗ ███████╗ ██████╗',
42
+ ' ██╔══██╗██╔════╝██╔════╝',
43
+ ' ██████╔╝███████╗██║ ',
44
+ ' ██╔══██╗╚════██║██║ ',
45
+ ' ██║ ██║███████║╚██████╗',
46
+ ' ╚═╝ ╚═╝╚══════╝ ╚═════╝',
47
+ ];
48
+ for (const l of art) say(tty ? C.cyan(l) : l);
49
+ say((tty ? C.dim(' 231 skills · one CLI · zero bloat') : ' 231 skills · one CLI · zero bloat'));
50
+ }
51
+
52
+ // Repaint a fixed block of lines in place (cursor ends just below the block).
53
+ function makePainter() {
54
+ let height = 0;
55
+ return (lines) => {
56
+ if (height) stdout.write(`\x1b[${height}A`);
57
+ stdout.write(lines.map((l) => `\x1b[2K${l}`).join('\n') + '\n');
58
+ height = lines.length;
59
+ };
60
+ }
61
+
62
+ function captureKeys(onKey) {
63
+ emitKeypressEvents(stdin);
64
+ if (stdin.isTTY) stdin.setRawMode(true);
65
+ stdin.resume();
66
+ const handler = (str, key) => onKey(str, key || {});
67
+ stdin.on('keypress', handler);
68
+ return () => {
69
+ stdin.off('keypress', handler);
70
+ if (stdin.isTTY) stdin.setRawMode(false);
71
+ stdin.pause();
72
+ };
73
+ }
74
+
75
+ // Single choice: ↑↓ to move, Enter to pick. options: [{ key, label }].
76
+ function tuiSelect(question, options) {
77
+ return new Promise((resolve) => {
78
+ let i = 0;
79
+ const paint = makePainter();
80
+ const render = () => paint([
81
+ C.bold(question),
82
+ C.dim(' ↑↓ move · enter select'),
83
+ ...options.map((o, idx) =>
84
+ idx === i ? `${C.cyan('❯')} ${C.cyan(o.label)}` : ` ${o.label}`),
85
+ ]);
86
+ render();
87
+ const stop = captureKeys((str, key) => {
88
+ if (key.name === 'up' || key.name === 'k') { i = (i - 1 + options.length) % options.length; render(); }
89
+ else if (key.name === 'down' || key.name === 'j') { i = (i + 1) % options.length; render(); }
90
+ else if (key.name === 'return') { stop(); resolve(options[i].key); }
91
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { stop(); stdout.write('\n'); process.exit(130); }
92
+ });
93
+ });
94
+ }
95
+
96
+ // Multi-select: ↑↓ move, space toggle, a = all, enter confirm. Viewport scrolls.
97
+ function tuiChecklist(title, items) {
98
+ return new Promise((resolve) => {
99
+ const VISIBLE = Math.min(items.length, 12);
100
+ const sel = new Set();
101
+ let cur = 0; let top = 0;
102
+ const paint = makePainter();
103
+ const render = () => {
104
+ if (cur < top) top = cur;
105
+ if (cur >= top + VISIBLE) top = cur - VISIBLE + 1;
106
+ const rows = [];
107
+ for (let r = top; r < top + VISIBLE; r++) {
108
+ const it = items[r];
109
+ const box = sel.has(r) ? C.green('◉') : '◯';
110
+ const line = `${box} ${it.label}`;
111
+ rows.push(r === cur ? `${C.cyan('❯')} ${C.cyan(line)}` : ` ${line}`);
112
+ }
113
+ const more = items.length > VISIBLE ? C.dim(` (${cur + 1}/${items.length})`) : '';
114
+ paint([
115
+ C.bold(title),
116
+ C.dim(' ↑↓ move · space toggle · a all · enter confirm') + more,
117
+ ...rows,
118
+ ]);
119
+ };
120
+ render();
121
+ const stop = captureKeys((str, key) => {
122
+ if (key.name === 'up' || key.name === 'k') { cur = (cur - 1 + items.length) % items.length; render(); }
123
+ else if (key.name === 'down' || key.name === 'j') { cur = (cur + 1) % items.length; render(); }
124
+ else if (key.name === 'space') { sel.has(cur) ? sel.delete(cur) : sel.add(cur); render(); }
125
+ else if (str === 'a') { if (sel.size === items.length) sel.clear(); else items.forEach((_, idx) => sel.add(idx)); render(); }
126
+ else if (key.name === 'return') { stop(); resolve([...sel].sort((x, y) => x - y).map((idx) => items[idx].id)); }
127
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { stop(); stdout.write('\n'); process.exit(130); }
128
+ });
129
+ });
130
+ }
131
+
132
+ // Public: single-choice menu. options: [{ key, label }]. Returns the chosen key.
21
133
  export async function select(question, options) {
134
+ if (interactive()) return tuiSelect(question, options);
135
+ // Fallback: typed number.
22
136
  say(question);
23
137
  options.forEach((o, i) => say(` ${i + 1}) ${o.label}`));
24
138
  const a = (await ask('> ')).toLowerCase();
@@ -28,19 +142,18 @@ export async function select(question, options) {
28
142
  return byKey ? byKey.key : null;
29
143
  }
30
144
 
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.
145
+ // Public: multi-select. items: array of strings or { id, label }. Returns ids.
33
146
  export async function pickFrom(title, items) {
147
+ const norm = items.map((it) => (typeof it === 'string' ? { id: it, label: it } : it));
148
+ if (interactive()) return tuiChecklist(title, norm);
149
+ // Fallback: comma-separated numbers.
34
150
  say(`\n${title}`);
35
- items.forEach((id, i) => say(` ${String(i + 1).padStart(2)}) ${id}`));
151
+ norm.forEach((it, i) => say(` ${String(i + 1).padStart(2)}) ${it.label}`));
36
152
  say(' Comma-separated numbers (e.g. 1,3,4), "all", or Enter for none.');
37
153
  const a = (await ask('> ')).trim();
38
154
  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)];
155
+ if (/^(all|todo|todas)$/i.test(a)) return norm.map((it) => it.id);
156
+ return [...new Set(
157
+ a.split(',').map((s) => parseInt(s.trim(), 10)).filter((x) => x >= 1 && x <= norm.length).map((x) => norm[x - 1].id),
158
+ )];
46
159
  }
package/scripts/rsc.js CHANGED
@@ -6,7 +6,7 @@ import { rank } from './consult.js';
6
6
  import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
7
7
  import { applyInstall, listInstalled, uninstall } from './install-apply.js';
8
8
  import { doctor } from './doctor.js';
9
- import { ask, say, yes, select, pickFrom } from './lib/ui.js';
9
+ import { ask, say, yes, select, pickFrom, banner } from './lib/ui.js';
10
10
  import { refreshRegistry, registryStatus } from './lib/registry.js';
11
11
  import { DOMAINS } from './lib/domains.js';
12
12
 
@@ -94,8 +94,9 @@ function printNextSteps(target, ids) {
94
94
 
95
95
  async function wizard() {
96
96
  const m = loadManifest();
97
- say('👋 rsc — the skill catalog for your assistant (Claude Code · Cursor · Codex · Gemini).');
98
- const choice = await select('\nWhat do you want to do?', [
97
+ banner();
98
+ say(' the skill catalog for your assistant (Claude Code · Cursor · Codex · Gemini)\n');
99
+ const choice = await select('What do you want to do?', [
99
100
  { key: 'base', label: 'Base install — the essentials (orient + suggest + harness + init)' },
100
101
  { key: 'sdd', label: 'Base + Spec-Driven Development — the specify → plan → implement → ship flow' },
101
102
  { key: 'manual', label: 'Pick skills by hand, by area' },