@morya-ui/setup 0.3.4 → 0.3.6

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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -103
  3. package/bin/morya-ui-setup.js +14 -14
  4. package/catalog/skills.json +46 -46
  5. package/package.json +4 -5
  6. package/src/__tests__/mcp.test.mjs +141 -0
  7. package/src/cli.mjs +330 -335
  8. package/src/copy-template.mjs +78 -78
  9. package/src/fs-utils.mjs +24 -24
  10. package/src/install.mjs +134 -63
  11. package/src/mcp.mjs +220 -50
  12. package/src/package-json.mjs +30 -30
  13. package/src/skills.mjs +224 -223
  14. package/src/styles.mjs +119 -119
  15. package/template/.agents/skills/morya-ui-pages/SKILL.md +191 -184
  16. package/template/.agents/skills/morya-ui-pages/evals/evals.json +89 -89
  17. package/template/.agents/skills/morya-ui-pages/references/component-index.md +99 -99
  18. package/template/.agents/skills/morya-ui-pages/references/decision-recipes.md +72 -5
  19. package/template/.agents/skills/morya-ui-pages/references/design-system.md +101 -101
  20. package/template/.agents/skills/morya-ui-pages/references/feedback.md +68 -68
  21. package/template/.agents/skills/morya-ui-pages/references/optional-companions.md +62 -107
  22. package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +133 -141
  23. package/template/.agents/skills/morya-ui-pages/references/review-checklist.md +76 -64
  24. package/template/.agents/skills/morya-ui-pages/references/style-presets.md +68 -73
  25. package/template/.agents/skills/morya-ui-pages/references/surfaces.md +91 -91
  26. package/template/.agents/skills/morya-ui-pages/references/visual-craft.md +131 -221
  27. package/template/.cursor/rules/coding-style.mdc +41 -41
  28. package/template/.cursor/rules/component-usage.mdc +41 -41
  29. package/template/.cursor/rules/design-system.mdc +18 -17
  30. package/template/.cursor/rules/page-layout.mdc +97 -97
  31. package/template/AGENTS.md +33 -0
  32. package/template/DESIGN.md +81 -59
  33. package/template/scripts/check-raw-colors.mjs +74 -74
@@ -1,30 +1,30 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { readJson, writeJson } from './fs-utils.mjs'
4
-
5
- const CHECK_COLORS = 'node scripts/check-raw-colors.mjs'
6
-
7
- /**
8
- * @returns {{ action: 'added' | 'skipped' | 'missing-package-json', dryRun?: boolean }}
9
- */
10
- export function ensureCheckColorsScript(cwd, { force = false, dryRun = false } = {}) {
11
- const path = join(cwd, 'package.json')
12
- if (!existsSync(path)) {
13
- return { action: 'missing-package-json' }
14
- }
15
-
16
- const pkg = readJson(path)
17
- pkg.scripts = pkg.scripts || {}
18
-
19
- if (pkg.scripts['check:colors'] && !force) {
20
- return { action: 'skipped' }
21
- }
22
-
23
- pkg.scripts['check:colors'] = CHECK_COLORS
24
-
25
- if (!dryRun) {
26
- writeJson(path, pkg)
27
- }
28
-
29
- return { action: 'added', dryRun }
30
- }
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { readJson, writeJson } from './fs-utils.mjs'
4
+
5
+ const CHECK_COLORS = 'node scripts/check-raw-colors.mjs'
6
+
7
+ /**
8
+ * @returns {{ action: 'added' | 'skipped' | 'missing-package-json', dryRun?: boolean }}
9
+ */
10
+ export function ensureCheckColorsScript(cwd, { force = false, dryRun = false } = {}) {
11
+ const path = join(cwd, 'package.json')
12
+ if (!existsSync(path)) {
13
+ return { action: 'missing-package-json' }
14
+ }
15
+
16
+ const pkg = readJson(path)
17
+ pkg.scripts = pkg.scripts || {}
18
+
19
+ if (pkg.scripts['check:colors'] && !force) {
20
+ return { action: 'skipped' }
21
+ }
22
+
23
+ pkg.scripts['check:colors'] = CHECK_COLORS
24
+
25
+ if (!dryRun) {
26
+ writeJson(path, pkg)
27
+ }
28
+
29
+ return { action: 'added', dryRun }
30
+ }
package/src/skills.mjs CHANGED
@@ -1,223 +1,224 @@
1
- import { execSync } from 'node:child_process'
2
- import { createInterface } from 'node:readline'
3
- import { dirname, join } from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import { readJson } from './fs-utils.mjs'
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url))
8
- const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
9
-
10
- /**
11
- * @typedef {{
12
- * id: string
13
- * name: string
14
- * description: string
15
- * required?: boolean
16
- * default?: boolean
17
- * install?: 'template' | 'skills-cli'
18
- * source?: string
19
- * skill?: string
20
- * path: string
21
- * }} SkillEntry
22
- */
23
-
24
- /**
25
- * @returns {{ skills: SkillEntry[] }}
26
- */
27
- export function loadSkillsCatalog() {
28
- return readJson(CATALOG_PATH)
29
- }
30
-
31
- /**
32
- * @param {SkillEntry} skill
33
- */
34
- export function isTemplateSkill(skill) {
35
- return !skill.install || skill.install === 'template'
36
- }
37
-
38
- /**
39
- * @param {SkillEntry} skill
40
- */
41
- export function isSkillsCliSkill(skill) {
42
- return skill.install === 'skills-cli'
43
- }
44
-
45
- /**
46
- * @param {SkillEntry[]} skills
47
- */
48
- export function defaultSkillIds(skills) {
49
- return skills.filter((s) => s.required || s.default).map((s) => s.id)
50
- }
51
-
52
- /**
53
- * @param {string | undefined} raw
54
- * @param {SkillEntry[]} skills
55
- * @returns {string[]}
56
- */
57
- export function parseSkillsFlag(raw, skills) {
58
- if (raw == null || raw === '') return defaultSkillIds(skills)
59
- const known = new Map(skills.map((s) => [s.id, s]))
60
- if (raw.trim() === 'all') return skills.map((s) => s.id)
61
-
62
- const ids = raw
63
- .split(',')
64
- .map((part) => part.trim())
65
- .filter(Boolean)
66
-
67
- if (ids.length === 0) return defaultSkillIds(skills)
68
-
69
- const missing = ids.filter((id) => !known.has(id))
70
- if (missing.length) {
71
- throw new Error(
72
- `Unknown skill id(s): ${missing.join(', ')}. Available: ${skills.map((s) => s.id).join(', ')}, or all`,
73
- )
74
- }
75
-
76
- const selected = new Set(ids)
77
- for (const skill of skills) {
78
- if (skill.required) selected.add(skill.id)
79
- }
80
- return skills.map((s) => s.id).filter((id) => selected.has(id))
81
- }
82
-
83
- /**
84
- * Template-relative paths for skills that ship inside the package template.
85
- * @param {string[]} skillIds
86
- * @param {SkillEntry[]} skills
87
- * @returns {string[]}
88
- */
89
- export function skillPathsForIds(skillIds, skills) {
90
- const byId = new Map(skills.map((s) => [s.id, s]))
91
- return skillIds.flatMap((id) => {
92
- const entry = byId.get(id)
93
- if (!entry) throw new Error(`Unknown skill id: ${id}`)
94
- if (!isTemplateSkill(entry)) return []
95
- return [entry.path]
96
- })
97
- }
98
-
99
- /**
100
- * Build template include prefixes for the AI pack (first-party files only).
101
- * @param {string[]} skillIds
102
- * @param {SkillEntry[]} skills
103
- */
104
- export function buildAiInclude(skillIds, skills) {
105
- return [
106
- 'DESIGN.md',
107
- ...skillPathsForIds(skillIds, skills),
108
- '.cursor/rules',
109
- 'scripts/check-raw-colors.mjs',
110
- ]
111
- }
112
-
113
- /**
114
- * Install selected companion skills via the skills CLI (always latest from source).
115
- * @param {string} cwd
116
- * @param {string[]} skillIds
117
- * @param {SkillEntry[]} skills
118
- * @param {{ dryRun?: boolean }} [options]
119
- * @returns {{ installed: string[], commands: string[], dryRun?: boolean, skipped?: boolean }}
120
- */
121
- export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {}) {
122
- const selected = skills.filter((s) => skillIds.includes(s.id) && isSkillsCliSkill(s))
123
- if (!selected.length) {
124
- return { installed: [], commands: [], skipped: true }
125
- }
126
-
127
- for (const entry of selected) {
128
- if (!entry.source) {
129
- throw new Error(`Skill "${entry.id}" is marked skills-cli but has no source`)
130
- }
131
- }
132
-
133
- /** @type {Map<string, string[]>} */
134
- const bySource = new Map()
135
- for (const entry of selected) {
136
- const name = entry.skill || entry.id
137
- const list = bySource.get(entry.source) || []
138
- list.push(name)
139
- bySource.set(entry.source, list)
140
- }
141
-
142
- const commands = []
143
- for (const [source, names] of bySource) {
144
- const skillFlags = names.map((name) => `-s ${name}`).join(' ')
145
- commands.push(`npx -y skills add ${source} ${skillFlags} -a cursor -y`)
146
- }
147
-
148
- if (dryRun) {
149
- return { installed: selected.map((s) => s.id), commands, dryRun: true }
150
- }
151
-
152
- console.log('Companion skills (latest via skills CLI):')
153
- for (const command of commands) {
154
- console.log(` $ ${command}`)
155
- execSync(command, { cwd, stdio: 'inherit', shell: true })
156
- }
157
-
158
- return { installed: selected.map((s) => s.id), commands }
159
- }
160
-
161
- /**
162
- * Prompt for optional skills when stdin is a TTY.
163
- * Required skills are always included.
164
- * @param {SkillEntry[]} skills
165
- * @param {{ skipPrompt?: boolean }} [options]
166
- * @returns {Promise<string[]>}
167
- */
168
- export async function resolveSkillSelection(skills, { skipPrompt = false } = {}) {
169
- const required = skills.filter((s) => s.required).map((s) => s.id)
170
- const optional = skills.filter((s) => !s.required)
171
-
172
- if (skipPrompt || optional.length === 0 || !process.stdin.isTTY) {
173
- return defaultSkillIds(skills)
174
- }
175
-
176
- console.log('Select optional Agent skills (in addition to required):')
177
- optional.forEach((skill, index) => {
178
- const mark = skill.default ? 'x' : ' '
179
- const via = isSkillsCliSkill(skill) ? ' [latest via skills CLI]' : ''
180
- console.log(` ${index + 1}. [${mark}] ${skill.id} — ${skill.description}${via}`)
181
- })
182
- console.log('')
183
- console.log('Enter comma-separated numbers or ids (empty = defaults only).')
184
- console.log(`Required: ${required.join(', ') || '(none)'}`)
185
-
186
- const answer = await question('> ')
187
- const trimmed = answer.trim()
188
- if (!trimmed) return defaultSkillIds(skills)
189
-
190
- const selected = new Set(required)
191
- const tokens = trimmed.split(/[\s,]+/).filter(Boolean)
192
- for (const token of tokens) {
193
- const asIndex = Number(token)
194
- if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= optional.length) {
195
- selected.add(optional[asIndex - 1].id)
196
- continue
197
- }
198
- if (token === 'all') {
199
- for (const skill of optional) selected.add(skill.id)
200
- continue
201
- }
202
- if (!skills.some((s) => s.id === token)) {
203
- throw new Error(`Unknown skill selection: ${token}`)
204
- }
205
- selected.add(token)
206
- }
207
-
208
- return skills.map((s) => s.id).filter((id) => selected.has(id))
209
- }
210
-
211
- /**
212
- * @param {string} prompt
213
- * @returns {Promise<string>}
214
- */
215
- function question(prompt) {
216
- const rl = createInterface({ input: process.stdin, output: process.stdout })
217
- return new Promise((resolve) => {
218
- rl.question(prompt, (answer) => {
219
- rl.close()
220
- resolve(answer)
221
- })
222
- })
223
- }
1
+ import { execSync } from 'node:child_process'
2
+ import { createInterface } from 'node:readline'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { readJson } from './fs-utils.mjs'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+ const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
9
+
10
+ /**
11
+ * @typedef {{
12
+ * id: string
13
+ * name: string
14
+ * description: string
15
+ * required?: boolean
16
+ * default?: boolean
17
+ * install?: 'template' | 'skills-cli'
18
+ * source?: string
19
+ * skill?: string
20
+ * path: string
21
+ * }} SkillEntry
22
+ */
23
+
24
+ /**
25
+ * @returns {{ skills: SkillEntry[] }}
26
+ */
27
+ export function loadSkillsCatalog() {
28
+ return readJson(CATALOG_PATH)
29
+ }
30
+
31
+ /**
32
+ * @param {SkillEntry} skill
33
+ */
34
+ export function isTemplateSkill(skill) {
35
+ return !skill.install || skill.install === 'template'
36
+ }
37
+
38
+ /**
39
+ * @param {SkillEntry} skill
40
+ */
41
+ export function isSkillsCliSkill(skill) {
42
+ return skill.install === 'skills-cli'
43
+ }
44
+
45
+ /**
46
+ * @param {SkillEntry[]} skills
47
+ */
48
+ export function defaultSkillIds(skills) {
49
+ return skills.filter((s) => s.required || s.default).map((s) => s.id)
50
+ }
51
+
52
+ /**
53
+ * @param {string | undefined} raw
54
+ * @param {SkillEntry[]} skills
55
+ * @returns {string[]}
56
+ */
57
+ export function parseSkillsFlag(raw, skills) {
58
+ if (raw == null || raw === '') return defaultSkillIds(skills)
59
+ const known = new Map(skills.map((s) => [s.id, s]))
60
+ if (raw.trim() === 'all') return skills.map((s) => s.id)
61
+
62
+ const ids = raw
63
+ .split(',')
64
+ .map((part) => part.trim())
65
+ .filter(Boolean)
66
+
67
+ if (ids.length === 0) return defaultSkillIds(skills)
68
+
69
+ const missing = ids.filter((id) => !known.has(id))
70
+ if (missing.length) {
71
+ throw new Error(
72
+ `Unknown skill id(s): ${missing.join(', ')}. Available: ${skills.map((s) => s.id).join(', ')}, or all`,
73
+ )
74
+ }
75
+
76
+ const selected = new Set(ids)
77
+ for (const skill of skills) {
78
+ if (skill.required) selected.add(skill.id)
79
+ }
80
+ return skills.map((s) => s.id).filter((id) => selected.has(id))
81
+ }
82
+
83
+ /**
84
+ * Template-relative paths for skills that ship inside the package template.
85
+ * @param {string[]} skillIds
86
+ * @param {SkillEntry[]} skills
87
+ * @returns {string[]}
88
+ */
89
+ export function skillPathsForIds(skillIds, skills) {
90
+ const byId = new Map(skills.map((s) => [s.id, s]))
91
+ return skillIds.flatMap((id) => {
92
+ const entry = byId.get(id)
93
+ if (!entry) throw new Error(`Unknown skill id: ${id}`)
94
+ if (!isTemplateSkill(entry)) return []
95
+ return [entry.path]
96
+ })
97
+ }
98
+
99
+ /**
100
+ * Build template include prefixes for the AI pack (first-party files only).
101
+ * @param {string[]} skillIds
102
+ * @param {SkillEntry[]} skills
103
+ */
104
+ export function buildAiInclude(skillIds, skills) {
105
+ return [
106
+ 'DESIGN.md',
107
+ 'AGENTS.md',
108
+ ...skillPathsForIds(skillIds, skills),
109
+ '.cursor/rules',
110
+ 'scripts/check-raw-colors.mjs',
111
+ ]
112
+ }
113
+
114
+ /**
115
+ * Install selected companion skills via the skills CLI (always latest from source).
116
+ * @param {string} cwd
117
+ * @param {string[]} skillIds
118
+ * @param {SkillEntry[]} skills
119
+ * @param {{ dryRun?: boolean }} [options]
120
+ * @returns {{ installed: string[], commands: string[], dryRun?: boolean, skipped?: boolean }}
121
+ */
122
+ export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {}) {
123
+ const selected = skills.filter((s) => skillIds.includes(s.id) && isSkillsCliSkill(s))
124
+ if (!selected.length) {
125
+ return { installed: [], commands: [], skipped: true }
126
+ }
127
+
128
+ for (const entry of selected) {
129
+ if (!entry.source) {
130
+ throw new Error(`Skill "${entry.id}" is marked skills-cli but has no source`)
131
+ }
132
+ }
133
+
134
+ /** @type {Map<string, string[]>} */
135
+ const bySource = new Map()
136
+ for (const entry of selected) {
137
+ const name = entry.skill || entry.id
138
+ const list = bySource.get(entry.source) || []
139
+ list.push(name)
140
+ bySource.set(entry.source, list)
141
+ }
142
+
143
+ const commands = []
144
+ for (const [source, names] of bySource) {
145
+ const skillFlags = names.map((name) => `-s ${name}`).join(' ')
146
+ commands.push(`npx -y skills add ${source} ${skillFlags} -a cursor -y`)
147
+ }
148
+
149
+ if (dryRun) {
150
+ return { installed: selected.map((s) => s.id), commands, dryRun: true }
151
+ }
152
+
153
+ console.log('Companion skills (latest via skills CLI):')
154
+ for (const command of commands) {
155
+ console.log(` $ ${command}`)
156
+ execSync(command, { cwd, stdio: 'inherit', shell: true })
157
+ }
158
+
159
+ return { installed: selected.map((s) => s.id), commands }
160
+ }
161
+
162
+ /**
163
+ * Prompt for optional skills when stdin is a TTY.
164
+ * Required skills are always included.
165
+ * @param {SkillEntry[]} skills
166
+ * @param {{ skipPrompt?: boolean }} [options]
167
+ * @returns {Promise<string[]>}
168
+ */
169
+ export async function resolveSkillSelection(skills, { skipPrompt = false } = {}) {
170
+ const required = skills.filter((s) => s.required).map((s) => s.id)
171
+ const optional = skills.filter((s) => !s.required)
172
+
173
+ if (skipPrompt || optional.length === 0 || !process.stdin.isTTY) {
174
+ return defaultSkillIds(skills)
175
+ }
176
+
177
+ console.log('Select optional Agent skills (in addition to required):')
178
+ optional.forEach((skill, index) => {
179
+ const mark = skill.default ? 'x' : ' '
180
+ const via = isSkillsCliSkill(skill) ? ' [latest via skills CLI]' : ''
181
+ console.log(` ${index + 1}. [${mark}] ${skill.id} — ${skill.description}${via}`)
182
+ })
183
+ console.log('')
184
+ console.log('Enter comma-separated numbers or ids (empty = defaults only).')
185
+ console.log(`Required: ${required.join(', ') || '(none)'}`)
186
+
187
+ const answer = await question('> ')
188
+ const trimmed = answer.trim()
189
+ if (!trimmed) return defaultSkillIds(skills)
190
+
191
+ const selected = new Set(required)
192
+ const tokens = trimmed.split(/[\s,]+/).filter(Boolean)
193
+ for (const token of tokens) {
194
+ const asIndex = Number(token)
195
+ if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= optional.length) {
196
+ selected.add(optional[asIndex - 1].id)
197
+ continue
198
+ }
199
+ if (token === 'all') {
200
+ for (const skill of optional) selected.add(skill.id)
201
+ continue
202
+ }
203
+ if (!skills.some((s) => s.id === token)) {
204
+ throw new Error(`Unknown skill selection: ${token}`)
205
+ }
206
+ selected.add(token)
207
+ }
208
+
209
+ return skills.map((s) => s.id).filter((id) => selected.has(id))
210
+ }
211
+
212
+ /**
213
+ * @param {string} prompt
214
+ * @returns {Promise<string>}
215
+ */
216
+ function question(prompt) {
217
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
218
+ return new Promise((resolve) => {
219
+ rl.question(prompt, (answer) => {
220
+ rl.close()
221
+ resolve(answer)
222
+ })
223
+ })
224
+ }