@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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +151 -3
  3. package/install.mjs +102 -33
  4. package/lib/args.mjs +104 -94
  5. package/lib/bootstrap/adapter.mjs +96 -95
  6. package/lib/bootstrap/apply.mjs +380 -269
  7. package/lib/bootstrap/flow.mjs +73 -54
  8. package/lib/bootstrap/record.mjs +154 -0
  9. package/lib/bootstrap/templates.mjs +148 -148
  10. package/lib/bootstrap/text.mjs +19 -0
  11. package/lib/catalog.mjs +213 -145
  12. package/lib/claude-plugins.mjs +64 -64
  13. package/lib/clis.mjs +102 -102
  14. package/lib/context.mjs +66 -65
  15. package/lib/deps.mjs +5 -7
  16. package/lib/design-md/catalog.json +454 -454
  17. package/lib/design-md/catalog.mjs +266 -268
  18. package/lib/design-md/flow.mjs +181 -166
  19. package/lib/design-md/open.mjs +51 -50
  20. package/lib/design-md/providers/awesome-design-md.mjs +117 -114
  21. package/lib/design-md/providers/index.mjs +9 -9
  22. package/lib/design-md/scan.mjs +14 -10
  23. package/lib/engine.mjs +48 -44
  24. package/lib/gitignore.mjs +18 -18
  25. package/lib/i18n/catalog/en.mjs +369 -0
  26. package/lib/i18n/catalog/ko.mjs +359 -0
  27. package/lib/i18n/detect.mjs +39 -0
  28. package/lib/i18n/index.mjs +93 -0
  29. package/lib/items/mcp.codebase-memory.mjs +6 -6
  30. package/lib/items/mcp.graphify.mjs +8 -0
  31. package/lib/items/mcp.headroom.mjs +8 -0
  32. package/lib/items/mcp.notion.mjs +6 -6
  33. package/lib/items/mcp.supabase.mjs +6 -6
  34. package/lib/items/mcp.vercel.mjs +6 -6
  35. package/lib/items/plugin.bkit.mjs +7 -7
  36. package/lib/items/plugin.ecc.mjs +11 -0
  37. package/lib/items/plugin.impeccable.mjs +14 -0
  38. package/lib/items/plugin.mattpocock-skills.mjs +2 -2
  39. package/lib/items/plugin.ponytail.mjs +104 -0
  40. package/lib/items/plugin.superpowers.mjs +7 -7
  41. package/lib/items/plugin.understand-anything.mjs +10 -0
  42. package/lib/items/skill.caveman.mjs +7 -0
  43. package/lib/items/skill.gsd.mjs +25 -24
  44. package/lib/items/skill.gstack.mjs +40 -39
  45. package/lib/items/skill.karpathy.mjs +7 -0
  46. package/lib/items/skill.taste.mjs +9 -0
  47. package/lib/jsonfile.mjs +40 -40
  48. package/lib/status.mjs +93 -0
  49. package/lib/tomlfile.mjs +60 -60
  50. package/lib/tui/render.mjs +41 -63
  51. package/lib/tui/rows.mjs +278 -206
  52. package/lib/tui/run.mjs +64 -28
  53. package/lib/update.mjs +85 -0
  54. package/lib/width.mjs +57 -0
  55. package/package.json +36 -36
  56. 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
- // `bootstrap` 서브커맨드 사용법. install.mjs와 두 런처(setup-agents.sh/.ps1) 모두에서 안내한다.
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 ROOT_USAGE = `사용법: npx @rch4com/agent-setup [옵션]
29
- npx @rch4com/agent-setup bootstrap [옵션]
30
- npx @rch4com/agent-setup design [옵션]
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 HELP_SPEC = { '-h': 'bool', '--help': 'bool' }
26
+ const COMMON_SPEC = { '-h': 'bool', '--help': 'bool', '--lang': 'value' }
83
27
  const ROOT_SPEC = {
84
- ...HELP_SPEC, '--dry-run': 'bool', '--list': 'bool',
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
- ...HELP_SPEC, '--dry-run': 'bool', '--list': 'bool', '--set': 'value',
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 = { ...HELP_SPEC, '--dry-run': 'bool', '--skill-mode': 'value' }
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 Error(`알 수 없는 인자입니다: ${token}\n\n${usage}`)
100
- if (kind === 'bool' && eq > 0) throw new Error(`${name}에는 값을 줄 수 없습니다: ${token}\n\n${usage}`)
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 Error(`함께 쓸 수 없습니다: ${given.join(', ')} — 동작 플래그는 한 번에 하나만 지정하세요.\n\n${usage}`)
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 Error(`--skill-mode는 ${SKILL_MODES.join(', ')} 중 하나여야 합니다: ${value}\n\n${usage}`)
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 Error(`--skill-mode 뒤에 값이 필요합니다 (${SKILL_MODES.join(', ')} 중 하나).\n\n${usage}`)
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 Error(`${name} 뒤에 값이 필요합니다.`)
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 Error(`${name} 뒤에 값이 필요합니다.`)
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 Error(`${name} 뒤에 값이 필요합니다.`)
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 Error(`${name} 뒤에 값이 필요합니다.`)
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 Error('--set 뒤에 항목 목록이 필요합니다. 전체 제거는 --set "" 로 명시하세요.')
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, ROOT_USAGE)
209
- assertSingleAction(argv, ['--list', '--set'], ROOT_USAGE)
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, ROOT_USAGE),
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, DESIGN_USAGE)
231
- assertSingleAction(argv, ['--list', '--set', '--preview', '--sync'], DESIGN_USAGE)
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 Error('--sync=installed|catalog|stale 형식으로 지정하세요.')
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, --skill-mode(+값)/--skill-mode=값, -h/--help.
246
+ // `bootstrap`에 허용되는 플래그: --dry-run, --adopt,
247
+ // --skill-mode(+값)/--skill-mode=값, --lang(+값)/--lang=값, -h/--help.
257
248
  // 그 외 인자는 거부한다 — 조용히 삼켜지는 것을 막기 위해서다.
258
- export function parseBootstrapArgs(argv) {
259
- if (wantsHelp(argv)) return { dryRun: false, skillMode: 'auto', help: true }
260
- assertKnownArgs(argv, BOOTSTRAP_SPEC, BOOTSTRAP_USAGE)
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
- skillMode: parseSkillMode(argv, BOOTSTRAP_USAGE),
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 { pathExists } from './apply.mjs'
8
-
9
- const MARKER = '.agent-kit-managed-copy'
10
- const SOURCE_REL = '.agents/skills'
11
-
12
- // 심볼릭 링크/Junction이면 가리키는 절대 경로를, 아니면 null을 돌려준다.
13
- function linkTarget(target) {
14
- try {
15
- if (!lstatSync(target).isSymbolicLink()) return null
16
- return resolve(dirname(target), readlinkSync(target))
17
- } catch {
18
- return null
19
- }
20
- }
21
-
22
- function createLink(source, target) {
23
- if (process.platform === 'win32') {
24
- // Junction은 절대 경로를 요구한다.
25
- symlinkSync(source, target, 'junction')
26
- } else {
27
- symlinkSync(relative(dirname(target), source), target)
28
- }
29
- }
30
-
31
- function createCopy(source, target) {
32
- mkdirSync(target, { recursive: true })
33
- cpSync(source, target, { recursive: true })
34
- writeFileSync(join(target, MARKER), '', { encoding: 'utf8' })
35
- }
36
-
37
- export function configureAdapter(root, { tool, path: rel }, { dryRun = false, skillMode = 'auto', log }) {
38
- // 기존 상태를 살펴보는 데에는 어휘적 경로를 쓴다. target이 저장소 밖을 가리키는
39
- // 링크일 수 있는데, 그 경우도 "보존 + 경고"가 정답이라 지켜야 할 쓰기가 없다.
40
- // repoPathStrict를 여기서 쓰면 이탈 링크에서 예외가 던져져 경고 대신 죽는다.
41
- const target = repoPath(root, rel)
42
-
43
- const linked = linkTarget(target)
44
- if (linked !== null) {
45
- if (linked === repoPath(root, SOURCE_REL)) {
46
- log(`${tool} 스킬 링크 확인: ${rel}`)
47
- return { ok: true, action: 'skip', path: rel }
48
- }
49
- log(`경고: ${rel} 경로가 다른 위치를 가리키는 링크입니다. 변경하지 않습니다.`)
50
- return { ok: true, action: 'warn', path: rel, message: '다른 위치를 가리키는 링크' }
51
- }
52
-
53
- const managedCopy = pathExists(target) && pathExists(join(target, MARKER))
54
- if (pathExists(target) && !managedCopy) {
55
- log(`경고: ${rel} 경로가 이미 존재하며 agent-kit 관리 대상이 아닙니다. 변경하지 않습니다.`)
56
- return { ok: true, action: 'warn', path: rel, message: '관리 대상이 아닌 기존 항목' }
57
- }
58
-
59
- // 여기부터는 실제로 링크/복사본을 만드는 경로다. 엄격 검사는 dry-run 여부와
60
- // 무관하게 항상 하고(dry-run에서도 이탈 경로를 걸러내려면), 실제 파일시스템
61
- // 쓰기만 !dryRun으로 막는다 — apply.mjs의 ensureDirs와 같은 형태.
62
- // source(.agents/skills)는 실제로 읽어 복사하는 대상이므로 여기서 함께 검사한다.
63
- const source = repoPathStrict(root, SOURCE_REL)
64
- const strictTarget = repoPathStrict(root, rel)
65
-
66
- if (dryRun) {
67
- log(`${tool} 스킬 어댑터 생성 예정: ${rel} (${skillMode})`)
68
- return { ok: true, action: 'skip', path: rel }
69
- }
70
-
71
- if (managedCopy) {
72
- log(`${tool} 스킬 복제본 동기화: ${rel}`)
73
- // 마커가 확인된 복사본만 지운다.
74
- rmSync(strictTarget, { recursive: true, force: true })
75
- }
76
-
77
- mkdirSync(dirname(strictTarget), { recursive: true })
78
-
79
- if (skillMode !== 'copy') {
80
- try {
81
- createLink(source, strictTarget)
82
- log(`${tool} 스킬 링크 생성: ${rel} -> ${SOURCE_REL}`)
83
- return { ok: true, action: 'link', path: rel }
84
- } catch (err) {
85
- if (skillMode === 'link') {
86
- return { ok: false, action: 'link', path: rel, message: `링크 생성 실패: ${err.message}` }
87
- }
88
- log(`경고: ${tool} 링크 생성에 실패하여 복사 방식으로 전환합니다.`)
89
- }
90
- }
91
-
92
- createCopy(source, strictTarget)
93
- log(`${tool} 스킬 복제본 생성: ${rel}`)
94
- return { ok: true, action: 'copy', path: rel }
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
+ }