@morya-ui/setup 0.3.5 → 0.3.7

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 -21
  2. package/README.md +119 -107
  3. package/bin/morya-ui-setup.js +14 -14
  4. package/catalog/skills.json +46 -46
  5. package/package.json +4 -3
  6. package/src/__tests__/mcp.test.mjs +141 -0
  7. package/src/cli.mjs +330 -312
  8. package/src/copy-template.mjs +78 -78
  9. package/src/fs-utils.mjs +24 -24
  10. package/src/install.mjs +134 -134
  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 -188
  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 +6 -27
  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 -62
  22. package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +133 -133
  23. package/template/.agents/skills/morya-ui-pages/references/review-checklist.md +76 -63
  24. package/template/.agents/skills/morya-ui-pages/references/style-presets.md +68 -45
  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 -129
  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
package/src/mcp.mjs CHANGED
@@ -1,50 +1,220 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { readJson, writeJson } from './fs-utils.mjs'
4
-
5
- const SERVER_NAME = 'morya-ui'
6
- const SERVER_CONFIG = {
7
- command: 'npx',
8
- args: ['-y', '@morya-ui/mcp@latest'],
9
- }
10
-
11
- /**
12
- * Merge Cursor MCP config for morya-ui.
13
- * @returns {{ path: string, action: 'created' | 'merged' | 'skipped' | 'forced', dryRun?: boolean }}
14
- */
15
- export function mergeMcpConfig(cwd, { force = false, dryRun = false } = {}) {
16
- const path = join(cwd, '.cursor', 'mcp.json')
17
- const exists = existsSync(path)
18
-
19
- let data = { mcpServers: {} }
20
- if (exists) {
21
- try {
22
- data = readJson(path)
23
- } catch {
24
- throw new Error(`Invalid JSON in ${path}`)
25
- }
26
- if (!data || typeof data !== 'object') {
27
- throw new Error(`Invalid MCP config shape in ${path}`)
28
- }
29
- if (!data.mcpServers || typeof data.mcpServers !== 'object') {
30
- data.mcpServers = {}
31
- }
32
- }
33
-
34
- const hasExisting = Boolean(data.mcpServers[SERVER_NAME])
35
- if (hasExisting && !force) {
36
- return { path, action: 'skipped' }
37
- }
38
-
39
- data.mcpServers[SERVER_NAME] = { ...SERVER_CONFIG }
40
-
41
- if (!dryRun) {
42
- writeJson(path, data)
43
- }
44
-
45
- let action = 'created'
46
- if (exists && hasExisting && force) action = 'forced'
47
- else if (exists) action = 'merged'
48
-
49
- return { path, action, dryRun }
50
- }
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { readJson, writeJson } from './fs-utils.mjs'
4
+
5
+ export const SERVER_NAME = 'morya-ui'
6
+
7
+ export const SERVER_CONFIG = {
8
+ command: 'npx',
9
+ args: ['-y', '@morya-ui/mcp@latest'],
10
+ }
11
+
12
+ /** VS Code workspace MCP uses `servers` + optional stdio `type`. */
13
+ export const VSCODE_SERVER_CONFIG = {
14
+ type: 'stdio',
15
+ ...SERVER_CONFIG,
16
+ }
17
+
18
+ export const KNOWN_EDITORS = ['cursor', 'vscode', 'zed']
19
+
20
+ export const DEFAULT_EDITORS = [...KNOWN_EDITORS]
21
+
22
+ /**
23
+ * @param {string | undefined} raw
24
+ * @returns {string[]}
25
+ */
26
+ export function parseEditorsFlag(raw) {
27
+ if (raw == null || raw.trim() === '' || raw.trim() === 'all') {
28
+ return [...DEFAULT_EDITORS]
29
+ }
30
+ const ids = raw
31
+ .split(',')
32
+ .map((part) => part.trim().toLowerCase())
33
+ .filter(Boolean)
34
+ if (!ids.length) return [...DEFAULT_EDITORS]
35
+
36
+ const unknown = ids.filter((id) => !KNOWN_EDITORS.includes(id))
37
+ if (unknown.length) {
38
+ throw new Error(
39
+ `Unknown editor id(s): ${unknown.join(', ')}. Available: ${KNOWN_EDITORS.join(', ')}, or all`,
40
+ )
41
+ }
42
+ // Preserve declared order; dedupe
43
+ return [...new Set(ids)]
44
+ }
45
+
46
+ /**
47
+ * @param {unknown} data
48
+ * @param {string} path
49
+ */
50
+ function assertObject(data, path) {
51
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
52
+ throw new Error(`Invalid MCP config shape in ${path}`)
53
+ }
54
+ }
55
+
56
+ /**
57
+ * @param {string} path
58
+ * @param {Record<string, unknown>} fallback
59
+ */
60
+ function readConfigOrDefault(path, fallback) {
61
+ if (!existsSync(path)) return { data: structuredClone(fallback), exists: false }
62
+ try {
63
+ const data = readJson(path)
64
+ assertObject(data, path)
65
+ return { data, exists: true }
66
+ } catch (error) {
67
+ if (error instanceof Error && error.message.startsWith('Invalid MCP')) {
68
+ throw error
69
+ }
70
+ if (error instanceof SyntaxError) {
71
+ throw new Error(`Invalid JSON in ${path}`)
72
+ }
73
+ throw error
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {object} args
79
+ * @param {string} args.path
80
+ * @param {boolean} args.exists
81
+ * @param {Record<string, unknown>} args.data
82
+ * @param {string} args.bucketKey
83
+ * @param {Record<string, unknown>} args.serverConfig
84
+ * @param {boolean} args.force
85
+ * @param {boolean} args.dryRun
86
+ * @param {string} args.editor
87
+ * @returns {{ editor: string, path: string, action: 'created' | 'merged' | 'skipped' | 'forced', dryRun?: boolean }}
88
+ */
89
+ function mergeServerEntry({ path, exists, data, bucketKey, serverConfig, force, dryRun, editor }) {
90
+ if (!data[bucketKey] || typeof data[bucketKey] !== 'object' || Array.isArray(data[bucketKey])) {
91
+ data[bucketKey] = {}
92
+ }
93
+ const bucket = /** @type {Record<string, unknown>} */ (data[bucketKey])
94
+ const hasExisting = Boolean(bucket[SERVER_NAME])
95
+ if (hasExisting && !force) {
96
+ return { editor, path, action: 'skipped', dryRun }
97
+ }
98
+
99
+ bucket[SERVER_NAME] = { ...serverConfig }
100
+
101
+ if (!dryRun) {
102
+ writeJson(path, data)
103
+ }
104
+
105
+ let action = 'created'
106
+ if (exists && hasExisting && force) action = 'forced'
107
+ else if (exists) action = 'merged'
108
+
109
+ return { editor, path, action, dryRun }
110
+ }
111
+
112
+ /**
113
+ * @param {string} cwd
114
+ * @param {{ force?: boolean, dryRun?: boolean }} [options]
115
+ */
116
+ function mergeCursorMcp(cwd, { force = false, dryRun = false } = {}) {
117
+ const path = join(cwd, '.cursor', 'mcp.json')
118
+ const { data, exists } = readConfigOrDefault(path, { mcpServers: {} })
119
+ return mergeServerEntry({
120
+ path,
121
+ exists,
122
+ data,
123
+ bucketKey: 'mcpServers',
124
+ serverConfig: SERVER_CONFIG,
125
+ force,
126
+ dryRun,
127
+ editor: 'cursor',
128
+ })
129
+ }
130
+
131
+ /**
132
+ * Portable root config (Agent Host / cross-tool discovery).
133
+ * @param {string} cwd
134
+ * @param {{ force?: boolean, dryRun?: boolean }} [options]
135
+ */
136
+ function mergePortableMcp(cwd, { force = false, dryRun = false } = {}) {
137
+ const path = join(cwd, '.mcp.json')
138
+ const { data, exists } = readConfigOrDefault(path, { mcpServers: {} })
139
+ return mergeServerEntry({
140
+ path,
141
+ exists,
142
+ data,
143
+ bucketKey: 'mcpServers',
144
+ serverConfig: SERVER_CONFIG,
145
+ force,
146
+ dryRun,
147
+ editor: 'portable',
148
+ })
149
+ }
150
+
151
+ /**
152
+ * @param {string} cwd
153
+ * @param {{ force?: boolean, dryRun?: boolean }} [options]
154
+ */
155
+ function mergeVscodeMcp(cwd, { force = false, dryRun = false } = {}) {
156
+ const path = join(cwd, '.vscode', 'mcp.json')
157
+ const { data, exists } = readConfigOrDefault(path, { servers: {} })
158
+ return mergeServerEntry({
159
+ path,
160
+ exists,
161
+ data,
162
+ bucketKey: 'servers',
163
+ serverConfig: VSCODE_SERVER_CONFIG,
164
+ force,
165
+ dryRun,
166
+ editor: 'vscode',
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Merge into `.zed/settings.json` under `context_servers` without wiping other settings.
172
+ * @param {string} cwd
173
+ * @param {{ force?: boolean, dryRun?: boolean }} [options]
174
+ */
175
+ function mergeZedMcp(cwd, { force = false, dryRun = false } = {}) {
176
+ const path = join(cwd, '.zed', 'settings.json')
177
+ const { data, exists } = readConfigOrDefault(path, {})
178
+ return mergeServerEntry({
179
+ path,
180
+ exists,
181
+ data,
182
+ bucketKey: 'context_servers',
183
+ serverConfig: SERVER_CONFIG,
184
+ force,
185
+ dryRun,
186
+ editor: 'zed',
187
+ })
188
+ }
189
+
190
+ const EDITOR_MERGERS = {
191
+ cursor: mergeCursorMcp,
192
+ vscode: mergeVscodeMcp,
193
+ zed: mergeZedMcp,
194
+ }
195
+
196
+ /**
197
+ * Merge morya-ui MCP into the selected editors plus root `.mcp.json`.
198
+ *
199
+ * @param {string} cwd
200
+ * @param {{ force?: boolean, dryRun?: boolean, editors?: string[] }} [options]
201
+ * @returns {{ results: Array<{ editor: string, path: string, action: string, dryRun?: boolean }>, dryRun?: boolean }}
202
+ */
203
+ export function mergeMcpConfig(cwd, { force = false, dryRun = false, editors = DEFAULT_EDITORS } = {}) {
204
+ const selected = Array.isArray(editors)
205
+ ? parseEditorsFlag(editors.join(','))
206
+ : parseEditorsFlag(editors)
207
+ /** @type {Array<{ editor: string, path: string, action: string, dryRun?: boolean }>} */
208
+ const results = []
209
+
210
+ for (const editor of selected) {
211
+ const merge = EDITOR_MERGERS[editor]
212
+ if (!merge) throw new Error(`Unknown editor: ${editor}`)
213
+ results.push(merge(cwd, { force, dryRun }))
214
+ }
215
+
216
+ // Always write portable root config when MCP step runs.
217
+ results.push(mergePortableMcp(cwd, { force, dryRun }))
218
+
219
+ return { results, dryRun }
220
+ }
@@ -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
+ }