@rch4com/agent-setup 1.4.0 → 1.6.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 +11 -1
- package/lib/bootstrap/flow.mjs +75 -8
- package/lib/catalog.mjs +36 -15
- package/lib/clis.mjs +15 -6
- package/lib/design-md/flow.mjs +12 -1
- package/lib/engine.mjs +57 -5
- package/lib/i18n/catalog/en.mjs +41 -5
- package/lib/i18n/catalog/ko.mjs +38 -5
- package/lib/items/plugin.bkit.mjs +16 -0
- package/lib/items/plugin.impeccable.mjs +15 -1
- package/lib/items/plugin.ponytail.mjs +3 -3
- package/lib/items/plugin.superpowers.mjs +16 -0
- package/lib/items/skill.gsd.mjs +22 -3
- package/lib/items/skill.gstack.mjs +18 -4
- package/lib/tui/detail.mjs +118 -0
- package/lib/tui/progress.mjs +160 -0
- package/lib/tui/render.mjs +103 -20
- package/lib/tui/rows.mjs +41 -14
- package/lib/tui/run.mjs +101 -12
- package/lib/tui/state.mjs +39 -9
- package/lib/width.mjs +58 -0
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -52,6 +52,7 @@ async function openTui(root, { dryRun, skillMode, designDirs, t, localeForced })
|
|
|
52
52
|
async function runClassic(root, { dryRun, listOnly, setArg, t }) {
|
|
53
53
|
const { loadItems } = await withDeps(() => import('./lib/catalog.mjs'), t)
|
|
54
54
|
const { scan, planChanges, apply } = await withDeps(() => import('./lib/engine.mjs'), t)
|
|
55
|
+
const { plainLine } = await withDeps(() => import('./lib/tui/progress.mjs'), t)
|
|
55
56
|
const items = await loadItems()
|
|
56
57
|
const states = await scan(root, items)
|
|
57
58
|
const statusWidth = labelWidth(t, STATUS_KEYS)
|
|
@@ -71,7 +72,16 @@ async function runClassic(root, { dryRun, listOnly, setArg, t }) {
|
|
|
71
72
|
const changes = planChanges(states, selectedIds)
|
|
72
73
|
if (changes.length === 0) { console.log(t('apply.noChanges')); return }
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// 비대화형 경로다 — 바를 그리지 않고 평문 한 줄씩 흘린다.
|
|
76
|
+
// ANSI 제어문자로 CI 로그를 더럽히지 않기 위해서다.
|
|
77
|
+
const results = await apply(root, changes, {
|
|
78
|
+
dryRun,
|
|
79
|
+
t,
|
|
80
|
+
onProgress: (event) => {
|
|
81
|
+
const line = plainLine(event, t)
|
|
82
|
+
if (line) console.log(line)
|
|
83
|
+
},
|
|
84
|
+
})
|
|
75
85
|
for (const r of results) {
|
|
76
86
|
console.log(`${r.ok ? '✔' : '✖'} ${t(`change.${r.action}`)} ${r.item.label}${r.message ? ` — ${toText(t, r.message)}` : ''}`)
|
|
77
87
|
}
|
package/lib/bootstrap/flow.mjs
CHANGED
|
@@ -6,11 +6,26 @@ import {
|
|
|
6
6
|
} from './apply.mjs'
|
|
7
7
|
import { RECORD_REL, collectManaged, emptyRecord, readRecord, writeRecord } from './record.mjs'
|
|
8
8
|
import { createT, LocalizedError, toText } from '../i18n/index.mjs'
|
|
9
|
+
import { width } from '../width.mjs'
|
|
9
10
|
|
|
10
11
|
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
11
12
|
|
|
13
|
+
// 오른쪽 정렬. width.mjs의 pad()는 오른쪽에 공백을 채워 왼쪽 정렬만 하므로
|
|
14
|
+
// 숫자에는 못 쓴다 — 자릿수가 9에서 10으로 늘 때 열이 흔들리지 않으려면
|
|
15
|
+
// 왼쪽에 채워야 한다. 숫자만 다루니 실제로는 문자 수와 같지만, 이 저장소의
|
|
16
|
+
// 다른 폭 계산과 방식을 맞추려고 width()로 잰다.
|
|
17
|
+
function alignRight(text, w) {
|
|
18
|
+
return ' '.repeat(Math.max(0, w - width(text))) + text
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// "[ 3/39]" 꼴의 단계 번호. total의 자릿수에 맞춰 n을 오른쪽 정렬한다.
|
|
22
|
+
function counterTag(n, total) {
|
|
23
|
+
const totalText = String(total)
|
|
24
|
+
return `[${alignRight(String(n), width(totalText))}/${totalText}]`
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
export function runBootstrap(root, opts = {}) {
|
|
13
|
-
const { dryRun = false, skillMode = 'auto', adopt = false, log = console.log, manifest = MANIFEST, t = createT('en') } = opts
|
|
28
|
+
const { dryRun = false, skillMode = 'auto', adopt = false, log = console.log, manifest = MANIFEST, onProgress = null, t = createT('en') } = opts
|
|
14
29
|
|
|
15
30
|
if (!SKILL_MODES.includes(skillMode)) {
|
|
16
31
|
throw new LocalizedError('error.badSkillModeRuntime', { list: SKILL_MODES.join(', '), value: skillMode })
|
|
@@ -22,23 +37,71 @@ export function runBootstrap(root, opts = {}) {
|
|
|
22
37
|
say(t('log.repoRoot', { path: root }))
|
|
23
38
|
say(t('log.noGlobalWrites'))
|
|
24
39
|
|
|
40
|
+
// 총 단계 수. 동기 파일 작업이라 단계 바만 돌지만, 무엇이 남았는지는
|
|
41
|
+
// 알려 줄 수 있다. adopt는 파일을 만들지 않으므로 기록 쓰기 한 단계뿐이다.
|
|
42
|
+
// 예측일 뿐이다 — ensureIgnore는 부정 항목이 기존 .gitignore에 가려지면
|
|
43
|
+
// manifest.ignore.length보다 많은 결과(경고 포함)를 돌려줄 수 있다.
|
|
44
|
+
let total = adopt
|
|
45
|
+
? 1
|
|
46
|
+
: manifest.dirs.length + manifest.files.length + (manifest.settings ?? []).length
|
|
47
|
+
+ manifest.blocks.length + manifest.adapters.length + manifest.ignore.length + 1
|
|
48
|
+
let done = 0
|
|
49
|
+
const step = (results) => {
|
|
50
|
+
const list = Array.isArray(results) ? results : [results]
|
|
51
|
+
for (const r of list) {
|
|
52
|
+
done++
|
|
53
|
+
// 실제 진행이 예측을 앞지르면 천장을 함께 올린다. total을 고정해 두면
|
|
54
|
+
// done > total인 알림이 나가 진행률이 100%를 넘고 바가 자기 너비를
|
|
55
|
+
// 넘겨 그려진다 — done을 total에 맞춰 죽이면 화면은 멈춘 것처럼 보이는데
|
|
56
|
+
// 남은 단계가 조용히 계속 도는, 더 나쁜 오류가 된다.
|
|
57
|
+
if (done > total) total = done
|
|
58
|
+
if (onProgress) onProgress({ done, total, path: r?.path ?? null })
|
|
59
|
+
}
|
|
60
|
+
return list
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 단계 안에서 나온 로그는 모아 뒀다가 step()이 done/total을 확정한 뒤에
|
|
64
|
+
// 번호를 붙여 낸다. ensureDirs는 이미 있는 항목을 건너뛸 때 아예 로그를
|
|
65
|
+
// 내지 않고, ensureIgnore는 여러 항목을 한 줄로 묶어 보고한다 — "몇 번째
|
|
66
|
+
// 줄이 찍혔는가"와 "몇 번째 항목을 처리했는가"가 그래서 어긋난다. 번호는
|
|
67
|
+
// 항목 순서가 아니라 처리 위치(done)를 보여 주는 것이 목적이므로, 이
|
|
68
|
+
// 구간이 시작한 위치(before)부터 실제로 찍힌 줄 수만큼만 순서대로 매긴다.
|
|
69
|
+
// 못 채운 자리는 건너뛰지만, 다음 구간은 항상 정확한 위치에서 이어져
|
|
70
|
+
// 마지막 줄은 반드시 total 가까이에서 끝난다.
|
|
71
|
+
//
|
|
72
|
+
// onProgress는 그대로 step()이 몬다 — 기계가 읽는 진행 이벤트와 사람이
|
|
73
|
+
// 읽는 로그 줄은 타이밍이 다르다(ensureX 호출이 끝나야 결과 배열이
|
|
74
|
+
// 나오고, 그제서야 done/total이 확정된다). 두 소비자를 하나의 흐름으로
|
|
75
|
+
// 우겨넣는 대신, 같은 done/total 계산을 공유하는 별도 경로로 뒀다.
|
|
76
|
+
const runStep = (run) => {
|
|
77
|
+
const buffer = []
|
|
78
|
+
const before = done
|
|
79
|
+
const list = step(run({ ...ctx, log: (message) => buffer.push(message) }))
|
|
80
|
+
let n = before
|
|
81
|
+
for (const message of buffer) {
|
|
82
|
+
n++
|
|
83
|
+
say(`${counterTag(n, total)} ${message}`)
|
|
84
|
+
}
|
|
85
|
+
return list
|
|
86
|
+
}
|
|
87
|
+
|
|
25
88
|
// --adopt는 이미 있는 저장소를 기록 체계로 끌어오는 용도라 파일을 만들지
|
|
26
89
|
// 않는다. 설치기를 복사해 쓰던 저장소가 여기로 들어온다.
|
|
27
90
|
const results = adopt ? [] : [
|
|
28
|
-
...ensureDirs(root, manifest.dirs,
|
|
29
|
-
...ensureFiles(root, manifest.files,
|
|
30
|
-
...ensureJsonKeys(root, manifest.settings ?? [],
|
|
31
|
-
...ensureBlocks(root, manifest.blocks,
|
|
91
|
+
...runStep((c) => ensureDirs(root, manifest.dirs, c)),
|
|
92
|
+
...runStep((c) => ensureFiles(root, manifest.files, c)),
|
|
93
|
+
...runStep((c) => ensureJsonKeys(root, manifest.settings ?? [], c)),
|
|
94
|
+
...runStep((c) => ensureBlocks(root, manifest.blocks, c)),
|
|
32
95
|
]
|
|
33
96
|
|
|
34
97
|
if (!adopt) {
|
|
35
98
|
// 어댑터는 항목별로 실패를 격리한다 — 하나가 실패해도 나머지를 계속한다.
|
|
36
99
|
// update.mjs도 같은 격리를 써야 하므로 apply.mjs가 단일 출처다.
|
|
37
100
|
for (const entry of manifest.adapters) {
|
|
38
|
-
results.push(configureAdapterSafe(root, entry, { ...
|
|
101
|
+
results.push(...runStep((c) => configureAdapterSafe(root, entry, { ...c, skillMode })))
|
|
39
102
|
}
|
|
40
103
|
|
|
41
|
-
results.push(...ensureIgnore(root, manifest.ignore,
|
|
104
|
+
results.push(...runStep((c) => ensureIgnore(root, manifest.ignore, c)))
|
|
42
105
|
}
|
|
43
106
|
|
|
44
107
|
// 기존 기록의 items·design은 보존한다 — 부트스트랩은 배선만 다루므로
|
|
@@ -53,7 +116,11 @@ export function runBootstrap(root, opts = {}) {
|
|
|
53
116
|
design: previous?.design ?? [],
|
|
54
117
|
managed: collectManaged(root, manifest),
|
|
55
118
|
}
|
|
56
|
-
results.push(writeRecord(root, record,
|
|
119
|
+
results.push(...runStep((c) => writeRecord(root, record, c)))
|
|
120
|
+
|
|
121
|
+
// 실제 결과 수가 예측과 다르면 마지막 알림으로 100%를 맞춘다.
|
|
122
|
+
// 진행 바가 87%에서 끝나면 사용자는 무엇이 실패했다고 읽는다.
|
|
123
|
+
if (onProgress && done !== total) onProgress({ done: total, total, path: null })
|
|
57
124
|
|
|
58
125
|
const failed = results.filter((r) => !r.ok)
|
|
59
126
|
log('')
|
package/lib/catalog.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
5
6
|
import { CLIS, CLI_IDS } from './clis.mjs'
|
|
6
7
|
import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
|
|
7
8
|
import { repoPath, repoPathStrict } from './context.mjs'
|
|
@@ -58,8 +59,13 @@ export function shellQuote(text, platform = process.platform) {
|
|
|
58
59
|
return `'${s.replace(/'/g, "'\\''")}'`
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const execFileAsync = promisify(execFile)
|
|
63
|
+
|
|
64
|
+
// 비동기다. 동기 실행은 이벤트 루프를 통째로 막아, npx가 도는 수십 초 동안
|
|
65
|
+
// 진행 화면을 한 번도 다시 그릴 수 없었다. 반환 형태({ ok, output })는
|
|
66
|
+
// 그대로라 호출부는 await만 더하면 된다.
|
|
61
67
|
export function makeExec(dryRun, log = console.log) {
|
|
62
|
-
return (cmd, args, opts = {}) => {
|
|
68
|
+
return async (cmd, args, opts = {}) => {
|
|
63
69
|
if (dryRun) {
|
|
64
70
|
log(` [dry-run] ${cmd} ${args.join(' ')}`)
|
|
65
71
|
return { ok: true, output: '' }
|
|
@@ -70,15 +76,27 @@ export function makeExec(dryRun, log = console.log) {
|
|
|
70
76
|
// 어차피 우리가 직접 quote하므로, 완성된 한 줄 명령을 넘기고 인자 배열은 비운다.
|
|
71
77
|
const [file, fileArgs] = shell ? [[cmd, ...args].map((s) => shellQuote(s)).join(' '), []] : [cmd, args]
|
|
72
78
|
try {
|
|
73
|
-
|
|
79
|
+
// execFile은 stdio를 강제로 파이프로 고정하고 옵션으로 넘겨도 무시한다 —
|
|
80
|
+
// 그래서 stdout·stderr는 버퍼링이 공짜로 따라온다. 문제는 stdin이다:
|
|
81
|
+
// 파이프가 열린 채로 자식에게 넘어가므로, stdin을 읽는 명령(자격 증명·
|
|
82
|
+
// 확인 프롬프트 등)은 EOF를 못 받아 영원히 멈춘다. 옛 execFileSync는
|
|
83
|
+
// stdio: ['ignore', ...]로 자식의 stdin을 즉시 닫아 뒀다 — 그 동작을
|
|
84
|
+
// 복원하려면 스폰 직후 우리가 직접 stdin을 닫아야 한다.
|
|
85
|
+
const p = execFileAsync(file, fileArgs, {
|
|
74
86
|
encoding: 'utf8',
|
|
75
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
87
|
...opts,
|
|
77
88
|
shell,
|
|
78
89
|
})
|
|
79
|
-
|
|
90
|
+
// 옵션 조합에 따라 stdin이 없을 수 있다 — 없으면 조용히 넘어간다.
|
|
91
|
+
p.child.stdin?.end()
|
|
92
|
+
const { stdout } = await p
|
|
93
|
+
return { ok: true, output: stdout }
|
|
80
94
|
} catch (err) {
|
|
81
|
-
|
|
95
|
+
// spawn 자체가 실패하면(ENOENT 등) err.stderr가 undefined가 아니라
|
|
96
|
+
// 빈 문자열로 온다 — ??는 빈 문자열을 "값 있음"으로 보고 통과시켜
|
|
97
|
+
// err.message(바이너리 이름이 담긴 진단 텍스트)를 삼켜 버린다. ||로
|
|
98
|
+
// 빈 문자열도 폴백하게 한다.
|
|
99
|
+
return { ok: false, output: String(err.stderr || err.message) }
|
|
82
100
|
}
|
|
83
101
|
}
|
|
84
102
|
}
|
|
@@ -119,9 +137,12 @@ export function defineMcp({ id, label, server, supports = [...CLI_IDS], unsuppor
|
|
|
119
137
|
}
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
|
|
140
|
+
// 배선은 Claude 한 곳이라도 미배선 "사유"는 CLI마다 다를 수 있다 — 상류가
|
|
141
|
+
// 자체 경로로 지원하는 CLI에 일괄 "Claude 전용" 사유를 붙이면 거짓 정보가 된다.
|
|
142
|
+
// 항목이 사유를 넘기면 그 CLI만 덮고, 나머지는 기본 사유로 채운다.
|
|
143
|
+
export function definePlugin({ id, label, installId, detectIds, marketplace, note, group = null, unsupported: reasons = {} }) {
|
|
123
144
|
const unsupported = Object.fromEntries(
|
|
124
|
-
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.claudePlugin')]),
|
|
145
|
+
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, reasons[c] ?? msg('item.unsupported.claudePlugin')]),
|
|
125
146
|
)
|
|
126
147
|
return {
|
|
127
148
|
id, category: 'plugin', label, scope: 'project', supports: ['claude'], unsupported, note, group,
|
|
@@ -130,8 +151,8 @@ export function definePlugin({ id, label, installId, detectIds, marketplace, not
|
|
|
130
151
|
},
|
|
131
152
|
async install(ctx) {
|
|
132
153
|
const { root, dryRun, exec } = ctx
|
|
133
|
-
if (marketplace) exec('claude', ['plugin', 'marketplace', 'add', marketplace.repo], { cwd: root })
|
|
134
|
-
const r = exec('claude', ['plugin', 'install', installId, '--scope', 'project'], { cwd: root })
|
|
154
|
+
if (marketplace) await exec('claude', ['plugin', 'marketplace', 'add', marketplace.repo], { cwd: root })
|
|
155
|
+
const r = await exec('claude', ['plugin', 'install', installId, '--scope', 'project'], { cwd: root })
|
|
135
156
|
if (!r.ok) {
|
|
136
157
|
if (!dryRun) enablePlugin(root, installId, marketplace)
|
|
137
158
|
return { fallback: true, message: msg('item.plugin.deferred') }
|
|
@@ -139,15 +160,15 @@ export function definePlugin({ id, label, installId, detectIds, marketplace, not
|
|
|
139
160
|
},
|
|
140
161
|
async uninstall(ctx) {
|
|
141
162
|
const { root, dryRun, exec } = ctx
|
|
142
|
-
const r = exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
|
|
163
|
+
const r = await exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
|
|
143
164
|
if (!r.ok && !dryRun) disablePlugin(root, detectIds)
|
|
144
165
|
},
|
|
145
166
|
}
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
export function defineSkill({ id, label, scope, detect, install, uninstall, note, group = null }) {
|
|
169
|
+
export function defineSkill({ id, label, scope, detect, install, uninstall, note, group = null, unsupported: reasons = {} }) {
|
|
149
170
|
const unsupported = Object.fromEntries(
|
|
150
|
-
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.claudeSkill')]),
|
|
171
|
+
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, reasons[c] ?? msg('item.unsupported.claudeSkill')]),
|
|
151
172
|
)
|
|
152
173
|
return { id, category: 'skill', label, scope, supports: ['claude'], unsupported, note, group, detect, install, uninstall }
|
|
153
174
|
}
|
|
@@ -197,14 +218,14 @@ export function defineRegistrySkill({ id, label, source, skill, note, group = nu
|
|
|
197
218
|
return { status: findSkillDir(root, skill) ? 'installed' : 'absent' }
|
|
198
219
|
},
|
|
199
220
|
async install({ root, exec }) {
|
|
200
|
-
const r = exec('npx', ['-y', 'skills@latest', 'add', source, '--skill', skill, '--agent', 'universal', '--yes', '--copy'], { cwd: root })
|
|
221
|
+
const r = await exec('npx', ['-y', 'skills@latest', 'add', source, '--skill', skill, '--agent', 'universal', '--yes', '--copy'], { cwd: root })
|
|
201
222
|
if (!r.ok) throw new LocalizedError('error.registrySkillInstall', { skill, output: r.output })
|
|
202
223
|
},
|
|
203
224
|
async uninstall({ root, dryRun, exec }) {
|
|
204
225
|
// 레지스트리의 remove는 --agent universal에서 "Done!"을 찍고도 디렉터리를
|
|
205
226
|
// 남긴다(실측). 그래서 성공/실패와 무관하게 남은 디렉터리를 우리가 지운다 —
|
|
206
227
|
// 남겨 두면 detect가 계속 installed로 읽어 제거가 안 된 채 성공으로 보인다.
|
|
207
|
-
exec('npx', ['-y', 'skills@latest', 'remove', skill, '--agent', 'universal', '--yes'], { cwd: root })
|
|
228
|
+
await exec('npx', ['-y', 'skills@latest', 'remove', skill, '--agent', 'universal', '--yes'], { cwd: root })
|
|
208
229
|
if (dryRun) return
|
|
209
230
|
const dir = findSkillDir(root, skill)
|
|
210
231
|
if (dir) rmSync(repoPathStrict(root, `${SHARED_SKILLS}/${dir}`), { recursive: true, force: true })
|
package/lib/clis.mjs
CHANGED
|
@@ -8,6 +8,9 @@ import { hasSection, appendSection, removeSection } from './tomlfile.mjs'
|
|
|
8
8
|
// Junction일 때 MCP 등록 한 번으로 글로벌 설정이 생성·수정되는 것을 막는다.
|
|
9
9
|
function jsonAdapter(relFile, topKey, toEntry) {
|
|
10
10
|
return {
|
|
11
|
+
// 경로를 밖으로 낸다 — 상세 패널이 "이 항목이 어디에 쓰이나"를 보여 준다.
|
|
12
|
+
// 어댑터가 실제로 쓰는 값과 같은 변수라 두 곳이 갈릴 수 없다.
|
|
13
|
+
file: relFile,
|
|
11
14
|
has(root, name) {
|
|
12
15
|
const data = readJson(repoPath(root, relFile))
|
|
13
16
|
return getIn(data, [topKey, name]) !== undefined
|
|
@@ -29,6 +32,16 @@ function tomlLines(server) {
|
|
|
29
32
|
]
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
// codex·grok은 TOML을 쓴다. jsonAdapter와 같은 이유로 경로를 한 번만 적는다.
|
|
36
|
+
function tomlAdapter(file) {
|
|
37
|
+
return {
|
|
38
|
+
file,
|
|
39
|
+
has: (root, name) => hasSection(repoPath(root, file), name),
|
|
40
|
+
add: (root, name, s) => appendSection(repoPathStrict(root, file), name, tomlLines(s)),
|
|
41
|
+
remove: (root, name) => removeSection(repoPathStrict(root, file), name),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export const CLIS = {
|
|
33
46
|
claude: {
|
|
34
47
|
label: 'Claude Code',
|
|
@@ -39,9 +52,7 @@ export const CLIS = {
|
|
|
39
52
|
},
|
|
40
53
|
codex: {
|
|
41
54
|
label: 'Codex',
|
|
42
|
-
|
|
43
|
-
add: (root, name, s) => appendSection(repoPathStrict(root, '.codex/config.toml'), name, tomlLines(s)),
|
|
44
|
-
remove: (root, name) => removeSection(repoPathStrict(root, '.codex/config.toml'), name),
|
|
55
|
+
...tomlAdapter('.codex/config.toml'),
|
|
45
56
|
},
|
|
46
57
|
gemini: {
|
|
47
58
|
label: 'Gemini CLI',
|
|
@@ -76,9 +87,7 @@ export const CLIS = {
|
|
|
76
87
|
},
|
|
77
88
|
grok: {
|
|
78
89
|
label: 'Grok Build',
|
|
79
|
-
|
|
80
|
-
add: (root, name, s) => appendSection(repoPathStrict(root, '.grok/config.toml'), name, tomlLines(s)),
|
|
81
|
-
remove: (root, name) => removeSection(repoPathStrict(root, '.grok/config.toml'), name),
|
|
90
|
+
...tomlAdapter('.grok/config.toml'),
|
|
82
91
|
},
|
|
83
92
|
copilot: {
|
|
84
93
|
label: 'GitHub Copilot CLI',
|
package/lib/design-md/flow.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { discoverSources, extraDirsFromEnv } from './scan.mjs'
|
|
|
5
5
|
import { makeOpener, openPreview } from './open.mjs'
|
|
6
6
|
import { createT, toText } from '../i18n/index.mjs'
|
|
7
7
|
import { labelWidth, pad } from '../width.mjs'
|
|
8
|
+
import { plainLine } from '../tui/progress.mjs'
|
|
8
9
|
|
|
9
10
|
// design 항목의 detect는 installed/absent만 돌려준다(catalog.mjs) — partial은
|
|
10
11
|
// 열을 넓히기만 하고 실제로 나오지 않는다.
|
|
@@ -62,7 +63,17 @@ function openPreviews(items, tokensStr, opener, log, t) {
|
|
|
62
63
|
async function applyVisible(root, visibleStates, selectedIds, { dryRun, log, t }) {
|
|
63
64
|
const changes = planChanges(visibleStates, selectedIds)
|
|
64
65
|
if (changes.length === 0) { log(t('apply.noChanges')); return [] }
|
|
65
|
-
|
|
66
|
+
// design --set도 비대화형 경로다 — runClassic의 --set과 대칭으로, 바를
|
|
67
|
+
// 그리지 않고 평문 한 줄씩 흘린다. ANSI 제어문자로 로그를 더럽히지 않는다.
|
|
68
|
+
const results = await apply(root, changes, {
|
|
69
|
+
dryRun,
|
|
70
|
+
log,
|
|
71
|
+
t,
|
|
72
|
+
onProgress: (event) => {
|
|
73
|
+
const line = plainLine(event, t)
|
|
74
|
+
if (line) log(line)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
66
77
|
report(results, log, t)
|
|
67
78
|
return results
|
|
68
79
|
}
|
package/lib/engine.mjs
CHANGED
|
@@ -25,24 +25,76 @@ export function planChanges(states, selectedIds) {
|
|
|
25
25
|
return changes
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en') } = {}) {
|
|
29
|
-
const
|
|
28
|
+
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en'), onProgress = null, shouldStop = null } = {}) {
|
|
29
|
+
const baseExec = makeExec(dryRun, log)
|
|
30
30
|
const results = []
|
|
31
|
-
|
|
31
|
+
const total = changes.length
|
|
32
|
+
|
|
33
|
+
// 알림은 참고용이지 설치를 좌우하지 않는다. onProgress는 훗날 실제 렌더러가
|
|
34
|
+
// 될 자리라(Task 11) 그 안의 버그가 던지면, notify를 감싸지 않는 한 예외가
|
|
35
|
+
// apply()를 통째로 빠져나가 이미 디스크에 쓴 항목의 결과를 아무도 못 받는다.
|
|
36
|
+
// command 단계 알림은 install/uninstall의 try 안에서 불리므로, 여기서 삼키지
|
|
37
|
+
// 않으면 렌더링 버그가 그 항목의 설치 실패로 잘못 보고된다.
|
|
38
|
+
const notify = (event) => {
|
|
39
|
+
if (!onProgress) return
|
|
40
|
+
try {
|
|
41
|
+
onProgress({ total, ...event })
|
|
42
|
+
} catch {
|
|
43
|
+
// 무시한다 — 알림 실패로 설치를 멈추거나 결과를 뒤틀면 안 된다.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// shouldStop도 같은 이유로 감싼다. 여기서 다시 던지면 UI 술어의 버그 하나가
|
|
48
|
+
// 설치 전체를 중단시킨다 — 알림을 못 받는 것보다 더 나쁘다. 던지면 "멈추지
|
|
49
|
+
// 않음"으로 취급해 계속 진행한다.
|
|
50
|
+
const isStopRequested = () => {
|
|
51
|
+
if (!shouldStop) return false
|
|
52
|
+
try {
|
|
53
|
+
return Boolean(shouldStop())
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < total; index++) {
|
|
60
|
+
const { item, action } = changes[index]
|
|
61
|
+
|
|
62
|
+
// 중단은 **항목 경계에서만** 본다. 외부 명령을 중간에 죽이면 반쯤 설치된
|
|
63
|
+
// 상태가 남는다 — 그래서 AbortSignal이 아니라 술어(predicate)다.
|
|
64
|
+
if (isStopRequested()) {
|
|
65
|
+
// skipped: true는 "시도조차 하지 않았다"는 뜻이다. ok는 성공하지 못했으니
|
|
66
|
+
// false지만 실패는 아니다 — 소비자는 실패로 보고하거나 exitCode를 세우기
|
|
67
|
+
// 전에 반드시 skipped를 먼저 확인해야 한다(!r.ok && !r.skipped).
|
|
68
|
+
results.push({ item, action, ok: false, skipped: true })
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
notify({ index, item, action, phase: 'start' })
|
|
73
|
+
const startedAt = Date.now()
|
|
74
|
+
|
|
75
|
+
// 화면이 "지금 무엇이 도는가"를 보여 줄 수 있는 유일한 경로다.
|
|
76
|
+
const exec = async (cmd, args, opts) => {
|
|
77
|
+
notify({ index, item, action, phase: 'command', command: [cmd, ...args].join(' ') })
|
|
78
|
+
return baseExec(cmd, args, opts)
|
|
79
|
+
}
|
|
80
|
+
|
|
32
81
|
// log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
|
|
33
82
|
// 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
|
|
34
83
|
// 무엇이 바뀔지는 알 수 없는 상태였다.
|
|
35
84
|
const ctx = { root, dryRun, exec, log, t }
|
|
85
|
+
let result
|
|
36
86
|
try {
|
|
37
87
|
const fn = action === 'uninstall' ? item.uninstall : item.install
|
|
38
88
|
const r = await fn.call(item, ctx)
|
|
39
|
-
|
|
89
|
+
result = { item, action, ok: true, message: r?.message }
|
|
40
90
|
} catch (err) {
|
|
41
91
|
// LocalizedError는 .key를 들고 있다 — 구조체로 옮겨 담아야 표시하는
|
|
42
92
|
// 쪽에서 toText로 활성 로케일에 맞게 다시 렌더할 수 있다. 그냥
|
|
43
93
|
// err.message만 쓰면 LocalizedError.message는 항상 영어라 로케일이 섞인다.
|
|
44
|
-
|
|
94
|
+
result = { item, action, ok: false, message: err.key ? msg(err.key, err.params) : err.message }
|
|
45
95
|
}
|
|
96
|
+
results.push(result)
|
|
97
|
+
notify({ index, item, action, phase: 'done', ok: result.ok, ms: Date.now() - startedAt, message: result.message })
|
|
46
98
|
}
|
|
47
99
|
return results
|
|
48
100
|
}
|
package/lib/i18n/catalog/en.mjs
CHANGED
|
@@ -228,6 +228,13 @@ export default {
|
|
|
228
228
|
'item.unsupported.claudeSkill': 'Claude Code skill install',
|
|
229
229
|
'item.unsupported.ponytailUser': 'upstream installs it per user (codex/copilot plugin, gemini extension)',
|
|
230
230
|
'item.unsupported.ponytailRules': 'no plugin mechanism — copy the ruleset into AGENTS.md by hand',
|
|
231
|
+
'item.unsupported.upstreamNone': 'no official install path upstream',
|
|
232
|
+
'item.unsupported.superpowersSeparate': 'upstream supports it via a separate per-harness install — this item wires the Claude Code plugin only',
|
|
233
|
+
'item.unsupported.bkitPort': 'upstream ships separate ports (bkit-codex / bkit-gemini) installed apart from this plugin; the Gemini port presumably succeeds to Antigravity',
|
|
234
|
+
'item.unsupported.impeccableJunction': 'upstream supports it, but npx impeccable install breaks the shared .agents/skills link, so only the plugin route is wired',
|
|
235
|
+
'item.unsupported.gstackHost': 'upstream supports it via ./setup --host — this item installs for Claude Code only',
|
|
236
|
+
'item.unsupported.gsdFlag': 'upstream supports it via a runtime flag — this item installs with --claude only',
|
|
237
|
+
'item.unsupported.gsdGemini': 'upstream dropped Gemini CLI (sunset 2026-06) — Antigravity succeeds it via --antigravity',
|
|
231
238
|
'item.mcp.partial': 'registered: {present} / missing: {missing}',
|
|
232
239
|
'item.plugin.partial': 'wired: {present} / missing: {missing}',
|
|
233
240
|
'item.plugin.deferred': 'recorded in config — it downloads on the next Claude Code run',
|
|
@@ -243,7 +250,7 @@ export default {
|
|
|
243
250
|
'item.mcp.codebase-memory.note': 'needs the codebase-memory-mcp binary on PATH. Install: https://github.com/DeusData/codebase-memory-mcp (install.sh / install.ps1)',
|
|
244
251
|
'item.mcp.graphify.note': 'needs the graphify-mcp binary on PATH. Install: uv tool install "graphifyy[mcp]". Reads graphify-out/graph.json — build it first with /graphify .',
|
|
245
252
|
'item.mcp.headroom.note': 'needs the headroom binary on PATH. Install: uv tool install --python 3.13 "headroom-ai[proxy,mcp]". The proxy (headroom wrap) is a way of launching, not repo config',
|
|
246
|
-
'item.plugin.superpowers.note': 'official marketplace plugin',
|
|
253
|
+
'item.plugin.superpowers.note': 'official marketplace plugin. Upstream supports 11 harnesses (Codex, Antigravity, …) via per-harness installs',
|
|
247
254
|
'item.plugin.mattpocock-skills.note': '22 engineering and productivity skills (tdd, code-review, research, …)',
|
|
248
255
|
'item.plugin.ponytail.note': 'lean-code ruleset. Wires the Claude Code plugin and the OpenCode plugin entry, both repo-scoped',
|
|
249
256
|
'item.plugin.ecc.note': 'skills, instincts, memory and security for agent harnesses. Installed as a plugin — its skills/ holds 281 entries, too many to copy into the shared skills directory',
|
|
@@ -253,7 +260,7 @@ export default {
|
|
|
253
260
|
'item.skill.taste.note': 'anti-slop frontend skill for landing pages, portfolios and redesigns (skill name: design-taste-frontend)',
|
|
254
261
|
'item.skill.karpathy.note': 'behavioral guidelines that curb overbuilding and vague success criteria',
|
|
255
262
|
'item.skill.gstack.note': 'repo-local clone + setup (needs bash; Git Bash on Windows). Runtime state (~/.gstack) may be created globally.',
|
|
256
|
-
'item.skill.gsd.note': 'npx @opengsd/gsd-core, installed per project',
|
|
263
|
+
'item.skill.gsd.note': 'npx @opengsd/gsd-core, installed per project. Upstream supports 18 runtimes (Codex, Antigravity, …); this item wires --claude only',
|
|
257
264
|
'error.gstackClone': 'gstack clone failed: {output}',
|
|
258
265
|
'error.gstackSetup': 'gstack setup failed: {output}',
|
|
259
266
|
'error.gsdInstall': 'GSD install failed: {output}',
|
|
@@ -280,8 +287,8 @@ export default {
|
|
|
280
287
|
'tui.search.placeholder': 'type to search · ↓ for the list',
|
|
281
288
|
'tui.empty.filtered': ' Nothing matches in this tab. Press Tab to try another.',
|
|
282
289
|
'tui.empty.none': ' Nothing here.',
|
|
283
|
-
'tui.hint.search': 'type to search (spaces included) ↓ to list Tab
|
|
284
|
-
'tui.hint.list': 'Space select ↑↓ move (↑ at top = search) Tab
|
|
290
|
+
'tui.hint.search': 'type to search (spaces included) ↓ to list Tab tab Esc clear Ctrl+F CLI Ctrl+D detail',
|
|
291
|
+
'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',
|
|
285
292
|
'tui.review.title': 'Review — {count} change(s)',
|
|
286
293
|
'tui.review.more': ' …and {count} more',
|
|
287
294
|
'tui.review.hint': 'Enter apply Esc cancel',
|
|
@@ -293,21 +300,50 @@ export default {
|
|
|
293
300
|
'tui.toggledAll': 'Selected every visible item in the {tab} tab.',
|
|
294
301
|
'tui.toggledNone': 'Cleared every visible item in the {tab} tab.',
|
|
295
302
|
'tui.nonInteractive': 'The interactive screen only opens in a terminal. --list and --set work too.',
|
|
296
|
-
'tui.applyHeader': '{count} change(s) to apply{suffix}:',
|
|
297
303
|
'tui.pressAnyKey': 'Press any key to continue…',
|
|
298
304
|
'tui.confirmSuffix': ' [y/N] ',
|
|
299
305
|
|
|
306
|
+
// 진행 화면(tui/progress.mjs) 문구. Task 9의 이벤트(start·command·done)를
|
|
307
|
+
// 사람이 읽는 줄로 바꾼다. plain*은 비TTY(CI·파이프)용 한 줄 출력이다.
|
|
308
|
+
'progress.title': 'Applying — {count} change(s){suffix}',
|
|
309
|
+
'progress.counter': '{done}/{total} {percent}%',
|
|
310
|
+
'progress.elapsed': '{seconds}s',
|
|
311
|
+
'progress.elapsedMin': '{minutes}m{seconds}s',
|
|
312
|
+
'progress.running': '{seconds}s elapsed',
|
|
313
|
+
'progress.more': ' …and {count} more',
|
|
314
|
+
'progress.abortHint': 'Ctrl+C stops after the current item',
|
|
315
|
+
'progress.aborted': 'Stopped — {count} item(s) skipped',
|
|
316
|
+
'progress.skipped': 'skipped',
|
|
317
|
+
'progress.done': 'Done — {ok} succeeded, {failed} failed{skippedSuffix}',
|
|
318
|
+
'progress.plain': '[{index}/{total}] {action} {label}',
|
|
319
|
+
'progress.plainDone': ' {mark} {seconds}s',
|
|
320
|
+
|
|
300
321
|
// 언어 행(action.language) 전용. 저장 성공·dry-run·실패·강제 로케일 네 갈래를 덮는다.
|
|
301
322
|
'tui.lang.saved': 'Saved to {path}',
|
|
302
323
|
'tui.lang.dryRun': 'dry-run — applied to this session only',
|
|
303
324
|
'tui.lang.saveFailed': 'Could not save the language: {message}',
|
|
304
325
|
'tui.lang.overridden': '--lang / AGENT_SETUP_LANG still wins on the next run.',
|
|
305
326
|
|
|
327
|
+
// CLI 필터 표시. 검색줄 오른쪽 끝에 붙는다(Task 7) — filterSegment가 쓴다.
|
|
328
|
+
// 필터가 없을 때는 아무것도 내지 않는다(잡음 방지) — "전체" 라벨을 위한
|
|
329
|
+
// 키를 따로 두지 않는다.
|
|
330
|
+
'tui.filter.prefix': 'CLI › ',
|
|
331
|
+
'tui.filter.position': '{current}/{total}',
|
|
332
|
+
'tui.filter.empty': ' Nothing in this tab is wired for {cli}.',
|
|
333
|
+
|
|
306
334
|
'item.location.user': 'installs to: user global',
|
|
307
335
|
// 커버리지는 전부 지원할 때도 찍는다 — 표시가 없는 것과 "10/10"은 뜻이 다르다.
|
|
308
336
|
'item.cliCoverage': 'CLI {covered}/{total}',
|
|
309
337
|
'item.unsupportedList': 'not wired for {count}: {groups}',
|
|
310
338
|
'item.unsupportedGroup': '{clis} — {why}',
|
|
339
|
+
'detail.wired': 'wired',
|
|
340
|
+
'detail.unwired': 'unwired',
|
|
341
|
+
'detail.provider': 'source',
|
|
342
|
+
'detail.preview': 'preview',
|
|
343
|
+
'detail.scope.project': 'repo scope',
|
|
344
|
+
'detail.scope.user': 'user global',
|
|
345
|
+
'detail.more': ' …and {count} more line(s) — Ctrl+D to expand',
|
|
346
|
+
'detail.empty': ' Move the cursor onto an item to see its details.',
|
|
311
347
|
|
|
312
348
|
// 상주 언어 행 — 작업 탭 맨 위. hint는 현재 로케일의 자기 이름을 담는다.
|
|
313
349
|
'action.language.label': 'Language',
|
package/lib/i18n/catalog/ko.mjs
CHANGED
|
@@ -221,6 +221,13 @@ export default {
|
|
|
221
221
|
'item.unsupported.claudeSkill': 'Claude Code 스킬 설치본',
|
|
222
222
|
'item.unsupported.ponytailUser': '상류 설치 경로가 사용자 스코프입니다 (codex·copilot 플러그인, gemini 확장)',
|
|
223
223
|
'item.unsupported.ponytailRules': '플러그인 기구가 없습니다 — 규칙을 AGENTS.md에 직접 옮겨 적으세요',
|
|
224
|
+
'item.unsupported.upstreamNone': '상류에 공식 설치 경로가 없습니다',
|
|
225
|
+
'item.unsupported.superpowersSeparate': '상류가 하니스별 별도 설치로 지원합니다 — 이 항목은 Claude Code 플러그인만 배선합니다',
|
|
226
|
+
'item.unsupported.bkitPort': '상류가 별도 배포판(bkit-codex·bkit-gemini)으로 지원합니다 — 이 플러그인과 별개 설치이며, Gemini판은 Antigravity 승계 추정입니다',
|
|
227
|
+
'item.unsupported.impeccableJunction': '상류는 지원하지만 npx impeccable install이 공유 .agents/skills 연결을 끊어 플러그인 경로만 배선합니다',
|
|
228
|
+
'item.unsupported.gstackHost': '상류는 ./setup --host로 지원합니다 — 이 항목은 Claude Code에만 설치합니다',
|
|
229
|
+
'item.unsupported.gsdFlag': '상류는 런타임 플래그로 지원합니다 — 이 항목은 --claude로만 설치합니다',
|
|
230
|
+
'item.unsupported.gsdGemini': '상류가 Gemini CLI 지원을 제거했습니다(2026-06 종료) — Antigravity가 --antigravity로 승계합니다',
|
|
224
231
|
'item.mcp.partial': '등록됨: {present} / 누락: {missing}',
|
|
225
232
|
'item.plugin.partial': '배선됨: {present} / 누락: {missing}',
|
|
226
233
|
'item.plugin.deferred': '설정 기록됨 — 다음 Claude Code 실행 시 다운로드됩니다',
|
|
@@ -236,7 +243,7 @@ export default {
|
|
|
236
243
|
'item.mcp.codebase-memory.note': 'PATH에 codebase-memory-mcp 바이너리 필요. 설치: https://github.com/DeusData/codebase-memory-mcp (install.sh / install.ps1)',
|
|
237
244
|
'item.mcp.graphify.note': 'PATH에 graphify-mcp 바이너리 필요. 설치: uv tool install "graphifyy[mcp]". graphify-out/graph.json을 읽으므로 /graphify .로 그래프를 먼저 만드세요',
|
|
238
245
|
'item.mcp.headroom.note': 'PATH에 headroom 바이너리 필요. 설치: uv tool install --python 3.13 "headroom-ai[proxy,mcp]". 프록시(headroom wrap)는 실행 방식이라 저장소 설정 대상이 아닙니다',
|
|
239
|
-
'item.plugin.superpowers.note': '공식 마켓플레이스
|
|
246
|
+
'item.plugin.superpowers.note': '공식 마켓플레이스 플러그인. 상류는 Codex·Antigravity 등 11개 하니스를 하니스별 설치로 지원합니다',
|
|
240
247
|
'item.plugin.mattpocock-skills.note': '엔지니어링·생산성 스킬 22종 (tdd, code-review, research 등)',
|
|
241
248
|
'item.plugin.ponytail.note': '군더더기 없는 코드 규칙 묶음. Claude Code 플러그인과 OpenCode plugin 항목을 저장소 범위로 배선합니다',
|
|
242
249
|
'item.plugin.ecc.note': '에이전트 하네스용 스킬·본능·메모리·보안 묶음. skills/에 281개가 들어 있어 공유 스킬 디렉터리에 복사하지 않고 플러그인으로 설치합니다',
|
|
@@ -246,7 +253,7 @@ export default {
|
|
|
246
253
|
'item.skill.taste.note': '랜딩 페이지·포트폴리오·리디자인용 안티슬롭 프런트엔드 스킬 (스킬 이름: design-taste-frontend)',
|
|
247
254
|
'item.skill.karpathy.note': '과설계와 모호한 완료 기준을 줄이는 행동 지침',
|
|
248
255
|
'item.skill.gstack.note': '저장소 로컬 clone + setup (bash 필요, Windows는 Git Bash). 런타임 상태(~/.gstack)는 전역에 생길 수 있음.',
|
|
249
|
-
'item.skill.gsd.note': 'npx @opengsd/gsd-core 프로젝트 로컬
|
|
256
|
+
'item.skill.gsd.note': 'npx @opengsd/gsd-core 프로젝트 로컬 설치. 상류는 Codex·Antigravity 등 18개 런타임을 지원합니다(이 항목은 --claude)',
|
|
250
257
|
'error.gstackClone': 'gstack clone 실패: {output}',
|
|
251
258
|
'error.gstackSetup': 'gstack setup 실패: {output}',
|
|
252
259
|
'error.gsdInstall': 'GSD 설치 실패: {output}',
|
|
@@ -271,8 +278,8 @@ export default {
|
|
|
271
278
|
'tui.search.placeholder': '타이핑하면 검색 · ↓ 로 목록',
|
|
272
279
|
'tui.empty.filtered': ' 이 탭에는 일치하는 항목이 없습니다. Tab으로 다른 탭을 보세요.',
|
|
273
280
|
'tui.empty.none': ' 항목이 없습니다.',
|
|
274
|
-
'tui.hint.search': '입력=검색어
|
|
275
|
-
'tui.hint.list': 'Space 선택 ↑↓ 이동
|
|
281
|
+
'tui.hint.search': '입력=검색어 ↓ 목록으로 Tab 탭이동 Esc 검색해제 Ctrl+F CLI Ctrl+D 상세',
|
|
282
|
+
'tui.hint.list': 'Space 선택 ↑↓ 이동 Tab 탭 Enter 실행/제출 Ctrl+A 전체 Ctrl+O 미리보기 Ctrl+F CLI Ctrl+D 상세',
|
|
276
283
|
'tui.review.title': '제출 검토 — 변경 {count}건',
|
|
277
284
|
'tui.review.more': ' …외 {count}건',
|
|
278
285
|
'tui.review.hint': 'Enter 적용 Esc 취소',
|
|
@@ -284,21 +291,47 @@ export default {
|
|
|
284
291
|
'tui.toggledAll': '{tab} 탭의 보이는 항목을 모두 선택했습니다.',
|
|
285
292
|
'tui.toggledNone': '{tab} 탭의 보이는 항목을 모두 해제했습니다.',
|
|
286
293
|
'tui.nonInteractive': '대화형 화면은 터미널에서만 열립니다. --list · --set 으로도 다룰 수 있습니다.',
|
|
287
|
-
'tui.applyHeader': '적용할 변경 {count}건{suffix}:',
|
|
288
294
|
'tui.pressAnyKey': '계속하려면 아무 키나 누르세요…',
|
|
289
295
|
'tui.confirmSuffix': ' [y/N] ',
|
|
290
296
|
|
|
297
|
+
// 진행 화면(tui/progress.mjs) 문구. en.mjs 쪽 주석 참고.
|
|
298
|
+
'progress.title': '적용 중 — 변경 {count}건{suffix}',
|
|
299
|
+
'progress.counter': '{done}/{total} {percent}%',
|
|
300
|
+
'progress.elapsed': '{seconds}초',
|
|
301
|
+
'progress.elapsedMin': '{minutes}분{seconds}초',
|
|
302
|
+
'progress.running': '{seconds}초 경과',
|
|
303
|
+
'progress.more': ' …외 {count}건',
|
|
304
|
+
'progress.abortHint': 'Ctrl+C 중단 (현재 항목까지 마칩니다)',
|
|
305
|
+
'progress.aborted': '중단했습니다 — {count}건 건너뜀',
|
|
306
|
+
'progress.skipped': '건너뜀',
|
|
307
|
+
'progress.done': '끝났습니다 — 성공 {ok}건, 실패 {failed}건{skippedSuffix}',
|
|
308
|
+
'progress.plain': '[{index}/{total}] {action} {label}',
|
|
309
|
+
'progress.plainDone': ' {mark} {seconds}초',
|
|
310
|
+
|
|
291
311
|
// 언어 행(action.language) 전용. en.mjs 쪽 주석 참고.
|
|
292
312
|
'tui.lang.saved': '{path}에 저장했습니다',
|
|
293
313
|
'tui.lang.dryRun': 'dry-run — 이번 세션에만 적용했습니다',
|
|
294
314
|
'tui.lang.saveFailed': '언어를 저장하지 못했습니다: {message}',
|
|
295
315
|
'tui.lang.overridden': '다음 실행에서는 --lang / AGENT_SETUP_LANG이 여전히 이깁니다.',
|
|
296
316
|
|
|
317
|
+
// CLI 필터 표시. en.mjs 쪽 주석 참고.
|
|
318
|
+
'tui.filter.prefix': 'CLI › ',
|
|
319
|
+
'tui.filter.position': '{current}/{total}',
|
|
320
|
+
'tui.filter.empty': ' 이 탭에는 {cli}에 배선되는 항목이 없습니다.',
|
|
321
|
+
|
|
297
322
|
'item.location.user': '설치 위치: 사용자 글로벌',
|
|
298
323
|
// en.mjs 쪽 주석 참고.
|
|
299
324
|
'item.cliCoverage': 'CLI {covered}/{total}',
|
|
300
325
|
'item.unsupportedList': '미배선 {count}곳: {groups}',
|
|
301
326
|
'item.unsupportedGroup': '{clis} — {why}',
|
|
327
|
+
'detail.wired': '배선',
|
|
328
|
+
'detail.unwired': '미배선',
|
|
329
|
+
'detail.provider': '공급자',
|
|
330
|
+
'detail.preview': '미리보기',
|
|
331
|
+
'detail.scope.project': '저장소 스코프',
|
|
332
|
+
'detail.scope.user': '사용자 전역',
|
|
333
|
+
'detail.more': ' …외 {count}줄 — Ctrl+D로 펼치기',
|
|
334
|
+
'detail.empty': ' 커서를 항목 위에 올리면 상세가 보입니다.',
|
|
302
335
|
|
|
303
336
|
// 상주 언어 행 — en.mjs 쪽 주석 참고.
|
|
304
337
|
'action.language.label': '언어',
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { definePlugin } from '../catalog.mjs'
|
|
2
|
+
import { CLI_IDS } from '../clis.mjs'
|
|
3
|
+
import { msg } from '../i18n/index.mjs'
|
|
4
|
+
|
|
5
|
+
// 상류의 Codex·Gemini 지원은 이 플러그인이 아니라 별도 리포의 독립 포팅본이다
|
|
6
|
+
// (bkit-codex: install 스크립트+MCP, 기능 241개 중 80개, hooks 없음 /
|
|
7
|
+
// bkit-gemini: Gemini CLI extension). 플러그인 항목을 재사용할 수 없어 사유만
|
|
8
|
+
// 남긴다. Gemini CLI는 2026-06-18 종료라 Gemini판은 Antigravity 승계 추정
|
|
9
|
+
// (2026-08-02 검증).
|
|
10
|
+
const PORTS = ['codex', 'gemini']
|
|
11
|
+
|
|
2
12
|
export default definePlugin({
|
|
3
13
|
id: 'plugin.bkit', label: 'bkit', group: '__flow',
|
|
4
14
|
installId: 'bkit@bkit-marketplace',
|
|
5
15
|
detectIds: ['bkit@bkit-marketplace'],
|
|
6
16
|
marketplace: { name: 'bkit-marketplace', repo: 'popup-studio-ai/bkit-claude-code' },
|
|
17
|
+
unsupported: Object.fromEntries(
|
|
18
|
+
CLI_IDS.filter((c) => c !== 'claude').map((c) => [
|
|
19
|
+
c,
|
|
20
|
+
PORTS.includes(c) ? msg('item.unsupported.bkitPort') : msg('item.unsupported.upstreamNone'),
|
|
21
|
+
]),
|
|
22
|
+
),
|
|
7
23
|
})
|