@rch4com/agent-setup 1.0.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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/install.mjs +83 -0
  4. package/lib/args.mjs +266 -0
  5. package/lib/bootstrap/adapter.mjs +95 -0
  6. package/lib/bootstrap/apply.mjs +269 -0
  7. package/lib/bootstrap/flow.mjs +54 -0
  8. package/lib/bootstrap/manifest.mjs +74 -0
  9. package/lib/bootstrap/templates.mjs +148 -0
  10. package/lib/catalog.mjs +145 -0
  11. package/lib/claude-plugins.mjs +64 -0
  12. package/lib/clis.mjs +102 -0
  13. package/lib/context.mjs +65 -0
  14. package/lib/deps.mjs +19 -0
  15. package/lib/design-md/cache/awesome-design-md/airbnb/DESIGN.md +545 -0
  16. package/lib/design-md/cache/awesome-design-md/airtable/DESIGN.md +554 -0
  17. package/lib/design-md/cache/awesome-design-md/apple/DESIGN.md +562 -0
  18. package/lib/design-md/cache/awesome-design-md/binance/DESIGN.md +634 -0
  19. package/lib/design-md/cache/awesome-design-md/bmw/DESIGN.md +544 -0
  20. package/lib/design-md/cache/awesome-design-md/bmw-m/DESIGN.md +503 -0
  21. package/lib/design-md/cache/awesome-design-md/bugatti/DESIGN.md +454 -0
  22. package/lib/design-md/cache/awesome-design-md/cal/DESIGN.md +542 -0
  23. package/lib/design-md/cache/awesome-design-md/claude/DESIGN.md +589 -0
  24. package/lib/design-md/cache/awesome-design-md/clay/DESIGN.md +541 -0
  25. package/lib/design-md/cache/awesome-design-md/clickhouse/DESIGN.md +544 -0
  26. package/lib/design-md/cache/awesome-design-md/cohere/DESIGN.md +451 -0
  27. package/lib/design-md/cache/awesome-design-md/coinbase/DESIGN.md +570 -0
  28. package/lib/design-md/cache/awesome-design-md/composio/DESIGN.md +506 -0
  29. package/lib/design-md/cache/awesome-design-md/cursor/DESIGN.md +537 -0
  30. package/lib/design-md/cache/awesome-design-md/dell-1996/DESIGN.md +632 -0
  31. package/lib/design-md/cache/awesome-design-md/elevenlabs/DESIGN.md +504 -0
  32. package/lib/design-md/cache/awesome-design-md/expo/DESIGN.md +526 -0
  33. package/lib/design-md/cache/awesome-design-md/ferrari/DESIGN.md +531 -0
  34. package/lib/design-md/cache/awesome-design-md/figma/DESIGN.md +578 -0
  35. package/lib/design-md/cache/awesome-design-md/framer/DESIGN.md +544 -0
  36. package/lib/design-md/cache/awesome-design-md/hashicorp/DESIGN.md +575 -0
  37. package/lib/design-md/cache/awesome-design-md/hp/DESIGN.md +670 -0
  38. package/lib/design-md/cache/awesome-design-md/ibm/DESIGN.md +550 -0
  39. package/lib/design-md/cache/awesome-design-md/intercom/DESIGN.md +546 -0
  40. package/lib/design-md/cache/awesome-design-md/kraken/DESIGN.md +125 -0
  41. package/lib/design-md/cache/awesome-design-md/lamborghini/DESIGN.md +288 -0
  42. package/lib/design-md/cache/awesome-design-md/linear.app/DESIGN.md +548 -0
  43. package/lib/design-md/cache/awesome-design-md/lovable/DESIGN.md +298 -0
  44. package/lib/design-md/cache/awesome-design-md/mastercard/DESIGN.md +365 -0
  45. package/lib/design-md/cache/awesome-design-md/meta/DESIGN.md +683 -0
  46. package/lib/design-md/cache/awesome-design-md/minimax/DESIGN.md +746 -0
  47. package/lib/design-md/cache/awesome-design-md/mintlify/DESIGN.md +852 -0
  48. package/lib/design-md/cache/awesome-design-md/miro/DESIGN.md +825 -0
  49. package/lib/design-md/cache/awesome-design-md/mistral.ai/DESIGN.md +773 -0
  50. package/lib/design-md/cache/awesome-design-md/mongodb/DESIGN.md +767 -0
  51. package/lib/design-md/cache/awesome-design-md/nike/DESIGN.md +575 -0
  52. package/lib/design-md/cache/awesome-design-md/nintendo-2001/DESIGN.md +649 -0
  53. package/lib/design-md/cache/awesome-design-md/notion/DESIGN.md +821 -0
  54. package/lib/design-md/cache/awesome-design-md/nvidia/DESIGN.md +640 -0
  55. package/lib/design-md/cache/awesome-design-md/ollama/DESIGN.md +539 -0
  56. package/lib/design-md/cache/awesome-design-md/opencode.ai/DESIGN.md +521 -0
  57. package/lib/design-md/cache/awesome-design-md/pinterest/DESIGN.md +597 -0
  58. package/lib/design-md/cache/awesome-design-md/playstation/DESIGN.md +661 -0
  59. package/lib/design-md/cache/awesome-design-md/posthog/DESIGN.md +690 -0
  60. package/lib/design-md/cache/awesome-design-md/raycast/DESIGN.md +669 -0
  61. package/lib/design-md/cache/awesome-design-md/renault/DESIGN.md +589 -0
  62. package/lib/design-md/cache/awesome-design-md/replicate/DESIGN.md +616 -0
  63. package/lib/design-md/cache/awesome-design-md/resend/DESIGN.md +585 -0
  64. package/lib/design-md/cache/awesome-design-md/revolut/DESIGN.md +636 -0
  65. package/lib/design-md/cache/awesome-design-md/runwayml/DESIGN.md +244 -0
  66. package/lib/design-md/cache/awesome-design-md/sanity/DESIGN.md +357 -0
  67. package/lib/design-md/cache/awesome-design-md/sentry/DESIGN.md +551 -0
  68. package/lib/design-md/cache/awesome-design-md/shopify/DESIGN.md +516 -0
  69. package/lib/design-md/cache/awesome-design-md/slack/DESIGN.md +482 -0
  70. package/lib/design-md/cache/awesome-design-md/spacex/DESIGN.md +363 -0
  71. package/lib/design-md/cache/awesome-design-md/spotify/DESIGN.md +246 -0
  72. package/lib/design-md/cache/awesome-design-md/starbucks/DESIGN.md +580 -0
  73. package/lib/design-md/cache/awesome-design-md/stripe/DESIGN.md +487 -0
  74. package/lib/design-md/cache/awesome-design-md/supabase/DESIGN.md +462 -0
  75. package/lib/design-md/cache/awesome-design-md/superhuman/DESIGN.md +448 -0
  76. package/lib/design-md/cache/awesome-design-md/tesla/DESIGN.md +286 -0
  77. package/lib/design-md/cache/awesome-design-md/theverge/DESIGN.md +339 -0
  78. package/lib/design-md/cache/awesome-design-md/together.ai/DESIGN.md +633 -0
  79. package/lib/design-md/cache/awesome-design-md/uber/DESIGN.md +636 -0
  80. package/lib/design-md/cache/awesome-design-md/vercel/DESIGN.md +736 -0
  81. package/lib/design-md/cache/awesome-design-md/vodafone/DESIGN.md +538 -0
  82. package/lib/design-md/cache/awesome-design-md/voltagent/DESIGN.md +521 -0
  83. package/lib/design-md/cache/awesome-design-md/warp/DESIGN.md +526 -0
  84. package/lib/design-md/cache/awesome-design-md/webflow/DESIGN.md +588 -0
  85. package/lib/design-md/cache/awesome-design-md/wired/DESIGN.md +497 -0
  86. package/lib/design-md/cache/awesome-design-md/wise/DESIGN.md +544 -0
  87. package/lib/design-md/cache/awesome-design-md/x.ai/DESIGN.md +465 -0
  88. package/lib/design-md/cache/awesome-design-md/zapier/DESIGN.md +537 -0
  89. package/lib/design-md/cache/feelanet/faux/design.md +288 -0
  90. package/lib/design-md/cache/feelanet/faux-teams/design.md +382 -0
  91. package/lib/design-md/catalog.json +454 -0
  92. package/lib/design-md/catalog.mjs +268 -0
  93. package/lib/design-md/flow.mjs +166 -0
  94. package/lib/design-md/open.mjs +50 -0
  95. package/lib/design-md/providers/awesome-design-md.mjs +114 -0
  96. package/lib/design-md/providers/index.mjs +9 -0
  97. package/lib/design-md/scan.mjs +291 -0
  98. package/lib/engine.mjs +44 -0
  99. package/lib/gitignore.mjs +18 -0
  100. package/lib/items/.omc/state/sessions/a859aff7-59d9-4ff6-afc6-8cafe4471dc4/pre-tool-advisory-throttle.json +14 -0
  101. package/lib/items/mcp.codebase-memory.mjs +6 -0
  102. package/lib/items/mcp.notion.mjs +6 -0
  103. package/lib/items/mcp.supabase.mjs +6 -0
  104. package/lib/items/mcp.vercel.mjs +6 -0
  105. package/lib/items/plugin.bkit.mjs +7 -0
  106. package/lib/items/plugin.mattpocock-skills.mjs +8 -0
  107. package/lib/items/plugin.superpowers.mjs +7 -0
  108. package/lib/items/skill.gsd.mjs +24 -0
  109. package/lib/items/skill.gstack.mjs +39 -0
  110. package/lib/jsonfile.mjs +40 -0
  111. package/lib/tomlfile.mjs +60 -0
  112. package/lib/tui/render.mjs +192 -0
  113. package/lib/tui/rows.mjs +206 -0
  114. package/lib/tui/run.mjs +257 -0
  115. package/lib/tui/state.mjs +188 -0
  116. package/package.json +36 -0
@@ -0,0 +1,145 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join, dirname } from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+ import { execFileSync } from 'node:child_process'
5
+ import { CLIS, CLI_IDS } from './clis.mjs'
6
+ import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
7
+
8
+ const ITEMS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'items')
9
+
10
+ export async function loadItems() {
11
+ const files = readdirSync(ITEMS_DIR).filter((f) => f.endsWith('.mjs')).sort()
12
+ const items = []
13
+ for (const f of files) {
14
+ const mod = await import(pathToFileURL(join(ITEMS_DIR, f)).href)
15
+ items.push(validate(mod.default, f))
16
+ }
17
+ return items
18
+ }
19
+
20
+ function validate(item, file) {
21
+ for (const field of ['id', 'category', 'label', 'detect', 'install', 'uninstall']) {
22
+ if (!item?.[field]) throw new Error(`${file}: ${field} 누락`)
23
+ }
24
+ return item
25
+ }
26
+
27
+ function assertReasons(id, supports, unsupported) {
28
+ for (const cli of CLI_IDS) {
29
+ if (!supports.includes(cli) && !unsupported[cli]) {
30
+ throw new Error(`${id}: 미지원 CLI '${cli}'의 사유(unsupported.${cli})가 필요합니다`)
31
+ }
32
+ }
33
+ }
34
+
35
+ // shell 모드에서 Node는 명령과 인자를 공백으로 이어붙일 뿐 quote하지 않으므로
36
+ // 직접 감싼다. 공백이 있을 때만 감싸면 셸 메타문자가 그대로 노출된다 —
37
+ // `D:\R&D\repo` 아래에서 gstack clone 대상이 명령 두 개로 쪼개졌다.
38
+ // 조건 없이 항상 감싸고, 셸별로 실제로 안전한 인용 규칙을 쓴다.
39
+ // - cmd.exe: 큰따옴표 안에서 & | ^ ( ) < >는 리터럴이다. 큰따옴표 자체는
40
+ // 이 위치에서 일관되게 이스케이프할 방법이 없어(cmd는 \"를 해석하지 않는다)
41
+ // 깨진 명령을 만드는 대신 거부한다 — Windows 경로에 올 수 없는 문자다.
42
+ // 닫는 따옴표 앞의 백슬래시 연속은 자식의 argv 파서가 이스케이프로 읽으므로
43
+ // 두 배로 늘린다(Windows 표준 규칙).
44
+ // 남는 것은 %VAR% 확장뿐인데, 짝을 이룬 %가 실제 환경변수일 때만 치환되고
45
+ // 그러면 경로가 달라져 명령이 실패한다 — 조용한 오작동이 아니라 보이는 실패다.
46
+ // - POSIX sh: 작은따옴표 안은 $ ` \ 까지 전부 리터럴이다. 안의 작은따옴표만
47
+ // '\'' 로 끊어 이어 붙인다. 큰따옴표로 감싸면 $(...)가 살아남는다.
48
+ export function shellQuote(text, platform = process.platform) {
49
+ const s = String(text)
50
+ if (platform === 'win32') {
51
+ if (s.includes('"')) {
52
+ throw new Error(`셸에 넘길 수 없는 큰따옴표가 인자에 있습니다: ${s}`)
53
+ }
54
+ return `"${s.replace(/(\\*)$/, '$1$1')}"`
55
+ }
56
+ return `'${s.replace(/'/g, "'\\''")}'`
57
+ }
58
+
59
+ export function makeExec(dryRun, log = console.log) {
60
+ return (cmd, args, opts = {}) => {
61
+ if (dryRun) {
62
+ log(` [dry-run] ${cmd} ${args.join(' ')}`)
63
+ return { ok: true, output: '' }
64
+ }
65
+ // Windows에서는 npx/claude가 .cmd 심이라 shell 경유가 필요하다.
66
+ const shell = opts.shell ?? process.platform === 'win32'
67
+ // shell + 인자 배열을 함께 넘기면 Node가 DEP0190으로 경고한다(인자가 quote 없이 이어붙기 때문).
68
+ // 어차피 우리가 직접 quote하므로, 완성된 한 줄 명령을 넘기고 인자 배열은 비운다.
69
+ const [file, fileArgs] = shell ? [[cmd, ...args].map((s) => shellQuote(s)).join(' '), []] : [cmd, args]
70
+ try {
71
+ const output = execFileSync(file, fileArgs, {
72
+ encoding: 'utf8',
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ ...opts,
75
+ shell,
76
+ })
77
+ return { ok: true, output }
78
+ } catch (err) {
79
+ return { ok: false, output: String(err.stderr ?? err.message) }
80
+ }
81
+ }
82
+ }
83
+
84
+ export function defineMcp({ id, label, server, supports = [...CLI_IDS], unsupported = {}, note }) {
85
+ assertReasons(id, supports, unsupported)
86
+ const name = id.replace(/^mcp\./, '')
87
+ return {
88
+ id, category: 'mcp', label, scope: 'project', supports, unsupported, note,
89
+ async detect({ root }) {
90
+ const present = supports.filter((cli) => CLIS[cli].has(root, name))
91
+ if (present.length === 0) return { status: 'absent' }
92
+ if (present.length === supports.length) return { status: 'installed' }
93
+ return { status: 'partial', detail: `등록됨: ${present.join(', ')} / 누락: ${supports.filter((c) => !present.includes(c)).join(', ')}` }
94
+ },
95
+ async install({ root, dryRun, log = () => {} }) {
96
+ for (const cli of supports) {
97
+ if (!CLIS[cli].has(root, name)) {
98
+ if (dryRun) log(` [dry-run] ${CLIS[cli].label} 설정에 ${name} 등록`)
99
+ else CLIS[cli].add(root, name, server)
100
+ }
101
+ }
102
+ },
103
+ async uninstall({ root, dryRun, log = () => {} }) {
104
+ for (const cli of supports) {
105
+ if (CLIS[cli].has(root, name)) {
106
+ if (dryRun) log(` [dry-run] ${CLIS[cli].label} 설정에서 ${name} 제거`)
107
+ else CLIS[cli].remove(root, name)
108
+ }
109
+ }
110
+ },
111
+ }
112
+ }
113
+
114
+ export function definePlugin({ id, label, installId, detectIds, marketplace, note }) {
115
+ const unsupported = Object.fromEntries(
116
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, 'Claude Code 전용 플러그인']),
117
+ )
118
+ return {
119
+ id, category: 'plugin', label, scope: 'project', supports: ['claude'], unsupported, note,
120
+ async detect({ root }) {
121
+ return { status: isPluginEnabled(root, detectIds) ? 'installed' : 'absent' }
122
+ },
123
+ async install(ctx) {
124
+ const { root, dryRun, exec } = ctx
125
+ if (marketplace) exec('claude', ['plugin', 'marketplace', 'add', marketplace.repo], { cwd: root })
126
+ const r = exec('claude', ['plugin', 'install', installId, '--scope', 'project'], { cwd: root })
127
+ if (!r.ok) {
128
+ if (!dryRun) enablePlugin(root, installId, marketplace)
129
+ return { fallback: true, message: '설정 기록됨 — 다음 Claude Code 실행 시 다운로드됩니다' }
130
+ }
131
+ },
132
+ async uninstall(ctx) {
133
+ const { root, dryRun, exec } = ctx
134
+ const r = exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
135
+ if (!r.ok && !dryRun) disablePlugin(root, detectIds)
136
+ },
137
+ }
138
+ }
139
+
140
+ export function defineSkill({ id, label, scope, detect, install, uninstall, note }) {
141
+ const unsupported = Object.fromEntries(
142
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, 'Claude Code 스킬 설치본']),
143
+ )
144
+ return { id, category: 'skill', label, scope, supports: ['claude'], unsupported, note, detect, install, uninstall }
145
+ }
@@ -0,0 +1,64 @@
1
+ import { repoPath, repoPathStrict } from './context.mjs'
2
+ import { readJson, setKey, removeKey } from './jsonfile.mjs'
3
+
4
+ const SETTINGS = '.claude/settings.json'
5
+
6
+ function readSettings(root) {
7
+ return readJson(repoPath(root, SETTINGS)) ?? {}
8
+ }
9
+
10
+ // 쓰기 경로만 링크 이탈까지 검사한다 — 저장소 안 `.claude`가 홈을 가리키는
11
+ // Junction이면 어휘적 검사는 통과하고 글로벌 settings.json이 수정된다.
12
+ // clis.mjs의 add·remove, bootstrap의 apply.mjs와 같은 규칙.
13
+ function settingsFile(root) {
14
+ return repoPathStrict(root, SETTINGS)
15
+ }
16
+
17
+ function enabledList(settings) {
18
+ const ep = settings.enabledPlugins
19
+ if (Array.isArray(ep)) return ep
20
+ if (ep && typeof ep === 'object') return Object.keys(ep).filter((k) => ep[k])
21
+ return []
22
+ }
23
+
24
+ export function isPluginEnabled(root, ids) {
25
+ const list = enabledList(readSettings(root))
26
+ return ids.some((id) => list.includes(id))
27
+ }
28
+
29
+ export function enablePlugin(root, id, marketplace) {
30
+ const file = settingsFile(root)
31
+ const settings = readSettings(root)
32
+ if (Array.isArray(settings.enabledPlugins)) {
33
+ if (!settings.enabledPlugins.includes(id)) {
34
+ setKey(file, ['enabledPlugins', settings.enabledPlugins.length], id)
35
+ }
36
+ } else {
37
+ setKey(file, ['enabledPlugins', id], true)
38
+ }
39
+ if (marketplace) {
40
+ setKey(file, ['extraKnownMarketplaces', marketplace.name], {
41
+ source: { source: 'github', repo: marketplace.repo },
42
+ })
43
+ }
44
+ }
45
+
46
+ export function disablePlugin(root, ids) {
47
+ const file = settingsFile(root)
48
+ const settings = readSettings(root)
49
+ if (Array.isArray(settings.enabledPlugins)) {
50
+ const kept = settings.enabledPlugins.filter((e) => !ids.includes(e))
51
+ setKey(file, ['enabledPlugins'], kept)
52
+ } else if (settings.enabledPlugins) {
53
+ for (const id of ids) removeKey(file, ['enabledPlugins', id])
54
+ }
55
+ // 제거한 플러그인이 쓰던 마켓플레이스만 정리 대상으로 삼는다 (무관한 사용자 항목 보존).
56
+ const candidates = new Set(ids.map((id) => id.slice(id.indexOf('@') + 1)))
57
+ const after = readSettings(root)
58
+ const remaining = enabledList(after).map((e) => e.slice(e.indexOf('@') + 1))
59
+ for (const mkt of candidates) {
60
+ if (!remaining.includes(mkt) && after.extraKnownMarketplaces?.[mkt] !== undefined) {
61
+ removeKey(file, ['extraKnownMarketplaces', mkt])
62
+ }
63
+ }
64
+ }
package/lib/clis.mjs ADDED
@@ -0,0 +1,102 @@
1
+ import { repoPath, repoPathStrict } from './context.mjs'
2
+ import { readJson, setKey, removeKey, getIn } from './jsonfile.mjs'
3
+ import { hasSection, appendSection, removeSection } from './tomlfile.mjs'
4
+
5
+ // 읽기(has)는 어휘적 경로면 충분하고, 실제로 만들거나 고치는 경로(add·remove)만
6
+ // 링크를 통한 저장소 이탈까지 검사한다 — bootstrap의 apply.mjs, design-md의
7
+ // designPaths와 같은 규칙이다. 저장소 안 `.codex`·`.claude`가 홈을 가리키는
8
+ // Junction일 때 MCP 등록 한 번으로 글로벌 설정이 생성·수정되는 것을 막는다.
9
+ function jsonAdapter(relFile, topKey, toEntry) {
10
+ return {
11
+ has(root, name) {
12
+ const data = readJson(repoPath(root, relFile))
13
+ return getIn(data, [topKey, name]) !== undefined
14
+ },
15
+ add(root, name, server) {
16
+ setKey(repoPathStrict(root, relFile), [topKey, name], toEntry(server))
17
+ },
18
+ remove(root, name) {
19
+ removeKey(repoPathStrict(root, relFile), [topKey, name])
20
+ },
21
+ }
22
+ }
23
+
24
+ function tomlLines(server) {
25
+ if (server.kind === 'http') return [`url = ${JSON.stringify(server.url)}`]
26
+ return [
27
+ `command = ${JSON.stringify(server.command)}`,
28
+ `args = [${server.args.map((a) => JSON.stringify(a)).join(', ')}]`,
29
+ ]
30
+ }
31
+
32
+ export const CLIS = {
33
+ claude: {
34
+ label: 'Claude Code',
35
+ ...jsonAdapter('.mcp.json', 'mcpServers', (s) =>
36
+ s.kind === 'http'
37
+ ? { type: 'http', url: s.url }
38
+ : { type: 'stdio', command: s.command, args: s.args }),
39
+ },
40
+ codex: {
41
+ label: 'Codex',
42
+ has: (root, name) => hasSection(repoPath(root, '.codex/config.toml'), name),
43
+ add: (root, name, s) => appendSection(repoPathStrict(root, '.codex/config.toml'), name, tomlLines(s)),
44
+ remove: (root, name) => removeSection(repoPathStrict(root, '.codex/config.toml'), name),
45
+ },
46
+ gemini: {
47
+ label: 'Gemini CLI',
48
+ ...jsonAdapter('.gemini/settings.json', 'mcpServers', (s) =>
49
+ s.kind === 'http' ? { httpUrl: s.url } : { command: s.command, args: s.args }),
50
+ },
51
+ opencode: {
52
+ label: 'OpenCode',
53
+ ...jsonAdapter('opencode.jsonc', 'mcp', (s) =>
54
+ s.kind === 'http'
55
+ ? { type: 'remote', url: s.url, enabled: true }
56
+ : { type: 'local', command: [s.command, ...s.args], enabled: true }),
57
+ },
58
+ kilo: {
59
+ label: 'Kilo Code',
60
+ ...jsonAdapter('.kilocode/mcp.json', 'mcpServers', (s) =>
61
+ s.kind === 'http'
62
+ ? { type: 'streamable-http', url: s.url, disabled: false }
63
+ : { command: s.command, args: s.args, disabled: false }),
64
+ },
65
+ kiro: {
66
+ label: 'Kiro',
67
+ ...jsonAdapter('.kiro/settings/mcp.json', 'mcpServers', (s) =>
68
+ s.kind === 'http'
69
+ ? { url: s.url, disabled: false, autoApprove: [] }
70
+ : { command: s.command, args: s.args, disabled: false, autoApprove: [] }),
71
+ },
72
+ kimi: {
73
+ label: 'Kimi Code',
74
+ ...jsonAdapter('.kimi-code/mcp.json', 'mcpServers', (s) =>
75
+ s.kind === 'http' ? { url: s.url } : { command: s.command, args: s.args }),
76
+ },
77
+ grok: {
78
+ label: 'Grok Build',
79
+ has: (root, name) => hasSection(repoPath(root, '.grok/config.toml'), name),
80
+ add: (root, name, s) => appendSection(repoPathStrict(root, '.grok/config.toml'), name, tomlLines(s)),
81
+ remove: (root, name) => removeSection(repoPathStrict(root, '.grok/config.toml'), name),
82
+ },
83
+ copilot: {
84
+ label: 'GitHub Copilot CLI',
85
+ // Copilot CLI는 루트 .mcp.json도 읽지만(같은 이름이면 그쪽이 우선),
86
+ // 도구마다 자기 파일을 갖는 이 저장소의 패턴을 따라 .github/mcp.json에 쓴다.
87
+ ...jsonAdapter('.github/mcp.json', 'mcpServers', (s) =>
88
+ s.kind === 'http'
89
+ ? { type: 'http', url: s.url }
90
+ : { type: 'local', command: s.command, args: s.args }),
91
+ },
92
+ vscode: {
93
+ label: 'VS Code Copilot',
94
+ // VS Code는 최상위 키가 servers이고 로컬 서버 타입이 stdio다.
95
+ ...jsonAdapter('.vscode/mcp.json', 'servers', (s) =>
96
+ s.kind === 'http'
97
+ ? { type: 'http', url: s.url }
98
+ : { type: 'stdio', command: s.command, args: s.args }),
99
+ },
100
+ }
101
+
102
+ export const CLI_IDS = Object.keys(CLIS)
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { lstatSync, realpathSync } from 'node:fs'
3
+ import { dirname, resolve, sep } from 'node:path'
4
+
5
+ export function findRepoRoot(cwd = process.cwd()) {
6
+ let out
7
+ try {
8
+ out = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
9
+ } catch {
10
+ throw new Error('git 저장소 안에서 실행해야 합니다.')
11
+ }
12
+ return resolve(out.trim())
13
+ }
14
+
15
+ export function repoPath(root, rel) {
16
+ const abs = resolve(root, rel)
17
+ const normRoot = resolve(root)
18
+ if (abs !== normRoot && !abs.startsWith(normRoot + sep)) {
19
+ throw new Error(`저장소 밖의 경로에는 쓸 수 없습니다: ${abs}`)
20
+ }
21
+ return abs
22
+ }
23
+
24
+ function pathExists(target) {
25
+ try {
26
+ lstatSync(target)
27
+ return true
28
+ } catch {
29
+ return false
30
+ }
31
+ }
32
+
33
+ // repoPath의 어휘적 검사에 더해, 가장 가까운 존재하는 조상의 realpath가
34
+ // 저장소 안인지 확인한다. 어휘적 검사만으로는 심볼릭 링크/Junction을 통한
35
+ // 이탈을 막지 못한다. 부트스트랩의 모든 쓰기 경로가 이 함수를 쓴다.
36
+ export function repoPathStrict(root, rel) {
37
+ const abs = repoPath(root, rel)
38
+
39
+ let probe = abs
40
+ while (!pathExists(probe)) {
41
+ const parent = dirname(probe)
42
+ if (parent === probe) break
43
+ probe = parent
44
+ }
45
+
46
+ let realProbe
47
+ try {
48
+ realProbe = realpathSync(probe)
49
+ } catch (err) {
50
+ // 경합으로 사라졌거나(ENOENT) 링크가 순환하는(ELOOP) 경우다.
51
+ // 원인을 알 수 없는 raw 예외 대신 진단 가능한 메시지로 바꾼다.
52
+ throw new Error(`경로를 확인할 수 없습니다: ${probe} (${err.code ?? err.message})`)
53
+ }
54
+
55
+ let realRoot
56
+ try {
57
+ realRoot = realpathSync(root)
58
+ } catch (err) {
59
+ throw new Error(`경로를 확인할 수 없습니다: ${root} (${err.code ?? err.message})`)
60
+ }
61
+ if (realProbe !== realRoot && !realProbe.startsWith(realRoot + sep)) {
62
+ throw new Error(`저장소 내부 경로가 외부 링크를 통해 이탈합니다: ${probe} -> ${realProbe}`)
63
+ }
64
+ return abs
65
+ }
package/lib/deps.mjs ADDED
@@ -0,0 +1,19 @@
1
+ // 선택적 의존성 로딩. 부트스트랩은 npm install 없이 돌아야 하므로
2
+ // jsonc-parser·smol-toml에 닿는 모듈은 전부 동적 import로 가져오고,
3
+ // 없을 때는 Node의 원시 메시지 대신 설치 방법을 안내한다.
4
+
5
+ export const DEPS_HINT =
6
+ '이 기능에는 의존성이 필요합니다. 다음 중 하나를 실행하세요:\n' +
7
+ ' ./setup-agents.sh --tui\n' +
8
+ ' npm install --prefix agent-installer'
9
+
10
+ export async function withDeps(load) {
11
+ try {
12
+ return await load()
13
+ } catch (err) {
14
+ // 모듈이 정말 없을 때만 안내로 바꾼다. 로드된 모듈 안에서 난 오류는
15
+ // 그대로 올려보내야 한다 — 삼키면 진짜 버그가 "의존성 없음"으로 둔갑한다.
16
+ if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err
17
+ throw new Error(DEPS_HINT)
18
+ }
19
+ }