spirewise 1.0.3 → 1.1.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 CHANGED
@@ -10,66 +10,77 @@ project** or **globally** for every project.
10
10
 
11
11
  ## Install
12
12
 
13
- No clone needed:
13
+ No clone needed — just run:
14
14
 
15
15
  ```bash
16
- npx spirewise install
16
+ npx spirewise
17
17
  ```
18
18
 
19
- You'll be asked where to install:
19
+ You get a **full-screen interactive picker** with three steps:
20
20
 
21
21
  ```
22
- ==> Where should the skills be installed?
23
- 1) Workspace (this folder only the current project directory)
24
- 2) Global (your home folders, all projects)
25
- 3) Both
22
+ ══════════════════ Step 1/3 · Select skills ══════════════════
23
+ ◉ f6s-copywriting Generate F6S startup/company profile copy…
24
+ linkedin-copywriting Generate LinkedIn Company Page copy…
25
+
26
+ ↑/↓ move · space toggle · a all/none · enter confirm · esc cancel
26
27
  ```
27
28
 
28
- Skip the prompt with a scope flag:
29
+ Step 1 picks **skills**, step 2 picks **agents** (checkbox lists — move with
30
+ ↑/↓, toggle with **space**, **a** for all/none), step 3 picks **scope**
31
+ (Workspace / Global / Both). Press **enter** to confirm each step.
32
+
33
+ ### Or drive it with flags (skips the matching step)
34
+
35
+ ```bash
36
+ -s, --skills <a,b> skills to install (default: all / pick)
37
+ -a, --agents <a,b> agents to target (default: all / pick) alias: --agent
38
+ -sc, --scope <s> workspace | global | both
39
+ --workspace | --global | --both scope shortcuts
40
+ ```
29
41
 
30
42
  ```bash
31
- npx spirewise install --workspace # this folder only (current project)
32
- npx spirewise install --global # all projects (home folders)
33
- npx spirewise install --both # both
43
+ npx spirewise -sc both # pick skills+agents, scope both
44
+ npx spirewise -s f6s-copywriting -a claude,cursor -sc workspace
45
+ npx spirewise --global # pick skills+agents, scope global
34
46
  ```
35
47
 
36
48
  Or install the CLI globally:
37
49
 
38
50
  ```bash
39
51
  npm install -g spirewise
40
- spirewise install --both
52
+ spirewise -sc both
41
53
  ```
42
54
 
43
55
  ## What gets installed where
44
56
 
45
- | Agent | Project folder | Global folder | Format |
46
- |-------|----------------|---------------|--------|
57
+ | Agent | Workspace folder | Global folder | Format |
58
+ |-------|------------------|---------------|--------|
47
59
  | Claude Code | `.claude/skills/` | `~/.claude/skills/` | SKILL.md |
48
60
  | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | SKILL.md |
49
61
  | Cursor | `.cursor/rules/` | `~/.cursor/rules/` | `.mdc` rule |
50
62
  | Windsurf | `.windsurf/rules/` | `~/.codeium/windsurf/global_rules/` | `.md` rule |
51
63
  | Codex CLI | `.codex/skills/` | `~/.codex/skills/` | SKILL.md |
52
64
  | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` | SKILL.md |
65
+ | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | SKILL.md |
66
+ | Cline | `.clinerules/` | `~/.clinerules/` | `.md` rule |
67
+ | Roo Code | `.roo/rules/` | `~/.roo/rules/` | `.md` rule |
68
+ | Kilo Code | `.kilocode/rules/` | `~/.kilocode/rules/` | `.md` rule |
69
+ | Continue | `.continue/rules/` | `~/.continue/rules/` | `.md` rule |
70
+ | Amp | `.amp/rules/` | `~/.config/amp/rules/` | `.md` rule |
53
71
 
54
- Rule-based agents (Cursor, Windsurf) get a single rule file generated from the
55
- skill; the rest get the full `SKILL.md` folder.
72
+ Rule-based agents get a single rule file generated from the skill; the rest get
73
+ the full `SKILL.md` folder. (Newer tools' folders are best-effort conventions —
74
+ edit the `AGENTS` registry at the top of `bin/cli.js` if any differ.)
56
75
 
57
- ## Narrow it down
76
+ ## Inspect
58
77
 
59
78
  ```bash
60
- # only some agents
61
- npx spirewise install --both --agent claude,cursor
62
-
63
- # only some skills
64
- npx spirewise install f6s-copywriting --global
65
-
66
- # inspect
67
79
  npx spirewise list # available skills
68
80
  npx spirewise agents # supported agents + their folders
81
+ npx spirewise --help # all options
69
82
  ```
70
83
 
71
- Run `npx spirewise --help` for all options.
72
-
73
84
  ## Skills included
74
85
 
75
86
  | Skill | Output file | Purpose |
package/bin/cli.js CHANGED
@@ -2,26 +2,25 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * spirewise — install copywriting Agent Skills into ALL your AI agents at once.
5
+ * spirewise — install copywriting Agent Skills into your AI agents.
6
6
  *
7
- * Installs into each agent's correct folder, in the format that agent expects,
8
- * and asks whether to install for THIS project (cwd) or GLOBALLY (home dirs).
7
+ * Run with no flags for a full-screen interactive picker:
8
+ * npx spirewise (or: npx spirewise install)
9
9
  *
10
- * Usage:
11
- * npx spirewise install # interactive: pick scope, install all
12
- * npx spirewise install --project # this project only
13
- * npx spirewise install --global # global (home) only
14
- * npx spirewise install --both # project + global
15
- * npx spirewise install --agent claude,cursor # restrict agents
16
- * npx spirewise install f6s-copywriting # restrict skills
17
- * npx spirewise list # list skills
18
- * npx spirewise agents # list supported agents + folders
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
- const tag = ` Agent Skills · F6S & LinkedIn copywriting${PKG_VERSION ? ' · v' + PKG_VERSION : ''}`;
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` is resolved from cwd; `global` uses ~ for the home directory.
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
- label: 'Claude Code', format: 'skill',
93
- project: '.claude/skills', global: '~/.claude/skills',
94
- },
95
- copilot: {
96
- label: 'GitHub Copilot', format: 'skill',
97
- project: '.github/skills', global: '~/.copilot/skills',
98
- },
99
- cursor: {
100
- label: 'Cursor', format: 'rule', ext: '.mdc',
101
- project: '.cursor/rules', global: '~/.cursor/rules',
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
- const d = path.join(dest, entry.name);
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,87 +133,147 @@ 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
- function usage() {
180
- console.log(`spirewise install copywriting Agent Skills into all your AI agents
181
-
182
- Usage:
183
- spirewise install [skill ...] [options]
184
- spirewise list
185
- spirewise agents
186
-
187
- Scope (where to install):
188
- --workspace Install only into this folder / current project (alias: --project)
189
- --global Install only into global/home folders
190
- --both Install into both workspace and global
191
- (no scope flag -> you are asked interactively)
192
-
193
- Selection:
194
- [skill ...] Limit to named skills (default: all)
195
- --agent <a,b> Limit to named agents (default: all)
196
-
197
- Other:
198
- -h, --help Show this help
147
+ function box(lines) {
148
+ const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
149
+ const width = Math.max(...lines.map((l) => vis(l).length));
150
+ console.log(paint(RAW.green, ' ┌' + '─'.repeat(width + 2) + '┐'));
151
+ for (const l of lines) console.log(paint(RAW.green, ' │ ') + l + ' '.repeat(width - vis(l).length) + paint(RAW.green, ' │'));
152
+ console.log(paint(RAW.green, ' └' + '─'.repeat(width + 2) + '┘'));
153
+ }
199
154
 
200
- Supported agents:
201
- ${Object.entries(AGENTS).map(([k, a]) => ` - ${k} (${a.label})`).join('\n')}
155
+ // --- Interactive full-width selector ---------------------------------------
156
+ function interactiveSelect({ title, subtitle, items, multi = true, preselected = [] }) {
157
+ return new Promise((resolve) => {
158
+ const stdin = process.stdin, stdout = process.stdout;
159
+ if (!stdin.isTTY) { resolve(null); return; }
160
+
161
+ let index = 0;
162
+ const selected = new Set();
163
+ if (multi) items.forEach((it, i) => { if (preselected.includes(it.value)) selected.add(i); });
164
+ else { const pi = items.findIndex((it) => preselected.includes(it.value)); if (pi >= 0) index = pi; }
165
+
166
+ const cols = () => stdout.columns || 80;
167
+ let lastLines = 0;
168
+
169
+ const bar = (text) => {
170
+ const w = cols(), label = ` ${text} `;
171
+ const side = Math.max(0, w - label.length), left = Math.floor(side / 2);
172
+ return paint(RAW.cyan, '═'.repeat(left) + label + '═'.repeat(side - left));
173
+ };
174
+
175
+ function render() {
176
+ const lines = ['', bar(title)];
177
+ if (subtitle) lines.push(paint(RAW.dim, ' ' + subtitle));
178
+ lines.push('');
179
+ items.forEach((it, i) => {
180
+ const cur = i === index ? paint(RAW.cyan, '❯') : ' ';
181
+ const mark = multi ? (selected.has(i) ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'))
182
+ : (i === index ? paint(RAW.green, '◉') : paint(RAW.dim, '◯'));
183
+ const label = i === index ? paint(RAW.bold, it.label) : it.label;
184
+ const hint = it.hint ? paint(RAW.dim, ' ' + it.hint) : '';
185
+ lines.push(` ${cur} ${mark} ${label}${hint}`);
186
+ });
187
+ lines.push('');
188
+ lines.push(paint(RAW.dim, ' ' + (multi
189
+ ? '↑/↓ move · space toggle · a all/none · enter confirm · esc cancel'
190
+ : '↑/↓ move · enter confirm · esc cancel')));
191
+ if (lastLines > 0) stdout.write(`\x1b[${lastLines}A`);
192
+ stdout.write('\x1b[0J' + lines.join('\n') + '\n');
193
+ lastLines = lines.length;
194
+ }
202
195
 
203
- Skills:
204
- ${availableSkills().map((s) => ' - ' + s).join('\n') || ' (none found)'}
196
+ function cleanup() {
197
+ try { stdin.setRawMode(false); } catch (_) {}
198
+ stdin.pause();
199
+ stdin.removeListener('data', onData);
200
+ }
201
+ const finish = (r) => { cleanup(); resolve(r); };
202
+
203
+ function onData(s) {
204
+ if (s === '\x03' || s === '\x1b' || s === 'q') return finish(null); // ctrl-c / esc / q
205
+ if (s === '\r' || s === '\n') {
206
+ return finish(multi ? items.filter((_, i) => selected.has(i)).map((it) => it.value) : items[index].value);
207
+ }
208
+ if (s === '\x1b[A' || s === 'k') { index = (index - 1 + items.length) % items.length; return render(); }
209
+ if (s === '\x1b[B' || s === 'j') { index = (index + 1) % items.length; return render(); }
210
+ if (multi && s === ' ') { selected.has(index) ? selected.delete(index) : selected.add(index); return render(); }
211
+ if (multi && (s === 'a' || s === 'A')) {
212
+ if (selected.size === items.length) selected.clear(); else items.forEach((_, i) => selected.add(i));
213
+ return render();
214
+ }
215
+ if (!multi && s === ' ') return finish(items[index].value);
216
+ }
205
217
 
206
- Examples:
207
- npx spirewise install --both
208
- npx spirewise install --project --agent claude,cursor
209
- npx spirewise install f6s-copywriting --global
210
- `);
218
+ stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
219
+ stdin.on('data', onData);
220
+ render();
221
+ });
211
222
  }
212
223
 
224
+ // --- args -------------------------------------------------------------------
225
+ function splitList(v) { return String(v).split(',').map((s) => s.trim()).filter(Boolean); }
226
+
213
227
  function parseArgs(argv) {
214
- const o = { scope: null, agents: null, skills: [] };
228
+ const o = { scope: null, agents: null, skills: null, positional: [] };
229
+ const norm = (s) => (s === 'project' || s === 'workspace' || s === 'local') ? 'project' : s;
215
230
  for (let i = 0; i < argv.length; i++) {
216
- const a = argv[i];
217
- if (a === '--project' || a === '--workspace' || a === '--local') o.scope = 'project';
218
- else if (a === '--global') o.scope = 'global';
219
- else if (a === '--both') o.scope = 'both';
220
- else if (a === '--scope') o.scope = (argv[++i] || die('--scope needs a value'));
221
- else if (a === '--agent' || a === '--agents') o.agents = (argv[++i] || die('--agent needs a value')).split(',').map((s) => s.trim()).filter(Boolean);
222
- else if (a === '-h' || a === '--help') { usage(); process.exit(0); }
223
- else if (a.startsWith('-')) die(`Unknown option: ${a}`);
224
- else o.skills.push(a);
231
+ let a = argv[i], val = null;
232
+ if (a.startsWith('--') && a.includes('=')) { const idx = a.indexOf('='); val = a.slice(idx + 1); a = a.slice(0, idx); }
233
+ const next = () => (val !== null ? val : argv[++i]);
234
+ switch (a) {
235
+ case '-s': case '--skills': o.skills = splitList(next() || die('--skills needs a value')); break;
236
+ case '-a': case '--agents': case '--agent': o.agents = splitList(next() || die('--agents needs a value')); break;
237
+ case '-sc': case '--scope': o.scope = norm((next() || die('--scope needs a value')).toLowerCase()); break;
238
+ case '--workspace': case '--project': case '--local': o.scope = 'project'; break;
239
+ case '--global': o.scope = 'global'; break;
240
+ case '--both': o.scope = 'both'; break;
241
+ case '-h': case '--help': usage(); process.exit(0); break;
242
+ default:
243
+ if (a.startsWith('-')) die(`Unknown option: ${a}`);
244
+ o.positional.push(a);
245
+ }
225
246
  }
247
+ if (!o.skills && o.positional.length) o.skills = o.positional;
226
248
  return o;
227
249
  }
228
250
 
229
- function ask(question) {
230
- return new Promise((resolve) => {
231
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
232
- rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
233
- });
234
- }
251
+ function usage() {
252
+ console.log(`spirewise install copywriting Agent Skills into your AI agents
235
253
 
236
- async function promptScope() {
237
- if (!process.stdin.isTTY) {
238
- warn('No terminal detected; defaulting scope to "project". Use --global or --both to change.');
239
- return 'project';
240
- }
241
- info('Where should the skills be installed?');
242
- console.log(' 1) Workspace (this folder only the current project directory)');
243
- console.log(' 2) Global (your home folders applies to all projects)');
244
- console.log(' 3) Both');
245
- const ans = (await ask(' Choose 1/2/3 [1]: ')).toLowerCase();
246
- if (ans === '2' || ans === 'global') return 'global';
247
- if (ans === '3' || ans === 'both') return 'both';
248
- return 'project';
254
+ Usage:
255
+ spirewise [install] [options] run interactive picker for anything not set
256
+ spirewise list list available skills
257
+ spirewise agents list supported agents + folders
258
+
259
+ Options:
260
+ -s, --skills <a,b> skills to install (default: all / pick)
261
+ -a, --agents <a,b> agents to target (default: all / pick) alias: --agent
262
+ -sc, --scope <s> workspace | global | both (default: pick)
263
+ --workspace | --global | --both scope shortcuts
264
+ -h, --help
265
+
266
+ Agents:
267
+ ${Object.entries(AGENTS).map(([k, a]) => ` - ${k} (${a.label})`).join('\n')}
268
+
269
+ Skills:
270
+ ${availableSkills().map((s) => ' - ' + s).join('\n') || ' (none found)'}
271
+
272
+ Examples:
273
+ npx spirewise # full interactive picker
274
+ npx spirewise -sc both # pick skills+agents, scope=both
275
+ npx spirewise -s f6s-copywriting -a claude,cursor -sc workspace
276
+ `);
249
277
  }
250
278
 
251
279
  async function main() {
@@ -258,48 +286,75 @@ async function main() {
258
286
  if (available.length === 0) die('No skills found in package.');
259
287
 
260
288
  if (command === 'list') {
261
- info('Available skills:');
262
- available.forEach((s) => console.log(' - ' + s));
263
- return;
289
+ info('Available skills:'); available.forEach((s) => console.log(' - ' + s)); return;
264
290
  }
265
291
  if (command === 'agents') {
266
292
  info('Supported agents and their folders:');
267
293
  for (const [k, a] of Object.entries(AGENTS)) {
268
- console.log(` ${k} (${a.label}) [${a.format}]`);
269
- console.log(` project: ${a.project}`);
270
- console.log(` global: ${a.global}`);
294
+ console.log(` ${paint(RAW.bold, k)} (${a.label}) [${a.format}]`);
295
+ console.log(` workspace: ${a.project}`);
296
+ console.log(` global: ${a.global}`);
271
297
  }
272
298
  return;
273
299
  }
274
300
 
275
301
  const o = parseArgs(argv);
276
-
302
+ const tty = process.stdin.isTTY;
277
303
  banner();
278
304
 
279
- step(1, 'Scanning available skills');
280
- let skills = o.skills.length ? o.skills : available.slice();
281
- for (const s of skills) if (!available.includes(s)) die(`Unknown skill '${s}'. Run "spirewise list".`);
282
- substep(`${skills.length} skill(s): ${skills.join(', ')}`);
283
-
284
- step(2, 'Selecting target agents');
285
- let agentKeys = Object.keys(AGENTS);
286
- if (o.agents) {
287
- for (const k of o.agents) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
288
- agentKeys = o.agents;
289
- }
290
- substep(`${agentKeys.length} agent(s): ${agentKeys.join(', ')}`);
291
-
292
- step(3, 'Choosing install scope');
293
- let scope = o.scope || await promptScope();
305
+ // 1) SKILLS
306
+ let skills = o.skills;
307
+ if (skills) {
308
+ for (const s of skills) if (!available.includes(s)) die(`Unknown skill '${s}'. Run "spirewise list".`);
309
+ } else if (tty) {
310
+ skills = await interactiveSelect({
311
+ title: 'Step 1/3 · Select skills',
312
+ subtitle: 'Copy templates to install into your agents',
313
+ items: available.map((s) => ({ value: s, label: s, hint: skillHint(s) })),
314
+ multi: true, preselected: available,
315
+ });
316
+ if (skills === null) die('Cancelled.');
317
+ if (!skills.length) die('No skills selected.');
318
+ } else { warn('No terminal; defaulting to all skills.'); skills = available.slice(); }
319
+
320
+ // 2) AGENTS
321
+ let agentKeys = o.agents;
322
+ if (agentKeys) {
323
+ for (const k of agentKeys) if (!AGENTS[k]) die(`Unknown agent '${k}'. Run "spirewise agents".`);
324
+ } else if (tty) {
325
+ agentKeys = await interactiveSelect({
326
+ title: 'Step 2/3 · Select agents',
327
+ subtitle: 'Each agent gets the skills in its own folder + format',
328
+ items: Object.entries(AGENTS).map(([k, a]) => ({ value: k, label: a.label, hint: `(${k}) · ${a.format}` })),
329
+ multi: true, preselected: Object.keys(AGENTS),
330
+ });
331
+ if (agentKeys === null) die('Cancelled.');
332
+ if (!agentKeys.length) die('No agents selected.');
333
+ } else { warn('No terminal; defaulting to all agents.'); agentKeys = Object.keys(AGENTS); }
334
+
335
+ // 3) SCOPE
336
+ let scope = o.scope;
337
+ if (!scope && tty) {
338
+ scope = await interactiveSelect({
339
+ title: 'Step 3/3 · Select scope',
340
+ subtitle: 'Where should the skills live?',
341
+ items: [
342
+ { value: 'project', label: 'Workspace', hint: 'this folder only — the current project' },
343
+ { value: 'global', label: 'Global', hint: 'your home folders — applies to all projects' },
344
+ { value: 'both', label: 'Both', hint: 'workspace + global' },
345
+ ],
346
+ multi: false, preselected: ['project'],
347
+ });
348
+ if (scope === null) die('Cancelled.');
349
+ } else if (!scope) { warn('No terminal; defaulting scope to "workspace".'); scope = 'project'; }
294
350
  if (!['project', 'global', 'both'].includes(scope)) die(`Invalid scope '${scope}'.`);
295
351
  const scopes = scope === 'both' ? ['project', 'global'] : [scope];
296
- substep(`scope: ${paint(RAW.bold, scope === 'project' ? 'workspace' : scope)}`);
297
352
 
298
- step(4, 'Installing');
353
+ // INSTALL
354
+ console.log('');
355
+ info(`Installing ${paint(RAW.bold, String(skills.length))} skill(s) into ${paint(RAW.bold, String(agentKeys.length))} agent(s) · scope ${paint(RAW.bold, scope === 'project' ? 'workspace' : scope)}`);
299
356
  let count = 0;
300
- for (const sc of scopes) {
301
- for (const k of agentKeys) { installToAgent(k, AGENTS[k], sc, skills); count += skills.length; }
302
- }
357
+ for (const sc of scopes) for (const k of agentKeys) { installToAgent(k, AGENTS[k], sc, skills); count += skills.length; }
303
358
 
304
359
  console.log('');
305
360
  box([
@@ -310,4 +365,9 @@ async function main() {
310
365
  console.log('');
311
366
  }
312
367
 
313
- main().catch((e) => die(e && e.message ? e.message : String(e)));
368
+ if (require.main === module) {
369
+ main().catch((e) => die(e && e.message ? e.message : String(e)));
370
+ } else {
371
+ module.exports = { interactiveSelect, AGENTS, availableSkills, parseArgs };
372
+ }
373
+
package/install.sh CHANGED
@@ -30,6 +30,12 @@ AGENTS=(
30
30
  "windsurf|Windsurf|rule|.md|.windsurf/rules|HOME/.codeium/windsurf/global_rules"
31
31
  "codex|Codex CLI|skill||.codex/skills|HOME/.codex/skills"
32
32
  "gemini|Gemini CLI|skill||.gemini/skills|HOME/.gemini/skills"
33
+ "opencode|OpenCode|skill||.opencode/skills|HOME/.config/opencode/skills"
34
+ "cline|Cline|rule|.md|.clinerules|HOME/.clinerules"
35
+ "roo|Roo Code|rule|.md|.roo/rules|HOME/.roo/rules"
36
+ "kilocode|Kilo Code|rule|.md|.kilocode/rules|HOME/.kilocode/rules"
37
+ "continue|Continue|rule|.md|.continue/rules|HOME/.continue/rules"
38
+ "amp|Amp|rule|.md|.amp/rules|HOME/.config/amp/rules"
33
39
  )
34
40
 
35
41
  color() { printf '\033[%sm%s\033[0m' "$1" "$2"; }
@@ -56,21 +62,22 @@ usage() {
56
62
  spirewise — install copywriting Agent Skills into all your AI agents.
57
63
 
58
64
  Usage:
59
- install.sh [scope] [--agent a,b] [skill ...]
65
+ install.sh [scope] [-a agents] [-s skills]
60
66
 
61
67
  Scope:
62
- --workspace install only into this folder / current project (alias: --project)
63
- --global install only into global/home folders
64
- --both install into both
68
+ -sc, --scope <s> workspace | global | both
69
+ --workspace this folder / current project (alias: --project)
70
+ --global global/home folders
71
+ --both both
65
72
  (no scope -> you are asked interactively)
66
73
 
67
74
  Selection:
68
- [skill ...] limit to named skills (default: all)
69
- --agent <a,b> limit to named agents (default: all)
70
- --list list available skills
71
- -h, --help show this help
75
+ -s, --skills <a,b> limit to named skills (default: all)
76
+ -a, --agents <a,b> limit to named agents (default: all) alias: --agent
77
+ --list list available skills
78
+ -h, --help show this help
72
79
 
73
- Agents: claude copilot cursor windsurf codex gemini
80
+ Agents: claude copilot cursor windsurf codex gemini opencode cline roo kilocode continue amp
74
81
  EOF
75
82
  }
76
83
 
@@ -182,7 +189,12 @@ while [[ $# -gt 0 ]]; do
182
189
  --project|--workspace|--local) SCOPE="project"; shift ;;
183
190
  --global) SCOPE="global"; shift ;;
184
191
  --both) SCOPE="both"; shift ;;
185
- --agent|--agents) AGENT_FILTER="${2:?--agent needs a value}"; shift 2 ;;
192
+ -sc|--scope)
193
+ v="${2:?--scope needs a value}"; shift 2
194
+ case "$v" in workspace|project|local) SCOPE="project";; global) SCOPE="global";; both) SCOPE="both";; *) die "Invalid scope '$v'";; esac ;;
195
+ -a|--agent|--agents) AGENT_FILTER="${2:?--agents needs a value}"; shift 2 ;;
196
+ -s|--skills)
197
+ IFS=',' read -r -a _sk <<<"${2:?--skills needs a value}"; SELECTED+=("${_sk[@]}"); shift 2 ;;
186
198
  --list) DO_LIST=true; shift ;;
187
199
  -h|--help) usage; exit 0 ;;
188
200
  -*) die "Unknown option: $1" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spirewise",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Installable Agent Skills for copywriting (F6S & LinkedIn company pages) for GitHub Copilot, Claude Code, and Cursor.",
5
5
  "bin": {
6
6
  "spirewise": "bin/cli.js"
package/skills/README.md CHANGED
@@ -31,35 +31,45 @@ LinkedIn company page copy" and it will follow the skill: gather inputs, write t
31
31
 
32
32
  ## Install via command
33
33
 
34
- The easiest way is the npm CLI it installs into **all** supported agents at
35
- once and asks whether to install for the current **project** or **globally**:
34
+ The easiest way is the npm CLI. Run it with no flags for a full-screen
35
+ interactive picker (skills agents scope), or pass flags to skip steps:
36
36
 
37
37
  ```bash
38
- npx spirewise install # asks: project / global / both
39
- npx spirewise install --both # project + global, all agents
40
- npx spirewise install --workspace # this folder only (current project)
41
- npx spirewise install --global # all projects (home folders)
38
+ npx spirewise # interactive picker for everything
39
+ npx spirewise -sc both # pick skills+agents, scope = both
40
+ npx spirewise -s f6s-copywriting -a claude,cursor -sc workspace
41
+ npx spirewise list # list skills
42
+ npx spirewise agents # agents + folders
42
43
  ```
43
44
 
44
- Narrow it down:
45
+ Flags:
45
46
 
46
- ```bash
47
- npx spirewise install --both --agent claude,cursor # only some agents
48
- npx spirewise install f6s-copywriting --global # only some skills
49
- npx spirewise list # list skills
50
- npx spirewise agents # agents + folders
51
47
  ```
48
+ -s, --skills <a,b> skills to install (default: all / pick)
49
+ -a, --agents <a,b> agents to target (default: all / pick) alias: --agent
50
+ -sc, --scope <s> workspace | global | both
51
+ --workspace | --global | --both scope shortcuts
52
+ ```
53
+
54
+ In the picker: **↑/↓** move, **space** toggle, **a** all/none, **enter**
55
+ confirm, **esc** cancel.
52
56
 
53
- Supported agents and their folders (project / global):
57
+ Supported agents and their folders (workspace / global):
54
58
 
55
- | Agent | Project | Global | Format |
56
- |-------|---------|--------|--------|
59
+ | Agent | Workspace | Global | Format |
60
+ |-------|-----------|--------|--------|
57
61
  | Claude Code | `.claude/skills/` | `~/.claude/skills/` | SKILL.md |
58
62
  | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | SKILL.md |
59
63
  | Cursor | `.cursor/rules/` | `~/.cursor/rules/` | `.mdc` |
60
64
  | Windsurf | `.windsurf/rules/` | `~/.codeium/windsurf/global_rules/` | `.md` |
61
65
  | Codex CLI | `.codex/skills/` | `~/.codex/skills/` | SKILL.md |
62
66
  | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` | SKILL.md |
67
+ | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | SKILL.md |
68
+ | Cline | `.clinerules/` | `~/.clinerules/` | `.md` |
69
+ | Roo Code | `.roo/rules/` | `~/.roo/rules/` | `.md` |
70
+ | Kilo Code | `.kilocode/rules/` | `~/.kilocode/rules/` | `.md` |
71
+ | Continue | `.continue/rules/` | `~/.continue/rules/` | `.md` |
72
+ | Amp | `.amp/rules/` | `~/.config/amp/rules/` | `.md` |
63
73
 
64
74
  ### No-Node fallback: `install.sh`
65
75