spirewise 1.0.3 → 1.6.0
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 +53 -25
- package/bin/cli.js +266 -164
- package/install.sh +55 -17
- package/package.json +1 -1
- package/skills/README.md +39 -14
- package/skills/f6s-copywriting/README.md +53 -0
- package/skills/linkedin-copywriting/README.md +49 -0
- package/skills/nvidia-inception-idea-booster/README.md +55 -0
- package/skills/nvidia-inception-idea-booster/SKILL.md +151 -0
- package/skills/nvidia-inception-starter/README.md +61 -0
- package/skills/nvidia-inception-starter/SKILL.md +179 -0
- package/skills/nvidia-product-inventor/README.md +66 -0
- package/skills/nvidia-product-inventor/SKILL.md +167 -0
package/bin/cli.js
CHANGED
|
@@ -2,26 +2,25 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* spirewise — install copywriting Agent Skills into
|
|
5
|
+
* spirewise — install copywriting Agent Skills into your AI agents.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Run with no flags for a full-screen interactive picker:
|
|
8
|
+
* npx spirewise (or: npx spirewise install)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* npx spirewise list
|
|
18
|
-
* npx spirewise agents
|
|
10
|
+
* Or drive everything from flags (skips the matching picker step):
|
|
11
|
+
* -s, --skills <a,b> skills to install (default: all)
|
|
12
|
+
* -a, --agents <a,b> agents to target (default: all) alias: --agent
|
|
13
|
+
* -sc, --scope <s> workspace | global | both
|
|
14
|
+
* --workspace|--global|--both scope shortcuts
|
|
15
|
+
*
|
|
16
|
+
* Other commands:
|
|
17
|
+
* npx spirewise list list skills
|
|
18
|
+
* npx spirewise agents list supported agents + folders
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const os = require('os');
|
|
24
|
-
const readline = require('readline');
|
|
25
24
|
|
|
26
25
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
27
26
|
const SKILLS_DIR = path.join(PKG_ROOT, 'skills');
|
|
@@ -34,8 +33,7 @@ const RAW = {
|
|
|
34
33
|
red: '\x1b[1;31m', cyan: '\x1b[1;36m', magenta: '\x1b[1;35m',
|
|
35
34
|
};
|
|
36
35
|
const paint = (code, s) => (USE_COLOR ? `${code}${s}${RAW.reset}` : s);
|
|
37
|
-
const c = USE_COLOR
|
|
38
|
-
? RAW
|
|
36
|
+
const c = USE_COLOR ? RAW
|
|
39
37
|
: { reset: '', bold: '', dim: '', blue: '', green: '', yellow: '', red: '', cyan: '', magenta: '' };
|
|
40
38
|
|
|
41
39
|
const info = (m) => console.log(`${c.blue}==>${c.reset} ${m}`);
|
|
@@ -58,60 +56,30 @@ const BANNER = [
|
|
|
58
56
|
function banner() {
|
|
59
57
|
console.log('');
|
|
60
58
|
for (const line of BANNER) console.log(paint(RAW.cyan, line));
|
|
61
|
-
|
|
62
|
-
console.log(paint(RAW.magenta, tag));
|
|
59
|
+
console.log(paint(RAW.magenta, ` Agent Skills · F6S & LinkedIn copywriting${PKG_VERSION ? ' · v' + PKG_VERSION : ''}`));
|
|
63
60
|
console.log('');
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
let STEP_TOTAL = 4;
|
|
67
|
-
function step(n, msg) {
|
|
68
|
-
console.log(`${paint(RAW.cyan, ' ▸')} ${paint(RAW.bold, `Step ${n}/${STEP_TOTAL}`)} ${msg}`);
|
|
69
|
-
}
|
|
70
|
-
function substep(msg) {
|
|
71
|
-
console.log(` ${paint(RAW.green, '✓')} ${msg}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function box(lines) {
|
|
75
|
-
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
76
|
-
const width = Math.max(...lines.map((l) => vis(l).length));
|
|
77
|
-
const top = ' ┌' + '─'.repeat(width + 2) + '┐';
|
|
78
|
-
const bot = ' └' + '─'.repeat(width + 2) + '┘';
|
|
79
|
-
console.log(paint(RAW.green, top));
|
|
80
|
-
for (const l of lines) console.log(paint(RAW.green, ' │ ') + l + ' '.repeat(width - vis(l).length) + paint(RAW.green, ' │'));
|
|
81
|
-
console.log(paint(RAW.green, bot));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
63
|
/**
|
|
85
64
|
* Agent registry. `format`:
|
|
86
65
|
* - 'skill': copy the skill folder verbatim (SKILL.md + any files)
|
|
87
66
|
* - 'rule' : write one rule file derived from SKILL.md (ext required)
|
|
88
|
-
* `project`
|
|
67
|
+
* `project` resolves from cwd; `global` uses ~ for the home directory.
|
|
68
|
+
* Newer tools' folders are best-effort conventions — edit freely.
|
|
89
69
|
*/
|
|
90
70
|
const AGENTS = {
|
|
91
|
-
claude:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
windsurf: {
|
|
104
|
-
label: 'Windsurf', format: 'rule', ext: '.md',
|
|
105
|
-
project: '.windsurf/rules', global: '~/.codeium/windsurf/global_rules',
|
|
106
|
-
},
|
|
107
|
-
codex: {
|
|
108
|
-
label: 'Codex CLI', format: 'skill',
|
|
109
|
-
project: '.codex/skills', global: '~/.codex/skills',
|
|
110
|
-
},
|
|
111
|
-
gemini: {
|
|
112
|
-
label: 'Gemini CLI', format: 'skill',
|
|
113
|
-
project: '.gemini/skills', global: '~/.gemini/skills',
|
|
114
|
-
},
|
|
71
|
+
claude: { label: 'Claude Code', format: 'skill', project: '.claude/skills', global: '~/.claude/skills' },
|
|
72
|
+
copilot: { label: 'GitHub Copilot', format: 'skill', project: '.github/skills', global: '~/.copilot/skills' },
|
|
73
|
+
cursor: { label: 'Cursor', format: 'rule', ext: '.mdc', project: '.cursor/rules', global: '~/.cursor/rules' },
|
|
74
|
+
windsurf: { label: 'Windsurf', format: 'rule', ext: '.md', project: '.windsurf/rules', global: '~/.codeium/windsurf/global_rules' },
|
|
75
|
+
codex: { label: 'Codex CLI', format: 'skill', project: '.codex/skills', global: '~/.codex/skills' },
|
|
76
|
+
gemini: { label: 'Gemini CLI', format: 'skill', project: '.gemini/skills', global: '~/.gemini/skills' },
|
|
77
|
+
opencode: { label: 'OpenCode', format: 'skill', project: '.opencode/skills', global: '~/.config/opencode/skills' },
|
|
78
|
+
cline: { label: 'Cline', format: 'rule', ext: '.md', project: '.clinerules', global: '~/.clinerules' },
|
|
79
|
+
roo: { label: 'Roo Code', format: 'rule', ext: '.md', project: '.roo/rules', global: '~/.roo/rules' },
|
|
80
|
+
kilocode: { label: 'Kilo Code', format: 'rule', ext: '.md', project: '.kilocode/rules', global: '~/.kilocode/rules' },
|
|
81
|
+
continue: { label: 'Continue', format: 'rule', ext: '.md', project: '.continue/rules', global: '~/.continue/rules' },
|
|
82
|
+
amp: { label: 'Amp', format: 'rule', ext: '.md', project: '.amp/rules', global: '~/.config/amp/rules' },
|
|
115
83
|
};
|
|
116
84
|
|
|
117
85
|
function expandHome(p) {
|
|
@@ -119,26 +87,21 @@ function expandHome(p) {
|
|
|
119
87
|
if (p.startsWith('~/')) return path.join(HOME, p.slice(2));
|
|
120
88
|
return p;
|
|
121
89
|
}
|
|
122
|
-
|
|
123
90
|
function resolveTarget(agent, scope) {
|
|
124
91
|
const rel = scope === 'project' ? agent.project : agent.global;
|
|
125
|
-
return scope === 'project'
|
|
126
|
-
? path.resolve(process.cwd(), rel)
|
|
127
|
-
: path.resolve(expandHome(rel));
|
|
92
|
+
return scope === 'project' ? path.resolve(process.cwd(), rel) : path.resolve(expandHome(rel));
|
|
128
93
|
}
|
|
129
94
|
|
|
130
95
|
function availableSkills() {
|
|
131
96
|
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
132
97
|
return fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
133
98
|
.filter((d) => d.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, d.name, 'SKILL.md')))
|
|
134
|
-
.map((d) => d.name)
|
|
135
|
-
.sort();
|
|
99
|
+
.map((d) => d.name).sort();
|
|
136
100
|
}
|
|
137
101
|
|
|
138
102
|
function readSkill(name) {
|
|
139
103
|
const raw = fs.readFileSync(path.join(SKILLS_DIR, name, 'SKILL.md'), 'utf8');
|
|
140
|
-
let description = name;
|
|
141
|
-
let body = raw;
|
|
104
|
+
let description = name, body = raw;
|
|
142
105
|
const m = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
143
106
|
if (m) {
|
|
144
107
|
body = m[2];
|
|
@@ -147,14 +110,19 @@ function readSkill(name) {
|
|
|
147
110
|
}
|
|
148
111
|
return { description, body: body.replace(/^\s+/, '') };
|
|
149
112
|
}
|
|
113
|
+
function skillHint(name) {
|
|
114
|
+
try {
|
|
115
|
+
const d = readSkill(name).description;
|
|
116
|
+
const first = d.split('. ')[0];
|
|
117
|
+
return first.length > 52 ? first.slice(0, 49) + '…' : first;
|
|
118
|
+
} catch (_) { return ''; }
|
|
119
|
+
}
|
|
150
120
|
|
|
151
121
|
function copyDir(src, dest) {
|
|
152
122
|
fs.mkdirSync(dest, { recursive: true });
|
|
153
123
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
154
|
-
const s = path.join(src, entry.name);
|
|
155
|
-
|
|
156
|
-
if (entry.isDirectory()) copyDir(s, d);
|
|
157
|
-
else fs.copyFileSync(s, d);
|
|
124
|
+
const s = path.join(src, entry.name), d = path.join(dest, entry.name);
|
|
125
|
+
if (entry.isDirectory()) copyDir(s, d); else fs.copyFileSync(s, d);
|
|
158
126
|
}
|
|
159
127
|
}
|
|
160
128
|
|
|
@@ -165,149 +133,283 @@ function installToAgent(agentKey, agent, scope, skills) {
|
|
|
165
133
|
if (agent.format === 'skill') {
|
|
166
134
|
const dest = path.join(targetDir, skill);
|
|
167
135
|
copyDir(path.join(SKILLS_DIR, skill), dest);
|
|
168
|
-
console.log(` ${paint(RAW.green, '✓')} ${paint(RAW.bold, agent.label)} ${c.dim}(${scope})${c.reset} ${skill} ${c.dim}→ ${dest}${c.reset}`);
|
|
136
|
+
console.log(` ${paint(RAW.green, '✓')} ${paint(RAW.bold, agent.label)} ${c.dim}(${scope === 'project' ? 'workspace' : scope})${c.reset} ${skill} ${c.dim}→ ${dest}${c.reset}`);
|
|
169
137
|
} else {
|
|
170
138
|
const { description, body } = readSkill(skill);
|
|
171
139
|
const front = `---\ndescription: ${description}\nalwaysApply: false\n---\n\n`;
|
|
172
140
|
const file = path.join(targetDir, skill + (agent.ext || '.md'));
|
|
173
141
|
fs.writeFileSync(file, front + body);
|
|
174
|
-
console.log(` ${paint(RAW.green, '✓')} ${paint(RAW.bold, agent.label)} ${c.dim}(${scope})${c.reset} ${skill} ${c.dim}→ ${file}${c.reset}`);
|
|
142
|
+
console.log(` ${paint(RAW.green, '✓')} ${paint(RAW.bold, agent.label)} ${c.dim}(${scope === 'project' ? 'workspace' : scope})${c.reset} ${skill} ${c.dim}→ ${file}${c.reset}`);
|
|
175
143
|
}
|
|
176
144
|
}
|
|
177
145
|
}
|
|
178
146
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
147
|
+
// Returns the number of items actually removed.
|
|
148
|
+
function removeFromAgent(agentKey, agent, scope, skills) {
|
|
149
|
+
const targetDir = resolveTarget(agent, scope);
|
|
150
|
+
const tag = scope === 'project' ? 'workspace' : scope;
|
|
151
|
+
let removed = 0;
|
|
152
|
+
for (const skill of skills) {
|
|
153
|
+
const target = agent.format === 'skill'
|
|
154
|
+
? path.join(targetDir, skill)
|
|
155
|
+
: path.join(targetDir, skill + (agent.ext || '.md'));
|
|
156
|
+
if (fs.existsSync(target)) {
|
|
157
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
158
|
+
removed++;
|
|
159
|
+
console.log(` ${paint(RAW.green, '✓')} ${paint(RAW.bold, agent.label)} ${c.dim}(${tag})${c.reset} ${skill} ${c.dim}removed ← ${target}${c.reset}`);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(` ${paint(RAW.dim, '·')} ${agent.label} ${c.dim}(${tag})${c.reset} ${skill} ${c.dim}— not found, skipped${c.reset}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Tidy up the now-empty rules/skills dir we may have created.
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length === 0) fs.rmdirSync(targetDir);
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
return removed;
|
|
169
|
+
}
|
|
196
170
|
|
|
197
|
-
|
|
198
|
-
-
|
|
171
|
+
function box(lines) {
|
|
172
|
+
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
173
|
+
const width = Math.max(...lines.map((l) => vis(l).length));
|
|
174
|
+
console.log(paint(RAW.green, ' ┌' + '─'.repeat(width + 2) + '┐'));
|
|
175
|
+
for (const l of lines) console.log(paint(RAW.green, ' │ ') + l + ' '.repeat(width - vis(l).length) + paint(RAW.green, ' │'));
|
|
176
|
+
console.log(paint(RAW.green, ' └' + '─'.repeat(width + 2) + '┘'));
|
|
177
|
+
}
|
|
199
178
|
|
|
200
|
-
|
|
201
|
-
|
|
179
|
+
// --- Interactive full-width selector ---------------------------------------
|
|
180
|
+
function interactiveSelect({ title, subtitle, items, multi = true, preselected = [] }) {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
const stdin = process.stdin, stdout = process.stdout;
|
|
183
|
+
if (!stdin.isTTY) { resolve(null); return; }
|
|
184
|
+
|
|
185
|
+
let index = 0;
|
|
186
|
+
const selected = new Set();
|
|
187
|
+
if (multi) items.forEach((it, i) => { if (preselected.includes(it.value)) selected.add(i); });
|
|
188
|
+
else { const pi = items.findIndex((it) => preselected.includes(it.value)); if (pi >= 0) index = pi; }
|
|
189
|
+
|
|
190
|
+
const cols = () => stdout.columns || 80;
|
|
191
|
+
let lastLines = 0;
|
|
192
|
+
|
|
193
|
+
const bar = (text) => {
|
|
194
|
+
const w = cols(), label = ` ${text} `;
|
|
195
|
+
const side = Math.max(0, w - label.length), left = Math.floor(side / 2);
|
|
196
|
+
return paint(RAW.cyan, '═'.repeat(left) + label + '═'.repeat(side - left));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function render() {
|
|
200
|
+
const lines = ['', bar(title)];
|
|
201
|
+
if (subtitle) lines.push(paint(RAW.dim, ' ' + subtitle));
|
|
202
|
+
lines.push('');
|
|
203
|
+
items.forEach((it, i) => {
|
|
204
|
+
const cur = i === index ? paint(RAW.cyan, '❯') : ' ';
|
|
205
|
+
const mark = multi ? (selected.has(i) ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'))
|
|
206
|
+
: (i === index ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'));
|
|
207
|
+
const label = i === index ? paint(RAW.bold, it.label) : it.label;
|
|
208
|
+
const hint = it.hint ? paint(RAW.dim, ' ' + it.hint) : '';
|
|
209
|
+
lines.push(` ${cur} ${mark} ${label}${hint}`);
|
|
210
|
+
});
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(paint(RAW.dim, ' ' + (multi
|
|
213
|
+
? '↑/↓ move · space toggle · a all/none · enter confirm · esc cancel'
|
|
214
|
+
: '↑/↓ move · enter confirm · esc cancel')));
|
|
215
|
+
if (lastLines > 0) stdout.write(`\x1b[${lastLines}A`);
|
|
216
|
+
stdout.write('\x1b[0J' + lines.join('\n') + '\n');
|
|
217
|
+
lastLines = lines.length;
|
|
218
|
+
}
|
|
202
219
|
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
function cleanup() {
|
|
221
|
+
try { stdin.setRawMode(false); } catch (_) {}
|
|
222
|
+
stdin.pause();
|
|
223
|
+
stdin.removeListener('data', onData);
|
|
224
|
+
}
|
|
225
|
+
const finish = (r) => { cleanup(); resolve(r); };
|
|
226
|
+
|
|
227
|
+
function onData(s) {
|
|
228
|
+
if (s === '\x03' || s === '\x1b' || s === 'q') return finish(null); // ctrl-c / esc / q
|
|
229
|
+
if (s === '\r' || s === '\n') {
|
|
230
|
+
return finish(multi ? items.filter((_, i) => selected.has(i)).map((it) => it.value) : items[index].value);
|
|
231
|
+
}
|
|
232
|
+
if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return render(); }
|
|
233
|
+
if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return render(); }
|
|
234
|
+
if (multi && s === ' ') { selected.has(index) ? selected.delete(index) : selected.add(index); return render(); }
|
|
235
|
+
if (multi && (s === 'a' || s === 'A')) {
|
|
236
|
+
if (selected.size === items.length) selected.clear(); else items.forEach((_, i) => selected.add(i));
|
|
237
|
+
return render();
|
|
238
|
+
}
|
|
239
|
+
if (!multi && s === ' ') return finish(items[index].value);
|
|
240
|
+
}
|
|
205
241
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`);
|
|
242
|
+
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
|
|
243
|
+
stdin.on('data', onData);
|
|
244
|
+
render();
|
|
245
|
+
});
|
|
211
246
|
}
|
|
212
247
|
|
|
248
|
+
// --- args -------------------------------------------------------------------
|
|
249
|
+
function splitList(v) { return String(v).split(',').map((s) => s.trim()).filter(Boolean); }
|
|
250
|
+
|
|
213
251
|
function parseArgs(argv) {
|
|
214
|
-
const o = { scope: null, agents: null, skills: [] };
|
|
252
|
+
const o = { scope: null, agents: null, skills: null, positional: [] };
|
|
253
|
+
const norm = (s) => (s === 'project' || s === 'workspace' || s === 'local') ? 'project' : s;
|
|
215
254
|
for (let i = 0; i < argv.length; i++) {
|
|
216
|
-
|
|
217
|
-
if (a
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
let a = argv[i], val = null;
|
|
256
|
+
if (a.startsWith('--') && a.includes('=')) { const idx = a.indexOf('='); val = a.slice(idx + 1); a = a.slice(0, idx); }
|
|
257
|
+
const next = () => (val !== null ? val : argv[++i]);
|
|
258
|
+
switch (a) {
|
|
259
|
+
case '-s': case '--skills': o.skills = splitList(next() || die('--skills needs a value')); break;
|
|
260
|
+
case '-a': case '--agents': case '--agent': o.agents = splitList(next() || die('--agents needs a value')); break;
|
|
261
|
+
case '-sc': case '--scope': o.scope = norm((next() || die('--scope needs a value')).toLowerCase()); break;
|
|
262
|
+
case '--workspace': case '--project': case '--local': o.scope = 'project'; break;
|
|
263
|
+
case '--global': o.scope = 'global'; break;
|
|
264
|
+
case '--both': o.scope = 'both'; break;
|
|
265
|
+
case '-h': case '--help': usage(); process.exit(0); break;
|
|
266
|
+
default:
|
|
267
|
+
if (a.startsWith('-')) die(`Unknown option: ${a}`);
|
|
268
|
+
o.positional.push(a);
|
|
269
|
+
}
|
|
225
270
|
}
|
|
271
|
+
if (!o.skills && o.positional.length) o.skills = o.positional;
|
|
226
272
|
return o;
|
|
227
273
|
}
|
|
228
274
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
232
|
-
rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
|
|
233
|
-
});
|
|
234
|
-
}
|
|
275
|
+
function usage() {
|
|
276
|
+
console.log(`spirewise — install copywriting Agent Skills into your AI agents
|
|
235
277
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
278
|
+
Usage:
|
|
279
|
+
spirewise [install] [options] run interactive picker for anything not set
|
|
280
|
+
spirewise remove [options] uninstall skills (alias: uninstall)
|
|
281
|
+
spirewise list list available skills
|
|
282
|
+
spirewise agents list supported agents + folders
|
|
283
|
+
|
|
284
|
+
Options:
|
|
285
|
+
-s, --skills <a,b> skills to install/remove (default: all / pick)
|
|
286
|
+
-a, --agents <a,b> agents to target (default: all / pick) alias: --agent
|
|
287
|
+
-sc, --scope <s> workspace | global | both (default: pick)
|
|
288
|
+
--workspace | --global | --both scope shortcuts
|
|
289
|
+
-h, --help
|
|
290
|
+
|
|
291
|
+
Agents:
|
|
292
|
+
${Object.entries(AGENTS).map(([k, a]) => ` - ${k} (${a.label})`).join('\n')}
|
|
293
|
+
|
|
294
|
+
Skills:
|
|
295
|
+
${availableSkills().map((s) => ' - ' + s).join('\n') || ' (none found)'}
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
npx spirewise # full interactive picker
|
|
299
|
+
npx spirewise -sc both # pick skills+agents, scope=both
|
|
300
|
+
npx spirewise -s f6s-copywriting -a claude,cursor -sc workspace
|
|
301
|
+
npx spirewise remove -sc both # uninstall (pick skills+agents)
|
|
302
|
+
`);
|
|
249
303
|
}
|
|
250
304
|
|
|
251
305
|
async function main() {
|
|
252
306
|
let argv = process.argv.slice(2);
|
|
253
307
|
let command = 'install';
|
|
254
|
-
if (['list', 'install', 'agents'].includes(argv[0])) { command = argv[0]; argv = argv.slice(1); }
|
|
308
|
+
if (['list', 'install', 'agents', 'remove', 'uninstall'].includes(argv[0])) { command = argv[0]; argv = argv.slice(1); }
|
|
255
309
|
else if (argv[0] === '-h' || argv[0] === '--help') { usage(); return; }
|
|
256
310
|
|
|
311
|
+
const action = (command === 'remove' || command === 'uninstall') ? 'remove' : 'install';
|
|
312
|
+
|
|
257
313
|
const available = availableSkills();
|
|
258
314
|
if (available.length === 0) die('No skills found in package.');
|
|
259
315
|
|
|
260
316
|
if (command === 'list') {
|
|
261
|
-
info('Available skills:');
|
|
262
|
-
available.forEach((s) => console.log(' - ' + s));
|
|
263
|
-
return;
|
|
317
|
+
info('Available skills:'); available.forEach((s) => console.log(' - ' + s)); return;
|
|
264
318
|
}
|
|
265
319
|
if (command === 'agents') {
|
|
266
320
|
info('Supported agents and their folders:');
|
|
267
321
|
for (const [k, a] of Object.entries(AGENTS)) {
|
|
268
|
-
console.log(` ${k} (${a.label}) [${a.format}]`);
|
|
269
|
-
console.log(`
|
|
270
|
-
console.log(` global:
|
|
322
|
+
console.log(` ${paint(RAW.bold, k)} (${a.label}) [${a.format}]`);
|
|
323
|
+
console.log(` workspace: ${a.project}`);
|
|
324
|
+
console.log(` global: ${a.global}`);
|
|
271
325
|
}
|
|
272
326
|
return;
|
|
273
327
|
}
|
|
274
328
|
|
|
275
329
|
const o = parseArgs(argv);
|
|
276
|
-
|
|
330
|
+
const tty = process.stdin.isTTY;
|
|
331
|
+
const verb = action === 'remove' ? 'remove' : 'install';
|
|
332
|
+
const Verb = action === 'remove' ? 'Remove' : 'Install';
|
|
277
333
|
banner();
|
|
278
334
|
|
|
279
|
-
|
|
280
|
-
let skills = o.skills
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
335
|
+
// 1) SKILLS
|
|
336
|
+
let skills = o.skills;
|
|
337
|
+
if (skills) {
|
|
338
|
+
for (const s of skills) if (!available.includes(s)) die(`Unknown skill '${s}'. Run "spirewise list".`);
|
|
339
|
+
} else if (tty) {
|
|
340
|
+
skills = await interactiveSelect({
|
|
341
|
+
title: `Step 1/3 · Select skills to ${verb}`,
|
|
342
|
+
subtitle: action === 'remove' ? 'These will be deleted from the chosen agents' : 'Copy templates to install into your agents',
|
|
343
|
+
items: available.map((s) => ({ value: s, label: s, hint: skillHint(s) })),
|
|
344
|
+
multi: true, preselected: available,
|
|
345
|
+
});
|
|
346
|
+
if (skills === null) die('Cancelled.');
|
|
347
|
+
if (!skills.length) die('No skills selected.');
|
|
348
|
+
} else { warn('No terminal; defaulting to all skills.'); skills = available.slice(); }
|
|
349
|
+
|
|
350
|
+
// 2) AGENTS
|
|
351
|
+
let agentKeys = o.agents;
|
|
352
|
+
if (agentKeys) {
|
|
353
|
+
for (const k of agentKeys) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
|
|
354
|
+
} else if (tty) {
|
|
355
|
+
agentKeys = await interactiveSelect({
|
|
356
|
+
title: `Step 2/3 · Select agents`,
|
|
357
|
+
subtitle: action === 'remove' ? 'Skills are removed from each agent’s folder' : 'Each agent gets the skills in its own folder + format',
|
|
358
|
+
items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k}) · ${a.format}` })),
|
|
359
|
+
multi: true, preselected: Object.keys(AGENTS),
|
|
360
|
+
});
|
|
361
|
+
if (agentKeys === null) die('Cancelled.');
|
|
362
|
+
if (!agentKeys.length) die('No agents selected.');
|
|
363
|
+
} else { warn('No terminal; defaulting to all agents.'); agentKeys = Object.keys(AGENTS); }
|
|
364
|
+
|
|
365
|
+
// 3) SCOPE
|
|
366
|
+
let scope = o.scope;
|
|
367
|
+
if (!scope && tty) {
|
|
368
|
+
scope = await interactiveSelect({
|
|
369
|
+
title: 'Step 3/3 · Select scope',
|
|
370
|
+
subtitle: action === 'remove' ? 'Where to remove the skills from?' : 'Where should the skills live?',
|
|
371
|
+
items: [
|
|
372
|
+
{ value: 'project', label: 'Workspace', hint: 'this folder only — the current project' },
|
|
373
|
+
{ value: 'global', label: 'Global', hint: 'your home folders — applies to all projects' },
|
|
374
|
+
{ value: 'both', label: 'Both', hint: 'workspace + global' },
|
|
375
|
+
],
|
|
376
|
+
multi: false, preselected: ['project'],
|
|
377
|
+
});
|
|
378
|
+
if (scope === null) die('Cancelled.');
|
|
379
|
+
} else if (!scope) { warn('No terminal; defaulting scope to "workspace".'); scope = 'project'; }
|
|
294
380
|
if (!['project', 'global', 'both'].includes(scope)) die(`Invalid scope '${scope}'.`);
|
|
295
381
|
const scopes = scope === 'both' ? ['project', 'global'] : [scope];
|
|
296
|
-
substep(`scope: ${paint(RAW.bold, scope === 'project' ? 'workspace' : scope)}`);
|
|
297
382
|
|
|
298
|
-
|
|
383
|
+
// ACTION
|
|
384
|
+
console.log('');
|
|
385
|
+
info(`${Verb}ing ${paint(RAW.bold, String(skills.length))} skill(s) ${action === 'remove' ? 'from' : 'into'} ${paint(RAW.bold, String(agentKeys.length))} agent(s) · scope ${paint(RAW.bold, scope === 'project' ? 'workspace' : scope)}`);
|
|
386
|
+
|
|
299
387
|
let count = 0;
|
|
300
|
-
for (const sc of scopes) {
|
|
301
|
-
|
|
388
|
+
for (const sc of scopes) for (const k of agentKeys) {
|
|
389
|
+
if (action === 'remove') count += removeFromAgent(k, AGENTS[k], sc, skills);
|
|
390
|
+
else { installToAgent(k, AGENTS[k], sc, skills); count += skills.length; }
|
|
302
391
|
}
|
|
303
392
|
|
|
304
393
|
console.log('');
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
394
|
+
if (action === 'remove') {
|
|
395
|
+
box([
|
|
396
|
+
`Removed ${paint(RAW.bold, String(count))} item(s) from ${paint(RAW.bold, String(agentKeys.length))} agent(s)`,
|
|
397
|
+
`scope: ${scopes.map((s) => (s === 'project' ? 'workspace' : s)).join(' + ')}`,
|
|
398
|
+
count === 0 ? `nothing matched — already clean` : `done — skills removed`,
|
|
399
|
+
]);
|
|
400
|
+
} else {
|
|
401
|
+
box([
|
|
402
|
+
`Installed ${paint(RAW.bold, String(skills.length))} skill(s) into ${paint(RAW.bold, String(agentKeys.length))} agent(s)`,
|
|
403
|
+
`scope: ${scopes.map((s) => (s === 'project' ? 'workspace' : s)).join(' + ')} · ${count} item(s) written`,
|
|
404
|
+
`next: open your agent and say "write our F6S profile copy"`,
|
|
405
|
+
]);
|
|
406
|
+
}
|
|
310
407
|
console.log('');
|
|
311
408
|
}
|
|
312
409
|
|
|
313
|
-
|
|
410
|
+
if (require.main === module) {
|
|
411
|
+
main().catch((e) => die(e && e.message ? e.message : String(e)));
|
|
412
|
+
} else {
|
|
413
|
+
module.exports = { interactiveSelect, AGENTS, availableSkills, parseArgs };
|
|
414
|
+
}
|
|
415
|
+
|