@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/install.mjs +83 -0
- package/lib/args.mjs +266 -0
- package/lib/bootstrap/adapter.mjs +95 -0
- package/lib/bootstrap/apply.mjs +269 -0
- package/lib/bootstrap/flow.mjs +54 -0
- package/lib/bootstrap/manifest.mjs +74 -0
- package/lib/bootstrap/templates.mjs +148 -0
- package/lib/catalog.mjs +145 -0
- package/lib/claude-plugins.mjs +64 -0
- package/lib/clis.mjs +102 -0
- package/lib/context.mjs +65 -0
- package/lib/deps.mjs +19 -0
- package/lib/design-md/cache/awesome-design-md/airbnb/DESIGN.md +545 -0
- package/lib/design-md/cache/awesome-design-md/airtable/DESIGN.md +554 -0
- package/lib/design-md/cache/awesome-design-md/apple/DESIGN.md +562 -0
- package/lib/design-md/cache/awesome-design-md/binance/DESIGN.md +634 -0
- package/lib/design-md/cache/awesome-design-md/bmw/DESIGN.md +544 -0
- package/lib/design-md/cache/awesome-design-md/bmw-m/DESIGN.md +503 -0
- package/lib/design-md/cache/awesome-design-md/bugatti/DESIGN.md +454 -0
- package/lib/design-md/cache/awesome-design-md/cal/DESIGN.md +542 -0
- package/lib/design-md/cache/awesome-design-md/claude/DESIGN.md +589 -0
- package/lib/design-md/cache/awesome-design-md/clay/DESIGN.md +541 -0
- package/lib/design-md/cache/awesome-design-md/clickhouse/DESIGN.md +544 -0
- package/lib/design-md/cache/awesome-design-md/cohere/DESIGN.md +451 -0
- package/lib/design-md/cache/awesome-design-md/coinbase/DESIGN.md +570 -0
- package/lib/design-md/cache/awesome-design-md/composio/DESIGN.md +506 -0
- package/lib/design-md/cache/awesome-design-md/cursor/DESIGN.md +537 -0
- package/lib/design-md/cache/awesome-design-md/dell-1996/DESIGN.md +632 -0
- package/lib/design-md/cache/awesome-design-md/elevenlabs/DESIGN.md +504 -0
- package/lib/design-md/cache/awesome-design-md/expo/DESIGN.md +526 -0
- package/lib/design-md/cache/awesome-design-md/ferrari/DESIGN.md +531 -0
- package/lib/design-md/cache/awesome-design-md/figma/DESIGN.md +578 -0
- package/lib/design-md/cache/awesome-design-md/framer/DESIGN.md +544 -0
- package/lib/design-md/cache/awesome-design-md/hashicorp/DESIGN.md +575 -0
- package/lib/design-md/cache/awesome-design-md/hp/DESIGN.md +670 -0
- package/lib/design-md/cache/awesome-design-md/ibm/DESIGN.md +550 -0
- package/lib/design-md/cache/awesome-design-md/intercom/DESIGN.md +546 -0
- package/lib/design-md/cache/awesome-design-md/kraken/DESIGN.md +125 -0
- package/lib/design-md/cache/awesome-design-md/lamborghini/DESIGN.md +288 -0
- package/lib/design-md/cache/awesome-design-md/linear.app/DESIGN.md +548 -0
- package/lib/design-md/cache/awesome-design-md/lovable/DESIGN.md +298 -0
- package/lib/design-md/cache/awesome-design-md/mastercard/DESIGN.md +365 -0
- package/lib/design-md/cache/awesome-design-md/meta/DESIGN.md +683 -0
- package/lib/design-md/cache/awesome-design-md/minimax/DESIGN.md +746 -0
- package/lib/design-md/cache/awesome-design-md/mintlify/DESIGN.md +852 -0
- package/lib/design-md/cache/awesome-design-md/miro/DESIGN.md +825 -0
- package/lib/design-md/cache/awesome-design-md/mistral.ai/DESIGN.md +773 -0
- package/lib/design-md/cache/awesome-design-md/mongodb/DESIGN.md +767 -0
- package/lib/design-md/cache/awesome-design-md/nike/DESIGN.md +575 -0
- package/lib/design-md/cache/awesome-design-md/nintendo-2001/DESIGN.md +649 -0
- package/lib/design-md/cache/awesome-design-md/notion/DESIGN.md +821 -0
- package/lib/design-md/cache/awesome-design-md/nvidia/DESIGN.md +640 -0
- package/lib/design-md/cache/awesome-design-md/ollama/DESIGN.md +539 -0
- package/lib/design-md/cache/awesome-design-md/opencode.ai/DESIGN.md +521 -0
- package/lib/design-md/cache/awesome-design-md/pinterest/DESIGN.md +597 -0
- package/lib/design-md/cache/awesome-design-md/playstation/DESIGN.md +661 -0
- package/lib/design-md/cache/awesome-design-md/posthog/DESIGN.md +690 -0
- package/lib/design-md/cache/awesome-design-md/raycast/DESIGN.md +669 -0
- package/lib/design-md/cache/awesome-design-md/renault/DESIGN.md +589 -0
- package/lib/design-md/cache/awesome-design-md/replicate/DESIGN.md +616 -0
- package/lib/design-md/cache/awesome-design-md/resend/DESIGN.md +585 -0
- package/lib/design-md/cache/awesome-design-md/revolut/DESIGN.md +636 -0
- package/lib/design-md/cache/awesome-design-md/runwayml/DESIGN.md +244 -0
- package/lib/design-md/cache/awesome-design-md/sanity/DESIGN.md +357 -0
- package/lib/design-md/cache/awesome-design-md/sentry/DESIGN.md +551 -0
- package/lib/design-md/cache/awesome-design-md/shopify/DESIGN.md +516 -0
- package/lib/design-md/cache/awesome-design-md/slack/DESIGN.md +482 -0
- package/lib/design-md/cache/awesome-design-md/spacex/DESIGN.md +363 -0
- package/lib/design-md/cache/awesome-design-md/spotify/DESIGN.md +246 -0
- package/lib/design-md/cache/awesome-design-md/starbucks/DESIGN.md +580 -0
- package/lib/design-md/cache/awesome-design-md/stripe/DESIGN.md +487 -0
- package/lib/design-md/cache/awesome-design-md/supabase/DESIGN.md +462 -0
- package/lib/design-md/cache/awesome-design-md/superhuman/DESIGN.md +448 -0
- package/lib/design-md/cache/awesome-design-md/tesla/DESIGN.md +286 -0
- package/lib/design-md/cache/awesome-design-md/theverge/DESIGN.md +339 -0
- package/lib/design-md/cache/awesome-design-md/together.ai/DESIGN.md +633 -0
- package/lib/design-md/cache/awesome-design-md/uber/DESIGN.md +636 -0
- package/lib/design-md/cache/awesome-design-md/vercel/DESIGN.md +736 -0
- package/lib/design-md/cache/awesome-design-md/vodafone/DESIGN.md +538 -0
- package/lib/design-md/cache/awesome-design-md/voltagent/DESIGN.md +521 -0
- package/lib/design-md/cache/awesome-design-md/warp/DESIGN.md +526 -0
- package/lib/design-md/cache/awesome-design-md/webflow/DESIGN.md +588 -0
- package/lib/design-md/cache/awesome-design-md/wired/DESIGN.md +497 -0
- package/lib/design-md/cache/awesome-design-md/wise/DESIGN.md +544 -0
- package/lib/design-md/cache/awesome-design-md/x.ai/DESIGN.md +465 -0
- package/lib/design-md/cache/awesome-design-md/zapier/DESIGN.md +537 -0
- package/lib/design-md/cache/feelanet/faux/design.md +288 -0
- package/lib/design-md/cache/feelanet/faux-teams/design.md +382 -0
- package/lib/design-md/catalog.json +454 -0
- package/lib/design-md/catalog.mjs +268 -0
- package/lib/design-md/flow.mjs +166 -0
- package/lib/design-md/open.mjs +50 -0
- package/lib/design-md/providers/awesome-design-md.mjs +114 -0
- package/lib/design-md/providers/index.mjs +9 -0
- package/lib/design-md/scan.mjs +291 -0
- package/lib/engine.mjs +44 -0
- package/lib/gitignore.mjs +18 -0
- package/lib/items/.omc/state/sessions/a859aff7-59d9-4ff6-afc6-8cafe4471dc4/pre-tool-advisory-throttle.json +14 -0
- package/lib/items/mcp.codebase-memory.mjs +6 -0
- package/lib/items/mcp.notion.mjs +6 -0
- package/lib/items/mcp.supabase.mjs +6 -0
- package/lib/items/mcp.vercel.mjs +6 -0
- package/lib/items/plugin.bkit.mjs +7 -0
- package/lib/items/plugin.mattpocock-skills.mjs +8 -0
- package/lib/items/plugin.superpowers.mjs +7 -0
- package/lib/items/skill.gsd.mjs +24 -0
- package/lib/items/skill.gstack.mjs +39 -0
- package/lib/jsonfile.mjs +40 -0
- package/lib/tomlfile.mjs +60 -0
- package/lib/tui/render.mjs +192 -0
- package/lib/tui/rows.mjs +206 -0
- package/lib/tui/run.mjs +257 -0
- package/lib/tui/state.mjs +188 -0
- 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,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
|
+
})
|
package/lib/jsonfile.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/tomlfile.mjs
ADDED
|
@@ -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
|
+
}
|