@rch4com/agent-setup 1.6.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 +2 -1
- package/lib/engine.mjs +16 -1
- package/lib/gitmessage.mjs +227 -0
- package/lib/i18n/catalog/en.mjs +11 -0
- package/lib/i18n/catalog/ko.mjs +10 -0
- package/lib/items/config.gitmessage.en.mjs +10 -0
- package/lib/items/config.gitmessage.ko.mjs +11 -0
- package/lib/tui/rows.mjs +6 -1
- package/lib/tui/state.mjs +36 -5
- package/package.json +1 -1
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/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
|
+
}
|
package/lib/i18n/catalog/en.mjs
CHANGED
|
@@ -261,6 +261,16 @@ export default {
|
|
|
261
261
|
'item.skill.karpathy.note': 'behavioral guidelines that curb overbuilding and vague success criteria',
|
|
262
262
|
'item.skill.gstack.note': 'repo-local clone + setup (needs bash; Git Bash on Windows). Runtime state (~/.gstack) may be created globally.',
|
|
263
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}',
|
|
264
274
|
'error.gstackClone': 'gstack clone failed: {output}',
|
|
265
275
|
'error.gstackSetup': 'gstack setup failed: {output}',
|
|
266
276
|
'error.gsdInstall': 'GSD install failed: {output}',
|
|
@@ -277,6 +287,7 @@ export default {
|
|
|
277
287
|
'section.plugin': 'PLUGIN',
|
|
278
288
|
'section.mcp': 'MCP',
|
|
279
289
|
'section.skill': 'SKILL',
|
|
290
|
+
'section.config': 'CONFIG',
|
|
280
291
|
'section.design': 'DESIGN.MD',
|
|
281
292
|
|
|
282
293
|
// TUI 목록·검토 화면 문구. item.*는 agentHint가, action.*.label/hint는
|
package/lib/i18n/catalog/ko.mjs
CHANGED
|
@@ -254,6 +254,15 @@ export default {
|
|
|
254
254
|
'item.skill.karpathy.note': '과설계와 모호한 완료 기준을 줄이는 행동 지침',
|
|
255
255
|
'item.skill.gstack.note': '저장소 로컬 clone + setup (bash 필요, Windows는 Git Bash). 런타임 상태(~/.gstack)는 전역에 생길 수 있음.',
|
|
256
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}',
|
|
257
266
|
'error.gstackClone': 'gstack clone 실패: {output}',
|
|
258
267
|
'error.gstackSetup': 'gstack setup 실패: {output}',
|
|
259
268
|
'error.gsdInstall': 'GSD 설치 실패: {output}',
|
|
@@ -270,6 +279,7 @@ export default {
|
|
|
270
279
|
'section.plugin': 'PLUGIN',
|
|
271
280
|
'section.mcp': 'MCP',
|
|
272
281
|
'section.skill': 'SKILL',
|
|
282
|
+
'section.config': 'CONFIG',
|
|
273
283
|
'section.design': 'DESIGN.MD',
|
|
274
284
|
|
|
275
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
|
+
})
|
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))
|
|
144
|
-
|
|
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.
|
|
157
|
-
|
|
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.
|
|
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",
|