@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.
- package/LICENSE +21 -21
- package/README.md +151 -3
- package/install.mjs +102 -33
- package/lib/args.mjs +104 -94
- package/lib/bootstrap/adapter.mjs +96 -95
- package/lib/bootstrap/apply.mjs +380 -269
- package/lib/bootstrap/flow.mjs +73 -54
- package/lib/bootstrap/record.mjs +154 -0
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -0
- 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 -0
- 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 -0
- package/lib/width.mjs +57 -0
- package/package.json +36 -36
- package/lib/items/.omc/state/sessions/a859aff7-59d9-4ff6-afc6-8cafe4471dc4/pre-tool-advisory-throttle.json +0 -14
package/lib/tui/render.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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('
|
|
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 =
|
|
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 ? '
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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('
|
|
168
|
+
lines.push(paint(DIM, cut(t('tui.review.hint'), w)))
|
|
191
169
|
return lines
|
|
192
170
|
}
|