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 +30 -15
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/lib/ui.js +127 -14
- package/scripts/rsc.js +4 -3
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 — specify → plan → 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
|
|
84
|
-
|
|
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
package/package.json
CHANGED
package/scripts/lib/ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline
|
|
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|
|
|
16
|
+
return /^(s|si|sí|y|yes|ok|sure|yeah)/i.test(s.trim());
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
.split(',')
|
|
42
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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' },
|