@ericrisco/rsc 0.1.11 → 0.1.13
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/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/lib/ui.js +38 -12
- package/scripts/rsc.js +45 -31
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/lib/ui.js
CHANGED
|
@@ -57,13 +57,33 @@ function hsl(h, s = 1, l = 0.6) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// One frame: rainbow per character, diagonal phase, revealed up to `cols`.
|
|
60
|
-
|
|
60
|
+
const SPARK = ['✦', '✧', '⋆', '✺', '·', '*'];
|
|
61
|
+
const SPARK_C = ['\x1b[1;97m', '\x1b[1;93m', '\x1b[1;96m']; // bright white / yellow / cyan
|
|
62
|
+
|
|
63
|
+
// `n` random twinkles as a Map "r,c" -> pre-coloured glyph string.
|
|
64
|
+
function sparkles(n, rows, W) {
|
|
65
|
+
const m = new Map();
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
const r = Math.floor(Math.random() * rows);
|
|
68
|
+
const c = Math.floor(Math.random() * W);
|
|
69
|
+
const g = SPARK[Math.floor(Math.random() * SPARK.length)];
|
|
70
|
+
m.set(`${r},${c}`, SPARK_C[Math.floor(Math.random() * SPARK_C.length)] + g);
|
|
71
|
+
}
|
|
72
|
+
return m;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// One frame: rainbow per character (or white), revealed up to `cols`, with
|
|
76
|
+
// sparkles overlaid on top wherever the map has a position.
|
|
77
|
+
function frame(phase, cols, sparks, white) {
|
|
61
78
|
return ART.map((line, r) => {
|
|
62
79
|
const chars = [...line];
|
|
63
80
|
let out = '';
|
|
64
81
|
for (let c = 0; c < chars.length && c < cols; c++) {
|
|
82
|
+
const sp = sparks && sparks.get(`${r},${c}`);
|
|
83
|
+
if (sp) { out += sp; continue; }
|
|
65
84
|
const ch = chars[c];
|
|
66
85
|
if (ch === ' ') { out += ' '; continue; }
|
|
86
|
+
if (white) { out += `\x1b[1;97m${ch}`; continue; }
|
|
67
87
|
const [rr, gg, bb] = hsl((c * 5 + r * 14 + phase * 20) % 360);
|
|
68
88
|
out += `\x1b[1;38;2;${rr};${gg};${bb}m${ch}`;
|
|
69
89
|
}
|
|
@@ -83,27 +103,31 @@ export async function banner() {
|
|
|
83
103
|
const W = Math.max(...ART.map((l) => [...l].length));
|
|
84
104
|
stdout.write('\x1b[?25l'); // hide cursor
|
|
85
105
|
say('');
|
|
86
|
-
// 1) letters slide in left → right
|
|
106
|
+
// 1) letters slide in left → right, a sparkle riding the drawing edge
|
|
87
107
|
let first = true;
|
|
88
108
|
for (let cols = 2; cols <= W; cols += 2) {
|
|
89
109
|
if (!first) stdout.write(`\x1b[${rows}A`);
|
|
90
110
|
first = false;
|
|
91
|
-
|
|
111
|
+
const edge = new Map();
|
|
112
|
+
const er = Math.floor(Math.random() * rows);
|
|
113
|
+
edge.set(`${er},${Math.min(cols - 1, W - 1)}`, '\x1b[1;97m✦');
|
|
114
|
+
if (Math.random() < 0.5) edge.set(`${Math.floor(Math.random() * rows)},${Math.min(cols, W - 1)}`, '\x1b[1;93m·');
|
|
115
|
+
stdout.write(frame(0, cols, edge));
|
|
92
116
|
await sleep(12);
|
|
93
117
|
}
|
|
94
|
-
// 2) flowing rainbow diagonal sweeps
|
|
118
|
+
// 2) flowing rainbow diagonal sweeps with twinkling sparkles
|
|
95
119
|
for (let phase = 1; phase <= 30; phase++) {
|
|
96
120
|
stdout.write(`\x1b[${rows}A`);
|
|
97
|
-
stdout.write(frame(phase, W));
|
|
121
|
+
stdout.write(frame(phase, W, sparkles(4, rows, W)));
|
|
98
122
|
await sleep(26);
|
|
99
123
|
}
|
|
100
|
-
// 3) double white flash — the pop
|
|
124
|
+
// 3) double white flash with a sparkle burst — the pop
|
|
101
125
|
for (let f = 0; f < 2; f++) {
|
|
102
126
|
stdout.write(`\x1b[${rows}A`);
|
|
103
|
-
stdout.write(
|
|
127
|
+
stdout.write(frame(0, W, sparkles(12, rows, W), true));
|
|
104
128
|
await sleep(60);
|
|
105
129
|
stdout.write(`\x1b[${rows}A`);
|
|
106
|
-
stdout.write(frame(15, W));
|
|
130
|
+
stdout.write(frame(15, W, sparkles(3, rows, W)));
|
|
107
131
|
await sleep(60);
|
|
108
132
|
}
|
|
109
133
|
// settle on a final rainbow snapshot
|
|
@@ -143,7 +167,7 @@ function tuiSelect(question, options) {
|
|
|
143
167
|
const paint = makePainter();
|
|
144
168
|
const render = () => paint([
|
|
145
169
|
C.bold(question),
|
|
146
|
-
C.dim(' ↑↓ move · enter select'),
|
|
170
|
+
C.dim(' ↑↓ move · enter select · esc back'),
|
|
147
171
|
...options.map((o, idx) =>
|
|
148
172
|
idx === i ? `${C.cyan('❯')} ${C.cyan(o.label)}` : ` ${o.label}`),
|
|
149
173
|
]);
|
|
@@ -152,7 +176,8 @@ function tuiSelect(question, options) {
|
|
|
152
176
|
if (key.name === 'up' || key.name === 'k') { i = (i - 1 + options.length) % options.length; render(); }
|
|
153
177
|
else if (key.name === 'down' || key.name === 'j') { i = (i + 1) % options.length; render(); }
|
|
154
178
|
else if (key.name === 'return') { stop(); resolve(options[i].key); }
|
|
155
|
-
else if (key.name === 'escape'
|
|
179
|
+
else if (key.name === 'escape') { stop(); resolve(null); } // back / cancel
|
|
180
|
+
else if (key.ctrl && key.name === 'c') { stop(); stdout.write('\n'); process.exit(130); } // hard quit
|
|
156
181
|
});
|
|
157
182
|
});
|
|
158
183
|
}
|
|
@@ -177,7 +202,7 @@ function tuiChecklist(title, items) {
|
|
|
177
202
|
const more = items.length > VISIBLE ? C.dim(` (${cur + 1}/${items.length})`) : '';
|
|
178
203
|
paint([
|
|
179
204
|
C.bold(title),
|
|
180
|
-
C.dim(' ↑↓ move · space toggle · a all · enter confirm') + more,
|
|
205
|
+
C.dim(' ↑↓ move · space toggle · a all · enter confirm · esc back') + more,
|
|
181
206
|
...rows,
|
|
182
207
|
]);
|
|
183
208
|
};
|
|
@@ -188,7 +213,8 @@ function tuiChecklist(title, items) {
|
|
|
188
213
|
else if (key.name === 'space') { sel.has(cur) ? sel.delete(cur) : sel.add(cur); render(); }
|
|
189
214
|
else if (str === 'a') { if (sel.size === items.length) sel.clear(); else items.forEach((_, idx) => sel.add(idx)); render(); }
|
|
190
215
|
else if (key.name === 'return') { stop(); resolve([...sel].sort((x, y) => x - y).map((idx) => items[idx].id)); }
|
|
191
|
-
else if (key.name === 'escape'
|
|
216
|
+
else if (key.name === 'escape') { stop(); resolve(null); } // back without changing
|
|
217
|
+
else if (key.ctrl && key.name === 'c') { stop(); stdout.write('\n'); process.exit(130); } // hard quit
|
|
192
218
|
});
|
|
193
219
|
});
|
|
194
220
|
}
|
package/scripts/rsc.js
CHANGED
|
@@ -31,15 +31,19 @@ async function recommendIds(query, { labeledOnly = false } = {}) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Pick skills by browsing the domains, accumulating across rounds.
|
|
34
|
+
// Returns the chosen skill ids, or null if the user backed out to the main menu.
|
|
34
35
|
async function manualSelect() {
|
|
35
36
|
const chosen = new Set();
|
|
36
37
|
for (;;) {
|
|
37
38
|
const opts = DOMAINS.map((d, i) => ({ key: String(i), label: `${d.title} (${d.ids.length})` }));
|
|
38
|
-
opts.push({ key: 'done', label: chosen.size ? `✅ Finish & install (${chosen.size} chosen)` : 'Finish
|
|
39
|
+
opts.push({ key: 'done', label: chosen.size ? `✅ Finish & install (${chosen.size} chosen)` : 'Finish (install nothing)' });
|
|
40
|
+
opts.push({ key: 'back', label: '← Back to the main menu' });
|
|
39
41
|
const k = await select('\nWhich area do you want to install skills from?', opts);
|
|
40
|
-
if (k === '
|
|
42
|
+
if (k === 'back' || k === null) return null; // esc or Back → main menu
|
|
43
|
+
if (k === 'done') break;
|
|
41
44
|
const d = DOMAINS[parseInt(k, 10)];
|
|
42
|
-
|
|
45
|
+
const picked = await pickFrom(`${d.title}:`, d.ids); // null = esc → leave this area unchanged
|
|
46
|
+
if (picked) picked.forEach((id) => chosen.add(id));
|
|
43
47
|
say(` → ${chosen.size} skills chosen so far.`);
|
|
44
48
|
}
|
|
45
49
|
return [...chosen];
|
|
@@ -54,6 +58,7 @@ async function selectAgents() {
|
|
|
54
58
|
label: `${t.label} (${t.hint})${t.id === detected ? ' ⟵ detected here' : ''}`,
|
|
55
59
|
}));
|
|
56
60
|
const chosen = await pickFrom('Which assistants do you want to install for? (space to toggle, a = all)', items);
|
|
61
|
+
if (chosen === null) return null; // esc → back to the main menu
|
|
57
62
|
return chosen.length ? chosen : [detected];
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -91,39 +96,48 @@ async function wizard() {
|
|
|
91
96
|
const m = loadManifest();
|
|
92
97
|
await banner();
|
|
93
98
|
say(' the skill catalog for your assistant (Claude Code · Codex · Cursor · Gemini · Antigravity)\n');
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
// Navigable loop: esc / "← Back" / "no" all return here instead of quitting.
|
|
100
|
+
for (;;) {
|
|
101
|
+
const choice = await select('What do you want to do?', [
|
|
102
|
+
{ key: 'base', label: 'Base install — the essentials (orient + suggest + harness + init)' },
|
|
103
|
+
{ key: 'sdd', label: 'Base + Spec-Driven Development — the specify → plan → implement → ship flow' },
|
|
104
|
+
{ key: 'manual', label: 'Pick skills by hand, by area' },
|
|
105
|
+
]);
|
|
106
|
+
if (choice === null) { say('\nOK — nothing installed. Anytime: npx @ericrisco/rsc'); return; }
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
let ids;
|
|
109
|
+
if (choice === 'base') ids = skillsForProfile(m, 'minimal');
|
|
110
|
+
else if (choice === 'sdd') ids = skillsForProfile(m, 'core');
|
|
111
|
+
else if (choice === 'manual') {
|
|
112
|
+
const picked = await manualSelect();
|
|
113
|
+
if (picked === null) continue; // backed out → re-show this menu
|
|
114
|
+
ids = picked;
|
|
115
|
+
} else continue;
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// The floor is always installed: the compass + the detector.
|
|
118
|
+
ids = [...new Set(['orient', 'suggest', ...ids])];
|
|
119
|
+
if (ids.length <= 2 && choice !== 'base') {
|
|
120
|
+
say('\nNothing chosen — back to the menu.');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
const targets = await selectAgents();
|
|
125
|
+
if (targets === null) continue; // esc in the assistant picker → back to menu
|
|
126
|
+
say(`\nI'll install ${ids.length} skills for: ${targets.join(', ')}`);
|
|
127
|
+
say(' ' + ids.join(', '));
|
|
128
|
+
say(' (real files live once in .rsc/skills/ — each assistant just links to them)');
|
|
129
|
+
if (!(await confirm('Install it?'))) {
|
|
130
|
+
say('Cancelled — back to the menu.');
|
|
131
|
+
continue; // "no" / esc → back to menu, not quit
|
|
132
|
+
}
|
|
133
|
+
for (const target of targets) {
|
|
134
|
+
await applyInstall({ skillIds: ids, target });
|
|
135
|
+
say(` ✅ ${target}`);
|
|
136
|
+
}
|
|
137
|
+
say(`\n✅ Installed ${ids.length} skills for ${targets.length} assistant(s).`);
|
|
138
|
+
printNextSteps(targets, ids);
|
|
119
139
|
return;
|
|
120
140
|
}
|
|
121
|
-
for (const target of targets) {
|
|
122
|
-
await applyInstall({ skillIds: ids, target });
|
|
123
|
-
say(` ✅ ${target}`);
|
|
124
|
-
}
|
|
125
|
-
say(`\n✅ Installed ${ids.length} skills for ${targets.length} assistant(s).`);
|
|
126
|
-
printNextSteps(targets, ids);
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
async function main() {
|