@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 CHANGED
@@ -125,7 +125,11 @@ async function main() {
125
125
  // 살아남는다. 조용히 되돌아가는 대신 화면이 그 사실을 알리게 한다.
126
126
  const localeForced = isLocaleForced({ flag, env: process.env })
127
127
 
128
- if (root === null) throw new LocalizedError('error.notGitRepo')
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
  })
@@ -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}',
@@ -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 parse(text)
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(parse(text), path) === undefined) return
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
- export async function runStatus(root, { json = false, log = console.log, t = createT('en') } = {}) {
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.19.0",
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",