@rch4com/agent-setup 1.3.0 → 1.5.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/install.mjs +11 -1
- package/lib/bootstrap/flow.mjs +75 -8
- package/lib/catalog.mjs +29 -11
- package/lib/clis.mjs +15 -6
- package/lib/design-md/cache/awesome-design-md/LICENSE.md +38 -0
- package/lib/design-md/flow.mjs +12 -1
- package/lib/design-md/providers/awesome-design-md.mjs +7 -0
- package/lib/engine.mjs +57 -5
- package/lib/i18n/catalog/en.mjs +32 -3
- package/lib/i18n/catalog/ko.mjs +29 -3
- package/lib/items/plugin.ponytail.mjs +3 -3
- package/lib/items/skill.gsd.mjs +2 -2
- package/lib/items/skill.gstack.mjs +3 -3
- package/lib/tui/detail.mjs +118 -0
- package/lib/tui/progress.mjs +160 -0
- package/lib/tui/render.mjs +103 -20
- package/lib/tui/rows.mjs +41 -14
- package/lib/tui/run.mjs +101 -12
- package/lib/tui/state.mjs +39 -9
- package/lib/width.mjs +58 -0
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -52,6 +52,7 @@ async function openTui(root, { dryRun, skillMode, designDirs, t, localeForced })
|
|
|
52
52
|
async function runClassic(root, { dryRun, listOnly, setArg, t }) {
|
|
53
53
|
const { loadItems } = await withDeps(() => import('./lib/catalog.mjs'), t)
|
|
54
54
|
const { scan, planChanges, apply } = await withDeps(() => import('./lib/engine.mjs'), t)
|
|
55
|
+
const { plainLine } = await withDeps(() => import('./lib/tui/progress.mjs'), t)
|
|
55
56
|
const items = await loadItems()
|
|
56
57
|
const states = await scan(root, items)
|
|
57
58
|
const statusWidth = labelWidth(t, STATUS_KEYS)
|
|
@@ -71,7 +72,16 @@ async function runClassic(root, { dryRun, listOnly, setArg, t }) {
|
|
|
71
72
|
const changes = planChanges(states, selectedIds)
|
|
72
73
|
if (changes.length === 0) { console.log(t('apply.noChanges')); return }
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// 비대화형 경로다 — 바를 그리지 않고 평문 한 줄씩 흘린다.
|
|
76
|
+
// ANSI 제어문자로 CI 로그를 더럽히지 않기 위해서다.
|
|
77
|
+
const results = await apply(root, changes, {
|
|
78
|
+
dryRun,
|
|
79
|
+
t,
|
|
80
|
+
onProgress: (event) => {
|
|
81
|
+
const line = plainLine(event, t)
|
|
82
|
+
if (line) console.log(line)
|
|
83
|
+
},
|
|
84
|
+
})
|
|
75
85
|
for (const r of results) {
|
|
76
86
|
console.log(`${r.ok ? '✔' : '✖'} ${t(`change.${r.action}`)} ${r.item.label}${r.message ? ` — ${toText(t, r.message)}` : ''}`)
|
|
77
87
|
}
|
package/lib/bootstrap/flow.mjs
CHANGED
|
@@ -6,11 +6,26 @@ import {
|
|
|
6
6
|
} from './apply.mjs'
|
|
7
7
|
import { RECORD_REL, collectManaged, emptyRecord, readRecord, writeRecord } from './record.mjs'
|
|
8
8
|
import { createT, LocalizedError, toText } from '../i18n/index.mjs'
|
|
9
|
+
import { width } from '../width.mjs'
|
|
9
10
|
|
|
10
11
|
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
11
12
|
|
|
13
|
+
// 오른쪽 정렬. width.mjs의 pad()는 오른쪽에 공백을 채워 왼쪽 정렬만 하므로
|
|
14
|
+
// 숫자에는 못 쓴다 — 자릿수가 9에서 10으로 늘 때 열이 흔들리지 않으려면
|
|
15
|
+
// 왼쪽에 채워야 한다. 숫자만 다루니 실제로는 문자 수와 같지만, 이 저장소의
|
|
16
|
+
// 다른 폭 계산과 방식을 맞추려고 width()로 잰다.
|
|
17
|
+
function alignRight(text, w) {
|
|
18
|
+
return ' '.repeat(Math.max(0, w - width(text))) + text
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// "[ 3/39]" 꼴의 단계 번호. total의 자릿수에 맞춰 n을 오른쪽 정렬한다.
|
|
22
|
+
function counterTag(n, total) {
|
|
23
|
+
const totalText = String(total)
|
|
24
|
+
return `[${alignRight(String(n), width(totalText))}/${totalText}]`
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
export function runBootstrap(root, opts = {}) {
|
|
13
|
-
const { dryRun = false, skillMode = 'auto', adopt = false, log = console.log, manifest = MANIFEST, t = createT('en') } = opts
|
|
28
|
+
const { dryRun = false, skillMode = 'auto', adopt = false, log = console.log, manifest = MANIFEST, onProgress = null, t = createT('en') } = opts
|
|
14
29
|
|
|
15
30
|
if (!SKILL_MODES.includes(skillMode)) {
|
|
16
31
|
throw new LocalizedError('error.badSkillModeRuntime', { list: SKILL_MODES.join(', '), value: skillMode })
|
|
@@ -22,23 +37,71 @@ export function runBootstrap(root, opts = {}) {
|
|
|
22
37
|
say(t('log.repoRoot', { path: root }))
|
|
23
38
|
say(t('log.noGlobalWrites'))
|
|
24
39
|
|
|
40
|
+
// 총 단계 수. 동기 파일 작업이라 단계 바만 돌지만, 무엇이 남았는지는
|
|
41
|
+
// 알려 줄 수 있다. adopt는 파일을 만들지 않으므로 기록 쓰기 한 단계뿐이다.
|
|
42
|
+
// 예측일 뿐이다 — ensureIgnore는 부정 항목이 기존 .gitignore에 가려지면
|
|
43
|
+
// manifest.ignore.length보다 많은 결과(경고 포함)를 돌려줄 수 있다.
|
|
44
|
+
let total = adopt
|
|
45
|
+
? 1
|
|
46
|
+
: manifest.dirs.length + manifest.files.length + (manifest.settings ?? []).length
|
|
47
|
+
+ manifest.blocks.length + manifest.adapters.length + manifest.ignore.length + 1
|
|
48
|
+
let done = 0
|
|
49
|
+
const step = (results) => {
|
|
50
|
+
const list = Array.isArray(results) ? results : [results]
|
|
51
|
+
for (const r of list) {
|
|
52
|
+
done++
|
|
53
|
+
// 실제 진행이 예측을 앞지르면 천장을 함께 올린다. total을 고정해 두면
|
|
54
|
+
// done > total인 알림이 나가 진행률이 100%를 넘고 바가 자기 너비를
|
|
55
|
+
// 넘겨 그려진다 — done을 total에 맞춰 죽이면 화면은 멈춘 것처럼 보이는데
|
|
56
|
+
// 남은 단계가 조용히 계속 도는, 더 나쁜 오류가 된다.
|
|
57
|
+
if (done > total) total = done
|
|
58
|
+
if (onProgress) onProgress({ done, total, path: r?.path ?? null })
|
|
59
|
+
}
|
|
60
|
+
return list
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 단계 안에서 나온 로그는 모아 뒀다가 step()이 done/total을 확정한 뒤에
|
|
64
|
+
// 번호를 붙여 낸다. ensureDirs는 이미 있는 항목을 건너뛸 때 아예 로그를
|
|
65
|
+
// 내지 않고, ensureIgnore는 여러 항목을 한 줄로 묶어 보고한다 — "몇 번째
|
|
66
|
+
// 줄이 찍혔는가"와 "몇 번째 항목을 처리했는가"가 그래서 어긋난다. 번호는
|
|
67
|
+
// 항목 순서가 아니라 처리 위치(done)를 보여 주는 것이 목적이므로, 이
|
|
68
|
+
// 구간이 시작한 위치(before)부터 실제로 찍힌 줄 수만큼만 순서대로 매긴다.
|
|
69
|
+
// 못 채운 자리는 건너뛰지만, 다음 구간은 항상 정확한 위치에서 이어져
|
|
70
|
+
// 마지막 줄은 반드시 total 가까이에서 끝난다.
|
|
71
|
+
//
|
|
72
|
+
// onProgress는 그대로 step()이 몬다 — 기계가 읽는 진행 이벤트와 사람이
|
|
73
|
+
// 읽는 로그 줄은 타이밍이 다르다(ensureX 호출이 끝나야 결과 배열이
|
|
74
|
+
// 나오고, 그제서야 done/total이 확정된다). 두 소비자를 하나의 흐름으로
|
|
75
|
+
// 우겨넣는 대신, 같은 done/total 계산을 공유하는 별도 경로로 뒀다.
|
|
76
|
+
const runStep = (run) => {
|
|
77
|
+
const buffer = []
|
|
78
|
+
const before = done
|
|
79
|
+
const list = step(run({ ...ctx, log: (message) => buffer.push(message) }))
|
|
80
|
+
let n = before
|
|
81
|
+
for (const message of buffer) {
|
|
82
|
+
n++
|
|
83
|
+
say(`${counterTag(n, total)} ${message}`)
|
|
84
|
+
}
|
|
85
|
+
return list
|
|
86
|
+
}
|
|
87
|
+
|
|
25
88
|
// --adopt는 이미 있는 저장소를 기록 체계로 끌어오는 용도라 파일을 만들지
|
|
26
89
|
// 않는다. 설치기를 복사해 쓰던 저장소가 여기로 들어온다.
|
|
27
90
|
const results = adopt ? [] : [
|
|
28
|
-
...ensureDirs(root, manifest.dirs,
|
|
29
|
-
...ensureFiles(root, manifest.files,
|
|
30
|
-
...ensureJsonKeys(root, manifest.settings ?? [],
|
|
31
|
-
...ensureBlocks(root, manifest.blocks,
|
|
91
|
+
...runStep((c) => ensureDirs(root, manifest.dirs, c)),
|
|
92
|
+
...runStep((c) => ensureFiles(root, manifest.files, c)),
|
|
93
|
+
...runStep((c) => ensureJsonKeys(root, manifest.settings ?? [], c)),
|
|
94
|
+
...runStep((c) => ensureBlocks(root, manifest.blocks, c)),
|
|
32
95
|
]
|
|
33
96
|
|
|
34
97
|
if (!adopt) {
|
|
35
98
|
// 어댑터는 항목별로 실패를 격리한다 — 하나가 실패해도 나머지를 계속한다.
|
|
36
99
|
// update.mjs도 같은 격리를 써야 하므로 apply.mjs가 단일 출처다.
|
|
37
100
|
for (const entry of manifest.adapters) {
|
|
38
|
-
results.push(configureAdapterSafe(root, entry, { ...
|
|
101
|
+
results.push(...runStep((c) => configureAdapterSafe(root, entry, { ...c, skillMode })))
|
|
39
102
|
}
|
|
40
103
|
|
|
41
|
-
results.push(...ensureIgnore(root, manifest.ignore,
|
|
104
|
+
results.push(...runStep((c) => ensureIgnore(root, manifest.ignore, c)))
|
|
42
105
|
}
|
|
43
106
|
|
|
44
107
|
// 기존 기록의 items·design은 보존한다 — 부트스트랩은 배선만 다루므로
|
|
@@ -53,7 +116,11 @@ export function runBootstrap(root, opts = {}) {
|
|
|
53
116
|
design: previous?.design ?? [],
|
|
54
117
|
managed: collectManaged(root, manifest),
|
|
55
118
|
}
|
|
56
|
-
results.push(writeRecord(root, record,
|
|
119
|
+
results.push(...runStep((c) => writeRecord(root, record, c)))
|
|
120
|
+
|
|
121
|
+
// 실제 결과 수가 예측과 다르면 마지막 알림으로 100%를 맞춘다.
|
|
122
|
+
// 진행 바가 87%에서 끝나면 사용자는 무엇이 실패했다고 읽는다.
|
|
123
|
+
if (onProgress && done !== total) onProgress({ done: total, total, path: null })
|
|
57
124
|
|
|
58
125
|
const failed = results.filter((r) => !r.ok)
|
|
59
126
|
log('')
|
package/lib/catalog.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
5
6
|
import { CLIS, CLI_IDS } from './clis.mjs'
|
|
6
7
|
import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
|
|
7
8
|
import { repoPath, repoPathStrict } from './context.mjs'
|
|
@@ -58,8 +59,13 @@ export function shellQuote(text, platform = process.platform) {
|
|
|
58
59
|
return `'${s.replace(/'/g, "'\\''")}'`
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const execFileAsync = promisify(execFile)
|
|
63
|
+
|
|
64
|
+
// 비동기다. 동기 실행은 이벤트 루프를 통째로 막아, npx가 도는 수십 초 동안
|
|
65
|
+
// 진행 화면을 한 번도 다시 그릴 수 없었다. 반환 형태({ ok, output })는
|
|
66
|
+
// 그대로라 호출부는 await만 더하면 된다.
|
|
61
67
|
export function makeExec(dryRun, log = console.log) {
|
|
62
|
-
return (cmd, args, opts = {}) => {
|
|
68
|
+
return async (cmd, args, opts = {}) => {
|
|
63
69
|
if (dryRun) {
|
|
64
70
|
log(` [dry-run] ${cmd} ${args.join(' ')}`)
|
|
65
71
|
return { ok: true, output: '' }
|
|
@@ -70,15 +76,27 @@ export function makeExec(dryRun, log = console.log) {
|
|
|
70
76
|
// 어차피 우리가 직접 quote하므로, 완성된 한 줄 명령을 넘기고 인자 배열은 비운다.
|
|
71
77
|
const [file, fileArgs] = shell ? [[cmd, ...args].map((s) => shellQuote(s)).join(' '), []] : [cmd, args]
|
|
72
78
|
try {
|
|
73
|
-
|
|
79
|
+
// execFile은 stdio를 강제로 파이프로 고정하고 옵션으로 넘겨도 무시한다 —
|
|
80
|
+
// 그래서 stdout·stderr는 버퍼링이 공짜로 따라온다. 문제는 stdin이다:
|
|
81
|
+
// 파이프가 열린 채로 자식에게 넘어가므로, stdin을 읽는 명령(자격 증명·
|
|
82
|
+
// 확인 프롬프트 등)은 EOF를 못 받아 영원히 멈춘다. 옛 execFileSync는
|
|
83
|
+
// stdio: ['ignore', ...]로 자식의 stdin을 즉시 닫아 뒀다 — 그 동작을
|
|
84
|
+
// 복원하려면 스폰 직후 우리가 직접 stdin을 닫아야 한다.
|
|
85
|
+
const p = execFileAsync(file, fileArgs, {
|
|
74
86
|
encoding: 'utf8',
|
|
75
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
87
|
...opts,
|
|
77
88
|
shell,
|
|
78
89
|
})
|
|
79
|
-
|
|
90
|
+
// 옵션 조합에 따라 stdin이 없을 수 있다 — 없으면 조용히 넘어간다.
|
|
91
|
+
p.child.stdin?.end()
|
|
92
|
+
const { stdout } = await p
|
|
93
|
+
return { ok: true, output: stdout }
|
|
80
94
|
} catch (err) {
|
|
81
|
-
|
|
95
|
+
// spawn 자체가 실패하면(ENOENT 등) err.stderr가 undefined가 아니라
|
|
96
|
+
// 빈 문자열로 온다 — ??는 빈 문자열을 "값 있음"으로 보고 통과시켜
|
|
97
|
+
// err.message(바이너리 이름이 담긴 진단 텍스트)를 삼켜 버린다. ||로
|
|
98
|
+
// 빈 문자열도 폴백하게 한다.
|
|
99
|
+
return { ok: false, output: String(err.stderr || err.message) }
|
|
82
100
|
}
|
|
83
101
|
}
|
|
84
102
|
}
|
|
@@ -130,8 +148,8 @@ export function definePlugin({ id, label, installId, detectIds, marketplace, not
|
|
|
130
148
|
},
|
|
131
149
|
async install(ctx) {
|
|
132
150
|
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 })
|
|
151
|
+
if (marketplace) await exec('claude', ['plugin', 'marketplace', 'add', marketplace.repo], { cwd: root })
|
|
152
|
+
const r = await exec('claude', ['plugin', 'install', installId, '--scope', 'project'], { cwd: root })
|
|
135
153
|
if (!r.ok) {
|
|
136
154
|
if (!dryRun) enablePlugin(root, installId, marketplace)
|
|
137
155
|
return { fallback: true, message: msg('item.plugin.deferred') }
|
|
@@ -139,7 +157,7 @@ export function definePlugin({ id, label, installId, detectIds, marketplace, not
|
|
|
139
157
|
},
|
|
140
158
|
async uninstall(ctx) {
|
|
141
159
|
const { root, dryRun, exec } = ctx
|
|
142
|
-
const r = exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
|
|
160
|
+
const r = await exec('claude', ['plugin', 'uninstall', installId], { cwd: root })
|
|
143
161
|
if (!r.ok && !dryRun) disablePlugin(root, detectIds)
|
|
144
162
|
},
|
|
145
163
|
}
|
|
@@ -197,14 +215,14 @@ export function defineRegistrySkill({ id, label, source, skill, note, group = nu
|
|
|
197
215
|
return { status: findSkillDir(root, skill) ? 'installed' : 'absent' }
|
|
198
216
|
},
|
|
199
217
|
async install({ root, exec }) {
|
|
200
|
-
const r = exec('npx', ['-y', 'skills@latest', 'add', source, '--skill', skill, '--agent', 'universal', '--yes', '--copy'], { cwd: root })
|
|
218
|
+
const r = await exec('npx', ['-y', 'skills@latest', 'add', source, '--skill', skill, '--agent', 'universal', '--yes', '--copy'], { cwd: root })
|
|
201
219
|
if (!r.ok) throw new LocalizedError('error.registrySkillInstall', { skill, output: r.output })
|
|
202
220
|
},
|
|
203
221
|
async uninstall({ root, dryRun, exec }) {
|
|
204
222
|
// 레지스트리의 remove는 --agent universal에서 "Done!"을 찍고도 디렉터리를
|
|
205
223
|
// 남긴다(실측). 그래서 성공/실패와 무관하게 남은 디렉터리를 우리가 지운다 —
|
|
206
224
|
// 남겨 두면 detect가 계속 installed로 읽어 제거가 안 된 채 성공으로 보인다.
|
|
207
|
-
exec('npx', ['-y', 'skills@latest', 'remove', skill, '--agent', 'universal', '--yes'], { cwd: root })
|
|
225
|
+
await exec('npx', ['-y', 'skills@latest', 'remove', skill, '--agent', 'universal', '--yes'], { cwd: root })
|
|
208
226
|
if (dryRun) return
|
|
209
227
|
const dir = findSkillDir(root, skill)
|
|
210
228
|
if (dir) rmSync(repoPathStrict(root, `${SHARED_SKILLS}/${dir}`), { recursive: true, force: true })
|
package/lib/clis.mjs
CHANGED
|
@@ -8,6 +8,9 @@ import { hasSection, appendSection, removeSection } from './tomlfile.mjs'
|
|
|
8
8
|
// Junction일 때 MCP 등록 한 번으로 글로벌 설정이 생성·수정되는 것을 막는다.
|
|
9
9
|
function jsonAdapter(relFile, topKey, toEntry) {
|
|
10
10
|
return {
|
|
11
|
+
// 경로를 밖으로 낸다 — 상세 패널이 "이 항목이 어디에 쓰이나"를 보여 준다.
|
|
12
|
+
// 어댑터가 실제로 쓰는 값과 같은 변수라 두 곳이 갈릴 수 없다.
|
|
13
|
+
file: relFile,
|
|
11
14
|
has(root, name) {
|
|
12
15
|
const data = readJson(repoPath(root, relFile))
|
|
13
16
|
return getIn(data, [topKey, name]) !== undefined
|
|
@@ -29,6 +32,16 @@ function tomlLines(server) {
|
|
|
29
32
|
]
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
// codex·grok은 TOML을 쓴다. jsonAdapter와 같은 이유로 경로를 한 번만 적는다.
|
|
36
|
+
function tomlAdapter(file) {
|
|
37
|
+
return {
|
|
38
|
+
file,
|
|
39
|
+
has: (root, name) => hasSection(repoPath(root, file), name),
|
|
40
|
+
add: (root, name, s) => appendSection(repoPathStrict(root, file), name, tomlLines(s)),
|
|
41
|
+
remove: (root, name) => removeSection(repoPathStrict(root, file), name),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export const CLIS = {
|
|
33
46
|
claude: {
|
|
34
47
|
label: 'Claude Code',
|
|
@@ -39,9 +52,7 @@ export const CLIS = {
|
|
|
39
52
|
},
|
|
40
53
|
codex: {
|
|
41
54
|
label: 'Codex',
|
|
42
|
-
|
|
43
|
-
add: (root, name, s) => appendSection(repoPathStrict(root, '.codex/config.toml'), name, tomlLines(s)),
|
|
44
|
-
remove: (root, name) => removeSection(repoPathStrict(root, '.codex/config.toml'), name),
|
|
55
|
+
...tomlAdapter('.codex/config.toml'),
|
|
45
56
|
},
|
|
46
57
|
gemini: {
|
|
47
58
|
label: 'Gemini CLI',
|
|
@@ -76,9 +87,7 @@ export const CLIS = {
|
|
|
76
87
|
},
|
|
77
88
|
grok: {
|
|
78
89
|
label: 'Grok Build',
|
|
79
|
-
|
|
80
|
-
add: (root, name, s) => appendSection(repoPathStrict(root, '.grok/config.toml'), name, tomlLines(s)),
|
|
81
|
-
remove: (root, name) => removeSection(repoPathStrict(root, '.grok/config.toml'), name),
|
|
90
|
+
...tomlAdapter('.grok/config.toml'),
|
|
82
91
|
},
|
|
83
92
|
copilot: {
|
|
84
93
|
label: 'GitHub Copilot CLI',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Third-party notice — awesome-design-md
|
|
2
|
+
|
|
3
|
+
이 디렉터리의 `DESIGN.md` 파일들은 아래 저장소에서 그대로 가져온 사본입니다.
|
|
4
|
+
오프라인 설치를 위해 동봉하며, 원문을 고치지 않습니다.
|
|
5
|
+
|
|
6
|
+
- Source: https://github.com/VoltAgent/awesome-design-md
|
|
7
|
+
- License: MIT
|
|
8
|
+
|
|
9
|
+
MIT는 재배포를 허용하되 저작권 고지와 허가 문구를 사본에 함께 담을 것을
|
|
10
|
+
요구합니다. 아래가 상류 `LICENSE`의 원문입니다.
|
|
11
|
+
|
|
12
|
+
파일 이름이 `LICENSE`가 아니라 `LICENSE.md`인 이유는 발행물 검사가 최상위
|
|
13
|
+
`LICENSE` 하나만 확장자 없는 파일로 허용하기 때문입니다 — 이름이 아니라
|
|
14
|
+
담긴 내용이 고지의 요건을 채웁니다.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
MIT License
|
|
19
|
+
|
|
20
|
+
Copyright (c) 2026 VoltAgent
|
|
21
|
+
|
|
22
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
23
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
24
|
+
in the Software without restriction, including without limitation the rights
|
|
25
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
26
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
27
|
+
furnished to do so, subject to the following conditions:
|
|
28
|
+
|
|
29
|
+
The above copyright notice and this permission notice shall be included in all
|
|
30
|
+
copies or substantial portions of the Software.
|
|
31
|
+
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
35
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
36
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
37
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
38
|
+
SOFTWARE.
|
package/lib/design-md/flow.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { discoverSources, extraDirsFromEnv } from './scan.mjs'
|
|
|
5
5
|
import { makeOpener, openPreview } from './open.mjs'
|
|
6
6
|
import { createT, toText } from '../i18n/index.mjs'
|
|
7
7
|
import { labelWidth, pad } from '../width.mjs'
|
|
8
|
+
import { plainLine } from '../tui/progress.mjs'
|
|
8
9
|
|
|
9
10
|
// design 항목의 detect는 installed/absent만 돌려준다(catalog.mjs) — partial은
|
|
10
11
|
// 열을 넓히기만 하고 실제로 나오지 않는다.
|
|
@@ -62,7 +63,17 @@ function openPreviews(items, tokensStr, opener, log, t) {
|
|
|
62
63
|
async function applyVisible(root, visibleStates, selectedIds, { dryRun, log, t }) {
|
|
63
64
|
const changes = planChanges(visibleStates, selectedIds)
|
|
64
65
|
if (changes.length === 0) { log(t('apply.noChanges')); return [] }
|
|
65
|
-
|
|
66
|
+
// design --set도 비대화형 경로다 — runClassic의 --set과 대칭으로, 바를
|
|
67
|
+
// 그리지 않고 평문 한 줄씩 흘린다. ANSI 제어문자로 로그를 더럽히지 않는다.
|
|
68
|
+
const results = await apply(root, changes, {
|
|
69
|
+
dryRun,
|
|
70
|
+
log,
|
|
71
|
+
t,
|
|
72
|
+
onProgress: (event) => {
|
|
73
|
+
const line = plainLine(event, t)
|
|
74
|
+
if (line) log(line)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
66
77
|
report(results, log, t)
|
|
67
78
|
return results
|
|
68
79
|
}
|
|
@@ -14,6 +14,9 @@ const REPO = 'VoltAgent/awesome-design-md'
|
|
|
14
14
|
const BRANCH = 'main'
|
|
15
15
|
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/design-md`
|
|
16
16
|
const README_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/README.md`
|
|
17
|
+
// 번들에 함께 담는 상류 고지의 출처. MIT는 사본에 저작권 고지와 허가 문구를
|
|
18
|
+
// 담을 것을 요구하므로, 파일을 내려받아 재배포하는 이상 이 URL이 필요하다.
|
|
19
|
+
const LICENSE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/LICENSE`
|
|
17
20
|
const TREE_URL = `https://api.github.com/repos/${REPO}/git/trees/${BRANCH}?recursive=1`
|
|
18
21
|
// scan.mjs의 BUNDLE_CATEGORY와 같은 값이어야 한다 — 둘 다 카테고리 못 얻은
|
|
19
22
|
// 항목이 모이는 자리라, 갈라지면 같은 그룹이 헤더 두 개로 쪼개진다.
|
|
@@ -61,6 +64,10 @@ export const awesomeDesignMd = {
|
|
|
61
64
|
id: 'awesome-design-md',
|
|
62
65
|
label: 'awesome-design-md (VoltAgent)',
|
|
63
66
|
files: ['DESIGN.md'],
|
|
67
|
+
licenseUrl: LICENSE_URL,
|
|
68
|
+
// 번들과 함께 발행되는 고지 파일. 이름이 LICENSE가 아닌 이유는 발행물 검사가
|
|
69
|
+
// 최상위 LICENSE 하나만 확장자 없는 파일로 허용하기 때문이다.
|
|
70
|
+
noticeFile: 'LICENSE.md',
|
|
64
71
|
|
|
65
72
|
fileUrl(name, file = 'DESIGN.md') {
|
|
66
73
|
return `${RAW_BASE}/${encodeURIComponent(name)}/${file}`
|
package/lib/engine.mjs
CHANGED
|
@@ -25,24 +25,76 @@ export function planChanges(states, selectedIds) {
|
|
|
25
25
|
return changes
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en') } = {}) {
|
|
29
|
-
const
|
|
28
|
+
export async function apply(root, changes, { dryRun = false, log = console.log, t = createT('en'), onProgress = null, shouldStop = null } = {}) {
|
|
29
|
+
const baseExec = makeExec(dryRun, log)
|
|
30
30
|
const results = []
|
|
31
|
-
|
|
31
|
+
const total = changes.length
|
|
32
|
+
|
|
33
|
+
// 알림은 참고용이지 설치를 좌우하지 않는다. onProgress는 훗날 실제 렌더러가
|
|
34
|
+
// 될 자리라(Task 11) 그 안의 버그가 던지면, notify를 감싸지 않는 한 예외가
|
|
35
|
+
// apply()를 통째로 빠져나가 이미 디스크에 쓴 항목의 결과를 아무도 못 받는다.
|
|
36
|
+
// command 단계 알림은 install/uninstall의 try 안에서 불리므로, 여기서 삼키지
|
|
37
|
+
// 않으면 렌더링 버그가 그 항목의 설치 실패로 잘못 보고된다.
|
|
38
|
+
const notify = (event) => {
|
|
39
|
+
if (!onProgress) return
|
|
40
|
+
try {
|
|
41
|
+
onProgress({ total, ...event })
|
|
42
|
+
} catch {
|
|
43
|
+
// 무시한다 — 알림 실패로 설치를 멈추거나 결과를 뒤틀면 안 된다.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// shouldStop도 같은 이유로 감싼다. 여기서 다시 던지면 UI 술어의 버그 하나가
|
|
48
|
+
// 설치 전체를 중단시킨다 — 알림을 못 받는 것보다 더 나쁘다. 던지면 "멈추지
|
|
49
|
+
// 않음"으로 취급해 계속 진행한다.
|
|
50
|
+
const isStopRequested = () => {
|
|
51
|
+
if (!shouldStop) return false
|
|
52
|
+
try {
|
|
53
|
+
return Boolean(shouldStop())
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < total; index++) {
|
|
60
|
+
const { item, action } = changes[index]
|
|
61
|
+
|
|
62
|
+
// 중단은 **항목 경계에서만** 본다. 외부 명령을 중간에 죽이면 반쯤 설치된
|
|
63
|
+
// 상태가 남는다 — 그래서 AbortSignal이 아니라 술어(predicate)다.
|
|
64
|
+
if (isStopRequested()) {
|
|
65
|
+
// skipped: true는 "시도조차 하지 않았다"는 뜻이다. ok는 성공하지 못했으니
|
|
66
|
+
// false지만 실패는 아니다 — 소비자는 실패로 보고하거나 exitCode를 세우기
|
|
67
|
+
// 전에 반드시 skipped를 먼저 확인해야 한다(!r.ok && !r.skipped).
|
|
68
|
+
results.push({ item, action, ok: false, skipped: true })
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
notify({ index, item, action, phase: 'start' })
|
|
73
|
+
const startedAt = Date.now()
|
|
74
|
+
|
|
75
|
+
// 화면이 "지금 무엇이 도는가"를 보여 줄 수 있는 유일한 경로다.
|
|
76
|
+
const exec = async (cmd, args, opts) => {
|
|
77
|
+
notify({ index, item, action, phase: 'command', command: [cmd, ...args].join(' ') })
|
|
78
|
+
return baseExec(cmd, args, opts)
|
|
79
|
+
}
|
|
80
|
+
|
|
32
81
|
// log까지 넘긴다 — 파일을 직접 쓰는 항목(MCP·design.md)은 exec를 거치지
|
|
33
82
|
// 않아 dry-run에서 아무것도 보고하지 못했다. 결과줄은 ✔ 설치라고 찍히는데
|
|
34
83
|
// 무엇이 바뀔지는 알 수 없는 상태였다.
|
|
35
84
|
const ctx = { root, dryRun, exec, log, t }
|
|
85
|
+
let result
|
|
36
86
|
try {
|
|
37
87
|
const fn = action === 'uninstall' ? item.uninstall : item.install
|
|
38
88
|
const r = await fn.call(item, ctx)
|
|
39
|
-
|
|
89
|
+
result = { item, action, ok: true, message: r?.message }
|
|
40
90
|
} catch (err) {
|
|
41
91
|
// LocalizedError는 .key를 들고 있다 — 구조체로 옮겨 담아야 표시하는
|
|
42
92
|
// 쪽에서 toText로 활성 로케일에 맞게 다시 렌더할 수 있다. 그냥
|
|
43
93
|
// err.message만 쓰면 LocalizedError.message는 항상 영어라 로케일이 섞인다.
|
|
44
|
-
|
|
94
|
+
result = { item, action, ok: false, message: err.key ? msg(err.key, err.params) : err.message }
|
|
45
95
|
}
|
|
96
|
+
results.push(result)
|
|
97
|
+
notify({ index, item, action, phase: 'done', ok: result.ok, ms: Date.now() - startedAt, message: result.message })
|
|
46
98
|
}
|
|
47
99
|
return results
|
|
48
100
|
}
|
package/lib/i18n/catalog/en.mjs
CHANGED
|
@@ -280,8 +280,8 @@ export default {
|
|
|
280
280
|
'tui.search.placeholder': 'type to search · ↓ for the list',
|
|
281
281
|
'tui.empty.filtered': ' Nothing matches in this tab. Press Tab to try another.',
|
|
282
282
|
'tui.empty.none': ' Nothing here.',
|
|
283
|
-
'tui.hint.search': 'type to search (spaces included) ↓ to list Tab
|
|
284
|
-
'tui.hint.list': 'Space select ↑↓ move (↑ at top = search) Tab
|
|
283
|
+
'tui.hint.search': 'type to search (spaces included) ↓ to list Tab tab Esc clear Ctrl+F CLI Ctrl+D detail',
|
|
284
|
+
'tui.hint.list': 'Space select ↑↓ move (↑ at top = search) Tab tab Enter run/submit Ctrl+A all Ctrl+O preview Ctrl+F CLI Ctrl+D detail',
|
|
285
285
|
'tui.review.title': 'Review — {count} change(s)',
|
|
286
286
|
'tui.review.more': ' …and {count} more',
|
|
287
287
|
'tui.review.hint': 'Enter apply Esc cancel',
|
|
@@ -293,21 +293,50 @@ export default {
|
|
|
293
293
|
'tui.toggledAll': 'Selected every visible item in the {tab} tab.',
|
|
294
294
|
'tui.toggledNone': 'Cleared every visible item in the {tab} tab.',
|
|
295
295
|
'tui.nonInteractive': 'The interactive screen only opens in a terminal. --list and --set work too.',
|
|
296
|
-
'tui.applyHeader': '{count} change(s) to apply{suffix}:',
|
|
297
296
|
'tui.pressAnyKey': 'Press any key to continue…',
|
|
298
297
|
'tui.confirmSuffix': ' [y/N] ',
|
|
299
298
|
|
|
299
|
+
// 진행 화면(tui/progress.mjs) 문구. Task 9의 이벤트(start·command·done)를
|
|
300
|
+
// 사람이 읽는 줄로 바꾼다. plain*은 비TTY(CI·파이프)용 한 줄 출력이다.
|
|
301
|
+
'progress.title': 'Applying — {count} change(s){suffix}',
|
|
302
|
+
'progress.counter': '{done}/{total} {percent}%',
|
|
303
|
+
'progress.elapsed': '{seconds}s',
|
|
304
|
+
'progress.elapsedMin': '{minutes}m{seconds}s',
|
|
305
|
+
'progress.running': '{seconds}s elapsed',
|
|
306
|
+
'progress.more': ' …and {count} more',
|
|
307
|
+
'progress.abortHint': 'Ctrl+C stops after the current item',
|
|
308
|
+
'progress.aborted': 'Stopped — {count} item(s) skipped',
|
|
309
|
+
'progress.skipped': 'skipped',
|
|
310
|
+
'progress.done': 'Done — {ok} succeeded, {failed} failed{skippedSuffix}',
|
|
311
|
+
'progress.plain': '[{index}/{total}] {action} {label}',
|
|
312
|
+
'progress.plainDone': ' {mark} {seconds}s',
|
|
313
|
+
|
|
300
314
|
// 언어 행(action.language) 전용. 저장 성공·dry-run·실패·강제 로케일 네 갈래를 덮는다.
|
|
301
315
|
'tui.lang.saved': 'Saved to {path}',
|
|
302
316
|
'tui.lang.dryRun': 'dry-run — applied to this session only',
|
|
303
317
|
'tui.lang.saveFailed': 'Could not save the language: {message}',
|
|
304
318
|
'tui.lang.overridden': '--lang / AGENT_SETUP_LANG still wins on the next run.',
|
|
305
319
|
|
|
320
|
+
// CLI 필터 표시. 검색줄 오른쪽 끝에 붙는다(Task 7) — filterSegment가 쓴다.
|
|
321
|
+
// 필터가 없을 때는 아무것도 내지 않는다(잡음 방지) — "전체" 라벨을 위한
|
|
322
|
+
// 키를 따로 두지 않는다.
|
|
323
|
+
'tui.filter.prefix': 'CLI › ',
|
|
324
|
+
'tui.filter.position': '{current}/{total}',
|
|
325
|
+
'tui.filter.empty': ' Nothing in this tab is wired for {cli}.',
|
|
326
|
+
|
|
306
327
|
'item.location.user': 'installs to: user global',
|
|
307
328
|
// 커버리지는 전부 지원할 때도 찍는다 — 표시가 없는 것과 "10/10"은 뜻이 다르다.
|
|
308
329
|
'item.cliCoverage': 'CLI {covered}/{total}',
|
|
309
330
|
'item.unsupportedList': 'not wired for {count}: {groups}',
|
|
310
331
|
'item.unsupportedGroup': '{clis} — {why}',
|
|
332
|
+
'detail.wired': 'wired',
|
|
333
|
+
'detail.unwired': 'unwired',
|
|
334
|
+
'detail.provider': 'source',
|
|
335
|
+
'detail.preview': 'preview',
|
|
336
|
+
'detail.scope.project': 'repo scope',
|
|
337
|
+
'detail.scope.user': 'user global',
|
|
338
|
+
'detail.more': ' …and {count} more line(s) — Ctrl+D to expand',
|
|
339
|
+
'detail.empty': ' Move the cursor onto an item to see its details.',
|
|
311
340
|
|
|
312
341
|
// 상주 언어 행 — 작업 탭 맨 위. hint는 현재 로케일의 자기 이름을 담는다.
|
|
313
342
|
'action.language.label': 'Language',
|
package/lib/i18n/catalog/ko.mjs
CHANGED
|
@@ -271,8 +271,8 @@ export default {
|
|
|
271
271
|
'tui.search.placeholder': '타이핑하면 검색 · ↓ 로 목록',
|
|
272
272
|
'tui.empty.filtered': ' 이 탭에는 일치하는 항목이 없습니다. Tab으로 다른 탭을 보세요.',
|
|
273
273
|
'tui.empty.none': ' 항목이 없습니다.',
|
|
274
|
-
'tui.hint.search': '입력=검색어
|
|
275
|
-
'tui.hint.list': 'Space 선택 ↑↓ 이동
|
|
274
|
+
'tui.hint.search': '입력=검색어 ↓ 목록으로 Tab 탭이동 Esc 검색해제 Ctrl+F CLI Ctrl+D 상세',
|
|
275
|
+
'tui.hint.list': 'Space 선택 ↑↓ 이동 Tab 탭 Enter 실행/제출 Ctrl+A 전체 Ctrl+O 미리보기 Ctrl+F CLI Ctrl+D 상세',
|
|
276
276
|
'tui.review.title': '제출 검토 — 변경 {count}건',
|
|
277
277
|
'tui.review.more': ' …외 {count}건',
|
|
278
278
|
'tui.review.hint': 'Enter 적용 Esc 취소',
|
|
@@ -284,21 +284,47 @@ export default {
|
|
|
284
284
|
'tui.toggledAll': '{tab} 탭의 보이는 항목을 모두 선택했습니다.',
|
|
285
285
|
'tui.toggledNone': '{tab} 탭의 보이는 항목을 모두 해제했습니다.',
|
|
286
286
|
'tui.nonInteractive': '대화형 화면은 터미널에서만 열립니다. --list · --set 으로도 다룰 수 있습니다.',
|
|
287
|
-
'tui.applyHeader': '적용할 변경 {count}건{suffix}:',
|
|
288
287
|
'tui.pressAnyKey': '계속하려면 아무 키나 누르세요…',
|
|
289
288
|
'tui.confirmSuffix': ' [y/N] ',
|
|
290
289
|
|
|
290
|
+
// 진행 화면(tui/progress.mjs) 문구. en.mjs 쪽 주석 참고.
|
|
291
|
+
'progress.title': '적용 중 — 변경 {count}건{suffix}',
|
|
292
|
+
'progress.counter': '{done}/{total} {percent}%',
|
|
293
|
+
'progress.elapsed': '{seconds}초',
|
|
294
|
+
'progress.elapsedMin': '{minutes}분{seconds}초',
|
|
295
|
+
'progress.running': '{seconds}초 경과',
|
|
296
|
+
'progress.more': ' …외 {count}건',
|
|
297
|
+
'progress.abortHint': 'Ctrl+C 중단 (현재 항목까지 마칩니다)',
|
|
298
|
+
'progress.aborted': '중단했습니다 — {count}건 건너뜀',
|
|
299
|
+
'progress.skipped': '건너뜀',
|
|
300
|
+
'progress.done': '끝났습니다 — 성공 {ok}건, 실패 {failed}건{skippedSuffix}',
|
|
301
|
+
'progress.plain': '[{index}/{total}] {action} {label}',
|
|
302
|
+
'progress.plainDone': ' {mark} {seconds}초',
|
|
303
|
+
|
|
291
304
|
// 언어 행(action.language) 전용. en.mjs 쪽 주석 참고.
|
|
292
305
|
'tui.lang.saved': '{path}에 저장했습니다',
|
|
293
306
|
'tui.lang.dryRun': 'dry-run — 이번 세션에만 적용했습니다',
|
|
294
307
|
'tui.lang.saveFailed': '언어를 저장하지 못했습니다: {message}',
|
|
295
308
|
'tui.lang.overridden': '다음 실행에서는 --lang / AGENT_SETUP_LANG이 여전히 이깁니다.',
|
|
296
309
|
|
|
310
|
+
// CLI 필터 표시. en.mjs 쪽 주석 참고.
|
|
311
|
+
'tui.filter.prefix': 'CLI › ',
|
|
312
|
+
'tui.filter.position': '{current}/{total}',
|
|
313
|
+
'tui.filter.empty': ' 이 탭에는 {cli}에 배선되는 항목이 없습니다.',
|
|
314
|
+
|
|
297
315
|
'item.location.user': '설치 위치: 사용자 글로벌',
|
|
298
316
|
// en.mjs 쪽 주석 참고.
|
|
299
317
|
'item.cliCoverage': 'CLI {covered}/{total}',
|
|
300
318
|
'item.unsupportedList': '미배선 {count}곳: {groups}',
|
|
301
319
|
'item.unsupportedGroup': '{clis} — {why}',
|
|
320
|
+
'detail.wired': '배선',
|
|
321
|
+
'detail.unwired': '미배선',
|
|
322
|
+
'detail.provider': '공급자',
|
|
323
|
+
'detail.preview': '미리보기',
|
|
324
|
+
'detail.scope.project': '저장소 스코프',
|
|
325
|
+
'detail.scope.user': '사용자 전역',
|
|
326
|
+
'detail.more': ' …외 {count}줄 — Ctrl+D로 펼치기',
|
|
327
|
+
'detail.empty': ' 커서를 항목 위에 올리면 상세가 보입니다.',
|
|
302
328
|
|
|
303
329
|
// 상주 언어 행 — en.mjs 쪽 주석 참고.
|
|
304
330
|
'action.language.label': '언어',
|
|
@@ -67,8 +67,8 @@ export default {
|
|
|
67
67
|
async install({ root, dryRun, exec, log = () => {}, t }) {
|
|
68
68
|
let deferred = false
|
|
69
69
|
if (!isPluginEnabled(root, [INSTALL_ID])) {
|
|
70
|
-
exec('claude', ['plugin', 'marketplace', 'add', MARKETPLACE.repo], { cwd: root })
|
|
71
|
-
const r = exec('claude', ['plugin', 'install', INSTALL_ID, '--scope', 'project'], { cwd: root })
|
|
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
72
|
if (!r.ok) {
|
|
73
73
|
if (!dryRun) enablePlugin(root, INSTALL_ID, MARKETPLACE)
|
|
74
74
|
deferred = true
|
|
@@ -86,7 +86,7 @@ export default {
|
|
|
86
86
|
|
|
87
87
|
async uninstall({ root, dryRun, exec, log = () => {}, t }) {
|
|
88
88
|
if (isPluginEnabled(root, [INSTALL_ID])) {
|
|
89
|
-
const r = exec('claude', ['plugin', 'uninstall', INSTALL_ID], { cwd: root })
|
|
89
|
+
const r = await exec('claude', ['plugin', 'uninstall', INSTALL_ID], { cwd: root })
|
|
90
90
|
if (!r.ok && !dryRun) disablePlugin(root, [INSTALL_ID])
|
|
91
91
|
}
|
|
92
92
|
if (!hasOpencode(root)) return
|
package/lib/items/skill.gsd.mjs
CHANGED
|
@@ -15,11 +15,11 @@ export default defineSkill({
|
|
|
15
15
|
return { status: found ? 'installed' : 'absent' }
|
|
16
16
|
},
|
|
17
17
|
async install({ root, exec }) {
|
|
18
|
-
const r = exec('npx', ['-y', '@opengsd/gsd-core@latest', '--claude', '--local'], { cwd: root })
|
|
18
|
+
const r = await exec('npx', ['-y', '@opengsd/gsd-core@latest', '--claude', '--local'], { cwd: root })
|
|
19
19
|
if (!r.ok) throw new LocalizedError('error.gsdInstall', { output: r.output })
|
|
20
20
|
},
|
|
21
21
|
async uninstall({ root, exec }) {
|
|
22
|
-
const r = exec('npx', ['-y', '@opengsd/gsd-core@latest', '--uninstall'], { cwd: root })
|
|
22
|
+
const r = await exec('npx', ['-y', '@opengsd/gsd-core@latest', '--uninstall'], { cwd: root })
|
|
23
23
|
if (!r.ok) throw new LocalizedError('error.gsdUninstall', { output: r.output })
|
|
24
24
|
},
|
|
25
25
|
})
|
|
@@ -19,9 +19,9 @@ export default defineSkill({
|
|
|
19
19
|
// .agents/skills Junction은 저장소 안이라 그대로 통과한다).
|
|
20
20
|
const dir = repoPathStrict(root, REL_DIR)
|
|
21
21
|
if (!existsSync(dir)) {
|
|
22
|
-
const clone = exec('git', ['clone', '--single-branch', '--depth', '1', 'https://github.com/garrytan/gstack.git', dir])
|
|
22
|
+
const clone = await exec('git', ['clone', '--single-branch', '--depth', '1', 'https://github.com/garrytan/gstack.git', dir])
|
|
23
23
|
if (!clone.ok) throw new LocalizedError('error.gstackClone', { output: clone.output })
|
|
24
|
-
const setup = exec('bash', ['./setup'], { cwd: dir })
|
|
24
|
+
const setup = await exec('bash', ['./setup'], { cwd: dir })
|
|
25
25
|
if (!setup.ok) {
|
|
26
26
|
// setup 실패 잔존물이 detect를 installed로 오판시키지 않도록 정리한다.
|
|
27
27
|
rmSync(dir, { recursive: true, force: true })
|
|
@@ -34,7 +34,7 @@ export default defineSkill({
|
|
|
34
34
|
async uninstall({ root, dryRun, exec }) {
|
|
35
35
|
const dir = repoPathStrict(root, REL_DIR)
|
|
36
36
|
if (!existsSync(dir)) return
|
|
37
|
-
exec('bash', [join(dir, 'bin', 'gstack-uninstall'), '--force'], { cwd: dir }) // 실패해도 디렉터리 삭제로 폴백
|
|
37
|
+
await exec('bash', [join(dir, 'bin', 'gstack-uninstall'), '--force'], { cwd: dir }) // 실패해도 디렉터리 삭제로 폴백
|
|
38
38
|
if (!dryRun && existsSync(dir)) rmSync(dir, { recursive: true, force: true })
|
|
39
39
|
},
|
|
40
40
|
})
|