pantheon-opencode 1.6.0-beta.11 → 1.6.0-beta.13
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/AGENTS.global.md +3 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +85 -31
- package/README.pt-BR.md +7 -4
- package/bin/pantheon-init.mjs +278 -102
- package/package.json +5 -3
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +77 -13
- package/scripts/check-plugin-v2-coverage.mjs +7 -7
- package/scripts/doctor.mjs +242 -19
- package/scripts/install/backup.mjs +191 -18
- package/scripts/install/cli-ui.mjs +73 -0
- package/scripts/install/file-inventory.mjs +555 -0
- package/scripts/install/interactive.mjs +69 -391
- package/scripts/install/migrate.mjs +45 -38
- package/scripts/install/model-picker.mjs +63 -22
- package/scripts/install/opencode-version.mjs +9 -1
- package/scripts/install/opencode.mjs +279 -79
- package/scripts/install/plan-artifact.mjs +517 -0
- package/scripts/install/plan.mjs +165 -3
- package/scripts/install/plugin.mjs +118 -14
- package/scripts/install/prompt-result.mjs +2 -0
- package/scripts/install/shared.mjs +188 -65
- package/scripts/install/state.mjs +63 -8
- package/scripts/install/venv.mjs +15 -6
- package/scripts/install/verify.mjs +128 -0
- package/scripts/install/vision-prompt.mjs +2 -0
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/manifest-inventory.mjs +49 -3
- package/scripts/node-engine-policy.mjs +85 -0
- package/scripts/package-evidence.mjs +6 -0
- package/scripts/postinstall.mjs +26 -26
- package/scripts/reconcile.mjs +325 -0
- package/scripts/sync-tui.mjs +5 -1
- package/scripts/uninstall.mjs +258 -34
- package/scripts/update.mjs +164 -0
- package/scripts/validate-node-engine-policy.mjs +13 -0
- package/scripts/version-check.mjs +34 -1
- package/scripts/versioning.mjs +9 -1
- package/skills-lock.json +3 -3
- package/src/agents/mnemosyne.md +7 -2
- package/src/instructions/memory-protocol.instructions.md +2 -2
- package/src/instructions/zeus-memory-operations.instructions.md +4 -3
- package/src/pantheon/v2-unsupported.mjs +2 -3
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +3 -3
- package/src/plugins/tui/package.json +2 -2
- package/src/skills/artifact-management/SKILL.md +5 -1
- package/src/skills/memory-bank/SKILL.md +5 -1
- package/src/skills/worktrees/SKILL.md +30 -11
|
@@ -1,420 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import * as clack from '@clack/prompts'
|
|
4
|
+
import { PROMPT_CANCELED } from './prompt-result.mjs'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const cursorShow = () => process.stdout.write('\x1b[?25h')
|
|
8
|
-
const cursorUp = (n) => process.stdout.write(`\x1b[${n}A`)
|
|
9
|
-
const clearDown = () => process.stdout.write('\x1b[J')
|
|
6
|
+
export { PROMPT_CANCELED }
|
|
10
7
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
if (!process.stdout.isTTY) return false
|
|
14
|
-
emitKeypressEvents(process.stdin)
|
|
15
|
-
try {
|
|
16
|
-
process.stdin.setRawMode(true)
|
|
17
|
-
} catch {
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
process.stdin.resume()
|
|
21
|
-
return true
|
|
8
|
+
function isTerminal() {
|
|
9
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
22
10
|
}
|
|
23
11
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
process.stdin.pause()
|
|
29
|
-
} catch {
|
|
30
|
-
/* safe to ignore */
|
|
31
|
-
}
|
|
12
|
+
function cancelPrompt(prompts, value) {
|
|
13
|
+
if (!prompts.isCancel(value)) return false
|
|
14
|
+
prompts.cancel('Installation canceled.')
|
|
15
|
+
return true
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Prompt for authorization to apply a reviewed installer plan.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} planSummary Summary of the changes that require approval.
|
|
22
|
+
* @param {{ default?: boolean, prompts?: typeof clack, isTTY?: boolean }} [options]
|
|
23
|
+
* @returns {Promise<boolean | typeof PROMPT_CANCELED>} True if authorized, false if declined, or the cancellation sentinel.
|
|
24
|
+
*/
|
|
25
|
+
export async function promptReview(
|
|
26
|
+
planSummary,
|
|
27
|
+
{ default: defaultValue = false, prompts = clack, isTTY = isTerminal() } = {},
|
|
37
28
|
) {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
name: o.name,
|
|
43
|
-
value: o.value,
|
|
44
|
-
description: o.description || '',
|
|
45
|
-
checked: o.checked || preSelected.has(o.value),
|
|
46
|
-
}))
|
|
47
|
-
|
|
48
|
-
let cursor = 0
|
|
49
|
-
let scroll = 0
|
|
50
|
-
const maxVisible = Math.min(pageSize, options.length)
|
|
51
|
-
let previousLineCount = 0
|
|
52
|
-
|
|
53
|
-
const isTTY = setupStdin()
|
|
54
|
-
if (!isTTY) {
|
|
55
|
-
return new Set(items.filter((i) => i.checked).map((i) => i.value))
|
|
56
|
-
}
|
|
57
|
-
cursorHide()
|
|
58
|
-
|
|
59
|
-
function render() {
|
|
60
|
-
if (cursor < scroll) scroll = cursor
|
|
61
|
-
if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
|
|
62
|
-
|
|
63
|
-
const visible = items.slice(scroll, scroll + maxVisible)
|
|
64
|
-
const lines = []
|
|
65
|
-
|
|
66
|
-
if (title) lines.push(colors.bold(title))
|
|
67
|
-
|
|
68
|
-
const hasMoreAbove = scroll > 0
|
|
69
|
-
const hasMoreBelow = scroll + maxVisible < options.length
|
|
70
|
-
|
|
71
|
-
if (hasMoreAbove) lines.push(colors.dim(' ...'))
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < visible.length; i++) {
|
|
74
|
-
const item = visible[i]
|
|
75
|
-
const idx = scroll + i
|
|
76
|
-
const active = idx === cursor
|
|
77
|
-
const box = item.checked ? '[x]' : '[ ]'
|
|
78
|
-
const ptr = active ? `${colors.cyan('>')} ` : ' '
|
|
79
|
-
const name = active ? colors.cyan(item.name) : item.name
|
|
80
|
-
const desc = item.description ? colors.dim(` — ${item.description}`) : ''
|
|
81
|
-
lines.push(`${ptr}${colors.bold(box)} ${name}${desc}`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (hasMoreBelow) lines.push(colors.dim(' ...'))
|
|
85
|
-
|
|
86
|
-
const count = items.filter((i) => i.checked).length
|
|
87
|
-
const total = items.length
|
|
88
|
-
const noun = count === 1 ? 'selected' : 'selected'
|
|
89
|
-
lines.push(
|
|
90
|
-
colors.dim(
|
|
91
|
-
`(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`,
|
|
92
|
-
),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
const lineCount = lines.length
|
|
96
|
-
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
97
|
-
clearDown()
|
|
98
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
99
|
-
previousLineCount = lineCount
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
let settled = false
|
|
104
|
-
|
|
105
|
-
function finalize(result) {
|
|
106
|
-
if (settled) return
|
|
107
|
-
settled = true
|
|
108
|
-
cursorShow()
|
|
109
|
-
teardownStdin()
|
|
110
|
-
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
111
|
-
clearDown()
|
|
112
|
-
resolve(result)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function cancel() {
|
|
116
|
-
if (settled) return
|
|
117
|
-
settled = true
|
|
118
|
-
cursorShow()
|
|
119
|
-
teardownStdin()
|
|
120
|
-
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
121
|
-
clearDown()
|
|
122
|
-
reject(new Error('Canceled'))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function handler(_str, key) {
|
|
126
|
-
if (!key) return
|
|
127
|
-
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
128
|
-
cancel()
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
132
|
-
const result = new Set(items.filter((i) => i.checked).map((i) => i.value))
|
|
133
|
-
finalize(result)
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
if (key.name === 'up') {
|
|
137
|
-
cursor = (cursor - 1 + items.length) % items.length
|
|
138
|
-
render()
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
if (key.name === 'down') {
|
|
142
|
-
cursor = (cursor + 1) % items.length
|
|
143
|
-
render()
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
if (key.name === 'space') {
|
|
147
|
-
items[cursor].checked = !items[cursor].checked
|
|
148
|
-
render()
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
process.stdin.on('keypress', handler)
|
|
154
|
-
render()
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export async function promptConfirm(message, { default: def = true } = {}) {
|
|
159
|
-
const label = def ? 'Y/n' : 'y/N'
|
|
160
|
-
const isTTY = setupStdin()
|
|
161
|
-
if (!isTTY) return def
|
|
162
|
-
|
|
163
|
-
cursorHide()
|
|
164
|
-
process.stdout.write(`${colors.bold('?')} ${message} ${colors.cyan(label)} `)
|
|
165
|
-
|
|
166
|
-
return new Promise((resolve, reject) => {
|
|
167
|
-
let settled = false
|
|
168
|
-
|
|
169
|
-
function finalize(result) {
|
|
170
|
-
if (settled) return
|
|
171
|
-
settled = true
|
|
172
|
-
cursorShow()
|
|
173
|
-
teardownStdin()
|
|
174
|
-
process.stdout.write(`${result ? 'Yes' : 'No'}\n`)
|
|
175
|
-
resolve(result)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function cancel() {
|
|
179
|
-
if (settled) return
|
|
180
|
-
settled = true
|
|
181
|
-
cursorShow()
|
|
182
|
-
teardownStdin()
|
|
183
|
-
process.stdout.write('\n')
|
|
184
|
-
reject(new Error('Canceled'))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function handler(str, key) {
|
|
188
|
-
if (!key) return
|
|
189
|
-
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
190
|
-
cancel()
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
194
|
-
finalize(def)
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
if (str) {
|
|
198
|
-
const ch = str.toLowerCase()
|
|
199
|
-
if (ch === 'y' || ch === 'yes') {
|
|
200
|
-
finalize(true)
|
|
201
|
-
} else if (ch === 'n' || ch === 'no') {
|
|
202
|
-
finalize(false)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
process.stdin.on('keypress', handler)
|
|
208
|
-
})
|
|
29
|
+
if (!isTTY) return false
|
|
30
|
+
const result = await prompts.confirm({ message: planSummary, initialValue: defaultValue })
|
|
31
|
+
if (cancelPrompt(prompts, result)) return PROMPT_CANCELED
|
|
32
|
+
return result === true
|
|
209
33
|
}
|
|
210
34
|
|
|
211
35
|
/**
|
|
212
|
-
*
|
|
213
|
-
* bare Enter never authorizes a write. In a non-TTY it returns the default
|
|
214
|
-
* (`false`) without blocking — callers that must not prompt at all should branch
|
|
215
|
-
* on TTY before calling.
|
|
36
|
+
* Collect interactive setup choices before the installer begins any writes.
|
|
216
37
|
*
|
|
217
|
-
* @param {string
|
|
218
|
-
* @
|
|
219
|
-
* @returns {Promise<boolean>}
|
|
38
|
+
* @param {{ components?: string[], defaultComponents?: string[], prompts?: typeof clack, isTTY?: boolean }} [options]
|
|
39
|
+
* @returns {Promise<{ components?: Set<string>, target?: string, confirmed?: boolean, canceled?: boolean }>}
|
|
220
40
|
*/
|
|
221
|
-
export async function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const items = options.map((o) => ({
|
|
230
|
-
name: o.name,
|
|
231
|
-
value: o.value,
|
|
232
|
-
description: o.description || '',
|
|
233
|
-
}))
|
|
234
|
-
|
|
235
|
-
let cursor = 0
|
|
236
|
-
const pageSize = Math.min(8, options.length)
|
|
237
|
-
let scroll = 0
|
|
238
|
-
const maxVisible = Math.min(pageSize, options.length)
|
|
239
|
-
let previousLineCount = 0
|
|
240
|
-
|
|
241
|
-
const isTTY = setupStdin()
|
|
242
|
-
if (!isTTY) return items[0].value
|
|
243
|
-
cursorHide()
|
|
244
|
-
|
|
245
|
-
function render() {
|
|
246
|
-
if (cursor < scroll) scroll = cursor
|
|
247
|
-
if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
|
|
248
|
-
|
|
249
|
-
const visible = items.slice(scroll, scroll + maxVisible)
|
|
250
|
-
const lines = []
|
|
251
|
-
|
|
252
|
-
if (title) lines.push(colors.bold(title))
|
|
253
|
-
|
|
254
|
-
const hasMoreAbove = scroll > 0
|
|
255
|
-
const hasMoreBelow = scroll + maxVisible < options.length
|
|
256
|
-
|
|
257
|
-
if (hasMoreAbove) lines.push(colors.dim(' ...'))
|
|
258
|
-
|
|
259
|
-
for (let i = 0; i < visible.length; i++) {
|
|
260
|
-
const item = visible[i]
|
|
261
|
-
const idx = scroll + i
|
|
262
|
-
const active = idx === cursor
|
|
263
|
-
const ptr = active ? `${colors.cyan('>')} ` : ' '
|
|
264
|
-
const name = active ? colors.cyan(item.name) : item.name
|
|
265
|
-
const desc = item.description ? colors.dim(` — ${item.description}`) : ''
|
|
266
|
-
lines.push(`${ptr}${colors.bold('◉')} ${name}${desc}`)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (hasMoreBelow) lines.push(colors.dim(' ...'))
|
|
270
|
-
|
|
271
|
-
lines.push(
|
|
272
|
-
colors.dim(
|
|
273
|
-
`(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`,
|
|
274
|
-
),
|
|
275
|
-
)
|
|
41
|
+
export async function runInteractiveInstall({
|
|
42
|
+
components = [],
|
|
43
|
+
defaultComponents = [],
|
|
44
|
+
prompts = clack,
|
|
45
|
+
isTTY = isTerminal(),
|
|
46
|
+
} = {}) {
|
|
47
|
+
const defaults = defaultComponents.length > 0 ? defaultComponents : components
|
|
48
|
+
const defaultSet = new Set(defaults)
|
|
276
49
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
281
|
-
previousLineCount = lineCount
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return new Promise((resolve, reject) => {
|
|
285
|
-
let settled = false
|
|
286
|
-
|
|
287
|
-
function finalize(result) {
|
|
288
|
-
if (settled) return
|
|
289
|
-
settled = true
|
|
290
|
-
cursorShow()
|
|
291
|
-
teardownStdin()
|
|
292
|
-
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
293
|
-
clearDown()
|
|
294
|
-
resolve(result)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function cancel() {
|
|
298
|
-
if (settled) return
|
|
299
|
-
settled = true
|
|
300
|
-
cursorShow()
|
|
301
|
-
teardownStdin()
|
|
302
|
-
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
303
|
-
clearDown()
|
|
304
|
-
reject(new Error('Canceled'))
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function handler(_str, key) {
|
|
308
|
-
if (!key) return
|
|
309
|
-
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
310
|
-
cancel()
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
314
|
-
finalize(items[cursor].value)
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
if (key.name === 'up') {
|
|
318
|
-
cursor = (cursor - 1 + items.length) % items.length
|
|
319
|
-
render()
|
|
320
|
-
return
|
|
321
|
-
}
|
|
322
|
-
if (key.name === 'down') {
|
|
323
|
-
cursor = (cursor + 1) % items.length
|
|
324
|
-
render()
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
process.stdin.on('keypress', handler)
|
|
330
|
-
render()
|
|
331
|
-
})
|
|
332
|
-
}
|
|
50
|
+
// Preserve the prior no-TTY fallback (defaults, global target, and no apply
|
|
51
|
+
// authorization) without asking a terminal prompt library to access stdin.
|
|
52
|
+
if (!isTTY) return { components: defaultSet, target: 'global', confirmed: true }
|
|
333
53
|
|
|
334
|
-
|
|
335
|
-
const defaultSet = new Set(defaultComponents.length > 0 ? defaultComponents : components)
|
|
54
|
+
process.stdout.write('\n📦 OpenCode Installer\n\n')
|
|
336
55
|
|
|
337
56
|
const componentOptions = [
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
value: 'skills',
|
|
346
|
-
description: `Reusable skill workflows (${20} skills)`,
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
name: 'instructions',
|
|
350
|
-
value: 'instructions',
|
|
351
|
-
description: 'AGENTS.md and instructional files',
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
name: 'prompts',
|
|
355
|
-
value: 'prompts',
|
|
356
|
-
description: 'Prompt templates for agents',
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
name: 'commands',
|
|
360
|
-
value: 'commands',
|
|
361
|
-
description: 'OpenCode command shortcuts',
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
name: 'plugins',
|
|
365
|
-
value: 'plugins',
|
|
366
|
-
description: 'TUI sidebar plugin',
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
name: 'runtime',
|
|
370
|
-
value: 'runtime',
|
|
371
|
-
description: 'Python venv + MCP server scripts',
|
|
372
|
-
},
|
|
57
|
+
{ label: 'agents', value: 'agents', hint: 'Agent configurations and routing (14 agents)' },
|
|
58
|
+
{ label: 'skills', value: 'skills', hint: `Reusable skill workflows (${20} skills)` },
|
|
59
|
+
{ label: 'instructions', value: 'instructions', hint: 'AGENTS.md and instructional files' },
|
|
60
|
+
{ label: 'prompts', value: 'prompts', hint: 'Prompt templates for agents' },
|
|
61
|
+
{ label: 'commands', value: 'commands', hint: 'OpenCode command shortcuts' },
|
|
62
|
+
{ label: 'plugins', value: 'plugins', hint: 'TUI sidebar plugin' },
|
|
63
|
+
{ label: 'runtime', value: 'runtime', hint: 'Python venv + MCP server scripts' },
|
|
373
64
|
]
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
title: 'Select components to install:',
|
|
380
|
-
selected: defaultSet,
|
|
381
|
-
pageSize: 7,
|
|
65
|
+
const chosen = await prompts.multiselect({
|
|
66
|
+
message: 'Select components to install:',
|
|
67
|
+
options: componentOptions,
|
|
68
|
+
initialValues: [...defaultSet],
|
|
69
|
+
required: false,
|
|
382
70
|
})
|
|
71
|
+
if (cancelPrompt(prompts, chosen)) return { canceled: true }
|
|
383
72
|
|
|
384
|
-
|
|
385
|
-
|
|
73
|
+
const selectedComponents = new Set(chosen)
|
|
386
74
|
if (selectedComponents.size === 0) {
|
|
387
|
-
|
|
388
|
-
defaultSet.forEach((v) => {
|
|
389
|
-
selectedComponents.add(v)
|
|
390
|
-
})
|
|
75
|
+
for (const value of defaults) selectedComponents.add(value)
|
|
391
76
|
}
|
|
392
77
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
value: 'project',
|
|
402
|
-
|
|
403
|
-
},
|
|
404
|
-
]
|
|
405
|
-
|
|
406
|
-
const target = await promptList(targetOptions, {
|
|
407
|
-
title: 'Install location:',
|
|
78
|
+
const target = await prompts.select({
|
|
79
|
+
message: 'Install location:',
|
|
80
|
+
options: [
|
|
81
|
+
{
|
|
82
|
+
label: 'Global directory',
|
|
83
|
+
value: 'global',
|
|
84
|
+
hint: '~/.config/opencode (shared across projects)',
|
|
85
|
+
},
|
|
86
|
+
{ label: 'Current project', value: 'project', hint: './.opencode (scoped to this project)' },
|
|
87
|
+
],
|
|
408
88
|
})
|
|
89
|
+
if (cancelPrompt(prompts, target)) return { canceled: true }
|
|
409
90
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
process.stdout.write('\n')
|
|
91
|
+
const confirmed = await prompts.confirm({
|
|
92
|
+
message: `Install ${selectedComponents.size} component(s) in ${target === 'global' ? 'global config' : 'current project'}?`,
|
|
93
|
+
initialValue: true,
|
|
94
|
+
})
|
|
95
|
+
if (cancelPrompt(prompts, confirmed)) return { canceled: true }
|
|
418
96
|
|
|
419
97
|
return { components: selectedComponents, target, confirmed }
|
|
420
98
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { dirname, join } from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
|
-
import { ACTIONS,
|
|
14
|
+
import { ACTIONS, fingerprintOutput, recordChange } from './plan.mjs'
|
|
15
15
|
import { createInitialState, readState, writeState } from './state.mjs'
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -48,7 +48,11 @@ export function detectVersion(target) {
|
|
|
48
48
|
* @param {{ dryRun?: boolean, recorder?: (change: object) => void }} [options]
|
|
49
49
|
* @returns {{ applied: number, messages: string[] }}
|
|
50
50
|
*/
|
|
51
|
-
export function runMigrations(
|
|
51
|
+
export function runMigrations(
|
|
52
|
+
target,
|
|
53
|
+
currentVersion,
|
|
54
|
+
{ dryRun = false, recorder = null, updatedAt = new Date().toISOString() } = {},
|
|
55
|
+
) {
|
|
52
56
|
const applied = []
|
|
53
57
|
const semver = parseSemver(currentVersion)
|
|
54
58
|
|
|
@@ -107,49 +111,52 @@ export function runMigrations(target, currentVersion, { dryRun = false, recorder
|
|
|
107
111
|
// ── Write updated state ───────────────────────────────────────────
|
|
108
112
|
if (applied.length > 0) {
|
|
109
113
|
const statePath = join(target, '.pantheon', 'install-state.json')
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!state.components) {
|
|
130
|
-
state.components = {
|
|
131
|
-
agents: { version: 1, status: 'installed' },
|
|
132
|
-
skills: { version: 1, status: 'installed' },
|
|
133
|
-
plugins: { version: 1, status: 'installed' },
|
|
134
|
-
runtime: { version: 1, status: 'installed' },
|
|
135
|
-
}
|
|
114
|
+
const newVersion = formatSemver(semver)
|
|
115
|
+
let state = readState(target)
|
|
116
|
+
const stateExisted = state !== null
|
|
117
|
+
if (!state) state = createInitialState(newVersion, updatedAt)
|
|
118
|
+
state.pantheon_version = newVersion
|
|
119
|
+
for (const m of applied) {
|
|
120
|
+
state.applied_migrations.push({
|
|
121
|
+
from: m.from,
|
|
122
|
+
to: m.to,
|
|
123
|
+
applied_at: updatedAt,
|
|
124
|
+
status: 'completed',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
if (!state.components) {
|
|
128
|
+
state.components = {
|
|
129
|
+
agents: { version: 1, status: 'installed' },
|
|
130
|
+
skills: { version: 1, status: 'installed' },
|
|
131
|
+
plugins: { version: 1, status: 'installed' },
|
|
132
|
+
runtime: { version: 1, status: 'installed' },
|
|
136
133
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
}
|
|
135
|
+
state.updated_at = updatedAt
|
|
136
|
+
state.schema_version = 2
|
|
137
|
+
const stateContent = `${JSON.stringify(state, null, 2)}\n`
|
|
138
|
+
if (!dryRun) writeState(target, state, { updatedAt })
|
|
139
|
+
recordChange(
|
|
140
|
+
{
|
|
141
|
+
path: statePath,
|
|
142
|
+
action: stateExisted ? ACTIONS.UPDATE : ACTIONS.CREATE,
|
|
143
|
+
bytes: Buffer.byteLength(stateContent, 'utf8'),
|
|
144
|
+
outputFingerprint: fingerprintOutput(stateContent),
|
|
145
|
+
scored: false,
|
|
146
|
+
},
|
|
147
|
+
recorder,
|
|
148
|
+
)
|
|
149
|
+
return {
|
|
150
|
+
applied: applied.length,
|
|
151
|
+
messages: applied.map((m) => `${m.from} → ${m.to}: ${m.description}`),
|
|
152
|
+
plannedState: state,
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
return {
|
|
151
157
|
applied: applied.length,
|
|
152
158
|
messages: applied.map((m) => `${m.from} → ${m.to}: ${m.description}`),
|
|
159
|
+
plannedState: null,
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|