@rch4com/agent-setup 1.18.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 +15 -3
- package/lib/catalog.mjs +44 -10
- package/lib/clis.mjs +11 -0
- package/lib/engine.mjs +4 -2
- package/lib/global-plugin.mjs +38 -6
- package/lib/i18n/catalog/en.mjs +42 -13
- package/lib/i18n/catalog/ko.mjs +42 -13
- package/lib/items/global.ponytail.mjs +8 -4
- package/lib/items/global.superpowers.mjs +12 -6
- package/lib/items/plugin.ecc.mjs +1 -0
- package/lib/items/plugin.impeccable.mjs +2 -1
- package/lib/items/plugin.mattpocock-skills.mjs +1 -0
- package/lib/items/plugin.ponytail.mjs +1 -0
- package/lib/items/plugin.superpowers.mjs +2 -1
- package/lib/items/plugin.understand-anything.mjs +1 -0
- package/lib/items/skill.agent-browser.mjs +1 -0
- package/lib/items/skill.find-skills.mjs +1 -0
- package/lib/items/skill.gsd.mjs +5 -0
- package/lib/items/skill.gstack.mjs +1 -0
- package/lib/items/skill.hallmark.mjs +1 -0
- package/lib/items/skill.mcp-builder.mjs +1 -0
- package/lib/items/skill.strix.mjs +1 -0
- package/lib/jsonfile.mjs +22 -3
- package/lib/status.mjs +39 -3
- package/lib/tui/detail.mjs +42 -9
- package/lib/tui/render.mjs +155 -23
- package/lib/tui/rows.mjs +36 -7
- package/lib/tui/run.mjs +51 -11
- package/lib/tui/state.mjs +9 -5
- package/lib/update.mjs +13 -3
- package/package.json +1 -1
|
@@ -18,27 +18,31 @@ import { msg } from '../i18n/index.mjs'
|
|
|
18
18
|
//
|
|
19
19
|
// opencode는 프로젝트 항목(plugin.ponytail)이 opencode.jsonc의 plugin 배열로
|
|
20
20
|
// 이미 배선한다 — 전역 판에서 겹치면 같은 플러그인이 두 번 등록된다. claude도
|
|
21
|
-
// 같은 이유로 프로젝트 항목의 자리다.
|
|
22
|
-
//
|
|
21
|
+
// 같은 이유로 프로젝트 항목의 자리다.
|
|
22
|
+
// grok — `grok plugin install DietrichGebert/ponytail --trust`. 2026-09-05
|
|
23
|
+
// 이 머신(grok 1.0.5)에서 설치·감지·제거 왕복을 실측했다. 상류
|
|
24
|
+
// README는 "기본 꺼짐이라 /plugins에서 켜라"고 적지만 1.0.5는
|
|
25
|
+
// --trust 설치 직후 config.toml [plugins].enabled에 이름을 넣었다.
|
|
23
26
|
// kiro·vscode는 상류가 룰 파일 수동 배치만 안내하고, kilo·kimi는 경로가 없다.
|
|
24
27
|
//
|
|
25
28
|
// 상류는 플러그인 제거 전에 node scripts/uninstall.js로 ~/.config/ponytail
|
|
26
29
|
// 설정과 statusLine 항목을 정리하라고 안내한다 — 하니스마다 스크립트 위치가
|
|
27
30
|
// 달라 자동화하지 않고 note로 알린다. 이 항목은 플러그인만 제거한다.
|
|
28
|
-
const SUPPORTS = ['codex', 'gemini', 'copilot']
|
|
31
|
+
const SUPPORTS = ['codex', 'gemini', 'copilot', 'grok']
|
|
29
32
|
|
|
30
33
|
const CONFIG = {
|
|
31
34
|
id: 'global.ponytail', label: 'Ponytail', group: '__token',
|
|
32
35
|
note: 'item.global.ponytail.note',
|
|
36
|
+
verified: '2026-09-05',
|
|
33
37
|
pluginName: 'ponytail',
|
|
34
38
|
marketplaceRepo: 'DietrichGebert/ponytail',
|
|
35
39
|
installId: 'ponytail@ponytail',
|
|
36
40
|
gemini: 'https://github.com/DietrichGebert/ponytail',
|
|
41
|
+
grok: 'DietrichGebert/ponytail',
|
|
37
42
|
unsupported: Object.fromEntries(
|
|
38
43
|
CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
|
|
39
44
|
c,
|
|
40
45
|
['claude', 'opencode'].includes(c) ? msg('item.unsupported.globalProjectItem', { item: 'Ponytail' })
|
|
41
|
-
: c === 'grok' ? msg('item.unsupported.ponytailGlobalGrok')
|
|
42
46
|
: ['kiro', 'vscode'].includes(c) ? msg('item.unsupported.ponytailRules')
|
|
43
47
|
: msg('item.unsupported.upstreamNone'),
|
|
44
48
|
]),
|
|
@@ -14,31 +14,37 @@ import { msg } from '../i18n/index.mjs'
|
|
|
14
14
|
// opencode— 상류 .opencode/INSTALL.md가 전역 opencode.json의 plugin 배열에
|
|
15
15
|
// 아래 항목 한 줄을 더하라고 안내한다.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// grok — 2026-09-05 이 머신(grok 1.0.5)에서 `grok plugin install
|
|
18
|
+
// superpowers@xai-official --trust`로 설치·감지·제거 왕복을 실측.
|
|
19
|
+
// 기록은 ~/.grok/installed-plugins/registry.json(global-plugin.mjs).
|
|
20
|
+
//
|
|
21
|
+
// kimi는 설치가 대화형 /plugins뿐이라 헤드리스 경로가 없다. claude는 프로젝트
|
|
22
|
+
// 스코프 항목(plugin.superpowers)의 자리다.
|
|
21
23
|
//
|
|
22
24
|
// skill.superpowers(공유 .agents/skills)와 배타로 묶지 않는다 — 저쪽은 이
|
|
23
25
|
// 저장소에만, 이쪽은 머신 전체에 작용해 평면이 다르고, "claude는 프로젝트
|
|
24
26
|
// 플러그인 + 나머지는 전역" 같은 조합이 정당하다. 겹치는 하니스가 같은 스킬을
|
|
25
27
|
// 두 곳에서 본다는 것은 note에 적어 둔다.
|
|
26
|
-
const SUPPORTS = ['codex', 'gemini', 'opencode', 'copilot']
|
|
28
|
+
const SUPPORTS = ['codex', 'gemini', 'opencode', 'copilot', 'grok']
|
|
27
29
|
|
|
28
30
|
const CONFIG = {
|
|
29
31
|
id: 'global.superpowers', label: 'superpowers', group: '__flow',
|
|
30
32
|
note: 'item.global.superpowers.note',
|
|
33
|
+
verified: '2026-09-05',
|
|
31
34
|
pluginName: 'superpowers',
|
|
32
35
|
marketplaceRepo: 'obra/superpowers-marketplace',
|
|
33
36
|
installId: 'superpowers@superpowers-marketplace',
|
|
34
37
|
gemini: 'https://github.com/obra/superpowers',
|
|
35
38
|
opencode: 'superpowers@git+https://github.com/obra/superpowers.git',
|
|
39
|
+
grok: 'superpowers@xai-official',
|
|
36
40
|
unsupported: Object.fromEntries(
|
|
37
41
|
CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
|
|
38
42
|
c,
|
|
39
43
|
c === 'claude' ? msg('item.unsupported.superpowersGlobalClaude')
|
|
40
|
-
: c === 'grok' ? msg('item.unsupported.superpowersGlobalGrok')
|
|
41
44
|
: c === 'kimi' ? msg('item.unsupported.superpowersGlobalKimi')
|
|
45
|
+
// 상류 README가 Antigravity 설치를 안내하지만 감지·제거 경로를 실측할
|
|
46
|
+
// CLI가 없다 — grok과 같은 기준으로 아직 배선하지 않는다.
|
|
47
|
+
: c === 'antigravity' ? msg('item.unsupported.unmeasuredUpstream')
|
|
42
48
|
: msg('item.unsupported.upstreamNone'),
|
|
43
49
|
]),
|
|
44
50
|
),
|
package/lib/items/plugin.ecc.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export default definePlugin({
|
|
|
17
17
|
detectIds: ['ecc@ecc'],
|
|
18
18
|
marketplace: { name: 'ecc', repo: 'affaan-m/ECC' },
|
|
19
19
|
note: 'item.plugin.ecc.note',
|
|
20
|
+
verified: '2026-08-15',
|
|
20
21
|
unsupported: Object.fromEntries(
|
|
21
22
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [
|
|
22
23
|
c,
|
|
@@ -15,13 +15,14 @@ import { msg } from '../i18n/index.mjs'
|
|
|
15
15
|
// 상류 공식 프로바이더는 3.6.0 기준 16개(.agent=antigravity·.hermes가 새로
|
|
16
16
|
// 들어옴 — README는 아직 수동 복사만 안내해 코드가 앞선다). 이 저장소의 CLI와
|
|
17
17
|
// 겹치는 것은 아래 UPSTREAM 그대로다.
|
|
18
|
-
const UPSTREAM = ['codex', 'gemini', 'opencode', 'kiro', 'grok', 'copilot']
|
|
18
|
+
const UPSTREAM = ['codex', 'gemini', 'opencode', 'kiro', 'grok', 'copilot', 'antigravity']
|
|
19
19
|
export default definePlugin({
|
|
20
20
|
id: 'plugin.impeccable', label: 'impeccable', group: '__style',
|
|
21
21
|
installId: 'impeccable@impeccable',
|
|
22
22
|
detectIds: ['impeccable@impeccable'],
|
|
23
23
|
marketplace: { name: 'impeccable', repo: 'pbakaus/impeccable' },
|
|
24
24
|
note: 'item.plugin.impeccable.note',
|
|
25
|
+
verified: '2026-08-15',
|
|
25
26
|
unsupported: Object.fromEntries(
|
|
26
27
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [
|
|
27
28
|
c,
|
|
@@ -13,6 +13,7 @@ export default definePlugin({
|
|
|
13
13
|
detectIds: ['mattpocock-skills@mattpocock'],
|
|
14
14
|
marketplace: { name: 'mattpocock', repo: 'mattpocock/skills' },
|
|
15
15
|
note: 'item.plugin.mattpocock-skills.note',
|
|
16
|
+
verified: '2026-08-15',
|
|
16
17
|
unsupported: Object.fromEntries(
|
|
17
18
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.mattpocockRegistry')]),
|
|
18
19
|
),
|
|
@@ -10,7 +10,7 @@ import { msg } from '../i18n/index.mjs'
|
|
|
10
10
|
// Grok은 6.2.0에는 없다가 6.3.0에서 들어왔다 — `### Grok Build CLI`의
|
|
11
11
|
// `grok plugin install superpowers@xai-official --trust`다. Devin·Hermes도 같이
|
|
12
12
|
// 늘었지만 둘 다 이 저장소가 다루는 CLI가 아니다.
|
|
13
|
-
const SEPARATE = ['codex', 'gemini', 'opencode', 'kimi', 'copilot', 'grok']
|
|
13
|
+
const SEPARATE = ['codex', 'gemini', 'opencode', 'kimi', 'copilot', 'grok', 'antigravity']
|
|
14
14
|
|
|
15
15
|
export default definePlugin({
|
|
16
16
|
id: 'plugin.superpowers', label: 'superpowers', group: '__flow',
|
|
@@ -18,6 +18,7 @@ export default definePlugin({
|
|
|
18
18
|
installId: 'superpowers@claude-plugins-official',
|
|
19
19
|
detectIds: ['superpowers@claude-plugins-official', 'superpowers@superpowers-marketplace'],
|
|
20
20
|
note: 'item.plugin.superpowers.note',
|
|
21
|
+
verified: '2026-08-15',
|
|
21
22
|
unsupported: Object.fromEntries(
|
|
22
23
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [
|
|
23
24
|
c,
|
|
@@ -15,6 +15,7 @@ export default definePlugin({
|
|
|
15
15
|
detectIds: ['understand-anything@understand-anything'],
|
|
16
16
|
marketplace: { name: 'understand-anything', repo: 'Egonex-AI/Understand-Anything' },
|
|
17
17
|
note: 'item.plugin.understand-anything.note',
|
|
18
|
+
verified: '2026-08-15',
|
|
18
19
|
unsupported: Object.fromEntries(
|
|
19
20
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.uaUser')]),
|
|
20
21
|
),
|
package/lib/items/skill.gsd.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { LocalizedError, msg } from '../i18n/index.mjs'
|
|
|
26
26
|
// kimi `--kimi --local`이 "Project-level Kimi install semantics remain
|
|
27
27
|
// deferred"를 찍고 거부한다 — 전역 설치만 가능하다.
|
|
28
28
|
// kiro·grok·vscode 상류 런타임 목록에 없다.
|
|
29
|
+
// antigravity --antigravity 플래그가 있지만 설치 경로를 실측하지 못했다.
|
|
29
30
|
const RUNTIMES = [
|
|
30
31
|
{ cli: 'claude', dirs: ['.claude/commands', '.claude/skills'] },
|
|
31
32
|
{ cli: 'codex', dirs: ['.codex/skills'] },
|
|
@@ -56,11 +57,15 @@ export default defineSkill({
|
|
|
56
57
|
id: 'skill.gsd', label: 'GSD (Get Shit Done)', group: '__flow', scope: 'project',
|
|
57
58
|
note: 'item.skill.gsd.note',
|
|
58
59
|
supports: SUPPORTS,
|
|
60
|
+
verified: '2026-08-15',
|
|
59
61
|
unsupported: Object.fromEntries(
|
|
60
62
|
CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
|
|
61
63
|
c,
|
|
62
64
|
c === 'gemini' ? msg('item.unsupported.gsdGemini')
|
|
63
65
|
: c === 'kimi' ? msg('item.unsupported.gsdKimiLocal')
|
|
66
|
+
// --antigravity 플래그가 있지만 어느 경로에 무엇을 쓰는지 실측하지
|
|
67
|
+
// 못했다 — 실측 전에는 배선하지 않는다.
|
|
68
|
+
: c === 'antigravity' ? msg('item.unsupported.unmeasuredUpstream')
|
|
64
69
|
: msg('item.unsupported.upstreamNone'),
|
|
65
70
|
]),
|
|
66
71
|
),
|
|
@@ -29,6 +29,7 @@ const SHARED_RECURSIVE = ['codex', 'opencode']
|
|
|
29
29
|
export default defineSkill({
|
|
30
30
|
id: 'skill.gstack', label: 'gstack', group: '__flow', scope: 'project',
|
|
31
31
|
note: 'item.skill.gstack.note',
|
|
32
|
+
verified: '2026-08-15',
|
|
32
33
|
unsupported: Object.fromEntries(
|
|
33
34
|
CLI_IDS.filter((c) => c !== 'claude').map((c) => [
|
|
34
35
|
c,
|
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
|
|
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(
|
|
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
|
@@ -8,7 +8,17 @@ import { updateBlocks, updateFiles } from './bootstrap/apply.mjs'
|
|
|
8
8
|
import { readRecord, toolVersion } from './bootstrap/record.mjs'
|
|
9
9
|
import { createT } from './i18n/index.mjs'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// 미배선 사유는 상류를 실측한 날의 사실이다. 그 날짜(item.verified)가 이보다
|
|
12
|
+
// 오래되면 status가 알린다 — 사유 문장이 화면에 낡은 채 남는 것을 막는 유일한
|
|
13
|
+
// 장치다. 90일은 상류 하니스들의 릴리스 간격을 넉넉히 덮는 값이다.
|
|
14
|
+
export const STALE_DAYS = 90
|
|
15
|
+
|
|
16
|
+
function daysSince(iso, now) {
|
|
17
|
+
const ms = now - Date.parse(`${iso}T00:00:00Z`)
|
|
18
|
+
return Number.isFinite(ms) ? Math.floor(ms / 86_400_000) : 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function collectStatus(root, { manifest = MANIFEST, items = [], latest, now = Date.now() } = {}) {
|
|
12
22
|
const record = readRecord(root)
|
|
13
23
|
|
|
14
24
|
// dry-run으로 갱신 판정만 얻는다 — 판정 로직을 두 벌 두면 status와 update가
|
|
@@ -35,6 +45,9 @@ export async function collectStatus(root, { manifest = MANIFEST, items = [], lat
|
|
|
35
45
|
}
|
|
36
46
|
const installed = states.filter((s) => s.status !== 'absent').map((s) => s.id)
|
|
37
47
|
const intended = new Set(record?.items ?? [])
|
|
48
|
+
const stale = items
|
|
49
|
+
.filter((i) => i.verified && daysSince(i.verified, now) > STALE_DAYS)
|
|
50
|
+
.map((i) => ({ id: i.id, verified: i.verified }))
|
|
38
51
|
|
|
39
52
|
return {
|
|
40
53
|
hasRecord: Boolean(record),
|
|
@@ -53,6 +66,7 @@ export async function collectStatus(root, { manifest = MANIFEST, items = [], lat
|
|
|
53
66
|
installed,
|
|
54
67
|
recordOnly: [...intended].filter((id) => !installed.includes(id)),
|
|
55
68
|
repoOnly: installed.filter((id) => !intended.has(id)),
|
|
69
|
+
stale,
|
|
56
70
|
},
|
|
57
71
|
}
|
|
58
72
|
}
|
|
@@ -81,13 +95,35 @@ export function formatStatus(report, t = createT('en')) {
|
|
|
81
95
|
lines.push(t('status.row.items', { list: items.installed.join(', ') || t('status.none') }))
|
|
82
96
|
if (items.recordOnly.length) lines.push(t('status.row.recordOnly', { list: items.recordOnly.join(', ') }))
|
|
83
97
|
if (items.repoOnly.length) lines.push(t('status.row.repoOnly', { list: items.repoOnly.join(', ') }))
|
|
98
|
+
if (items.stale?.length) {
|
|
99
|
+
lines.push(t('status.row.stale', { list: items.stale.map((s) => `${s.id} (${s.verified})`).join(', ') }))
|
|
100
|
+
lines.push(t('status.hint.stale', { days: STALE_DAYS }))
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
return lines.join('\n')
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
|
|
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') } = {}) {
|
|
89
125
|
const { loadItems } = await import('./catalog.mjs')
|
|
90
|
-
const report = await collectStatus(root, { items: await loadItems() })
|
|
126
|
+
const report = await collectStatus(root, { items: await loadItems(), latest: await fetchLatestVersion(fetchImpl) })
|
|
91
127
|
log(json ? JSON.stringify(report, null, 2) : formatStatus(report, t))
|
|
92
128
|
return report
|
|
93
129
|
}
|
package/lib/tui/detail.mjs
CHANGED
|
@@ -47,27 +47,60 @@ function headLine(row, w, t) {
|
|
|
47
47
|
return cut(`${item.label} ${bits.filter(Boolean).join(' · ')}`, w)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// CLI 이름 나열(a·b·c)을 이름 경계에서만 접는다. width.wrap은 공백에서만
|
|
51
|
+
// 끊어, 공백 없는 나열은 낱말 중간(`ant` / `igravity`)에서 갈라졌다.
|
|
52
|
+
function wrapNames(names, limit) {
|
|
53
|
+
const lines = []
|
|
54
|
+
let cur = ''
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
const next = cur ? `${cur}·${name}` : name
|
|
57
|
+
if (cur && width(next) > limit) {
|
|
58
|
+
lines.push(cur)
|
|
59
|
+
cur = name
|
|
60
|
+
} else {
|
|
61
|
+
cur = next
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (cur) lines.push(cur)
|
|
65
|
+
return lines
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
function wiredLines(item, w, lead, t) {
|
|
51
69
|
const supports = item.supports ?? []
|
|
52
70
|
if (supports.length === 0) return []
|
|
53
71
|
// 경로는 MCP에서만 붙인다. 그때만 clis.mjs 어댑터가 경로의 유일한 진실이다 —
|
|
54
72
|
// plugin·skill은 설치 자리가 항목마다 달라 어댑터가 알지 못한다.
|
|
55
73
|
const showFile = item.category === 'mcp'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
// 경로가 없으면 세로로 늘어놓을 이유가 없다 — CLI 열한 개를 한 줄에 하나씩
|
|
75
|
+
// 놓으면 80×24에서 `배선 ✔ claude` 뒤가 `…외 9줄`로 잘려 정작 "어디에
|
|
76
|
+
// 되나"를 펼쳐야만 읽을 수 있었다. 이름을 ·로 이어 접는다(미배선과 같은 꼴).
|
|
77
|
+
if (!showFile) {
|
|
78
|
+
return wrapNames(supports, Math.max(1, w - lead - 2)).map((line, i) =>
|
|
79
|
+
cut(`${i === 0 ? pad(t('detail.wired'), lead) : ' '.repeat(lead)}${i === 0 ? '✔ ' : ' '}${line}`, w))
|
|
80
|
+
}
|
|
81
|
+
// MCP는 "CLI 경로" 쌍을 한 줄에 들어가는 만큼 채운다. 한 줄에 하나씩 놓으면
|
|
82
|
+
// 열 개가 열 줄이라 80×24 패널에서 절반이 `…외 N줄`로 접혔다. 폭이 좁으면
|
|
83
|
+
// 한 줄에 하나로 자연히 돌아간다.
|
|
84
|
+
const pairs = supports.map((cli) => (CLIS[cli]?.file ? `${cli} ${CLIS[cli].file}` : cli))
|
|
85
|
+
const colW = Math.max(...pairs.map((p) => width(p))) + 3
|
|
86
|
+
const room = Math.max(1, w - lead - 2)
|
|
87
|
+
const perLine = Math.max(1, Math.floor(room / colW))
|
|
88
|
+
const rows = []
|
|
89
|
+
for (let i = 0; i < pairs.length; i += perLine) {
|
|
90
|
+
const chunk = pairs.slice(i, i + perLine)
|
|
91
|
+
rows.push(chunk.map((p, j) => (j < chunk.length - 1 ? pad(p, colW) : p)).join(''))
|
|
92
|
+
}
|
|
93
|
+
return rows.map((line, i) => cut(`${i === 0 ? pad(t('detail.wired'), lead) : ' '.repeat(lead)}${i === 0 ? '✔ ' : ' '}${line}`, w))
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
function unwiredLines(item, w, lead, t) {
|
|
67
97
|
const out = []
|
|
68
98
|
unsupportedGroups(item, t).forEach((group, i) => {
|
|
69
99
|
const head = i === 0 ? pad(t('detail.unwired'), lead) : ' '.repeat(lead)
|
|
70
|
-
|
|
100
|
+
// 미배선 CLI 이름도 같은 규칙으로 접는다 — 11개를 한 줄에 두면 뒤가 잘린다.
|
|
101
|
+
wrapNames(group.clis, Math.max(1, w - lead - 2)).forEach((line, j) => {
|
|
102
|
+
out.push(cut(`${j === 0 ? head : ' '.repeat(lead)}${j === 0 ? '✖ ' : ' '}${line}`, w))
|
|
103
|
+
})
|
|
71
104
|
// 사유는 한 칸 더 들여 이어 붙인다. 첫 줄만 └를 달아 어느 CLI 묶음의
|
|
72
105
|
// 사유인지 눈으로 잇는다.
|
|
73
106
|
wrap(group.why, Math.max(1, w - lead - 3)).forEach((line, j) => {
|