pantheon-opencode 1.1.1-beta.3 → 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/cli-ui.mjs +160 -0
- package/scripts/install/component.mjs +24 -0
- package/scripts/install/interactive.mjs +395 -0
- package/scripts/install/migrate-config.mjs +308 -0
- package/scripts/install/migrate.mjs +210 -30
- package/scripts/install/opencode.mjs +96 -78
- package/scripts/install/plugin.mjs +139 -0
- package/scripts/install/state.mjs +194 -0
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,160 @@
|
|
|
1
|
+
export const icons = {
|
|
2
|
+
success: '\u2705',
|
|
3
|
+
warning: '\u26a0\ufe0f ',
|
|
4
|
+
error: '\u274c',
|
|
5
|
+
info: '\u2139\ufe0f ',
|
|
6
|
+
progress: '\u23f3',
|
|
7
|
+
done: '\u2705',
|
|
8
|
+
arrow: '\u2192',
|
|
9
|
+
bullet: '\u2022',
|
|
10
|
+
star: '\u2b50',
|
|
11
|
+
wrench: '\U0001f527',
|
|
12
|
+
rocket: '\U0001f680',
|
|
13
|
+
package: '\U0001f4e6',
|
|
14
|
+
gear: '\u2699\ufe0f',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const colors = {
|
|
18
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
19
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
20
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
21
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
22
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
23
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let _quiet = false
|
|
27
|
+
let _dryRun = false
|
|
28
|
+
|
|
29
|
+
export function configure(opts = {}) {
|
|
30
|
+
if (opts.quiet !== undefined) _quiet = opts.quiet
|
|
31
|
+
if (opts.dryRun !== undefined) _dryRun = opts.dryRun
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tag(msg) {
|
|
35
|
+
return _dryRun ? `[DRY-RUN] ${msg}` : msg
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tty() {
|
|
39
|
+
return process.stdout.isTTY
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function section(title) {
|
|
43
|
+
if (_quiet) return
|
|
44
|
+
const width = Math.min(process.stdout.columns || 60, 72)
|
|
45
|
+
const text = `\u2500\u2500 ${title} `
|
|
46
|
+
const line = text + '\u2500'.repeat(Math.max(0, width - text.length + 1))
|
|
47
|
+
console.log(tag(line))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function step(message) {
|
|
51
|
+
const text = tag(`${icons.progress} ${message}...`)
|
|
52
|
+
if (!_quiet) process.stdout.write(text)
|
|
53
|
+
return (ok = true) => {
|
|
54
|
+
const icon = ok ? icons.success : icons.error
|
|
55
|
+
const doneText = tag(`${icon} ${message}`)
|
|
56
|
+
if (_quiet && !ok) {
|
|
57
|
+
console.warn(doneText)
|
|
58
|
+
} else if (!_quiet) {
|
|
59
|
+
process.stdout.write(`\r${doneText}\n`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function success(message) {
|
|
65
|
+
console.log(tag(`${icons.success} ${message}`))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function warning(message) {
|
|
69
|
+
console.warn(tag(`${icons.warning}${message}`))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function error(message) {
|
|
73
|
+
console.error(tag(`${icons.error} ${message}`))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function info(message) {
|
|
77
|
+
if (_quiet) return
|
|
78
|
+
console.log(tag(`${icons.info}${message}`))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function bullet(text, indent = 0) {
|
|
82
|
+
if (_quiet) return
|
|
83
|
+
const pad = ' '.repeat(indent)
|
|
84
|
+
console.log(tag(`${pad}${icons.bullet} ${text}`))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function summaryTable(items) {
|
|
88
|
+
if (_quiet || items.length === 0) return
|
|
89
|
+
|
|
90
|
+
const nameWidth = Math.max(...items.map((i) => i.name.length), 15)
|
|
91
|
+
const statusWidth = Math.max(...items.map((i) => String(i.status).length), 8)
|
|
92
|
+
|
|
93
|
+
const top = `\u250c${'\u2500'.repeat(nameWidth + 2)}\u252c${'\u2500'.repeat(statusWidth + 2)}\u2510`
|
|
94
|
+
const sep = `\u251c${'\u2500'.repeat(nameWidth + 2)}\u253c${'\u2500'.repeat(statusWidth + 2)}\u2524`
|
|
95
|
+
const bot = `\u2514${'\u2500'.repeat(nameWidth + 2)}\u2534${'\u2500'.repeat(statusWidth + 2)}\u2518`
|
|
96
|
+
|
|
97
|
+
console.log(tag(top))
|
|
98
|
+
console.log(tag(`\u2502 ${'Componente'.padEnd(nameWidth)} \u2502 ${'Status'.padEnd(statusWidth)} \u2502`))
|
|
99
|
+
console.log(tag(sep))
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
console.log(tag(`\u2502 ${item.name.padEnd(nameWidth)} \u2502 ${String(item.status).padEnd(statusWidth)} \u2502`))
|
|
102
|
+
}
|
|
103
|
+
console.log(tag(bot))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function spinner(message) {
|
|
107
|
+
const frames = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
|
|
108
|
+
let i = 0
|
|
109
|
+
let running = true
|
|
110
|
+
|
|
111
|
+
if (tty() && !_quiet) {
|
|
112
|
+
const interval = setInterval(() => {
|
|
113
|
+
if (!running) { clearInterval(interval); return }
|
|
114
|
+
process.stdout.write(`\r${frames[i % frames.length]} ${tag(message)}...`)
|
|
115
|
+
i++
|
|
116
|
+
}, 80)
|
|
117
|
+
process.stdout.write(`\r${frames[0]} ${tag(message)}...`)
|
|
118
|
+
|
|
119
|
+
return (ok = true) => {
|
|
120
|
+
running = false
|
|
121
|
+
clearInterval(interval)
|
|
122
|
+
const icon = ok ? icons.success : icons.error
|
|
123
|
+
process.stdout.write(`\r${icon} ${tag(message)}\n`)
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
const text = tag(`${icons.progress} ${message}...`)
|
|
127
|
+
if (!_quiet) process.stdout.write(text)
|
|
128
|
+
return (ok = true) => {
|
|
129
|
+
const icon = ok ? icons.success : icons.error
|
|
130
|
+
const doneText = tag(`${icon} ${message}`)
|
|
131
|
+
if (_quiet && !ok) {
|
|
132
|
+
console.warn(doneText)
|
|
133
|
+
} else if (!_quiet) {
|
|
134
|
+
process.stdout.write(`\r${doneText}\n`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function printSummary(target, platforms, stats) {
|
|
141
|
+
if (_quiet) return
|
|
142
|
+
|
|
143
|
+
const line = '\u2500'.repeat(Math.min(process.stdout.columns || 60, 60))
|
|
144
|
+
console.log(tag(`${icons.rocket} OpenCode instalado em ${target}`))
|
|
145
|
+
console.log(tag(line))
|
|
146
|
+
console.log('')
|
|
147
|
+
|
|
148
|
+
if (stats) {
|
|
149
|
+
console.log(tag(` Componentes:\t${stats.created} instalados, ${stats.skipped} pulados`))
|
|
150
|
+
if (stats.errors > 0) {
|
|
151
|
+
console.warn(tag(` ${icons.warning}${stats.errors} erro(s) encontrados`))
|
|
152
|
+
}
|
|
153
|
+
console.log('')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(tag(` ${icons.star} Próximos passos:`))
|
|
157
|
+
console.log(tag(` ${icons.bullet} Configure seus agentes em opencode.json`))
|
|
158
|
+
console.log(tag(` ${icons.bullet} Adicione MCP servers em mcp.json`))
|
|
159
|
+
console.log(tag(` ${icons.bullet} Rode 'opencode doctor' para verificar a instalação`))
|
|
160
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* component.mjs — Component resolution utilities for the OpenCode installer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Available component identifiers */
|
|
6
|
+
export const COMPONENT_NAMES = [
|
|
7
|
+
'agents',
|
|
8
|
+
'skills',
|
|
9
|
+
'instructions',
|
|
10
|
+
'prompts',
|
|
11
|
+
'commands',
|
|
12
|
+
'plugins',
|
|
13
|
+
'runtime',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse --components flag into a Set.
|
|
18
|
+
* @param {string|string[]} spec - Comma-separated string or array of component names
|
|
19
|
+
* @returns {Set<string>}
|
|
20
|
+
*/
|
|
21
|
+
export function resolveComponents(spec) {
|
|
22
|
+
if (Array.isArray(spec)) return new Set(spec)
|
|
23
|
+
return new Set(spec.split(',').map((s) => s.trim()))
|
|
24
|
+
}
|
|
@@ -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
|
+
}
|