@rch4com/agent-setup 1.1.0 → 1.4.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 -21
- package/README.md +197 -55
- package/install.mjs +152 -101
- package/lib/args.mjs +276 -311
- package/lib/bootstrap/adapter.mjs +96 -95
- package/lib/bootstrap/apply.mjs +380 -379
- package/lib/bootstrap/flow.mjs +73 -70
- package/lib/bootstrap/record.mjs +154 -142
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -19
- package/lib/catalog.mjs +213 -145
- package/lib/claude-plugins.mjs +64 -64
- package/lib/clis.mjs +102 -102
- package/lib/context.mjs +66 -65
- package/lib/deps.mjs +5 -7
- package/lib/design-md/cache/awesome-design-md/LICENSE.md +38 -0
- package/lib/design-md/catalog.json +454 -454
- package/lib/design-md/catalog.mjs +266 -268
- package/lib/design-md/flow.mjs +181 -166
- package/lib/design-md/open.mjs +51 -50
- package/lib/design-md/providers/awesome-design-md.mjs +124 -114
- package/lib/design-md/providers/index.mjs +9 -9
- package/lib/design-md/scan.mjs +14 -10
- package/lib/engine.mjs +48 -44
- package/lib/gitignore.mjs +18 -18
- package/lib/i18n/catalog/en.mjs +369 -0
- package/lib/i18n/catalog/ko.mjs +359 -0
- package/lib/i18n/detect.mjs +39 -0
- package/lib/i18n/index.mjs +93 -0
- package/lib/items/mcp.codebase-memory.mjs +6 -6
- package/lib/items/mcp.graphify.mjs +8 -0
- package/lib/items/mcp.headroom.mjs +8 -0
- package/lib/items/mcp.notion.mjs +6 -6
- package/lib/items/mcp.supabase.mjs +6 -6
- package/lib/items/mcp.vercel.mjs +6 -6
- package/lib/items/plugin.bkit.mjs +7 -7
- package/lib/items/plugin.ecc.mjs +11 -0
- package/lib/items/plugin.impeccable.mjs +14 -0
- package/lib/items/plugin.mattpocock-skills.mjs +2 -2
- package/lib/items/plugin.ponytail.mjs +104 -0
- package/lib/items/plugin.superpowers.mjs +7 -7
- package/lib/items/plugin.understand-anything.mjs +10 -0
- package/lib/items/skill.caveman.mjs +7 -0
- package/lib/items/skill.gsd.mjs +25 -24
- package/lib/items/skill.gstack.mjs +40 -39
- package/lib/items/skill.karpathy.mjs +7 -0
- package/lib/items/skill.taste.mjs +9 -0
- package/lib/jsonfile.mjs +40 -40
- package/lib/status.mjs +93 -92
- package/lib/tomlfile.mjs +60 -60
- package/lib/tui/render.mjs +41 -63
- package/lib/tui/rows.mjs +278 -206
- package/lib/tui/run.mjs +64 -28
- package/lib/update.mjs +85 -85
- package/lib/width.mjs +57 -0
- package/package.json +36 -36
|
@@ -1,114 +1,124 @@
|
|
|
1
|
-
// awesome-design-md (VoltAgent) 프로바이더.
|
|
2
|
-
// 카탈로그: README `## Collection`의 `### 카테고리` + 링크 목록을 파싱한다.
|
|
3
|
-
// 링크 URL `getdesign.md/<name>/design-md`의 <name>이 곧 GitHub 폴더명이다.
|
|
4
|
-
// 파일: 폴더에는 DESIGN.md만 있다(preview.html 없음). 렌더된 미리보기는 웹페이지.
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
-
import { join, dirname } from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
continue
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
// awesome-design-md (VoltAgent) 프로바이더.
|
|
2
|
+
// 카탈로그: README `## Collection`의 `### 카테고리` + 링크 목록을 파싱한다.
|
|
3
|
+
// 링크 URL `getdesign.md/<name>/design-md`의 <name>이 곧 GitHub 폴더명이다.
|
|
4
|
+
// 파일: 폴더에는 DESIGN.md만 있다(preview.html 없음). 렌더된 미리보기는 웹페이지.
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join, dirname } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { LocalizedError } from '../../i18n/index.mjs'
|
|
9
|
+
|
|
10
|
+
// lib/design-md/cache/<name>/DESIGN.md — 인스톨러에 동봉된 오프라인 번들.
|
|
11
|
+
const BUNDLE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'cache')
|
|
12
|
+
|
|
13
|
+
const REPO = 'VoltAgent/awesome-design-md'
|
|
14
|
+
const BRANCH = 'main'
|
|
15
|
+
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/design-md`
|
|
16
|
+
const README_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/README.md`
|
|
17
|
+
// 번들에 함께 담는 상류 고지의 출처. MIT는 사본에 저작권 고지와 허가 문구를
|
|
18
|
+
// 담을 것을 요구하므로, 파일을 내려받아 재배포하는 이상 이 URL이 필요하다.
|
|
19
|
+
const LICENSE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/LICENSE`
|
|
20
|
+
const TREE_URL = `https://api.github.com/repos/${REPO}/git/trees/${BRANCH}?recursive=1`
|
|
21
|
+
// scan.mjs의 BUNDLE_CATEGORY와 같은 값이어야 한다 — 둘 다 카테고리 못 얻은
|
|
22
|
+
// 항목이 모이는 자리라, 갈라지면 같은 그룹이 헤더 두 개로 쪼개진다.
|
|
23
|
+
export const UNCATEGORIZED = '__other'
|
|
24
|
+
|
|
25
|
+
const ENTRY_RE = /^-\s*\[\*\*(.+?)\*\*\]\(\s*https?:\/\/getdesign\.md\/([^/)]+)\/design-md\s*\)\s*-\s*(.*)$/
|
|
26
|
+
|
|
27
|
+
// README `## Collection` 블록만 대상으로 카테고리·항목을 뽑는다.
|
|
28
|
+
export function parseReadme(md) {
|
|
29
|
+
const lines = md.split(/\r?\n/)
|
|
30
|
+
const entries = []
|
|
31
|
+
let inCollection = false
|
|
32
|
+
let category = UNCATEGORIZED
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const h2 = /^##\s+(.*)$/.exec(line)
|
|
35
|
+
if (h2) {
|
|
36
|
+
inCollection = /^collection\b/i.test(h2[1].trim())
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (!inCollection) continue
|
|
40
|
+
const h3 = /^###\s+(.*)$/.exec(line)
|
|
41
|
+
if (h3) {
|
|
42
|
+
category = h3[1].trim()
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
const m = ENTRY_RE.exec(line.trim())
|
|
46
|
+
if (m) {
|
|
47
|
+
entries.push({ name: m[2].trim(), label: m[1].trim(), category, description: m[3].trim() })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return entries
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// tree API에서 design-md/<name>/ 폴더명 집합을 뽑는다(orphan 보강용).
|
|
54
|
+
export function parseTreeNames(treeJson) {
|
|
55
|
+
const names = new Set()
|
|
56
|
+
for (const e of treeJson?.tree ?? []) {
|
|
57
|
+
const m = /^design-md\/([^/]+)\//.exec(e.path)
|
|
58
|
+
if (m) names.add(m[1])
|
|
59
|
+
}
|
|
60
|
+
return names
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const awesomeDesignMd = {
|
|
64
|
+
id: 'awesome-design-md',
|
|
65
|
+
label: 'awesome-design-md (VoltAgent)',
|
|
66
|
+
files: ['DESIGN.md'],
|
|
67
|
+
licenseUrl: LICENSE_URL,
|
|
68
|
+
// 번들과 함께 발행되는 고지 파일. 이름이 LICENSE가 아닌 이유는 발행물 검사가
|
|
69
|
+
// 최상위 LICENSE 하나만 확장자 없는 파일로 허용하기 때문이다.
|
|
70
|
+
noticeFile: 'LICENSE.md',
|
|
71
|
+
|
|
72
|
+
fileUrl(name, file = 'DESIGN.md') {
|
|
73
|
+
return `${RAW_BASE}/${encodeURIComponent(name)}/${file}`
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// fileUrl과 같이 인코딩한다 — name은 원격 README에서 오므로, 그대로 이어
|
|
77
|
+
// 붙이면 URL이 아닌 문자가 미리보기 대상 문자열에 섞인다.
|
|
78
|
+
webUrl(name) {
|
|
79
|
+
return `https://getdesign.md/${encodeURIComponent(name)}/design-md`
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
bundlePath(name, file = 'DESIGN.md') {
|
|
83
|
+
return join(BUNDLE_DIR, this.id, name, file)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// 동봉 번들에서 읽는다(없으면 null). 오프라인 설치용.
|
|
87
|
+
bundledText(name, file = 'DESIGN.md') {
|
|
88
|
+
if (/[\\/]/.test(name)) return null
|
|
89
|
+
const p = this.bundlePath(name, file)
|
|
90
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async fetchFile(fetchImpl, name, file = 'DESIGN.md') {
|
|
94
|
+
const res = await fetchImpl(this.fileUrl(name, file))
|
|
95
|
+
if (!res.ok) return null
|
|
96
|
+
return res.text()
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// README 파싱(주) + tree orphan 보강(선택). 이름순 정렬 반환.
|
|
100
|
+
async fetchCatalog(fetchImpl) {
|
|
101
|
+
const readmeRes = await fetchImpl(README_URL)
|
|
102
|
+
if (!readmeRes.ok) throw new LocalizedError('error.readmeFetch', { status: readmeRes.status })
|
|
103
|
+
const entries = parseReadme(await readmeRes.text())
|
|
104
|
+
const seen = new Set(entries.map((e) => e.name))
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const treeRes = await fetchImpl(TREE_URL, { headers: { 'User-Agent': 'agent-installer' } })
|
|
108
|
+
if (treeRes.ok) {
|
|
109
|
+
for (const name of parseTreeNames(await treeRes.json())) {
|
|
110
|
+
if (!seen.has(name)) {
|
|
111
|
+
entries.push({ name, label: name, category: UNCATEGORIZED, description: '' })
|
|
112
|
+
seen.add(name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// tree 실패는 무시 — README만으로도 카탈로그는 유효하다.
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default awesomeDesignMd
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// design.md 프로바이더 레지스트리 — 소스 확장 지점.
|
|
2
|
-
// 새 소스 추가 = 프로바이더 모듈 1개 작성 + 여기 등록.
|
|
3
|
-
import { awesomeDesignMd } from './awesome-design-md.mjs'
|
|
4
|
-
|
|
5
|
-
export const PROVIDERS = [awesomeDesignMd]
|
|
6
|
-
|
|
7
|
-
export function getProvider(id) {
|
|
8
|
-
return PROVIDERS.find((p) => p.id === id)
|
|
9
|
-
}
|
|
1
|
+
// design.md 프로바이더 레지스트리 — 소스 확장 지점.
|
|
2
|
+
// 새 소스 추가 = 프로바이더 모듈 1개 작성 + 여기 등록.
|
|
3
|
+
import { awesomeDesignMd } from './awesome-design-md.mjs'
|
|
4
|
+
|
|
5
|
+
export const PROVIDERS = [awesomeDesignMd]
|
|
6
|
+
|
|
7
|
+
export function getProvider(id) {
|
|
8
|
+
return PROVIDERS.find((p) => p.id === id)
|
|
9
|
+
}
|
package/lib/design-md/scan.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
import { StringDecoder } from 'node:string_decoder'
|
|
9
9
|
import { isSafeSegment } from './catalog.mjs'
|
|
10
10
|
import { PROVIDERS } from './providers/index.mjs'
|
|
11
|
+
import { createT } from '../i18n/index.mjs'
|
|
11
12
|
|
|
12
13
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
13
14
|
export const BUNDLE_DIR = join(HERE, 'cache')
|
|
@@ -25,8 +26,11 @@ const SKIP_DIRS = new Set(['node_modules', '.git', '.svn'])
|
|
|
25
26
|
const FM_OPEN = /^---[ \t]*\r?\n/
|
|
26
27
|
const FM_CLOSE = /\r?\n---[ \t]*(\r?\n|$)/
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// 카테고리는 정렬 키이자 그룹 헤더다. 표시 문자열을 그대로 쓰면 번역하는
|
|
30
|
+
// 순간 정렬과 그룹 묶기가 어긋나므로 id를 두고 표시할 때만 번역한다.
|
|
31
|
+
// tui/rows.mjs의 CATCH_ALL_CATEGORY와 같은 값이어야 한다.
|
|
32
|
+
export const BUNDLE_CATEGORY = '__other'
|
|
33
|
+
export const LOCAL_CATEGORY = '__local'
|
|
30
34
|
|
|
31
35
|
// 설치 경로 `design-md/<소스>/<이름>/`에 쓰이므로 경로에 위험한 문자만 걷어낸다.
|
|
32
36
|
// 한글 등 유니코드 이름은 그대로 살린다 — 사내 디렉터리명이 대개 그렇다.
|
|
@@ -165,7 +169,7 @@ function subdirs(dir) {
|
|
|
165
169
|
// 디렉터리 트리에서 DESIGN.md를 찾아 엔트리로 만든다.
|
|
166
170
|
// `<루트>/<카테고리…>/<이름>/DESIGN.md` — DESIGN.md가 있는 폴더가 곧 항목(리프)이다.
|
|
167
171
|
// 루트 바로 아래 DESIGN.md가 있으면 루트 폴더명이 항목 이름이 된다.
|
|
168
|
-
export function scanDesignDir(dir, { defaultCategory = LOCAL_CATEGORY, maxDepth = MAX_DEPTH, log } = {}) {
|
|
172
|
+
export function scanDesignDir(dir, { defaultCategory = LOCAL_CATEGORY, maxDepth = MAX_DEPTH, log, t = createT('en') } = {}) {
|
|
169
173
|
const root = resolve(dir)
|
|
170
174
|
const entries = []
|
|
171
175
|
const seen = new Set()
|
|
@@ -177,7 +181,7 @@ export function scanDesignDir(dir, { defaultCategory = LOCAL_CATEGORY, maxDepth
|
|
|
177
181
|
const prefixed = sanitizeId([...segs.slice(0, -1), name].join('-'))
|
|
178
182
|
let candidate = prefixed
|
|
179
183
|
for (let n = 2; seen.has(candidate); n++) candidate = `${prefixed}-${n}`
|
|
180
|
-
if (log) log(
|
|
184
|
+
if (log) log(t('design.nameClash', { picked: candidate, path: segs.join('/') }))
|
|
181
185
|
return candidate
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -251,7 +255,7 @@ export function makeDirProvider({ id, label, dir, entries }) {
|
|
|
251
255
|
|
|
252
256
|
// 번들 캐시 하위 디렉터리 + 지정한 외부 경로를 소스 목록으로 만든다.
|
|
253
257
|
// 반환: [{ id, dir, label, bundled, entries, provider }]
|
|
254
|
-
export function discoverSources({ bundleDir = BUNDLE_DIR, extraDirs = [], reservedIds, log } = {}) {
|
|
258
|
+
export function discoverSources({ bundleDir = BUNDLE_DIR, extraDirs = [], reservedIds, log, t = createT('en') } = {}) {
|
|
255
259
|
const sources = []
|
|
256
260
|
const usedIds = new Set()
|
|
257
261
|
// 등록된 프로바이더 id는 번들 캐시 전용이다. 외부 경로가 그 id를 차지하면
|
|
@@ -259,18 +263,18 @@ export function discoverSources({ bundleDir = BUNDLE_DIR, extraDirs = [], reserv
|
|
|
259
263
|
const reserved = new Set(reservedIds ?? PROVIDERS.map((p) => p.id))
|
|
260
264
|
|
|
261
265
|
const add = ({ id, dir, bundled, defaultCategory }) => {
|
|
262
|
-
const entries = scanDesignDir(dir, { defaultCategory, log })
|
|
266
|
+
const entries = scanDesignDir(dir, { defaultCategory, log, t })
|
|
263
267
|
if (entries.length === 0) {
|
|
264
|
-
if (!bundled && log) log(
|
|
268
|
+
if (!bundled && log) log(t('design.noDesignFile', { path: dir }))
|
|
265
269
|
return
|
|
266
270
|
}
|
|
267
271
|
const base = sanitizeId(id)
|
|
268
272
|
const taken = (candidate) => usedIds.has(candidate) || (!bundled && reserved.has(candidate))
|
|
269
273
|
let uid = base
|
|
270
274
|
for (let n = 2; taken(uid); n++) uid = `${base}-${n}`
|
|
271
|
-
if (uid !== base && log) log(
|
|
275
|
+
if (uid !== base && log) log(t('design.sourceIdClash', { picked: uid, path: dir }))
|
|
272
276
|
usedIds.add(uid)
|
|
273
|
-
const label = bundled ? uid :
|
|
277
|
+
const label = bundled ? uid : t('design.localSuffix', { id: uid })
|
|
274
278
|
sources.push({ id: uid, dir, label, bundled, entries, provider: makeDirProvider({ id: uid, label, dir, entries }) })
|
|
275
279
|
}
|
|
276
280
|
|
|
@@ -282,7 +286,7 @@ export function discoverSources({ bundleDir = BUNDLE_DIR, extraDirs = [], reserv
|
|
|
282
286
|
for (const spec of extraDirs) {
|
|
283
287
|
const { id, dir } = parseDirSpec(spec)
|
|
284
288
|
if (!existsSync(dir)) {
|
|
285
|
-
if (log) log(
|
|
289
|
+
if (log) log(t('design.pathMissing', { path: dir }))
|
|
286
290
|
continue
|
|
287
291
|
}
|
|
288
292
|
add({ id, dir, bundled: false, defaultCategory: LOCAL_CATEGORY })
|
package/lib/engine.mjs
CHANGED
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
import { makeExec } from './catalog.mjs'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
1
|
+
import { makeExec } from './catalog.mjs'
|
|
2
|
+
import { createT, msg } from './i18n/index.mjs'
|
|
3
|
+
|
|
4
|
+
export async function scan(root, items) {
|
|
5
|
+
const states = []
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
try {
|
|
8
|
+
const r = await item.detect({ root })
|
|
9
|
+
states.push({ item, status: r.status, detail: r.detail })
|
|
10
|
+
} catch (err) {
|
|
11
|
+
states.push({ item, status: 'absent', detail: msg('item.scanFailed', { message: err.message }) })
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return states
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function planChanges(states, selectedIds) {
|
|
18
|
+
const changes = []
|
|
19
|
+
for (const { item, status } of states) {
|
|
20
|
+
const selected = selectedIds.has(item.id)
|
|
21
|
+
if (selected && status === 'absent') changes.push({ item, action: 'install' })
|
|
22
|
+
else if (selected && status === 'partial') changes.push({ item, action: 'complete' })
|
|
23
|
+
else if (!selected && status !== 'absent') changes.push({ item, action: 'uninstall' })
|
|
24
|
+
}
|
|
25
|
+
return changes
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en') } = {}) {
|
|
29
|
+
const exec = makeExec(dryRun, log)
|
|
30
|
+
const results = []
|
|
31
|
+
for (const { item, action } of changes) {
|
|
32
|
+
// log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
|
|
33
|
+
// 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
|
|
34
|
+
// 무엇이 바뀔지는 알 수 없는 상태였다.
|
|
35
|
+
const ctx = { root, dryRun, exec, log, t }
|
|
36
|
+
try {
|
|
37
|
+
const fn = action === 'uninstall' ? item.uninstall : item.install
|
|
38
|
+
const r = await fn.call(item, ctx)
|
|
39
|
+
results.push({ item, action, ok: true, message: r?.message })
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// LocalizedError는 .key를 들고 있다 — 구조체로 옮겨 담아야 표시하는
|
|
42
|
+
// 쪽에서 toText로 활성 로케일에 맞게 다시 렌더할 수 있다. 그냥
|
|
43
|
+
// err.message만 쓰면 LocalizedError.message는 항상 영어라 로케일이 섞인다.
|
|
44
|
+
results.push({ item, action, ok: false, message: err.key ? msg(err.key, err.params) : err.message })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return results
|
|
48
|
+
}
|
package/lib/gitignore.mjs
CHANGED
|
@@ -1,18 +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
|
-
}
|
|
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
|
+
}
|