pantheon-opencode 1.1.1-beta.4 → 1.1.1-beta.5
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/bin/pantheon-init.mjs +19 -6
- package/package.json +1 -1
- package/scripts/install/interactive.mjs +395 -0
- package/scripts/install/opencode.mjs +55 -3
package/bin/pantheon-init.mjs
CHANGED
|
@@ -27,10 +27,13 @@ function printUsage() {
|
|
|
27
27
|
console.log(' npx pantheon-opencode init # Install globally');
|
|
28
28
|
console.log(' npx pantheon-opencode init --project # Install in project');
|
|
29
29
|
console.log(' npx pantheon-opencode init --dry-run # Preview only');
|
|
30
|
-
console.log(' npx pantheon-opencode init --no-mcp
|
|
31
|
-
console.log(' npx pantheon-opencode init --force
|
|
32
|
-
console.log(' npx pantheon-opencode init --doctor
|
|
33
|
-
console.log(' npx pantheon-opencode --
|
|
30
|
+
console.log(' npx pantheon-opencode init --no-mcp # Skip MCP + venv');
|
|
31
|
+
console.log(' npx pantheon-opencode init --force # Overwrite + recreate venv');
|
|
32
|
+
console.log(' npx pantheon-opencode init --doctor # Run health check after install');
|
|
33
|
+
console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode');
|
|
34
|
+
console.log(' npx pantheon-opencode init --headless # Force non-interactive mode');
|
|
35
|
+
console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults');
|
|
36
|
+
console.log(' npx pantheon-opencode --help # Show this help');
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
async function main() {
|
|
@@ -45,6 +48,9 @@ async function main() {
|
|
|
45
48
|
const skipMCP = args.includes('--no-mcp');
|
|
46
49
|
const forceReinstall = args.includes('--force');
|
|
47
50
|
const runDoctor = args.includes('--doctor');
|
|
51
|
+
const forceInteractive = args.includes('--interactive');
|
|
52
|
+
const forceHeadless = args.includes('--headless');
|
|
53
|
+
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
48
54
|
|
|
49
55
|
const components = ['agents', 'skills', 'instructions', 'commands', 'plugins'];
|
|
50
56
|
if (!skipMCP) components.push('runtime');
|
|
@@ -56,13 +62,20 @@ async function main() {
|
|
|
56
62
|
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?';
|
|
57
63
|
} catch { /* use default */ }
|
|
58
64
|
|
|
59
|
-
console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`);
|
|
60
65
|
console.log('');
|
|
66
|
+
if (!forceInteractive || isDryRun) {
|
|
67
|
+
console.log(`Pantheon OpenCode v${version} — ${isDryRun ? 'DRY RUN' : 'Installing...'}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
try {
|
|
63
72
|
const { installOpenCode } = await import('../scripts/install/opencode.mjs');
|
|
64
73
|
const target = isProject ? process.cwd() : undefined;
|
|
65
|
-
installOpenCode(target, isDryRun, forceReinstall, components
|
|
74
|
+
await installOpenCode(target, isDryRun, forceReinstall, components, {
|
|
75
|
+
interactive: forceInteractive,
|
|
76
|
+
headless: forceHeadless,
|
|
77
|
+
yes: autoYes,
|
|
78
|
+
});
|
|
66
79
|
} catch (err) {
|
|
67
80
|
console.error(`❌ Installation failed: ${err.message}\n Run with --no-mcp to skip Python dependencies:\n npx pantheon-opencode init --no-mcp\n Or retry with --force to recreate the venv:\n npx pantheon-opencode init --force`);
|
|
68
81
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { emitKeypressEvents } from 'node:readline'
|
|
4
|
+
import { colors, icons } from './cli-ui.mjs'
|
|
5
|
+
|
|
6
|
+
const cursorHide = () => process.stdout.write('\x1b[?25l')
|
|
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')
|
|
10
|
+
|
|
11
|
+
function setupStdin() {
|
|
12
|
+
if (!process.stdin.isTTY) return false
|
|
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
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function teardownStdin() {
|
|
25
|
+
try {
|
|
26
|
+
process.stdin.setRawMode(false)
|
|
27
|
+
process.stdin.removeAllListeners('keypress')
|
|
28
|
+
process.stdin.pause()
|
|
29
|
+
} catch {
|
|
30
|
+
/* safe to ignore */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function promptMultiSelect(options, { title = '', selected = new Set(), pageSize = 6 } = {}) {
|
|
35
|
+
if (options.length === 0) return new Set()
|
|
36
|
+
|
|
37
|
+
const preSelected = selected instanceof Set ? selected : new Set(selected)
|
|
38
|
+
const items = options.map((o) => ({
|
|
39
|
+
name: o.name,
|
|
40
|
+
value: o.value,
|
|
41
|
+
description: o.description || '',
|
|
42
|
+
checked: o.checked || preSelected.has(o.value),
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
let cursor = 0
|
|
46
|
+
let scroll = 0
|
|
47
|
+
const maxVisible = Math.min(pageSize, options.length)
|
|
48
|
+
let previousLineCount = 0
|
|
49
|
+
|
|
50
|
+
const isTTY = setupStdin()
|
|
51
|
+
if (!isTTY) {
|
|
52
|
+
return new Set(items.filter((i) => i.checked).map((i) => i.value))
|
|
53
|
+
}
|
|
54
|
+
cursorHide()
|
|
55
|
+
|
|
56
|
+
function render() {
|
|
57
|
+
if (cursor < scroll) scroll = cursor
|
|
58
|
+
if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
|
|
59
|
+
|
|
60
|
+
const visible = items.slice(scroll, scroll + maxVisible)
|
|
61
|
+
const lines = []
|
|
62
|
+
|
|
63
|
+
if (title) lines.push(colors.bold(title))
|
|
64
|
+
|
|
65
|
+
const hasMoreAbove = scroll > 0
|
|
66
|
+
const hasMoreBelow = scroll + maxVisible < options.length
|
|
67
|
+
|
|
68
|
+
if (hasMoreAbove) lines.push(colors.dim(' ...'))
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < visible.length; i++) {
|
|
71
|
+
const item = visible[i]
|
|
72
|
+
const idx = scroll + i
|
|
73
|
+
const active = idx === cursor
|
|
74
|
+
const box = item.checked ? '[x]' : '[ ]'
|
|
75
|
+
const ptr = active ? `${colors.cyan('>')} ` : ' '
|
|
76
|
+
const name = active ? colors.cyan(item.name) : item.name
|
|
77
|
+
const desc = item.description ? colors.dim(` — ${item.description}`) : ''
|
|
78
|
+
lines.push(`${ptr}${colors.bold(box)} ${name}${desc}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasMoreBelow) lines.push(colors.dim(' ...'))
|
|
82
|
+
|
|
83
|
+
const count = items.filter((i) => i.checked).length
|
|
84
|
+
const total = items.length
|
|
85
|
+
const noun = count === 1 ? 'selected' : 'selected'
|
|
86
|
+
lines.push(colors.dim(`(${count}/${total} ${noun}) — ${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Space')}${colors.dim(' toggle · ')}${colors.cyan('Enter')}${colors.dim(' confirm')}`))
|
|
87
|
+
|
|
88
|
+
const lineCount = lines.length
|
|
89
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
90
|
+
clearDown()
|
|
91
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
92
|
+
previousLineCount = lineCount
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let settled = false
|
|
97
|
+
|
|
98
|
+
function finalize(result) {
|
|
99
|
+
if (settled) return
|
|
100
|
+
settled = true
|
|
101
|
+
cursorShow()
|
|
102
|
+
teardownStdin()
|
|
103
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
104
|
+
clearDown()
|
|
105
|
+
resolve(result)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cancel() {
|
|
109
|
+
if (settled) return
|
|
110
|
+
settled = true
|
|
111
|
+
cursorShow()
|
|
112
|
+
teardownStdin()
|
|
113
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
114
|
+
clearDown()
|
|
115
|
+
reject(new Error('Canceled'))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handler(str, key) {
|
|
119
|
+
if (!key) return
|
|
120
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
121
|
+
cancel()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
125
|
+
const result = new Set(items.filter((i) => i.checked).map((i) => i.value))
|
|
126
|
+
finalize(result)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (key.name === 'up') {
|
|
130
|
+
cursor = (cursor - 1 + items.length) % items.length
|
|
131
|
+
render()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (key.name === 'down') {
|
|
135
|
+
cursor = (cursor + 1) % items.length
|
|
136
|
+
render()
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (key.name === 'space') {
|
|
140
|
+
items[cursor].checked = !items[cursor].checked
|
|
141
|
+
render()
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.stdin.on('keypress', handler)
|
|
147
|
+
render()
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function promptConfirm(message, { default: def = true } = {}) {
|
|
152
|
+
const label = def ? 'Y/n' : 'y/N'
|
|
153
|
+
const isTTY = setupStdin()
|
|
154
|
+
if (!isTTY) return def
|
|
155
|
+
|
|
156
|
+
cursorHide()
|
|
157
|
+
process.stdout.write(`${colors.bold('?')} ${message} ${colors.cyan(label)} `)
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
let settled = false
|
|
161
|
+
|
|
162
|
+
function finalize(result) {
|
|
163
|
+
if (settled) return
|
|
164
|
+
settled = true
|
|
165
|
+
cursorShow()
|
|
166
|
+
teardownStdin()
|
|
167
|
+
process.stdout.write(`${result ? 'Yes' : 'No'}\n`)
|
|
168
|
+
resolve(result)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cancel() {
|
|
172
|
+
if (settled) return
|
|
173
|
+
settled = true
|
|
174
|
+
cursorShow()
|
|
175
|
+
teardownStdin()
|
|
176
|
+
process.stdout.write('\n')
|
|
177
|
+
reject(new Error('Canceled'))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handler(str, key) {
|
|
181
|
+
if (!key) return
|
|
182
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
183
|
+
cancel()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
187
|
+
finalize(def)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (str) {
|
|
191
|
+
const ch = str.toLowerCase()
|
|
192
|
+
if (ch === 'y' || ch === 'yes') {
|
|
193
|
+
finalize(true)
|
|
194
|
+
} else if (ch === 'n' || ch === 'no') {
|
|
195
|
+
finalize(false)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.stdin.on('keypress', handler)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function promptList(options, { title = '' } = {}) {
|
|
205
|
+
if (options.length === 0) throw new Error('No options provided')
|
|
206
|
+
if (options.length === 1) return options[0].value
|
|
207
|
+
|
|
208
|
+
const items = options.map((o) => ({
|
|
209
|
+
name: o.name,
|
|
210
|
+
value: o.value,
|
|
211
|
+
description: o.description || '',
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
let cursor = 0
|
|
215
|
+
const pageSize = Math.min(8, options.length)
|
|
216
|
+
let scroll = 0
|
|
217
|
+
const maxVisible = Math.min(pageSize, options.length)
|
|
218
|
+
let previousLineCount = 0
|
|
219
|
+
|
|
220
|
+
const isTTY = setupStdin()
|
|
221
|
+
if (!isTTY) return items[0].value
|
|
222
|
+
cursorHide()
|
|
223
|
+
|
|
224
|
+
function render() {
|
|
225
|
+
if (cursor < scroll) scroll = cursor
|
|
226
|
+
if (cursor >= scroll + maxVisible) scroll = cursor - maxVisible + 1
|
|
227
|
+
|
|
228
|
+
const visible = items.slice(scroll, scroll + maxVisible)
|
|
229
|
+
const lines = []
|
|
230
|
+
|
|
231
|
+
if (title) lines.push(colors.bold(title))
|
|
232
|
+
|
|
233
|
+
const hasMoreAbove = scroll > 0
|
|
234
|
+
const hasMoreBelow = scroll + maxVisible < options.length
|
|
235
|
+
|
|
236
|
+
if (hasMoreAbove) lines.push(colors.dim(' ...'))
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < visible.length; i++) {
|
|
239
|
+
const item = visible[i]
|
|
240
|
+
const idx = scroll + i
|
|
241
|
+
const active = idx === cursor
|
|
242
|
+
const ptr = active ? `${colors.cyan('>')} ` : ' '
|
|
243
|
+
const name = active ? colors.cyan(item.name) : item.name
|
|
244
|
+
const desc = item.description ? colors.dim(` — ${item.description}`) : ''
|
|
245
|
+
lines.push(`${ptr}${colors.bold('◉')} ${name}${desc}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (hasMoreBelow) lines.push(colors.dim(' ...'))
|
|
249
|
+
|
|
250
|
+
lines.push(colors.dim(`(${colors.cyan('↑')}${colors.dim('/')}${colors.cyan('↓')} navigate · ${colors.cyan('Enter')}${colors.dim(' select')}`))
|
|
251
|
+
|
|
252
|
+
const lineCount = lines.length
|
|
253
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
254
|
+
clearDown()
|
|
255
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
256
|
+
previousLineCount = lineCount
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
let settled = false
|
|
261
|
+
|
|
262
|
+
function finalize(result) {
|
|
263
|
+
if (settled) return
|
|
264
|
+
settled = true
|
|
265
|
+
cursorShow()
|
|
266
|
+
teardownStdin()
|
|
267
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
268
|
+
clearDown()
|
|
269
|
+
resolve(result)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cancel() {
|
|
273
|
+
if (settled) return
|
|
274
|
+
settled = true
|
|
275
|
+
cursorShow()
|
|
276
|
+
teardownStdin()
|
|
277
|
+
if (previousLineCount > 0) cursorUp(previousLineCount)
|
|
278
|
+
clearDown()
|
|
279
|
+
reject(new Error('Canceled'))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function handler(str, key) {
|
|
283
|
+
if (!key) return
|
|
284
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
|
|
285
|
+
cancel()
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
289
|
+
finalize(items[cursor].value)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
if (key.name === 'up') {
|
|
293
|
+
cursor = (cursor - 1 + items.length) % items.length
|
|
294
|
+
render()
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
if (key.name === 'down') {
|
|
298
|
+
cursor = (cursor + 1) % items.length
|
|
299
|
+
render()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
process.stdin.on('keypress', handler)
|
|
305
|
+
render()
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function runInteractiveInstall({ components = [], defaultComponents = [] } = {}) {
|
|
310
|
+
const defaultSet = new Set(
|
|
311
|
+
defaultComponents.length > 0 ? defaultComponents : components,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
const componentOptions = [
|
|
315
|
+
{
|
|
316
|
+
name: 'agents',
|
|
317
|
+
value: 'agents',
|
|
318
|
+
description: `Agent configurations and routing (${14} agents)`,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'skills',
|
|
322
|
+
value: 'skills',
|
|
323
|
+
description: `Reusable skill workflows (${21} skills)`,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'instructions',
|
|
327
|
+
value: 'instructions',
|
|
328
|
+
description: 'AGENTS.md and instructional files',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'prompts',
|
|
332
|
+
value: 'prompts',
|
|
333
|
+
description: 'Prompt templates for agents',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'commands',
|
|
337
|
+
value: 'commands',
|
|
338
|
+
description: 'OpenCode command shortcuts',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'plugins',
|
|
342
|
+
value: 'plugins',
|
|
343
|
+
description: 'TUI sidebar plugin',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'runtime',
|
|
347
|
+
value: 'runtime',
|
|
348
|
+
description: 'Python venv + MCP server scripts',
|
|
349
|
+
},
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
process.stdout.write('\n')
|
|
353
|
+
process.stdout.write(`${colors.bold(colors.cyan('📦 OpenCode Installer'))}\n\n`)
|
|
354
|
+
|
|
355
|
+
const selectedComponents = await promptMultiSelect(componentOptions, {
|
|
356
|
+
title: 'Select components to install:',
|
|
357
|
+
selected: defaultSet,
|
|
358
|
+
pageSize: 7,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
process.stdout.write('\n')
|
|
362
|
+
|
|
363
|
+
if (selectedComponents.size === 0) {
|
|
364
|
+
process.stdout.write(`${colors.yellow('No components selected — using defaults\n')}`)
|
|
365
|
+
defaultSet.forEach((v) => selectedComponents.add(v))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const targetOptions = [
|
|
369
|
+
{
|
|
370
|
+
name: 'Global directory',
|
|
371
|
+
value: 'global',
|
|
372
|
+
description: '~/.config/opencode (shared across projects)',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'Current project',
|
|
376
|
+
value: 'project',
|
|
377
|
+
description: './.opencode (scoped to this project)',
|
|
378
|
+
},
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
const target = await promptList(targetOptions, {
|
|
382
|
+
title: 'Install location:',
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
process.stdout.write('\n')
|
|
386
|
+
|
|
387
|
+
const confirmed = await promptConfirm(
|
|
388
|
+
`Install ${selectedComponents.size} component(s) in ${target === 'global' ? 'global config' : 'current project'}?`,
|
|
389
|
+
{ default: true },
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
process.stdout.write('\n')
|
|
393
|
+
|
|
394
|
+
return { components: selectedComponents, target, confirmed }
|
|
395
|
+
}
|
|
@@ -8,7 +8,7 @@ import { homedir } from 'node:os'
|
|
|
8
8
|
import { join, resolve } from 'node:path'
|
|
9
9
|
import { healthCheck } from './health-check.mjs'
|
|
10
10
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
11
|
-
import { configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
|
|
11
|
+
import { colors, configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
|
|
12
12
|
import {
|
|
13
13
|
collectSkillNames,
|
|
14
14
|
copyFiles,
|
|
@@ -24,6 +24,8 @@ import { setupVenv } from './venv.mjs'
|
|
|
24
24
|
import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
25
25
|
import { resolveComponents } from './component.mjs'
|
|
26
26
|
|
|
27
|
+
const COMPONENT_NAMES = ['agents', 'skills', 'instructions', 'prompts', 'commands', 'plugins', 'runtime']
|
|
28
|
+
|
|
27
29
|
function deepClone(value) {
|
|
28
30
|
return JSON.parse(JSON.stringify(value))
|
|
29
31
|
}
|
|
@@ -72,11 +74,12 @@ function isGlobalConfigDir(target) {
|
|
|
72
74
|
return resolve(target) === xdgDir
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
export function installOpenCode(
|
|
77
|
+
export async function installOpenCode(
|
|
76
78
|
target,
|
|
77
79
|
dryRun = false,
|
|
78
80
|
clean = false,
|
|
79
81
|
components = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'],
|
|
82
|
+
opts = {},
|
|
80
83
|
) {
|
|
81
84
|
// Default target if not provided (global ~/.config/opencode)
|
|
82
85
|
if (!target) {
|
|
@@ -86,6 +89,35 @@ export function installOpenCode(
|
|
|
86
89
|
const componentSet = new Set(components)
|
|
87
90
|
const stats = summary.opencode
|
|
88
91
|
|
|
92
|
+
// Interactive mode handling
|
|
93
|
+
const stdinTTY = process.stdin.isTTY && process.stdout.isTTY
|
|
94
|
+
const forceInteractive = opts.interactive === true
|
|
95
|
+
const forceHeadless = opts.headless === true
|
|
96
|
+
const autoYes = opts.yes === true
|
|
97
|
+
const interactive = forceInteractive || (stdinTTY && !forceHeadless)
|
|
98
|
+
|
|
99
|
+
if (interactive && !dryRun) {
|
|
100
|
+
const { runInteractiveInstall } = await import('./interactive.mjs')
|
|
101
|
+
const result = await runInteractiveInstall({
|
|
102
|
+
components,
|
|
103
|
+
defaultComponents: COMPONENT_NAMES,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (!result.confirmed && !autoYes) {
|
|
107
|
+
info('Installation canceled.')
|
|
108
|
+
return stats
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Map target selection to actual path
|
|
112
|
+
const newComponents = [...result.components]
|
|
113
|
+
componentSet.clear()
|
|
114
|
+
for (const c of newComponents) componentSet.add(c)
|
|
115
|
+
|
|
116
|
+
if (result.target === 'project') {
|
|
117
|
+
target = process.cwd()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
// Determine layout based on install scope.
|
|
90
122
|
// Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
|
|
91
123
|
// agents/ skills/ commands/ plugins/
|
|
@@ -679,5 +711,25 @@ export function installOpenCode(
|
|
|
679
711
|
}
|
|
680
712
|
}
|
|
681
713
|
|
|
682
|
-
|
|
714
|
+
if (interactive && !dryRun) {
|
|
715
|
+
const created = stats.created
|
|
716
|
+
const skipped = stats.skipped
|
|
717
|
+
const errors = stats.errors
|
|
718
|
+
|
|
719
|
+
process.stdout.write('\n')
|
|
720
|
+
process.stdout.write(` ${colors.bold(colors.green('Installation complete'))}\n`)
|
|
721
|
+
process.stdout.write(` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`)
|
|
722
|
+
process.stdout.write(` ${colors.green('\u2713')} ${created} component(s) installed\n`)
|
|
723
|
+
if (skipped > 0) process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
|
|
724
|
+
if (errors > 0) process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
|
|
725
|
+
process.stdout.write('\n')
|
|
726
|
+
process.stdout.write(` ${colors.bold('Next steps:')}\n`)
|
|
727
|
+
process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
|
|
728
|
+
process.stdout.write(` ${colors.dim('\u2022')} Add MCP servers in mcp.json\n`)
|
|
729
|
+
process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode doctor' to verify\n`)
|
|
730
|
+
process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`)
|
|
731
|
+
process.stdout.write('\n')
|
|
732
|
+
} else {
|
|
733
|
+
printSummary(target, ['opencode'], stats)
|
|
734
|
+
}
|
|
683
735
|
}
|