@morya-ui/setup 0.2.5

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/bin/morya-ui-setup.js +14 -0
  4. package/package.json +35 -0
  5. package/src/cli.mjs +200 -0
  6. package/src/copy-template.mjs +62 -0
  7. package/src/fs-utils.mjs +23 -0
  8. package/src/install.mjs +63 -0
  9. package/src/mcp.mjs +50 -0
  10. package/src/package-json.mjs +30 -0
  11. package/src/styles.mjs +109 -0
  12. package/template/.agents/skills/morya-ui-pages/SKILL.md +150 -0
  13. package/template/.agents/skills/morya-ui-pages/evals/evals.json +53 -0
  14. package/template/.agents/skills/morya-ui-pages/references/component-index.md +72 -0
  15. package/template/.agents/skills/morya-ui-pages/references/design-system.md +90 -0
  16. package/template/.agents/skills/morya-ui-pages/references/feedback.md +66 -0
  17. package/template/.agents/skills/morya-ui-pages/references/optional-companions.md +29 -0
  18. package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +76 -0
  19. package/template/.agents/skills/morya-ui-pages/references/review-checklist.md +49 -0
  20. package/template/.agents/skills/morya-ui-pages/references/surfaces.md +89 -0
  21. package/template/.agents/skills/morya-ui-pages/references/visual-craft.md +83 -0
  22. package/template/.cursor/rules/coding-style.mdc +41 -0
  23. package/template/.cursor/rules/component-usage.mdc +41 -0
  24. package/template/.cursor/rules/design-system.mdc +17 -0
  25. package/template/.cursor/rules/page-layout.mdc +67 -0
  26. package/template/DESIGN.md +121 -0
  27. package/template/design-tokens/tokens.css +55 -0
  28. package/template/design-tokens/tokens.json +77 -0
  29. package/template/docs/components.md +144 -0
  30. package/template/docs/feedback-message-vs-toast.md +103 -0
  31. package/template/docs/golden-pages/dashboard-page.vue +103 -0
  32. package/template/docs/golden-pages/empty-state.vue +66 -0
  33. package/template/docs/golden-pages/form-page.vue +107 -0
  34. package/template/docs/golden-pages/landing-page.vue +328 -0
  35. package/template/docs/golden-pages/list-page.vue +127 -0
  36. package/template/docs/golden-pages/login-page.vue +191 -0
  37. package/template/scripts/check-raw-colors.mjs +74 -0
  38. package/template/src/examples/DashboardPageExample.vue +103 -0
  39. package/template/src/examples/EmptyStateExample.vue +66 -0
  40. package/template/src/examples/FormPageExample.vue +107 -0
  41. package/template/src/examples/LandingPageExample.vue +328 -0
  42. package/template/src/examples/ListPageExample.vue +127 -0
  43. package/template/src/examples/LoginPageExample.vue +191 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Morya UI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @morya-ui/setup
2
+
3
+ One-shot setup for consumer Vue projects using [`morya-ui`](https://www.npmjs.com/package/morya-ui).
4
+
5
+ Docs (zh/en):
6
+
7
+ - [AI setup](https://morya-space.github.io/morya-ui/docs/ai-setup) — one-shot CLI
8
+ - [Agent Skill](https://morya-space.github.io/morya-ui/docs/agent-skill) — `morya-ui-pages` behavior
9
+
10
+ Installs the UI library, copies Agent skill / Cursor rules / design docs from `ai-design-config`, merges Cursor MCP for [`@morya-ui/mcp`](https://www.npmjs.com/package/@morya-ui/mcp), and tries to inject `import 'morya-ui/styles.css'` into the app entry.
11
+
12
+ ## Usage
13
+
14
+ In your app project root:
15
+
16
+ ```bash
17
+ npx @morya-ui/setup
18
+ ```
19
+
20
+ ### Options
21
+
22
+ | Flag | Meaning |
23
+ | --- | --- |
24
+ | `--cwd <dir>` | Target project root (default: current directory) |
25
+ | `--pm pnpm\|yarn\|npm` | Package manager (default: detect from lockfile, else `pnpm`) |
26
+ | `--force` | Overwrite existing template files and the `morya-ui` MCP entry |
27
+ | `--dry-run` | Print actions without writing or installing |
28
+ | `--skip-install` | Do not install `morya-ui` |
29
+ | `--skip-template` | Do not copy AI skill / rules / docs / tokens |
30
+ | `--skip-mcp` | Do not write `.cursor/mcp.json` |
31
+ | `--skip-styles` | Do not inject `styles.css` import |
32
+ | `--skip-scripts` | Do not add `check:colors` to `package.json` |
33
+ | `-h`, `--help` | Show help |
34
+
35
+ ### Conflict policy
36
+
37
+ - Template files and `.cursor/rules/*`: **skip** if the destination exists (unless `--force`).
38
+ - `.cursor/mcp.json`: merge other servers; skip existing `morya-ui` entry unless `--force`.
39
+ - `package.json` `check:colors`: add only if missing (unless `--force`).
40
+ - Styles: inject only when an entry file is found and the import is not already present.
41
+
42
+ ### What gets copied
43
+
44
+ From the package `template/` (synced from repo `ai-design-config/`):
45
+
46
+ - `DESIGN.md`
47
+ - `.agents/skills/morya-ui-pages/`
48
+ - `.cursor/rules/`
49
+ - `docs/` (component index, golden pages, feedback guide)
50
+ - `design-tokens/`
51
+ - `scripts/check-raw-colors.mjs`
52
+ - `src/examples/`
53
+
54
+ ### MCP (Cursor)
55
+
56
+ Writes / merges:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "morya-ui": {
62
+ "command": "npx",
63
+ "args": ["-y", "@morya-ui/mcp"]
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ Restart Cursor (or reload MCP) after setup.
70
+
71
+ Other clients can use the same stdio command; this CLI only writes `.cursor/mcp.json`.
72
+
73
+ ### Styles
74
+
75
+ Looks for `src/main.ts` / `.js` / `.tsx` / `.jsx`, root `main.*`, `src/app.ts` / `.js`, or the module script in `index.html`. If none match, add manually:
76
+
77
+ ```ts
78
+ import 'morya-ui/styles.css'
79
+ ```
80
+
81
+ The CLI does **not** call `app.use(MoryaUI)` or edit `App.vue`.
82
+
83
+ ## Local development (this monorepo)
84
+
85
+ ```bash
86
+ pnpm setup:sync-template # refresh packages/setup/template from ai-design-config
87
+ pnpm setup:build
88
+ node packages/setup/bin/morya-ui-setup.js --cwd /path/to/app --dry-run
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, printHelp, runSetup } from '../src/cli.mjs'
3
+
4
+ try {
5
+ const options = parseArgs(process.argv.slice(2))
6
+ if (options.help) {
7
+ printHelp()
8
+ process.exit(0)
9
+ }
10
+ await runSetup(options)
11
+ } catch (error) {
12
+ console.error(error instanceof Error ? error.message : error)
13
+ process.exit(1)
14
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@morya-ui/setup",
3
+ "type": "module",
4
+ "version": "0.2.5",
5
+ "description": "One-shot setup for morya-ui: install the library, Cursor MCP, Agent skills, and design config.",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/morya-space/morya-ui/tree/main/packages/setup#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/morya-space/morya-ui.git",
11
+ "directory": "packages/setup"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/morya-space/morya-ui/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "morya-ui-setup": "./bin/morya-ui-setup.js"
21
+ },
22
+ "files": [
23
+ "README.md",
24
+ "bin",
25
+ "src",
26
+ "template"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20.19.0"
30
+ },
31
+ "scripts": {
32
+ "sync-template": "node ./scripts/sync-template.mjs",
33
+ "build": "pnpm run sync-template"
34
+ }
35
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,200 @@
1
+ import { dirname, join, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { copyTemplate } from './copy-template.mjs'
4
+ import { installMoryaUi } from './install.mjs'
5
+ import { mergeMcpConfig } from './mcp.mjs'
6
+ import { ensureCheckColorsScript } from './package-json.mjs'
7
+ import { ensureStylesImport } from './styles.mjs'
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ const PKG_ROOT = resolve(__dirname, '..')
11
+ const TEMPLATE_ROOT = join(PKG_ROOT, 'template')
12
+
13
+ const SKIP_FLAGS = {
14
+ '--skip-install': 'skipInstall',
15
+ '--skip-template': 'skipTemplate',
16
+ '--skip-mcp': 'skipMcp',
17
+ '--skip-styles': 'skipStyles',
18
+ '--skip-scripts': 'skipScripts',
19
+ }
20
+
21
+ export function printHelp() {
22
+ console.log(`Usage: morya-ui-setup [options]
23
+
24
+ One-shot setup for morya-ui in a consumer Vue project:
25
+ - install morya-ui
26
+ - copy DESIGN.md, Agent skill, Cursor rules, docs, tokens, examples
27
+ - merge .cursor/mcp.json for @morya-ui/mcp
28
+ - inject import 'morya-ui/styles.css' into the app entry when found
29
+ - add check:colors script when missing
30
+
31
+ Options:
32
+ --cwd <dir> Target project root (default: process.cwd())
33
+ --pm <name> Package manager: pnpm | yarn | npm (auto-detect by lockfile)
34
+ --force Overwrite existing template files and MCP server entry
35
+ --dry-run Print actions without writing or installing
36
+ --skip-install Skip dependency install
37
+ --skip-template Skip copying AI template files
38
+ --skip-mcp Skip writing .cursor/mcp.json
39
+ --skip-styles Skip injecting styles.css import
40
+ --skip-scripts Skip adding check:colors to package.json
41
+ -h, --help Show this help
42
+ `)
43
+ }
44
+
45
+ /**
46
+ * @param {string[]} argv
47
+ */
48
+ export function parseArgs(argv) {
49
+ const options = {
50
+ cwd: process.cwd(),
51
+ pm: undefined,
52
+ force: false,
53
+ dryRun: false,
54
+ skipInstall: false,
55
+ skipTemplate: false,
56
+ skipMcp: false,
57
+ skipStyles: false,
58
+ skipScripts: false,
59
+ help: false,
60
+ }
61
+
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const arg = argv[i]
64
+ if (arg === '-h' || arg === '--help') {
65
+ options.help = true
66
+ continue
67
+ }
68
+ if (arg === '--force') {
69
+ options.force = true
70
+ continue
71
+ }
72
+ if (arg === '--dry-run') {
73
+ options.dryRun = true
74
+ continue
75
+ }
76
+ if (SKIP_FLAGS[arg]) {
77
+ options[SKIP_FLAGS[arg]] = true
78
+ continue
79
+ }
80
+ if (arg === '--cwd') {
81
+ const value = argv[++i]
82
+ if (!value) throw new Error('--cwd requires a path')
83
+ options.cwd = resolve(value)
84
+ continue
85
+ }
86
+ if (arg.startsWith('--cwd=')) {
87
+ options.cwd = resolve(arg.slice('--cwd='.length))
88
+ continue
89
+ }
90
+ if (arg === '--pm') {
91
+ const value = argv[++i]
92
+ if (!value) throw new Error('--pm requires pnpm, yarn, or npm')
93
+ options.pm = value
94
+ continue
95
+ }
96
+ if (arg.startsWith('--pm=')) {
97
+ options.pm = arg.slice('--pm='.length)
98
+ continue
99
+ }
100
+ throw new Error(`Unknown argument: ${arg}`)
101
+ }
102
+
103
+ return options
104
+ }
105
+
106
+ function rel(cwd, path) {
107
+ if (!path) return ''
108
+ return path.startsWith(cwd) ? path.slice(cwd.length).replace(/^[\\/]/, '') || path : path
109
+ }
110
+
111
+ /**
112
+ * @param {ReturnType<typeof parseArgs>} options
113
+ */
114
+ export async function runSetup(options) {
115
+ const {
116
+ cwd,
117
+ force,
118
+ dryRun,
119
+ skipInstall,
120
+ skipTemplate,
121
+ skipMcp,
122
+ skipStyles,
123
+ skipScripts,
124
+ pm,
125
+ } = options
126
+
127
+ console.log(`@morya-ui/setup → ${cwd}${dryRun ? ' (dry-run)' : ''}`)
128
+ console.log('')
129
+
130
+ const install = installMoryaUi(cwd, { pm, dryRun, skipInstall })
131
+
132
+ const template = skipTemplate
133
+ ? { copied: [], skipped: [], forced: [], skippedStep: true }
134
+ : copyTemplate(TEMPLATE_ROOT, cwd, { force, dryRun })
135
+
136
+ const mcp = skipMcp
137
+ ? { path: join(cwd, '.cursor', 'mcp.json'), action: 'skipped-flag' }
138
+ : mergeMcpConfig(cwd, { force, dryRun })
139
+
140
+ const styles = skipStyles
141
+ ? { action: 'skipped', reason: 'skip-styles' }
142
+ : ensureStylesImport(cwd, { dryRun })
143
+
144
+ const scripts = skipScripts
145
+ ? { action: 'skipped-flag' }
146
+ : ensureCheckColorsScript(cwd, { force, dryRun })
147
+
148
+ console.log('--- Summary ---')
149
+ if (install.skipped) {
150
+ console.log(`Install: skipped (${install.reason}) — would run: ${install.command}`)
151
+ } else {
152
+ console.log(`Install: ok (${install.pm}) — ${install.command}`)
153
+ }
154
+
155
+ if (template.skippedStep) {
156
+ console.log('Template: skipped (--skip-template)')
157
+ } else {
158
+ console.log(
159
+ `Template: ${template.copied.length} copied, ${template.forced.length} overwritten, ${template.skipped.length} skipped`,
160
+ )
161
+ if (template.skipped.length && template.skipped.length <= 8) {
162
+ for (const file of template.skipped) console.log(` skip ${file}`)
163
+ } else if (template.skipped.length > 8) {
164
+ for (const file of template.skipped.slice(0, 5)) console.log(` skip ${file}`)
165
+ console.log(` … and ${template.skipped.length - 5} more (use --force to overwrite)`)
166
+ }
167
+ }
168
+
169
+ if (mcp.action === 'skipped-flag') {
170
+ console.log('MCP: skipped (--skip-mcp)')
171
+ } else {
172
+ console.log(`MCP: ${mcp.action} (${rel(cwd, mcp.path) || '.cursor/mcp.json'})`)
173
+ }
174
+
175
+ if (styles.reason === 'skip-styles') {
176
+ console.log('Styles: skipped (--skip-styles)')
177
+ } else if (styles.action === 'injected') {
178
+ console.log(`Styles: injected into ${rel(cwd, styles.path)}`)
179
+ } else if (styles.action === 'skipped') {
180
+ console.log(`Styles: skipped (${styles.reason}) in ${rel(cwd, styles.path)}`)
181
+ } else {
182
+ console.log('Styles: no entry file found — add manually:')
183
+ console.log(" import 'morya-ui/styles.css'")
184
+ }
185
+
186
+ if (scripts.action === 'skipped-flag') {
187
+ console.log('Scripts: skipped (--skip-scripts)')
188
+ } else {
189
+ console.log(`Scripts: check:colors ${scripts.action}`)
190
+ }
191
+
192
+ console.log('')
193
+ console.log('Next:')
194
+ console.log(' 1. Ensure the styles import is in your app entry (if not injected).')
195
+ console.log(' 2. Restart Cursor (or reload MCP) so morya-ui MCP tools appear.')
196
+ console.log(' 3. Have the agent read DESIGN.md before generating pages.')
197
+ console.log(' 4. Optional: pnpm check:colors')
198
+
199
+ return { install, template, mcp, styles, scripts }
200
+ }
@@ -0,0 +1,62 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+
4
+ /**
5
+ * Recursively list files under dir (relative to dir).
6
+ * @param {string} dir
7
+ * @returns {string[]}
8
+ */
9
+ function listFiles(dir) {
10
+ const out = []
11
+ function walk(current) {
12
+ for (const name of readdirSync(current)) {
13
+ const full = join(current, name)
14
+ const st = statSync(full)
15
+ if (st.isDirectory()) walk(full)
16
+ else out.push(relative(dir, full))
17
+ }
18
+ }
19
+ walk(dir)
20
+ return out
21
+ }
22
+
23
+ /**
24
+ * Copy template tree into target cwd.
25
+ * @returns {{ copied: string[], skipped: string[], forced: string[] }}
26
+ */
27
+ export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false } = {}) {
28
+ const copied = []
29
+ const skipped = []
30
+ const forced = []
31
+
32
+ if (!existsSync(templateRoot)) {
33
+ throw new Error(
34
+ `Template missing at ${templateRoot}. Run sync-template / build before publishing.`,
35
+ )
36
+ }
37
+
38
+ const files = listFiles(templateRoot)
39
+ for (const rel of files) {
40
+ const from = join(templateRoot, rel)
41
+ const to = join(cwd, rel)
42
+ const exists = existsSync(to)
43
+
44
+ if (exists && !force) {
45
+ skipped.push(rel)
46
+ continue
47
+ }
48
+
49
+ if (dryRun) {
50
+ if (exists) forced.push(rel)
51
+ else copied.push(rel)
52
+ continue
53
+ }
54
+
55
+ mkdirSync(dirname(to), { recursive: true })
56
+ cpSync(from, to)
57
+ if (exists) forced.push(rel)
58
+ else copied.push(rel)
59
+ }
60
+
61
+ return { copied, skipped, forced }
62
+ }
@@ -0,0 +1,23 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+
4
+ export function readJson(path) {
5
+ return JSON.parse(readFileSync(path, 'utf8'))
6
+ }
7
+
8
+ export function writeJson(path, value, { dryRun = false } = {}) {
9
+ const text = `${JSON.stringify(value, null, 2)}\n`
10
+ if (dryRun) return text
11
+ mkdirSync(dirname(path), { recursive: true })
12
+ writeFileSync(path, text, 'utf8')
13
+ return text
14
+ }
15
+
16
+ export function ensureDir(path, { dryRun = false } = {}) {
17
+ if (dryRun || existsSync(path)) return
18
+ mkdirSync(path, { recursive: true })
19
+ }
20
+
21
+ export function fileExists(path) {
22
+ return existsSync(path)
23
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { execSync } from 'node:child_process'
4
+
5
+ const LOCKFILES = [
6
+ { pm: 'pnpm', file: 'pnpm-lock.yaml' },
7
+ { pm: 'yarn', file: 'yarn.lock' },
8
+ { pm: 'npm', file: 'package-lock.json' },
9
+ ]
10
+
11
+ export function detectPackageManager(cwd, explicit) {
12
+ if (explicit) {
13
+ if (!['pnpm', 'yarn', 'npm'].includes(explicit)) {
14
+ throw new Error(`Unsupported --pm value: ${explicit}. Use pnpm, yarn, or npm.`)
15
+ }
16
+ return explicit
17
+ }
18
+ for (const { pm, file } of LOCKFILES) {
19
+ if (existsSync(join(cwd, file))) return pm
20
+ }
21
+ return 'pnpm'
22
+ }
23
+
24
+ function installCommand(pm) {
25
+ switch (pm) {
26
+ case 'pnpm':
27
+ return 'pnpm add morya-ui'
28
+ case 'yarn':
29
+ return 'yarn add morya-ui'
30
+ case 'npm':
31
+ return 'npm install morya-ui'
32
+ default:
33
+ throw new Error(`Unknown package manager: ${pm}`)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @returns {{ pm: string, command: string, skipped?: boolean, reason?: string }}
39
+ */
40
+ export function installMoryaUi(cwd, { pm, dryRun = false, skipInstall = false } = {}) {
41
+ const resolved = detectPackageManager(cwd, pm)
42
+ const command = installCommand(resolved)
43
+
44
+ if (skipInstall) {
45
+ return { pm: resolved, command, skipped: true, reason: 'skip-install' }
46
+ }
47
+
48
+ if (!existsSync(join(cwd, 'package.json'))) {
49
+ return {
50
+ pm: resolved,
51
+ command,
52
+ skipped: true,
53
+ reason: 'no-package-json',
54
+ }
55
+ }
56
+
57
+ if (dryRun) {
58
+ return { pm: resolved, command, skipped: true, reason: 'dry-run' }
59
+ }
60
+
61
+ execSync(command, { cwd, stdio: 'inherit', shell: true })
62
+ return { pm: resolved, command }
63
+ }
package/src/mcp.mjs ADDED
@@ -0,0 +1,50 @@
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'],
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
+ }
@@ -0,0 +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
+ }