@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,269 @@
|
|
|
1
|
+
// 매니페스트 선언을 실제 파일시스템 변경으로 옮기는 실행기.
|
|
2
|
+
// 추가 전용 — 여기에는 삭제 경로가 없다.
|
|
3
|
+
import { lstatSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
6
|
+
import { ensureGitignoreEntries } from '../gitignore.mjs'
|
|
7
|
+
|
|
8
|
+
// existsSync는 깨진 심볼릭 링크에 false를 반환한다. 그대로 쓰면 사용자가 만든
|
|
9
|
+
// 링크를 덮어쓰게 되므로 lstat으로 "항목이 있는가"를 본다.
|
|
10
|
+
export function pathExists(target) {
|
|
11
|
+
try {
|
|
12
|
+
lstatSync(target)
|
|
13
|
+
return true
|
|
14
|
+
} catch {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 두 OS가 같은 파일을 만들도록 항상 LF + 끝 개행 1개로 정규화한다.
|
|
20
|
+
// writeText(새 파일)와 ensureBlocks의 덧붙이기(기존 파일) 양쪽에서 쓴다 —
|
|
21
|
+
// 덧붙이기도 이 정규화를 거치지 않으면 CRLF가 LF 파일에 새어 들어간다.
|
|
22
|
+
function normalizeBody(text) {
|
|
23
|
+
return text.replace(/\r\n/g, '\n').trim() + '\n'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeText(file, text) {
|
|
27
|
+
const body = normalizeBody(text)
|
|
28
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
29
|
+
writeFileSync(file, body, { encoding: 'utf8' })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ensureDirs(root, dirs, { dryRun = false, log }) {
|
|
33
|
+
return dirs.map((rel) => {
|
|
34
|
+
// 존재 확인은 어휘적 경로로 한다 — 만들지 않을 것이면 지켜야 할 쓰기도 없다.
|
|
35
|
+
if (pathExists(repoPath(root, rel))) return { ok: true, action: 'skip', path: rel }
|
|
36
|
+
// 실제로 만드는 경로만 엄격 검사한다(링크를 통한 저장소 이탈 차단).
|
|
37
|
+
// 검사가 던지면 하지 않은 동작을 보고하지 않도록 로그보다 먼저 수행한다.
|
|
38
|
+
const target = repoPathStrict(root, rel)
|
|
39
|
+
log(`디렉터리 생성: ${rel}`)
|
|
40
|
+
if (!dryRun) mkdirSync(target, { recursive: true })
|
|
41
|
+
return { ok: true, action: 'create', path: rel }
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ensureFiles(root, files, { dryRun = false, log }) {
|
|
46
|
+
return files.map(({ path: rel, template }) => {
|
|
47
|
+
// 깨진 링크도 lstat으로는 존재한다 — 내용을 보지 않고 그대로 보존한다.
|
|
48
|
+
if (pathExists(repoPath(root, rel))) {
|
|
49
|
+
log(`기존 파일 유지: ${rel}`)
|
|
50
|
+
return { ok: true, action: 'keep', path: rel }
|
|
51
|
+
}
|
|
52
|
+
// 검사가 던지면 하지 않은 동작을 보고하지 않도록 로그보다 먼저 수행한다.
|
|
53
|
+
const target = repoPathStrict(root, rel)
|
|
54
|
+
log(`파일 생성: ${rel}`)
|
|
55
|
+
if (!dryRun) writeText(target, template)
|
|
56
|
+
return { ok: true, action: 'create', path: rel }
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BEGIN_MARKER = '<!-- agent-kit:begin -->'
|
|
61
|
+
|
|
62
|
+
export function ensureBlocks(root, blocks, { dryRun = false, log }) {
|
|
63
|
+
return blocks.map(({ path: rel, block }) => {
|
|
64
|
+
// 존재 확인은 어휘적 경로로 한다 — 만들지 않을 것이면 지켜야 할 쓰기도 없다.
|
|
65
|
+
if (!pathExists(repoPath(root, rel))) {
|
|
66
|
+
// 실제로 만드는 경로만 엄격 검사한다(링크를 통한 저장소 이탈 차단).
|
|
67
|
+
// 검사가 던지면 하지 않은 동작을 보고하지 않도록 로그보다 먼저 수행한다.
|
|
68
|
+
const target = repoPathStrict(root, rel)
|
|
69
|
+
log(`파일 생성: ${rel}`)
|
|
70
|
+
if (!dryRun) writeText(target, block)
|
|
71
|
+
return { ok: true, action: 'create', path: rel }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const target = repoPath(root, rel)
|
|
75
|
+
let text
|
|
76
|
+
try {
|
|
77
|
+
text = readFileSync(target, 'utf8')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
log(`경고: ${rel}을 읽을 수 없어 건너뜁니다 (${err.code ?? err.message})`)
|
|
80
|
+
return { ok: true, action: 'warn', path: rel, message: '읽기 실패' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (text.includes(BEGIN_MARKER)) {
|
|
84
|
+
log(`관리 블록 확인: ${rel}`)
|
|
85
|
+
return { ok: true, action: 'skip', path: rel }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 생성 분기와 같은 형태로: 검사는 dry-run 여부와 무관하게 항상 하고,
|
|
89
|
+
// 실제 쓰기만 !dryRun으로 막는다. dry-run 안에 검사를 가두면 저장소 밖으로
|
|
90
|
+
// 이탈하는 기존 파일에 대해 오류 없이 append 예정이라고 보고하게 된다.
|
|
91
|
+
// 검사가 던지면 하지 않은 동작을 보고하지 않도록 로그보다 먼저 수행한다.
|
|
92
|
+
const strictTarget = repoPathStrict(root, rel)
|
|
93
|
+
log(`관리 블록 추가: ${rel}`)
|
|
94
|
+
if (!dryRun) {
|
|
95
|
+
// 기존 마지막 줄을 닫고 빈 줄 하나를 띄운 뒤 블록을 붙인다.
|
|
96
|
+
const separator = text.endsWith('\n') ? '\n' : '\n\n'
|
|
97
|
+
appendFileSync(strictTarget, separator + normalizeBody(block), { encoding: 'utf8' })
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, action: 'append', path: rel }
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 원본 setup-agents.ps1의 Add-GitignoreEntries·setup-agents.sh의
|
|
104
|
+
// ensure_gitignore_entries가 항목을 추가할 때 앞에 넣는 안내 주석.
|
|
105
|
+
// 헤더가 이미 있으면 다시 넣지 않는다 — 원본과 글자 단위로 같아야 한다.
|
|
106
|
+
const IGNORE_HEADER = '# agent-kit: local skill adapters (do not commit duplicated skills)'
|
|
107
|
+
|
|
108
|
+
// 부정 항목(`!path`)의 부모 디렉터리 전체를 제외하는 패턴이 이미 있으면
|
|
109
|
+
// git 규칙상 그 부정 항목은 조용히 무효가 된다(F-3) — 부모가 제외되면
|
|
110
|
+
// git이 그 안을 들여다보지 않아 자식을 되살릴 수 없다. `.vscode/*`처럼
|
|
111
|
+
// 내용물만 제외하는 형태는 무해하다. 손으로 쓴 `.gitignore`·Node/Python
|
|
112
|
+
// 템플릿에서 흔한 두 형태(`.vscode`, `.vscode/`)만 검사한다 — gitignore
|
|
113
|
+
// 전체 문법을 구현하지 않고 실제로 부딪히는 모양만 잡는다. 위치는 무관하다:
|
|
114
|
+
// git은 부정 항목의 순서와 무관하게 부모 제외를 우선한다.
|
|
115
|
+
function warnBlockedNegations(lines, entries, log) {
|
|
116
|
+
const warnings = []
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (!entry.startsWith('!')) continue
|
|
119
|
+
const negated = entry.slice(1)
|
|
120
|
+
const parent = negated.split('/').slice(0, -1).join('/')
|
|
121
|
+
if (parent && (lines.has(parent) || lines.has(`${parent}/`))) {
|
|
122
|
+
const message = `.gitignore의 "${parent}"가 디렉터리 전체를 제외해 "${entry}" 부정 항목이 무효화됩니다`
|
|
123
|
+
log(`경고: ${message}. "${parent}/*" 형태로 바꾸면 다시 포함됩니다.`)
|
|
124
|
+
warnings.push({ ok: true, action: 'warn', path: entry, message })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return warnings
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function ensureIgnore(root, entries, { dryRun = false, log }) {
|
|
131
|
+
// ensureDirs/ensureFiles/ensureBlocks와 달리 경로가 '.gitignore' 하나로 고정돼
|
|
132
|
+
// 있고, 함수 자체의 목적이 "항목을 보장한다"는 쓰기 의도이므로 존재 확인과
|
|
133
|
+
// 쓰기 판단을 나누지 않고 맨 앞에서 한 번만 엄격 검사한다. ensureGitignoreEntries
|
|
134
|
+
// (lib/gitignore.mjs)도 같은 검사를 하지만, 그쪽을 부르지 않는 dry-run 경로에서도
|
|
135
|
+
// 이탈을 거부해야 하므로 여기서 먼저 확인한다.
|
|
136
|
+
const target = repoPathStrict(root, '.gitignore')
|
|
137
|
+
const text = pathExists(target) ? readFileSync(target, 'utf8') : ''
|
|
138
|
+
const lines = new Set(text.split(/\r?\n/))
|
|
139
|
+
const missing = entries.filter((e) => !lines.has(e))
|
|
140
|
+
// 이미 있는 항목이든 새로 추가하는 항목이든, 기존 .gitignore가 부모
|
|
141
|
+
// 디렉터리를 통째로 제외하고 있으면 매 실행마다 경고한다 — 사용자가
|
|
142
|
+
// 직접 고치기 전까지는 계속 알려야 "조용한 실패"가 아니게 된다.
|
|
143
|
+
const warnings = warnBlockedNegations(lines, entries, log)
|
|
144
|
+
|
|
145
|
+
if (missing.length === 0) {
|
|
146
|
+
log(`.gitignore 항목 확인: ${entries.join(', ')}`)
|
|
147
|
+
return [...entries.map((e) => ({ ok: true, action: 'skip', path: e })), ...warnings]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
log(`.gitignore 항목 추가: ${missing.join(', ')}`)
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
// 헤더가 아직 없을 때만 항목들보다 먼저 넣는다. ensureGitignoreEntries는
|
|
153
|
+
// 넘긴 항목 중 파일에 없는 줄만 추가하므로, 헤더도 그 목록의 맨 앞에
|
|
154
|
+
// 끼워 넣으면 "없을 때만 추가, 있으면 그대로"가 자연히 성립한다.
|
|
155
|
+
const toAdd = lines.has(IGNORE_HEADER) ? missing : [IGNORE_HEADER, ...missing]
|
|
156
|
+
ensureGitignoreEntries(root, toAdd)
|
|
157
|
+
}
|
|
158
|
+
return [...missing.map((e) => ({ ok: true, action: 'append', path: e })), ...warnings]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 줄 주석(//)과 블록 주석(/* */)을 건너뛰며 유의미한 문자를 하나씩 넘겨준다.
|
|
162
|
+
// findRootBrace(루트 '{' 탐색)와 isEmptyObjectBody(빈 객체 판정, F-1)가 같은
|
|
163
|
+
// 스캐너를 공유한다 — 판정 기준이 둘로 갈리면 한쪽만 고치는 회귀가 생기기 쉽다.
|
|
164
|
+
// 한계: 문자열 리터럴을 모른다. `"https://x"` 안의 `//`도 줄 주석으로 본다.
|
|
165
|
+
// 현재 두 소비자는 루트 '{' 이전과 그 바로 뒤 한 글자만 보므로 문자열 안까지
|
|
166
|
+
// 들어가지 않아 영향이 없다. 다른 위치에서 재사용하려면 문자열 인식을 먼저 더해야 한다.
|
|
167
|
+
function* scanSignificant(text) {
|
|
168
|
+
let inBlock = false
|
|
169
|
+
let offset = 0
|
|
170
|
+
|
|
171
|
+
for (const line of text.split('\n')) {
|
|
172
|
+
let i = 0
|
|
173
|
+
while (i < line.length) {
|
|
174
|
+
if (inBlock) {
|
|
175
|
+
const end = line.indexOf('*/', i)
|
|
176
|
+
if (end === -1) { i = line.length; break }
|
|
177
|
+
inBlock = false
|
|
178
|
+
i = end + 2
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (line[i] === '/' && line[i + 1] === '/') break
|
|
182
|
+
if (line[i] === '/' && line[i + 1] === '*') { inBlock = true; i += 2; continue }
|
|
183
|
+
yield { char: line[i], offset: offset + i }
|
|
184
|
+
i++
|
|
185
|
+
}
|
|
186
|
+
offset += line.length + 1 // split이 제거한 개행 1자
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// JSONC인 .vscode/settings.json은 파일 첫머리에 주석이 오는 일이 흔하고,
|
|
191
|
+
// 그 주석 안의 중괄호에 속으면 주석 한가운데에 키를 끼워 넣게 된다.
|
|
192
|
+
function findRootBrace(text) {
|
|
193
|
+
for (const { char, offset } of scanSignificant(text)) {
|
|
194
|
+
if (char === '{') return offset
|
|
195
|
+
}
|
|
196
|
+
return -1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// rest(루트 '{' 다음 텍스트)에서 공백을 건너뛴 첫 유의미 문자가 '}'인지 본다.
|
|
200
|
+
// trimStart만으로는 `{ // 안내\n}` 처럼 첫 항목이 주석인 빈 객체를 항목 있는
|
|
201
|
+
// 객체로 오판해 후행 콤마를 남긴다(F-1) — 주석도 whitespace와 함께 건너뛰어야
|
|
202
|
+
// 진짜 "다음 항목이 있는가"를 알 수 있다.
|
|
203
|
+
function isEmptyObjectBody(rest) {
|
|
204
|
+
for (const { char } of scanSignificant(rest)) {
|
|
205
|
+
if (/\s/.test(char)) continue
|
|
206
|
+
return char === '}'
|
|
207
|
+
}
|
|
208
|
+
return true // 닫는 중괄호를 못 찾은 경우(도달 불가에 가깝다) — 방어적으로 empty 취급
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 기존 JSON 파일을 보존한 채 최상위 키 하나를 보장한다.
|
|
212
|
+
// 부트스트랩은 외부 의존성을 쓸 수 없어(jsonc-parser는 설치기 경로 전용)
|
|
213
|
+
// 파싱·재직렬화 대신 텍스트 삽입으로 처리한다. 재직렬화하지 않으므로
|
|
214
|
+
// 사용자의 주석·들여쓰기·줄바꿈이 그대로 남는다.
|
|
215
|
+
export function ensureJsonKeys(root, entries, { dryRun = false, log }) {
|
|
216
|
+
return entries.map(({ path: rel, key, value }) => {
|
|
217
|
+
const pair = `${JSON.stringify(key)}: ${JSON.stringify(value)}`
|
|
218
|
+
|
|
219
|
+
// 존재 확인은 어휘적 경로로 한다 — 만들지 않을 것이면 지켜야 할 쓰기도 없다.
|
|
220
|
+
if (!pathExists(repoPath(root, rel))) {
|
|
221
|
+
// 검사가 던지면 하지 않은 동작을 보고하지 않도록 로그보다 먼저 수행한다.
|
|
222
|
+
const target = repoPathStrict(root, rel)
|
|
223
|
+
log(`파일 생성: ${rel}`)
|
|
224
|
+
if (!dryRun) writeText(target, `{\n ${pair}\n}`)
|
|
225
|
+
return { ok: true, action: 'create', path: rel }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let text
|
|
229
|
+
try {
|
|
230
|
+
text = readFileSync(repoPath(root, rel), 'utf8')
|
|
231
|
+
} catch (err) {
|
|
232
|
+
log(`경고: ${rel}을 읽을 수 없어 건너뜁니다 (${err.code ?? err.message})`)
|
|
233
|
+
return { ok: true, action: 'warn', path: rel, message: '읽기 실패' }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 주석 안에 있어도 건드리지 않는다 — 사용자가 언급한 키를 스크립트가
|
|
237
|
+
// 되살리지 않는 편이 "기존 설정을 덮어쓰지 않는다"는 원칙에 맞는다.
|
|
238
|
+
// 문자열 포함 검사라 키 이름이 어떤 값 안에 들어 있어도 건너뛴다. 보수적인
|
|
239
|
+
// 쪽으로 틀리는 것(넣지 않음)이라 파일을 망가뜨리지는 않는다. 정확히 하려면
|
|
240
|
+
// 최상위 키만 보는 파서가 필요한데, 부트스트랩은 의존성을 쓸 수 없다.
|
|
241
|
+
if (text.includes(JSON.stringify(key))) {
|
|
242
|
+
log(`설정 키 확인: ${rel} — ${key}`)
|
|
243
|
+
return { ok: true, action: 'skip', path: rel }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const brace = findRootBrace(text)
|
|
247
|
+
if (brace === -1) {
|
|
248
|
+
log(`경고: ${rel}에서 루트 객체를 찾지 못해 건너뜁니다`)
|
|
249
|
+
return { ok: true, action: 'warn', path: rel, message: '루트 객체 없음' }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const strictTarget = repoPathStrict(root, rel)
|
|
253
|
+
log(`설정 키 추가: ${rel} — ${key}`)
|
|
254
|
+
if (!dryRun) {
|
|
255
|
+
const rest = text.slice(brace + 1)
|
|
256
|
+
// 빈 객체면 콤마 없이, 뒤에 항목이 있으면 콤마를 붙여 유효한 JSON을 유지한다.
|
|
257
|
+
const empty = isEmptyObjectBody(rest)
|
|
258
|
+
// 파일의 우세 줄바꿈을 따른다 — CRLF 파일에 삽입한 줄만 LF로 섞이는 것을 막는다.
|
|
259
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
260
|
+
// 중괄호 사이가 공백뿐이면 그 공백을 우리 줄로 대체한다. 그대로 두면
|
|
261
|
+
// `{\n}` 같은 파일에 빈 줄이 하나 남는다. 주석이 들어 있는 빈 객체는
|
|
262
|
+
// 건드리지 않는다 — 주석의 들여쓰기를 지켜야 한다.
|
|
263
|
+
const body = empty && /^\s*\}/.test(rest) ? rest.replace(/^\s*/, '') : rest
|
|
264
|
+
const inserted = `${eol} ${pair}${empty ? eol : ','}`
|
|
265
|
+
writeFileSync(strictTarget, text.slice(0, brace + 1) + inserted + body, { encoding: 'utf8' })
|
|
266
|
+
}
|
|
267
|
+
return { ok: true, action: 'insert', path: rel }
|
|
268
|
+
})
|
|
269
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// 부트스트랩 진입점 — 순서와 보고만 담당한다.
|
|
2
|
+
// 무엇을 만들지는 manifest.mjs가, 어떻게 만들지는 apply.mjs·adapter.mjs가 안다.
|
|
3
|
+
import { MANIFEST } from './manifest.mjs'
|
|
4
|
+
import { ensureBlocks, ensureDirs, ensureFiles, ensureIgnore, ensureJsonKeys } from './apply.mjs'
|
|
5
|
+
import { configureAdapter } from './adapter.mjs'
|
|
6
|
+
|
|
7
|
+
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
8
|
+
|
|
9
|
+
export function runBootstrap(root, opts = {}) {
|
|
10
|
+
const { dryRun = false, skillMode = 'auto', log = console.log, manifest = MANIFEST } = opts
|
|
11
|
+
|
|
12
|
+
if (!SKILL_MODES.includes(skillMode)) {
|
|
13
|
+
throw new Error(`--skill-mode는 ${SKILL_MODES.join(', ')} 중 하나여야 합니다: ${skillMode}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const say = (message) => log(`[agent-setup] ${message}`)
|
|
17
|
+
const ctx = { dryRun, log: say }
|
|
18
|
+
|
|
19
|
+
say(`저장소 루트: ${root}`)
|
|
20
|
+
say('글로벌 설정 경로는 읽거나 수정하지 않습니다.')
|
|
21
|
+
|
|
22
|
+
const results = [
|
|
23
|
+
...ensureDirs(root, manifest.dirs, ctx),
|
|
24
|
+
...ensureFiles(root, manifest.files, ctx),
|
|
25
|
+
...ensureJsonKeys(root, manifest.settings ?? [], ctx),
|
|
26
|
+
...ensureBlocks(root, manifest.blocks, ctx),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// 어댑터는 항목별로 실패를 격리한다 — 하나가 실패해도 나머지를 계속한다.
|
|
30
|
+
for (const entry of manifest.adapters) {
|
|
31
|
+
try {
|
|
32
|
+
results.push(configureAdapter(root, entry, { ...ctx, skillMode }))
|
|
33
|
+
} catch (err) {
|
|
34
|
+
results.push({ ok: false, action: 'link', path: entry.path, message: err.message })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
results.push(...ensureIgnore(root, manifest.ignore, ctx))
|
|
39
|
+
|
|
40
|
+
const failed = results.filter((r) => !r.ok)
|
|
41
|
+
log('')
|
|
42
|
+
if (failed.length > 0) {
|
|
43
|
+
say(`실패 ${failed.length}건:`)
|
|
44
|
+
for (const f of failed) say(` ✖ ${f.path} — ${f.message}`)
|
|
45
|
+
}
|
|
46
|
+
say('완료되었습니다.')
|
|
47
|
+
say('공통 지침: AGENTS.md')
|
|
48
|
+
say('공통 스킬: .agents/skills/')
|
|
49
|
+
say(`적용 도구: ${manifest.tools.join(', ')}`)
|
|
50
|
+
say('도구별 설정은 모두 현재 저장소 안에만 생성되었습니다.')
|
|
51
|
+
say('기존 설정 파일은 덮어쓰지 않았습니다.')
|
|
52
|
+
|
|
53
|
+
return { results, failed }
|
|
54
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENTS_TEMPLATE, CLAUDE_BLOCK, GEMINI_BLOCK, SKILL_README, EXAMPLE_SKILL,
|
|
3
|
+
AGENT_KIT_README, CLAUDE_SETTINGS, CODEX_CONFIG, GEMINI_SETTINGS, GROK_CONFIG,
|
|
4
|
+
OPENCODE_CONFIG, KILO_CONFIG, KIRO_MCP_CONFIG, KIMI_MCP_CONFIG,
|
|
5
|
+
COPILOT_MCP_CONFIG, COPILOT_SETTINGS, VSCODE_MCP_CONFIG,
|
|
6
|
+
} from './templates.mjs'
|
|
7
|
+
|
|
8
|
+
// 저장소 부트스트랩이 만들 대상 선언.
|
|
9
|
+
// 도구 추가 = dirs 한 줄 + files 한 줄. 실행 로직은 apply.mjs·adapter.mjs에 있다.
|
|
10
|
+
export const MANIFEST = {
|
|
11
|
+
// 완료 리포트에 그대로 나열되는 도구 이름. 순서가 출력 순서다.
|
|
12
|
+
tools: [
|
|
13
|
+
'Claude Code', 'Codex', 'Gemini CLI', 'OpenCode', 'Kilo Code',
|
|
14
|
+
'Kiro', 'Kimi Code', 'Grok Build', 'Antigravity',
|
|
15
|
+
'GitHub Copilot CLI', 'VS Code Copilot',
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
dirs: [
|
|
19
|
+
'.agents/skills', '.agent-kit', '.claude', '.codex',
|
|
20
|
+
'.gemini', '.grok', '.kiro/settings', '.kimi-code',
|
|
21
|
+
'.github/copilot', '.vscode',
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
// 없을 때만 생성한다. 이미 있으면 내용을 보지 않고 보존한다.
|
|
25
|
+
files: [
|
|
26
|
+
{ path: 'AGENTS.md', template: AGENTS_TEMPLATE },
|
|
27
|
+
{ path: '.agents/skills/README.md', template: SKILL_README },
|
|
28
|
+
{ path: '.agents/skills/repository-check/SKILL.md', template: EXAMPLE_SKILL },
|
|
29
|
+
{ path: '.agent-kit/README.md', template: AGENT_KIT_README },
|
|
30
|
+
{ path: '.claude/settings.json', template: CLAUDE_SETTINGS },
|
|
31
|
+
{ path: '.codex/config.toml', template: CODEX_CONFIG },
|
|
32
|
+
{ path: '.gemini/settings.json', template: GEMINI_SETTINGS },
|
|
33
|
+
{ path: '.grok/config.toml', template: GROK_CONFIG },
|
|
34
|
+
{ path: 'opencode.jsonc', template: OPENCODE_CONFIG },
|
|
35
|
+
{ path: 'kilo.jsonc', template: KILO_CONFIG },
|
|
36
|
+
{ path: '.kiro/settings/mcp.json', template: KIRO_MCP_CONFIG },
|
|
37
|
+
{ path: '.kimi-code/mcp.json', template: KIMI_MCP_CONFIG },
|
|
38
|
+
{ path: '.github/mcp.json', template: COPILOT_MCP_CONFIG },
|
|
39
|
+
{ path: '.github/copilot/settings.json', template: COPILOT_SETTINGS },
|
|
40
|
+
{ path: '.vscode/mcp.json', template: VSCODE_MCP_CONFIG },
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
// 마커가 없을 때만 덧붙인다. 파일이 없으면 블록만으로 생성한다.
|
|
44
|
+
blocks: [
|
|
45
|
+
{ path: 'CLAUDE.md', block: CLAUDE_BLOCK },
|
|
46
|
+
{ path: 'GEMINI.md', block: GEMINI_BLOCK },
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
// 기존 파일을 보존한 채 최상위 키만 보장한다. 키가 이미 있으면 손대지 않는다.
|
|
50
|
+
// VS Code Copilot은 이 키가 있어야 루트 AGENTS.md를 읽는다.
|
|
51
|
+
settings: [
|
|
52
|
+
{ path: '.vscode/settings.json', key: 'chat.useAgentsMdFile', value: true },
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
// .agents/skills 를 가리키는 도구별 어댑터
|
|
56
|
+
// 두 Copilot은 .agents/skills를 네이티브로 탐색하므로 여기에 없다.
|
|
57
|
+
adapters: [
|
|
58
|
+
{ tool: 'Claude Code', path: '.claude/skills' },
|
|
59
|
+
{ tool: 'Kiro', path: '.kiro/skills' },
|
|
60
|
+
{ tool: 'Grok Build', path: '.grok/skills' },
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
// .vscode/ 아래 두 부정 항목은 널리 쓰이는 VisualStudio.gitignore가
|
|
64
|
+
// `.vscode/*`를 무시하기 때문에 필요하다. 부정 항목이 없으면 팀 공유가
|
|
65
|
+
// 깨진다 — 특히 settings.json은 VS Code가 AGENTS.md를 읽게 하는
|
|
66
|
+
// chat.useAgentsMdFile이 사는 곳이라, 커밋되지 않으면 키를 넣어도
|
|
67
|
+
// 그 설정이 팀에 전파되지 않는다. `.vscode/*`가 없는 저장소에서는
|
|
68
|
+
// 무해한 no-op이다.
|
|
69
|
+
ignore: [
|
|
70
|
+
'.claude/skills', '.kiro/skills', '.grok/skills', '.kimi-code/local.toml',
|
|
71
|
+
'.github/copilot/settings.local.json',
|
|
72
|
+
'!.vscode/mcp.json', '!.vscode/settings.json',
|
|
73
|
+
],
|
|
74
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// setup-agents.ps1의 here-string 내용을 옮긴 단일 출처.
|
|
2
|
+
// 여기만 고치면 모든 OS가 같은 파일을 만든다.
|
|
3
|
+
|
|
4
|
+
export const AGENTS_TEMPLATE = `# Repository Instructions
|
|
5
|
+
|
|
6
|
+
## Scope
|
|
7
|
+
|
|
8
|
+
These instructions apply only to this repository.
|
|
9
|
+
|
|
10
|
+
## Required workflow
|
|
11
|
+
|
|
12
|
+
1. Inspect the relevant existing implementation before editing.
|
|
13
|
+
2. Make the smallest change that satisfies the requirement.
|
|
14
|
+
3. Add or update tests for behavior changes.
|
|
15
|
+
4. Run focused verification first, then the full repository checks.
|
|
16
|
+
5. Report anything that could not be verified.
|
|
17
|
+
|
|
18
|
+
## Repository commands
|
|
19
|
+
|
|
20
|
+
Document the actual commands used by this repository:
|
|
21
|
+
|
|
22
|
+
- Install:
|
|
23
|
+
- Build:
|
|
24
|
+
- Test:
|
|
25
|
+
- Lint:
|
|
26
|
+
- Full verification:
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
- Do not modify files outside this repository.
|
|
31
|
+
- Do not add production dependencies without explicit approval.
|
|
32
|
+
- Do not edit generated files directly.
|
|
33
|
+
- Never commit credentials, access tokens, or connection strings.`
|
|
34
|
+
|
|
35
|
+
export const CLAUDE_BLOCK = `<!-- agent-kit:begin -->
|
|
36
|
+
@AGENTS.md
|
|
37
|
+
<!-- agent-kit:end -->`
|
|
38
|
+
|
|
39
|
+
export const GEMINI_BLOCK = `<!-- agent-kit:begin -->
|
|
40
|
+
@./AGENTS.md
|
|
41
|
+
<!-- agent-kit:end -->`
|
|
42
|
+
|
|
43
|
+
export const SKILL_README = `# Shared repository skills
|
|
44
|
+
|
|
45
|
+
Create each shared skill in:
|
|
46
|
+
|
|
47
|
+
\`.agents/skills/<skill-name>/SKILL.md\`
|
|
48
|
+
|
|
49
|
+
Use portable Agent Skills frontmatter:
|
|
50
|
+
|
|
51
|
+
\`\`\`yaml
|
|
52
|
+
---
|
|
53
|
+
name: example-skill
|
|
54
|
+
description: Explain when this skill should be used.
|
|
55
|
+
---
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
Claude Code, Kiro, and Grok Build receive these skills through project-local
|
|
59
|
+
adapters at \`.claude/skills\`, \`.kiro/skills\`, and \`.grok/skills\`. Codex,
|
|
60
|
+
Gemini CLI, OpenCode, Kilo Code, Kimi Code, Antigravity, GitHub Copilot CLI,
|
|
61
|
+
and VS Code Copilot discover \`.agents/skills\` directly.`
|
|
62
|
+
|
|
63
|
+
export const EXAMPLE_SKILL = `---
|
|
64
|
+
name: repository-check
|
|
65
|
+
description: Verify repository changes by reviewing the diff and running the documented build, test, and lint commands. Use before reporting implementation work as complete.
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
# Repository check
|
|
69
|
+
|
|
70
|
+
1. Read \`AGENTS.md\`.
|
|
71
|
+
2. Review the complete working-tree diff.
|
|
72
|
+
3. Run the smallest relevant checks first.
|
|
73
|
+
4. Run the repository's documented full verification command.
|
|
74
|
+
5. Report failures, skipped checks, and remaining risks.`
|
|
75
|
+
|
|
76
|
+
export const AGENT_KIT_README = `# Repository-local agent setup
|
|
77
|
+
|
|
78
|
+
This directory documents files generated or used by the bootstrap scripts.
|
|
79
|
+
|
|
80
|
+
The installer bootstrap:
|
|
81
|
+
|
|
82
|
+
- finds the current Git repository root;
|
|
83
|
+
- refuses to write outside that repository;
|
|
84
|
+
- creates only project-scoped configuration;
|
|
85
|
+
- never writes to user-home or system-wide agent configuration;
|
|
86
|
+
- preserves existing configuration files;
|
|
87
|
+
- adds a small managed import block to \`CLAUDE.md\` and \`GEMINI.md\`;
|
|
88
|
+
- exposes \`.agents/skills\` to Claude Code, Kiro, and Grok Build through
|
|
89
|
+
local adapters;
|
|
90
|
+
- adds \`chat.useAgentsMdFile\` to \`.vscode/settings.json\` only when the key
|
|
91
|
+
is absent, so VS Code Copilot reads the shared \`AGENTS.md\`;
|
|
92
|
+
- relies on the native support of Kilo Code, Kimi Code, Antigravity, GitHub
|
|
93
|
+
Copilot CLI, and VS Code Copilot for \`AGENTS.md\` and \`.agents/skills\`.`
|
|
94
|
+
|
|
95
|
+
export const CLAUDE_SETTINGS = `{}`
|
|
96
|
+
|
|
97
|
+
export const CODEX_CONFIG = `# Repository-local Codex configuration.
|
|
98
|
+
# Personal or machine-wide defaults belong outside this repository.
|
|
99
|
+
project_doc_max_bytes = 65536`
|
|
100
|
+
|
|
101
|
+
export const GEMINI_SETTINGS = `{
|
|
102
|
+
"skills": {
|
|
103
|
+
"enabled": true
|
|
104
|
+
},
|
|
105
|
+
"security": {
|
|
106
|
+
"folderTrust": {
|
|
107
|
+
"enabled": true
|
|
108
|
+
},
|
|
109
|
+
"disableAlwaysAllow": true,
|
|
110
|
+
"enablePermanentToolApproval": false
|
|
111
|
+
}
|
|
112
|
+
}`
|
|
113
|
+
|
|
114
|
+
export const GROK_CONFIG = `# Repository-local Grok Build configuration.
|
|
115
|
+
# Project scope supports mcp_servers, plugins, and permission rules only.
|
|
116
|
+
# Personal or machine-wide defaults belong in ~/.grok/config.toml.
|
|
117
|
+
# Grok Build reads the root AGENTS.md natively.`
|
|
118
|
+
|
|
119
|
+
export const OPENCODE_CONFIG = `{
|
|
120
|
+
"$schema": "https://opencode.ai/config.json"
|
|
121
|
+
}`
|
|
122
|
+
|
|
123
|
+
export const KILO_CONFIG = `{
|
|
124
|
+
// Project-local Kilo Code configuration.
|
|
125
|
+
// Kilo Code automatically loads ./AGENTS.md and ./.agents/skills/.
|
|
126
|
+
}`
|
|
127
|
+
|
|
128
|
+
export const KIRO_MCP_CONFIG = `{
|
|
129
|
+
"mcpServers": {}
|
|
130
|
+
}`
|
|
131
|
+
|
|
132
|
+
export const KIMI_MCP_CONFIG = `{
|
|
133
|
+
"mcpServers": {}
|
|
134
|
+
}`
|
|
135
|
+
|
|
136
|
+
export const COPILOT_MCP_CONFIG = `{
|
|
137
|
+
"mcpServers": {}
|
|
138
|
+
}`
|
|
139
|
+
|
|
140
|
+
// 모델·추론 강도·컨텍스트 티어를 저장소 단위로 고정하는 공유 설정 자리.
|
|
141
|
+
// 구체적인 값은 팀이 정한다. 개인 오버라이드는 settings.local.json이며
|
|
142
|
+
// .gitignore 대상이다.
|
|
143
|
+
export const COPILOT_SETTINGS = `{}`
|
|
144
|
+
|
|
145
|
+
// VS Code는 최상위 키가 servers다 (Copilot CLI의 mcpServers와 다르다).
|
|
146
|
+
export const VSCODE_MCP_CONFIG = `{
|
|
147
|
+
"servers": {}
|
|
148
|
+
}`
|