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 +21 -15
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/lib/ui.js +111 -14
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
84
|
-
|
|
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
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,110 @@ 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
|
+
// 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
|
-
//
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
.split(',')
|
|
42
|
-
|
|
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
|
}
|