@rch4com/agent-setup 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +151 -3
- package/install.mjs +102 -33
- package/lib/args.mjs +104 -94
- package/lib/bootstrap/adapter.mjs +96 -95
- package/lib/bootstrap/apply.mjs +380 -269
- package/lib/bootstrap/flow.mjs +73 -54
- package/lib/bootstrap/record.mjs +154 -0
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -0
- package/lib/catalog.mjs +213 -145
- package/lib/claude-plugins.mjs +64 -64
- package/lib/clis.mjs +102 -102
- package/lib/context.mjs +66 -65
- package/lib/deps.mjs +5 -7
- package/lib/design-md/catalog.json +454 -454
- package/lib/design-md/catalog.mjs +266 -268
- package/lib/design-md/flow.mjs +181 -166
- package/lib/design-md/open.mjs +51 -50
- package/lib/design-md/providers/awesome-design-md.mjs +117 -114
- package/lib/design-md/providers/index.mjs +9 -9
- package/lib/design-md/scan.mjs +14 -10
- package/lib/engine.mjs +48 -44
- package/lib/gitignore.mjs +18 -18
- package/lib/i18n/catalog/en.mjs +369 -0
- package/lib/i18n/catalog/ko.mjs +359 -0
- package/lib/i18n/detect.mjs +39 -0
- package/lib/i18n/index.mjs +93 -0
- package/lib/items/mcp.codebase-memory.mjs +6 -6
- package/lib/items/mcp.graphify.mjs +8 -0
- package/lib/items/mcp.headroom.mjs +8 -0
- package/lib/items/mcp.notion.mjs +6 -6
- package/lib/items/mcp.supabase.mjs +6 -6
- package/lib/items/mcp.vercel.mjs +6 -6
- package/lib/items/plugin.bkit.mjs +7 -7
- package/lib/items/plugin.ecc.mjs +11 -0
- package/lib/items/plugin.impeccable.mjs +14 -0
- package/lib/items/plugin.mattpocock-skills.mjs +2 -2
- package/lib/items/plugin.ponytail.mjs +104 -0
- package/lib/items/plugin.superpowers.mjs +7 -7
- package/lib/items/plugin.understand-anything.mjs +10 -0
- package/lib/items/skill.caveman.mjs +7 -0
- package/lib/items/skill.gsd.mjs +25 -24
- package/lib/items/skill.gstack.mjs +40 -39
- package/lib/items/skill.karpathy.mjs +7 -0
- package/lib/items/skill.taste.mjs +9 -0
- package/lib/jsonfile.mjs +40 -40
- package/lib/status.mjs +93 -0
- package/lib/tomlfile.mjs +60 -60
- package/lib/tui/render.mjs +41 -63
- package/lib/tui/rows.mjs +278 -206
- package/lib/tui/run.mjs +64 -28
- package/lib/update.mjs +85 -0
- package/lib/width.mjs +57 -0
- package/package.json +36 -36
- package/lib/items/.omc/state/sessions/a859aff7-59d9-4ff6-afc6-8cafe4471dc4/pre-tool-advisory-throttle.json +0 -14
package/lib/args.mjs
CHANGED
|
@@ -6,72 +6,16 @@
|
|
|
6
6
|
// 예전에는 --set만 process.exit(2)로 빠져나가 같은 사용자 오류가
|
|
7
7
|
// 경로에 따라 1과 2로 갈렸다.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
export const BOOTSTRAP_USAGE = `사용법: npx @rch4com/agent-setup bootstrap [--skill-mode auto|link|copy] [--dry-run]
|
|
11
|
-
node install.mjs bootstrap [옵션] # 저장소에 설치기를 두고 쓸 때
|
|
12
|
-
|
|
13
|
-
옵션:
|
|
14
|
-
--skill-mode auto|link|copy 스킬 연결 방식을 지정합니다. (기본값: auto)
|
|
15
|
-
auto: 심볼릭 링크를 먼저 시도하고, 실패하면 복사로 전환합니다.
|
|
16
|
-
link: 심볼릭 링크만 시도합니다. 실패하면 오류로 종료합니다.
|
|
17
|
-
copy: 항상 복제본을 만듭니다.
|
|
18
|
-
--dry-run 아무것도 바꾸지 않고 예정된 동작만 출력합니다.
|
|
19
|
-
-h, --help 이 도움말을 출력하고 종료합니다.
|
|
20
|
-
|
|
21
|
-
런처로도 사용할 수 있습니다:
|
|
22
|
-
./setup-agents.sh [옵션]
|
|
23
|
-
pwsh -File ./setup-agents.ps1 [옵션]
|
|
24
|
-
--tui (또는 -Tui)를 주면 의존성을 설치하고 대화형 화면을 엽니다.
|
|
25
|
-
거기서 부트스트랩·에이전트·design.md를 한 목록에서 검색하고 고릅니다.`
|
|
9
|
+
import { LOCALES, LocalizedError, createT } from './i18n/index.mjs'
|
|
26
10
|
|
|
11
|
+
// 사용법은 모듈 로드 시점에 로케일을 모르므로 상수가 될 수 없다.
|
|
12
|
+
// `bootstrap` 서브커맨드 사용법. install.mjs와 두 런처(setup-agents.sh/.ps1) 모두에서 안내한다.
|
|
13
|
+
export const bootstrapUsage = (t) => t('usage.bootstrap')
|
|
27
14
|
// 서브커맨드 없는 최상위 사용법. `design`은 자기 사용법을 따로 갖는다.
|
|
28
|
-
export const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
저장소에 설치기를 두고 쓸 때는 npx @rch4com/agent-setup 자리에
|
|
33
|
-
node install.mjs 를 넣으면 됩니다.
|
|
34
|
-
|
|
35
|
-
옵션이 없으면 대화형 화면을 엽니다 — 부트스트랩·에이전트·design.md를
|
|
36
|
-
한 목록에서 검색하고 고릅니다.
|
|
37
|
-
|
|
38
|
-
옵션:
|
|
39
|
-
--list 현재 설치 상태만 출력하고 종료합니다.
|
|
40
|
-
--set <목록> 쉼표로 구분한 항목을 목표 상태로 맞춥니다.
|
|
41
|
-
전체 제거는 --set "" 로 명시하세요.
|
|
42
|
-
--skill-mode auto|link|copy 대화형 화면에서 실행하는 부트스트랩의 스킬
|
|
43
|
-
연결 방식입니다. (기본값: auto)
|
|
44
|
-
--design-dir <경로> design.md 소스를 더합니다. <소스id>=<경로>
|
|
45
|
-
형식도 받으며 반복 지정할 수 있습니다.
|
|
46
|
-
--dry-run 아무것도 바꾸지 않고 예정된 동작만 출력합니다.
|
|
47
|
-
-h, --help 이 도움말을 출력하고 종료합니다.
|
|
48
|
-
|
|
49
|
-
--list와 --set은 동작을 고르는 플래그라 함께 쓸 수 없습니다.
|
|
50
|
-
|
|
51
|
-
서브커맨드 도움말: npx @rch4com/agent-setup bootstrap --help
|
|
52
|
-
npx @rch4com/agent-setup design --help`
|
|
53
|
-
|
|
54
|
-
export const DESIGN_USAGE = `사용법: npx @rch4com/agent-setup design [옵션]
|
|
55
|
-
node install.mjs design [옵션] # 저장소에 설치기를 두고 쓸 때
|
|
56
|
-
|
|
57
|
-
옵션이 없으면 대화형 화면을 엽니다 — DESIGN.MD 섹션이 그 안에 있습니다.
|
|
58
|
-
|
|
59
|
-
옵션:
|
|
60
|
-
--list 카탈로그와 설치 상태를 출력합니다.
|
|
61
|
-
--set <목록> 쉼표로 구분한 항목을 목표 상태로 맞춥니다.
|
|
62
|
-
토큰은 <이름> 또는 <제공자>/<이름> 입니다.
|
|
63
|
-
전체 제거는 --set "" 로 명시하세요.
|
|
64
|
-
--preview <목록> 미리보기를 엽니다.
|
|
65
|
-
--sync=installed 설치본을 원본 최신으로 다시 받습니다.
|
|
66
|
-
--sync=catalog 사용 가능 목록·카테고리를 소스에서 다시 만듭니다.
|
|
67
|
-
--sync=stale 설치본을 원본과 해시 비교합니다.
|
|
68
|
-
--design-dir <경로> design.md 소스를 더합니다. <소스id>=<경로>
|
|
69
|
-
형식도 받으며 반복 지정할 수 있습니다.
|
|
70
|
-
--dry-run 아무것도 바꾸지 않고 예정된 동작만 출력합니다.
|
|
71
|
-
-h, --help 이 도움말을 출력하고 종료합니다.
|
|
72
|
-
|
|
73
|
-
--list, --set, --preview, --sync는 동작을 고르는 플래그라
|
|
74
|
-
한 번에 하나만 지정할 수 있습니다.`
|
|
15
|
+
export const rootUsage = (t) => t('usage.root')
|
|
16
|
+
export const designUsage = (t) => t('usage.design')
|
|
17
|
+
export const updateUsage = (t) => t('usage.update')
|
|
18
|
+
export const statusUsage = (t) => t('usage.status')
|
|
75
19
|
|
|
76
20
|
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
77
21
|
|
|
@@ -79,16 +23,18 @@ const SKILL_MODES = ['auto', 'link', 'copy']
|
|
|
79
23
|
// 미지 인자를 조용히 삼키지 않기 위한 것이다 — 오타(`--dryrun`)나 지원하지
|
|
80
24
|
// 않는 형식(`--set=a`)이 무시되면 사용자는 명령이 먹혔다고 믿지만 아무 일도
|
|
81
25
|
// 일어나지 않는다. bootstrap 파서만 지키던 규칙을 세 파서 모두로 넓힌다.
|
|
82
|
-
const
|
|
26
|
+
const COMMON_SPEC = { '-h': 'bool', '--help': 'bool', '--lang': 'value' }
|
|
83
27
|
const ROOT_SPEC = {
|
|
84
|
-
...
|
|
28
|
+
...COMMON_SPEC, '--dry-run': 'bool', '--list': 'bool',
|
|
85
29
|
'--set': 'value', '--skill-mode': 'value', '--design-dir': 'value',
|
|
86
30
|
}
|
|
87
31
|
const DESIGN_SPEC = {
|
|
88
|
-
...
|
|
32
|
+
...COMMON_SPEC, '--dry-run': 'bool', '--list': 'bool', '--set': 'value',
|
|
89
33
|
'--preview': 'value', '--sync': 'value', '--design-dir': 'value',
|
|
90
34
|
}
|
|
91
|
-
const BOOTSTRAP_SPEC = { ...
|
|
35
|
+
const BOOTSTRAP_SPEC = { ...COMMON_SPEC, '--dry-run': 'bool', '--adopt': 'bool', '--skill-mode': 'value' }
|
|
36
|
+
const UPDATE_SPEC = { ...COMMON_SPEC, '--dry-run': 'bool', '--force': 'bool' }
|
|
37
|
+
const STATUS_SPEC = { ...COMMON_SPEC, '--json': 'bool' }
|
|
92
38
|
|
|
93
39
|
export function assertKnownArgs(argv, spec, usage) {
|
|
94
40
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -96,8 +42,8 @@ export function assertKnownArgs(argv, spec, usage) {
|
|
|
96
42
|
const eq = token.startsWith('--') ? token.indexOf('=') : -1
|
|
97
43
|
const name = eq > 0 ? token.slice(0, eq) : token
|
|
98
44
|
const kind = spec[name]
|
|
99
|
-
if (kind === undefined) throw new
|
|
100
|
-
if (kind === 'bool' && eq > 0) throw new
|
|
45
|
+
if (kind === undefined) throw new LocalizedError('error.unknownArg', { token, usage })
|
|
46
|
+
if (kind === 'bool' && eq > 0) throw new LocalizedError('error.flagTakesNoValue', { name, token, usage })
|
|
101
47
|
// `--플래그 값` 형태는 값 한 칸을 건너뛴다. `--플래그=값`은 이미 한 토큰이다.
|
|
102
48
|
if (kind === 'value' && eq < 0) i++
|
|
103
49
|
}
|
|
@@ -111,13 +57,13 @@ export function assertSingleAction(argv, names, usage) {
|
|
|
111
57
|
const given = names.filter((name) => hasFlag(argv, name))
|
|
112
58
|
if (given.length > 1) {
|
|
113
59
|
// 조사를 붙이지 않는다 — 플래그 이름에 따라 은/는이 갈려 문장이 어색해진다.
|
|
114
|
-
throw new
|
|
60
|
+
throw new LocalizedError('error.singleAction', { given: given.join(', '), usage })
|
|
115
61
|
}
|
|
116
62
|
}
|
|
117
63
|
|
|
118
64
|
function checkSkillMode(value, usage) {
|
|
119
65
|
if (!SKILL_MODES.includes(value)) {
|
|
120
|
-
throw new
|
|
66
|
+
throw new LocalizedError('error.badSkillMode', { list: SKILL_MODES.join(', '), value, usage })
|
|
121
67
|
}
|
|
122
68
|
return value
|
|
123
69
|
}
|
|
@@ -130,7 +76,7 @@ export function parseSkillMode(argv, usage) {
|
|
|
130
76
|
if (a === '--skill-mode') {
|
|
131
77
|
const value = argv[i + 1]
|
|
132
78
|
if (value === undefined || value.startsWith('--')) {
|
|
133
|
-
throw new
|
|
79
|
+
throw new LocalizedError('error.skillModeNeedsValue', { list: SKILL_MODES.join(', '), usage })
|
|
134
80
|
}
|
|
135
81
|
mode = checkSkillMode(value, usage)
|
|
136
82
|
i++
|
|
@@ -141,6 +87,45 @@ export function parseSkillMode(argv, usage) {
|
|
|
141
87
|
return mode
|
|
142
88
|
}
|
|
143
89
|
|
|
90
|
+
function checkLang(value, usage) {
|
|
91
|
+
if (!LOCALES.includes(value)) {
|
|
92
|
+
throw new LocalizedError('error.badLang', { list: LOCALES.join(', '), value, usage })
|
|
93
|
+
}
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `--lang <값>`과 `--lang=<값>`을 모두 받는다. 없으면 null —
|
|
98
|
+
// 이 자리에서 기본값을 정하지 않는다. 우선순위 판단은 resolveLocale의 몫이다.
|
|
99
|
+
export function parseLang(argv, usage) {
|
|
100
|
+
let lang = null
|
|
101
|
+
for (let i = 0; i < argv.length; i++) {
|
|
102
|
+
const a = argv[i]
|
|
103
|
+
if (a === '--lang') {
|
|
104
|
+
const value = argv[i + 1]
|
|
105
|
+
if (value === undefined || value.startsWith('--')) {
|
|
106
|
+
throw new LocalizedError('error.langNeedsValue', { list: LOCALES.join(', '), usage })
|
|
107
|
+
}
|
|
108
|
+
lang = checkLang(value, usage)
|
|
109
|
+
i++
|
|
110
|
+
} else if (a.startsWith('--lang=')) {
|
|
111
|
+
lang = checkLang(a.slice('--lang='.length), usage)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lang
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 진입점이 로케일을 정하기 **전에** 부르는 관대한 사전 스캔이다.
|
|
118
|
+
// 여기서 던지면 인자 오류가 늘 영어로 나온다 — 정식 파서가 지역화된
|
|
119
|
+
// 오류를 내도록 미지원 값은 조용히 null로 넘긴다.
|
|
120
|
+
export function preScanLang(argv) {
|
|
121
|
+
for (let i = 0; i < argv.length; i++) {
|
|
122
|
+
const a = argv[i]
|
|
123
|
+
const value = a === '--lang' ? argv[i + 1] : a.startsWith('--lang=') ? a.slice('--lang='.length) : null
|
|
124
|
+
if (typeof value === 'string' && LOCALES.includes(value)) return value
|
|
125
|
+
}
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
144
129
|
function wantsHelp(argv) {
|
|
145
130
|
return argv.includes('-h') || argv.includes('--help')
|
|
146
131
|
}
|
|
@@ -155,12 +140,12 @@ export function requireValue(argv, name) {
|
|
|
155
140
|
const eq = argv.find((a) => a.startsWith(`${name}=`))
|
|
156
141
|
if (eq !== undefined) {
|
|
157
142
|
const value = eq.slice(name.length + 1)
|
|
158
|
-
if (!value) throw new
|
|
143
|
+
if (!value) throw new LocalizedError('error.needsValue', { name })
|
|
159
144
|
return value
|
|
160
145
|
}
|
|
161
146
|
const value = argv[argv.indexOf(name) + 1]
|
|
162
147
|
if (value === undefined || value.startsWith('--')) {
|
|
163
|
-
throw new
|
|
148
|
+
throw new LocalizedError('error.needsValue', { name })
|
|
164
149
|
}
|
|
165
150
|
return value
|
|
166
151
|
}
|
|
@@ -171,12 +156,12 @@ export function collectValues(argv, name) {
|
|
|
171
156
|
for (let i = 0; i < argv.length; i++) {
|
|
172
157
|
if (argv[i] === name) {
|
|
173
158
|
const value = argv[i + 1]
|
|
174
|
-
if (value === undefined || value.startsWith('--')) throw new
|
|
159
|
+
if (value === undefined || value.startsWith('--')) throw new LocalizedError('error.needsValue', { name })
|
|
175
160
|
values.push(value)
|
|
176
161
|
i++
|
|
177
162
|
} else if (argv[i].startsWith(`${name}=`)) {
|
|
178
163
|
const value = argv[i].slice(name.length + 1)
|
|
179
|
-
if (!value) throw new
|
|
164
|
+
if (!value) throw new LocalizedError('error.needsValue', { name })
|
|
180
165
|
values.push(value)
|
|
181
166
|
}
|
|
182
167
|
}
|
|
@@ -192,7 +177,7 @@ export function parseSetArg(argv) {
|
|
|
192
177
|
if (!argv.includes('--set')) return null
|
|
193
178
|
const value = argv[argv.indexOf('--set') + 1]
|
|
194
179
|
if (value === undefined || value.startsWith('--')) {
|
|
195
|
-
throw new
|
|
180
|
+
throw new LocalizedError('error.setNeedsValue')
|
|
196
181
|
}
|
|
197
182
|
return value
|
|
198
183
|
}
|
|
@@ -201,12 +186,13 @@ export function parseSetArg(argv) {
|
|
|
201
186
|
// 먼저 내밀 이유가 없다. 아래 두 파서가 같은 규칙을 따른다.
|
|
202
187
|
|
|
203
188
|
// 서브커맨드 없는 최상위 인자.
|
|
204
|
-
export function parseRootArgs(argv) {
|
|
189
|
+
export function parseRootArgs(argv, t = createT('en')) {
|
|
190
|
+
const usage = rootUsage(t)
|
|
205
191
|
if (wantsHelp(argv)) {
|
|
206
|
-
return { help: true, dryRun: false, listOnly: false, setArg: null, skillMode: 'auto', designDirs: [], interactive: false }
|
|
192
|
+
return { help: true, dryRun: false, listOnly: false, setArg: null, skillMode: 'auto', designDirs: [], lang: null, interactive: false }
|
|
207
193
|
}
|
|
208
|
-
assertKnownArgs(argv, ROOT_SPEC,
|
|
209
|
-
assertSingleAction(argv, ['--list', '--set'],
|
|
194
|
+
assertKnownArgs(argv, ROOT_SPEC, usage)
|
|
195
|
+
assertSingleAction(argv, ['--list', '--set'], usage)
|
|
210
196
|
const setArg = parseSetArg(argv)
|
|
211
197
|
return {
|
|
212
198
|
help: false,
|
|
@@ -215,20 +201,23 @@ export function parseRootArgs(argv) {
|
|
|
215
201
|
setArg,
|
|
216
202
|
// 대화형 화면에서 실행하는 부트스트랩이 이 값을 쓴다. 예전에는 루트에서
|
|
217
203
|
// 읽지 않아, 런처가 넘긴 --skill-mode가 조용히 버려졌다.
|
|
218
|
-
skillMode: parseSkillMode(argv,
|
|
204
|
+
skillMode: parseSkillMode(argv, usage),
|
|
219
205
|
designDirs: collectValues(argv, '--design-dir'),
|
|
220
|
-
|
|
206
|
+
lang: parseLang(argv, usage),
|
|
207
|
+
// 목록·집합 지정이 없으면 대화형 화면으로 간다. --lang은 동작 플래그가
|
|
208
|
+
// 아니므로 여기에 끼지 않는다.
|
|
221
209
|
interactive: !argv.includes('--list') && setArg === null,
|
|
222
210
|
}
|
|
223
211
|
}
|
|
224
212
|
|
|
225
213
|
// `design` 서브커맨드 플래그.
|
|
226
|
-
export function parseDesignArgs(argv) {
|
|
214
|
+
export function parseDesignArgs(argv, t = createT('en')) {
|
|
215
|
+
const usage = designUsage(t)
|
|
227
216
|
if (wantsHelp(argv)) {
|
|
228
|
-
return { help: true, dryRun: false, list: false, set: null, preview: null, sync: null, designDirs: [], interactive: false }
|
|
217
|
+
return { help: true, dryRun: false, list: false, set: null, preview: null, sync: null, designDirs: [], lang: null, interactive: false }
|
|
229
218
|
}
|
|
230
|
-
assertKnownArgs(argv, DESIGN_SPEC,
|
|
231
|
-
assertSingleAction(argv, ['--list', '--set', '--preview', '--sync'],
|
|
219
|
+
assertKnownArgs(argv, DESIGN_SPEC, usage)
|
|
220
|
+
assertSingleAction(argv, ['--list', '--set', '--preview', '--sync'], usage)
|
|
232
221
|
const list = argv.includes('--list')
|
|
233
222
|
const set = parseSetArg(argv)
|
|
234
223
|
const preview = hasFlag(argv, '--preview') ? requireValue(argv, '--preview') : null
|
|
@@ -237,7 +226,7 @@ export function parseDesignArgs(argv) {
|
|
|
237
226
|
const s = argv.find((a) => a === '--sync' || a.startsWith('--sync='))
|
|
238
227
|
if (s) {
|
|
239
228
|
const m = /^--sync=(installed|catalog|stale)$/.exec(s)
|
|
240
|
-
if (!m) throw new
|
|
229
|
+
if (!m) throw new LocalizedError('error.badSync')
|
|
241
230
|
sync = m[1]
|
|
242
231
|
}
|
|
243
232
|
|
|
@@ -249,18 +238,39 @@ export function parseDesignArgs(argv) {
|
|
|
249
238
|
preview,
|
|
250
239
|
sync,
|
|
251
240
|
designDirs: collectValues(argv, '--design-dir'),
|
|
241
|
+
lang: parseLang(argv, usage),
|
|
252
242
|
interactive: !list && set === null && preview === null && sync === null,
|
|
253
243
|
}
|
|
254
244
|
}
|
|
255
245
|
|
|
256
|
-
// `bootstrap`에 허용되는 플래그: --dry-run, --
|
|
246
|
+
// `bootstrap`에 허용되는 플래그: --dry-run, --adopt,
|
|
247
|
+
// --skill-mode(+값)/--skill-mode=값, --lang(+값)/--lang=값, -h/--help.
|
|
257
248
|
// 그 외 인자는 거부한다 — 조용히 삼켜지는 것을 막기 위해서다.
|
|
258
|
-
export function parseBootstrapArgs(argv) {
|
|
259
|
-
|
|
260
|
-
|
|
249
|
+
export function parseBootstrapArgs(argv, t = createT('en')) {
|
|
250
|
+
const usage = bootstrapUsage(t)
|
|
251
|
+
if (wantsHelp(argv)) return { dryRun: false, skillMode: 'auto', adopt: false, help: true, lang: null }
|
|
252
|
+
assertKnownArgs(argv, BOOTSTRAP_SPEC, usage)
|
|
261
253
|
return {
|
|
262
254
|
dryRun: argv.includes('--dry-run'),
|
|
263
|
-
|
|
255
|
+
adopt: argv.includes('--adopt'),
|
|
256
|
+
skillMode: parseSkillMode(argv, usage),
|
|
257
|
+
lang: parseLang(argv, usage),
|
|
264
258
|
help: false,
|
|
265
259
|
}
|
|
266
260
|
}
|
|
261
|
+
|
|
262
|
+
// `update`에 허용되는 플래그: --dry-run, --force, --lang(+값)/--lang=값, -h/--help.
|
|
263
|
+
export function parseUpdateArgs(argv, t = createT('en')) {
|
|
264
|
+
const usage = updateUsage(t)
|
|
265
|
+
if (wantsHelp(argv)) return { help: true, dryRun: false, force: false, lang: null }
|
|
266
|
+
assertKnownArgs(argv, UPDATE_SPEC, usage)
|
|
267
|
+
return { help: false, dryRun: argv.includes('--dry-run'), force: argv.includes('--force'), lang: parseLang(argv, usage) }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// `status`에 허용되는 플래그: --json, --lang(+값)/--lang=값, -h/--help.
|
|
271
|
+
export function parseStatusArgs(argv, t = createT('en')) {
|
|
272
|
+
const usage = statusUsage(t)
|
|
273
|
+
if (wantsHelp(argv)) return { help: true, json: false, lang: null }
|
|
274
|
+
assertKnownArgs(argv, STATUS_SPEC, usage)
|
|
275
|
+
return { help: false, json: argv.includes('--json'), lang: parseLang(argv, usage) }
|
|
276
|
+
}
|
|
@@ -1,95 +1,96 @@
|
|
|
1
|
-
// 스킬 어댑터 — .agents/skills를 도구별 경로에서 보이게 한다.
|
|
2
|
-
// Windows는 Junction을 쓴다. 관리자 권한이 필요 없고, MSYS(Git Bash)의 ln -s가
|
|
3
|
-
// 링크 대신 복사를 만드는 문제도 우회한다.
|
|
4
|
-
import { cpSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
5
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
6
|
-
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
1
|
+
// 스킬 어댑터 — .agents/skills를 도구별 경로에서 보이게 한다.
|
|
2
|
+
// Windows는 Junction을 쓴다. 관리자 권한이 필요 없고, MSYS(Git Bash)의 ln -s가
|
|
3
|
+
// 링크 대신 복사를 만드는 문제도 우회한다.
|
|
4
|
+
import { cpSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
6
|
+
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
7
|
+
import { createT, msg } from '../i18n/index.mjs'
|
|
8
|
+
import { pathExists } from './apply.mjs'
|
|
9
|
+
|
|
10
|
+
const MARKER = '.agent-kit-managed-copy'
|
|
11
|
+
const SOURCE_REL = '.agents/skills'
|
|
12
|
+
|
|
13
|
+
// 심볼릭 링크/Junction이면 가리키는 절대 경로를, 아니면 null을 돌려준다.
|
|
14
|
+
function linkTarget(target) {
|
|
15
|
+
try {
|
|
16
|
+
if (!lstatSync(target).isSymbolicLink()) return null
|
|
17
|
+
return resolve(dirname(target), readlinkSync(target))
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createLink(source, target) {
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
// Junction은 절대 경로를 요구한다.
|
|
26
|
+
symlinkSync(source, target, 'junction')
|
|
27
|
+
} else {
|
|
28
|
+
symlinkSync(relative(dirname(target), source), target)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createCopy(source, target) {
|
|
33
|
+
mkdirSync(target, { recursive: true })
|
|
34
|
+
cpSync(source, target, { recursive: true })
|
|
35
|
+
writeFileSync(join(target, MARKER), '', { encoding: 'utf8' })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function configureAdapter(root, { tool, path: rel }, { dryRun = false, skillMode = 'auto', log, t = createT('en') }) {
|
|
39
|
+
// 기존 상태를 살펴보는 데에는 어휘적 경로를 쓴다. target이 저장소 밖을 가리키는
|
|
40
|
+
// 링크일 수 있는데, 그 경우도 "보존 + 경고"가 정답이라 지켜야 할 쓰기가 없다.
|
|
41
|
+
// repoPathStrict를 여기서 쓰면 이탈 링크에서 예외가 던져져 경고 대신 죽는다.
|
|
42
|
+
const target = repoPath(root, rel)
|
|
43
|
+
|
|
44
|
+
const linked = linkTarget(target)
|
|
45
|
+
if (linked !== null) {
|
|
46
|
+
if (linked === repoPath(root, SOURCE_REL)) {
|
|
47
|
+
log(t('log.skill.linkOk', { tool, path: rel }))
|
|
48
|
+
return { ok: true, action: 'skip', path: rel }
|
|
49
|
+
}
|
|
50
|
+
log(t('log.skill.warnForeignLink', { path: rel }))
|
|
51
|
+
return { ok: true, action: 'warn', path: rel, message: msg('msg.foreignLink') }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const managedCopy = pathExists(target) && pathExists(join(target, MARKER))
|
|
55
|
+
if (pathExists(target) && !managedCopy) {
|
|
56
|
+
log(t('log.skill.warnUnmanaged', { path: rel }))
|
|
57
|
+
return { ok: true, action: 'warn', path: rel, message: msg('msg.unmanagedExisting') }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 여기부터는 실제로 링크/복사본을 만드는 경로다. 엄격 검사는 dry-run 여부와
|
|
61
|
+
// 무관하게 항상 하고(dry-run에서도 이탈 경로를 걸러내려면), 실제 파일시스템
|
|
62
|
+
// 쓰기만 !dryRun으로 막는다 — apply.mjs의 ensureDirs와 같은 형태.
|
|
63
|
+
// source(.agents/skills)는 실제로 읽어 복사하는 대상이므로 여기서 함께 검사한다.
|
|
64
|
+
const source = repoPathStrict(root, SOURCE_REL)
|
|
65
|
+
const strictTarget = repoPathStrict(root, rel)
|
|
66
|
+
|
|
67
|
+
if (dryRun) {
|
|
68
|
+
log(t('log.skill.plan', { tool, path: rel, mode: skillMode }))
|
|
69
|
+
return { ok: true, action: 'skip', path: rel }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (managedCopy) {
|
|
73
|
+
log(t('log.skill.copySync', { tool, path: rel }))
|
|
74
|
+
// 마커가 확인된 복사본만 지운다.
|
|
75
|
+
rmSync(strictTarget, { recursive: true, force: true })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
mkdirSync(dirname(strictTarget), { recursive: true })
|
|
79
|
+
|
|
80
|
+
if (skillMode !== 'copy') {
|
|
81
|
+
try {
|
|
82
|
+
createLink(source, strictTarget)
|
|
83
|
+
log(t('log.skill.linkCreate', { tool, path: rel, target: SOURCE_REL }))
|
|
84
|
+
return { ok: true, action: 'link', path: rel }
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (skillMode === 'link') {
|
|
87
|
+
return { ok: false, action: 'link', path: rel, message: msg('msg.linkFailed', { message: err.message }) }
|
|
88
|
+
}
|
|
89
|
+
log(t('log.skill.linkFellBack', { tool }))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
createCopy(source, strictTarget)
|
|
94
|
+
log(t('log.skill.copyCreate', { tool, path: rel }))
|
|
95
|
+
return { ok: true, action: 'copy', path: rel }
|
|
96
|
+
}
|