@morya-ui/setup 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/package.json +1 -1
- package/src/__tests__/skills.test.mjs +174 -0
- package/src/cli.mjs +71 -6
- package/src/skills.mjs +246 -9
- package/template/AGENTS.md +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,12 @@ npx @morya-ui/setup ai --skills=all
|
|
|
38
38
|
# Only Cursor + VS Code MCP targets (still writes .mcp.json)
|
|
39
39
|
npx @morya-ui/setup ai --editors=cursor,vscode
|
|
40
40
|
|
|
41
|
+
# Narrow skill agents (default: cursor, github-copilot, zed, claude-code, windsurf)
|
|
42
|
+
npx @morya-ui/setup ai --agents=github-copilot,zed
|
|
43
|
+
|
|
44
|
+
# Install companion skills to every skills-CLI agent (many dirs)
|
|
45
|
+
npx @morya-ui/setup ai --agents=all
|
|
46
|
+
|
|
41
47
|
# Refresh AI template / MCP without touching dependencies
|
|
42
48
|
npx @morya-ui/setup ai --skip-install
|
|
43
49
|
```
|
|
@@ -50,8 +56,9 @@ npx @morya-ui/setup ai --skip-install
|
|
|
50
56
|
| `--pm pnpm\|yarn\|npm` | Package manager (default: detect from lockfile, else `pnpm`) |
|
|
51
57
|
| `--skills <list>` | Comma-separated skill ids, or `all` (skips the prompt) |
|
|
52
58
|
| `--editors <list>` | MCP targets: `cursor`, `vscode`, `zed`, or `all` (default: all three). Always also writes root `.mcp.json` |
|
|
59
|
+
| `--agents <list>` | Skill agents: `cursor`, `github-copilot`, `zed`, `claude-code`, `windsurf` (default), or `all` (`-a '*'`) |
|
|
53
60
|
| `--yes` / `-y` | Use default skills without prompting |
|
|
54
|
-
| `--force` | Overwrite existing template files
|
|
61
|
+
| `--force` | Overwrite existing template files, MCP `morya-ui` entry, and skill-dir links |
|
|
55
62
|
| `--dry-run` | Print actions without writing or installing |
|
|
56
63
|
| `--skip-install` | Do not install / upgrade `morya-ui` or `@morya-ui/*` |
|
|
57
64
|
| `--skip-template` | Do not copy AI skill / rules / docs (also skips companion skill install) |
|
|
@@ -75,7 +82,8 @@ Catalog: [`catalog/skills.json`](./catalog/skills.json).
|
|
|
75
82
|
|
|
76
83
|
- Dependencies: always install / upgrade `morya-ui@latest` and any existing `@morya-ui/*` (e.g. `@morya-ui/nuxt`) to `@latest` unless `--skip-install`.
|
|
77
84
|
- Template files and `.cursor/rules/*`: **skip** if the destination exists (unless `--force`).
|
|
78
|
-
- Companion skills (`skills-cli`): always reinstall/update to **latest** when selected.
|
|
85
|
+
- Companion skills (`skills-cli`): always reinstall/update to **latest** when selected (default agents: the five listed under `--agents`).
|
|
86
|
+
- Mirrored skill dirs (`.claude/skills`, `.windsurf/skills`, `.github/skills`): **skip** if the destination exists (unless `--force`).
|
|
79
87
|
- MCP configs: merge other servers / settings; skip existing `morya-ui` entry unless `--force`. New or forced entries use `npx -y @morya-ui/mcp@latest`.
|
|
80
88
|
- `package.json` `check:colors`: add only if missing (unless `--force`).
|
|
81
89
|
- Styles: inject only when an entry file is found and the import is not already present.
|
|
@@ -90,10 +98,20 @@ From the package `template/` (synced from repo `design-kit/`):
|
|
|
90
98
|
- `.cursor/rules/` — Cursor-specific always-apply rules
|
|
91
99
|
- `scripts/check-raw-colors.mjs`
|
|
92
100
|
|
|
93
|
-
Via `npx skills add …` when optional companions are selected:
|
|
101
|
+
Via `npx skills add …` when optional companions are selected (default agents: Cursor, GitHub Copilot, Zed, Claude Code, Windsurf):
|
|
94
102
|
|
|
95
103
|
- `.agents/skills/frontend-design/`, `fixing-accessibility/`, and/or `impeccable/` (latest upstream)
|
|
96
104
|
|
|
105
|
+
Setup then mirrors selected skills from `.agents/skills/` into agent-specific dirs that do not share that path:
|
|
106
|
+
|
|
107
|
+
| Agent | Extra project dir |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| Claude Code | `.claude/skills/` |
|
|
110
|
+
| Windsurf | `.windsurf/skills/` |
|
|
111
|
+
| GitHub Copilot | `.github/skills/` (in addition to `.agents/skills`) |
|
|
112
|
+
|
|
113
|
+
Cursor / Zed / Copilot already read `.agents/skills` directly.
|
|
114
|
+
|
|
97
115
|
### MCP (multi-editor)
|
|
98
116
|
|
|
99
117
|
| Editor | Path | JSON key |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morya-ui/setup",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.8",
|
|
5
5
|
"description": "One-shot setup for morya-ui: install the library, multi-editor MCP, Agent skills, and design config.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/morya-space/morya-ui/tree/main/packages/setup#readme",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { afterEach, describe, it } from 'node:test'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SKILL_AGENTS,
|
|
8
|
+
extraSkillDirsForAgents,
|
|
9
|
+
installSkillsCli,
|
|
10
|
+
parseAgentsFlag,
|
|
11
|
+
skillAgentCliFlags,
|
|
12
|
+
syncProjectSkillsToAgents,
|
|
13
|
+
} from '../skills.mjs'
|
|
14
|
+
|
|
15
|
+
/** @type {string[]} */
|
|
16
|
+
const temps = []
|
|
17
|
+
|
|
18
|
+
function makeCwd() {
|
|
19
|
+
const cwd = mkdtempSync(join(tmpdir(), 'morya-setup-skills-'))
|
|
20
|
+
temps.push(cwd)
|
|
21
|
+
return cwd
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
while (temps.length) {
|
|
26
|
+
rmSync(temps.pop(), { recursive: true, force: true })
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function seedSkill(cwd, name) {
|
|
31
|
+
const dir = join(cwd, '.agents', 'skills', name)
|
|
32
|
+
mkdirSync(dir, { recursive: true })
|
|
33
|
+
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: test\n---\n`, 'utf8')
|
|
34
|
+
return dir
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('parseAgentsFlag', () => {
|
|
38
|
+
it('defaults to the five curated agents', () => {
|
|
39
|
+
assert.deepEqual(parseAgentsFlag(), [...DEFAULT_SKILL_AGENTS])
|
|
40
|
+
assert.deepEqual(parseAgentsFlag(''), [...DEFAULT_SKILL_AGENTS])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns * for all', () => {
|
|
44
|
+
assert.equal(parseAgentsFlag('all'), '*')
|
|
45
|
+
assert.equal(parseAgentsFlag('*'), '*')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('parses and dedupes a subset', () => {
|
|
49
|
+
assert.deepEqual(parseAgentsFlag('zed,cursor,zed'), ['zed', 'cursor'])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('rejects unknown agents', () => {
|
|
53
|
+
assert.throws(() => parseAgentsFlag('cursor,not-a-real-agent'), /Unknown skill agent/)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('skillAgentCliFlags', () => {
|
|
58
|
+
it('emits one -a per default agent', () => {
|
|
59
|
+
const flags = skillAgentCliFlags(DEFAULT_SKILL_AGENTS)
|
|
60
|
+
assert.match(flags, /-a cursor/)
|
|
61
|
+
assert.match(flags, /-a github-copilot/)
|
|
62
|
+
assert.match(flags, /-a zed/)
|
|
63
|
+
assert.match(flags, /-a claude-code/)
|
|
64
|
+
assert.match(flags, /-a windsurf/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('emits wildcard for all', () => {
|
|
68
|
+
assert.equal(skillAgentCliFlags('*'), "-a '*'")
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('extraSkillDirsForAgents', () => {
|
|
73
|
+
it('mirrors claude / windsurf / github dirs for defaults', () => {
|
|
74
|
+
assert.deepEqual(extraSkillDirsForAgents(DEFAULT_SKILL_AGENTS), [
|
|
75
|
+
'.github/skills',
|
|
76
|
+
'.claude/skills',
|
|
77
|
+
'.windsurf/skills',
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('skips extras for agents that only use .agents/skills', () => {
|
|
82
|
+
assert.deepEqual(extraSkillDirsForAgents(['cursor', 'zed']), [])
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('installSkillsCli', () => {
|
|
87
|
+
it('dry-run builds multi-agent commands', () => {
|
|
88
|
+
const cwd = makeCwd()
|
|
89
|
+
const skills = [
|
|
90
|
+
{
|
|
91
|
+
id: 'frontend-design',
|
|
92
|
+
name: 'Frontend Design',
|
|
93
|
+
description: 'x',
|
|
94
|
+
install: 'skills-cli',
|
|
95
|
+
source: 'anthropics/skills',
|
|
96
|
+
skill: 'frontend-design',
|
|
97
|
+
path: '.agents/skills/frontend-design',
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
const result = installSkillsCli(cwd, ['frontend-design'], skills, {
|
|
101
|
+
dryRun: true,
|
|
102
|
+
agents: ['cursor', 'zed', 'github-copilot'],
|
|
103
|
+
})
|
|
104
|
+
assert.equal(result.dryRun, true)
|
|
105
|
+
assert.equal(result.commands.length, 1)
|
|
106
|
+
assert.match(result.commands[0], /skills add anthropics\/skills/)
|
|
107
|
+
assert.match(result.commands[0], /-s frontend-design/)
|
|
108
|
+
assert.match(result.commands[0], /-a cursor/)
|
|
109
|
+
assert.match(result.commands[0], /-a zed/)
|
|
110
|
+
assert.match(result.commands[0], /-a github-copilot/)
|
|
111
|
+
assert.doesNotMatch(result.commands[0], /-a cursor -y$/)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('syncProjectSkillsToAgents', () => {
|
|
116
|
+
it('links into claude / windsurf / github skill dirs', () => {
|
|
117
|
+
const cwd = makeCwd()
|
|
118
|
+
seedSkill(cwd, 'morya-ui-pages')
|
|
119
|
+
|
|
120
|
+
const { results } = syncProjectSkillsToAgents(cwd, ['morya-ui-pages'], {
|
|
121
|
+
agents: DEFAULT_SKILL_AGENTS,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const byDir = Object.fromEntries(results.map((r) => [r.dir, r]))
|
|
125
|
+
assert.ok(byDir['.claude/skills'])
|
|
126
|
+
assert.ok(byDir['.windsurf/skills'])
|
|
127
|
+
assert.ok(byDir['.github/skills'])
|
|
128
|
+
assert.ok(['linked', 'copied'].includes(byDir['.claude/skills'].action))
|
|
129
|
+
|
|
130
|
+
assert.ok(readFileSync(join(cwd, '.claude', 'skills', 'morya-ui-pages', 'SKILL.md'), 'utf8'))
|
|
131
|
+
assert.ok(readFileSync(join(cwd, '.windsurf', 'skills', 'morya-ui-pages', 'SKILL.md'), 'utf8'))
|
|
132
|
+
assert.ok(readFileSync(join(cwd, '.github', 'skills', 'morya-ui-pages', 'SKILL.md'), 'utf8'))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('skips existing links unless force', () => {
|
|
136
|
+
const cwd = makeCwd()
|
|
137
|
+
seedSkill(cwd, 'morya-ui-pages')
|
|
138
|
+
mkdirSync(join(cwd, '.claude', 'skills', 'morya-ui-pages'), { recursive: true })
|
|
139
|
+
writeFileSync(join(cwd, '.claude', 'skills', 'morya-ui-pages', 'SKILL.md'), 'old\n', 'utf8')
|
|
140
|
+
|
|
141
|
+
const skipped = syncProjectSkillsToAgents(cwd, ['morya-ui-pages'], {
|
|
142
|
+
agents: ['claude-code'],
|
|
143
|
+
})
|
|
144
|
+
assert.equal(skipped.results[0].action, 'skipped')
|
|
145
|
+
assert.equal(
|
|
146
|
+
readFileSync(join(cwd, '.claude', 'skills', 'morya-ui-pages', 'SKILL.md'), 'utf8'),
|
|
147
|
+
'old\n',
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const forced = syncProjectSkillsToAgents(cwd, ['morya-ui-pages'], {
|
|
151
|
+
agents: ['claude-code'],
|
|
152
|
+
force: true,
|
|
153
|
+
})
|
|
154
|
+
assert.ok(['linked', 'copied'].includes(forced.results[0].action))
|
|
155
|
+
assert.match(
|
|
156
|
+
readFileSync(join(cwd, '.claude', 'skills', 'morya-ui-pages', 'SKILL.md'), 'utf8'),
|
|
157
|
+
/morya-ui-pages/,
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('dry-run does not create dirs', () => {
|
|
162
|
+
const cwd = makeCwd()
|
|
163
|
+
seedSkill(cwd, 'morya-ui-pages')
|
|
164
|
+
const { results } = syncProjectSkillsToAgents(cwd, ['morya-ui-pages'], {
|
|
165
|
+
agents: ['claude-code'],
|
|
166
|
+
dryRun: true,
|
|
167
|
+
})
|
|
168
|
+
assert.equal(results[0].action, 'linked')
|
|
169
|
+
assert.throws(
|
|
170
|
+
() => readFileSync(join(cwd, '.claude', 'skills', 'morya-ui-pages', 'SKILL.md')),
|
|
171
|
+
/ENOENT/,
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
})
|
package/src/cli.mjs
CHANGED
|
@@ -5,11 +5,17 @@ import { installMoryaUi } from './install.mjs'
|
|
|
5
5
|
import { DEFAULT_EDITORS, mergeMcpConfig, parseEditorsFlag } from './mcp.mjs'
|
|
6
6
|
import { ensureCheckColorsScript } from './package-json.mjs'
|
|
7
7
|
import {
|
|
8
|
+
DEFAULT_SKILL_AGENTS,
|
|
8
9
|
buildAiInclude,
|
|
9
10
|
installSkillsCli,
|
|
11
|
+
listCanonicalSkillFolders,
|
|
10
12
|
loadSkillsCatalog,
|
|
13
|
+
parseAgentsFlag,
|
|
11
14
|
parseSkillsFlag,
|
|
15
|
+
resolveSkillAgents,
|
|
12
16
|
resolveSkillSelection,
|
|
17
|
+
skillFolderNames,
|
|
18
|
+
syncProjectSkillsToAgents,
|
|
13
19
|
} from './skills.mjs'
|
|
14
20
|
import { ensureStylesImport } from './styles.mjs'
|
|
15
21
|
|
|
@@ -54,6 +60,7 @@ Commands:
|
|
|
54
60
|
Default command:
|
|
55
61
|
- install / upgrade morya-ui@latest and any existing @morya-ui/* to @latest
|
|
56
62
|
- copy DESIGN.md, AGENTS.md, selected Agent skills, Cursor rules
|
|
63
|
+
- install companion skills + sync into multi-agent skill dirs
|
|
57
64
|
- merge MCP configs for @morya-ui/mcp@latest (Cursor, VS Code, Zed, + .mcp.json)
|
|
58
65
|
- inject import 'morya-ui/styles.css' into the app entry when found
|
|
59
66
|
- add check:colors script when missing
|
|
@@ -63,8 +70,9 @@ Options:
|
|
|
63
70
|
--pm <name> Package manager: pnpm | yarn | npm (auto-detect by lockfile)
|
|
64
71
|
--skills <list> Comma-separated skill ids, or "all" (skips interactive prompt)
|
|
65
72
|
--editors <list> MCP targets: cursor,vscode,zed (default: all); always also writes .mcp.json
|
|
73
|
+
--agents <list> Skill agents: cursor,github-copilot,zed,claude-code,windsurf (default), or all
|
|
66
74
|
--yes Use default skills without prompting (CI / non-interactive)
|
|
67
|
-
--force Overwrite existing template files and
|
|
75
|
+
--force Overwrite existing template files, MCP entry, and skill dir links
|
|
68
76
|
--dry-run Print actions without writing or installing
|
|
69
77
|
--skip-install Skip dependency install / upgrade
|
|
70
78
|
--skip-template Skip copying AI template files
|
|
@@ -88,6 +96,7 @@ export function parseArgs(argv) {
|
|
|
88
96
|
pm: undefined,
|
|
89
97
|
skills: undefined,
|
|
90
98
|
editors: undefined,
|
|
99
|
+
agents: undefined,
|
|
91
100
|
yes: false,
|
|
92
101
|
force: false,
|
|
93
102
|
dryRun: false,
|
|
@@ -165,6 +174,16 @@ export function parseArgs(argv) {
|
|
|
165
174
|
options.editors = arg.slice('--editors='.length)
|
|
166
175
|
continue
|
|
167
176
|
}
|
|
177
|
+
if (arg === '--agents') {
|
|
178
|
+
const value = argv[++i]
|
|
179
|
+
if (!value) throw new Error('--agents requires a comma-separated list or "all"')
|
|
180
|
+
options.agents = value
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if (arg.startsWith('--agents=')) {
|
|
184
|
+
options.agents = arg.slice('--agents='.length)
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
168
187
|
throw new Error(`Unknown argument: ${arg}`)
|
|
169
188
|
}
|
|
170
189
|
|
|
@@ -198,10 +217,13 @@ export async function runSetup(options) {
|
|
|
198
217
|
pm,
|
|
199
218
|
skills: skillsFlag,
|
|
200
219
|
editors: editorsFlag,
|
|
220
|
+
agents: agentsFlag,
|
|
201
221
|
yes,
|
|
202
222
|
} = options
|
|
203
223
|
|
|
204
224
|
const editors = editorsFlag != null ? parseEditorsFlag(editorsFlag) : [...DEFAULT_EDITORS]
|
|
225
|
+
const agents = agentsFlag != null ? parseAgentsFlag(agentsFlag) : [...DEFAULT_SKILL_AGENTS]
|
|
226
|
+
const agentsLabel = agents === '*' ? 'all (*)' : resolveSkillAgents(agents).join(',')
|
|
205
227
|
|
|
206
228
|
console.log(`@morya-ui/setup [${mode}] → ${cwd}${dryRun ? ' (dry-run)' : ''}`)
|
|
207
229
|
console.log('')
|
|
@@ -221,6 +243,7 @@ export async function runSetup(options) {
|
|
|
221
243
|
}
|
|
222
244
|
aiInclude = buildAiInclude(selectedSkills, catalog.skills)
|
|
223
245
|
console.log(`Skills: ${selectedSkills.join(', ')}`)
|
|
246
|
+
console.log(`Skill agents: ${agentsLabel}`)
|
|
224
247
|
console.log('')
|
|
225
248
|
}
|
|
226
249
|
|
|
@@ -236,8 +259,21 @@ export async function runSetup(options) {
|
|
|
236
259
|
})
|
|
237
260
|
|
|
238
261
|
const remoteSkills = skipTemplate || !selectedSkills.length
|
|
239
|
-
? { installed: [], commands: [], skipped: true }
|
|
240
|
-
: installSkillsCli(cwd, selectedSkills, catalog.skills, { dryRun })
|
|
262
|
+
? { installed: [], commands: [], agents, skipped: true }
|
|
263
|
+
: installSkillsCli(cwd, selectedSkills, catalog.skills, { dryRun, agents })
|
|
264
|
+
|
|
265
|
+
const skillNamesForSync = needsTemplate
|
|
266
|
+
? [
|
|
267
|
+
...new Set([
|
|
268
|
+
...skillFolderNames(selectedSkills, catalog.skills),
|
|
269
|
+
...(dryRun ? [] : listCanonicalSkillFolders(cwd)),
|
|
270
|
+
]),
|
|
271
|
+
]
|
|
272
|
+
: []
|
|
273
|
+
|
|
274
|
+
const skillSync = skipTemplate || !skillNamesForSync.length
|
|
275
|
+
? { results: [], skipped: true }
|
|
276
|
+
: syncProjectSkillsToAgents(cwd, skillNamesForSync, { force, dryRun, agents })
|
|
241
277
|
|
|
242
278
|
const mcp = skipMcp
|
|
243
279
|
? { skipped: true, results: [] }
|
|
@@ -278,10 +314,27 @@ export async function runSetup(options) {
|
|
|
278
314
|
if (remoteSkills.skipped) {
|
|
279
315
|
// no companions selected or template step skipped
|
|
280
316
|
} else if (remoteSkills.dryRun) {
|
|
281
|
-
console.log(`Skills CLI: dry-run — would install ${remoteSkills.installed.join(', ')}`)
|
|
317
|
+
console.log(`Skills CLI: dry-run — would install ${remoteSkills.installed.join(', ')} → ${agentsLabel}`)
|
|
282
318
|
for (const command of remoteSkills.commands) console.log(` $ ${command}`)
|
|
283
319
|
} else {
|
|
284
|
-
console.log(`Skills CLI: installed ${remoteSkills.installed.join(', ')} (latest)`)
|
|
320
|
+
console.log(`Skills CLI: installed ${remoteSkills.installed.join(', ')} (latest) → ${agentsLabel}`)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (skillSync.skipped) {
|
|
324
|
+
// nothing to mirror
|
|
325
|
+
} else {
|
|
326
|
+
const linked = skillSync.results.filter((r) => r.action === 'linked' || r.action === 'copied')
|
|
327
|
+
const skippedLinks = skillSync.results.filter((r) => r.action === 'skipped')
|
|
328
|
+
console.log(
|
|
329
|
+
`Skill dirs: ${linked.length} linked/copied, ${skippedLinks.length} skipped` +
|
|
330
|
+
(skillSync.dryRun ? ' (dry-run)' : ''),
|
|
331
|
+
)
|
|
332
|
+
for (const entry of skillSync.results.slice(0, 8)) {
|
|
333
|
+
console.log(` ${entry.action} ${entry.dir}/${entry.skill}`)
|
|
334
|
+
}
|
|
335
|
+
if (skillSync.results.length > 8) {
|
|
336
|
+
console.log(` … and ${skillSync.results.length - 8} more`)
|
|
337
|
+
}
|
|
285
338
|
}
|
|
286
339
|
|
|
287
340
|
if (mcp.skipped) {
|
|
@@ -326,5 +379,17 @@ export async function runSetup(options) {
|
|
|
326
379
|
console.log(' 4. Optional: pnpm check:colors')
|
|
327
380
|
}
|
|
328
381
|
|
|
329
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
mode,
|
|
384
|
+
install,
|
|
385
|
+
template,
|
|
386
|
+
remoteSkills,
|
|
387
|
+
skillSync,
|
|
388
|
+
mcp,
|
|
389
|
+
styles,
|
|
390
|
+
scripts,
|
|
391
|
+
skills: selectedSkills,
|
|
392
|
+
editors,
|
|
393
|
+
agents,
|
|
394
|
+
}
|
|
330
395
|
}
|
package/src/skills.mjs
CHANGED
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { dirname, join, relative } from 'node:path'
|
|
2
12
|
import { createInterface } from 'node:readline'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
13
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { readJson } from './fs-utils.mjs'
|
|
14
|
+
import { ensureDir, readJson } from './fs-utils.mjs'
|
|
6
15
|
|
|
7
16
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
17
|
const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
|
|
9
18
|
|
|
19
|
+
/** Canonical project skills root shared by Cursor / Zed / GitHub Copilot. */
|
|
20
|
+
export const CANONICAL_SKILLS_DIR = '.agents/skills'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default skills CLI agents for consumer projects.
|
|
24
|
+
* VS Code Copilot uses `github-copilot` (reads `.agents/skills`).
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_SKILL_AGENTS = [
|
|
27
|
+
'cursor',
|
|
28
|
+
'github-copilot',
|
|
29
|
+
'zed',
|
|
30
|
+
'claude-code',
|
|
31
|
+
'windsurf',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Project-relative skills directories for agents that do **not** share
|
|
36
|
+
* `.agents/skills`. Agents that already use the canonical dir are omitted.
|
|
37
|
+
*
|
|
38
|
+
* @type {Record<string, string[]>}
|
|
39
|
+
*/
|
|
40
|
+
export const EXTRA_AGENT_SKILL_DIRS = {
|
|
41
|
+
'claude-code': ['.claude/skills'],
|
|
42
|
+
windsurf: ['.windsurf/skills'],
|
|
43
|
+
// Copilot also discovers `.github/skills`; keep a link for clients that prefer it.
|
|
44
|
+
'github-copilot': ['.github/skills'],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const KNOWN_SKILL_AGENTS = [
|
|
48
|
+
...new Set([...DEFAULT_SKILL_AGENTS, ...Object.keys(EXTRA_AGENT_SKILL_DIRS)]),
|
|
49
|
+
]
|
|
50
|
+
|
|
10
51
|
/**
|
|
11
52
|
* @typedef {{
|
|
12
53
|
* id: string
|
|
@@ -49,6 +90,72 @@ export function defaultSkillIds(skills) {
|
|
|
49
90
|
return skills.filter((s) => s.required || s.default).map((s) => s.id)
|
|
50
91
|
}
|
|
51
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @param {string | undefined} raw
|
|
95
|
+
* @returns {string[] | '*'}
|
|
96
|
+
*/
|
|
97
|
+
export function parseAgentsFlag(raw) {
|
|
98
|
+
if (raw == null || raw.trim() === '') return [...DEFAULT_SKILL_AGENTS]
|
|
99
|
+
if (raw.trim() === 'all' || raw.trim() === '*') return '*'
|
|
100
|
+
|
|
101
|
+
const ids = raw
|
|
102
|
+
.split(',')
|
|
103
|
+
.map((part) => part.trim().toLowerCase())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
|
|
106
|
+
if (!ids.length) return [...DEFAULT_SKILL_AGENTS]
|
|
107
|
+
if (ids.includes('*') || ids.includes('all')) return '*'
|
|
108
|
+
|
|
109
|
+
const unknown = ids.filter((id) => !KNOWN_SKILL_AGENTS.includes(id))
|
|
110
|
+
if (unknown.length) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Unknown skill agent id(s): ${unknown.join(', ')}. Available: ${KNOWN_SKILL_AGENTS.join(', ')}, or all`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [...new Set(ids)]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve concrete agent names for CLI / sync (expands `*`).
|
|
121
|
+
* @param {string[] | '*'} agents
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
export function resolveSkillAgents(agents) {
|
|
125
|
+
if (agents === '*') return [...DEFAULT_SKILL_AGENTS]
|
|
126
|
+
if (!Array.isArray(agents) || !agents.length) return [...DEFAULT_SKILL_AGENTS]
|
|
127
|
+
return [...new Set(agents)]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build `-a` flags for the skills CLI.
|
|
132
|
+
* @param {string[] | '*'} agents
|
|
133
|
+
*/
|
|
134
|
+
export function skillAgentCliFlags(agents) {
|
|
135
|
+
if (agents === '*') return "-a '*'"
|
|
136
|
+
const list = resolveSkillAgents(agents)
|
|
137
|
+
return list.map((name) => `-a ${name}`).join(' ')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extra project skill dirs to mirror for the selected agents.
|
|
142
|
+
* @param {string[] | '*'} agents
|
|
143
|
+
* @returns {string[]}
|
|
144
|
+
*/
|
|
145
|
+
export function extraSkillDirsForAgents(agents) {
|
|
146
|
+
const list = agents === '*' ? [...DEFAULT_SKILL_AGENTS] : resolveSkillAgents(agents)
|
|
147
|
+
/** @type {string[]} */
|
|
148
|
+
const dirs = []
|
|
149
|
+
for (const agent of list) {
|
|
150
|
+
const extras = EXTRA_AGENT_SKILL_DIRS[agent]
|
|
151
|
+
if (!extras) continue
|
|
152
|
+
for (const dir of extras) {
|
|
153
|
+
if (!dirs.includes(dir)) dirs.push(dir)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return dirs
|
|
157
|
+
}
|
|
158
|
+
|
|
52
159
|
/**
|
|
53
160
|
* @param {string | undefined} raw
|
|
54
161
|
* @param {SkillEntry[]} skills
|
|
@@ -111,18 +218,38 @@ export function buildAiInclude(skillIds, skills) {
|
|
|
111
218
|
]
|
|
112
219
|
}
|
|
113
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Folder names under `.agents/skills` for the selected skill ids.
|
|
223
|
+
* @param {string[]} skillIds
|
|
224
|
+
* @param {SkillEntry[]} skills
|
|
225
|
+
* @returns {string[]}
|
|
226
|
+
*/
|
|
227
|
+
export function skillFolderNames(skillIds, skills) {
|
|
228
|
+
const byId = new Map(skills.map((s) => [s.id, s]))
|
|
229
|
+
return skillIds.map((id) => {
|
|
230
|
+
const entry = byId.get(id)
|
|
231
|
+
if (!entry) throw new Error(`Unknown skill id: ${id}`)
|
|
232
|
+
return entry.skill || entry.id
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
114
236
|
/**
|
|
115
237
|
* Install selected companion skills via the skills CLI (always latest from source).
|
|
116
238
|
* @param {string} cwd
|
|
117
239
|
* @param {string[]} skillIds
|
|
118
240
|
* @param {SkillEntry[]} skills
|
|
119
|
-
* @param {{ dryRun?: boolean }} [options]
|
|
120
|
-
* @returns {{ installed: string[], commands: string[], dryRun?: boolean, skipped?: boolean }}
|
|
241
|
+
* @param {{ dryRun?: boolean, agents?: string[] | '*' }} [options]
|
|
242
|
+
* @returns {{ installed: string[], commands: string[], agents: string[] | '*', dryRun?: boolean, skipped?: boolean }}
|
|
121
243
|
*/
|
|
122
|
-
export function installSkillsCli(
|
|
244
|
+
export function installSkillsCli(
|
|
245
|
+
cwd,
|
|
246
|
+
skillIds,
|
|
247
|
+
skills,
|
|
248
|
+
{ dryRun = false, agents = DEFAULT_SKILL_AGENTS } = {},
|
|
249
|
+
) {
|
|
123
250
|
const selected = skills.filter((s) => skillIds.includes(s.id) && isSkillsCliSkill(s))
|
|
124
251
|
if (!selected.length) {
|
|
125
|
-
return { installed: [], commands: [], skipped: true }
|
|
252
|
+
return { installed: [], commands: [], agents, skipped: true }
|
|
126
253
|
}
|
|
127
254
|
|
|
128
255
|
for (const entry of selected) {
|
|
@@ -140,14 +267,15 @@ export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {})
|
|
|
140
267
|
bySource.set(entry.source, list)
|
|
141
268
|
}
|
|
142
269
|
|
|
270
|
+
const agentFlags = skillAgentCliFlags(agents)
|
|
143
271
|
const commands = []
|
|
144
272
|
for (const [source, names] of bySource) {
|
|
145
273
|
const skillFlags = names.map((name) => `-s ${name}`).join(' ')
|
|
146
|
-
commands.push(`npx -y skills add ${source} ${skillFlags}
|
|
274
|
+
commands.push(`npx -y skills add ${source} ${skillFlags} ${agentFlags} -y`)
|
|
147
275
|
}
|
|
148
276
|
|
|
149
277
|
if (dryRun) {
|
|
150
|
-
return { installed: selected.map((s) => s.id), commands, dryRun: true }
|
|
278
|
+
return { installed: selected.map((s) => s.id), commands, agents, dryRun: true }
|
|
151
279
|
}
|
|
152
280
|
|
|
153
281
|
console.log('Companion skills (latest via skills CLI):')
|
|
@@ -156,7 +284,116 @@ export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {})
|
|
|
156
284
|
execSync(command, { cwd, stdio: 'inherit', shell: true })
|
|
157
285
|
}
|
|
158
286
|
|
|
159
|
-
return { installed: selected.map((s) => s.id), commands }
|
|
287
|
+
return { installed: selected.map((s) => s.id), commands, agents }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Link (or copy) canonical `.agents/skills/<name>` into agent-specific dirs.
|
|
292
|
+
* @param {string} cwd
|
|
293
|
+
* @param {string[]} skillNames folder names under `.agents/skills`
|
|
294
|
+
* @param {{ force?: boolean, dryRun?: boolean, agents?: string[] | '*' }} [options]
|
|
295
|
+
* @returns {{ results: Array<{ skill: string, dir: string, action: string }>, dryRun?: boolean, skipped?: boolean }}
|
|
296
|
+
*/
|
|
297
|
+
export function syncProjectSkillsToAgents(
|
|
298
|
+
cwd,
|
|
299
|
+
skillNames,
|
|
300
|
+
{ force = false, dryRun = false, agents = DEFAULT_SKILL_AGENTS } = {},
|
|
301
|
+
) {
|
|
302
|
+
const names = [...new Set(skillNames.filter(Boolean))]
|
|
303
|
+
const targetDirs = extraSkillDirsForAgents(agents)
|
|
304
|
+
|
|
305
|
+
if (!names.length || !targetDirs.length) {
|
|
306
|
+
return { results: [], skipped: true, dryRun }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @type {Array<{ skill: string, dir: string, action: string }>} */
|
|
310
|
+
const results = []
|
|
311
|
+
|
|
312
|
+
for (const dir of targetDirs) {
|
|
313
|
+
for (const name of names) {
|
|
314
|
+
const source = join(cwd, CANONICAL_SKILLS_DIR, name)
|
|
315
|
+
const dest = join(cwd, dir, name)
|
|
316
|
+
|
|
317
|
+
if (!existsSync(source)) {
|
|
318
|
+
results.push({ skill: name, dir, action: 'missing-source' })
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (existsSync(dest) || isSymlink(dest)) {
|
|
323
|
+
if (!force) {
|
|
324
|
+
results.push({ skill: name, dir, action: 'skipped' })
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
if (!dryRun) {
|
|
328
|
+
rmSync(dest, { recursive: true, force: true })
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (dryRun) {
|
|
333
|
+
results.push({ skill: name, dir, action: 'linked' })
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ensureDir(join(cwd, dir))
|
|
338
|
+
const action = linkOrCopySkill(source, dest)
|
|
339
|
+
results.push({ skill: name, dir, action })
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { results, dryRun }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @param {string} path
|
|
348
|
+
*/
|
|
349
|
+
function isSymlink(path) {
|
|
350
|
+
try {
|
|
351
|
+
return lstatSync(path).isSymbolicLink()
|
|
352
|
+
} catch {
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Prefer symlink; fall back to recursive copy (e.g. Windows without privilege).
|
|
359
|
+
* @param {string} source absolute
|
|
360
|
+
* @param {string} dest absolute
|
|
361
|
+
* @returns {'linked' | 'copied'}
|
|
362
|
+
*/
|
|
363
|
+
function linkOrCopySkill(source, dest) {
|
|
364
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
365
|
+
// Windows junctions need an absolute target; POSIX prefers a relative link.
|
|
366
|
+
const rel = relative(dirname(dest), source) || '.'
|
|
367
|
+
try {
|
|
368
|
+
if (process.platform === 'win32') {
|
|
369
|
+
symlinkSync(source, dest, 'junction')
|
|
370
|
+
} else {
|
|
371
|
+
symlinkSync(rel, dest, 'dir')
|
|
372
|
+
}
|
|
373
|
+
return 'linked'
|
|
374
|
+
} catch {
|
|
375
|
+
try {
|
|
376
|
+
symlinkSync(rel, dest, 'dir')
|
|
377
|
+
return 'linked'
|
|
378
|
+
} catch {
|
|
379
|
+
cpSync(source, dest, { recursive: true })
|
|
380
|
+
return 'copied'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Discover skill folder names already present under `.agents/skills`.
|
|
387
|
+
* @param {string} cwd
|
|
388
|
+
* @returns {string[]}
|
|
389
|
+
*/
|
|
390
|
+
export function listCanonicalSkillFolders(cwd) {
|
|
391
|
+
const root = join(cwd, CANONICAL_SKILLS_DIR)
|
|
392
|
+
if (!existsSync(root)) return []
|
|
393
|
+
return readdirSync(root, { withFileTypes: true })
|
|
394
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
395
|
+
.filter((entry) => existsSync(join(root, entry.name, 'SKILL.md')))
|
|
396
|
+
.map((entry) => entry.name)
|
|
160
397
|
}
|
|
161
398
|
|
|
162
399
|
/**
|
package/template/AGENTS.md
CHANGED
|
@@ -30,4 +30,4 @@ Editor-neutral always-on guidance for AI agents (Cursor, VS Code, Zed, Copilot,
|
|
|
30
30
|
| Component pick | `recommend_component`, `get_component`, `get_example` |
|
|
31
31
|
| Offline recipes | skill `references/` under `morya-ui-pages` |
|
|
32
32
|
|
|
33
|
-
Setup
|
|
33
|
+
Canonical skills live under `.agents/skills/` (Cursor, Zed, VS Code Copilot). Setup also mirrors them into `.claude/skills`, `.windsurf/skills`, and `.github/skills` for Claude Code / Windsurf / Copilot discovery. MCP configs: Cursor / VS Code / Zed + root `.mcp.json`. Reload MCP after `npx @morya-ui/setup ai`. Zed may require trusting the worktree before project skills load.
|