@rch4com/agent-setup 1.1.0 → 1.3.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/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 +117 -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
package/lib/tui/run.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { emitKeypressEvents } from 'node:readline'
|
|
|
4
4
|
import { planChanges, apply } from '../engine.mjs'
|
|
5
5
|
import { makeOpener, openPreview } from '../design-md/open.mjs'
|
|
6
6
|
import { netFetch, CATALOG_PATH } from '../design-md/catalog.mjs'
|
|
7
|
+
import { createT, toText, LOCALES } from '../i18n/index.mjs'
|
|
8
|
+
import { RECORD_REL, writeLang } from '../bootstrap/record.mjs'
|
|
7
9
|
import { collectRows, installedIds } from './rows.mjs'
|
|
8
10
|
import {
|
|
9
11
|
createState, setQuery, setFocus, move, moveTab, toggle, toggleVisible, scroll, currentRow, replaceRows, activeTab,
|
|
@@ -19,8 +21,6 @@ const HOME = `${ESC}[H`
|
|
|
19
21
|
const CLEAR_LINE = `${ESC}[K`
|
|
20
22
|
const CLEAR_DOWN = `${ESC}[J`
|
|
21
23
|
|
|
22
|
-
const ACTION_LABEL = { install: '설치', complete: '보완 설치', uninstall: '제거' }
|
|
23
|
-
|
|
24
24
|
// 키를 큐에 쌓아 두고 하나씩 꺼내 쓴다. 이벤트 상태 기계 대신
|
|
25
25
|
// `for(;;) await next()` 루프로 흐름을 읽을 수 있게 하기 위해서다.
|
|
26
26
|
function keyReader(stdin) {
|
|
@@ -42,10 +42,13 @@ function itemStates(rows) {
|
|
|
42
42
|
return rows.filter((r) => r.kind === 'item').map((r) => ({ item: r.item, status: r.status }))
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// row.section은 이제 소문자 id다(rows.mjs Task 9) — t 없이 그대로 찍으면
|
|
46
|
+
// 화면에 'action'·'plugin' 같은 raw id가 새어 나간다. 이 경로도 사용자가
|
|
47
|
+
// 보는 출력이라 여기서도 t를 받는다.
|
|
48
|
+
function printPlain(rows, log, t = createT('en')) {
|
|
46
49
|
let section = null
|
|
47
50
|
for (const row of rows) {
|
|
48
|
-
if (row.section !== section) { section = row.section; log(`[${section}]`) }
|
|
51
|
+
if (row.section !== section) { section = row.section; log(`[${t(`section.${section}`)}]`) }
|
|
49
52
|
const mark = row.kind === 'action' ? '▶' : row.status === 'absent' ? ' ' : '×'
|
|
50
53
|
log(` [${mark}] ${row.label}${row.hint ? ` — ${row.hint}` : ''}`)
|
|
51
54
|
}
|
|
@@ -71,13 +74,15 @@ export async function runTui(root, opts = {}) {
|
|
|
71
74
|
stdout = process.stdout,
|
|
72
75
|
} = opts
|
|
73
76
|
const opener = opts.opener ?? makeOpener(dryRun, log)
|
|
77
|
+
// let으로 둔다 — 언어 전환 행(cycleLanguage)이 실행 중에 갈아끼운다.
|
|
78
|
+
let { t = createT('en'), localeForced = false } = opts
|
|
74
79
|
|
|
75
|
-
let collected = await collectRows(root, { fetchImpl, designDirs, env, catalogFile, log: () => {} })
|
|
80
|
+
let collected = await collectRows(root, { fetchImpl, designDirs, env, catalogFile, log: () => {}, t })
|
|
76
81
|
|
|
77
82
|
// raw 모드를 켤 수 없으면(CI·파이프) 목록만 내고 끝낸다.
|
|
78
83
|
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
79
|
-
printPlain(collected.rows, log)
|
|
80
|
-
log(
|
|
84
|
+
printPlain(collected.rows, log, t)
|
|
85
|
+
log(`\n${t('tui.nonInteractive')}`)
|
|
81
86
|
return { interactive: false }
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -97,7 +102,7 @@ export async function runTui(root, opts = {}) {
|
|
|
97
102
|
const paint = () => {
|
|
98
103
|
const height = stdout.rows ?? 24
|
|
99
104
|
state = scroll(state, bodyHeight(height))
|
|
100
|
-
draw(render(state, { width: stdout.columns ?? 80, height, repo: root, dryRun, color, status }))
|
|
105
|
+
draw(render(state, { width: stdout.columns ?? 80, height, repo: root, dryRun, color, status, t }))
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
// 로그는 화면 안에 우겨넣지 않는다 — 실패 메시지가 길고, 잘리면 진단이 불가능해진다.
|
|
@@ -106,24 +111,49 @@ export async function runTui(root, opts = {}) {
|
|
|
106
111
|
try { return await fn() } finally { stdout.write(ALT_ON + HIDE_CURSOR) }
|
|
107
112
|
}
|
|
108
113
|
const confirm = async (message) => {
|
|
109
|
-
stdout.write(`\n${message}
|
|
114
|
+
stdout.write(`\n${message}${t('tui.confirmSuffix')}`)
|
|
110
115
|
const key = await keys.next()
|
|
111
116
|
stdout.write('\n')
|
|
112
117
|
return String(key.str ?? '').toLowerCase() === 'y'
|
|
113
118
|
}
|
|
114
119
|
const pause = async () => {
|
|
115
|
-
stdout.write(
|
|
120
|
+
stdout.write(`\n${t('tui.pressAnyKey')}`)
|
|
116
121
|
await keys.next()
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
// 언어 전환은 선택을 보존해야 한다. 적용·액션 실행 뒤 재스캔은 그 반대로
|
|
125
|
+
// 실제 설치 상태로 되돌려야 한다 — 둘을 한 함수로 묶으면 언어를 바꿀 때
|
|
126
|
+
// 사용자가 고르던 항목이 조용히 날아간다.
|
|
127
|
+
const rebuild = async (keepSelection) => {
|
|
128
|
+
collected = await collectRows(root, { fetchImpl, designDirs, env, catalogFile, t, log: () => {} })
|
|
129
|
+
const ids = keepSelection ? [...state.selected] : installedIds(collected.rows)
|
|
130
|
+
state = replaceRows(state, collected.rows, ids)
|
|
131
|
+
}
|
|
132
|
+
const recollect = () => rebuild(false)
|
|
133
|
+
|
|
134
|
+
const cycleLanguage = async () => {
|
|
135
|
+
const next = LOCALES[(LOCALES.indexOf(t.locale) + 1) % LOCALES.length]
|
|
136
|
+
t = createT(next)
|
|
137
|
+
let note
|
|
138
|
+
try {
|
|
139
|
+
writeLang(root, next, { dryRun, log: () => {}, t })
|
|
140
|
+
note = dryRun ? t('tui.lang.dryRun') : t('tui.lang.saved', { path: RECORD_REL })
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// 언어는 부수적 설정이다. 저장에 실패했다고 화면이 죽으면 설치기를 못 쓴다.
|
|
143
|
+
note = t('tui.lang.saveFailed', { message: err.message })
|
|
144
|
+
}
|
|
145
|
+
// 전환은 이번 실행에 이미 적용됐다(위에서 t를 갈아끼우고 아래에서 다시
|
|
146
|
+
// 그린다) — 읽을 수 없는 언어에 갇힌 사용자가 빠져나올 길이 있어야 한다.
|
|
147
|
+
// 알릴 것은 "다음 실행에서는 플래그·환경변수가 다시 이긴다"뿐이다.
|
|
148
|
+
// note는 이미 "저장했다"를 말하므로 겹치지 않게 이어 붙인다.
|
|
149
|
+
if (localeForced) note = `${note} · ${t('tui.lang.overridden')}`
|
|
150
|
+
await rebuild(true)
|
|
151
|
+
return note
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
const select = () => {
|
|
125
155
|
const row = currentRow(state)
|
|
126
|
-
if (row?.kind !== 'item') return '
|
|
156
|
+
if (row?.kind !== 'item') return t('tui.notItemRow')
|
|
127
157
|
state = toggle(state)
|
|
128
158
|
return ''
|
|
129
159
|
}
|
|
@@ -131,16 +161,16 @@ export async function runTui(root, opts = {}) {
|
|
|
131
161
|
// 미리보기는 두 포커스에서 모두 통해야 한다 — 검색으로 찾은 직후가 가장 열어 보고 싶은 순간이다.
|
|
132
162
|
const preview = () => {
|
|
133
163
|
const row = currentRow(state)
|
|
134
|
-
if (row?.kind !== 'item' || !row.previewTarget) return '
|
|
164
|
+
if (row?.kind !== 'item' || !row.previewTarget) return t('tui.noPreview')
|
|
135
165
|
const notes = []
|
|
136
|
-
openPreview(opener, row.item, (m) => notes.push(String(m).trim()))
|
|
137
|
-
return notes.length > 0 ? notes.join(' ') :
|
|
166
|
+
openPreview(opener, row.item, (m) => notes.push(String(m).trim()), t)
|
|
167
|
+
return notes.length > 0 ? notes.join(' ') : t('tui.opened', { target: row.previewTarget })
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
// 제출 검토 — 적용 직전에 변경 목록을 보여 주고 Enter/Esc만 받는다.
|
|
141
171
|
const review = async (changes) => {
|
|
142
172
|
for (;;) {
|
|
143
|
-
draw(renderReview(changes, { width: stdout.columns ?? 80, height: stdout.rows ?? 24, dryRun, color }))
|
|
173
|
+
draw(renderReview(changes, { width: stdout.columns ?? 80, height: stdout.rows ?? 24, dryRun, color, t }))
|
|
144
174
|
const key = await keys.next()
|
|
145
175
|
if (key.ctrl && key.name === 'c') return 'quit'
|
|
146
176
|
if (key.name === 'escape') return 'cancel'
|
|
@@ -206,7 +236,8 @@ export async function runTui(root, opts = {}) {
|
|
|
206
236
|
if (key.ctrl && key.name === 'a') {
|
|
207
237
|
const before = state.selected.size
|
|
208
238
|
state = toggleVisible(state)
|
|
209
|
-
|
|
239
|
+
const statusKey = state.selected.size >= before ? 'tui.toggledAll' : 'tui.toggledNone'
|
|
240
|
+
status = t(statusKey, { tab: t(`section.${activeTab(state)}`) })
|
|
210
241
|
continue
|
|
211
242
|
}
|
|
212
243
|
|
|
@@ -215,9 +246,11 @@ export async function runTui(root, opts = {}) {
|
|
|
215
246
|
if (key.name === 'return' || key.name === 'enter') {
|
|
216
247
|
// 액션 행은 그 자리에서 실행한다. 나머지는 제출(검토 → 일괄 적용)이다.
|
|
217
248
|
const row = currentRow(state)
|
|
249
|
+
// 언어 행은 화면을 벗어나지 않는다 — 그 자리에서 t를 갈아끼우고 다시 그린다.
|
|
250
|
+
if (row?.id === 'action.language') { status = await cycleLanguage(); continue }
|
|
218
251
|
if (row?.kind === 'action') {
|
|
219
252
|
await suspend(async () => {
|
|
220
|
-
await row.run({ root, dryRun, skillMode, fetchImpl, catalogFile, log, confirm })
|
|
253
|
+
await row.run({ root, dryRun, skillMode, fetchImpl, catalogFile, log, confirm, t })
|
|
221
254
|
await pause()
|
|
222
255
|
})
|
|
223
256
|
await recollect()
|
|
@@ -225,19 +258,22 @@ export async function runTui(root, opts = {}) {
|
|
|
225
258
|
}
|
|
226
259
|
|
|
227
260
|
const changes = planChanges(itemStates(state.rows), state.selected)
|
|
228
|
-
if (changes.length === 0) { status = '
|
|
261
|
+
if (changes.length === 0) { status = t('tui.noChanges'); continue }
|
|
229
262
|
|
|
230
263
|
const verdict = await review(changes)
|
|
231
264
|
if (verdict === 'quit') break
|
|
232
|
-
if (verdict === 'cancel') { status = '
|
|
265
|
+
if (verdict === 'cancel') { status = t('tui.submitCancelled'); continue }
|
|
233
266
|
|
|
234
267
|
await suspend(async () => {
|
|
235
|
-
log(`\n
|
|
236
|
-
for (const c of changes) log(` ${
|
|
237
|
-
const results = await apply(root, changes, { dryRun, log })
|
|
238
|
-
for (const r of results)
|
|
268
|
+
log(`\n${t('tui.applyHeader', { count: changes.length, suffix: dryRun ? ' (dry-run)' : '' })}`)
|
|
269
|
+
for (const c of changes) log(` ${t(`change.${c.action}`)} ${c.item.label}`)
|
|
270
|
+
const results = await apply(root, changes, { dryRun, log, t })
|
|
271
|
+
for (const r of results) {
|
|
272
|
+
const message = toText(t, r.message)
|
|
273
|
+
log(` ${r.ok ? '✔' : '✖'} ${t(`change.${r.action}`)} ${r.item.label}${message ? ` — ${message}` : ''}`)
|
|
274
|
+
}
|
|
239
275
|
if (results.some((r) => !r.ok)) process.exitCode = 1
|
|
240
|
-
log(
|
|
276
|
+
log(`\n${t('apply.seeGitDiff')}`)
|
|
241
277
|
await pause()
|
|
242
278
|
})
|
|
243
279
|
await recollect()
|
|
@@ -251,7 +287,7 @@ export async function runTui(root, opts = {}) {
|
|
|
251
287
|
stdin.pause()
|
|
252
288
|
}
|
|
253
289
|
|
|
254
|
-
return { interactive: true }
|
|
290
|
+
return { interactive: true, state }
|
|
255
291
|
}
|
|
256
292
|
|
|
257
293
|
export { printPlain, itemStates }
|
package/lib/update.mjs
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
// update 흐름 — 관리 파일을 최신 템플릿으로 옮기고 드리프트를 보고한다.
|
|
2
|
-
//
|
|
3
|
-
// 갱신 판정은 apply.mjs의 updateFiles·updateBlocks가 하고, 여기는 순서와
|
|
4
|
-
// 보고, 그리고 기록 갱신만 담당한다. flow.mjs가 생성 쪽에서 하는 역할과 같다.
|
|
5
|
-
import { execFileSync } from 'node:child_process'
|
|
6
|
-
import { MANIFEST } from './bootstrap/manifest.mjs'
|
|
7
|
-
import {
|
|
8
|
-
configureAdapterSafe, ensureIgnore, ensureJsonKeys, updateBlocks, updateFiles,
|
|
9
|
-
} from './bootstrap/apply.mjs'
|
|
10
|
-
import { RECORD_REL, managedKey, readRecord, toolVersion, writeRecord } from './bootstrap/record.mjs'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
say('
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { results, drift, record: { ...record, managed } }
|
|
85
|
-
}
|
|
1
|
+
// update 흐름 — 관리 파일을 최신 템플릿으로 옮기고 드리프트를 보고한다.
|
|
2
|
+
//
|
|
3
|
+
// 갱신 판정은 apply.mjs의 updateFiles·updateBlocks가 하고, 여기는 순서와
|
|
4
|
+
// 보고, 그리고 기록 갱신만 담당한다. flow.mjs가 생성 쪽에서 하는 역할과 같다.
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
import { MANIFEST } from './bootstrap/manifest.mjs'
|
|
7
|
+
import {
|
|
8
|
+
configureAdapterSafe, ensureIgnore, ensureJsonKeys, updateBlocks, updateFiles,
|
|
9
|
+
} from './bootstrap/apply.mjs'
|
|
10
|
+
import { RECORD_REL, managedKey, readRecord, toolVersion, writeRecord } from './bootstrap/record.mjs'
|
|
11
|
+
import { LocalizedError, createT, toText } from './i18n/index.mjs'
|
|
12
|
+
|
|
13
|
+
// git이 유일한 되돌리기 수단이다. 커밋되지 않은 변경 위에 덮어쓰면 사용자가
|
|
14
|
+
// 잃은 것을 복구할 방법이 없다.
|
|
15
|
+
function assertCleanWorktree(root) {
|
|
16
|
+
const out = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
17
|
+
if (out.trim()) {
|
|
18
|
+
throw new LocalizedError('error.forceNeedsCleanTree')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runUpdate(root, opts = {}) {
|
|
23
|
+
const { dryRun = false, force = false, log = console.log, manifest = MANIFEST, t = createT('en') } = opts
|
|
24
|
+
const say = (message) => log(`[agent-setup] ${message}`)
|
|
25
|
+
|
|
26
|
+
const record = readRecord(root)
|
|
27
|
+
if (!record) {
|
|
28
|
+
throw new LocalizedError('error.noRecordForUpdate', { path: RECORD_REL })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (force) assertCleanWorktree(root)
|
|
32
|
+
|
|
33
|
+
// 2단계에서는 버전 차이를 보고만 한다. 여기서 중단시키면 기존 사용자의
|
|
34
|
+
// 실행이 오류로 죽어 파괴적 변경이 된다. 엄격 검사는 apply와 함께 3단계다.
|
|
35
|
+
const running = toolVersion()
|
|
36
|
+
if (record.pinnedVersion && record.pinnedVersion !== running) {
|
|
37
|
+
say(t('update.versionMove', { pinned: record.pinnedVersion, running }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ctx = { dryRun, force, log: say, managed: record.managed, t }
|
|
41
|
+
const results = [
|
|
42
|
+
...updateFiles(root, manifest.files, ctx),
|
|
43
|
+
...updateBlocks(root, manifest.blocks, ctx),
|
|
44
|
+
// 키 보장과 gitignore 항목 추가는 원래 멱등이라 그대로 재실행한다.
|
|
45
|
+
...ensureJsonKeys(root, manifest.settings ?? [], { dryRun, log: say, t }),
|
|
46
|
+
...ensureIgnore(root, manifest.ignore, { dryRun, log: say, t }),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
// 어댑터는 링크가 끊겼을 수 있어 재검증한다. 항목별로 실패를 격리한다.
|
|
50
|
+
for (const entry of manifest.adapters) {
|
|
51
|
+
results.push(configureAdapterSafe(root, entry, {
|
|
52
|
+
dryRun, log: say, skillMode: record.skillMode, t,
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 성공한 것만 기록에 옮긴다. 드리프트는 hash를 담지 않으므로 옛 해시가
|
|
57
|
+
// 남아, 사용자가 원복하면 다음 update가 다시 집어간다.
|
|
58
|
+
const managed = { ...record.managed }
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
if (!r.hash) continue
|
|
61
|
+
const isBlock = manifest.blocks.some((b) => b.path === r.path)
|
|
62
|
+
managed[managedKey(r.path, isBlock)] = r.hash
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const drift = results.filter((r) => r.action === 'drift')
|
|
66
|
+
const updated = results.filter((r) => r.action === 'update')
|
|
67
|
+
const created = results.filter((r) => r.action === 'create')
|
|
68
|
+
|
|
69
|
+
if (!dryRun) writeRecord(root, { ...record, managed }, { dryRun, log: say, t })
|
|
70
|
+
|
|
71
|
+
log('')
|
|
72
|
+
say(t('update.summary', { updated: updated.length, created: created.length, drift: drift.length }))
|
|
73
|
+
if (drift.length > 0) {
|
|
74
|
+
say(t('update.driftHeader'))
|
|
75
|
+
// 결과 message는 구조화 메시지(msg())다. 문자열 삽입 자리에 그대로
|
|
76
|
+
// 꽂으면 "[object Object]"가 찍히므로 toText로 활성 로케일에 맞춰 푼다.
|
|
77
|
+
for (const d of drift) {
|
|
78
|
+
const drifted = toText(t, d.message)
|
|
79
|
+
say(` ${d.path}${drifted ? ` — ${drifted}` : ''}`)
|
|
80
|
+
}
|
|
81
|
+
say(t('update.driftHint'))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { results, drift, record: { ...record, managed } }
|
|
85
|
+
}
|
package/lib/width.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// 터미널 표시 폭 계산. TUI 화면과 비대화형 목록이 함께 쓴다.
|
|
2
|
+
//
|
|
3
|
+
// 아무것도 import 하지 않는다 — install.mjs가 정적으로 끌어오므로
|
|
4
|
+
// bootstrap.isolation.test.mjs가 이 파일까지 의존성 0을 요구한다. 그리고
|
|
5
|
+
// design-md/flow.mjs도 여기에 닿는데, 이 함수들이 tui/render.mjs에 남아
|
|
6
|
+
// 있으면 render → flow → render 순환 import가 된다.
|
|
7
|
+
//
|
|
8
|
+
// String.padEnd는 코드 유닛을 센다. 한글 한 글자가 두 칸을 차지하는 터미널에서
|
|
9
|
+
// padEnd로 열을 맞추면 로케일이 바뀔 때마다 열이 어긋난다 — 그래서 폭 계산은
|
|
10
|
+
// 화면이든 목록이든 전부 이 모듈을 거친다.
|
|
11
|
+
|
|
12
|
+
// 동아시아 글자는 터미널에서 두 칸을 차지한다. 전체 wcwidth 표 대신
|
|
13
|
+
// 실제로 쓰이는 구간(한글·한자·가나·전각)만 넓게 센다.
|
|
14
|
+
function charWidth(cp) {
|
|
15
|
+
return (cp >= 0x1100 && cp <= 0x115f) // 한글 자모
|
|
16
|
+
|| (cp >= 0x2e80 && cp <= 0xa4cf) // 한자 부수·가나·한자
|
|
17
|
+
|| (cp >= 0xac00 && cp <= 0xd7a3) // 한글 음절
|
|
18
|
+
|| (cp >= 0xf900 && cp <= 0xfaff) // 한자 호환
|
|
19
|
+
|| (cp >= 0xfe30 && cp <= 0xfe6f) // 전각 형태
|
|
20
|
+
|| (cp >= 0xff00 && cp <= 0xff60) // 전각 영숫자
|
|
21
|
+
|| (cp >= 0xffe0 && cp <= 0xffe6)
|
|
22
|
+
? 2 : 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function width(text) {
|
|
26
|
+
let w = 0
|
|
27
|
+
for (const ch of String(text ?? '')) w += charWidth(ch.codePointAt(0))
|
|
28
|
+
return w
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 색 코드는 폭에 포함되면 안 되므로, 자르기는 항상 색을 입히기 **전에** 한다.
|
|
32
|
+
export function cut(text, limit) {
|
|
33
|
+
if (limit <= 0) return ''
|
|
34
|
+
const s = String(text ?? '')
|
|
35
|
+
if (width(s) <= limit) return s
|
|
36
|
+
let out = ''
|
|
37
|
+
let w = 0
|
|
38
|
+
for (const ch of s) {
|
|
39
|
+
const cw = charWidth(ch.codePointAt(0))
|
|
40
|
+
if (w + cw > limit - 1) break
|
|
41
|
+
out += ch
|
|
42
|
+
w += cw
|
|
43
|
+
}
|
|
44
|
+
return `${out}…`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pad(text, limit) {
|
|
48
|
+
const s = cut(text, limit)
|
|
49
|
+
return s + ' '.repeat(Math.max(0, limit - width(s)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 상태 라벨은 로케일마다 길이가 다르다(영어 'Not installed' 13, 한국어
|
|
53
|
+
// '미설치' 6). 한 번 재어 상수로 박으면 다른 로케일에서 열이 깨지므로,
|
|
54
|
+
// 화면을 그릴 때 그 로케일의 실제 라벨에서 폭을 뽑는다.
|
|
55
|
+
export function labelWidth(t, keys) {
|
|
56
|
+
return Math.max(...keys.map((key) => width(t(key))))
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rch4com/agent-setup",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "여러 코딩 에이전트 CLI를 한 저장소에서 함께 쓰기 위한 저장소 범위 부트스트랩과 선택 항목 설치기",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"agents-md",
|
|
7
|
-
"claude-code",
|
|
8
|
-
"codex",
|
|
9
|
-
"gemini-cli",
|
|
10
|
-
"copilot",
|
|
11
|
-
"mcp",
|
|
12
|
-
"agent-skills"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/rch4com/Agent-Setup#readme",
|
|
15
|
-
"bugs": "https://github.com/rch4com/Agent-Setup/issues",
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/rch4com/Agent-Setup.git",
|
|
19
|
-
"directory": "agent-installer"
|
|
20
|
-
},
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"type": "module",
|
|
23
|
-
"engines": { "node": ">=20" },
|
|
24
|
-
"bin": { "agent-setup": "install.mjs" },
|
|
25
|
-
"files": ["install.mjs", "lib/", "README.md", "LICENSE"],
|
|
26
|
-
"publishConfig": { "access": "public" },
|
|
27
|
-
"scripts": {
|
|
28
|
-
"start": "node install.mjs",
|
|
29
|
-
"test": "node --test
|
|
30
|
-
"refresh-bundle": "node scripts/refresh-bundle.mjs"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"jsonc-parser": "^3.3.1",
|
|
34
|
-
"smol-toml": "^1.3.1"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@rch4com/agent-setup",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Repository-scoped bootstrap and optional-item installer for using multiple coding-agent CLIs in one repository | 여러 코딩 에이전트 CLI를 한 저장소에서 함께 쓰기 위한 저장소 범위 부트스트랩과 선택 항목 설치기",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents-md",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex",
|
|
9
|
+
"gemini-cli",
|
|
10
|
+
"copilot",
|
|
11
|
+
"mcp",
|
|
12
|
+
"agent-skills"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/rch4com/Agent-Setup#readme",
|
|
15
|
+
"bugs": "https://github.com/rch4com/Agent-Setup/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/rch4com/Agent-Setup.git",
|
|
19
|
+
"directory": "agent-installer"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"engines": { "node": ">=20" },
|
|
24
|
+
"bin": { "agent-setup": "install.mjs" },
|
|
25
|
+
"files": ["install.mjs", "lib/", "README.md", "LICENSE"],
|
|
26
|
+
"publishConfig": { "access": "public" },
|
|
27
|
+
"scripts": {
|
|
28
|
+
"start": "node install.mjs",
|
|
29
|
+
"test": "node --test",
|
|
30
|
+
"refresh-bundle": "node scripts/refresh-bundle.mjs"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"jsonc-parser": "^3.3.1",
|
|
34
|
+
"smol-toml": "^1.3.1"
|
|
35
|
+
}
|
|
36
|
+
}
|