@rch4com/agent-setup 1.0.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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +151 -3
  3. package/install.mjs +102 -33
  4. package/lib/args.mjs +104 -94
  5. package/lib/bootstrap/adapter.mjs +96 -95
  6. package/lib/bootstrap/apply.mjs +380 -269
  7. package/lib/bootstrap/flow.mjs +73 -54
  8. package/lib/bootstrap/record.mjs +154 -0
  9. package/lib/bootstrap/templates.mjs +148 -148
  10. package/lib/bootstrap/text.mjs +19 -0
  11. package/lib/catalog.mjs +213 -145
  12. package/lib/claude-plugins.mjs +64 -64
  13. package/lib/clis.mjs +102 -102
  14. package/lib/context.mjs +66 -65
  15. package/lib/deps.mjs +5 -7
  16. package/lib/design-md/catalog.json +454 -454
  17. package/lib/design-md/catalog.mjs +266 -268
  18. package/lib/design-md/flow.mjs +181 -166
  19. package/lib/design-md/open.mjs +51 -50
  20. package/lib/design-md/providers/awesome-design-md.mjs +117 -114
  21. package/lib/design-md/providers/index.mjs +9 -9
  22. package/lib/design-md/scan.mjs +14 -10
  23. package/lib/engine.mjs +48 -44
  24. package/lib/gitignore.mjs +18 -18
  25. package/lib/i18n/catalog/en.mjs +369 -0
  26. package/lib/i18n/catalog/ko.mjs +359 -0
  27. package/lib/i18n/detect.mjs +39 -0
  28. package/lib/i18n/index.mjs +93 -0
  29. package/lib/items/mcp.codebase-memory.mjs +6 -6
  30. package/lib/items/mcp.graphify.mjs +8 -0
  31. package/lib/items/mcp.headroom.mjs +8 -0
  32. package/lib/items/mcp.notion.mjs +6 -6
  33. package/lib/items/mcp.supabase.mjs +6 -6
  34. package/lib/items/mcp.vercel.mjs +6 -6
  35. package/lib/items/plugin.bkit.mjs +7 -7
  36. package/lib/items/plugin.ecc.mjs +11 -0
  37. package/lib/items/plugin.impeccable.mjs +14 -0
  38. package/lib/items/plugin.mattpocock-skills.mjs +2 -2
  39. package/lib/items/plugin.ponytail.mjs +104 -0
  40. package/lib/items/plugin.superpowers.mjs +7 -7
  41. package/lib/items/plugin.understand-anything.mjs +10 -0
  42. package/lib/items/skill.caveman.mjs +7 -0
  43. package/lib/items/skill.gsd.mjs +25 -24
  44. package/lib/items/skill.gstack.mjs +40 -39
  45. package/lib/items/skill.karpathy.mjs +7 -0
  46. package/lib/items/skill.taste.mjs +9 -0
  47. package/lib/jsonfile.mjs +40 -40
  48. package/lib/status.mjs +93 -0
  49. package/lib/tomlfile.mjs +60 -60
  50. package/lib/tui/render.mjs +41 -63
  51. package/lib/tui/rows.mjs +278 -206
  52. package/lib/tui/run.mjs +64 -28
  53. package/lib/update.mjs +85 -0
  54. package/lib/width.mjs +57 -0
  55. package/package.json +36 -36
  56. package/lib/items/.omc/state/sessions/a859aff7-59d9-4ff6-afc6-8cafe4471dc4/pre-tool-advisory-throttle.json +0 -14
@@ -1,6 +1,15 @@
1
1
  // 순수 렌더 — 상태와 크기를 받아 화면 줄 배열을 돌려준다.
2
2
  // 커서 이동·지우기 같은 제어 시퀀스는 run.mjs가 맡는다.
3
3
  import { displayList, tabCounts, activeTab } from './state.mjs'
4
+ import { CLI_IDS } from '../clis.mjs'
5
+ import { createT } from '../i18n/index.mjs'
6
+ import { categoryLabel } from '../design-md/flow.mjs'
7
+ import { cut, pad, width } from '../width.mjs'
8
+
9
+ // 폭 계산은 화면 전용이 아니다 — 비대화형 목록도 같은 열 맞춤이 필요해
10
+ // lib/width.mjs에 있다. 여기서 다시 내보내는 것은 호출부(테스트 포함)가
11
+ // "화면 폭"을 render에서 찾는 기존 습관을 깨지 않기 위해서다.
12
+ export { cut, width }
4
13
 
5
14
  const ESC = String.fromCharCode(27)
6
15
  const DIM = `${ESC}[2m`
@@ -10,53 +19,12 @@ const RESET = `${ESC}[0m`
10
19
 
11
20
  // 머리글·탭줄·검색줄·구분 공백·바닥글이 차지하는 줄 수.
12
21
  const CHROME = 6
13
- const LABEL_WIDTH = 24
22
+ export const LABEL_WIDTH = 24
14
23
 
15
24
  export function bodyHeight(height) {
16
25
  return Math.max(3, height - CHROME)
17
26
  }
18
27
 
19
- // 동아시아 글자는 터미널에서 두 칸을 차지한다. 전체 wcwidth 표 대신
20
- // 실제로 쓰이는 구간(한글·한자·가나·전각)만 넓게 센다 — 액션 행 라벨이 전부 한글이라
21
- // 한 칸씩 밀리면 첫 화면부터 열이 어긋난다.
22
- function charWidth(cp) {
23
- return (cp >= 0x1100 && cp <= 0x115f) // 한글 자모
24
- || (cp >= 0x2e80 && cp <= 0xa4cf) // 한자 부수·가나·한자
25
- || (cp >= 0xac00 && cp <= 0xd7a3) // 한글 음절
26
- || (cp >= 0xf900 && cp <= 0xfaff) // 한자 호환
27
- || (cp >= 0xfe30 && cp <= 0xfe6f) // 전각 형태
28
- || (cp >= 0xff00 && cp <= 0xff60) // 전각 영숫자
29
- || (cp >= 0xffe0 && cp <= 0xffe6)
30
- ? 2 : 1
31
- }
32
-
33
- export function width(text) {
34
- let w = 0
35
- for (const ch of String(text ?? '')) w += charWidth(ch.codePointAt(0))
36
- return w
37
- }
38
-
39
- // 색 코드는 폭에 포함되면 안 되므로, 자르기는 항상 색을 입히기 **전에** 한다.
40
- export function cut(text, limit) {
41
- if (limit <= 0) return ''
42
- const s = String(text ?? '')
43
- if (width(s) <= limit) return s
44
- let out = ''
45
- let w = 0
46
- for (const ch of s) {
47
- const cw = charWidth(ch.codePointAt(0))
48
- if (w + cw > limit - 1) break
49
- out += ch
50
- w += cw
51
- }
52
- return `${out}…`
53
- }
54
-
55
- function pad(text, limit) {
56
- const s = cut(text, limit)
57
- return s + ' '.repeat(Math.max(0, limit - width(s)))
58
- }
59
-
60
28
  const MARK = { action: '▶', on: '×', off: ' ' }
61
29
 
62
30
  function checkbox(row, selected) {
@@ -67,21 +35,26 @@ function checkbox(row, selected) {
67
35
  // 탭 줄. 검색 중이면 탭마다 적중 수를 보여 준다 —
68
36
  // 검색은 활성 탭 안으로만 걸리므로, 다른 탭에 결과가 있다는 사실을 여기서 알린다.
69
37
  // 폭이 모자라면 활성 탭 하나 + 위치 표시로 줄인다(줄바꿈은 화면을 무너뜨린다).
70
- export function tabBar(state, { width: limit, color = false, searching = false } = {}) {
38
+ // tab 자체는 rows.mjs가 만든 소문자 id다 — 화면에는 t로 번역한 이름만 낸다.
39
+ export function tabBar(state, { width: limit, color = false, searching = false, t = createT('en') } = {}) {
71
40
  const counts = tabCounts(state)
72
41
  if (counts.length === 0) return ''
73
42
  const active = activeTab(state)
74
43
 
75
44
  const segs = counts.map(({ tab, shown, total }) => ({
76
45
  tab,
77
- text: searching ? `${tab} ${shown}/${total}` : `${tab} ${total}`,
46
+ text: searching ? `${t(`section.${tab}`)} ${shown}/${total}` : `${t(`section.${tab}`)} ${total}`,
78
47
  active: tab === active,
79
48
  empty: searching && shown === 0,
80
49
  }))
81
50
 
82
51
  const SEP = ' '
83
52
  const plain = segs.map((s) => s.text).join(SEP)
84
- if (width(plain) > limit) {
53
+ // 실제로 그려지는 줄은 plain보다 늘 2칸 더 넓다 — 세그먼트 사이의 구분 공백(SEP)이
54
+ // 아니라 각 세그먼트 앞뒤의 표시(활성은 대괄호, 나머지는 공백)가 SEP 하나만큼을
55
+ // 대체하고 그 위에 1칸씩 더 얹기 때문이다(세그먼트 수와 무관하게 항상 +2).
56
+ // 이 2칸을 빼먹으면 영어처럼 긴 라벨에서 줄바꿈을 놓친다.
57
+ if (width(plain) + 2 > limit) {
85
58
  const i = segs.findIndex((s) => s.active)
86
59
  const compact = `‹ ${segs[i]?.text ?? ''} › ${i + 1}/${segs.length}`
87
60
  return color ? `${BOLD}${cut(compact, limit)}${RESET}` : cut(compact, limit)
@@ -95,20 +68,20 @@ export function tabBar(state, { width: limit, color = false, searching = false }
95
68
  // 검색줄 = 하나의 입력칸이다. 포커스가 여기 있으면 입력 커서(▌)로 드러내고,
96
69
  // 컬러에서는 줄 전체를 반전시켜 "지금 여기에 타이핑된다"를 분명히 한다 —
97
70
  // 이 상태에서만 스페이스가 선택이 아니라 검색어로 들어가기 때문이다.
98
- function searchLine(state, { limit, color, paint }) {
99
- const prefix = '검색 › '
71
+ function searchLine(state, { limit, color, paint, t }) {
72
+ const prefix = t('tui.search.prefix')
100
73
  const room = Math.max(0, limit - width(prefix))
101
74
  if (state.focus === 'search') {
102
75
  const text = `${prefix}${cut(`${state.query}▌`, room)}`
103
76
  return color ? `${REVERSE}${pad(text, limit)}${RESET}` : text
104
77
  }
105
78
  if (state.query) return `${prefix}${cut(state.query, room)}`
106
- return `${prefix}${paint(DIM, cut('타이핑하면 검색 · ↓ 로 목록', room))}`
79
+ return `${prefix}${paint(DIM, cut(t('tui.search.placeholder'), room))}`
107
80
  }
108
81
 
109
82
  export function render(state, opts = {}) {
110
83
  // columns로 받는다 — width로 두면 모듈의 width() 함수를 함수 스코프 전체에서 가린다.
111
- const { width: columns = 80, height = 24, repo = '', dryRun = false, color = false, status = '' } = opts
84
+ const { width: columns = 80, height = 24, repo = '', dryRun = false, color = false, status = '', t = createT('en') } = opts
112
85
 
113
86
  // 마지막 칸은 비워 둔다 — 폭을 꽉 채우면 터미널이 줄을 넘긴다.
114
87
  const w = Math.max(24, columns - 1)
@@ -118,14 +91,14 @@ export function render(state, opts = {}) {
118
91
  const picked = items.filter((r) => state.selected.has(r.id)).length
119
92
 
120
93
  const title = `agent-installer${dryRun ? ' (dry-run)' : ''}`
121
- const counts = `선택 ${picked} / 전체 ${items.length}`
94
+ const counts = t('tui.counts', { picked, total: items.length })
122
95
  const head = cut(`${title} ${counts} ${repo}`, w)
123
96
  const searching = String(state.query ?? '').trim() !== ''
124
97
 
125
98
  const lines = [
126
99
  color ? `${BOLD}${title}${RESET}${cut(` ${counts} ${repo}`, Math.max(0, w - width(title)))}` : head,
127
- tabBar(state, { width: w, color, searching }),
128
- searchLine(state, { limit: w, color, paint }),
100
+ tabBar(state, { width: w, color, searching, t }),
101
+ searchLine(state, { limit: w, color, paint, t }),
129
102
  '',
130
103
  ]
131
104
 
@@ -133,14 +106,17 @@ export function render(state, opts = {}) {
133
106
  const all = displayList(state)
134
107
 
135
108
  if (all.length === 0) {
136
- lines.push(paint(DIM, cut(searching ? ' 이 탭에는 일치하는 항목이 없습니다. Tab으로 다른 탭을 보세요.' : ' 항목이 없습니다.', w)))
109
+ lines.push(paint(DIM, cut(searching ? t('tui.empty.filtered') : t('tui.empty.none'), w)))
137
110
  for (let i = 1; i < body; i++) lines.push('')
138
111
  } else {
139
112
  const window = all.slice(state.offset, state.offset + body)
140
113
  for (const entry of window) {
141
114
  if (entry.type === 'header') {
142
115
  const count = entry.shown === entry.total ? `${entry.total}` : `${entry.shown}/${entry.total}`
143
- lines.push(paint(DIM, cut(` ${entry.section} (${count})`, w)))
116
+ // entry.section은 사실 row.group(디자인 카테고리) 값이다. categoryLabel은
117
+ // 우리가 만든 catch-all(__other·__local)만 번역하고, 공급자가 준 실제
118
+ // 카테고리는 이미 영어 데이터라 그대로 통과시킨다.
119
+ lines.push(paint(DIM, cut(` ${categoryLabel(t, entry.section)} (${count})`, w)))
144
120
  continue
145
121
  }
146
122
  const { row, index } = entry
@@ -152,41 +128,43 @@ export function render(state, opts = {}) {
152
128
  for (let i = window.length; i < body; i++) lines.push('')
153
129
  }
154
130
 
155
- const hint = state.focus === 'search'
156
- ? '입력=검색어(스페이스 포함) ↓ 목록으로 Tab 탭이동 Esc 검색해제'
157
- : 'Space 선택 ↑↓ 이동(맨 위 ↑=검색칸) Tab 탭 Enter 실행/제출 Ctrl+A 전체 Ctrl+O 미리보기'
131
+ const hint = state.focus === 'search' ? t('tui.hint.search') : t('tui.hint.list')
158
132
  lines.push('')
159
133
  lines.push(paint(DIM, cut(status || hint, w)))
160
134
  return lines
161
135
  }
162
136
 
163
137
  const CHANGE_MARK = { install: '+', complete: '±', uninstall: '−' }
164
- const CHANGE_LABEL = { install: '설치', complete: '보완 설치', uninstall: '제거' }
165
138
 
166
139
  // 제출 검토 화면 — 적용 직전에 무엇이 바뀌는지만 보여 준다.
167
140
  // 목록이 길면 잘라내고 남은 건수를 알린다(스크롤 대신) — 여기서 길을 잃을 이유는 없다.
168
141
  export function renderReview(changes, opts = {}) {
169
- const { width: columns = 80, height = 24, dryRun = false, color = false } = opts
142
+ const { width: columns = 80, height = 24, dryRun = false, color = false, t = createT('en') } = opts
170
143
  const w = Math.max(24, columns - 1)
171
144
  const paint = (code, text) => (color ? `${code}${text}${RESET}` : text)
172
145
 
173
- const title = `제출 검토 — 변경 ${changes.length}건${dryRun ? ' (dry-run)' : ''}`
146
+ const title = `${t('tui.review.title', { count: changes.length })}${dryRun ? ' (dry-run)' : ''}`
174
147
  const lines = [color ? `${BOLD}${cut(title, w)}${RESET}` : cut(title, w), '']
175
148
 
176
149
  const body = bodyHeight(height)
177
150
  const room = Math.max(1, body - 1)
178
151
  const shown = changes.slice(0, room)
179
152
  for (const c of shown) {
180
- lines.push(cut(` ${CHANGE_MARK[c.action] ?? '?'} ${pad(CHANGE_LABEL[c.action] ?? c.action, 10)} ${c.item.label}`, w))
153
+ // 적용 직전 마지막 화면이다. 일부 CLI에만 들어가는 항목은 여기서도 그 사실을
154
+ // 밝힌다 — 목록에서 지나쳤더라도 되돌릴 수 있는 마지막 지점이다.
155
+ // 전부 지원하는 항목은 조용히 둔다: 경고가 흔해지면 아무도 읽지 않는다.
156
+ const partial = c.item.supports && c.item.supports.length < CLI_IDS.length
157
+ const cov = partial ? ` · ${t('item.cliCoverage', { covered: c.item.supports.length, total: CLI_IDS.length })}` : ''
158
+ lines.push(cut(` ${CHANGE_MARK[c.action] ?? '?'} ${pad(t(`change.${c.action}`), 10)} ${c.item.label}${cov}`, w))
181
159
  }
182
160
  if (changes.length > shown.length) {
183
- lines.push(paint(DIM, cut(` …외 ${changes.length - shown.length}건`, w)))
161
+ lines.push(paint(DIM, cut(t('tui.review.more', { count: changes.length - shown.length }), w)))
184
162
  } else {
185
163
  lines.push('')
186
164
  }
187
165
  for (let i = shown.length + 1; i < body; i++) lines.push('')
188
166
 
189
167
  lines.push('')
190
- lines.push(paint(DIM, cut('Enter 적용 Esc 취소', w)))
168
+ lines.push(paint(DIM, cut(t('tui.review.hint'), w)))
191
169
  return lines
192
170
  }