@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.
- package/LICENSE +21 -0
- package/README.md +119 -103
- package/bin/morya-ui-setup.js +14 -14
- package/catalog/skills.json +46 -46
- package/package.json +4 -5
- package/src/__tests__/mcp.test.mjs +141 -0
- package/src/cli.mjs +330 -335
- package/src/copy-template.mjs +78 -78
- package/src/fs-utils.mjs +24 -24
- package/src/install.mjs +134 -63
- package/src/mcp.mjs +220 -50
- package/src/package-json.mjs +30 -30
- package/src/skills.mjs +224 -223
- package/src/styles.mjs +119 -119
- package/template/.agents/skills/morya-ui-pages/SKILL.md +191 -184
- package/template/.agents/skills/morya-ui-pages/evals/evals.json +89 -89
- package/template/.agents/skills/morya-ui-pages/references/component-index.md +99 -99
- package/template/.agents/skills/morya-ui-pages/references/decision-recipes.md +72 -5
- package/template/.agents/skills/morya-ui-pages/references/design-system.md +101 -101
- package/template/.agents/skills/morya-ui-pages/references/feedback.md +68 -68
- package/template/.agents/skills/morya-ui-pages/references/optional-companions.md +62 -107
- package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +133 -141
- package/template/.agents/skills/morya-ui-pages/references/review-checklist.md +76 -64
- package/template/.agents/skills/morya-ui-pages/references/style-presets.md +68 -73
- package/template/.agents/skills/morya-ui-pages/references/surfaces.md +91 -91
- package/template/.agents/skills/morya-ui-pages/references/visual-craft.md +131 -221
- package/template/.cursor/rules/coding-style.mdc +41 -41
- package/template/.cursor/rules/component-usage.mdc +41 -41
- package/template/.cursor/rules/design-system.mdc +18 -17
- package/template/.cursor/rules/page-layout.mdc +97 -97
- package/template/AGENTS.md +33 -0
- package/template/DESIGN.md +81 -59
- package/template/scripts/check-raw-colors.mjs +74 -74
package/src/copy-template.mjs
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
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
|
-
* @param {string} rel
|
|
25
|
-
* @param {string[] | undefined} include
|
|
26
|
-
*/
|
|
27
|
-
function matchesInclude(rel, include) {
|
|
28
|
-
if (!include || include.length === 0) return true
|
|
29
|
-
const norm = rel.replace(/\\/g, '/')
|
|
30
|
-
return include.some((prefix) => {
|
|
31
|
-
const p = prefix.replace(/\\/g, '/')
|
|
32
|
-
return norm === p || norm.startsWith(`${p}/`)
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Copy template tree into target cwd.
|
|
38
|
-
* @param {string} templateRoot
|
|
39
|
-
* @param {string} cwd
|
|
40
|
-
* @param {{ force?: boolean, dryRun?: boolean, include?: string[] }} [options]
|
|
41
|
-
* @returns {{ copied: string[], skipped: string[], forced: string[] }}
|
|
42
|
-
*/
|
|
43
|
-
export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false, include } = {}) {
|
|
44
|
-
const copied = []
|
|
45
|
-
const skipped = []
|
|
46
|
-
const forced = []
|
|
47
|
-
|
|
48
|
-
if (!existsSync(templateRoot)) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`Template missing at ${templateRoot}. Run sync-template / build before publishing.`,
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const files = listFiles(templateRoot).filter((rel) => matchesInclude(rel, include))
|
|
55
|
-
for (const rel of files) {
|
|
56
|
-
const from = join(templateRoot, rel)
|
|
57
|
-
const to = join(cwd, rel)
|
|
58
|
-
const exists = existsSync(to)
|
|
59
|
-
|
|
60
|
-
if (exists && !force) {
|
|
61
|
-
skipped.push(rel)
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (dryRun) {
|
|
66
|
-
if (exists) forced.push(rel)
|
|
67
|
-
else copied.push(rel)
|
|
68
|
-
continue
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
mkdirSync(dirname(to), { recursive: true })
|
|
72
|
-
cpSync(from, to)
|
|
73
|
-
if (exists) forced.push(rel)
|
|
74
|
-
else copied.push(rel)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { copied, skipped, forced }
|
|
78
|
-
}
|
|
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
|
+
* @param {string} rel
|
|
25
|
+
* @param {string[] | undefined} include
|
|
26
|
+
*/
|
|
27
|
+
function matchesInclude(rel, include) {
|
|
28
|
+
if (!include || include.length === 0) return true
|
|
29
|
+
const norm = rel.replace(/\\/g, '/')
|
|
30
|
+
return include.some((prefix) => {
|
|
31
|
+
const p = prefix.replace(/\\/g, '/')
|
|
32
|
+
return norm === p || norm.startsWith(`${p}/`)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Copy template tree into target cwd.
|
|
38
|
+
* @param {string} templateRoot
|
|
39
|
+
* @param {string} cwd
|
|
40
|
+
* @param {{ force?: boolean, dryRun?: boolean, include?: string[] }} [options]
|
|
41
|
+
* @returns {{ copied: string[], skipped: string[], forced: string[] }}
|
|
42
|
+
*/
|
|
43
|
+
export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false, include } = {}) {
|
|
44
|
+
const copied = []
|
|
45
|
+
const skipped = []
|
|
46
|
+
const forced = []
|
|
47
|
+
|
|
48
|
+
if (!existsSync(templateRoot)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Template missing at ${templateRoot}. Run sync-template / build before publishing.`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = listFiles(templateRoot).filter((rel) => matchesInclude(rel, include))
|
|
55
|
+
for (const rel of files) {
|
|
56
|
+
const from = join(templateRoot, rel)
|
|
57
|
+
const to = join(cwd, rel)
|
|
58
|
+
const exists = existsSync(to)
|
|
59
|
+
|
|
60
|
+
if (exists && !force) {
|
|
61
|
+
skipped.push(rel)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
if (exists) forced.push(rel)
|
|
67
|
+
else copied.push(rel)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
mkdirSync(dirname(to), { recursive: true })
|
|
72
|
+
cpSync(from, to)
|
|
73
|
+
if (exists) forced.push(rel)
|
|
74
|
+
else copied.push(rel)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { copied, skipped, forced }
|
|
78
|
+
}
|
package/src/fs-utils.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
3
|
-
|
|
4
|
-
export function readJson(path) {
|
|
5
|
-
const text = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
|
|
6
|
-
return JSON.parse(text)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function writeJson(path, value, { dryRun = false } = {}) {
|
|
10
|
-
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
11
|
-
if (dryRun) return text
|
|
12
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
13
|
-
writeFileSync(path, text, 'utf8')
|
|
14
|
-
return text
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function ensureDir(path, { dryRun = false } = {}) {
|
|
18
|
-
if (dryRun || existsSync(path)) return
|
|
19
|
-
mkdirSync(path, { recursive: true })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function fileExists(path) {
|
|
23
|
-
return existsSync(path)
|
|
24
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function readJson(path) {
|
|
5
|
+
const text = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
|
|
6
|
+
return JSON.parse(text)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function writeJson(path, value, { dryRun = false } = {}) {
|
|
10
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
11
|
+
if (dryRun) return text
|
|
12
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
13
|
+
writeFileSync(path, text, 'utf8')
|
|
14
|
+
return text
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ensureDir(path, { dryRun = false } = {}) {
|
|
18
|
+
if (dryRun || existsSync(path)) return
|
|
19
|
+
mkdirSync(path, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fileExists(path) {
|
|
23
|
+
return existsSync(path)
|
|
24
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -1,63 +1,134 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{ pm: '
|
|
8
|
-
{ pm: '
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { readJson } from './fs-utils.mjs'
|
|
5
|
+
|
|
6
|
+
const LOCKFILES = [
|
|
7
|
+
{ pm: 'pnpm', file: 'pnpm-lock.yaml' },
|
|
8
|
+
{ pm: 'yarn', file: 'yarn.lock' },
|
|
9
|
+
{ pm: 'npm', file: 'package-lock.json' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const CORE_PACKAGE = 'morya-ui'
|
|
13
|
+
const SCOPE_PREFIX = '@morya-ui/'
|
|
14
|
+
|
|
15
|
+
export function detectPackageManager(cwd, explicit) {
|
|
16
|
+
if (explicit) {
|
|
17
|
+
if (!['pnpm', 'yarn', 'npm'].includes(explicit)) {
|
|
18
|
+
throw new Error(`Unsupported --pm value: ${explicit}. Use pnpm, yarn, or npm.`)
|
|
19
|
+
}
|
|
20
|
+
return explicit
|
|
21
|
+
}
|
|
22
|
+
for (const { pm, file } of LOCKFILES) {
|
|
23
|
+
if (existsSync(join(cwd, file))) return pm
|
|
24
|
+
}
|
|
25
|
+
return 'pnpm'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collect morya-ui / @morya-ui/* names from package.json.
|
|
30
|
+
* Always includes `morya-ui` when ensureCore is true (full / app).
|
|
31
|
+
* @param {Record<string, unknown> | null} pkg
|
|
32
|
+
* @param {{ ensureCore?: boolean }} [opts]
|
|
33
|
+
* @returns {string[]}
|
|
34
|
+
*/
|
|
35
|
+
export function collectMoryaPackages(pkg, { ensureCore = true } = {}) {
|
|
36
|
+
const names = new Set()
|
|
37
|
+
if (pkg && typeof pkg === 'object') {
|
|
38
|
+
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
39
|
+
const deps = pkg[field]
|
|
40
|
+
if (!deps || typeof deps !== 'object') continue
|
|
41
|
+
for (const name of Object.keys(deps)) {
|
|
42
|
+
if (name === CORE_PACKAGE || name.startsWith(SCOPE_PREFIX)) {
|
|
43
|
+
names.add(name)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ensureCore) names.add(CORE_PACKAGE)
|
|
49
|
+
return [...names].sort()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} pm
|
|
54
|
+
* @param {string[]} packages bare package names
|
|
55
|
+
*/
|
|
56
|
+
export function installCommand(pm, packages) {
|
|
57
|
+
const specs = packages.map((name) => `${name}@latest`)
|
|
58
|
+
const joined = specs.join(' ')
|
|
59
|
+
switch (pm) {
|
|
60
|
+
case 'pnpm':
|
|
61
|
+
return `pnpm add ${joined}`
|
|
62
|
+
case 'yarn':
|
|
63
|
+
return `yarn add ${joined}`
|
|
64
|
+
case 'npm':
|
|
65
|
+
return `npm install ${joined}`
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`Unknown package manager: ${pm}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Install / upgrade `morya-ui` and any existing `@morya-ui/*` deps to the npm `latest` tag.
|
|
73
|
+
* @returns {{
|
|
74
|
+
* pm: string,
|
|
75
|
+
* command: string,
|
|
76
|
+
* packages: string[],
|
|
77
|
+
* skipped?: boolean,
|
|
78
|
+
* reason?: string,
|
|
79
|
+
* }}
|
|
80
|
+
*/
|
|
81
|
+
export function installMoryaUi(cwd, {
|
|
82
|
+
pm,
|
|
83
|
+
dryRun = false,
|
|
84
|
+
skipInstall = false,
|
|
85
|
+
ensureCore = true,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const resolved = detectPackageManager(cwd, pm)
|
|
88
|
+
const pkgPath = join(cwd, 'package.json')
|
|
89
|
+
|
|
90
|
+
let pkg = null
|
|
91
|
+
if (existsSync(pkgPath)) {
|
|
92
|
+
try {
|
|
93
|
+
pkg = readJson(pkgPath)
|
|
94
|
+
} catch {
|
|
95
|
+
pkg = null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const packages = collectMoryaPackages(pkg, { ensureCore })
|
|
100
|
+
const command = packages.length
|
|
101
|
+
? installCommand(resolved, packages)
|
|
102
|
+
: installCommand(resolved, [CORE_PACKAGE])
|
|
103
|
+
|
|
104
|
+
if (skipInstall) {
|
|
105
|
+
return { pm: resolved, command, packages, skipped: true, reason: 'skip-install' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!existsSync(pkgPath)) {
|
|
109
|
+
return {
|
|
110
|
+
pm: resolved,
|
|
111
|
+
command,
|
|
112
|
+
packages,
|
|
113
|
+
skipped: true,
|
|
114
|
+
reason: 'no-package-json',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!packages.length) {
|
|
119
|
+
return {
|
|
120
|
+
pm: resolved,
|
|
121
|
+
command,
|
|
122
|
+
packages,
|
|
123
|
+
skipped: true,
|
|
124
|
+
reason: 'no-morya-packages',
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (dryRun) {
|
|
129
|
+
return { pm: resolved, command, packages, skipped: true, reason: 'dry-run' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
execSync(command, { cwd, stdio: 'inherit', shell: true })
|
|
133
|
+
return { pm: resolved, command, packages }
|
|
134
|
+
}
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|