@rch4com/agent-setup 1.1.0 → 1.4.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 +197 -55
- package/install.mjs +152 -101
- package/lib/args.mjs +276 -311
- package/lib/bootstrap/adapter.mjs +96 -95
- package/lib/bootstrap/apply.mjs +380 -379
- package/lib/bootstrap/flow.mjs +73 -70
- package/lib/bootstrap/record.mjs +154 -142
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -19
- 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/cache/awesome-design-md/LICENSE.md +38 -0
- 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 +124 -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 -92
- 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 -85
- package/lib/width.mjs +57 -0
- package/package.json +36 -36
package/lib/catalog.mjs
CHANGED
|
@@ -1,145 +1,213 @@
|
|
|
1
|
-
import { readdirSync } from 'node:fs'
|
|
2
|
-
import { join, dirname } from 'node:path'
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
-
import { execFileSync } from 'node:child_process'
|
|
5
|
-
import { CLIS, CLI_IDS } from './clis.mjs'
|
|
6
|
-
import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return { ok:
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
async
|
|
104
|
-
for (const cli of supports) {
|
|
105
|
-
if (CLIS[cli].has(root, name)) {
|
|
106
|
-
if (dryRun) log(
|
|
107
|
-
else CLIS[cli].
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const r = exec('claude', ['plugin', '
|
|
135
|
-
if (!r.ok
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import { CLIS, CLI_IDS } from './clis.mjs'
|
|
6
|
+
import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
|
|
7
|
+
import { repoPath, repoPathStrict } from './context.mjs'
|
|
8
|
+
import { LocalizedError, msg } from './i18n/index.mjs'
|
|
9
|
+
|
|
10
|
+
const ITEMS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'items')
|
|
11
|
+
|
|
12
|
+
export async function loadItems() {
|
|
13
|
+
const files = readdirSync(ITEMS_DIR).filter((f) => f.endsWith('.mjs')).sort()
|
|
14
|
+
const items = []
|
|
15
|
+
for (const f of files) {
|
|
16
|
+
const mod = await import(pathToFileURL(join(ITEMS_DIR, f)).href)
|
|
17
|
+
items.push(validate(mod.default, f))
|
|
18
|
+
}
|
|
19
|
+
return items
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validate(item, file) {
|
|
23
|
+
for (const field of ['id', 'category', 'label', 'detect', 'install', 'uninstall']) {
|
|
24
|
+
if (!item?.[field]) throw new LocalizedError('error.itemFieldMissing', { file, field })
|
|
25
|
+
}
|
|
26
|
+
return item
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertReasons(id, supports, unsupported) {
|
|
30
|
+
for (const cli of CLI_IDS) {
|
|
31
|
+
if (!supports.includes(cli) && !unsupported[cli]) {
|
|
32
|
+
throw new LocalizedError('error.itemReasonMissing', { id, cli })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// shell 모드에서 Node는 명령과 인자를 공백으로 이어붙일 뿐 quote하지 않으므로
|
|
38
|
+
// 직접 감싼다. 공백이 있을 때만 감싸면 셸 메타문자가 그대로 노출된다 —
|
|
39
|
+
// `D:\R&D\repo` 아래에서 gstack clone 대상이 명령 두 개로 쪼개졌다.
|
|
40
|
+
// 조건 없이 항상 감싸고, 셸별로 실제로 안전한 인용 규칙을 쓴다.
|
|
41
|
+
// - cmd.exe: 큰따옴표 안에서 & | ^ ( ) < >는 리터럴이다. 큰따옴표 자체는
|
|
42
|
+
// 이 위치에서 일관되게 이스케이프할 방법이 없어(cmd는 \"를 해석하지 않는다)
|
|
43
|
+
// 깨진 명령을 만드는 대신 거부한다 — Windows 경로에 올 수 없는 문자다.
|
|
44
|
+
// 닫는 따옴표 앞의 백슬래시 연속은 자식의 argv 파서가 이스케이프로 읽으므로
|
|
45
|
+
// 두 배로 늘린다(Windows 표준 규칙).
|
|
46
|
+
// 남는 것은 %VAR% 확장뿐인데, 짝을 이룬 %가 실제 환경변수일 때만 치환되고
|
|
47
|
+
// 그러면 경로가 달라져 명령이 실패한다 — 조용한 오작동이 아니라 보이는 실패다.
|
|
48
|
+
// - POSIX sh: 작은따옴표 안은 $ ` \ 까지 전부 리터럴이다. 안의 작은따옴표만
|
|
49
|
+
// '\'' 로 끊어 이어 붙인다. 큰따옴표로 감싸면 $(...)가 살아남는다.
|
|
50
|
+
export function shellQuote(text, platform = process.platform) {
|
|
51
|
+
const s = String(text)
|
|
52
|
+
if (platform === 'win32') {
|
|
53
|
+
if (s.includes('"')) {
|
|
54
|
+
throw new LocalizedError('error.shellQuote', { value: s })
|
|
55
|
+
}
|
|
56
|
+
return `"${s.replace(/(\\*)$/, '$1$1')}"`
|
|
57
|
+
}
|
|
58
|
+
return `'${s.replace(/'/g, "'\\''")}'`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function makeExec(dryRun, log = console.log) {
|
|
62
|
+
return (cmd, args, opts = {}) => {
|
|
63
|
+
if (dryRun) {
|
|
64
|
+
log(` [dry-run] ${cmd} ${args.join(' ')}`)
|
|
65
|
+
return { ok: true, output: '' }
|
|
66
|
+
}
|
|
67
|
+
// Windows에서는 npx/claude가 .cmd 심이라 shell 경유가 필요하다.
|
|
68
|
+
const shell = opts.shell ?? process.platform === 'win32'
|
|
69
|
+
// shell + 인자 배열을 함께 넘기면 Node가 DEP0190으로 경고한다(인자가 quote 없이 이어붙기 때문).
|
|
70
|
+
// 어차피 우리가 직접 quote하므로, 완성된 한 줄 명령을 넘기고 인자 배열은 비운다.
|
|
71
|
+
const [file, fileArgs] = shell ? [[cmd, ...args].map((s) => shellQuote(s)).join(' '), []] : [cmd, args]
|
|
72
|
+
try {
|
|
73
|
+
const output = execFileSync(file, fileArgs, {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
+
...opts,
|
|
77
|
+
shell,
|
|
78
|
+
})
|
|
79
|
+
return { ok: true, output }
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { ok: false, output: String(err.stderr ?? err.message) }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function defineMcp({ id, label, server, supports = [...CLI_IDS], unsupported = {}, note, group = null }) {
|
|
87
|
+
assertReasons(id, supports, unsupported)
|
|
88
|
+
const name = id.replace(/^mcp\./, '')
|
|
89
|
+
return {
|
|
90
|
+
id, category: 'mcp', label, scope: 'project', supports, unsupported, note, group,
|
|
91
|
+
async detect({ root }) {
|
|
92
|
+
const present = supports.filter((cli) => CLIS[cli].has(root, name))
|
|
93
|
+
if (present.length === 0) return { status: 'absent' }
|
|
94
|
+
if (present.length === supports.length) return { status: 'installed' }
|
|
95
|
+
return {
|
|
96
|
+
status: 'partial',
|
|
97
|
+
detail: msg('item.mcp.partial', {
|
|
98
|
+
present: present.join(', '),
|
|
99
|
+
missing: supports.filter((c) => !present.includes(c)).join(', '),
|
|
100
|
+
}),
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async install({ root, dryRun, log = () => {}, t }) {
|
|
104
|
+
for (const cli of supports) {
|
|
105
|
+
if (!CLIS[cli].has(root, name)) {
|
|
106
|
+
if (dryRun) log(t('log.mcp.add', { cli: CLIS[cli].label, name }))
|
|
107
|
+
else CLIS[cli].add(root, name, server)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async uninstall({ root, dryRun, log = () => {}, t }) {
|
|
112
|
+
for (const cli of supports) {
|
|
113
|
+
if (CLIS[cli].has(root, name)) {
|
|
114
|
+
if (dryRun) log(t('log.mcp.remove', { cli: CLIS[cli].label, name }))
|
|
115
|
+
else CLIS[cli].remove(root, name)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function definePlugin({ id, label, installId, detectIds, marketplace, note, group = null }) {
|
|
123
|
+
const unsupported = Object.fromEntries(
|
|
124
|
+
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.claudePlugin')]),
|
|
125
|
+
)
|
|
126
|
+
return {
|
|
127
|
+
id, category: 'plugin', label, scope: 'project', supports: ['claude'], unsupported, note, group,
|
|
128
|
+
async detect({ root }) {
|
|
129
|
+
return { status: isPluginEnabled(root, detectIds) ? 'installed' : 'absent' }
|
|
130
|
+
},
|
|
131
|
+
async install(ctx) {
|
|
132
|
+
const { root, dryRun, exec } = ctx
|
|
133
|
+
if (marketplace) exec('claude', ['plugin', 'marketplace', 'add', marketplace.repo], { cwd: root })
|
|
134
|
+
const r = exec('claude', ['plugin', 'install', installId, '--scope', 'project'], { cwd: root })
|
|
135
|
+
if (!r.ok) {
|
|
136
|
+
if (!dryRun) enablePlugin(root, installId, marketplace)
|
|
137
|
+
return { fallback: true, message: msg('item.plugin.deferred') }
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async uninstall(ctx) {
|
|
141
|
+
const { root, dryRun, exec } = ctx
|
|
142
|
+
const r = exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
|
|
143
|
+
if (!r.ok && !dryRun) disablePlugin(root, detectIds)
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function defineSkill({ id, label, scope, detect, install, uninstall, note, group = null }) {
|
|
149
|
+
const unsupported = Object.fromEntries(
|
|
150
|
+
CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.claudeSkill')]),
|
|
151
|
+
)
|
|
152
|
+
return { id, category: 'skill', label, scope, supports: ['claude'], unsupported, note, group, detect, install, uninstall }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 이 저장소가 공유 스킬 자리로 쓰는 디렉터리. vercel-labs/skills 레지스트리의
|
|
156
|
+
// `--agent universal` 프로젝트 경로와 같은 값이다 — 그래서 레지스트리 설치가
|
|
157
|
+
// 우리 배선 위에 그대로 얹힌다.
|
|
158
|
+
const SHARED_SKILLS = '.agents/skills'
|
|
159
|
+
|
|
160
|
+
function skillDirs(root) {
|
|
161
|
+
const dir = repoPath(root, SHARED_SKILLS)
|
|
162
|
+
if (!existsSync(dir)) return []
|
|
163
|
+
return readdirSync(dir).filter((name) => {
|
|
164
|
+
try {
|
|
165
|
+
return statSync(join(dir, name)).isDirectory()
|
|
166
|
+
} catch {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 설치된 디렉터리 이름이 스킬 이름과 다를 수 있다(레지스트리는 SKILL.md의
|
|
173
|
+
// frontmatter name으로 고르는데, 저장소의 폴더 이름은 다른 경우가 있다 —
|
|
174
|
+
// taste-skill/이 design-taste-frontend를 담는 식이다). 이름이 같은 디렉터리를
|
|
175
|
+
// 먼저 보고, 없으면 frontmatter를 읽어 찾는다. 디렉터리 이름만 보면 설치해
|
|
176
|
+
// 놓고도 계속 '미설치'로 읽힌다.
|
|
177
|
+
function findSkillDir(root, skill) {
|
|
178
|
+
const base = repoPath(root, SHARED_SKILLS)
|
|
179
|
+
if (existsSync(join(base, skill, 'SKILL.md'))) return skill
|
|
180
|
+
for (const name of skillDirs(root)) {
|
|
181
|
+
const file = join(base, name, 'SKILL.md')
|
|
182
|
+
if (!existsSync(file)) continue
|
|
183
|
+
const head = readFileSync(file, 'utf8').slice(0, 2000)
|
|
184
|
+
if (new RegExp(`^name:\\s*['"]?${skill}['"]?\\s*$`, 'm').test(head)) return name
|
|
185
|
+
}
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// vercel-labs/skills 레지스트리로 설치하는 스킬. `--agent universal`의 프로젝트
|
|
190
|
+
// 경로가 .agents/skills라, 한 번 설치하면 10개 CLI가 함께 본다 — Claude·Kiro·
|
|
191
|
+
// Grok은 부트스트랩이 만든 Junction으로, 나머지는 그 경로를 네이티브로 읽는다.
|
|
192
|
+
// `--copy`로 실물을 남긴다: 커밋해서 팀과 나누는 자리라 링크는 클론 뒤 깨진다.
|
|
193
|
+
export function defineRegistrySkill({ id, label, source, skill, note, group = null }) {
|
|
194
|
+
return {
|
|
195
|
+
id, category: 'skill', label, scope: 'project', supports: [...CLI_IDS], unsupported: {}, note, group,
|
|
196
|
+
async detect({ root }) {
|
|
197
|
+
return { status: findSkillDir(root, skill) ? 'installed' : 'absent' }
|
|
198
|
+
},
|
|
199
|
+
async install({ root, exec }) {
|
|
200
|
+
const r = exec('npx', ['-y', 'skills@latest', 'add', source, '--skill', skill, '--agent', 'universal', '--yes', '--copy'], { cwd: root })
|
|
201
|
+
if (!r.ok) throw new LocalizedError('error.registrySkillInstall', { skill, output: r.output })
|
|
202
|
+
},
|
|
203
|
+
async uninstall({ root, dryRun, exec }) {
|
|
204
|
+
// 레지스트리의 remove는 --agent universal에서 "Done!"을 찍고도 디렉터리를
|
|
205
|
+
// 남긴다(실측). 그래서 성공/실패와 무관하게 남은 디렉터리를 우리가 지운다 —
|
|
206
|
+
// 남겨 두면 detect가 계속 installed로 읽어 제거가 안 된 채 성공으로 보인다.
|
|
207
|
+
exec('npx', ['-y', 'skills@latest', 'remove', skill, '--agent', 'universal', '--yes'], { cwd: root })
|
|
208
|
+
if (dryRun) return
|
|
209
|
+
const dir = findSkillDir(root, skill)
|
|
210
|
+
if (dir) rmSync(repoPathStrict(root, `${SHARED_SKILLS}/${dir}`), { recursive: true, force: true })
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
package/lib/claude-plugins.mjs
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { repoPath, repoPathStrict } from './context.mjs'
|
|
2
|
-
import { readJson, setKey, removeKey } from './jsonfile.mjs'
|
|
3
|
-
|
|
4
|
-
const SETTINGS = '.claude/settings.json'
|
|
5
|
-
|
|
6
|
-
function readSettings(root) {
|
|
7
|
-
return readJson(repoPath(root, SETTINGS)) ?? {}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// 쓰기 경로만 링크 이탈까지 검사한다 — 저장소 안 `.claude`가 홈을 가리키는
|
|
11
|
-
// Junction이면 어휘적 검사는 통과하고 글로벌 settings.json이 수정된다.
|
|
12
|
-
// clis.mjs의 add·remove, bootstrap의 apply.mjs와 같은 규칙.
|
|
13
|
-
function settingsFile(root) {
|
|
14
|
-
return repoPathStrict(root, SETTINGS)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function enabledList(settings) {
|
|
18
|
-
const ep = settings.enabledPlugins
|
|
19
|
-
if (Array.isArray(ep)) return ep
|
|
20
|
-
if (ep && typeof ep === 'object') return Object.keys(ep).filter((k) => ep[k])
|
|
21
|
-
return []
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isPluginEnabled(root, ids) {
|
|
25
|
-
const list = enabledList(readSettings(root))
|
|
26
|
-
return ids.some((id) => list.includes(id))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function enablePlugin(root, id, marketplace) {
|
|
30
|
-
const file = settingsFile(root)
|
|
31
|
-
const settings = readSettings(root)
|
|
32
|
-
if (Array.isArray(settings.enabledPlugins)) {
|
|
33
|
-
if (!settings.enabledPlugins.includes(id)) {
|
|
34
|
-
setKey(file, ['enabledPlugins', settings.enabledPlugins.length], id)
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
setKey(file, ['enabledPlugins', id], true)
|
|
38
|
-
}
|
|
39
|
-
if (marketplace) {
|
|
40
|
-
setKey(file, ['extraKnownMarketplaces', marketplace.name], {
|
|
41
|
-
source: { source: 'github', repo: marketplace.repo },
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function disablePlugin(root, ids) {
|
|
47
|
-
const file = settingsFile(root)
|
|
48
|
-
const settings = readSettings(root)
|
|
49
|
-
if (Array.isArray(settings.enabledPlugins)) {
|
|
50
|
-
const kept = settings.enabledPlugins.filter((e) => !ids.includes(e))
|
|
51
|
-
setKey(file, ['enabledPlugins'], kept)
|
|
52
|
-
} else if (settings.enabledPlugins) {
|
|
53
|
-
for (const id of ids) removeKey(file, ['enabledPlugins', id])
|
|
54
|
-
}
|
|
55
|
-
// 제거한 플러그인이 쓰던 마켓플레이스만 정리 대상으로 삼는다 (무관한 사용자 항목 보존).
|
|
56
|
-
const candidates = new Set(ids.map((id) => id.slice(id.indexOf('@') + 1)))
|
|
57
|
-
const after = readSettings(root)
|
|
58
|
-
const remaining = enabledList(after).map((e) => e.slice(e.indexOf('@') + 1))
|
|
59
|
-
for (const mkt of candidates) {
|
|
60
|
-
if (!remaining.includes(mkt) && after.extraKnownMarketplaces?.[mkt] !== undefined) {
|
|
61
|
-
removeKey(file, ['extraKnownMarketplaces', mkt])
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
import { repoPath, repoPathStrict } from './context.mjs'
|
|
2
|
+
import { readJson, setKey, removeKey } from './jsonfile.mjs'
|
|
3
|
+
|
|
4
|
+
const SETTINGS = '.claude/settings.json'
|
|
5
|
+
|
|
6
|
+
function readSettings(root) {
|
|
7
|
+
return readJson(repoPath(root, SETTINGS)) ?? {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 쓰기 경로만 링크 이탈까지 검사한다 — 저장소 안 `.claude`가 홈을 가리키는
|
|
11
|
+
// Junction이면 어휘적 검사는 통과하고 글로벌 settings.json이 수정된다.
|
|
12
|
+
// clis.mjs의 add·remove, bootstrap의 apply.mjs와 같은 규칙.
|
|
13
|
+
function settingsFile(root) {
|
|
14
|
+
return repoPathStrict(root, SETTINGS)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function enabledList(settings) {
|
|
18
|
+
const ep = settings.enabledPlugins
|
|
19
|
+
if (Array.isArray(ep)) return ep
|
|
20
|
+
if (ep && typeof ep === 'object') return Object.keys(ep).filter((k) => ep[k])
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isPluginEnabled(root, ids) {
|
|
25
|
+
const list = enabledList(readSettings(root))
|
|
26
|
+
return ids.some((id) => list.includes(id))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function enablePlugin(root, id, marketplace) {
|
|
30
|
+
const file = settingsFile(root)
|
|
31
|
+
const settings = readSettings(root)
|
|
32
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
33
|
+
if (!settings.enabledPlugins.includes(id)) {
|
|
34
|
+
setKey(file, ['enabledPlugins', settings.enabledPlugins.length], id)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
setKey(file, ['enabledPlugins', id], true)
|
|
38
|
+
}
|
|
39
|
+
if (marketplace) {
|
|
40
|
+
setKey(file, ['extraKnownMarketplaces', marketplace.name], {
|
|
41
|
+
source: { source: 'github', repo: marketplace.repo },
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function disablePlugin(root, ids) {
|
|
47
|
+
const file = settingsFile(root)
|
|
48
|
+
const settings = readSettings(root)
|
|
49
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
50
|
+
const kept = settings.enabledPlugins.filter((e) => !ids.includes(e))
|
|
51
|
+
setKey(file, ['enabledPlugins'], kept)
|
|
52
|
+
} else if (settings.enabledPlugins) {
|
|
53
|
+
for (const id of ids) removeKey(file, ['enabledPlugins', id])
|
|
54
|
+
}
|
|
55
|
+
// 제거한 플러그인이 쓰던 마켓플레이스만 정리 대상으로 삼는다 (무관한 사용자 항목 보존).
|
|
56
|
+
const candidates = new Set(ids.map((id) => id.slice(id.indexOf('@') + 1)))
|
|
57
|
+
const after = readSettings(root)
|
|
58
|
+
const remaining = enabledList(after).map((e) => e.slice(e.indexOf('@') + 1))
|
|
59
|
+
for (const mkt of candidates) {
|
|
60
|
+
if (!remaining.includes(mkt) && after.extraKnownMarketplaces?.[mkt] !== undefined) {
|
|
61
|
+
removeKey(file, ['extraKnownMarketplaces', mkt])
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|