@rch4com/agent-setup 1.5.0 → 1.7.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 CHANGED
@@ -51,7 +51,7 @@ async function openTui(root, { dryRun, skillMode, designDirs, t, localeForced })
51
51
  // --list / --set 전용 비대화형 경로.
52
52
  async function runClassic(root, { dryRun, listOnly, setArg, t }) {
53
53
  const { loadItems } = await withDeps(() => import('./lib/catalog.mjs'), t)
54
- const { scan, planChanges, apply } = await withDeps(() => import('./lib/engine.mjs'), t)
54
+ const { scan, planChanges, apply, assertExclusive } = await withDeps(() => import('./lib/engine.mjs'), t)
55
55
  const { plainLine } = await withDeps(() => import('./lib/tui/progress.mjs'), t)
56
56
  const items = await loadItems()
57
57
  const states = await scan(root, items)
@@ -68,6 +68,7 @@ async function runClassic(root, { dryRun, listOnly, setArg, t }) {
68
68
  const selectedIds = new Set(setArg.split(',').map((s) => s.trim()).filter(Boolean))
69
69
  const known = new Set(items.map((i) => i.id))
70
70
  for (const id of selectedIds) if (!known.has(id)) throw new LocalizedError('error.unknownItem', { id })
71
+ assertExclusive(items, selectedIds)
71
72
 
72
73
  const changes = planChanges(states, selectedIds)
73
74
  if (changes.length === 0) { console.log(t('apply.noChanges')); return }
package/lib/catalog.mjs CHANGED
@@ -137,9 +137,12 @@ export function defineMcp({ id, label, server, supports = [...CLI_IDS], unsuppor
137
137
  }
138
138
  }
139
139
 
140
- export function definePlugin({ id, label, installId, detectIds, marketplace, note, group = null }) {
140
+ // 배선은 Claude 한 곳이라도 미배선 "사유"는 CLI마다 다를 수 있다 — 상류가
141
+ // 자체 경로로 지원하는 CLI에 일괄 "Claude 전용" 사유를 붙이면 거짓 정보가 된다.
142
+ // 항목이 사유를 넘기면 그 CLI만 덮고, 나머지는 기본 사유로 채운다.
143
+ export function definePlugin({ id, label, installId, detectIds, marketplace, note, group = null, unsupported: reasons = {} }) {
141
144
  const unsupported = Object.fromEntries(
142
- 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')]),
143
146
  )
144
147
  return {
145
148
  id, category: 'plugin', label, scope: 'project', supports: ['claude'], unsupported, note, group,
@@ -163,9 +166,9 @@ export function definePlugin({ id, label, installId, detectIds, marketplace, not
163
166
  }
164
167
  }
165
168
 
166
- 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 = {} }) {
167
170
  const unsupported = Object.fromEntries(
168
- 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')]),
169
172
  )
170
173
  return { id, category: 'skill', label, scope, supports: ['claude'], unsupported, note, group, detect, install, uninstall }
171
174
  }
package/lib/engine.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { makeExec } from './catalog.mjs'
2
- import { createT, msg } from './i18n/index.mjs'
2
+ import { createT, LocalizedError, msg } from './i18n/index.mjs'
3
3
 
4
4
  export async function scan(root, items) {
5
5
  const states = []
@@ -14,6 +14,21 @@ export async function scan(root, items) {
14
14
  return states
15
15
  }
16
16
 
17
+ // item.exclusive는 "이 항목들은 같은 파일 하나를 두고 다툰다"는 표시다.
18
+ // TUI는 고르는 순간에 배타를 지키지만(state.mjs), --set은 목록을 통째로 받는다 —
19
+ // 둘을 함께 주면 나중 항목이 앞선 항목을 덮어써 성공한 것처럼 끝난다.
20
+ // 조용히 하나를 고르는 대신 어느 쪽을 원했는지 되묻는다.
21
+ export function assertExclusive(items, selectedIds) {
22
+ const groups = new Map()
23
+ for (const item of items) {
24
+ if (!item.exclusive || !selectedIds.has(item.id)) continue
25
+ groups.set(item.exclusive, [...(groups.get(item.exclusive) ?? []), item.id])
26
+ }
27
+ for (const ids of groups.values()) {
28
+ if (ids.length > 1) throw new LocalizedError('error.exclusiveItems', { ids: ids.join(', ') })
29
+ }
30
+ }
31
+
17
32
  export function planChanges(states, selectedIds) {
18
33
  const changes = []
19
34
  for (const { item, status } of states) {
@@ -0,0 +1,227 @@
1
+ // 커밋 메시지 템플릿(.gitmessage.txt) 항목의 단일 출처.
2
+ //
3
+ // 한 파일에 언어가 다른 두 판이 있다 — 그래서 항목도 둘이지만 대상 파일은
4
+ // 하나다. 둘을 동시에 고를 수 없게 하는 것은 TUI(state.mjs)와 --set
5
+ // (engine.mjs의 assertExclusive)이 exclusive 키로 맡는다. 여기서는 "내가 쓴
6
+ // 판이 지금 놓여 있는가"만 판정하고, 아니면 아무것도 건드리지 않는다.
7
+ //
8
+ // 항목 파일(lib/items/*.mjs)에 두지 않는 이유: loadItems가 그 디렉터리의
9
+ // 모든 .mjs를 항목으로 읽어 검증하므로, 보조 모듈이 하나라도 섞이면 로드가
10
+ // 통째로 실패한다.
11
+ import { execFileSync } from 'node:child_process'
12
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs'
13
+ import { normalizeBody } from './bootstrap/text.mjs'
14
+ import { repoPath, repoPathStrict } from './context.mjs'
15
+ import { LocalizedError, msg } from './i18n/index.mjs'
16
+
17
+ export const GITMESSAGE_REL = '.gitmessage.txt'
18
+ // 한 파일을 두고 다투는 항목들의 묶음 이름.
19
+ export const GITMESSAGE_EXCLUSIVE = 'gitmessage'
20
+
21
+ const CONFIG_KEY = 'commit.template'
22
+
23
+ // 이 저장소 루트의 .gitmessage.txt와 같은 내용이다 —
24
+ // test/items.gitmessage.test.mjs가 두 값이 갈리지 않게 고정한다.
25
+ export const GITMESSAGE_KO = `# Commit message template ( * 주석 처리(#)된 부분들은 커밋 메시지에 포함되지 않으니 참고해서 작성)
26
+ # <타입(<영역>)>: <주제>
27
+ # - 타입은 필수 작성. 해당 커밋의 성격을 나타내며 타입의 종류 중 하나만 선택, 영어 소문자로 시작
28
+ # - 영역은 필요 시 작성. 변경된 부분을 직접적으로 나타냄 ex) refactor(MemberService): loginMember 메소드 명을 login으로 수정
29
+ # - 주제는 필수 작성. 최대 50글자까지, 끝에 마침표 금지, 한글로 간단 명료하게 작성, 현재형 사용
30
+ # 바로 밑줄에 작성
31
+
32
+ # 바로 아래 한 줄 공백 유지 (타입 본문의 분리를 위함)
33
+
34
+ ################################
35
+ # <본문> - 필요시 작성. 한 줄당 최대 72자까지, 72자 초과시 줄바꿈, ”왜”, “무엇을 위해” 작업했는지에 초점을 맞춰 작성
36
+ # 바로 밑줄에 작성
37
+
38
+ # 바로 아래 한 줄 공백 유지 (본문과 바닥글의 분리를 위함)
39
+
40
+ ################################
41
+ # <바닥글> - 필요시 작성
42
+ # 바로 밑줄에 작성 (이슈 해결 시에는 resolve: #99와 같이 작성)
43
+
44
+ ######## [타입 리스트] ############
45
+ # feat: 새로운 기능 추가
46
+ # fix: 버그 수정
47
+ # style: 코드의 의미에 영향을 주지 않는 변경 사항(공백, 형식 지정, 세미콜론 누락 등), 클래스명 수정&가독성을 위해 변수명을 변경한 경우도 포함
48
+ # refactor: 버그 수정, 기능 추가도 아닌 코드 변경
49
+ # chore: 소스코드의 변경이 없으며 나열된 타입 이외 간단 변경
50
+ # add: feat 이외의 부수적인 코드, 라이브러리 등을 추가한 경우, 새로운 파일(Component나 Activity 등)을 생성한 경우도 포함
51
+ # remove: 코드, 파일을 삭제한 경우, 필요 없는 주석 삭제도 포함
52
+ # move: fix, refactor 등과 관계 없이 코드, 파일 등의 위치를 이동하는 작업만 수행한 경우
53
+ # comment: 필요한 주석을 추가, 수정한 경우( * 필요 없는 주석을 삭제한 경우는 remove)
54
+ # perf: 성능을 향상시키는 코드 변경
55
+ # test: 테스트 코드를 추가, 수정, 삭제한 경우
56
+ # docs: 문서를 추가, 수정한 경우
57
+ # design: CSS 등 사용자 UI 디자인을 추가, 수정한 경우
58
+ # revert: 이전 커밋을 되돌린 경우
59
+ # !BREAKING CHANGE: 커다란 API 변경의 경우 (MAJOR 버전과 상관 관계)
60
+ # !HOTFIX: 급하게 치명적인 버그를 고쳐야하는 경우
61
+ ######## [커밋 메시지 예시] #########
62
+ # fix: 로그인 기능 구현
63
+ #
64
+ # 로그인 시 JWT 발급
65
+ #
66
+ # resolve: #232
67
+ # ref: #122
68
+ # related to: #30, #50
69
+ # ------------------
70
+ # [* 지켜야 할 보편적 규칙]
71
+ # 1. 언어는 한국어를 사용한다.
72
+ # - 예외 사항) 최초 커밋 메시지는 “Initial commit”으로 한다.
73
+ # 2. 문장의 구성
74
+ # 1) 문장은 간결하게 구성한다.
75
+ # 2) 현재형을 사용한다.
76
+ # 3. 작성한 당사자가 아닌 제 3자가 이해할 수 있도록 작성한다.
77
+ # 4. 단순한 문장일수록 좋다.
78
+ # ------------------
79
+ # [* 커밋 타이밍]
80
+ # - 의미 있는 코드의 변화가 있다면 커밋을 진행한다.
81
+ # - 의미가 있다면 사소한 것을 커밋 하는 것은 전혀 잘못된 것이 아니다.
82
+ # - 다만 의미 없는 잦은 커밋은 히스토리 추적에 불편함을 주므로 의미 없는 커밋은 자제한다.
83
+ ################################
84
+ `
85
+
86
+ // 한국어판을 줄 단위로 옮긴 것이다. 타입 목록과 구획선은 같은 자리에 둔다 —
87
+ // 두 판을 오가도 커밋 히스토리의 모양이 달라지지 않아야 한다.
88
+ export const GITMESSAGE_EN = `# Commit message template ( * Lines that start with # are comments and never reach the commit message)
89
+ # <type(<scope>)>: <subject>
90
+ # - Type is required. It names the nature of this commit. Pick exactly one from the list below, lowercase English
91
+ # - Scope is optional. It points straight at what changed ex) refactor(MemberService): rename loginMember to login
92
+ # - Subject is required. 50 characters at most, no trailing period, plain and clear, present tense
93
+ # Write it on the line right below
94
+
95
+ # Keep one blank line right below (it separates the subject from the body)
96
+
97
+ ################################
98
+ # <Body> - optional. 72 characters per line at most, wrap past that, focus on "why" and "what for"
99
+ # Write it on the line right below
100
+
101
+ # Keep one blank line right below (it separates the body from the footer)
102
+
103
+ ################################
104
+ # <Footer> - optional
105
+ # Write it on the line right below (for a resolved issue, write resolve: #99)
106
+
107
+ ######## [Type list] ############
108
+ # feat: add a new feature
109
+ # fix: fix a bug
110
+ # style: changes that do not affect the meaning of the code (whitespace, formatting, missing semicolons), including class or variable renames made for readability
111
+ # refactor: a code change that is neither a bug fix nor a new feature
112
+ # chore: no source change; small changes outside the types listed here
113
+ # add: supporting code, libraries and the like added outside feat, including brand new files (a Component, an Activity)
114
+ # remove: code or files deleted, including the removal of comments that are no longer needed
115
+ # move: only moving code or files around, unrelated to fix, refactor and the rest
116
+ # comment: a needed comment added or reworded ( * deleting a comment that is not needed is remove)
117
+ # perf: a code change that improves performance
118
+ # test: test code added, changed or deleted
119
+ # docs: documentation added or changed
120
+ # design: user-facing UI design added or changed (CSS and the like)
121
+ # revert: an earlier commit rolled back
122
+ # !BREAKING CHANGE: a large API change (tied to the MAJOR version)
123
+ # !HOTFIX: an urgent fix for a critical bug
124
+ ######## [Example commit message] #########
125
+ # fix: implement the login feature
126
+ #
127
+ # Issue a JWT on login
128
+ #
129
+ # resolve: #232
130
+ # ref: #122
131
+ # related to: #30, #50
132
+ # ------------------
133
+ # [* Universal rules]
134
+ # 1. Write in English.
135
+ # - Exception) the very first commit message of a repository is "Initial commit".
136
+ # 2. Sentence shape
137
+ # 1) Keep sentences short.
138
+ # 2) Use the present tense.
139
+ # 3. Write so that a third party, not only the author, can follow it.
140
+ # 4. The simpler the sentence, the better.
141
+ # ------------------
142
+ # [* When to commit]
143
+ # - Commit whenever there is a meaningful change in the code.
144
+ # - Committing something small is not wrong at all as long as it means something.
145
+ # - Frequent meaningless commits make the history hard to follow, so hold those back.
146
+ ################################
147
+ `
148
+
149
+ export const GITMESSAGE_BODIES = [GITMESSAGE_EN, GITMESSAGE_KO].map(normalizeBody)
150
+
151
+ function readCurrent(root) {
152
+ try {
153
+ return readFileSync(repoPath(root, GITMESSAGE_REL), 'utf8')
154
+ } catch {
155
+ return null
156
+ }
157
+ }
158
+
159
+ // 저장소 설정만 본다. --local이 없으면 전역·시스템 설정까지 함께 읽혀,
160
+ // 사용자가 ~/.gitconfig에 같은 값을 둔 것만으로 이 저장소가 설치된 것처럼
161
+ // 보인다 — 이 도구는 저장소 안만 건드리므로 판정도 저장소 안만 봐야 한다.
162
+ // 값이 없으면 git이 종료 코드 1로 끝나므로 예외가 곧 "설정 없음"이다.
163
+ // git이 아예 없어도 같은 길로 떨어진다 — 그때는 상태 판정만 보수적으로
164
+ // 흐를 뿐, 화면이 죽지는 않는다.
165
+ function pointsHere(root) {
166
+ try {
167
+ const out = execFileSync('git', ['config', '--local', '--get', CONFIG_KEY], {
168
+ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
169
+ })
170
+ return out.trim() === GITMESSAGE_REL
171
+ } catch {
172
+ return false
173
+ }
174
+ }
175
+
176
+ // supports를 두지 않는다. 이 항목은 CLI 배선이 아니라 저장소 규약이라
177
+ // "10개 중 몇 개"라는 물음 자체가 성립하지 않는다 — 힌트의 커버리지 줄,
178
+ // 상세 패널의 배선표, CLI 필터가 모두 supports 없는 항목을 건너뛴다.
179
+ export function defineGitmessage({ id, label, body, note }) {
180
+ const wanted = normalizeBody(body)
181
+
182
+ return {
183
+ id,
184
+ category: 'config',
185
+ label,
186
+ scope: 'project',
187
+ group: '__flow',
188
+ exclusive: GITMESSAGE_EXCLUSIVE,
189
+ unsupported: {},
190
+ note,
191
+
192
+ async detect({ root }) {
193
+ const current = readCurrent(root)
194
+ if (current === null || normalizeBody(current) !== wanted) return { status: 'absent' }
195
+ // 파일만 놓여 있고 git이 그것을 모르면 템플릿은 한 번도 뜨지 않는다.
196
+ // 설치됨으로 찍으면 "왜 안 뜨지"를 사용자가 혼자 풀어야 한다.
197
+ return pointsHere(root) ? { status: 'installed' } : { status: 'partial', detail: msg('item.gitmessage.unregistered') }
198
+ },
199
+
200
+ async install({ root, dryRun, exec, log = () => {}, t }) {
201
+ const current = readCurrent(root)
202
+ // 손으로 쓴 템플릿은 덮지 않는다 — 이 저장소의 다른 모든 쓰기가 지키는
203
+ // 규칙이다. 우리가 아는 두 판 사이를 오가는 것만 덮어쓰기로 본다.
204
+ if (current !== null && !GITMESSAGE_BODIES.includes(normalizeBody(current))) {
205
+ throw new LocalizedError('error.gitmessageForeign', { path: GITMESSAGE_REL })
206
+ }
207
+ if (dryRun) log(t('log.gitmessage.write', { path: GITMESSAGE_REL }))
208
+ else writeFileSync(repoPathStrict(root, GITMESSAGE_REL), wanted, { encoding: 'utf8' })
209
+
210
+ const r = await exec('git', ['config', '--local', CONFIG_KEY, GITMESSAGE_REL], { cwd: root })
211
+ if (!r.ok) throw new LocalizedError('error.gitmessageConfig', { output: r.output })
212
+ },
213
+
214
+ async uninstall({ root, dryRun, exec, log = () => {}, t }) {
215
+ const current = readCurrent(root)
216
+ // 같은 적용 안에서 다른 언어 판이 먼저 설치돼 파일을 갈아 끼웠을 수
217
+ // 있다(en 설치 → ko 제거 순서). 그때 지우면 방금 쓴 파일이 날아간다 —
218
+ // 내용이 내 판일 때만 손댄다.
219
+ if (current === null || normalizeBody(current) !== wanted) return
220
+ if (dryRun) log(t('log.gitmessage.remove', { path: GITMESSAGE_REL }))
221
+ else rmSync(repoPathStrict(root, GITMESSAGE_REL), { force: true })
222
+
223
+ // 남의 값이나 빈 값에 --unset을 걸면 git이 실패를 돌려준다.
224
+ if (pointsHere(root)) await exec('git', ['config', '--local', '--unset', CONFIG_KEY], { cwd: root })
225
+ },
226
+ }
227
+ }
@@ -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,17 @@ 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',
264
+ // 커밋 템플릿은 CLI 배선이 아니라 저장소 규약이다 — note가 "무엇이 놓이고
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',
268
+ 'item.gitmessage.unregistered': 'file is in place, but commit.template does not point at it yet',
269
+ 'log.gitmessage.write': ' [dry-run] write {path}',
270
+ 'log.gitmessage.remove': ' [dry-run] remove {path}',
271
+ 'error.gitmessageForeign': '{path} already exists and was not written by this tool. A hand-written template is never overwritten — move or delete it first.',
272
+ 'error.gitmessageConfig': 'git config commit.template failed: {output}',
273
+ 'error.exclusiveItems': 'These items write the same file, so only one can be chosen: {ids}',
257
274
  'error.gstackClone': 'gstack clone failed: {output}',
258
275
  'error.gstackSetup': 'gstack setup failed: {output}',
259
276
  'error.gsdInstall': 'GSD install failed: {output}',
@@ -270,6 +287,7 @@ export default {
270
287
  'section.plugin': 'PLUGIN',
271
288
  'section.mcp': 'MCP',
272
289
  'section.skill': 'SKILL',
290
+ 'section.config': 'CONFIG',
273
291
  'section.design': 'DESIGN.MD',
274
292
 
275
293
  // TUI 목록·검토 화면 문구. item.*는 agentHint가, action.*.label/hint는
@@ -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,16 @@ 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)',
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이 그것을 가리키게 합니다. 저장소의 모든 도구와 사람이 함께 쓰는 한 벌이라 영어·한국어 중 하나만 고릅니다',
260
+ 'item.gitmessage.unregistered': '파일은 놓였지만 commit.template이 아직 가리키지 않습니다',
261
+ 'log.gitmessage.write': ' [dry-run] {path} 생성',
262
+ 'log.gitmessage.remove': ' [dry-run] {path} 제거',
263
+ 'error.gitmessageForeign': '{path}가 이미 있고 이 도구가 쓴 내용이 아닙니다. 손으로 쓴 템플릿은 덮어쓰지 않습니다 — 먼저 옮기거나 지우세요.',
264
+ 'error.gitmessageConfig': 'git config commit.template 실패: {output}',
265
+ 'error.exclusiveItems': '같은 파일을 쓰는 항목이라 하나만 고를 수 있습니다: {ids}',
250
266
  'error.gstackClone': 'gstack clone 실패: {output}',
251
267
  'error.gstackSetup': 'gstack setup 실패: {output}',
252
268
  'error.gsdInstall': 'GSD 설치 실패: {output}',
@@ -263,6 +279,7 @@ export default {
263
279
  'section.plugin': 'PLUGIN',
264
280
  'section.mcp': 'MCP',
265
281
  'section.skill': 'SKILL',
282
+ 'section.config': 'CONFIG',
266
283
  'section.design': 'DESIGN.MD',
267
284
 
268
285
  // TUI 목록·검토 화면 문구. en.mjs 쪽 주석 참고.
@@ -0,0 +1,10 @@
1
+ import { defineGitmessage, GITMESSAGE_EN } from '../gitmessage.mjs'
2
+
3
+ export default defineGitmessage({
4
+ id: 'config.gitmessage.en',
5
+ // 목록의 라벨 자리는 24칸이다 — 'Commit template (English)'는 한 칸이 넘쳐
6
+ // 잘린다. 이 도구가 이미 쓰는 --lang en|ko 표기를 따르고, 전문은 note가 편다.
7
+ label: 'Commit template (EN)',
8
+ body: GITMESSAGE_EN,
9
+ note: 'item.config.gitmessage.en.note',
10
+ })
@@ -0,0 +1,11 @@
1
+ import { defineGitmessage, GITMESSAGE_KO } from '../gitmessage.mjs'
2
+
3
+ export default defineGitmessage({
4
+ id: 'config.gitmessage.ko',
5
+ // 라벨은 우리 카탈로그 데이터라 두 로케일에서 같은 값이 나간다 — 영어 화면에
6
+ // 한글('한국어')이 새면 그 자리는 번역 누락으로 읽힌다(i18n.en.test.mjs).
7
+ // 어느 언어판인지는 라벨의 (EN)·(KO)가, 무엇이 놓이는지는 note가 말한다.
8
+ label: 'Commit template (KO)',
9
+ body: GITMESSAGE_KO,
10
+ note: 'item.config.gitmessage.ko.note',
11
+ })
@@ -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
  })
@@ -1,14 +1,28 @@
1
1
  import { definePlugin } from '../catalog.mjs'
2
+ import { CLI_IDS } from '../clis.mjs'
3
+ import { msg } from '../i18n/index.mjs'
2
4
  // 상류의 멀티 CLI 경로(`npx impeccable install --providers=... --scope=project`)는
3
5
  // 쓰지 않는다. 실측 결과 그 명령이 `.claude/skills`를 **실제 디렉터리로 갈아치워**
4
6
  // 부트스트랩이 만든 `.agents/skills` Junction을 끊는다 — 공유 스킬이 전부
5
- // Claude Code 화면에서 사라진다. 플러그인 경로는 `.claude/skills`를 건드리지 않는다.
7
+ // Claude Code 화면에서 사라진다. 버그가 아니라 설계다: 상류
8
+ // `cli/bin/commands/skills.mjs`의 isInProjectProviderLink가 "프로젝트 안 다른
9
+ // 프로바이더를 가리키는 링크"를 의도적으로 unlink한다(3.5.0 재확인, 2026-08-02).
10
+ // 플러그인 경로는 `.claude/skills`를 건드리지 않는다.
6
11
  // 또 그 명령은 `--providers`와 `--scope`를 함께 주지 않으면 대화형으로 물으며,
7
12
  // 비TTY에서는 조용히 global을 골라 홈에 설치한다.
13
+ // 상류 공식 프로바이더 14개 중 이 저장소의 CLI와 겹치는 것(아래 UPSTREAM).
14
+ // Antigravity는 README에만 있고 설치 CLI는 거부한다(수동 복사 전용).
15
+ const UPSTREAM = ['codex', 'gemini', 'opencode', 'kiro', 'grok', 'copilot']
8
16
  export default definePlugin({
9
17
  id: 'plugin.impeccable', label: 'impeccable', group: '__style',
10
18
  installId: 'impeccable@impeccable',
11
19
  detectIds: ['impeccable@impeccable'],
12
20
  marketplace: { name: 'impeccable', repo: 'pbakaus/impeccable' },
13
21
  note: 'item.plugin.impeccable.note',
22
+ unsupported: Object.fromEntries(
23
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [
24
+ c,
25
+ UPSTREAM.includes(c) ? msg('item.unsupported.impeccableJunction') : msg('item.unsupported.upstreamNone'),
26
+ ]),
27
+ ),
14
28
  })
@@ -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
+ // 상류(obra/superpowers v6.2.0)는 Codex·Antigravity·Copilot CLI 등 11개
6
+ // 하니스를 지원하지만 설치 명령이 하니스마다 전부 달라 공유 경로가 없다
7
+ // (README: "install Superpowers separately for each one"). 이 항목이 배선하는
8
+ // 것은 Claude Code 플러그인 판뿐이다. Kilo·Kiro·Grok·VS Code는 상류에 공식
9
+ // 설치 경로가 없다(2026-08-02 검증).
10
+ const SEPARATE = ['codex', 'gemini', 'opencode', 'kimi', 'copilot']
11
+
2
12
  export default definePlugin({
3
13
  id: 'plugin.superpowers', label: 'superpowers', group: '__flow',
4
14
  installId: 'superpowers@claude-plugins-official',
5
15
  detectIds: ['superpowers@claude-plugins-official', 'superpowers@superpowers-marketplace'],
6
16
  note: 'item.plugin.superpowers.note',
17
+ unsupported: Object.fromEntries(
18
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [
19
+ c,
20
+ SEPARATE.includes(c) ? msg('item.unsupported.superpowersSeparate') : msg('item.unsupported.upstreamNone'),
21
+ ]),
22
+ ),
7
23
  })
@@ -1,15 +1,31 @@
1
1
  import { existsSync, readdirSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { defineSkill } from '../catalog.mjs'
4
- import { LocalizedError } from '../i18n/index.mjs'
4
+ import { CLI_IDS } from '../clis.mjs'
5
+ import { LocalizedError, msg } from '../i18n/index.mjs'
5
6
 
6
7
  function hasGsdFiles(dir) {
7
8
  return existsSync(dir) && readdirSync(dir).some((f) => f.startsWith('gsd-'))
8
9
  }
9
10
 
11
+ // 상류(v1.9.1)는 런타임 플래그 18개(--codex·--antigravity 등)를 지원하지만
12
+ // 이 항목은 --claude만 배선한다. Gemini CLI는 상류가 명시 제거했다(2026-06-18
13
+ // Google 종료, --gemini는 --antigravity 안내 후 종료). 상류의 --kilo는
14
+ // OpenCode 파생 "Kilo"(~/.config/kilo)라 이 저장소의 Kilo Code(.kilocode)와
15
+ // 같은 제품인지 미확인이라 지원으로 표기하지 않는다(2026-08-02 검증).
16
+ const FLAGS = ['codex', 'opencode', 'copilot', 'kimi']
17
+
10
18
  export default defineSkill({
11
19
  id: 'skill.gsd', label: 'GSD (Get Shit Done)', group: '__flow', scope: 'project',
12
20
  note: 'item.skill.gsd.note',
21
+ unsupported: Object.fromEntries(
22
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [
23
+ c,
24
+ FLAGS.includes(c) ? msg('item.unsupported.gsdFlag')
25
+ : c === 'gemini' ? msg('item.unsupported.gsdGemini')
26
+ : msg('item.unsupported.upstreamNone'),
27
+ ]),
28
+ ),
13
29
  async detect({ root }) {
14
30
  const found = hasGsdFiles(join(root, '.claude', 'commands')) || hasGsdFiles(join(root, '.claude', 'skills'))
15
31
  return { status: found ? 'installed' : 'absent' }
@@ -19,6 +35,9 @@ export default defineSkill({
19
35
  if (!r.ok) throw new LocalizedError('error.gsdInstall', { output: r.output })
20
36
  },
21
37
  async uninstall({ root, exec }) {
38
+ // 런타임 플래그가 없으면 상류가 기본값 claude를 지운다 — 지금은 --claude만
39
+ // 설치하므로 맞지만, 다른 런타임을 설치하게 되면 여기에도 같은 플래그를
40
+ // 넘겨야 엉뚱한 설치본이 지워지지 않는다.
22
41
  const r = await exec('npx', ['-y', '@opengsd/gsd-core@latest', '--uninstall'], { cwd: root })
23
42
  if (!r.ok) throw new LocalizedError('error.gsdUninstall', { output: r.output })
24
43
  },
@@ -1,15 +1,29 @@
1
1
  import { existsSync, rmSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { defineSkill } from '../catalog.mjs'
4
+ import { CLI_IDS } from '../clis.mjs'
4
5
  import { repoPath, repoPathStrict } from '../context.mjs'
5
6
  import { ensureGitignoreEntries } from '../gitignore.mjs'
6
- import { LocalizedError } from '../i18n/index.mjs'
7
+ import { LocalizedError, msg } from '../i18n/index.mjs'
7
8
 
8
9
  const REL_DIR = '.claude/skills/gstack'
9
10
 
11
+ // 상류 setup은 --host codex·kiro·opencode(와 factory)도 지원하지만 이 항목은
12
+ // 인자 없이 실행하며 setup의 기본값이 claude 단독이다. README 표에 있는
13
+ // --host cursor·slate는 setup의 case 문이 거부해 exit 1로 죽는다 —
14
+ // 확장한다면 안전한 값은 claude·codex·kiro·factory·opencode·auto뿐이다
15
+ // (2026-08-02 검증).
16
+ const HOSTS = ['codex', 'kiro', 'opencode']
17
+
10
18
  export default defineSkill({
11
19
  id: 'skill.gstack', label: 'gstack', group: '__flow', scope: 'project',
12
20
  note: 'item.skill.gstack.note',
21
+ unsupported: Object.fromEntries(
22
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [
23
+ c,
24
+ HOSTS.includes(c) ? msg('item.unsupported.gstackHost') : msg('item.unsupported.upstreamNone'),
25
+ ]),
26
+ ),
13
27
  async detect({ root }) {
14
28
  return { status: existsSync(repoPath(root, REL_DIR)) ? 'installed' : 'absent' }
15
29
  },
package/lib/tui/rows.mjs CHANGED
@@ -20,7 +20,7 @@ export const ACTION_SECTION = 'action'
20
20
  // design-md/scan.mjs의 BUNDLE_CATEGORY와 같은 값이다 — 카테고리를 못 얻은
21
21
  // 항목이 모이는 자리. 두 값이 갈리면 같은 그룹이 두 개로 쪼개진다.
22
22
  export const CATCH_ALL_CATEGORY = '__other'
23
- export const SECTION_ORDER = [ACTION_SECTION, 'plugin', 'mcp', 'skill', 'design']
23
+ export const SECTION_ORDER = [ACTION_SECTION, 'plugin', 'mcp', 'skill', 'config', 'design']
24
24
 
25
25
  // 탭 안쪽 소분류는 **성격**으로 가른다 — 같은 탭에 "무엇으로 설치되는가"가 같고
26
26
  // "무엇을 해 주는가"가 다른 항목이 섞여 있어서, 기구(plugin·mcp·skill)만으로는
@@ -123,6 +123,10 @@ function itemRow({ id, section, label, hint, fullHint = hint, statusDetail = nul
123
123
  id,
124
124
  section,
125
125
  group,
126
+ // 같은 파일을 두고 다투는 항목들의 묶음 이름(.gitmessage.txt의 두 언어판).
127
+ // 선택 배타는 state.mjs가 이 값만 보고 처리한다 — 순수 리듀서가 항목의
128
+ // 속내를 몰라도 되게 행에 미리 올려 둔다.
129
+ exclusive: item?.exclusive ?? null,
126
130
  label,
127
131
  hint,
128
132
  // 화면에 안 찍히는 긴 힌트. 비대화형 목록(printPlain)과 검색이 쓴다 —
@@ -142,6 +146,7 @@ function actionRow({ id, label, hint, run = null, t = createT('en') }) {
142
146
  id,
143
147
  section: ACTION_SECTION,
144
148
  group: null,
149
+ exclusive: null,
145
150
  label,
146
151
  hint,
147
152
  fullHint: hint,
package/lib/tui/state.mjs CHANGED
@@ -135,13 +135,40 @@ export function move(state, delta) {
135
135
  return cursor === state.cursor ? state : { ...state, cursor }
136
136
  }
137
137
 
138
+ // row.exclusive는 "이 항목들은 같은 파일 하나를 두고 다툰다"는 표시다
139
+ // (.gitmessage.txt의 영어판·한국어판). 하나를 켜면 나머지는 꺼진다 — 둘을 함께
140
+ // 고르면 나중에 적용된 쪽이 앞선 쪽을 조용히 덮어써, 화면의 체크 두 개가
141
+ // 디스크의 한 파일을 가리키는 거짓말이 된다.
142
+ //
143
+ // 화면 밖(검색·CLI 필터로 가려진) 형제도 끈다. 보이는 것만 건드린다는 이
144
+ // 모듈의 규칙에서 유일하게 벗어나는 자리인데, 여기서는 그래야 한다 —
145
+ // 가려진 형제를 남겨 두면 사용자가 방금 명시적으로 고른 판이 적용 순서에
146
+ // 따라 뒤집힐 수 있다.
147
+ function dropSiblings(rows, selected, row) {
148
+ if (!row.exclusive) return
149
+ for (const other of rows) {
150
+ if (other.id !== row.id && other.exclusive === row.exclusive) selected.delete(other.id)
151
+ }
152
+ }
153
+
154
+ // 형제 중 하나라도 이미 켜져 있는가. 배타 묶음은 "전부 켜짐"이 불가능하므로
155
+ // 전체 토글의 방향 판정도 이 기준으로 봐야 한다 — 아니면 Ctrl+A가 영원히
156
+ // 켜기만 하고 끄지 못한다.
157
+ function groupCovered(items, selected, row) {
158
+ return items.some((other) => other.exclusive === row.exclusive && selected.has(other.id))
159
+ }
160
+
138
161
  // 커서가 항목 행일 때만 뒤집는다. 액션 행은 제거 개념이 없어 체크 대상이 아니다.
139
162
  export function toggle(state) {
140
163
  const row = currentRow(state)
141
164
  if (row?.kind !== 'item') return state
142
165
  const selected = new Set(state.selected)
143
- if (selected.has(row.id)) selected.delete(row.id)
144
- else selected.add(row.id)
166
+ if (selected.has(row.id)) {
167
+ selected.delete(row.id)
168
+ } else {
169
+ selected.add(row.id)
170
+ dropSiblings(state.rows, selected, row)
171
+ }
145
172
  return { ...state, selected }
146
173
  }
147
174
 
@@ -151,10 +178,14 @@ export function toggleVisible(state, on) {
151
178
  const items = state.filtered.filter((r) => r.kind === 'item')
152
179
  if (items.length === 0) return state
153
180
  const selected = new Set(state.selected)
154
- const turnOn = on ?? !items.every((r) => selected.has(r.id))
181
+ const turnOn = on ?? !items.every((r) => selected.has(r.id) || (r.exclusive && groupCovered(items, selected, r)))
155
182
  for (const row of items) {
156
- if (turnOn) selected.add(row.id)
157
- else selected.delete(row.id)
183
+ if (!turnOn) { selected.delete(row.id); continue }
184
+ // 배타 묶음에서 이미 켜진 형제가 있으면 그 선택을 이긴다 — 전체 선택이
185
+ // 사용자가 고른 언어판을 목록 순서로 뒤집으면 안 된다.
186
+ if (row.exclusive && groupCovered(items, selected, row)) continue
187
+ selected.add(row.id)
188
+ dropSiblings(state.rows, selected, row)
158
189
  }
159
190
  return { ...state, selected }
160
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rch4com/agent-setup",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",