@rch4com/agent-setup 1.28.0 → 2.2.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 (66) hide show
  1. package/README.md +31 -20
  2. package/install.mjs +25 -8
  3. package/lib/activation.mjs +31 -0
  4. package/lib/args.mjs +5 -2
  5. package/lib/bootstrap/manifest.mjs +21 -47
  6. package/lib/bootstrap/templates.mjs +7 -60
  7. package/lib/catalog.mjs +22 -241
  8. package/lib/clis.mjs +11 -118
  9. package/lib/commands.mjs +47 -0
  10. package/lib/engine.mjs +16 -11
  11. package/lib/i18n/catalog/en.mjs +58 -49
  12. package/lib/i18n/catalog/ko.mjs +58 -49
  13. package/lib/items/mcp.codebase-memory.mjs +6 -4
  14. package/lib/items/mcp.graft.mjs +6 -4
  15. package/lib/items/mcp.graphify.mjs +6 -4
  16. package/lib/items/mcp.headroom.mjs +6 -4
  17. package/lib/items/mcp.notion.mjs +7 -4
  18. package/lib/items/mcp.supabase.mjs +7 -4
  19. package/lib/items/mcp.vercel.mjs +7 -4
  20. package/lib/items/plugin.ai-memory.mjs +29 -0
  21. package/lib/items/plugin.cli-anything.mjs +18 -15
  22. package/lib/items/plugin.ecc.mjs +22 -20
  23. package/lib/items/plugin.hyperframes.mjs +17 -13
  24. package/lib/items/plugin.impeccable.mjs +19 -22
  25. package/lib/items/plugin.mattpocock-skills.mjs +23 -17
  26. package/lib/items/plugin.ponytail.mjs +24 -106
  27. package/lib/items/plugin.superpowers.mjs +20 -24
  28. package/lib/items/plugin.understand-anything.mjs +21 -19
  29. package/lib/items/skill.agent-browser.mjs +6 -6
  30. package/lib/items/skill.agent-handoff-manager.mjs +14 -0
  31. package/lib/items/skill.archify.mjs +5 -5
  32. package/lib/items/skill.caveman.mjs +5 -5
  33. package/lib/items/skill.diagram-design.mjs +5 -5
  34. package/lib/items/skill.eli5.mjs +6 -6
  35. package/lib/items/skill.find-skills.mjs +5 -5
  36. package/lib/items/skill.gsd.mjs +28 -89
  37. package/lib/items/skill.gstack.mjs +58 -65
  38. package/lib/items/skill.hallmark.mjs +5 -5
  39. package/lib/items/skill.html-plan.mjs +15 -0
  40. package/lib/items/skill.i-have-adhd.mjs +6 -6
  41. package/lib/items/skill.im-not-ai.mjs +5 -6
  42. package/lib/items/skill.impeccable.mjs +6 -6
  43. package/lib/items/skill.karpathy.mjs +5 -5
  44. package/lib/items/skill.mattpocock-skills.mjs +5 -6
  45. package/lib/items/skill.mcp-builder.mjs +5 -5
  46. package/lib/items/skill.officecli.mjs +5 -5
  47. package/lib/items/skill.prompt-master.mjs +5 -5
  48. package/lib/items/skill.remotion.mjs +5 -5
  49. package/lib/items/skill.strix.mjs +5 -6
  50. package/lib/items/skill.superpowers.mjs +6 -7
  51. package/lib/items/skill.taste.mjs +5 -5
  52. package/lib/items/skill.unslop.mjs +12 -0
  53. package/lib/probe.mjs +136 -0
  54. package/lib/recipe-helpers.mjs +281 -0
  55. package/lib/recipe.mjs +181 -0
  56. package/lib/skills-registry.mjs +93 -0
  57. package/lib/status.mjs +7 -3
  58. package/lib/tui/detail.mjs +5 -25
  59. package/lib/tui/rows.mjs +25 -14
  60. package/lib/tui/run.mjs +35 -8
  61. package/package.json +4 -3
  62. package/lib/claude-plugins.mjs +0 -64
  63. package/lib/global-plugin.mjs +0 -239
  64. package/lib/items/global.ponytail.mjs +0 -56
  65. package/lib/items/global.superpowers.mjs +0 -58
  66. package/lib/tomlfile.mjs +0 -67
@@ -1,239 +0,0 @@
1
- // 사용자 전역(scope: 'user') 플러그인 항목의 공통 기구. 상류가 하니스별 전역
2
- // 설치만 지원하는 도구(superpowers, ponytail)가 공유한다 — 항목 파일
3
- // (lib/items/global.*.mjs)은 상류별 좌표와 미배선 사유만 든다.
4
- //
5
- // 전역 파일을 만지므로 repoPath 계열 가드를 일부러 안 쓴다 — 저장소 이탈이
6
- // 목적인 기구다. 항목 파일에 두지 않는 이유는 gitmessage.mjs와 같다:
7
- // loadItems가 items/의 모든 .mjs를 항목으로 읽는다.
8
- //
9
- // 하니스별 기록 위치(2026-08-15 실측, 상세는 global.superpowers.mjs):
10
- // codex $CODEX_HOME/config.toml의 [plugins."<이름>@…"] — CODEX_HOME
11
- // 재지정을 실제로 쓰는 환경(Orca)이 있어 환경변수를 따라간다.
12
- // copilot ~/.copilot/config.json의 installedPlugins[] (주석 있는 JSONC).
13
- // gemini ~/.gemini/extensions/<이름>/ (공식 extensions 문서).
14
- // opencode 전역 opencode.json의 plugin 배열 (XDG_CONFIG_HOME 존중).
15
- // grok ~/.grok/installed-plugins/registry.json의 repos[*].plugins 키
16
- // (2026-09-05 grok 1.0.5로 설치·제거 왕복 실측). 설치는
17
- // `grok plugin install <소스> --trust`가 registry와 디렉터리를 만들고
18
- // config.toml [plugins].enabled에도 이름을 넣는다. 제거
19
- // (`grok plugin uninstall <이름> --confirm`)는 디렉터리·registry만
20
- // 지우고 enabled 항목은 남기므로, 감지의 근거는 registry여야 한다 —
21
- // enabled를 보면 제거한 뒤에도 영원히 설치됨으로 읽힌다.
22
- import { existsSync, readFileSync } from 'node:fs'
23
- import { homedir } from 'node:os'
24
- import { delimiter, join } from 'node:path'
25
- import { parse as parseToml } from 'smol-toml'
26
- import { CLIS } from './clis.mjs'
27
- import { readJson, setKey, removeKey } from './jsonfile.mjs'
28
- import { msg, LocalizedError } from './i18n/index.mjs'
29
-
30
- // 실행은 cmd 셸을 거치므로(catalog.mjs makeExec) win32에서는 PATHEXT 확장자가
31
- // 있어야 실제로 부를 수 있다 — copilot.ps1만 있는 PATH는 "없음"이 맞다.
32
- function defaultHasBinary(name, env = process.env) {
33
- const dirs = (env.PATH ?? '').split(delimiter).filter(Boolean)
34
- const exts = process.platform === 'win32'
35
- ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
36
- : ['']
37
- return dirs.some((dir) => exts.some((ext) => existsSync(join(dir, name + ext))))
38
- }
39
-
40
- // superpowers@openai-curated처럼 다른 마켓플레이스로 설치된 판도 잡아야
41
- // 하므로(실측: 이런 머신이 실제로 있다) 이름이 아니라 접두사로 고른다.
42
- // 제거도 여기서 찾은 id를 그대로 쓴다 — 감지가 "있다"고 말한 판을 지워야
43
- // 상태가 맞아떨어진다.
44
- function codexPluginIds(file, pluginName) {
45
- if (!existsSync(file)) return []
46
- try {
47
- const data = parseToml(readFileSync(file, 'utf8'))
48
- return Object.keys(data.plugins ?? {}).filter((id) => id.startsWith(`${pluginName}@`))
49
- } catch {
50
- // 깨진 config.toml은 미설치로 둔다 — 설치를 시도하면 codex가 스스로
51
- // 진단하고, "설치됨"으로 오판해 제거를 그리로 보내는 것보다 낫다.
52
- return []
53
- }
54
- }
55
-
56
- function copilotInstalled(file, pluginName) {
57
- const list = readJson(file)?.installedPlugins
58
- return Array.isArray(list) && list.some((p) => p?.name === pluginName)
59
- }
60
-
61
- // ponytail(items/plugin.ponytail.mjs)과 같은 규칙: plugin 키가 배열이 아니면
62
- // 손대지 않는다.
63
- function opencodePlugins(file) {
64
- const value = readJson(file)?.plugin
65
- if (value === undefined) return []
66
- if (!Array.isArray(value)) return null
67
- return value
68
- }
69
-
70
- // config:
71
- // pluginName 감지·제거가 쓰는 상류 이름 (codex 접두사, copilot name,
72
- // gemini 확장 디렉터리 이름)
73
- // marketplaceRepo codex·copilot 마켓플레이스 (owner/repo)
74
- // installId 마켓플레이스 설치 id (<이름>@<마켓>)
75
- // gemini gemini extensions install에 줄 git URL, 없으면 null
76
- // opencode 전역 opencode.json plugin 배열에 넣을 항목, 없으면 null
77
- // grok grok plugin install에 줄 소스(마켓 id 또는 owner/repo), 없으면 null
78
- // supports는 어댑터 유무로 정해진다 — codex·copilot은 항상, gemini·opencode·grok은
79
- // 좌표가 있을 때만.
80
- export function defineGlobalPlugin(
81
- { id, label, group, note, exclusive = null, unsupported = {}, pluginName, marketplaceRepo, installId, gemini = null, opencode = null, grok = null, verified = null },
82
- { home = null, env = process.env, hasBinary = defaultHasBinary } = {},
83
- ) {
84
- const supports = ['codex', ...(gemini ? ['gemini'] : []), ...(opencode ? ['opencode'] : []), 'copilot', ...(grok ? ['grok'] : [])]
85
-
86
- const files = () => {
87
- const base = home ?? homedir()
88
- return {
89
- codex: join(env.CODEX_HOME || join(base, '.codex'), 'config.toml'),
90
- copilot: join(base, '.copilot', 'config.json'),
91
- gemini: join(base, '.gemini', 'extensions', pluginName),
92
- opencode: join(env.XDG_CONFIG_HOME || join(base, '.config'), 'opencode', 'opencode.json'),
93
- grok: join(base, '.grok', 'installed-plugins', 'registry.json'),
94
- }
95
- }
96
-
97
- // registry.json: { repos: { "<이름>-<해시>": { plugins: { "<이름>": {...} } } } }.
98
- // 이름은 repos 키(해시가 붙는다)가 아니라 plugins 키에서 본다.
99
- const grokInstalled = (file) => {
100
- const repos = readJson(file)?.repos
101
- if (!repos || typeof repos !== 'object') return false
102
- return Object.values(repos).some((r) => Object.hasOwn(r?.plugins ?? {}, pluginName))
103
- }
104
-
105
- const isOurEntry = (p) => String(p).startsWith(`${pluginName}@`)
106
- const wired = {
107
- codex: (f) => codexPluginIds(f.codex, pluginName).length > 0,
108
- copilot: (f) => copilotInstalled(f.copilot, pluginName),
109
- gemini: (f) => existsSync(f.gemini),
110
- opencode: (f) => (opencodePlugins(f.opencode) ?? []).some(isOurEntry),
111
- grok: (f) => grokInstalled(f.grok),
112
- }
113
-
114
- return {
115
- id, category: 'plugin', label, scope: 'user', group, exclusive, verified,
116
- supports: [...supports],
117
- unsupported,
118
- note,
119
-
120
- // 상태는 "이 머신에 있는 하니스" 기준이다. CLI가 없는 하니스까지 누락으로
121
- // 세면 gemini 없는 머신은 영원히 partial이라 apply가 매번 헛돈다.
122
- async detect() {
123
- const f = files()
124
- const present = supports.filter((c) => wired[c](f))
125
- const machine = supports.filter((c) => hasBinary(c))
126
- const noCli = supports.filter((c) => !machine.includes(c))
127
- const missing = machine.filter((c) => !present.includes(c))
128
- const detail = noCli.length > 0 ? msg('item.global.noCli', { list: noCli.join(', ') }) : undefined
129
- // excluded는 detail과 별도로 구조체로 넘긴다 — 행 힌트가 CLI 이름을
130
- // 나열할 때 이 머신에 없는 것을 빼고 따로 적으려면 문장이 아니라 목록이 필요하다.
131
- if (present.length === 0) return { status: 'absent', detail, excluded: noCli }
132
- if (missing.length === 0) return { status: 'installed', detail, excluded: noCli }
133
- return {
134
- status: 'partial',
135
- detail: msg('item.plugin.partial', { present: present.join(', '), missing: missing.join(', ') }),
136
- excluded: noCli,
137
- }
138
- },
139
-
140
- async install({ dryRun, exec, log = () => {}, t }) {
141
- const f = files()
142
- // opencode 설정이 손댈 수 없는 꼴이면 아무것도 만지기 전에 멈춘다 —
143
- // 절반만 설치된 채 실패하는 것보다 통째로 거절이 낫다.
144
- const needsOpencode = opencode && !wired.opencode(f) && hasBinary('opencode')
145
- if (needsOpencode && !dryRun && opencodePlugins(f.opencode) === null) {
146
- throw new LocalizedError('error.globalOpencodePlugin', { file: f.opencode, entry: opencode })
147
- }
148
- const skipped = []
149
- const failures = []
150
- const fail = (cli, output) => failures.push(`${cli}: ${String(output).trim().split('\n')[0]}`)
151
- for (const cli of supports) {
152
- if (wired[cli](f)) continue
153
- if (!hasBinary(cli)) {
154
- skipped.push(cli)
155
- continue
156
- }
157
- if (cli === 'codex') {
158
- // 마켓 등록은 이미 있어도 해가 없다 — 성패는 add가 판정한다.
159
- await exec('codex', ['plugin', 'marketplace', 'add', marketplaceRepo])
160
- const r = await exec('codex', ['plugin', 'add', installId])
161
- if (!r.ok) fail(cli, r.output)
162
- } else if (cli === 'copilot') {
163
- await exec('copilot', ['plugin', 'marketplace', 'add', marketplaceRepo])
164
- // 실측(2026-08-15): 다른 copilot 세션이 떠 있으면 os error 5로 한 번
165
- // 실패하고 재시도에 성공한다. 한 번만 다시 시도한다.
166
- let r = await exec('copilot', ['plugin', 'install', installId])
167
- if (!r.ok) r = await exec('copilot', ['plugin', 'install', installId])
168
- if (!r.ok) fail(cli, r.output)
169
- } else if (cli === 'gemini') {
170
- const r = await exec('gemini', ['extensions', 'install', gemini])
171
- if (!r.ok) fail(cli, r.output)
172
- } else if (cli === 'grok') {
173
- // --trust는 확인 프롬프트를 건너뛴다 — stdin이 닫힌 채 도는 실행이라
174
- // 프롬프트가 뜨면 영원히 멈춘다.
175
- const r = await exec('grok', ['plugin', 'install', grok, '--trust'])
176
- if (!r.ok) fail(cli, r.output)
177
- } else {
178
- if (dryRun) {
179
- log(t('log.plugin.add', { cli: CLIS.opencode.label, name: opencode }))
180
- continue
181
- }
182
- const list = opencodePlugins(f.opencode) ?? []
183
- if (list.length === 0) setKey(f.opencode, ['plugin'], [opencode])
184
- else setKey(f.opencode, ['plugin', list.length], opencode)
185
- }
186
- }
187
- if (failures.length > 0) throw new LocalizedError('error.globalExec', { detail: failures.join(' / ') })
188
- if (skipped.length > 0) return { message: msg('item.global.skipped', { list: skipped.join(', ') }) }
189
- },
190
-
191
- async uninstall({ dryRun, exec, log = () => {}, t }) {
192
- const f = files()
193
- const skipped = []
194
- const failures = []
195
- const fail = (cli, output) => failures.push(`${cli}: ${String(output).trim().split('\n')[0]}`)
196
- for (const cli of supports) {
197
- if (!wired[cli](f)) continue
198
- if (cli === 'opencode') {
199
- if (dryRun) {
200
- log(t('log.plugin.remove', { cli: CLIS.opencode.label, name: opencode }))
201
- continue
202
- }
203
- const kept = (opencodePlugins(f.opencode) ?? []).filter((p) => !isOurEntry(p))
204
- // 우리 항목만 빼고 비면 키째 지운다 — 빈 배열은 우리가 남긴 흔적이다.
205
- if (kept.length === 0) removeKey(f.opencode, ['plugin'])
206
- else setKey(f.opencode, ['plugin'], kept)
207
- continue
208
- }
209
- if (!hasBinary(cli)) {
210
- skipped.push(cli)
211
- continue
212
- }
213
- if (cli === 'codex') {
214
- for (const pid of codexPluginIds(f.codex, pluginName)) {
215
- const r = await exec('codex', ['plugin', 'remove', pid])
216
- if (!r.ok) fail(cli, r.output)
217
- }
218
- // 마켓플레이스 등록은 남긴다 — 우리가 등록했다는 보장이 없고,
219
- // 같은 마켓의 다른 플러그인이 쓰고 있을 수 있다.
220
- } else if (cli === 'copilot') {
221
- let r = await exec('copilot', ['plugin', 'uninstall', pluginName])
222
- if (!r.ok) r = await exec('copilot', ['plugin', 'uninstall', pluginName])
223
- if (!r.ok) fail(cli, r.output)
224
- } else if (cli === 'grok') {
225
- // --confirm은 한 저장소에 플러그인이 여럿일 때의 확인을 건너뛴다.
226
- // config.toml [plugins].enabled의 이름은 상류가 남긴다 — 우리가
227
- // 등록한 줄이라는 보장이 없어 건드리지 않는다(note에 적는다).
228
- const r = await exec('grok', ['plugin', 'uninstall', pluginName, '--confirm'])
229
- if (!r.ok) fail(cli, r.output)
230
- } else {
231
- const r = await exec('gemini', ['extensions', 'uninstall', pluginName])
232
- if (!r.ok) fail(cli, r.output)
233
- }
234
- }
235
- if (failures.length > 0) throw new LocalizedError('error.globalExec', { detail: failures.join(' / ') })
236
- if (skipped.length > 0) return { message: msg('item.global.skipped', { list: skipped.join(', ') }) }
237
- },
238
- }
239
- }
@@ -1,56 +0,0 @@
1
- import { defineGlobalPlugin } from '../global-plugin.mjs'
2
- import { CLI_IDS } from '../clis.mjs'
3
- import { msg } from '../i18n/index.mjs'
4
-
5
- // Ponytail의 하니스별 전역 설치 판. 공통 기구는 global-plugin.mjs에 있다.
6
- //
7
- // 하니스별 근거(2026-08-15 상류 README 재검증):
8
- // codex — `codex plugin marketplace add DietrichGebert/ponytail` +
9
- // `codex plugin add ponytail@ponytail`. 상류는 설치 후 codex에서
10
- // /hooks로 라이프사이클 훅 등록을 확인하고 새 스레드를 시작하라는
11
- // 대화형 후속 단계를 안내한다 — 명령으로 대신할 수 없어 note에
12
- // 적는다. 제거는 `codex plugin remove <id>`.
13
- // copilot — `copilot plugin marketplace add` + `copilot plugin install
14
- // ponytail@ponytail`. 상류 README에 제거 명령은 없지만
15
- // `copilot plugin uninstall <이름>`은 copilot CLI 자체의 범용
16
- // 명령이다(--help 실측).
17
- // gemini — `gemini extensions install https://github.com/DietrichGebert/ponytail`.
18
- //
19
- // opencode는 프로젝트 항목(plugin.ponytail)이 opencode.jsonc의 plugin 배열로
20
- // 이미 배선한다 — 전역 판에서 겹치면 같은 플러그인이 두 번 등록된다. claude도
21
- // 같은 이유로 프로젝트 항목의 자리다.
22
- // grok — `grok plugin install DietrichGebert/ponytail --trust`. 2026-09-05
23
- // 이 머신(grok 1.0.5)에서 설치·감지·제거 왕복을 실측했다. 상류
24
- // README는 "기본 꺼짐이라 /plugins에서 켜라"고 적지만 1.0.5는
25
- // --trust 설치 직후 config.toml [plugins].enabled에 이름을 넣었다.
26
- // kiro·vscode는 상류가 룰 파일 수동 배치만 안내하고, kilo·kimi는 경로가 없다.
27
- //
28
- // 상류는 플러그인 제거 전에 node scripts/uninstall.js로 ~/.config/ponytail
29
- // 설정과 statusLine 항목을 정리하라고 안내한다 — 하니스마다 스크립트 위치가
30
- // 달라 자동화하지 않고 note로 알린다. 이 항목은 플러그인만 제거한다.
31
- const SUPPORTS = ['codex', 'gemini', 'copilot', 'grok']
32
-
33
- const CONFIG = {
34
- id: 'global.ponytail', label: 'Ponytail', group: '__token',
35
- note: 'item.global.ponytail.note',
36
- verified: '2026-09-05',
37
- pluginName: 'ponytail',
38
- marketplaceRepo: 'DietrichGebert/ponytail',
39
- installId: 'ponytail@ponytail',
40
- gemini: 'https://github.com/DietrichGebert/ponytail',
41
- grok: 'DietrichGebert/ponytail',
42
- unsupported: Object.fromEntries(
43
- CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
44
- c,
45
- ['claude', 'opencode'].includes(c) ? msg('item.unsupported.globalProjectItem', { item: 'Ponytail' })
46
- : ['kiro', 'vscode'].includes(c) ? msg('item.unsupported.ponytailRules')
47
- : msg('item.unsupported.upstreamNone'),
48
- ]),
49
- ),
50
- }
51
-
52
- export function createItem(deps = {}) {
53
- return defineGlobalPlugin(CONFIG, deps)
54
- }
55
-
56
- export default createItem()
@@ -1,58 +0,0 @@
1
- import { defineGlobalPlugin } from '../global-plugin.mjs'
2
- import { CLI_IDS } from '../clis.mjs'
3
- import { msg } from '../i18n/index.mjs'
4
-
5
- // superpowers를 프로젝트 스코프로 배선할 길이 없는 하니스에 상류의 하니스별
6
- // 전역 설치("install separately for each one")를 대신 해 주는 항목. 공통
7
- // 기구는 global-plugin.mjs에 있고, 여기는 상류 좌표와 사유만 든다.
8
- //
9
- // 하니스별 근거(2026-08-15, v6.3.0):
10
- // codex — 이 머신에서 marketplace add + plugin add를 실측. 기록은
11
- // $CODEX_HOME/config.toml의 [plugins."superpowers@…"].
12
- // copilot — 이 머신에서 설치·목록·제거를 실측(~/.copilot/config.json).
13
- // gemini — 공식 extensions 기구. 로컬 CLI가 없어 문서 근거다.
14
- // opencode— 상류 .opencode/INSTALL.md가 전역 opencode.json의 plugin 배열에
15
- // 아래 항목 한 줄을 더하라고 안내한다.
16
- //
17
- // grok — 2026-09-05 이 머신(grok 1.0.5)에서 `grok plugin install
18
- // superpowers@xai-official --trust`로 설치·감지·제거 왕복을 실측.
19
- // 기록은 ~/.grok/installed-plugins/registry.json(global-plugin.mjs).
20
- //
21
- // kimi는 설치가 대화형 /plugins뿐이라 헤드리스 경로가 없다. claude는 프로젝트
22
- // 스코프 항목(plugin.superpowers)의 자리다.
23
- //
24
- // skill.superpowers(공유 .agents/skills)와 배타로 묶지 않는다 — 저쪽은 이
25
- // 저장소에만, 이쪽은 머신 전체에 작용해 평면이 다르고, "claude는 프로젝트
26
- // 플러그인 + 나머지는 전역" 같은 조합이 정당하다. 겹치는 하니스가 같은 스킬을
27
- // 두 곳에서 본다는 것은 note에 적어 둔다.
28
- const SUPPORTS = ['codex', 'gemini', 'opencode', 'copilot', 'grok']
29
-
30
- const CONFIG = {
31
- id: 'global.superpowers', label: 'superpowers', group: '__flow',
32
- note: 'item.global.superpowers.note',
33
- verified: '2026-09-05',
34
- pluginName: 'superpowers',
35
- marketplaceRepo: 'obra/superpowers-marketplace',
36
- installId: 'superpowers@superpowers-marketplace',
37
- gemini: 'https://github.com/obra/superpowers',
38
- opencode: 'superpowers@git+https://github.com/obra/superpowers.git',
39
- grok: 'superpowers@xai-official',
40
- unsupported: Object.fromEntries(
41
- CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
42
- c,
43
- c === 'claude' ? msg('item.unsupported.superpowersGlobalClaude')
44
- : c === 'kimi' ? msg('item.unsupported.superpowersGlobalKimi')
45
- // 상류 README가 Antigravity 설치를 안내하지만 감지·제거 경로를 실측할
46
- // CLI가 없다 — grok과 같은 기준으로 아직 배선하지 않는다.
47
- : c === 'antigravity' ? msg('item.unsupported.unmeasuredUpstream')
48
- : msg('item.unsupported.upstreamNone'),
49
- ]),
50
- ),
51
- }
52
-
53
- // 테스트가 홈·환경·PATH 탐지를 갈아끼울 수 있게 공장으로 노출한다.
54
- export function createItem(deps = {}) {
55
- return defineGlobalPlugin(CONFIG, deps)
56
- }
57
-
58
- export default createItem()
package/lib/tomlfile.mjs DELETED
@@ -1,67 +0,0 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
2
- import { dirname } from 'node:path'
3
- import { parse } from 'smol-toml'
4
- import { LocalizedError } from './i18n/index.mjs'
5
- import { escapeRegExp } from './untrusted.mjs'
6
-
7
- function readText(file) {
8
- return existsSync(file) ? readFileSync(file, 'utf8') : ''
9
- }
10
-
11
- // 깨진 TOML은 읽기·쓰기 모두 거부한다. 예전에는 파싱 실패를 "섹션 없음"으로
12
- // 돌려줘, 닫는 대괄호가 빠진 파일 끝에 같은 섹션이 하나 더 붙고 사용자는
13
- // 파일이 깨진 것을 끝까지 몰랐다 — jsonfile.mjs의 JSON 규칙과 같다.
14
- function parseStrict(text, file) {
15
- try {
16
- return parse(text)
17
- } catch (err) {
18
- throw new LocalizedError('error.tomlInvalid', { path: file, message: err.message.split('\n')[0] })
19
- }
20
- }
21
-
22
- export function hasSection(file, name) {
23
- const text = readText(file)
24
- if (!text.trim()) return false
25
- const data = parseStrict(text, file)
26
- return data.mcp_servers != null && Object.hasOwn(data.mcp_servers, name)
27
- }
28
-
29
- export function appendSection(file, name, lines) {
30
- let text = readText(file)
31
- if (text.trim()) parseStrict(text, file)
32
- if (text.length > 0 && !text.endsWith('\n')) text += '\n'
33
- const separator = text.length > 0 ? '\n' : ''
34
- text += `${separator}[mcp_servers.${name}]\n${lines.join('\n')}\n`
35
- mkdirSync(dirname(file), { recursive: true })
36
- writeFileSync(file, text)
37
- }
38
-
39
- export function removeSection(file, name) {
40
- const text = readText(file)
41
- if (!text) return
42
- parseStrict(text, file)
43
- const lines = text.split('\n')
44
- const headerRe = new RegExp(`^\\s*\\[mcp_servers\\.${escapeRegExp(name)}(\\.|\\])`)
45
- const anyHeaderRe = /^\s*\[/
46
- const out = []
47
- let skipping = false
48
- let removedAt = -1
49
- for (const line of lines) {
50
- if (skipping && anyHeaderRe.test(line) && !headerRe.test(line)) skipping = false
51
- if (headerRe.test(line)) {
52
- skipping = true
53
- if (removedAt === -1) removedAt = out.length
54
- }
55
- if (!skipping) out.push(line)
56
- }
57
- // 제거 지점의 경계에서만 빈 줄 하나를 정리해, 다른 위치의 빈 줄들은 그대로 보존한다.
58
- if (
59
- removedAt > 0 &&
60
- removedAt < out.length &&
61
- out[removedAt - 1].trim() === '' &&
62
- (out[removedAt] ?? '').trim() === ''
63
- ) {
64
- out.splice(removedAt - 1, 1)
65
- }
66
- writeFileSync(file, out.join('\n'))
67
- }