@rch4com/agent-setup 1.10.0 → 1.11.1

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 (53) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +199 -199
  3. package/install.mjs +163 -163
  4. package/lib/args.mjs +276 -276
  5. package/lib/bootstrap/adapter.mjs +96 -96
  6. package/lib/bootstrap/apply.mjs +380 -380
  7. package/lib/bootstrap/flow.mjs +140 -140
  8. package/lib/bootstrap/record.mjs +154 -154
  9. package/lib/bootstrap/templates.mjs +148 -148
  10. package/lib/bootstrap/text.mjs +19 -19
  11. package/lib/catalog.mjs +234 -234
  12. package/lib/claude-plugins.mjs +64 -64
  13. package/lib/clis.mjs +111 -111
  14. package/lib/context.mjs +66 -66
  15. package/lib/deps.mjs +17 -17
  16. package/lib/design-md/catalog.json +454 -454
  17. package/lib/design-md/catalog.mjs +266 -266
  18. package/lib/design-md/flow.mjs +192 -192
  19. package/lib/design-md/open.mjs +51 -51
  20. package/lib/design-md/providers/awesome-design-md.mjs +124 -124
  21. package/lib/design-md/providers/index.mjs +9 -9
  22. package/lib/design-md/scan.mjs +295 -295
  23. package/lib/engine.mjs +115 -115
  24. package/lib/gitignore.mjs +18 -18
  25. package/lib/i18n/catalog/en.mjs +431 -426
  26. package/lib/i18n/catalog/ko.mjs +416 -411
  27. package/lib/i18n/detect.mjs +39 -39
  28. package/lib/i18n/index.mjs +93 -93
  29. package/lib/items/mcp.codebase-memory.mjs +6 -6
  30. package/lib/items/mcp.notion.mjs +6 -6
  31. package/lib/items/mcp.supabase.mjs +6 -6
  32. package/lib/items/mcp.vercel.mjs +6 -6
  33. package/lib/items/plugin.bkit.mjs +5 -3
  34. package/lib/items/plugin.ecc.mjs +17 -2
  35. package/lib/items/plugin.mattpocock-skills.mjs +18 -8
  36. package/lib/items/plugin.ponytail.mjs +106 -104
  37. package/lib/items/plugin.superpowers.mjs +7 -4
  38. package/lib/items/plugin.understand-anything.mjs +13 -2
  39. package/lib/items/skill.diagram-design.mjs +14 -0
  40. package/lib/items/skill.gsd.mjs +46 -44
  41. package/lib/items/skill.gstack.mjs +54 -54
  42. package/lib/jsonfile.mjs +40 -40
  43. package/lib/status.mjs +93 -93
  44. package/lib/tomlfile.mjs +60 -60
  45. package/lib/tui/detail.mjs +127 -127
  46. package/lib/tui/progress.mjs +160 -160
  47. package/lib/tui/render.mjs +268 -268
  48. package/lib/tui/rows.mjs +317 -317
  49. package/lib/tui/run.mjs +396 -396
  50. package/lib/tui/state.mjs +249 -249
  51. package/lib/update.mjs +85 -85
  52. package/lib/width.mjs +115 -115
  53. package/package.json +36 -36
@@ -1,39 +1,39 @@
1
- // OS 언어 감지. 순수 함수다 — env와 Intl 결과를 인자로 받는다.
2
- // 여기서 지원 여부를 판단하지 않는다: OS가 뭐라고 하는지만 그대로 보고하고,
3
- // 그중 무엇을 쓸지는 resolveLocale이 정한다. 둘을 섞으면 "감지는 됐는데
4
- // 지원하지 않아 영어로 갔다"와 "감지 자체가 실패했다"를 구별할 수 없다.
5
-
6
- // POSIX 관례상 뒤에 오는 것이 앞을 덮지 않는다 — 먼저 잡히는 것이 이긴다.
7
- const ENV_KEYS = ['LC_ALL', 'LC_MESSAGES', 'LANG', 'LANGUAGE']
8
-
9
- // 로케일을 지정하지 않았다는 뜻의 값. 여기서 멈추면 Intl로 내려가지 못한다.
10
- const NEUTRAL = new Set(['c', 'posix'])
11
-
12
- // `ko_KR.UTF-8` · `en_US:en` · `ko-KR` 을 모두 `ko`/`en`으로 줄인다.
13
- // `.`은 인코딩, `@`는 변형(modifier), `:`은 LANGUAGE의 목록 구분자다.
14
- export function baseTag(raw) {
15
- if (typeof raw !== 'string') return null
16
- const head = raw.trim().split(/[.:@]/)[0]
17
- if (!head) return null
18
- if (NEUTRAL.has(head.toLowerCase())) return null
19
- const tag = head.replace(/_/g, '-').split('-')[0].toLowerCase()
20
- return tag || null
21
- }
22
-
23
- // Node 20+는 full-icu가 기본이라 Windows에서도 OS 언어를 반영한다.
24
- // 축소 빌드에서는 'en-US'로 고정되는데, 그때는 영어로 시작하는 것이 맞다.
25
- function intlLocale() {
26
- try {
27
- return Intl.DateTimeFormat().resolvedOptions().locale
28
- } catch {
29
- return null
30
- }
31
- }
32
-
33
- export function detectLocale(env = {}, intl = intlLocale()) {
34
- for (const key of ENV_KEYS) {
35
- const tag = baseTag(env[key])
36
- if (tag) return tag
37
- }
38
- return baseTag(intl)
39
- }
1
+ // OS 언어 감지. 순수 함수다 — env와 Intl 결과를 인자로 받는다.
2
+ // 여기서 지원 여부를 판단하지 않는다: OS가 뭐라고 하는지만 그대로 보고하고,
3
+ // 그중 무엇을 쓸지는 resolveLocale이 정한다. 둘을 섞으면 "감지는 됐는데
4
+ // 지원하지 않아 영어로 갔다"와 "감지 자체가 실패했다"를 구별할 수 없다.
5
+
6
+ // POSIX 관례상 뒤에 오는 것이 앞을 덮지 않는다 — 먼저 잡히는 것이 이긴다.
7
+ const ENV_KEYS = ['LC_ALL', 'LC_MESSAGES', 'LANG', 'LANGUAGE']
8
+
9
+ // 로케일을 지정하지 않았다는 뜻의 값. 여기서 멈추면 Intl로 내려가지 못한다.
10
+ const NEUTRAL = new Set(['c', 'posix'])
11
+
12
+ // `ko_KR.UTF-8` · `en_US:en` · `ko-KR` 을 모두 `ko`/`en`으로 줄인다.
13
+ // `.`은 인코딩, `@`는 변형(modifier), `:`은 LANGUAGE의 목록 구분자다.
14
+ export function baseTag(raw) {
15
+ if (typeof raw !== 'string') return null
16
+ const head = raw.trim().split(/[.:@]/)[0]
17
+ if (!head) return null
18
+ if (NEUTRAL.has(head.toLowerCase())) return null
19
+ const tag = head.replace(/_/g, '-').split('-')[0].toLowerCase()
20
+ return tag || null
21
+ }
22
+
23
+ // Node 20+는 full-icu가 기본이라 Windows에서도 OS 언어를 반영한다.
24
+ // 축소 빌드에서는 'en-US'로 고정되는데, 그때는 영어로 시작하는 것이 맞다.
25
+ function intlLocale() {
26
+ try {
27
+ return Intl.DateTimeFormat().resolvedOptions().locale
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ export function detectLocale(env = {}, intl = intlLocale()) {
34
+ for (const key of ENV_KEYS) {
35
+ const tag = baseTag(env[key])
36
+ if (tag) return tag
37
+ }
38
+ return baseTag(intl)
39
+ }
@@ -1,93 +1,93 @@
1
- // 로케일 해석과 문구 조회. 잎(leaf) 모듈이라 아무것도 import 하지 않는다 —
2
- // args.mjs·context.mjs·bootstrap/*가 전부 여기에 닿으므로, 외부 의존성이
3
- // 하나라도 들어오면 npm install 없이 도는 부트스트랩이 깨진다.
4
- // bootstrap.isolation.test.mjs가 이 불변식을 지킨다.
5
- //
6
- // 전역 싱글턴을 두지 않는다. t는 만들어서 넘기는 값이다 — 그래야 순수
7
- // 모듈(state.mjs)이 계속 아무것도 import 하지 않고, 테스트가 파일 병렬로
8
- // 돌아도 로케일이 서로를 오염시키지 않는다.
9
- import EN from './catalog/en.mjs'
10
- import KO from './catalog/ko.mjs'
11
-
12
- export const LOCALES = ['en', 'ko']
13
-
14
- const CATALOGS = { en: EN, ko: KO }
15
- const PLACEHOLDER = /\{(\w+)\}/g
16
-
17
- // 자리에 값이 없으면 던진다. `12 of undefined files`가 화면에 남는 것보다
18
- // 즉시 실패가 낫다 — 이건 번역 누락이 아니라 호출부의 버그다.
19
- function fill(template, params, key) {
20
- return template.replace(PLACEHOLDER, (_, name) => {
21
- if (!Object.hasOwn(params, name)) {
22
- throw new Error(`i18n: '${key}'의 자리 {${name}}에 값이 없습니다`)
23
- }
24
- return String(params[name])
25
- })
26
- }
27
-
28
- // 활성 카탈로그 → 영어 폴백 → 예외.
29
- // 번역 누락은 사용자 화면을 깨뜨리지 않아야 하지만, 영어에도 없는 키는
30
- // 개발자 오타다. 조용히 넘기면 화면에 'log.file.creat'가 그대로 찍힌다.
31
- function lookup(locale, key) {
32
- const value = CATALOGS[locale]?.[key]
33
- if (value !== undefined) return value
34
- const base = EN[key]
35
- if (base === undefined) throw new Error(`i18n: 알 수 없는 키입니다: ${key}`)
36
- return base
37
- }
38
-
39
- export function createT(locale) {
40
- const use = LOCALES.includes(locale) ? locale : 'en'
41
- const t = (key, params = {}) => {
42
- const value = lookup(use, key)
43
- return Array.isArray(value)
44
- ? value.map((line) => fill(line, params, key)).join('\n')
45
- : fill(value, params, key)
46
- }
47
- t.locale = use
48
- return t
49
- }
50
-
51
- // 앞이 이긴다: 플래그 > 환경변수 > 설치 기록 > OS 감지 > 영어.
52
- // 지원하지 않는 값은 조용히 건너뛴다 — 손으로 편집된 기록이나 환경에 남은
53
- // 값 때문에 도구가 죽으면 안 된다. 사용자가 --lang으로 명시한 경우만
54
- // args.mjs가 따로 거부한다.
55
- export function resolveLocale({ flag = null, env = {}, record = null, detected = null } = {}) {
56
- for (const value of [flag, env.AGENT_SETUP_LANG, record?.lang, detected]) {
57
- if (typeof value === 'string' && LOCALES.includes(value)) return value
58
- }
59
- return 'en'
60
- }
61
-
62
- // 플래그나 환경변수가 로케일을 못박았는가. TUI에서 고른 언어는 이번 실행에는
63
- // 적용되지만 기록에만 남고 다음 실행에서 다시 밀리므로, 화면이 그 사실을
64
- // 알려야 한다. 판정 규칙이 resolveLocale과 갈리면 안 되므로 같은 모듈에 둔다 —
65
- // 지원하지 않는 값은 resolveLocale이 이미 건너뛰었고, 셸에 남은
66
- // AGENT_SETUP_LANG=ja까지 "환경변수가 이긴다"고 말하면 거짓말이 된다.
67
- export function isLocaleForced({ flag = null, env = {} } = {}) {
68
- return [flag, env.AGENT_SETUP_LANG].some((v) => typeof v === 'string' && LOCALES.includes(v))
69
- }
70
-
71
- // 옵션 주머니가 없어 t를 꿸 수 없는 곳(context.mjs의 repoPath,
72
- // record.mjs의 readRecord)이 쓴다. .message는 영어로 즉시 채워 스택
73
- // 트레이스가 그대로 읽히게 하고, 진입점이 .key를 보고 다시 렌더한다.
74
- export class LocalizedError extends Error {
75
- constructor(key, params = {}) {
76
- super(createT('en')(key, params))
77
- this.name = 'LocalizedError'
78
- this.key = key
79
- this.params = params
80
- }
81
- }
82
-
83
- // 데이터 계층이 만들어 나중에 표시되는 문구용. detect()의 detail이나
84
- // unsupported 사유는 만들어지는 시점에 로케일을 모른다 — 키를 담아 두고
85
- // 렌더 시점에 toText로 푼다.
86
- export function msg(key, params = {}) {
87
- return { key, params }
88
- }
89
-
90
- export function toText(t, value) {
91
- if (value === null || value === undefined) return value
92
- return typeof value === 'string' ? value : t(value.key, value.params)
93
- }
1
+ // 로케일 해석과 문구 조회. 잎(leaf) 모듈이라 아무것도 import 하지 않는다 —
2
+ // args.mjs·context.mjs·bootstrap/*가 전부 여기에 닿으므로, 외부 의존성이
3
+ // 하나라도 들어오면 npm install 없이 도는 부트스트랩이 깨진다.
4
+ // bootstrap.isolation.test.mjs가 이 불변식을 지킨다.
5
+ //
6
+ // 전역 싱글턴을 두지 않는다. t는 만들어서 넘기는 값이다 — 그래야 순수
7
+ // 모듈(state.mjs)이 계속 아무것도 import 하지 않고, 테스트가 파일 병렬로
8
+ // 돌아도 로케일이 서로를 오염시키지 않는다.
9
+ import EN from './catalog/en.mjs'
10
+ import KO from './catalog/ko.mjs'
11
+
12
+ export const LOCALES = ['en', 'ko']
13
+
14
+ const CATALOGS = { en: EN, ko: KO }
15
+ const PLACEHOLDER = /\{(\w+)\}/g
16
+
17
+ // 자리에 값이 없으면 던진다. `12 of undefined files`가 화면에 남는 것보다
18
+ // 즉시 실패가 낫다 — 이건 번역 누락이 아니라 호출부의 버그다.
19
+ function fill(template, params, key) {
20
+ return template.replace(PLACEHOLDER, (_, name) => {
21
+ if (!Object.hasOwn(params, name)) {
22
+ throw new Error(`i18n: '${key}'의 자리 {${name}}에 값이 없습니다`)
23
+ }
24
+ return String(params[name])
25
+ })
26
+ }
27
+
28
+ // 활성 카탈로그 → 영어 폴백 → 예외.
29
+ // 번역 누락은 사용자 화면을 깨뜨리지 않아야 하지만, 영어에도 없는 키는
30
+ // 개발자 오타다. 조용히 넘기면 화면에 'log.file.creat'가 그대로 찍힌다.
31
+ function lookup(locale, key) {
32
+ const value = CATALOGS[locale]?.[key]
33
+ if (value !== undefined) return value
34
+ const base = EN[key]
35
+ if (base === undefined) throw new Error(`i18n: 알 수 없는 키입니다: ${key}`)
36
+ return base
37
+ }
38
+
39
+ export function createT(locale) {
40
+ const use = LOCALES.includes(locale) ? locale : 'en'
41
+ const t = (key, params = {}) => {
42
+ const value = lookup(use, key)
43
+ return Array.isArray(value)
44
+ ? value.map((line) => fill(line, params, key)).join('\n')
45
+ : fill(value, params, key)
46
+ }
47
+ t.locale = use
48
+ return t
49
+ }
50
+
51
+ // 앞이 이긴다: 플래그 > 환경변수 > 설치 기록 > OS 감지 > 영어.
52
+ // 지원하지 않는 값은 조용히 건너뛴다 — 손으로 편집된 기록이나 환경에 남은
53
+ // 값 때문에 도구가 죽으면 안 된다. 사용자가 --lang으로 명시한 경우만
54
+ // args.mjs가 따로 거부한다.
55
+ export function resolveLocale({ flag = null, env = {}, record = null, detected = null } = {}) {
56
+ for (const value of [flag, env.AGENT_SETUP_LANG, record?.lang, detected]) {
57
+ if (typeof value === 'string' && LOCALES.includes(value)) return value
58
+ }
59
+ return 'en'
60
+ }
61
+
62
+ // 플래그나 환경변수가 로케일을 못박았는가. TUI에서 고른 언어는 이번 실행에는
63
+ // 적용되지만 기록에만 남고 다음 실행에서 다시 밀리므로, 화면이 그 사실을
64
+ // 알려야 한다. 판정 규칙이 resolveLocale과 갈리면 안 되므로 같은 모듈에 둔다 —
65
+ // 지원하지 않는 값은 resolveLocale이 이미 건너뛰었고, 셸에 남은
66
+ // AGENT_SETUP_LANG=ja까지 "환경변수가 이긴다"고 말하면 거짓말이 된다.
67
+ export function isLocaleForced({ flag = null, env = {} } = {}) {
68
+ return [flag, env.AGENT_SETUP_LANG].some((v) => typeof v === 'string' && LOCALES.includes(v))
69
+ }
70
+
71
+ // 옵션 주머니가 없어 t를 꿸 수 없는 곳(context.mjs의 repoPath,
72
+ // record.mjs의 readRecord)이 쓴다. .message는 영어로 즉시 채워 스택
73
+ // 트레이스가 그대로 읽히게 하고, 진입점이 .key를 보고 다시 렌더한다.
74
+ export class LocalizedError extends Error {
75
+ constructor(key, params = {}) {
76
+ super(createT('en')(key, params))
77
+ this.name = 'LocalizedError'
78
+ this.key = key
79
+ this.params = params
80
+ }
81
+ }
82
+
83
+ // 데이터 계층이 만들어 나중에 표시되는 문구용. detect()의 detail이나
84
+ // unsupported 사유는 만들어지는 시점에 로케일을 모른다 — 키를 담아 두고
85
+ // 렌더 시점에 toText로 푼다.
86
+ export function msg(key, params = {}) {
87
+ return { key, params }
88
+ }
89
+
90
+ export function toText(t, value) {
91
+ if (value === null || value === undefined) return value
92
+ return typeof value === 'string' ? value : t(value.key, value.params)
93
+ }
@@ -1,6 +1,6 @@
1
- import { defineMcp } from '../catalog.mjs'
2
- export default defineMcp({
3
- id: 'mcp.codebase-memory', label: 'Codebase Memory MCP', group: '__context',
4
- server: { kind: 'stdio', command: 'codebase-memory-mcp', args: [] },
5
- note: 'item.mcp.codebase-memory.note',
6
- })
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.codebase-memory', label: 'Codebase Memory MCP', group: '__context',
4
+ server: { kind: 'stdio', command: 'codebase-memory-mcp', args: [] },
5
+ note: 'item.mcp.codebase-memory.note',
6
+ })
@@ -1,6 +1,6 @@
1
- import { defineMcp } from '../catalog.mjs'
2
- export default defineMcp({
3
- id: 'mcp.notion', label: 'Notion MCP', group: '__service',
4
- server: { kind: 'http', url: 'https://mcp.notion.com/mcp' },
5
- note: 'item.mcp.notion.note',
6
- })
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.notion', label: 'Notion MCP', group: '__service',
4
+ server: { kind: 'http', url: 'https://mcp.notion.com/mcp' },
5
+ note: 'item.mcp.notion.note',
6
+ })
@@ -1,6 +1,6 @@
1
- import { defineMcp } from '../catalog.mjs'
2
- export default defineMcp({
3
- id: 'mcp.supabase', label: 'Supabase MCP', group: '__service',
4
- server: { kind: 'http', url: 'https://mcp.supabase.com/mcp' },
5
- note: 'item.mcp.supabase.note',
6
- })
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.supabase', label: 'Supabase MCP', group: '__service',
4
+ server: { kind: 'http', url: 'https://mcp.supabase.com/mcp' },
5
+ note: 'item.mcp.supabase.note',
6
+ })
@@ -1,6 +1,6 @@
1
- import { defineMcp } from '../catalog.mjs'
2
- export default defineMcp({
3
- id: 'mcp.vercel', label: 'Vercel MCP', group: '__service',
4
- server: { kind: 'http', url: 'https://mcp.vercel.com' },
5
- note: 'item.mcp.vercel.note',
6
- })
1
+ import { defineMcp } from '../catalog.mjs'
2
+ export default defineMcp({
3
+ id: 'mcp.vercel', label: 'Vercel MCP', group: '__service',
4
+ server: { kind: 'http', url: 'https://mcp.vercel.com' },
5
+ note: 'item.mcp.vercel.note',
6
+ })
@@ -4,9 +4,11 @@ import { msg } from '../i18n/index.mjs'
4
4
 
5
5
  // 상류의 Codex·Gemini 지원은 이 플러그인이 아니라 별도 리포의 독립 포팅본이다
6
6
  // (bkit-codex: install 스크립트+MCP, 기능 241개 중 80개, hooks 없음 /
7
- // bkit-gemini: Gemini CLI extension). 플러그인 항목을 재사용할 수 없어 사유만
8
- // 남긴다. Gemini CLI는 2026-06-18 종료라 Gemini판은 Antigravity 승계 추정
9
- // (2026-08-02 검증).
7
+ // bkit-gemini: Gemini CLI extension). 플러그인 항목을 재사용할 수 없어 사유만 남긴다.
8
+ // 예전에 "Gemini판은 Antigravity 승계 추정"이라 적었는데 근거가 없었다 —
9
+ // bkit-gemini는 v2.0.7까지 Gemini CLI 확장 그대로이고 README에 Antigravity가
10
+ // 한 번도 나오지 않는다. 다만 최종 푸시가 2026-05-20이라 Gemini CLI 종료
11
+ // (2026-06-18) 이후로는 갱신이 없다. 추정 대신 그 사실만 적는다(2026-08-15 실측).
10
12
  const PORTS = ['codex', 'gemini']
11
13
 
12
14
  export default definePlugin({
@@ -1,11 +1,26 @@
1
1
  import { definePlugin } from '../catalog.mjs'
2
+ import { CLI_IDS } from '../clis.mjs'
3
+ import { msg } from '../i18n/index.mjs'
2
4
  // 레지스트리 경로(`npx skills add`)도 열려 있지만 쓰지 않는다 — ECC의 skills/에는
3
- // 281개가 들어 있어 `--skill` 없이 넣으면 공유 스킬 디렉터리를 통째로 덮는다.
4
- // 플러그인은 프로필을 상류가 관리하므로 저장소에 남는 파일이 없다.
5
+ // 284개가 들어 있어 `--skill` 없이 넣으면 공유 스킬 디렉터리를 통째로 덮는다
6
+ // (2026-08-15 저장소 트리로 실측). 플러그인은 프로필을 상류가 관리하므로
7
+ // 저장소에 남는 파일이 없다.
8
+ //
9
+ // 미배선 사유는 CLI마다 갈린다. Codex만 상류가 네이티브 플러그인을 주는데,
10
+ // README가 못을 박는다 — "Codex stores one enabled plugin state in the active
11
+ // CODEX_HOME; it does not offer Claude's user, project, and local scopes."
12
+ // 나머지는 ./install.sh --target으로 열려 있고 Gemini(.gemini/)·Kimi(.kimi-code/)는
13
+ // 프로젝트 로컬이기까지 하다. 그쪽을 막는 것은 스코프가 아니라 284개라는 규모다.
5
14
  export default definePlugin({
6
15
  id: 'plugin.ecc', label: 'ECC', group: '__flow',
7
16
  installId: 'ecc@ecc',
8
17
  detectIds: ['ecc@ecc'],
9
18
  marketplace: { name: 'ecc', repo: 'affaan-m/ECC' },
10
19
  note: 'item.plugin.ecc.note',
20
+ unsupported: Object.fromEntries(
21
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [
22
+ c,
23
+ c === 'codex' ? msg('item.unsupported.eccCodexHome') : msg('item.unsupported.eccScale'),
24
+ ]),
25
+ ),
11
26
  })
@@ -1,8 +1,18 @@
1
- import { definePlugin } from '../catalog.mjs'
2
- export default definePlugin({
3
- id: 'plugin.mattpocock-skills', label: 'Matt Pocock skills', group: '__flow',
4
- installId: 'mattpocock-skills@mattpocock',
5
- detectIds: ['mattpocock-skills@mattpocock'],
6
- marketplace: { name: 'mattpocock', repo: 'mattpocock/skills' },
7
- note: 'item.plugin.mattpocock-skills.note',
8
- })
1
+ import { definePlugin } from '../catalog.mjs'
2
+ import { CLI_IDS } from '../clis.mjs'
3
+ import { msg } from '../i18n/index.mjs'
4
+ // 상류가 "Codex, and other agents" 절에서 `npx skills add mattpocock/skills`를
5
+ // 안내한다 — 이 저장소의 레지스트리 스킬 항목이 쓰는 바로 그 경로다(레지스트리로
6
+ // 35개가 보이는 것을 실측, 2026-08-15). 그러니 미배선 사유는 "상류에 없다"가
7
+ // 아니라 "우리가 플러그인 쪽을 골랐다"이고, 사유가 그렇게 말해야 레지스트리
8
+ // 항목으로 옮길 수 있다는 것도 화면에서 읽힌다.
9
+ export default definePlugin({
10
+ id: 'plugin.mattpocock-skills', label: 'Matt Pocock skills', group: '__flow',
11
+ installId: 'mattpocock-skills@mattpocock',
12
+ detectIds: ['mattpocock-skills@mattpocock'],
13
+ marketplace: { name: 'mattpocock', repo: 'mattpocock/skills' },
14
+ note: 'item.plugin.mattpocock-skills.note',
15
+ unsupported: Object.fromEntries(
16
+ CLI_IDS.filter((c) => c !== 'claude').map((c) => [c, msg('item.unsupported.mattpocockRegistry')]),
17
+ ),
18
+ })
@@ -1,104 +1,106 @@
1
- import { CLI_IDS } from '../clis.mjs'
2
- import { isPluginEnabled, enablePlugin, disablePlugin } from '../claude-plugins.mjs'
3
- import { repoPath, repoPathStrict } from '../context.mjs'
4
- import { readJson, setKey, removeKey } from '../jsonfile.mjs'
5
- import { msg, LocalizedError } from '../i18n/index.mjs'
6
-
7
- // 상류 안내는 `--scope user`지만 이 저장소의 항목은 전부 프로젝트 스코프다.
8
- // 같은 규칙 묶음이 두 경로로 들어간다 — Claude Code는 플러그인 CLI(실패하면
9
- // .claude/settings.json 기록), OpenCode는 프로젝트 설정의 plugin 배열.
10
- // 두 경로 모두 저장소 안에만 쓴다.
11
- const INSTALL_ID = 'ponytail@ponytail'
12
- const MARKETPLACE = { name: 'ponytail', repo: 'DietrichGebert/ponytail' }
13
- const OPENCODE_FILE = 'opencode.jsonc'
14
- const OPENCODE_ENTRY = '@dietrichgebert/ponytail'
15
- const SUPPORTS = ['claude', 'opencode']
16
-
17
- // plugin 키가 배열이 아니면 손대지 않는다 — 통째로 덮어쓰면 사용자가 적어 둔
18
- // 설정이 조용히 사라진다. 없는 것(undefined)과 다른 타입을 구분해서 돌려준다.
19
- function opencodePlugins(root) {
20
- const value = readJson(repoPath(root, OPENCODE_FILE))?.plugin
21
- if (value === undefined) return []
22
- if (!Array.isArray(value)) return null
23
- return value
24
- }
25
-
26
- function opencodeListOrThrow(root) {
27
- const list = opencodePlugins(root)
28
- if (list === null) throw new LocalizedError('error.ponytailOpencodePlugin', { file: OPENCODE_FILE })
29
- return list
30
- }
31
-
32
- function hasOpencode(root) {
33
- return (opencodePlugins(root) ?? []).includes(OPENCODE_ENTRY)
34
- }
35
-
36
- export default {
37
- id: 'plugin.ponytail', category: 'plugin', label: 'Ponytail', scope: 'project',
38
- group: '__token',
39
- supports: SUPPORTS,
40
- unsupported: Object.fromEntries(
41
- CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
42
- c,
43
- // codex·copilot·gemini는 상류에 설치 경로가 있지만 전부 사용자 스코프다.
44
- // 나머지는 플러그인 기구 자체가 없어 AGENTS.md에 규칙을 옮겨 적어야 한다.
45
- ['codex', 'copilot', 'gemini'].includes(c)
46
- ? msg('item.unsupported.ponytailUser')
47
- : msg('item.unsupported.ponytailRules'),
48
- ]),
49
- ),
50
- note: 'item.plugin.ponytail.note',
51
-
52
- async detect({ root }) {
53
- const present = []
54
- if (isPluginEnabled(root, [INSTALL_ID])) present.push('claude')
55
- if (hasOpencode(root)) present.push('opencode')
56
- if (present.length === 0) return { status: 'absent' }
57
- if (present.length === SUPPORTS.length) return { status: 'installed' }
58
- return {
59
- status: 'partial',
60
- detail: msg('item.plugin.partial', {
61
- present: present.join(', '),
62
- missing: SUPPORTS.filter((c) => !present.includes(c)).join(', '),
63
- }),
64
- }
65
- },
66
-
67
- async install({ root, dryRun, exec, log = () => {}, t }) {
68
- let deferred = false
69
- if (!isPluginEnabled(root, [INSTALL_ID])) {
70
- await exec('claude', ['plugin', 'marketplace', 'add', MARKETPLACE.repo], { cwd: root })
71
- const r = await exec('claude', ['plugin', 'install', INSTALL_ID, '--scope', 'project'], { cwd: root })
72
- if (!r.ok) {
73
- if (!dryRun) enablePlugin(root, INSTALL_ID, MARKETPLACE)
74
- deferred = true
75
- }
76
- }
77
- if (!hasOpencode(root)) {
78
- const list = opencodeListOrThrow(root)
79
- if (dryRun) log(t('log.plugin.add', { cli: 'OpenCode', name: OPENCODE_ENTRY }))
80
- // 파일이 없거나 plugin 키가 없으면 배열째 만들고, 있으면 끝에 덧붙인다.
81
- else if (list.length === 0) setKey(repoPathStrict(root, OPENCODE_FILE), ['plugin'], [OPENCODE_ENTRY])
82
- else setKey(repoPathStrict(root, OPENCODE_FILE), ['plugin', list.length], OPENCODE_ENTRY)
83
- }
84
- if (deferred) return { fallback: true, message: msg('item.plugin.deferred') }
85
- },
86
-
87
- async uninstall({ root, dryRun, exec, log = () => {}, t }) {
88
- if (isPluginEnabled(root, [INSTALL_ID])) {
89
- const r = await exec('claude', ['plugin', 'uninstall', INSTALL_ID], { cwd: root })
90
- if (!r.ok && !dryRun) disablePlugin(root, [INSTALL_ID])
91
- }
92
- if (!hasOpencode(root)) return
93
- if (dryRun) {
94
- log(t('log.plugin.remove', { cli: 'OpenCode', name: OPENCODE_ENTRY }))
95
- return
96
- }
97
- const kept = opencodeListOrThrow(root).filter((p) => p !== OPENCODE_ENTRY)
98
- const file = repoPathStrict(root, OPENCODE_FILE)
99
- // 우리가 넣은 항목만 빼고 비면 키째 지운다 — 빈 배열을 남기면 우리가
100
- // 만든 흔적이 설정에 계속 남는다.
101
- if (kept.length === 0) removeKey(file, ['plugin'])
102
- else setKey(file, ['plugin'], kept)
103
- },
104
- }
1
+ import { CLI_IDS } from '../clis.mjs'
2
+ import { isPluginEnabled, enablePlugin, disablePlugin } from '../claude-plugins.mjs'
3
+ import { repoPath, repoPathStrict } from '../context.mjs'
4
+ import { readJson, setKey, removeKey } from '../jsonfile.mjs'
5
+ import { msg, LocalizedError } from '../i18n/index.mjs'
6
+
7
+ // 상류 안내는 `--scope user`지만 이 저장소의 항목은 전부 프로젝트 스코프다.
8
+ // 같은 규칙 묶음이 두 경로로 들어간다 — Claude Code는 플러그인 CLI(실패하면
9
+ // .claude/settings.json 기록), OpenCode는 프로젝트 설정의 plugin 배열.
10
+ // 두 경로 모두 저장소 안에만 쓴다.
11
+ const INSTALL_ID = 'ponytail@ponytail'
12
+ const MARKETPLACE = { name: 'ponytail', repo: 'DietrichGebert/ponytail' }
13
+ const OPENCODE_FILE = 'opencode.jsonc'
14
+ const OPENCODE_ENTRY = '@dietrichgebert/ponytail'
15
+ const SUPPORTS = ['claude', 'opencode']
16
+
17
+ // plugin 키가 배열이 아니면 손대지 않는다 — 통째로 덮어쓰면 사용자가 적어 둔
18
+ // 설정이 조용히 사라진다. 없는 것(undefined)과 다른 타입을 구분해서 돌려준다.
19
+ function opencodePlugins(root) {
20
+ const value = readJson(repoPath(root, OPENCODE_FILE))?.plugin
21
+ if (value === undefined) return []
22
+ if (!Array.isArray(value)) return null
23
+ return value
24
+ }
25
+
26
+ function opencodeListOrThrow(root) {
27
+ const list = opencodePlugins(root)
28
+ if (list === null) throw new LocalizedError('error.ponytailOpencodePlugin', { file: OPENCODE_FILE })
29
+ return list
30
+ }
31
+
32
+ function hasOpencode(root) {
33
+ return (opencodePlugins(root) ?? []).includes(OPENCODE_ENTRY)
34
+ }
35
+
36
+ export default {
37
+ id: 'plugin.ponytail', category: 'plugin', label: 'Ponytail', scope: 'project',
38
+ group: '__token',
39
+ supports: SUPPORTS,
40
+ unsupported: Object.fromEntries(
41
+ CLI_IDS.filter((c) => !SUPPORTS.includes(c)).map((c) => [
42
+ c,
43
+ // codex·copilot·grok·gemini는 상류에 설치 경로가 있지만 전부 사용자
44
+ // 스코프다. grok은 `grok plugin install DietrichGebert/ponytail --trust`로
45
+ // 플러그인 기구가 엄연히 있다 — "기구가 없다" 쪽에 두면 거짓이다
46
+ // (2026-08-15 재측정). 나머지는 상류의 규칙 파일을 손으로 옮겨야 한다.
47
+ ['codex', 'copilot', 'gemini', 'grok'].includes(c)
48
+ ? msg('item.unsupported.ponytailUser')
49
+ : msg('item.unsupported.ponytailRules'),
50
+ ]),
51
+ ),
52
+ note: 'item.plugin.ponytail.note',
53
+
54
+ async detect({ root }) {
55
+ const present = []
56
+ if (isPluginEnabled(root, [INSTALL_ID])) present.push('claude')
57
+ if (hasOpencode(root)) present.push('opencode')
58
+ if (present.length === 0) return { status: 'absent' }
59
+ if (present.length === SUPPORTS.length) return { status: 'installed' }
60
+ return {
61
+ status: 'partial',
62
+ detail: msg('item.plugin.partial', {
63
+ present: present.join(', '),
64
+ missing: SUPPORTS.filter((c) => !present.includes(c)).join(', '),
65
+ }),
66
+ }
67
+ },
68
+
69
+ async install({ root, dryRun, exec, log = () => {}, t }) {
70
+ let deferred = false
71
+ if (!isPluginEnabled(root, [INSTALL_ID])) {
72
+ await exec('claude', ['plugin', 'marketplace', 'add', MARKETPLACE.repo], { cwd: root })
73
+ const r = await exec('claude', ['plugin', 'install', INSTALL_ID, '--scope', 'project'], { cwd: root })
74
+ if (!r.ok) {
75
+ if (!dryRun) enablePlugin(root, INSTALL_ID, MARKETPLACE)
76
+ deferred = true
77
+ }
78
+ }
79
+ if (!hasOpencode(root)) {
80
+ const list = opencodeListOrThrow(root)
81
+ if (dryRun) log(t('log.plugin.add', { cli: 'OpenCode', name: OPENCODE_ENTRY }))
82
+ // 파일이 없거나 plugin 키가 없으면 배열째 만들고, 있으면 끝에 덧붙인다.
83
+ else if (list.length === 0) setKey(repoPathStrict(root, OPENCODE_FILE), ['plugin'], [OPENCODE_ENTRY])
84
+ else setKey(repoPathStrict(root, OPENCODE_FILE), ['plugin', list.length], OPENCODE_ENTRY)
85
+ }
86
+ if (deferred) return { fallback: true, message: msg('item.plugin.deferred') }
87
+ },
88
+
89
+ async uninstall({ root, dryRun, exec, log = () => {}, t }) {
90
+ if (isPluginEnabled(root, [INSTALL_ID])) {
91
+ const r = await exec('claude', ['plugin', 'uninstall', INSTALL_ID], { cwd: root })
92
+ if (!r.ok && !dryRun) disablePlugin(root, [INSTALL_ID])
93
+ }
94
+ if (!hasOpencode(root)) return
95
+ if (dryRun) {
96
+ log(t('log.plugin.remove', { cli: 'OpenCode', name: OPENCODE_ENTRY }))
97
+ return
98
+ }
99
+ const kept = opencodeListOrThrow(root).filter((p) => p !== OPENCODE_ENTRY)
100
+ const file = repoPathStrict(root, OPENCODE_FILE)
101
+ // 우리가 넣은 항목만 빼고 비면 키째 지운다 — 빈 배열을 남기면 우리가
102
+ // 만든 흔적이 설정에 계속 남는다.
103
+ if (kept.length === 0) removeKey(file, ['plugin'])
104
+ else setKey(file, ['plugin'], kept)
105
+ },
106
+ }
@@ -2,12 +2,15 @@ import { definePlugin } from '../catalog.mjs'
2
2
  import { CLI_IDS } from '../clis.mjs'
3
3
  import { msg } from '../i18n/index.mjs'
4
4
 
5
- // 상류(obra/superpowers v6.2.0)는 Codex·Antigravity·Copilot CLI 등 11개
5
+ // 상류(obra/superpowers v6.3.0)는 Codex·Antigravity·Copilot CLI 등 14개
6
6
  // 하니스를 지원하지만 설치 명령이 하니스마다 전부 달라 공유 경로가 없다
7
7
  // (README: "install Superpowers separately for each one"). 이 항목이 배선하는
8
- // 것은 Claude Code 플러그인 판뿐이다. Kilo·Kiro·Grok·VS Code는 상류에 공식
9
- // 설치 경로가 없다(2026-08-02 검증).
10
- const SEPARATE = ['codex', 'gemini', 'opencode', 'kimi', 'copilot']
8
+ // 것은 Claude Code 플러그인 판뿐이다. Kilo·Kiro·VS Code는 상류에 공식 설치
9
+ // 경로가 없다(2026-08-15 재측정).
10
+ // Grok은 6.2.0에는 없다가 6.3.0에서 들어왔다 — `### Grok Build CLI`의
11
+ // `grok plugin install superpowers@xai-official --trust`다. Devin·Hermes도 같이
12
+ // 늘었지만 둘 다 이 저장소가 다루는 CLI가 아니다.
13
+ const SEPARATE = ['codex', 'gemini', 'opencode', 'kimi', 'copilot', 'grok']
11
14
 
12
15
  export default definePlugin({
13
16
  id: 'plugin.superpowers', label: 'superpowers', group: '__flow',