@morya-ui/setup 0.3.7 → 0.4.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 +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/skills/morya-ui-pages/references/component-index.md +5 -4
- package/template/.agents/skills/morya-ui-pages/references/decision-recipes.md +1 -1
- package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +10 -7
- package/template/.agents/skills/morya-ui-pages/references/surfaces.md +5 -5
- package/template/.agents/skills/morya-ui-pages/references/visual-craft.md +21 -2
- package/template/AGENTS.md +1 -1
- package/template/scripts/check-raw-colors.mjs +10 -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.
|
|
4
|
+
"version": "0.4.0",
|
|
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
|
/**
|
|
@@ -22,7 +22,7 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
|
|
|
22
22
|
|
|
23
23
|
## Forms · inputs
|
|
24
24
|
|
|
25
|
-
`MForm
|
|
25
|
+
`MForm` / `MFormItem` / `MFormList`, `MInput`, `MInputPassword`, `MInputNumber`, `MTextarea`, `MInputOtp`, `MInputColor`, `MInputGroup`, `MInputTags`, `MMentions`, `MSelect`, `MTreeSelect`, `MCascadeSelect`, `MListbox`, `MSelectButton`, `MDatePicker`, `MTimePicker`, `MCalendar`, `MAutoComplete`, `MCheckbox` / `MCheckboxGroup`, `MRadio` / `MRadioGroup`, `MSwitch`, `MToggleButton`, `MSlider`, `MRating`, `MKnob`, `MFileUpload`, `MFloatLabel`, `MIconField`, `MLabel`
|
|
26
26
|
|
|
27
27
|
## Layout helpers
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
|
|
|
30
30
|
|
|
31
31
|
## Data
|
|
32
32
|
|
|
33
|
-
`MTable` (`columns` + **`rows`**, not `data`; optional `virtual`, `resizable`/`filterable`, `children`, `span-method`, `tree-config`), `MDataView`, `MTree`, `MPagination`, `MOrderList`, `MPickList`, `MStatus` / `MTag` / `MChip` / `MBadge`, `MAvatar` / `MAvatarGroup`, `MTimeline`, `MMeterGroup`, `MVirtualScroller`
|
|
33
|
+
`MTable` (`columns` + **`rows`**, not `data`; optional `virtual`, `resizable`/`filterable`, `children`, `span-method`, `tree-config`), `MDataView`, `MList` / `MListItem` / `MListItemMeta`, `MTree`, `MPagination`, `MOrderList`, `MPickList`, `MDescriptions`, `MStatistic`, `MStatus` / `MTag` / `MChip` / `MBadge`, `MAvatar` / `MAvatarGroup`, `MTimeline`, `MMeterGroup`, `MVirtualScroller`
|
|
34
34
|
|
|
35
35
|
## Feedback
|
|
36
36
|
|
|
@@ -40,6 +40,7 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
|
|
|
40
40
|
| `toast` | `summary` + `detail`, or async / background feel | `feedback-choice` |
|
|
41
41
|
| `<MMessage>` | Optional host for the `message` service. **Not** an inline alert | `feedback-choice` |
|
|
42
42
|
| field `errorMessage` or token `role="alert"` | Persistent form / auth error | `feedback-choice` |
|
|
43
|
+
| `MAlert` | Persistent in-page notice (not a toast) | `feedback-choice` |
|
|
43
44
|
| `MEmpty` | No-data / first-use / filtered empty | `empty-result-choice` |
|
|
44
45
|
| `MResult` | Terminal outcome: success, failure, 403 / 404 / 500 | `empty-result-choice` |
|
|
45
46
|
| `MLoading` / `v-loading` / `loading.service` | **Default** region or fullscreen loading mask | `loading-choice` |
|
|
@@ -50,11 +51,11 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
|
|
|
50
51
|
|
|
51
52
|
## Overlays & menus
|
|
52
53
|
|
|
53
|
-
`MDialog`, `MDrawer`, `MConfirmDialog` / `MConfirmPopup`, `MPopover`, `MTooltip`, `MDropdown` (**actions only**), `MContextMenu`, `MCommandMenu`, `MMenu` / `MMenubar` / `MTieredMenu` / `MMegaMenu`, `MTabs`, `MStepper`
|
|
54
|
+
`MDialog`, `MDrawer`, `MConfirmDialog` / `MConfirmPopup`, `MPopover`, `MTooltip`, `MDropdown` (**actions only**), `MContextMenu`, `MCommandMenu`, `MMenu` / `MMenubar` / `MTieredMenu` / `MMegaMenu`, `MTabs`, `MStepper`, `MTour`, `MAffix`, `MAnchor`
|
|
54
55
|
|
|
55
56
|
## Surfaces / media
|
|
56
57
|
|
|
57
|
-
`MCard`, `MPanel`, `MAccordion`, `MCarousel`, `MGallery`, `MIcon`, `MScrollbar`, `MInplace`, `MScrollTop`, `MTerminal`
|
|
58
|
+
`MCard`, `MPanel`, `MAccordion`, `MCarousel`, `MGallery`, `MImage`, `MIcon`, `MQRCode`, `MScrollbar`, `MInplace`, `MScrollTop`, `MTerminal`, `MTypography` / `MTitle` / `MText` / `MParagraph` / `MLink`, `MWatermark`
|
|
58
59
|
|
|
59
60
|
## Scenario → decision
|
|
60
61
|
|
|
@@ -1308,7 +1308,7 @@ This file is the **offline** mirror for agents without MCP.
|
|
|
1308
1308
|
**Recipe · props**
|
|
1309
1309
|
|
|
1310
1310
|
- title + description
|
|
1311
|
-
-
|
|
1311
|
+
- 主区域优先 illustration(no-content / no-result…)
|
|
1312
1312
|
- 表格内放在 #empty
|
|
1313
1313
|
|
|
1314
1314
|
**Recipe · slots**
|
|
@@ -9,7 +9,7 @@ Golden pages are **assembly demos / structure baselines**, not the default clone
|
|
|
9
9
|
| List | List page | `list-page` (`list-page-dense` / `list-page-rail` craft only) | `layout-app-shell`, `page-header-actions`, **`list-filters-stack`** (dense → `list-filters-dense`), `list-table`, `form-in-dialog`, `confirm-delete` |
|
|
10
10
|
| Form (long / dedicated) | Form page | `form-page` | `page-content-form`, `form-header`, `form-body`, `form-actions` |
|
|
11
11
|
| List create/edit dialog | List create/edit dialog | `form-in-dialog` | **`form-in-dialog`** (preferred) |
|
|
12
|
-
| Detail | Detail page | `detail-page` | `detail-toolbar`, `form-in-dialog` |
|
|
12
|
+
| Detail | Detail page | `detail-page` | `detail-toolbar`, `detail-descriptions`, `form-in-dialog` |
|
|
13
13
|
| Dashboard | Dashboard | `dashboard-page` | `dashboard-kpi-grid`, `dashboard-chart-card`, `dashboard-recent-table` |
|
|
14
14
|
| Login | — (see [surfaces.md](surfaces.md)) | `login-page` | **`auth-split-shell`** |
|
|
15
15
|
| Landing | — (see [surfaces.md](surfaces.md)) | `landing-page` | — |
|
|
@@ -74,8 +74,10 @@ Craft: [visual-craft.md](visual-craft.md) § Ops polish.
|
|
|
74
74
|
|
|
75
75
|
1. `MLayout fillViewport` → `MLayoutHeader` → `MBreadcrumb`
|
|
76
76
|
2. `MPageContent density="spacious"` → `MPageHeader` (title + short domain description when useful)
|
|
77
|
-
3.
|
|
78
|
-
4.
|
|
77
|
+
3. Optional `MAlert` for ops tip / degraded dependency (dismissible when appropriate)
|
|
78
|
+
4. KPI row: `MGrid` + **`MPageStat`** (trend + icon) — `dashboard-kpi-grid`
|
|
79
|
+
5. Main split: `MCard shadow="always"` + **`MEmpty illustration`** (chart pending) and/or recent `MTable` / `MList` — `dashboard-chart-card` / `dashboard-recent-table`
|
|
80
|
+
6. Plain numeric callouts inside a card (no trend UI) → `MStatistic`, not a second custom KPI CSS
|
|
79
81
|
|
|
80
82
|
Craft: spacious density + Ops polish; do not turn the first viewport into a marketing hero.
|
|
81
83
|
|
|
@@ -89,7 +91,7 @@ Craft: spacious density + Ops polish; do not turn the first viewport into a mark
|
|
|
89
91
|
| Spacing | `MSpace` / `MFlex` for peers; page gap from `MPageContent` | Nested padded divs stacking gaps |
|
|
90
92
|
| Scroll | Full-viewport main lists may use table-body scroll via `fill`; otherwise layout / local `MScrollbar` | Forcing overflow on every content slot; stacked page + table scrollbars without reason |
|
|
91
93
|
| Color | `--m-*` | Page-level hex / rgb |
|
|
92
|
-
| Feedback | One-line → `message`; danger → confirm dialog | Toast for a single short string |
|
|
94
|
+
| Feedback | One-line → `message`; danger → confirm dialog; sticky page tip → `MAlert` | Toast for a single short string |
|
|
93
95
|
| A11y | Labels + icon `aria-label` | Unlabeled icon controls |
|
|
94
96
|
|
|
95
97
|
Inline style is acceptable for control widths (e.g. filter `width: 14rem`).
|
|
@@ -98,9 +100,10 @@ Inline style is acceptable for control widths (e.g. filter `width: 14rem`).
|
|
|
98
100
|
|
|
99
101
|
1. Same admin chrome as list (breadcrumb → `MPageContent`)
|
|
100
102
|
2. `MPageHeader` — title, `MStatus` in `#actions`, primary/secondary/danger → `detail-toolbar`
|
|
101
|
-
3. Summary
|
|
102
|
-
4.
|
|
103
|
-
5.
|
|
103
|
+
3. Summary chips optional (`MTag`) — keep quiet
|
|
104
|
+
4. **Attributes** → `MDescriptions` / `MDescriptionsItem` (bordered when comparing many fields) — `detail-descriptions`
|
|
105
|
+
5. Related activity: prefer `MList` + `MListItemMeta` for narrative feeds; `MTable` when columns matter
|
|
106
|
+
6. Short edit → `form-in-dialog`; long edit → form page / `form-in-drawer`
|
|
104
107
|
|
|
105
108
|
## List create/edit dialog — block order
|
|
106
109
|
|
|
@@ -9,7 +9,7 @@ Pick a lane, then a surface. Prefer project golden pages for **Ops**; use the re
|
|
|
9
9
|
| List | Scan, filter, act on many rows | Golden list + [page-layouts.md](page-layouts.md); **create/edit with ≤~8 fields → `MDialog` on the list** |
|
|
10
10
|
| Form | Create / edit one entity as a **dedicated page** | Golden form — only when multi-section, many fields, or user asked for an independent form route |
|
|
11
11
|
| Dashboard | Orient + jump to work | Golden dashboard |
|
|
12
|
-
| Detail | Read-heavy record + secondary actions | `MPageHeader` +
|
|
12
|
+
| Detail | Read-heavy record + secondary actions | `MPageHeader` + **`MDescriptions`** for attributes; related feed via `MList` or `MTable`; edit via `MDialog` / `MDrawer` |
|
|
13
13
|
| Settings | Grouped preferences | Narrow `MPageContent` + stacked `MPageSection` / `MTabs` + `MForm` |
|
|
14
14
|
| Hybrid list | List + drawer/dialog | **Default Ops CRUD pattern**: list golden + `MDialog`/`MDrawer` form |
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ Pick a lane, then a surface. Prefer project golden pages for **Ops**; use the re
|
|
|
17
17
|
|
|
18
18
|
| Surface | Job | Recipe |
|
|
19
19
|
| --- | --- | --- |
|
|
20
|
-
| Login / register | Authenticate with trust | Golden: MCP `get_golden_page` `login-page` — split brand + `MForm
|
|
20
|
+
| Login / register | Authenticate with trust | Golden: MCP `get_golden_page` `login-page` — split brand + `MForm`; prefer `MConfigProvider input-variant="filled"`; optional `themeConfig` for brand seed; failures via `role="alert"` / `errorMessage` (not Toast). Brand wash: [visual-craft.md](visual-craft.md) § Shell recipes |
|
|
21
21
|
| Invite / accept | Join org | Same shell; show org name clearly |
|
|
22
22
|
| Forgot / reset | Recover access | Short form + success state with next step |
|
|
23
23
|
| Profile / account | Edit self | Settings-like sections; avatar via `MAvatar` |
|
|
@@ -28,8 +28,8 @@ Keep credential forms quiet: one primary CTA, clear labels, no decorative noise
|
|
|
28
28
|
|
|
29
29
|
| Surface | Job | Recipe |
|
|
30
30
|
| --- | --- | --- |
|
|
31
|
-
| Empty state | Invite first action | Golden: MCP `get_golden_page` `empty-state` — `MEmpty` +
|
|
32
|
-
| Onboarding | Teach the product path | `MStepper` or paced cards; one decision per step; finish → Ops home |
|
|
31
|
+
| Empty state | Invite first action | Golden: MCP `get_golden_page` `empty-state` — `MEmpty` + **`illustration`** (`no-content` / `no-result` / …) + primary/secondary in `#extra` |
|
|
32
|
+
| Onboarding | Teach the product path | `MStepper` or paced cards; optional `MTour` for in-app spotlight; one decision per step; finish → Ops home |
|
|
33
33
|
| Wizard | Multi-step create | `MStepper` + `MForm` per step + sticky actions (`上一步` / `下一步` / `提交`) |
|
|
34
34
|
| Success / result | Confirm completion | `MResult status="success"` / `error` + `#footer` next actions (`查看详情` / `返回列表`); same shell idea as `result-page` |
|
|
35
35
|
|
|
@@ -51,7 +51,7 @@ Use when the brief is landing, pricing, launch, or docs marketing — **not** fo
|
|
|
51
51
|
|
|
52
52
|
| Surface | Job | Recipe |
|
|
53
53
|
| --- | --- | --- |
|
|
54
|
-
| Landing | Convert / explain product | Golden: MCP `get_golden_page` `landing-page` — one-job hero; CTAs → `MButton`; chips → `MTag`; FAQ → `MAccordion` |
|
|
54
|
+
| Landing | Convert / explain product | Golden: MCP `get_golden_page` `landing-page` — one-job hero; CTAs → `MButton`; chips → `MTag`; FAQ → `MAccordion`; prefer `MTypography` / `MTitle` / `MParagraph` for hierarchy when available |
|
|
55
55
|
| Pricing | Choose a plan | Clear plan cards (`MCard`) + primary CTA; highlight recommended plan without clutter |
|
|
56
56
|
| Feature showcase | Prove capability | Alternating media/copy; live `M*` demo only if lightweight |
|
|
57
57
|
| Docs marketing chrome | Frame documentation | Header + nav using `M*` where suitable; content area stays readable |
|
|
@@ -48,13 +48,32 @@ If the plan looks like any-SaaS (purple wash, cream serif, neon glow) and the us
|
|
|
48
48
|
3. **Status** → `MStatus`; `MTag` for categories/filters only.
|
|
49
49
|
4. **Row actions** — text / outlined / small.
|
|
50
50
|
5. **Filter rhythm** — search ~`14rem`, selects ~`10–12rem`.
|
|
51
|
-
6. **Empty** — `MEmpty` with next
|
|
51
|
+
6. **Empty** — `MEmpty` with domain next-step copy + **one** primary in `#extra`. Prefer `illustration` (`no-content` / `no-result` / `no-message` / `network-error` / …) over a generic icon when the empty is a main region.
|
|
52
52
|
7. **No extra cards** around filters/table “for polish”.
|
|
53
53
|
8. **Domain copy**.
|
|
54
54
|
9. **Density** — follow user cues (`compact` / spacious); don’t invent a second scale.
|
|
55
55
|
10. **Quiet chrome** — no marketing heroes / glow unless the user asked.
|
|
56
|
+
11. **Page alerts** — persistent in-page notices → `MAlert` (not Toast, not `<MMessage>` as markup).
|
|
57
|
+
12. **Detail fields** — attribute grids → `MDescriptions` / `MDescriptionsItem` (not hand-rolled `<dl>`).
|
|
58
|
+
13. **Plain metrics** — numbers without trend chrome → `MStatistic` inside a section/card; KPI strips with trend → keep `MPageStat`.
|
|
59
|
+
14. **Activity / feed rows** — same-shape items without dense columns → `MList` + `MListItem` / `MListItemMeta`; tabular compare → `MTable`.
|
|
56
60
|
|
|
57
|
-
Ops beauty = alignment + semantics + restraint, **not** gradients.
|
|
61
|
+
Ops beauty = alignment + semantics + restraint + the right `M*` for the job, **not** gradients.
|
|
62
|
+
|
|
63
|
+
## Component craft map (post-library upgrades)
|
|
64
|
+
|
|
65
|
+
| Need | Prefer | Avoid |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Admin KPI with trend | `MPageStat` (`dashboard-kpi-grid`) | Custom stat CSS |
|
|
68
|
+
| Inline / detail number | `MStatistic` | Fake big numbers with raw `<p>` |
|
|
69
|
+
| Detail attribute grid | `MDescriptions` (`detail-descriptions`) | Hand-written `<dl>` / nested tables |
|
|
70
|
+
| Lightweight feed | `MList` + Meta | Forcing a full `MTable` for 3–5 narrative rows |
|
|
71
|
+
| Empty main region | `MEmpty` + `illustration` | “暂无数据” + bare icon only |
|
|
72
|
+
| In-page notice | `MAlert` | Toast for sticky page tips |
|
|
73
|
+
| Auth / form inputs | `MConfigProvider input-variant="filled"` (or filled controls) | Mixing outlined/filled without reason |
|
|
74
|
+
| Brand seed | `themeConfig` / project theme CSS | Page-level hex |
|
|
75
|
+
| Express headings | `MTypography` / `MTitle` / `MParagraph` where it helps hierarchy | Second font kit |
|
|
76
|
+
| First-run teach | `MTour` after empty/onboarding | Modal essay walls |
|
|
58
77
|
|
|
59
78
|
## Anti-default looks
|
|
60
79
|
|
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.
|
|
@@ -18,6 +18,12 @@ const HSL = /\bhsl\s*\(/g
|
|
|
18
18
|
/** Allow transparent, currentColor, inherit in CSS values */
|
|
19
19
|
const ALLOW_LINE = /var\s*\(\s*--m-|color-mix\s*\(|transparent|currentColor|inherit|none/
|
|
20
20
|
|
|
21
|
+
/** Theme seed / createTheme brand inputs must be concrete colors. */
|
|
22
|
+
const ALLOW_THEME_SEED = /\bcolorPrimary\s*:|\bseed\s*:\s*\{[^}]*colorPrimary/
|
|
23
|
+
|
|
24
|
+
/** Canvas / encoder fallbacks that must stay concrete for off-DOM drawing. */
|
|
25
|
+
const ALLOW_CANVAS_FALLBACK = /rgb\(\s*0\s*,\s*0\s*,\s*0\s*\)|rgb\(\s*255\s*,\s*255\s*,\s*255\s*\)/
|
|
26
|
+
|
|
21
27
|
/** Skip demo IDs like '#1024' or 'WO-1024' in script/template strings */
|
|
22
28
|
const DEMO_ID = /['"]#?[A-Z0-9-]{2,}['"]/
|
|
23
29
|
|
|
@@ -28,7 +34,7 @@ function walk(dir) {
|
|
|
28
34
|
const full = path.join(dir, name)
|
|
29
35
|
const st = statSync(full)
|
|
30
36
|
if (st.isDirectory()) {
|
|
31
|
-
if (IGNORE_DIRS.has(name)) continue
|
|
37
|
+
if (IGNORE_DIRS.has(name) || name === 'demos') continue
|
|
32
38
|
walk(full)
|
|
33
39
|
continue
|
|
34
40
|
}
|
|
@@ -36,12 +42,15 @@ function walk(dir) {
|
|
|
36
42
|
if (!EXT.has(ext)) continue
|
|
37
43
|
const normalized = full.replace(/\\/g, '/')
|
|
38
44
|
if (normalized.includes('design-tokens/') || normalized.includes('/theme/')) continue
|
|
45
|
+
if (normalized.includes('/docs/demos/')) continue
|
|
39
46
|
if (normalized.includes('.test.') || normalized.includes('/__tests__/')) continue
|
|
40
47
|
|
|
41
48
|
const text = readFileSync(full, 'utf8')
|
|
42
49
|
const lines = text.split(/\r?\n/)
|
|
43
50
|
lines.forEach((line, index) => {
|
|
44
51
|
if (ALLOW_LINE.test(line)) return
|
|
52
|
+
if (ALLOW_THEME_SEED.test(line)) return
|
|
53
|
+
if (ALLOW_CANVAS_FALLBACK.test(line)) return
|
|
45
54
|
if (DEMO_ID.test(line)) return
|
|
46
55
|
if (HEX.test(line) || RGB.test(line) || HSL.test(line)) {
|
|
47
56
|
HEX.lastIndex = 0
|