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 +37 -26
- package/bin/cli.js +220 -160
- package/install.sh +22 -10
- package/package.json +1 -1
- package/skills/README.md +25 -15
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
|
|
16
|
+
npx spirewise
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
You
|
|
19
|
+
You get a **full-screen interactive picker** with three steps:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
npx spirewise
|
|
33
|
-
npx spirewise
|
|
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
|
|
52
|
+
spirewise -sc both
|
|
41
53
|
```
|
|
42
54
|
|
|
43
55
|
## What gets installed where
|
|
44
56
|
|
|
45
|
-
| Agent |
|
|
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
|
|
55
|
-
|
|
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
|
-
##
|
|
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
|
|
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,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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
217
|
-
if (a
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
230
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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(`
|
|
270
|
-
console.log(` 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
|
-
|
|
280
|
-
let skills = o.skills
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] [
|
|
65
|
+
install.sh [scope] [-a agents] [-s skills]
|
|
60
66
|
|
|
61
67
|
Scope:
|
|
62
|
-
--workspace
|
|
63
|
-
--
|
|
64
|
-
--
|
|
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
|
-
|
|
69
|
-
--
|
|
70
|
-
--list
|
|
71
|
-
-h, --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
|
-
|
|
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
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
|
|
35
|
-
|
|
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
|
|
39
|
-
npx spirewise
|
|
40
|
-
npx spirewise
|
|
41
|
-
npx spirewise
|
|
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
|
-
|
|
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 (
|
|
57
|
+
Supported agents and their folders (workspace / global):
|
|
54
58
|
|
|
55
|
-
| Agent |
|
|
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
|
|