@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,291 @@
1
+ // design.md 디렉터리 스캔 — 프로바이더 등록 없이 **디렉터리 구조만으로** 항목을 찾는다.
2
+ // 번들 캐시(lib/design-md/cache/<소스>/…)에 폴더를 넣거나, 외부 경로를
3
+ // AGENT_INSTALLER_DESIGN_MD_DIRS 환경변수/`--design-dir`로 지정하면 그대로 목록에 오른다.
4
+ // 사내에서 오프라인으로 정의한 DESIGN.md를 코드 수정 없이 포함하기 위한 경로다.
5
+ import { existsSync, statSync, readdirSync, readFileSync, openSync, readSync, closeSync } from 'node:fs'
6
+ import { join, dirname, basename, resolve, delimiter } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { StringDecoder } from 'node:string_decoder'
9
+ import { isSafeSegment } from './catalog.mjs'
10
+ import { PROVIDERS } from './providers/index.mjs'
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url))
13
+ export const BUNDLE_DIR = join(HERE, 'cache')
14
+
15
+ const DESIGN_FILE = 'DESIGN.md'
16
+ const DESIGN_FILE_RE = /^design\.md$/i
17
+ const MAX_DEPTH = 5 // <소스>/<카테고리…>/<이름>/DESIGN.md — 과도한 순회 방지
18
+ const META_BYTES = 4096 // 메타데이터는 앞부분만 읽는다(전체 로드 회피)
19
+ // getdesign.md 형식은 디자인 토큰 전체가 frontmatter라 수 KB에 이른다.
20
+ // 닫는 구분자가 첫 창을 넘어가면 이만큼까지 늘려 다시 읽는다.
21
+ const META_BYTES_MAX = 65536
22
+ const MAX_DESCRIPTION = 200
23
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.svn'])
24
+
25
+ const FM_OPEN = /^---[ \t]*\r?\n/
26
+ const FM_CLOSE = /\r?\n---[ \t]*(\r?\n|$)/
27
+
28
+ export const BUNDLE_CATEGORY = '기타' // 번들 캐시에서 카테고리를 못 얻었을 때
29
+ export const LOCAL_CATEGORY = '사내' // 외부(사내) 경로에서 카테고리를 못 얻었을 때
30
+
31
+ // 설치 경로 `design-md/<소스>/<이름>/`에 쓰이므로 경로에 위험한 문자만 걷어낸다.
32
+ // 한글 등 유니코드 이름은 그대로 살린다 — 사내 디렉터리명이 대개 그렇다.
33
+ export function sanitizeId(text) {
34
+ const cleaned = String(text)
35
+ .replace(/[\\/:*?"<>|\x00-\x1f]+/g, '-') // 경로 구분자·Windows 금지 문자·제어문자
36
+ .replace(/\s+/g, '-')
37
+ .replace(/^[-.]+|[-.]+$/g, '') // 선행·후행 점은 Windows에서 문제가 된다
38
+ .replace(/-{2,}/g, '-')
39
+ return cleaned && cleaned !== '.' && cleaned !== '..' ? cleaned : 'local'
40
+ }
41
+
42
+ // `--design-dir` / 환경변수 항목: `<경로>` 또는 `<소스id>=<경로>`.
43
+ // id에는 경로 구분자가 올 수 없으므로 `D:\a=b` 같은 경로를 id로 오인하지 않는다.
44
+ export function parseDirSpec(spec) {
45
+ const text = String(spec).trim()
46
+ const m = /^([^=\\/]+)=(.+)$/.exec(text)
47
+ const dir = resolve(m ? m[2].trim() : text)
48
+ return { id: m ? m[1].trim() : basename(dir), dir }
49
+ }
50
+
51
+ // 환경변수의 경로 목록. 구분자는 OS 기본(Windows `;`, POSIX `:`).
52
+ export function extraDirsFromEnv(env = process.env) {
53
+ const raw = env.AGENT_INSTALLER_DESIGN_MD_DIRS
54
+ if (!raw) return []
55
+ return raw.split(delimiter).map((s) => s.trim()).filter(Boolean)
56
+ }
57
+
58
+ // 앞부분만 읽는다. 경계에서 멀티바이트 문자가 잘려도 대체 문자가 남지 않도록
59
+ // StringDecoder로 불완전 시퀀스를 버린다.
60
+ function readHead(file, bytes = META_BYTES) {
61
+ const fd = openSync(file, 'r')
62
+ try {
63
+ const buf = Buffer.alloc(bytes)
64
+ const n = readSync(fd, buf, 0, bytes, 0)
65
+ return new StringDecoder('utf8').write(buf.subarray(0, n))
66
+ } finally {
67
+ closeSync(fd)
68
+ }
69
+ }
70
+
71
+ // frontmatter가 첫 창 안에서 닫히지 않으면 한 번 더 크게 읽는다.
72
+ function readMetaText(file) {
73
+ const head = readHead(file)
74
+ if (!FM_OPEN.test(head) || FM_CLOSE.test(head)) return head
75
+ return readHead(file, META_BYTES_MAX)
76
+ }
77
+
78
+ // DESIGN.md 본문에서 라벨·설명·카테고리를 뽑는다.
79
+ // frontmatter(있으면)가 우선, 없으면 첫 제목 → 라벨, 첫 문단 → 설명.
80
+ export function parseDesignMeta(text) {
81
+ const meta = {}
82
+ let body = String(text ?? '')
83
+
84
+ const open = FM_OPEN.exec(body)
85
+ if (open) {
86
+ const rest = body.slice(open[0].length)
87
+ const close = FM_CLOSE.exec(rest)
88
+ // 닫는 구분자를 못 찾으면(읽기 창보다 긴 frontmatter) 본문 후보를 비워
89
+ // `version: alpha` 같은 frontmatter 줄이 설명으로 새어 나가지 않게 한다.
90
+ const block = close ? rest.slice(0, close.index) : rest
91
+ body = close ? rest.slice(close.index + close[0].length) : ''
92
+
93
+ const fm = {}
94
+ for (const line of block.split(/\r?\n/)) {
95
+ // 최상위 키만 본다 — 들여쓴 줄은 디자인 토큰 같은 중첩 값이다.
96
+ const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*)$/.exec(line)
97
+ if (!kv) continue
98
+ const key = kv[1].toLowerCase()
99
+ const value = kv[2].trim().replace(/^["']|["']$/g, '').trim()
100
+ if (value) fm[key] ??= value
101
+ }
102
+ meta.label ??= fm.title || fm.label || fm.name || undefined
103
+ meta.category ??= fm.category
104
+ meta.description ??= (fm.description || fm.summary)?.slice(0, MAX_DESCRIPTION)
105
+ for (const key of Object.keys(meta)) if (meta[key] === undefined) delete meta[key]
106
+ }
107
+
108
+ let sawHeading = false
109
+ let fence = null // 코드블록 안에서는 어떤 줄도 설명으로 쓰지 않는다
110
+ for (const raw of body.split(/\r?\n/)) {
111
+ const line = raw.trim()
112
+ if (!line) continue
113
+ const fenceMark = /^(```+|~~~+)/.exec(line)
114
+ if (fenceMark) {
115
+ if (fence && line.startsWith(fence)) fence = null
116
+ else if (!fence) fence = fenceMark[1]
117
+ continue
118
+ }
119
+ if (fence) continue
120
+ const heading = /^#{1,3}\s+(.+)$/.exec(line)
121
+ if (heading) {
122
+ if (!sawHeading) {
123
+ meta.label ??= heading[1].trim()
124
+ sawHeading = true
125
+ }
126
+ continue
127
+ }
128
+ // 주석·표·구분선·목록·인용은 설명으로 쓰지 않는다.
129
+ if (/^(<!--|<|\||-{3,}|={3,}|[-*+]\s|\d+\.\s|>)/.test(line)) continue
130
+ meta.description ??= line.slice(0, MAX_DESCRIPTION)
131
+ if (meta.label && meta.description) break
132
+ }
133
+ return meta
134
+ }
135
+
136
+ // 디렉터리 1회 읽기로 하위 폴더와 design 파일을 함께 얻는다.
137
+ // 읽을 수 없는 디렉터리(권한·경합)는 목록을 통째로 실패시키지 않고 건너뛴다.
138
+ // 심볼릭 링크는 따라가지 않는다(순환 방지) — isDirectory()가 링크에는 false다.
139
+ // 파일명은 대소문자를 가리지 않는다: 사내 문서는 `design.md`로 쓰는 경우가 많고,
140
+ // 대소문자를 구분하는 파일시스템(Linux·macOS)에서 통째로 누락되면 안 된다.
141
+ function readDir(dir) {
142
+ try {
143
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return { dirs: [], designFile: null }
144
+ const dirents = readdirSync(dir, { withFileTypes: true })
145
+ const designs = dirents
146
+ .filter((d) => d.isFile() && DESIGN_FILE_RE.test(d.name))
147
+ .map((d) => d.name)
148
+ .sort() // 여러 표기가 공존하면 결정적으로 고른다
149
+ return {
150
+ dirs: dirents
151
+ .filter((d) => d.isDirectory() && !d.name.startsWith('.') && !SKIP_DIRS.has(d.name))
152
+ .map((d) => d.name)
153
+ .sort(),
154
+ designFile: designs.includes(DESIGN_FILE) ? DESIGN_FILE : (designs[0] ?? null),
155
+ }
156
+ } catch {
157
+ return { dirs: [], designFile: null }
158
+ }
159
+ }
160
+
161
+ function subdirs(dir) {
162
+ return readDir(dir).dirs
163
+ }
164
+
165
+ // 디렉터리 트리에서 DESIGN.md를 찾아 엔트리로 만든다.
166
+ // `<루트>/<카테고리…>/<이름>/DESIGN.md` — DESIGN.md가 있는 폴더가 곧 항목(리프)이다.
167
+ // 루트 바로 아래 DESIGN.md가 있으면 루트 폴더명이 항목 이름이 된다.
168
+ export function scanDesignDir(dir, { defaultCategory = LOCAL_CATEGORY, maxDepth = MAX_DEPTH, log } = {}) {
169
+ const root = resolve(dir)
170
+ const entries = []
171
+ const seen = new Set()
172
+
173
+ // 설치 경로가 `design-md/<소스>/<이름>/`이라 소스 안에서 이름이 유일해야 한다.
174
+ // 카테고리가 다른 동명 항목(웹/버튼, 모바일/버튼)은 카테고리를 접두사로 붙여 살린다.
175
+ const uniqueName = (name, segs) => {
176
+ if (!seen.has(name)) return name
177
+ const prefixed = sanitizeId([...segs.slice(0, -1), name].join('-'))
178
+ let candidate = prefixed
179
+ for (let n = 2; seen.has(candidate); n++) candidate = `${prefixed}-${n}`
180
+ if (log) log(` design.md 이름이 겹쳐 '${candidate}'로 구분합니다: ${segs.join('/')}`)
181
+ return candidate
182
+ }
183
+
184
+ const visit = (current, segs, depth) => {
185
+ const { dirs, designFile } = readDir(current)
186
+ if (designFile) {
187
+ const file = join(current, designFile)
188
+ const found = segs.length > 0 ? segs[segs.length - 1] : basename(root)
189
+ if (found && isSafeSegment(found)) {
190
+ const name = uniqueName(found, segs)
191
+ seen.add(name)
192
+ let meta = {}
193
+ try {
194
+ meta = parseDesignMeta(readMetaText(file))
195
+ } catch {
196
+ // 읽기 실패는 메타데이터 없이 진행 — 목록에서 빠지지 않게 한다.
197
+ }
198
+ entries.push({
199
+ name,
200
+ label: meta.label || found,
201
+ category: meta.category || segs.slice(0, -1).join(' / ') || defaultCategory,
202
+ description: meta.description || '',
203
+ file,
204
+ })
205
+ }
206
+ return // 항목 폴더 안쪽은 더 내려가지 않는다
207
+ }
208
+ if (depth >= maxDepth) return
209
+ for (const sub of dirs) visit(join(current, sub), [...segs, sub], depth + 1)
210
+ }
211
+
212
+ visit(root, [], 0)
213
+ return entries.sort((a, b) => a.name.localeCompare(b.name))
214
+ }
215
+
216
+ // 스캔한 디렉터리를 프로바이더 인터페이스로 감싼다.
217
+ // 네트워크 원본이 없으므로 로컬 파일이 곧 최신본이고, 웹 미리보기 URL은 없다.
218
+ export function makeDirProvider({ id, label, dir, entries }) {
219
+ const files = new Map(entries.map((e) => [e.name, e.file]))
220
+ return {
221
+ id,
222
+ label,
223
+ dir,
224
+ local: true,
225
+ files: [DESIGN_FILE],
226
+
227
+ fileUrl(name) {
228
+ return files.get(name) ?? null
229
+ },
230
+
231
+ webUrl() {
232
+ return null // 로컬 정의는 getdesign.md 페이지가 없다
233
+ },
234
+
235
+ bundledText(name) {
236
+ const file = files.get(name)
237
+ if (!file || !existsSync(file)) return null
238
+ return readFileSync(file, 'utf8')
239
+ },
240
+
241
+ // fresh 업데이트도 같은 로컬 파일을 읽는다 — 디렉터리가 곧 원본이다.
242
+ async fetchFile(fetchImpl, name) {
243
+ return this.bundledText(name)
244
+ },
245
+
246
+ async fetchCatalog() {
247
+ return entries.map(({ file, ...rest }) => rest)
248
+ },
249
+ }
250
+ }
251
+
252
+ // 번들 캐시 하위 디렉터리 + 지정한 외부 경로를 소스 목록으로 만든다.
253
+ // 반환: [{ id, dir, label, bundled, entries, provider }]
254
+ export function discoverSources({ bundleDir = BUNDLE_DIR, extraDirs = [], reservedIds, log } = {}) {
255
+ const sources = []
256
+ const usedIds = new Set()
257
+ // 등록된 프로바이더 id는 번들 캐시 전용이다. 외부 경로가 그 id를 차지하면
258
+ // 로컬 파일 대신 네트워크로 나가버리므로 접미사로 비켜 간다.
259
+ const reserved = new Set(reservedIds ?? PROVIDERS.map((p) => p.id))
260
+
261
+ const add = ({ id, dir, bundled, defaultCategory }) => {
262
+ const entries = scanDesignDir(dir, { defaultCategory, log })
263
+ if (entries.length === 0) {
264
+ if (!bundled && log) log(` design.md 소스에 DESIGN.md가 없습니다: ${dir}`)
265
+ return
266
+ }
267
+ const base = sanitizeId(id)
268
+ const taken = (candidate) => usedIds.has(candidate) || (!bundled && reserved.has(candidate))
269
+ let uid = base
270
+ for (let n = 2; taken(uid); n++) uid = `${base}-${n}`
271
+ if (uid !== base && log) log(` design.md 소스 id가 겹쳐 '${uid}'로 사용합니다: ${dir}`)
272
+ usedIds.add(uid)
273
+ const label = bundled ? uid : `${uid} (로컬)`
274
+ sources.push({ id: uid, dir, label, bundled, entries, provider: makeDirProvider({ id: uid, label, dir, entries }) })
275
+ }
276
+
277
+ for (const name of subdirs(bundleDir)) {
278
+ // 캐시에 직접 넣은 사내 소스는 등록 프로바이더가 없다 — 기본 카테고리를 달리 준다.
279
+ const defaultCategory = reserved.has(sanitizeId(name)) ? BUNDLE_CATEGORY : LOCAL_CATEGORY
280
+ add({ id: name, dir: join(bundleDir, name), bundled: true, defaultCategory })
281
+ }
282
+ for (const spec of extraDirs) {
283
+ const { id, dir } = parseDirSpec(spec)
284
+ if (!existsSync(dir)) {
285
+ if (log) log(` design.md 경로를 찾을 수 없습니다: ${dir}`)
286
+ continue
287
+ }
288
+ add({ id, dir, bundled: false, defaultCategory: LOCAL_CATEGORY })
289
+ }
290
+ return sources
291
+ }
package/lib/engine.mjs ADDED
@@ -0,0 +1,44 @@
1
+ import { makeExec } from './catalog.mjs'
2
+
3
+ export async function scan(root, items) {
4
+ const states = []
5
+ for (const item of items) {
6
+ try {
7
+ const r = await item.detect({ root })
8
+ states.push({ item, status: r.status, detail: r.detail })
9
+ } catch (err) {
10
+ states.push({ item, status: 'absent', detail: `감지 실패: ${err.message}` })
11
+ }
12
+ }
13
+ return states
14
+ }
15
+
16
+ export function planChanges(states, selectedIds) {
17
+ const changes = []
18
+ for (const { item, status } of states) {
19
+ const selected = selectedIds.has(item.id)
20
+ if (selected && status === 'absent') changes.push({ item, action: 'install' })
21
+ else if (selected && status === 'partial') changes.push({ item, action: 'complete' })
22
+ else if (!selected && status !== 'absent') changes.push({ item, action: 'uninstall' })
23
+ }
24
+ return changes
25
+ }
26
+
27
+ export async function apply(root, changes, { dryRun = false, log = console.log } = {}) {
28
+ const exec = makeExec(dryRun, log)
29
+ const results = []
30
+ for (const { item, action } of changes) {
31
+ // log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
32
+ // 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
33
+ // 무엇이 바뀔지는 알 수 없는 상태였다.
34
+ const ctx = { root, dryRun, exec, log }
35
+ try {
36
+ const fn = action === 'uninstall' ? item.uninstall : item.install
37
+ const r = await fn.call(item, ctx)
38
+ results.push({ item, action, ok: true, message: r?.message })
39
+ } catch (err) {
40
+ results.push({ item, action, ok: false, message: err.message })
41
+ }
42
+ }
43
+ return results
44
+ }
@@ -0,0 +1,18 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
2
+ import { repoPathStrict } from './context.mjs'
3
+
4
+ export function ensureGitignoreEntries(root, entries) {
5
+ // 함수의 목적 자체가 쓰기이므로 맨 앞에서 한 번 엄격 검사한다 —
6
+ // bootstrap의 ensureIgnore와 같은 규칙. 어휘적 검사만으로는 .gitignore가
7
+ // 저장소 밖을 가리키는 링크일 때를 막지 못한다.
8
+ const file = repoPathStrict(root, '.gitignore')
9
+ const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
10
+ const lines = new Set(text.split(/\r?\n/))
11
+ const missing = entries.filter((e) => !lines.has(e))
12
+ if (missing.length === 0) return
13
+ // 파일의 우세 줄바꿈을 따른다 — CRLF 파일에 덧붙인 줄만 LF로 섞이지 않게.
14
+ // apply.mjs의 ensureJsonKeys가 JSON에 대해 지키는 규칙과 같다.
15
+ const eol = text.includes('\r\n') ? '\r\n' : '\n'
16
+ const sep = text.length === 0 || text.endsWith('\n') ? '' : eol
17
+ writeFileSync(file, text + sep + missing.join(eol) + eol)
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1785327065269,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ },
8
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
9
+ "last_emitted_at_ms": 1785327034117,
10
+ "message": "Read multiple files in parallel when possible for faster analysis."
11
+ }
12
+ },
13
+ "updated_at": "2026-07-29T12:11:05.269Z"
14
+ }
@@ -0,0 +1,6 @@
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.codebase-memory', label: 'Codebase Memory MCP',
4
+ server: { kind: 'stdio', command: 'codebase-memory-mcp', args: [] },
5
+ note: 'PATH에 codebase-memory-mcp 바이너리 필요. 설치: https://github.com/DeusData/codebase-memory-mcp (install.sh / install.ps1)',
6
+ })
@@ -0,0 +1,6 @@
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.notion', label: 'Notion MCP',
4
+ server: { kind: 'http', url: 'https://mcp.notion.com/mcp' },
5
+ note: '인증: 각 CLI 첫 사용 시 OAuth',
6
+ })
@@ -0,0 +1,6 @@
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.supabase', label: 'Supabase MCP',
4
+ server: { kind: 'http', url: 'https://mcp.supabase.com/mcp' },
5
+ note: '인증: OAuth 동적 등록. 프로젝트 고정이 필요하면 URL에 ?project_ref=<id> 추가',
6
+ })
@@ -0,0 +1,6 @@
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.vercel', label: 'Vercel MCP',
4
+ server: { kind: 'http', url: 'https://mcp.vercel.com' },
5
+ note: '인증: 첫 사용 시 OAuth (승인된 클라이언트만 연결 가능)',
6
+ })
@@ -0,0 +1,7 @@
1
+ import { definePlugin } from '../catalog.mjs'
2
+ export default definePlugin({
3
+ id: 'plugin.bkit', label: 'bkit',
4
+ installId: 'bkit@bkit-marketplace',
5
+ detectIds: ['bkit@bkit-marketplace'],
6
+ marketplace: { name: 'bkit-marketplace', repo: 'popup-studio-ai/bkit-claude-code' },
7
+ })
@@ -0,0 +1,8 @@
1
+ import { definePlugin } from '../catalog.mjs'
2
+ export default definePlugin({
3
+ id: 'plugin.mattpocock-skills', label: 'Matt Pocock skills',
4
+ installId: 'mattpocock-skills@mattpocock',
5
+ detectIds: ['mattpocock-skills@mattpocock'],
6
+ marketplace: { name: 'mattpocock', repo: 'mattpocock/skills' },
7
+ note: '엔지니어링·생산성 스킬 22종 (tdd, code-review, research 등)',
8
+ })
@@ -0,0 +1,7 @@
1
+ import { definePlugin } from '../catalog.mjs'
2
+ export default definePlugin({
3
+ id: 'plugin.superpowers', label: 'superpowers',
4
+ installId: 'superpowers@claude-plugins-official',
5
+ detectIds: ['superpowers@claude-plugins-official', 'superpowers@superpowers-marketplace'],
6
+ note: '공식 마켓플레이스 플러그인',
7
+ })
@@ -0,0 +1,24 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { defineSkill } from '../catalog.mjs'
4
+
5
+ function hasGsdFiles(dir) {
6
+ return existsSync(dir) && readdirSync(dir).some((f) => f.startsWith('gsd-'))
7
+ }
8
+
9
+ export default defineSkill({
10
+ id: 'skill.gsd', label: 'GSD (Get Shit Done)', scope: 'project',
11
+ note: 'npx @opengsd/gsd-core 프로젝트 로컬 설치',
12
+ async detect({ root }) {
13
+ const found = hasGsdFiles(join(root, '.claude', 'commands')) || hasGsdFiles(join(root, '.claude', 'skills'))
14
+ return { status: found ? 'installed' : 'absent' }
15
+ },
16
+ async install({ root, exec }) {
17
+ const r = exec('npx', ['-y', '@opengsd/gsd-core@latest', '--claude', '--local'], { cwd: root })
18
+ if (!r.ok) throw new Error(`GSD 설치 실패: ${r.output}`)
19
+ },
20
+ async uninstall({ root, exec }) {
21
+ const r = exec('npx', ['-y', '@opengsd/gsd-core@latest', '--uninstall'], { cwd: root })
22
+ if (!r.ok) throw new Error(`GSD 제거 실패: ${r.output}`)
23
+ },
24
+ })
@@ -0,0 +1,39 @@
1
+ import { existsSync, rmSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { defineSkill } from '../catalog.mjs'
4
+ import { repoPath, repoPathStrict } from '../context.mjs'
5
+ import { ensureGitignoreEntries } from '../gitignore.mjs'
6
+
7
+ const REL_DIR = '.claude/skills/gstack'
8
+
9
+ export default defineSkill({
10
+ id: 'skill.gstack', label: 'gstack', scope: 'project',
11
+ note: '저장소 로컬 clone + setup (bash 필요, Windows는 Git Bash). 런타임 상태(~/.gstack)는 전역에 생길 수 있음.',
12
+ async detect({ root }) {
13
+ return { status: existsSync(repoPath(root, REL_DIR)) ? 'installed' : 'absent' }
14
+ },
15
+ async install({ root, dryRun, exec }) {
16
+ // clone·삭제가 일어나는 경로다. 어휘적 검사만으로는 .claude/skills가
17
+ // 저장소 밖을 가리키는 링크일 때를 막지 못한다(부트스트랩이 만드는
18
+ // .agents/skills Junction은 저장소 안이라 그대로 통과한다).
19
+ const dir = repoPathStrict(root, REL_DIR)
20
+ if (!existsSync(dir)) {
21
+ const clone = exec('git', ['clone', '--single-branch', '--depth', '1', 'https://github.com/garrytan/gstack.git', dir])
22
+ if (!clone.ok) throw new Error(`gstack clone 실패: ${clone.output}`)
23
+ const setup = exec('bash', ['./setup'], { cwd: dir })
24
+ if (!setup.ok) {
25
+ // setup 실패 잔존물이 detect를 installed로 오판시키지 않도록 정리한다.
26
+ rmSync(dir, { recursive: true, force: true })
27
+ throw new Error(`gstack setup 실패: ${setup.output}`)
28
+ }
29
+ }
30
+ // 부트스트랩 저장소에서는 .claude/skills가 .agents/skills Junction이므로 두 경로 모두 무시 처리
31
+ if (!dryRun) ensureGitignoreEntries(root, ['.claude/skills/gstack', '.agents/skills/gstack'])
32
+ },
33
+ async uninstall({ root, dryRun, exec }) {
34
+ const dir = repoPathStrict(root, REL_DIR)
35
+ if (!existsSync(dir)) return
36
+ exec('bash', [join(dir, 'bin', 'gstack-uninstall'), '--force'], { cwd: dir }) // 실패해도 디렉터리 삭제로 폴백
37
+ if (!dryRun && existsSync(dir)) rmSync(dir, { recursive: true, force: true })
38
+ },
39
+ })
@@ -0,0 +1,40 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import { parse, modify, applyEdits } from 'jsonc-parser'
4
+
5
+ const FORMAT = { formattingOptions: { insertSpaces: true, tabSize: 2 } }
6
+
7
+ function readText(file) {
8
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
9
+ }
10
+
11
+ export function readJson(file) {
12
+ const text = readText(file)
13
+ if (!text.trim()) return undefined
14
+ return parse(text)
15
+ }
16
+
17
+ export function setKey(file, path, value) {
18
+ let text = readText(file)
19
+ if (!text.trim()) text = '{}\n'
20
+ const edits = modify(text, path, value, FORMAT)
21
+ mkdirSync(dirname(file), { recursive: true })
22
+ writeFileSync(file, applyEdits(text, edits))
23
+ }
24
+
25
+ export function removeKey(file, path) {
26
+ const text = readText(file)
27
+ if (!text.trim()) return
28
+ if (getIn(parse(text), path) === undefined) return
29
+ const edits = modify(text, path, undefined, FORMAT)
30
+ writeFileSync(file, applyEdits(text, edits))
31
+ }
32
+
33
+ export function getIn(data, path) {
34
+ let cur = data
35
+ for (const key of path) {
36
+ if (cur == null || typeof cur !== 'object') return undefined
37
+ cur = cur[key]
38
+ }
39
+ return cur
40
+ }
@@ -0,0 +1,60 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import { parse } from 'smol-toml'
4
+
5
+ function readText(file) {
6
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
7
+ }
8
+
9
+ function escapeRegExp(text) {
10
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
11
+ }
12
+
13
+ export function hasSection(file, name) {
14
+ const text = readText(file)
15
+ if (!text.trim()) return false
16
+ try {
17
+ const data = parse(text)
18
+ return data.mcp_servers != null && Object.hasOwn(data.mcp_servers, name)
19
+ } catch {
20
+ return false
21
+ }
22
+ }
23
+
24
+ export function appendSection(file, name, lines) {
25
+ let text = readText(file)
26
+ if (text.length > 0 && !text.endsWith('\n')) text += '\n'
27
+ const separator = text.length > 0 ? '\n' : ''
28
+ text += `${separator}[mcp_servers.${name}]\n${lines.join('\n')}\n`
29
+ mkdirSync(dirname(file), { recursive: true })
30
+ writeFileSync(file, text)
31
+ }
32
+
33
+ export function removeSection(file, name) {
34
+ const text = readText(file)
35
+ if (!text) return
36
+ const lines = text.split('\n')
37
+ const headerRe = new RegExp(`^\\s*\\[mcp_servers\\.${escapeRegExp(name)}(\\.|\\])`)
38
+ const anyHeaderRe = /^\s*\[/
39
+ const out = []
40
+ let skipping = false
41
+ let removedAt = -1
42
+ for (const line of lines) {
43
+ if (skipping && anyHeaderRe.test(line) && !headerRe.test(line)) skipping = false
44
+ if (headerRe.test(line)) {
45
+ skipping = true
46
+ if (removedAt === -1) removedAt = out.length
47
+ }
48
+ if (!skipping) out.push(line)
49
+ }
50
+ // 제거 지점의 경계에서만 빈 줄 하나를 정리해, 다른 위치의 빈 줄들은 그대로 보존한다.
51
+ if (
52
+ removedAt > 0 &&
53
+ removedAt < out.length &&
54
+ out[removedAt - 1].trim() === '' &&
55
+ (out[removedAt] ?? '').trim() === ''
56
+ ) {
57
+ out.splice(removedAt - 1, 1)
58
+ }
59
+ writeFileSync(file, out.join('\n'))
60
+ }