@rch4com/agent-setup 1.19.0 → 1.20.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/install.mjs +7 -2
- package/lib/catalog.mjs +7 -0
- package/lib/i18n/catalog/en.mjs +1 -0
- package/lib/i18n/catalog/ko.mjs +1 -0
- package/lib/jsonfile.mjs +22 -3
- package/lib/status.mjs +20 -2
- package/lib/update.mjs +13 -3
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -125,7 +125,11 @@ async function main() {
|
|
|
125
125
|
// 살아남는다. 조용히 되돌아가는 대신 화면이 그 사실을 알리게 한다.
|
|
126
126
|
const localeForced = isLocaleForced({ flag, env: process.env })
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
// 도움말은 저장소가 필요 없다. 이 검사를 먼저 하면 `--help`가 Git 저장소
|
|
129
|
+
// 밖에서 "Run this inside a Git repository"로 죽는다 — 사용법을 보려는
|
|
130
|
+
// 사람에게 그 오류를 내밀 이유가 없다.
|
|
131
|
+
const wantsHelp = argv.includes('-h') || argv.includes('--help')
|
|
132
|
+
if (root === null && !wantsHelp) throw new LocalizedError('error.notGitRepo')
|
|
129
133
|
|
|
130
134
|
if (argv[0] === 'bootstrap') {
|
|
131
135
|
const opts = parseBootstrapArgs(argv.slice(1), t)
|
|
@@ -140,7 +144,8 @@ async function main() {
|
|
|
140
144
|
if (opts.help) { console.log(updateUsage(t)); return }
|
|
141
145
|
// 항목 수렴을 위해 스캔이 필요해 의존성 있는 모듈에 닿는다.
|
|
142
146
|
const { runUpdate } = await withDeps(() => import('./lib/update.mjs'), t)
|
|
143
|
-
await runUpdate(root, { ...opts, t })
|
|
147
|
+
const { failed } = await runUpdate(root, { ...opts, t })
|
|
148
|
+
if (failed.length > 0) process.exitCode = 1
|
|
144
149
|
return
|
|
145
150
|
}
|
|
146
151
|
|
package/lib/catalog.mjs
CHANGED
|
@@ -67,6 +67,12 @@ const execFileAsync = promisify(execFile)
|
|
|
67
67
|
// 보고된다. 호출부가 opts.timeout으로 바꿀 수 있다.
|
|
68
68
|
export const EXEC_TIMEOUT_MS = 10 * 60 * 1000
|
|
69
69
|
|
|
70
|
+
// 자식 출력의 상한. execFile 기본값(1MB)을 넘기면 Node가 자식을 **죽이고**
|
|
71
|
+
// "maxBuffer length exceeded"로 실패시킨다 — gsd(3,500파일)·hallmark
|
|
72
|
+
// (references 100개 이상)처럼 파일마다 한 줄씩 찍는 설치가 반쯤 깔린 채
|
|
73
|
+
// 실패로 보고됐다. 64MB면 어떤 설치 로그도 넉넉히 담긴다.
|
|
74
|
+
export const EXEC_MAX_BUFFER = 64 * 1024 * 1024
|
|
75
|
+
|
|
70
76
|
// 비동기다. 동기 실행은 이벤트 루프를 통째로 막아, npx가 도는 수십 초 동안
|
|
71
77
|
// 진행 화면을 한 번도 다시 그릴 수 없었다. 반환 형태({ ok, output })는
|
|
72
78
|
// 그대로라 호출부는 await만 더하면 된다.
|
|
@@ -91,6 +97,7 @@ export function makeExec(dryRun, log = console.log, t = null) {
|
|
|
91
97
|
const p = execFileAsync(file, fileArgs, {
|
|
92
98
|
encoding: 'utf8',
|
|
93
99
|
timeout: EXEC_TIMEOUT_MS,
|
|
100
|
+
maxBuffer: EXEC_MAX_BUFFER,
|
|
94
101
|
...opts,
|
|
95
102
|
shell,
|
|
96
103
|
})
|
package/lib/i18n/catalog/en.mjs
CHANGED
|
@@ -150,6 +150,7 @@ export default {
|
|
|
150
150
|
'log.record.write': 'install record written: {path}',
|
|
151
151
|
'error.recordUnreadable': 'Cannot read {path} ({code})',
|
|
152
152
|
'error.recordNotJson': 'Cannot read {path} — not JSON ({message})',
|
|
153
|
+
'error.jsonInvalid': 'Cannot read {path} — invalid JSON: {message}. Fix the file by hand; nothing was written.',
|
|
153
154
|
'error.recordVersion': '{path} has format version {found}. This tool uses {expected} — upgrade the tool or recreate the record.',
|
|
154
155
|
|
|
155
156
|
'log.repoRoot': 'repository root: {path}',
|
package/lib/i18n/catalog/ko.mjs
CHANGED
|
@@ -144,6 +144,7 @@ export default {
|
|
|
144
144
|
'log.record.write': '설치 기록 기록: {path}',
|
|
145
145
|
'error.recordUnreadable': '{path}을 읽을 수 없습니다 ({code})',
|
|
146
146
|
'error.recordNotJson': '{path}을 읽을 수 없습니다 — JSON이 아닙니다 ({message})',
|
|
147
|
+
'error.jsonInvalid': '{path}을 읽을 수 없습니다 — JSON이 깨져 있습니다: {message}. 파일을 손으로 고치세요. 아무것도 쓰지 않았습니다.',
|
|
147
148
|
'error.recordVersion': '{path}의 형식 버전이 {found}입니다. 이 도구는 {expected}을 씁니다 — 도구를 올리거나 기록을 다시 만드세요.',
|
|
148
149
|
|
|
149
150
|
'log.repoRoot': '저장소 루트: {path}',
|
package/lib/jsonfile.mjs
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
|
-
import { parse, modify, applyEdits } from 'jsonc-parser'
|
|
3
|
+
import { parse, modify, applyEdits, printParseErrorCode } from 'jsonc-parser'
|
|
4
|
+
import { LocalizedError } from './i18n/index.mjs'
|
|
4
5
|
|
|
5
6
|
const FORMAT = { formattingOptions: { insertSpaces: true, tabSize: 2 } }
|
|
7
|
+
// 이 파일들은 도구가 읽는 JSONC다 — 주석과 후행 콤마는 정상이다.
|
|
8
|
+
const PARSE = { allowTrailingComma: true }
|
|
6
9
|
|
|
7
10
|
function readText(file) {
|
|
8
11
|
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
// jsonc-parser의 parse는 던지지 않는다 — 닫는 괄호가 빠진 파일도 "복구한"
|
|
15
|
+
// 값을 돌려준다. 그대로 두면 깨진 파일에 키를 끼워 넣고 has()는 설치됨으로
|
|
16
|
+
// 읽어, 사용자는 파일이 깨진 것을 끝까지 모른다. 오류 목록을 받아 하나라도
|
|
17
|
+
// 있으면 record.mjs의 readRecord처럼 어느 파일이 왜 깨졌는지 말하며 던진다.
|
|
18
|
+
function parseStrict(text, file) {
|
|
19
|
+
const errors = []
|
|
20
|
+
const data = parse(text, errors, PARSE)
|
|
21
|
+
if (errors.length > 0) {
|
|
22
|
+
const [first] = errors
|
|
23
|
+
const line = text.slice(0, first.offset).split('\n').length
|
|
24
|
+
throw new LocalizedError('error.jsonInvalid', { path: file, message: `${printParseErrorCode(first.error)} (line ${line})` })
|
|
25
|
+
}
|
|
26
|
+
return data
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
export function readJson(file) {
|
|
12
30
|
const text = readText(file)
|
|
13
31
|
if (!text.trim()) return undefined
|
|
14
|
-
return
|
|
32
|
+
return parseStrict(text, file)
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
export function setKey(file, path, value) {
|
|
18
36
|
let text = readText(file)
|
|
19
37
|
if (!text.trim()) text = '{}\n'
|
|
38
|
+
else parseStrict(text, file)
|
|
20
39
|
const edits = modify(text, path, value, FORMAT)
|
|
21
40
|
mkdirSync(dirname(file), { recursive: true })
|
|
22
41
|
writeFileSync(file, applyEdits(text, edits))
|
|
@@ -25,7 +44,7 @@ export function setKey(file, path, value) {
|
|
|
25
44
|
export function removeKey(file, path) {
|
|
26
45
|
const text = readText(file)
|
|
27
46
|
if (!text.trim()) return
|
|
28
|
-
if (getIn(
|
|
47
|
+
if (getIn(parseStrict(text, file), path) === undefined) return
|
|
29
48
|
const edits = modify(text, path, undefined, FORMAT)
|
|
30
49
|
writeFileSync(file, applyEdits(text, edits))
|
|
31
50
|
}
|
package/lib/status.mjs
CHANGED
|
@@ -103,9 +103,27 @@ export function formatStatus(report, t = createT('en')) {
|
|
|
103
103
|
return lines.join('\n')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
// 레지스트리의 최신 발행 버전. status는 진단 명령이라 네트워크가 없거나
|
|
107
|
+
// 느려도 죽으면 안 된다 — 어떤 실패든 null로 떨어뜨리고 화면은 "최신"
|
|
108
|
+
// 자리만 비운다. 시간 제한은 짧게 둔다: 이 한 줄 때문에 status가 20초를
|
|
109
|
+
// 기다리면 진단 도구가 아니다.
|
|
110
|
+
export const LATEST_TIMEOUT_MS = 5000
|
|
111
|
+
const LATEST_URL = 'https://registry.npmjs.org/@rch4com%2Fagent-setup/latest'
|
|
112
|
+
|
|
113
|
+
export async function fetchLatestVersion(fetchImpl = fetch) {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetchImpl(LATEST_URL, { signal: AbortSignal.timeout(LATEST_TIMEOUT_MS) })
|
|
116
|
+
if (!res.ok) return null
|
|
117
|
+
const version = (await res.json())?.version
|
|
118
|
+
return typeof version === 'string' && version ? version : null
|
|
119
|
+
} catch {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function runStatus(root, { json = false, log = console.log, fetchImpl = fetch, t = createT('en') } = {}) {
|
|
107
125
|
const { loadItems } = await import('./catalog.mjs')
|
|
108
|
-
const report = await collectStatus(root, { items: await loadItems() })
|
|
126
|
+
const report = await collectStatus(root, { items: await loadItems(), latest: await fetchLatestVersion(fetchImpl) })
|
|
109
127
|
log(json ? JSON.stringify(report, null, 2) : formatStatus(report, t))
|
|
110
128
|
return report
|
|
111
129
|
}
|
package/lib/update.mjs
CHANGED
|
@@ -11,9 +11,11 @@ import { RECORD_REL, managedKey, readRecord, toolVersion, writeRecord } from './
|
|
|
11
11
|
import { LocalizedError, createT, toText } from './i18n/index.mjs'
|
|
12
12
|
|
|
13
13
|
// git이 유일한 되돌리기 수단이다. 커밋되지 않은 변경 위에 덮어쓰면 사용자가
|
|
14
|
-
// 잃은 것을 복구할 방법이 없다.
|
|
14
|
+
// 잃은 것을 복구할 방법이 없다. 추적되지 않는 파일은 세지 않는다 — 갱신
|
|
15
|
+
// 대상은 기록에 해시가 있는 추적 파일뿐이라, 스크래치 파일 하나가 --force를
|
|
16
|
+
// 막을 이유가 없다.
|
|
15
17
|
function assertCleanWorktree(root) {
|
|
16
|
-
const out = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
18
|
+
const out = execFileSync('git', ['-C', root, 'status', '--porcelain', '--untracked-files=no'], { encoding: 'utf8' })
|
|
17
19
|
if (out.trim()) {
|
|
18
20
|
throw new LocalizedError('error.forceNeedsCleanTree')
|
|
19
21
|
}
|
|
@@ -65,10 +67,18 @@ export async function runUpdate(root, opts = {}) {
|
|
|
65
67
|
const drift = results.filter((r) => r.action === 'drift')
|
|
66
68
|
const updated = results.filter((r) => r.action === 'update')
|
|
67
69
|
const created = results.filter((r) => r.action === 'create')
|
|
70
|
+
// 어댑터가 격리해 돌려준 실패다. 요약이 갱신·신규·드리프트만 세면 이
|
|
71
|
+
// 결과는 아무 데도 나오지 않고 종료 코드도 0이라, 링크가 끊긴 채 "갱신
|
|
72
|
+
// 0건"으로 끝난다. flow.mjs의 bootstrap과 같은 형식으로 보고한다.
|
|
73
|
+
const failed = results.filter((r) => !r.ok)
|
|
68
74
|
|
|
69
75
|
if (!dryRun) writeRecord(root, { ...record, managed }, { dryRun, log: say, t })
|
|
70
76
|
|
|
71
77
|
log('')
|
|
78
|
+
if (failed.length > 0) {
|
|
79
|
+
say(t('bootstrap.failures', { count: failed.length }))
|
|
80
|
+
for (const f of failed) say(` ✖ ${f.path} — ${toText(t, f.message)}`)
|
|
81
|
+
}
|
|
72
82
|
say(t('update.summary', { updated: updated.length, created: created.length, drift: drift.length }))
|
|
73
83
|
if (drift.length > 0) {
|
|
74
84
|
say(t('update.driftHeader'))
|
|
@@ -81,5 +91,5 @@ export async function runUpdate(root, opts = {}) {
|
|
|
81
91
|
say(t('update.driftHint'))
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
return { results, drift, record: { ...record, managed } }
|
|
94
|
+
return { results, drift, failed, record: { ...record, managed } }
|
|
85
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rch4com/agent-setup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Repository-scoped bootstrap and optional-item installer for using multiple coding-agent CLIs in one repository | 여러 코딩 에이전트 CLI를 한 저장소에서 함께 쓰기 위한 저장소 범위 부트스트랩과 선택 항목 설치기",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents-md",
|