@rch4com/agent-setup 1.7.0 → 1.8.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.
@@ -184,8 +184,11 @@ export function defineGitmessage({ id, label, body, note }) {
184
184
  category: 'config',
185
185
  label,
186
186
  scope: 'project',
187
- group: '__flow',
187
+ group: '__commit',
188
188
  exclusive: GITMESSAGE_EXCLUSIVE,
189
+ // 이 항목이 손대는 파일. 그룹 헤더가 없는 평평한 목록에서도 무엇이
190
+ // 놓이는지 보이고, '.gitmessage'로 검색해도 걸린다.
191
+ target: GITMESSAGE_REL,
189
192
  unsupported: {},
190
193
  note,
191
194
 
@@ -263,8 +263,8 @@ export default {
263
263
  'item.skill.gsd.note': 'npx @opengsd/gsd-core, installed per project. Upstream supports 18 runtimes (Codex, Antigravity, …); this item wires --claude only',
264
264
  // 커밋 템플릿은 CLI 배선이 아니라 저장소 규약이다 — note가 "무엇이 놓이고
265
265
  // 무엇이 설정되는가"를 대신 말한다(상세 패널에 배선표가 없다).
266
- 'item.config.gitmessage.en.note': 'writes .gitmessage.txt in English and points git commit.template at it. One template for every tool and person in this repository — pick English or Korean, not both',
267
- 'item.config.gitmessage.ko.note': 'writes .gitmessage.txt in Korean and points git commit.template at it. One template for every tool and person in this repository — pick English or Korean, not both',
266
+ 'item.config.gitmessage.en.note': 'commit messages in English. Writes the template and points git commit.template at it, so it opens in every editor and every tool in this repository',
267
+ 'item.config.gitmessage.ko.note': 'commit messages in Korean. Writes the template and points git commit.template at it, so it opens in every editor and every tool in this repository',
268
268
  'item.gitmessage.unregistered': 'file is in place, but commit.template does not point at it yet',
269
269
  'log.gitmessage.write': ' [dry-run] write {path}',
270
270
  'log.gitmessage.remove': ' [dry-run] remove {path}',
@@ -300,9 +300,11 @@ export default {
300
300
  'tui.empty.none': ' Nothing here.',
301
301
  'tui.hint.search': 'type to search (spaces included) ↓ to list Tab tab Esc clear Ctrl+F CLI Ctrl+D detail',
302
302
  'tui.hint.list': 'Space select ↑↓ move (↑ at top = search) Tab tab Enter run/submit Ctrl+A all Ctrl+O preview Ctrl+F CLI Ctrl+D detail',
303
+ // 힌트 줄 오른쪽 끝의 고정 자리. 나머지 힌트가 다 잘려 나가도 이 자리는 남는다.
304
+ 'tui.hint.quit': 'Ctrl+Q quit',
303
305
  'tui.review.title': 'Review — {count} change(s)',
304
306
  'tui.review.more': ' …and {count} more',
305
- 'tui.review.hint': 'Enter apply Esc cancel',
307
+ 'tui.review.hint': 'Enter apply Esc cancel Ctrl+Q quit',
306
308
  'tui.notItemRow': 'Press Enter to run this row.',
307
309
  'tui.noPreview': 'This item has no preview.',
308
310
  'tui.opened': 'Opened: {target}',
@@ -310,6 +312,8 @@ export default {
310
312
  'tui.submitCancelled': 'Submission cancelled.',
311
313
  'tui.toggledAll': 'Selected every visible item in the {tab} tab.',
312
314
  'tui.toggledNone': 'Cleared every visible item in the {tab} tab.',
315
+ // 배타 묶음에서 형제가 함께 꺼졌을 때. 조용히 뒤집히면 오조작으로 읽힌다.
316
+ 'tui.exclusiveSwitched': 'Switched to {kept} — they share one file, so {dropped} was cleared.',
313
317
  'tui.nonInteractive': 'The interactive screen only opens in a terminal. --list and --set work too.',
314
318
  'tui.pressAnyKey': 'Press any key to continue…',
315
319
  'tui.confirmSuffix': ' [y/N] ',
@@ -343,6 +347,7 @@ export default {
343
347
  'tui.filter.empty': ' Nothing in this tab is wired for {cli}.',
344
348
 
345
349
  'item.location.user': 'installs to: user global',
350
+ 'item.target': 'file: {path}',
346
351
  // 커버리지는 전부 지원할 때도 찍는다 — 표시가 없는 것과 "10/10"은 뜻이 다르다.
347
352
  'item.cliCoverage': 'CLI {covered}/{total}',
348
353
  'item.unsupportedList': 'not wired for {count}: {groups}',
@@ -351,6 +356,7 @@ export default {
351
356
  'detail.unwired': 'unwired',
352
357
  'detail.provider': 'source',
353
358
  'detail.preview': 'preview',
359
+ 'detail.target': 'file',
354
360
  'detail.scope.project': 'repo scope',
355
361
  'detail.scope.user': 'user global',
356
362
  'detail.more': ' …and {count} more line(s) — Ctrl+D to expand',
@@ -377,6 +383,9 @@ export default {
377
383
  'category.context': 'Code understanding',
378
384
  'category.style': 'Design taste',
379
385
  'category.flow': 'Way of working',
386
+ // 헤더 하나가 "무엇을 고르는 자리이고 규칙이 무엇인가"를 함께 말한다 —
387
+ // 대상 파일이 하나뿐인 배타 묶음이라 라디오 표시와 짝이 된다.
388
+ 'category.commit': 'Commit template · .gitmessage.txt · pick one',
380
389
  'category.service': 'External services',
381
390
  'design.localSuffix': '{id} (local)',
382
391
  'design.localTag': 'local',
@@ -255,8 +255,8 @@ export default {
255
255
  'item.skill.gstack.note': '저장소 로컬 clone + setup (bash 필요, Windows는 Git Bash). 런타임 상태(~/.gstack)는 전역에 생길 수 있음.',
256
256
  'item.skill.gsd.note': 'npx @opengsd/gsd-core 프로젝트 로컬 설치. 상류는 Codex·Antigravity 등 18개 런타임을 지원합니다(이 항목은 --claude)',
257
257
  // 커밋 템플릿은 CLI 배선이 아니라 저장소 규약이다 — en.mjs 쪽 주석 참고.
258
- 'item.config.gitmessage.en.note': '.gitmessage.txt를 영어판으로 쓰고 git commit.template이 그것을 가리키게 합니다. 저장소의 모든 도구와 사람이 함께 쓰는 한 벌이라 영어·한국어 중 하나만 고릅니다',
259
- 'item.config.gitmessage.ko.note': '.gitmessage.txt를 한국어판으로 쓰고 git commit.template이 그것을 가리키게 합니다. 저장소의 모든 도구와 사람이 함께 쓰는 한 벌이라 영어·한국어 중 하나만 고릅니다',
258
+ 'item.config.gitmessage.en.note': '커밋 메시지를 영어로 씁니다. 템플릿을 놓고 git commit.template이 그것을 가리키게 하므로, 저장소의 모든 편집기와 도구에서 함께 열립니다',
259
+ 'item.config.gitmessage.ko.note': '커밋 메시지를 한국어로 씁니다. 템플릿을 놓고 git commit.template이 그것을 가리키게 하므로, 저장소의 모든 편집기와 도구에서 함께 열립니다',
260
260
  'item.gitmessage.unregistered': '파일은 놓였지만 commit.template이 아직 가리키지 않습니다',
261
261
  'log.gitmessage.write': ' [dry-run] {path} 생성',
262
262
  'log.gitmessage.remove': ' [dry-run] {path} 제거',
@@ -290,9 +290,11 @@ export default {
290
290
  'tui.empty.none': ' 항목이 없습니다.',
291
291
  'tui.hint.search': '입력=검색어 ↓ 목록으로 Tab 탭이동 Esc 검색해제 Ctrl+F CLI Ctrl+D 상세',
292
292
  'tui.hint.list': 'Space 선택 ↑↓ 이동 Tab 탭 Enter 실행/제출 Ctrl+A 전체 Ctrl+O 미리보기 Ctrl+F CLI Ctrl+D 상세',
293
+ // 힌트 줄 오른쪽 끝의 고정 자리. en.mjs 쪽 주석 참고.
294
+ 'tui.hint.quit': 'Ctrl+Q 종료',
293
295
  'tui.review.title': '제출 검토 — 변경 {count}건',
294
296
  'tui.review.more': ' …외 {count}건',
295
- 'tui.review.hint': 'Enter 적용 Esc 취소',
297
+ 'tui.review.hint': 'Enter 적용 Esc 취소 Ctrl+Q 종료',
296
298
  'tui.notItemRow': '이 행은 Enter로 실행합니다.',
297
299
  'tui.noPreview': '이 항목은 미리보기를 제공하지 않습니다.',
298
300
  'tui.opened': '열었습니다: {target}',
@@ -300,6 +302,8 @@ export default {
300
302
  'tui.submitCancelled': '제출을 취소했습니다.',
301
303
  'tui.toggledAll': '{tab} 탭의 보이는 항목을 모두 선택했습니다.',
302
304
  'tui.toggledNone': '{tab} 탭의 보이는 항목을 모두 해제했습니다.',
305
+ // 배타 묶음에서 형제가 함께 꺼졌을 때. en.mjs 쪽 주석 참고.
306
+ 'tui.exclusiveSwitched': '{kept}(으)로 바꿨습니다 — 같은 파일이라 {dropped}은(는) 해제했습니다.',
303
307
  'tui.nonInteractive': '대화형 화면은 터미널에서만 열립니다. --list · --set 으로도 다룰 수 있습니다.',
304
308
  'tui.pressAnyKey': '계속하려면 아무 키나 누르세요…',
305
309
  'tui.confirmSuffix': ' [y/N] ',
@@ -330,6 +334,7 @@ export default {
330
334
  'tui.filter.empty': ' 이 탭에는 {cli}에 배선되는 항목이 없습니다.',
331
335
 
332
336
  'item.location.user': '설치 위치: 사용자 글로벌',
337
+ 'item.target': '대상 파일: {path}',
333
338
  // en.mjs 쪽 주석 참고.
334
339
  'item.cliCoverage': 'CLI {covered}/{total}',
335
340
  'item.unsupportedList': '미배선 {count}곳: {groups}',
@@ -338,6 +343,7 @@ export default {
338
343
  'detail.unwired': '미배선',
339
344
  'detail.provider': '공급자',
340
345
  'detail.preview': '미리보기',
346
+ 'detail.target': '대상 파일',
341
347
  'detail.scope.project': '저장소 스코프',
342
348
  'detail.scope.user': '사용자 전역',
343
349
  'detail.more': ' …외 {count}줄 — Ctrl+D로 펼치기',
@@ -363,6 +369,8 @@ export default {
363
369
  'category.context': '코드 이해',
364
370
  'category.style': '디자인 감각',
365
371
  'category.flow': '작업 방식',
372
+ // 헤더 하나가 무엇을 고르는 자리이고 규칙이 무엇인지 함께 말한다. en.mjs 쪽 주석 참고.
373
+ 'category.commit': '커밋 메시지 템플릿 · .gitmessage.txt · 하나만 고릅니다',
366
374
  'category.service': '외부 서비스',
367
375
  'design.localSuffix': '{id} (로컬)',
368
376
  'design.localTag': '로컬',
@@ -2,9 +2,9 @@ import { defineGitmessage, GITMESSAGE_EN } from '../gitmessage.mjs'
2
2
 
3
3
  export default defineGitmessage({
4
4
  id: 'config.gitmessage.en',
5
- // 목록의 라벨 자리는 24칸이다 — 'Commit template (English)'는 한 칸이 넘쳐
6
- // 잘린다. 이 도구가 이미 쓰는 --lang en|ko 표기를 따르고, 전문은 note가 편다.
7
- label: 'Commit template (EN)',
5
+ // 두 판을 가르는 낱말을 맨 앞에 둔다 — 라벨 자리(24칸)에서 눈이 먼저 닿는
6
+ // 곳이고, 그룹 헤더가 이미 "커밋 템플릿"을 말하고 있어 뒤쪽은 확인용이다.
7
+ label: 'English commit template',
8
8
  body: GITMESSAGE_EN,
9
9
  note: 'item.config.gitmessage.en.note',
10
10
  })
@@ -4,8 +4,7 @@ export default defineGitmessage({
4
4
  id: 'config.gitmessage.ko',
5
5
  // 라벨은 우리 카탈로그 데이터라 두 로케일에서 같은 값이 나간다 — 영어 화면에
6
6
  // 한글('한국어')이 새면 그 자리는 번역 누락으로 읽힌다(i18n.en.test.mjs).
7
- // 어느 언어판인지는 라벨의 (EN)·(KO)가, 무엇이 놓이는지는 note가 말한다.
8
- label: 'Commit template (KO)',
7
+ label: 'Korean commit template',
9
8
  body: GITMESSAGE_KO,
10
9
  note: 'item.config.gitmessage.ko.note',
11
10
  })
@@ -8,7 +8,7 @@
8
8
  import { toText, createT } from '../i18n/index.mjs'
9
9
  import { CLIS } from '../clis.mjs'
10
10
  import { categoryLabel } from '../design-md/flow.mjs'
11
- import { cut, pad, width, wrap } from '../width.mjs'
11
+ import { cut, labelWidth, pad, width, wrap } from '../width.mjs'
12
12
 
13
13
  // 미배선 사유를 **같은 사유끼리** 묶는다. 사유 하나가 CLI 아홉 개에 그대로
14
14
  // 반복되면 줄만 길어지고 "무엇이 왜 빠졌는가"는 오히려 묻힌다.
@@ -25,10 +25,16 @@ export function unsupportedGroups(item, t) {
25
25
  return [...byReason].map(([why, clis]) => ({ clis, why }))
26
26
  }
27
27
 
28
- // '배선 ' / '미배선 ' 라벨 자리. 로케일마다 길이가 달라 상수로 박으면 열이
29
- // 어긋난다 — 그 로케일의 실제 라벨에서 폭을 뽑는다.
28
+ // 왼쪽 라벨 열('배선'·'미배선'·'출처'·'미리보기'·'대상 파일')의 폭. 로케일마다
29
+ // 길이가 달라 상수로 박으면 열이 어긋나므로 그 로케일의 실제 라벨에서 뽑는다.
30
+ //
31
+ // **이 열에 놓이는 라벨을 하나도 빠뜨리면 안 된다.** 예전에는 배선·미배선만
32
+ // 재서, 한국어의 '미리보기'(8칸)가 폭 7의 열에 들어가 '미리…'로 잘렸다 —
33
+ // pad는 넘치는 글을 cut으로 버리기 때문에 조용히 사라진다.
34
+ const LEAD_KEYS = ['detail.wired', 'detail.unwired', 'detail.provider', 'detail.preview', 'detail.target']
35
+
30
36
  function leadWidth(t) {
31
- return Math.max(width(t('detail.wired')), width(t('detail.unwired'))) + 1
37
+ return labelWidth(t, LEAD_KEYS) + 1
32
38
  }
33
39
 
34
40
  function headLine(row, w, t) {
@@ -75,6 +81,9 @@ function agentLines(row, w, t) {
75
81
  const item = row.item
76
82
  const lead = leadWidth(t)
77
83
  const out = [headLine(row, w, t)]
84
+ // 대상 파일은 note보다 먼저 낸다 — MCP의 배선표가 "어느 파일에 쓰이나"를
85
+ // 답하는 자리와 같은 몫이고, 산문보다 먼저 눈에 들어와야 한다.
86
+ if (item.target) out.push(cut(`${pad(t('detail.target'), lead)}${item.target}`, w))
78
87
  if (item.note) out.push(...wrap(t(item.note), w))
79
88
  if (row.statusDetail) out.push(...wrap(row.statusDetail, w))
80
89
  out.push(...wiredLines(item, w, lead, t))
@@ -55,9 +55,15 @@ export function reviewBodyHeight(height) {
55
55
 
56
56
  const MARK = { action: '▶', on: '×', off: ' ' }
57
57
 
58
+ // 대괄호는 "여러 개 고를 수 있다"는 보편적 신호다 — 한 파일에 한 벌만 놓이는
59
+ // 배타 묶음(커밋 템플릿의 두 언어판)에 그대로 쓰면 화면이 거짓말을 한다.
60
+ // 둥근 괄호로 바꿔 라디오임을 드러낸다. 안쪽 글자는 그대로 둔다: ×는 이미
61
+ // 이 화면이 쓰는 글자라 폭 계산이 검증된 값이고, ●·•는 동아시아 폭이
62
+ // 모호(Ambiguous)해 터미널에 따라 두 칸으로 그려져 열을 밀 수 있다.
58
63
  function checkbox(row, selected) {
59
64
  if (row.kind === 'action') return `[${MARK.action}]`
60
- return `[${selected.has(row.id) ? MARK.on : MARK.off}]`
65
+ const mark = selected.has(row.id) ? MARK.on : MARK.off
66
+ return row.exclusive ? `(${mark})` : `[${mark}]`
61
67
  }
62
68
 
63
69
  // 탭 줄. 목록이 좁혀졌으면(검색어·CLI 필터 어느 쪽이든) 탭마다 적중 수를
@@ -211,9 +217,18 @@ export function render(state, opts = {}) {
211
217
  for (let i = shown.length; i < room; i++) lines.push('')
212
218
  }
213
219
 
220
+ // 종료 키는 힌트 줄 **오른쪽 끝에 고정**한다. 나머지 힌트는 좁은 터미널에서
221
+ // 뒤쪽부터 잘려 나가는데, 하필 그때 가장 절실한 것이 "여기서 어떻게
222
+ // 빠져나가나"다 — 잘려도 되는 안내와 절대 사라지면 안 되는 안내를 같은
223
+ // 줄에 이어 붙여 두면 늘 후자가 먼저 사라진다. 상태 메시지가 힌트를
224
+ // 대신할 때도 이 자리는 그대로 남는다.
214
225
  const hint = state.focus === 'search' ? t('tui.hint.search') : t('tui.hint.list')
226
+ const quit = t('tui.hint.quit')
227
+ const room = w - width(quit) - 2
215
228
  lines.push('')
216
- lines.push(paint(DIM, cut(status || hint, w)))
229
+ lines.push(room < 8
230
+ ? paint(BOLD, cut(quit, w))
231
+ : `${paint(DIM, pad(status || hint, room))} ${paint(BOLD, quit)}`)
217
232
  return lines
218
233
  }
219
234
 
package/lib/tui/rows.mjs CHANGED
@@ -26,7 +26,10 @@ export const SECTION_ORDER = [ACTION_SECTION, 'plugin', 'mcp', 'skill', 'config'
26
26
  // "무엇을 해 주는가"가 다른 항목이 섞여 있어서, 기구(plugin·mcp·skill)만으로는
27
27
  // 고를 때 비교가 되지 않는다. 표시 순서는 이 배열이 정한다.
28
28
  // `__`로 시작하는 id만 categoryLabel이 번역한다(design.md 카테고리와 같은 규칙).
29
- export const GROUP_ORDER = ['__token', '__context', '__style', '__flow', '__service']
29
+ // __commit은 CONFIG 탭에만 나오지만 규칙은 같다 — 헤더가 "무엇을 고르는
30
+ // 자리인가"를 말한다. 커밋 템플릿은 대상 파일이 하나뿐이라 헤더에 그 파일
31
+ // 이름과 "하나만"이라는 규칙까지 실어, 라디오 표시와 함께 읽히게 한다.
32
+ export const GROUP_ORDER = ['__token', '__context', '__style', '__flow', '__commit', '__service']
30
33
 
31
34
  function groupRank(group) {
32
35
  const i = GROUP_ORDER.indexOf(group)
@@ -75,6 +78,10 @@ export function agentHint(item, state, t = createT('en')) {
75
78
  if (item.supports) parts.push(t('item.cliCoverage', { covered: item.supports.length, total: CLI_IDS.length }))
76
79
  const detail = toText(t, state.detail)
77
80
  if (detail) parts.push(detail)
81
+ // 대상 파일이 하나로 정해진 항목만 target을 갖는다. 평평한 목록(printPlain·
82
+ // --list)에는 그룹 헤더가 없어 무엇을 건드리는지 알 길이 없고, 검색어로도
83
+ // '.gitmessage'가 걸리지 않았다.
84
+ if (item.target) parts.push(t('item.target', { path: item.target }))
78
85
  if (item.scope === 'user') parts.push(t('item.location.user'))
79
86
  // note는 이제 카탈로그 키다.
80
87
  if (item.note) parts.push(t(item.note))
package/lib/tui/run.mjs CHANGED
@@ -176,7 +176,13 @@ export async function runTui(root, opts = {}) {
176
176
  const select = () => {
177
177
  const row = currentRow(state)
178
178
  if (row?.kind !== 'item') return t('tui.notItemRow')
179
+ const before = state.selected
179
180
  state = toggle(state)
181
+ // 배타 묶음에서 형제가 함께 꺼졌으면 그 사실을 말한다. 체크가 조용히
182
+ // 뒤집히면 사용자는 자기가 잘못 눌렀다고 읽는다 — 검색으로 형제가
183
+ // 화면 밖에 있을 때는 아예 무슨 일이 일어났는지 볼 수조차 없다.
184
+ const dropped = state.rows.filter((r) => r.id !== row.id && before.has(r.id) && !state.selected.has(r.id))
185
+ if (dropped.length > 0) return t('tui.exclusiveSwitched', { kept: row.label, dropped: dropped.map((r) => r.label).join(', ') })
180
186
  return ''
181
187
  }
182
188
 
@@ -194,7 +200,7 @@ export async function runTui(root, opts = {}) {
194
200
  for (;;) {
195
201
  draw(renderReview(changes, { width: stdout.columns ?? 80, height: stdout.rows ?? 24, dryRun, color, t }))
196
202
  const key = await keys.next()
197
- if (key.ctrl && key.name === 'c') return 'quit'
203
+ if (key.ctrl && (key.name === 'c' || key.name === 'q')) return 'quit'
198
204
  if (key.name === 'escape') return 'cancel'
199
205
  if (key.name === 'return' || key.name === 'enter') return 'apply'
200
206
  }
@@ -260,6 +266,14 @@ export async function runTui(root, opts = {}) {
260
266
  // 필터를 겹쳐 걸고 싶은 순간이다. 글자 키(c·d)를 쓰지 않는 이유는
261
267
  // 목록 포커스에서 아무 글자나 누르면 검색칸으로 올라가기 때문이다:
262
268
  // c를 필터에 배정하면 codex·claude를 검색어로 칠 수 없다.
269
+ // 종료 전용 키. Ctrl+C는 "중단"이고 Esc는 검색어·포커스를 먼저 되돌리느라
270
+ // 최대 세 번을 눌러야 나간다 — 어느 쪽도 "나가는 키"로 배우기 어렵다.
271
+ // Ctrl+Q는 두 포커스와 검토 화면에서 한 번에 통하며, 화면 오른쪽 끝에
272
+ // 늘 표시된다(render의 고정 자리). 글자 q를 쓰지 않는 이유는 Ctrl+F·
273
+ // Ctrl+D와 같다: 목록에서 아무 글자나 누르면 검색칸으로 올라간다.
274
+ // raw 모드가 IXON(흐름 제어)을 끄므로 Ctrl+Q는 터미널에 먹히지 않는다.
275
+ if (key.ctrl && key.name === 'q') break
276
+
263
277
  if (key.ctrl && (key.name === 'f' || key.name === 'b')) {
264
278
  state = cycleCliFilter(state, key.name === 'f' ? 1 : -1, CLI_OPTIONS)
265
279
  continue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rch4com/agent-setup",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",