@rch4com/agent-setup 1.8.0 → 1.10.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 +199 -197
- package/install.mjs +163 -163
- package/lib/args.mjs +276 -276
- package/lib/bootstrap/adapter.mjs +96 -96
- package/lib/bootstrap/apply.mjs +380 -380
- package/lib/bootstrap/flow.mjs +140 -140
- package/lib/bootstrap/record.mjs +154 -154
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -19
- package/lib/catalog.mjs +234 -234
- package/lib/claude-plugins.mjs +64 -64
- package/lib/clis.mjs +111 -111
- package/lib/context.mjs +66 -66
- package/lib/deps.mjs +17 -17
- package/lib/design-md/catalog.json +454 -454
- package/lib/design-md/catalog.mjs +266 -266
- package/lib/design-md/flow.mjs +192 -192
- package/lib/design-md/open.mjs +51 -51
- package/lib/design-md/providers/awesome-design-md.mjs +124 -124
- package/lib/design-md/providers/index.mjs +9 -9
- package/lib/design-md/scan.mjs +295 -295
- package/lib/engine.mjs +115 -115
- package/lib/gitignore.mjs +18 -18
- package/lib/i18n/catalog/en.mjs +426 -425
- package/lib/i18n/catalog/ko.mjs +411 -410
- package/lib/i18n/detect.mjs +39 -39
- package/lib/i18n/index.mjs +93 -93
- package/lib/items/mcp.codebase-memory.mjs +6 -6
- 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.mattpocock-skills.mjs +8 -8
- package/lib/items/plugin.ponytail.mjs +104 -104
- package/lib/items/skill.gsd.mjs +44 -44
- package/lib/items/skill.gstack.mjs +54 -54
- package/lib/items/skill.hallmark.mjs +10 -0
- package/lib/jsonfile.mjs +40 -40
- package/lib/status.mjs +93 -93
- package/lib/tomlfile.mjs +60 -60
- package/lib/tui/detail.mjs +127 -127
- package/lib/tui/progress.mjs +160 -160
- package/lib/tui/render.mjs +268 -268
- package/lib/tui/rows.mjs +317 -317
- package/lib/tui/run.mjs +396 -396
- package/lib/tui/state.mjs +249 -249
- package/lib/update.mjs +85 -85
- package/lib/width.mjs +115 -115
- package/package.json +36 -36
package/lib/engine.mjs
CHANGED
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import { makeExec } from './catalog.mjs'
|
|
2
|
-
import { createT, LocalizedError, msg } from './i18n/index.mjs'
|
|
3
|
-
|
|
4
|
-
export async function scan(root, items) {
|
|
5
|
-
const states = []
|
|
6
|
-
for (const item of items) {
|
|
7
|
-
try {
|
|
8
|
-
const r = await item.detect({ root })
|
|
9
|
-
states.push({ item, status: r.status, detail: r.detail })
|
|
10
|
-
} catch (err) {
|
|
11
|
-
states.push({ item, status: 'absent', detail: msg('item.scanFailed', { message: err.message }) })
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return states
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// item.exclusive는 "이 항목들은 같은 파일 하나를 두고 다툰다"는 표시다.
|
|
18
|
-
// TUI는 고르는 순간에 배타를 지키지만(state.mjs), --set은 목록을 통째로 받는다 —
|
|
19
|
-
// 둘을 함께 주면 나중 항목이 앞선 항목을 덮어써 성공한 것처럼 끝난다.
|
|
20
|
-
// 조용히 하나를 고르는 대신 어느 쪽을 원했는지 되묻는다.
|
|
21
|
-
export function assertExclusive(items, selectedIds) {
|
|
22
|
-
const groups = new Map()
|
|
23
|
-
for (const item of items) {
|
|
24
|
-
if (!item.exclusive || !selectedIds.has(item.id)) continue
|
|
25
|
-
groups.set(item.exclusive, [...(groups.get(item.exclusive) ?? []), item.id])
|
|
26
|
-
}
|
|
27
|
-
for (const ids of groups.values()) {
|
|
28
|
-
if (ids.length > 1) throw new LocalizedError('error.exclusiveItems', { ids: ids.join(', ') })
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function planChanges(states, selectedIds) {
|
|
33
|
-
const changes = []
|
|
34
|
-
for (const { item, status } of states) {
|
|
35
|
-
const selected = selectedIds.has(item.id)
|
|
36
|
-
if (selected && status === 'absent') changes.push({ item, action: 'install' })
|
|
37
|
-
else if (selected && status === 'partial') changes.push({ item, action: 'complete' })
|
|
38
|
-
else if (!selected && status !== 'absent') changes.push({ item, action: 'uninstall' })
|
|
39
|
-
}
|
|
40
|
-
return changes
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en'), onProgress = null, shouldStop = null } = {}) {
|
|
44
|
-
const baseExec = makeExec(dryRun, log)
|
|
45
|
-
const results = []
|
|
46
|
-
const total = changes.length
|
|
47
|
-
|
|
48
|
-
// 알림은 참고용이지 설치를 좌우하지 않는다. onProgress는 훗날 실제 렌더러가
|
|
49
|
-
// 될 자리라(Task 11) 그 안의 버그가 던지면, notify를 감싸지 않는 한 예외가
|
|
50
|
-
// apply()를 통째로 빠져나가 이미 디스크에 쓴 항목의 결과를 아무도 못 받는다.
|
|
51
|
-
// command 단계 알림은 install/uninstall의 try 안에서 불리므로, 여기서 삼키지
|
|
52
|
-
// 않으면 렌더링 버그가 그 항목의 설치 실패로 잘못 보고된다.
|
|
53
|
-
const notify = (event) => {
|
|
54
|
-
if (!onProgress) return
|
|
55
|
-
try {
|
|
56
|
-
onProgress({ total, ...event })
|
|
57
|
-
} catch {
|
|
58
|
-
// 무시한다 — 알림 실패로 설치를 멈추거나 결과를 뒤틀면 안 된다.
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// shouldStop도 같은 이유로 감싼다. 여기서 다시 던지면 UI 술어의 버그 하나가
|
|
63
|
-
// 설치 전체를 중단시킨다 — 알림을 못 받는 것보다 더 나쁘다. 던지면 "멈추지
|
|
64
|
-
// 않음"으로 취급해 계속 진행한다.
|
|
65
|
-
const isStopRequested = () => {
|
|
66
|
-
if (!shouldStop) return false
|
|
67
|
-
try {
|
|
68
|
-
return Boolean(shouldStop())
|
|
69
|
-
} catch {
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (let index = 0; index < total; index++) {
|
|
75
|
-
const { item, action } = changes[index]
|
|
76
|
-
|
|
77
|
-
// 중단은 **항목 경계에서만** 본다. 외부 명령을 중간에 죽이면 반쯤 설치된
|
|
78
|
-
// 상태가 남는다 — 그래서 AbortSignal이 아니라 술어(predicate)다.
|
|
79
|
-
if (isStopRequested()) {
|
|
80
|
-
// skipped: true는 "시도조차 하지 않았다"는 뜻이다. ok는 성공하지 못했으니
|
|
81
|
-
// false지만 실패는 아니다 — 소비자는 실패로 보고하거나 exitCode를 세우기
|
|
82
|
-
// 전에 반드시 skipped를 먼저 확인해야 한다(!r.ok && !r.skipped).
|
|
83
|
-
results.push({ item, action, ok: false, skipped: true })
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
notify({ index, item, action, phase: 'start' })
|
|
88
|
-
const startedAt = Date.now()
|
|
89
|
-
|
|
90
|
-
// 화면이 "지금 무엇이 도는가"를 보여 줄 수 있는 유일한 경로다.
|
|
91
|
-
const exec = async (cmd, args, opts) => {
|
|
92
|
-
notify({ index, item, action, phase: 'command', command: [cmd, ...args].join(' ') })
|
|
93
|
-
return baseExec(cmd, args, opts)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
|
|
97
|
-
// 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
|
|
98
|
-
// 무엇이 바뀔지는 알 수 없는 상태였다.
|
|
99
|
-
const ctx = { root, dryRun, exec, log, t }
|
|
100
|
-
let result
|
|
101
|
-
try {
|
|
102
|
-
const fn = action === 'uninstall' ? item.uninstall : item.install
|
|
103
|
-
const r = await fn.call(item, ctx)
|
|
104
|
-
result = { item, action, ok: true, message: r?.message }
|
|
105
|
-
} catch (err) {
|
|
106
|
-
// LocalizedError는 .key를 들고 있다 — 구조체로 옮겨 담아야 표시하는
|
|
107
|
-
// 쪽에서 toText로 활성 로케일에 맞게 다시 렌더할 수 있다. 그냥
|
|
108
|
-
// err.message만 쓰면 LocalizedError.message는 항상 영어라 로케일이 섞인다.
|
|
109
|
-
result = { item, action, ok: false, message: err.key ? msg(err.key, err.params) : err.message }
|
|
110
|
-
}
|
|
111
|
-
results.push(result)
|
|
112
|
-
notify({ index, item, action, phase: 'done', ok: result.ok, ms: Date.now() - startedAt, message: result.message })
|
|
113
|
-
}
|
|
114
|
-
return results
|
|
115
|
-
}
|
|
1
|
+
import { makeExec } from './catalog.mjs'
|
|
2
|
+
import { createT, LocalizedError, msg } from './i18n/index.mjs'
|
|
3
|
+
|
|
4
|
+
export async function scan(root, items) {
|
|
5
|
+
const states = []
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
try {
|
|
8
|
+
const r = await item.detect({ root })
|
|
9
|
+
states.push({ item, status: r.status, detail: r.detail })
|
|
10
|
+
} catch (err) {
|
|
11
|
+
states.push({ item, status: 'absent', detail: msg('item.scanFailed', { message: err.message }) })
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return states
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// item.exclusive는 "이 항목들은 같은 파일 하나를 두고 다툰다"는 표시다.
|
|
18
|
+
// TUI는 고르는 순간에 배타를 지키지만(state.mjs), --set은 목록을 통째로 받는다 —
|
|
19
|
+
// 둘을 함께 주면 나중 항목이 앞선 항목을 덮어써 성공한 것처럼 끝난다.
|
|
20
|
+
// 조용히 하나를 고르는 대신 어느 쪽을 원했는지 되묻는다.
|
|
21
|
+
export function assertExclusive(items, selectedIds) {
|
|
22
|
+
const groups = new Map()
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
if (!item.exclusive || !selectedIds.has(item.id)) continue
|
|
25
|
+
groups.set(item.exclusive, [...(groups.get(item.exclusive) ?? []), item.id])
|
|
26
|
+
}
|
|
27
|
+
for (const ids of groups.values()) {
|
|
28
|
+
if (ids.length > 1) throw new LocalizedError('error.exclusiveItems', { ids: ids.join(', ') })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function planChanges(states, selectedIds) {
|
|
33
|
+
const changes = []
|
|
34
|
+
for (const { item, status } of states) {
|
|
35
|
+
const selected = selectedIds.has(item.id)
|
|
36
|
+
if (selected && status === 'absent') changes.push({ item, action: 'install' })
|
|
37
|
+
else if (selected && status === 'partial') changes.push({ item, action: 'complete' })
|
|
38
|
+
else if (!selected && status !== 'absent') changes.push({ item, action: 'uninstall' })
|
|
39
|
+
}
|
|
40
|
+
return changes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en'), onProgress = null, shouldStop = null } = {}) {
|
|
44
|
+
const baseExec = makeExec(dryRun, log)
|
|
45
|
+
const results = []
|
|
46
|
+
const total = changes.length
|
|
47
|
+
|
|
48
|
+
// 알림은 참고용이지 설치를 좌우하지 않는다. onProgress는 훗날 실제 렌더러가
|
|
49
|
+
// 될 자리라(Task 11) 그 안의 버그가 던지면, notify를 감싸지 않는 한 예외가
|
|
50
|
+
// apply()를 통째로 빠져나가 이미 디스크에 쓴 항목의 결과를 아무도 못 받는다.
|
|
51
|
+
// command 단계 알림은 install/uninstall의 try 안에서 불리므로, 여기서 삼키지
|
|
52
|
+
// 않으면 렌더링 버그가 그 항목의 설치 실패로 잘못 보고된다.
|
|
53
|
+
const notify = (event) => {
|
|
54
|
+
if (!onProgress) return
|
|
55
|
+
try {
|
|
56
|
+
onProgress({ total, ...event })
|
|
57
|
+
} catch {
|
|
58
|
+
// 무시한다 — 알림 실패로 설치를 멈추거나 결과를 뒤틀면 안 된다.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// shouldStop도 같은 이유로 감싼다. 여기서 다시 던지면 UI 술어의 버그 하나가
|
|
63
|
+
// 설치 전체를 중단시킨다 — 알림을 못 받는 것보다 더 나쁘다. 던지면 "멈추지
|
|
64
|
+
// 않음"으로 취급해 계속 진행한다.
|
|
65
|
+
const isStopRequested = () => {
|
|
66
|
+
if (!shouldStop) return false
|
|
67
|
+
try {
|
|
68
|
+
return Boolean(shouldStop())
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let index = 0; index < total; index++) {
|
|
75
|
+
const { item, action } = changes[index]
|
|
76
|
+
|
|
77
|
+
// 중단은 **항목 경계에서만** 본다. 외부 명령을 중간에 죽이면 반쯤 설치된
|
|
78
|
+
// 상태가 남는다 — 그래서 AbortSignal이 아니라 술어(predicate)다.
|
|
79
|
+
if (isStopRequested()) {
|
|
80
|
+
// skipped: true는 "시도조차 하지 않았다"는 뜻이다. ok는 성공하지 못했으니
|
|
81
|
+
// false지만 실패는 아니다 — 소비자는 실패로 보고하거나 exitCode를 세우기
|
|
82
|
+
// 전에 반드시 skipped를 먼저 확인해야 한다(!r.ok && !r.skipped).
|
|
83
|
+
results.push({ item, action, ok: false, skipped: true })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
notify({ index, item, action, phase: 'start' })
|
|
88
|
+
const startedAt = Date.now()
|
|
89
|
+
|
|
90
|
+
// 화면이 "지금 무엇이 도는가"를 보여 줄 수 있는 유일한 경로다.
|
|
91
|
+
const exec = async (cmd, args, opts) => {
|
|
92
|
+
notify({ index, item, action, phase: 'command', command: [cmd, ...args].join(' ') })
|
|
93
|
+
return baseExec(cmd, args, opts)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
|
|
97
|
+
// 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
|
|
98
|
+
// 무엇이 바뀔지는 알 수 없는 상태였다.
|
|
99
|
+
const ctx = { root, dryRun, exec, log, t }
|
|
100
|
+
let result
|
|
101
|
+
try {
|
|
102
|
+
const fn = action === 'uninstall' ? item.uninstall : item.install
|
|
103
|
+
const r = await fn.call(item, ctx)
|
|
104
|
+
result = { item, action, ok: true, message: r?.message }
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// LocalizedError는 .key를 들고 있다 — 구조체로 옮겨 담아야 표시하는
|
|
107
|
+
// 쪽에서 toText로 활성 로케일에 맞게 다시 렌더할 수 있다. 그냥
|
|
108
|
+
// err.message만 쓰면 LocalizedError.message는 항상 영어라 로케일이 섞인다.
|
|
109
|
+
result = { item, action, ok: false, message: err.key ? msg(err.key, err.params) : err.message }
|
|
110
|
+
}
|
|
111
|
+
results.push(result)
|
|
112
|
+
notify({ index, item, action, phase: 'done', ok: result.ok, ms: Date.now() - startedAt, message: result.message })
|
|
113
|
+
}
|
|
114
|
+
return results
|
|
115
|
+
}
|
package/lib/gitignore.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
-
import { repoPathStrict } from './context.mjs'
|
|
3
|
-
|
|
4
|
-
export function ensureGitignoreEntries(root, entries) {
|
|
5
|
-
// 함수의 목적 자체가 쓰기이므로 맨 앞에서 한 번 엄격 검사한다 —
|
|
6
|
-
// bootstrap의 ensureIgnore와 같은 규칙. 어휘적 검사만으로는 .gitignore가
|
|
7
|
-
// 저장소 밖을 가리키는 링크일 때를 막지 못한다.
|
|
8
|
-
const file = repoPathStrict(root, '.gitignore')
|
|
9
|
-
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
10
|
-
const lines = new Set(text.split(/\r?\n/))
|
|
11
|
-
const missing = entries.filter((e) => !lines.has(e))
|
|
12
|
-
if (missing.length === 0) return
|
|
13
|
-
// 파일의 우세 줄바꿈을 따른다 — CRLF 파일에 덧붙인 줄만 LF로 섞이지 않게.
|
|
14
|
-
// apply.mjs의 ensureJsonKeys가 JSON에 대해 지키는 규칙과 같다.
|
|
15
|
-
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
16
|
-
const sep = text.length === 0 || text.endsWith('\n') ? '' : eol
|
|
17
|
-
writeFileSync(file, text + sep + missing.join(eol) + eol)
|
|
18
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { repoPathStrict } from './context.mjs'
|
|
3
|
+
|
|
4
|
+
export function ensureGitignoreEntries(root, entries) {
|
|
5
|
+
// 함수의 목적 자체가 쓰기이므로 맨 앞에서 한 번 엄격 검사한다 —
|
|
6
|
+
// bootstrap의 ensureIgnore와 같은 규칙. 어휘적 검사만으로는 .gitignore가
|
|
7
|
+
// 저장소 밖을 가리키는 링크일 때를 막지 못한다.
|
|
8
|
+
const file = repoPathStrict(root, '.gitignore')
|
|
9
|
+
const text = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
10
|
+
const lines = new Set(text.split(/\r?\n/))
|
|
11
|
+
const missing = entries.filter((e) => !lines.has(e))
|
|
12
|
+
if (missing.length === 0) return
|
|
13
|
+
// 파일의 우세 줄바꿈을 따른다 — CRLF 파일에 덧붙인 줄만 LF로 섞이지 않게.
|
|
14
|
+
// apply.mjs의 ensureJsonKeys가 JSON에 대해 지키는 규칙과 같다.
|
|
15
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
16
|
+
const sep = text.length === 0 || text.endsWith('\n') ? '' : eol
|
|
17
|
+
writeFileSync(file, text + sep + missing.join(eol) + eol)
|
|
18
|
+
}
|