pantheon-opencode 1.6.0-beta.12 → 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/CHANGELOG.md CHANGED
@@ -18,6 +18,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
18
18
 
19
19
  ## ✅ Closed Issues
20
20
 
21
+ ## [v1.6.0-beta.13] - 2026-10-17
22
+
23
+ ## 🆕 What's New
24
+
25
+ - **Modernize interactive TTY setup** with `@clack/prompts` while keeping the
26
+ existing component, install-location, authorization, and worktree guidance
27
+ choices. Canceling the setup wizard leaves configuration unchanged; headless,
28
+ `--yes`, dry-run, and non-TTY behavior remain available.
29
+
30
+ ## 🐞 Fixed
31
+
32
+ - **Let npm finish on an unsupported Node runtime:** postinstall now warns and
33
+ skips Node-dependent TUI/config sync, leaving the CLI to show its explicit
34
+ Node >=26.4.0 hard gate and user-managed upgrade instructions.
35
+ - **Synchronize Beta 13 package and release metadata** across manifests and
36
+ lockfiles without changing the Node >=26.4.0 engine floor.
37
+
21
38
  ## [v1.6.0-beta.12] - 2026-10-17
22
39
 
23
40
  ## 🆕 What's New
package/README.md CHANGED
@@ -43,6 +43,18 @@ asserted here. The optional `pantheon_cost` SQLite backend has its own capabilit
43
43
  threshold, `node:sqlite` on Node >= 22.5; that feature threshold does not lower
44
44
  Pantheon's package or CLI support floor.
45
45
 
46
+ With npm's default configuration, an unsupported Node version may produce an
47
+ `EBADENGINE` warning during install. Pantheon's postinstall then skips Node-based
48
+ sync without changing OpenCode configuration, and the CLI itself exits with a
49
+ clear error before setup. If needed, switch using a user-managed version
50
+ manager—no system-wide install is required:
51
+
52
+ ```bash
53
+ nvm install 26.4.0 && nvm use 26.4.0
54
+ # or: fnm install 26.4.0 && fnm use 26.4.0
55
+ # or: volta install node@26.4.0
56
+ ```
57
+
46
58
  From the project where you want to use Pantheon:
47
59
 
48
60
  ```bash
@@ -50,7 +62,10 @@ npx pantheon-opencode init
50
62
  opencode
51
63
  ```
52
64
 
53
- The installer guides you through the available setup. For optional MCP servers,
65
+ In a TTY, the interactive setup uses Clack prompts to choose components and a
66
+ global or project location before reviewing the apply. Canceling writes nothing.
67
+ `--yes`, `--headless`, `--dry-run`, and non-TTY workflows retain their existing
68
+ behavior. The installer guides you through the available setup. For optional MCP servers,
54
69
  project-local installation, or non-interactive setup, see the
55
70
  [installation guide](docs/INSTALLATION.md).
56
71
 
package/README.pt-BR.md CHANGED
@@ -33,6 +33,8 @@ de planejar o trabalho, avançar, conferir resultados e preservar o contexto
33
33
 
34
34
  Requisitos: [OpenCode 1.18.4+](https://opencode.ai/docs/) e Node.js `>=26.4.0` para a CLI, o instalador e as ferramentas Node do Pantheon. Node 22, Node 24 e Node 26.0–26.3 não são compatíveis com esta versão.
35
35
 
36
+ Com a configuração padrão do npm, versões incompatíveis podem gerar um aviso `EBADENGINE`; o postinstall pula a sincronização Node/config, e a CLI bloqueia a configuração antes de gravar arquivos. Para atualizar sem instalação de sistema, use um gerenciador de Node do usuário: `nvm install 26.4.0 && nvm use 26.4.0`, `fnm install 26.4.0 && fnm use 26.4.0` ou `volta install node@26.4.0`.
37
+
36
38
  Esse requisito do pacote não significa que todo host OpenCode execute Node: o OpenCode V2.0.31 oficial oferece um caminho de runtime Bun e um build Node separado fixado em Node 26.4.0. Não há aqui um requisito mínimo para Bun. O backend SQLite opcional de `pantheon_cost` tem um limite de capacidade distinto (`node:sqlite` requer Node >=22.5), que não reduz o requisito de suporte do pacote.
37
39
 
38
40
  No projeto em que você quer usar o Pantheon:
@@ -77,7 +79,7 @@ verificações repetíveis e passagens claras entre etapas do trabalho.
77
79
 
78
80
  ## Status
79
81
 
80
- Versão operacional neste checkout: **v1.6.0-beta.12** (esta página não afirma publicação). O Pantheon foi feito para OpenCode e depende da
82
+ Versão operacional neste checkout: **v1.6.0-beta.13** (esta página não afirma publicação). O Pantheon foi feito para OpenCode e depende da
81
83
  disponibilidade e da configuração do OpenCode e dos serviços opcionais que você
82
84
  escolher. Veja as [releases](https://github.com/ils15/pantheon-opencode/releases)
83
85
  e o [changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
@@ -26,9 +26,10 @@ import {
26
26
  loadPlanArtifact,
27
27
  savePlanArtifact,
28
28
  } from '../scripts/install/plan-artifact.mjs'
29
+ import { PROMPT_CANCELED } from '../scripts/install/prompt-result.mjs'
29
30
  import {
30
31
  isSupportedNodeVersion,
31
- MIN_SUPPORTED_NODE_VERSION,
32
+ unsupportedNodeVersionMessage,
32
33
  } from '../scripts/node-engine-policy.mjs'
33
34
  import { runUpdate } from '../scripts/update.mjs'
34
35
 
@@ -36,10 +37,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
36
37
  const ROOT = path.resolve(__dirname, '..')
37
38
 
38
39
  if (!isSupportedNodeVersion(process.versions.node)) {
39
- console.error(
40
- `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${process.versions.node})`,
41
- )
42
- console.error(' Install a newer Node via nvm: https://github.com/nvm-sh/nvm')
40
+ console.error(unsupportedNodeVersionMessage(process.versions.node))
43
41
  process.exit(1)
44
42
  }
45
43
 
@@ -111,14 +109,8 @@ async function authorizeReviewedPlan(plan) {
111
109
  for (const operation of operations) {
112
110
  console.log(` ${operation.action.padEnd(9)} ${operation.path}`)
113
111
  }
114
- const readline = await import('node:readline/promises')
115
- const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })
116
- try {
117
- const answer = await prompt.question('Apply this reviewed plan? [y/N] ')
118
- return ['y', 'yes'].includes(answer.trim().toLowerCase())
119
- } finally {
120
- prompt.close()
121
- }
112
+ const { promptReview } = await import('../scripts/install/interactive.mjs')
113
+ return promptReview('Apply this reviewed plan?', { default: false })
122
114
  }
123
115
 
124
116
  async function main() {
@@ -490,8 +482,14 @@ async function main() {
490
482
 
491
483
  if (reviewedPlan && !autoYes) {
492
484
  if (!forceHeadless && process.stdin.isTTY && process.stdout.isTTY) {
493
- if (!(await authorizeReviewedPlan(reviewedPlan))) {
485
+ const authorization = await authorizeReviewedPlan(reviewedPlan)
486
+ if (authorization === PROMPT_CANCELED) {
494
487
  console.log('❌ Install canceled; no files were written.')
488
+ process.exitCode = 130
489
+ return
490
+ }
491
+ if (!authorization) {
492
+ console.log('❌ Install declined; no files were written.')
495
493
  return
496
494
  }
497
495
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.6.0-beta.12",
3
+ "version": "1.6.0-beta.13",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -99,6 +99,7 @@
99
99
  "!**/*.pyc"
100
100
  ],
101
101
  "dependencies": {
102
+ "@clack/prompts": "1.8.1",
102
103
  "@opencode-ai/plugin": "1.18.33",
103
104
  "js-yaml": "^5.4.2",
104
105
  "zod": "4.1.8",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.6.0-beta.12",
4
+ "version": "1.6.0-beta.13",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 20 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.6.0-beta.12"
7
+ version = "1.6.0-beta.13"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -1,420 +1,98 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { emitKeypressEvents } from 'node:readline'
4
- import { colors } from './cli-ui.mjs'
3
+ import * as clack from '@clack/prompts'
4
+ import { PROMPT_CANCELED } from './prompt-result.mjs'
5
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')
6
+ export { PROMPT_CANCELED }
10
7
 
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
8
+ function isTerminal() {
9
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY)
22
10
  }
23
11
 
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
- }
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
- export async function promptMultiSelect(
35
- options,
36
- { title = '', selected = new Set(), pageSize = 6 } = {},
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 (options.length === 0) return new Set()
39
-
40
- const preSelected = selected instanceof Set ? selected : new Set(selected)
41
- const items = options.map((o) => ({
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
- * Apply-gate prompt. Unlike the pre-install confirm this defaults to NO, so a
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} planSummary e.g. "Apply these 12 changes?"
218
- * @param {{ default?: boolean }} [opts]
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 promptReview(planSummary, { default: def = false } = {}) {
222
- return promptConfirm(planSummary, { default: def })
223
- }
224
-
225
- export async function promptList(options, { title = '' } = {}) {
226
- if (options.length === 0) throw new Error('No options provided')
227
- if (options.length === 1) return options[0].value
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
- const lineCount = lines.length
278
- if (previousLineCount > 0) cursorUp(previousLineCount)
279
- clearDown()
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
- export async function runInteractiveInstall({ components = [], defaultComponents = [] } = {}) {
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
- name: 'agents',
340
- value: 'agents',
341
- description: `Agent configurations and routing (${14} agents)`,
342
- },
343
- {
344
- name: 'skills',
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
- process.stdout.write('\n')
376
- process.stdout.write(`${colors.bold(colors.cyan('📦 OpenCode Installer'))}\n\n`)
377
-
378
- const selectedComponents = await promptMultiSelect(componentOptions, {
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
- process.stdout.write('\n')
385
-
73
+ const selectedComponents = new Set(chosen)
386
74
  if (selectedComponents.size === 0) {
387
- process.stdout.write(`${colors.yellow('No components selected — using defaults\n')}`)
388
- defaultSet.forEach((v) => {
389
- selectedComponents.add(v)
390
- })
75
+ for (const value of defaults) selectedComponents.add(value)
391
76
  }
392
77
 
393
- const targetOptions = [
394
- {
395
- name: 'Global directory',
396
- value: 'global',
397
- description: '~/.config/opencode (shared across projects)',
398
- },
399
- {
400
- name: 'Current project',
401
- value: 'project',
402
- description: './.opencode (scoped to this project)',
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
- process.stdout.write('\n')
411
-
412
- const confirmed = await promptConfirm(
413
- `Install ${selectedComponents.size} component(s) in ${target === 'global' ? 'global config' : 'current project'}?`,
414
- { default: true },
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
  }
@@ -63,6 +63,7 @@ import {
63
63
  } from './plan.mjs'
64
64
  import { findPlanDrift } from './plan-artifact.mjs'
65
65
  import { copyPluginFiles, installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
66
+ import { PROMPT_CANCELED } from './prompt-result.mjs'
66
67
  import {
67
68
  checkRuntimePrerequisites,
68
69
  collectSkillNames,
@@ -1152,8 +1153,15 @@ async function installPass(
1152
1153
  const result = await runInteractiveInstall({
1153
1154
  components,
1154
1155
  defaultComponents: COMPONENT_NAMES,
1156
+ prompts: opts.deps?.prompts,
1157
+ isTTY: opts.deps?.isTTY,
1155
1158
  })
1156
1159
 
1160
+ if (result.canceled) {
1161
+ info('Installation canceled.')
1162
+ return { ...stats, applied: false, reason: 'canceled' }
1163
+ }
1164
+
1157
1165
  if (!result.confirmed && !autoYes) {
1158
1166
  info('Installation canceled.')
1159
1167
  return { ...stats, applied: false, reason: 'canceled' }
@@ -1266,7 +1274,13 @@ async function installPass(
1266
1274
  const ask = typeof injected === 'function' ? injected : promptReview
1267
1275
  const accepted = await ask(`Apply these ${planned?.planned ?? 0} changes?`, {
1268
1276
  default: false,
1277
+ prompts: opts.deps?.prompts,
1278
+ isTTY: opts.deps?.isTTY,
1269
1279
  })
1280
+ if (accepted === PROMPT_CANCELED) {
1281
+ info(S.nothingApplied)
1282
+ return { ...stats, applied: false, reason: 'canceled' }
1283
+ }
1270
1284
  if (!accepted) {
1271
1285
  info(S.nothingApplied)
1272
1286
  return { ...stats, applied: false, reason: 'declined' }
@@ -0,0 +1,2 @@
1
+ /** Sentinel returned when the user cancels an interactive authorization prompt. */
2
+ export const PROMPT_CANCELED = Symbol('pantheon-prompt-canceled')
@@ -19,12 +19,14 @@ import {
19
19
  } from 'node:fs'
20
20
  import { dirname, join } from 'node:path'
21
21
  import { fileURLToPath } from 'node:url'
22
- import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_VERSION } from '../node-engine-policy.mjs'
22
+ import {
23
+ isSupportedNodeVersion,
24
+ MIN_SUPPORTED_NODE_VERSION,
25
+ unsupportedNodeVersionMessage,
26
+ } from '../node-engine-policy.mjs'
23
27
 
24
28
  if (!isSupportedNodeVersion(process.versions.node)) {
25
- console.error(
26
- `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${process.versions.node})`,
27
- )
29
+ console.error(unsupportedNodeVersionMessage(process.versions.node))
28
30
  process.exit(1)
29
31
  }
30
32
 
@@ -4,6 +4,17 @@ import { join } from 'node:path'
4
4
  export const MIN_SUPPORTED_NODE_VERSION = '26.4.0'
5
5
  export const REQUIRED_NODE_ENGINE_RANGE = `>=${MIN_SUPPORTED_NODE_VERSION}`
6
6
 
7
+ /** Return actionable, user-managed upgrade guidance for an unsupported runtime. */
8
+ export function unsupportedNodeVersionMessage(version) {
9
+ return [
10
+ `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${version})`,
11
+ ' Install and switch with a user-managed Node version manager:',
12
+ ` nvm install ${MIN_SUPPORTED_NODE_VERSION} && nvm use ${MIN_SUPPORTED_NODE_VERSION}`,
13
+ ` fnm install ${MIN_SUPPORTED_NODE_VERSION} && fnm use ${MIN_SUPPORTED_NODE_VERSION}`,
14
+ ` volta install node@${MIN_SUPPORTED_NODE_VERSION}`,
15
+ ].join('\n')
16
+ }
17
+
7
18
  function parseNodeVersion(version) {
8
19
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+([\w.-]+))?$/.exec(String(version))
9
20
  if (!match) return null
@@ -5,36 +5,37 @@
5
5
  * Runs after `npm install` completes. Validates environment and prints
6
6
  * setup instructions. Does NOT install Pantheon platform components.
7
7
  */
8
- import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_VERSION } from './node-engine-policy.mjs'
8
+ import { isSupportedNodeVersion, unsupportedNodeVersionMessage } from './node-engine-policy.mjs'
9
9
 
10
10
  try {
11
- // Validate Node.js version
11
+ // npm may warn about `engines.node`, but must still finish installing so the
12
+ // bin can print the actionable hard-gate message. No Node-dependent setup is
13
+ // attempted from an incompatible lifecycle runtime.
12
14
  if (!isSupportedNodeVersion(process.versions.node)) {
13
- console.error(
14
- `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${process.versions.node})`,
15
+ console.warn(
16
+ `⚠️ Pantheon postinstall skipped because this runtime is unsupported.\n${unsupportedNodeVersionMessage(process.versions.node)}\n Switch to Node.js >= 26.4.0 before running Pantheon setup.`,
15
17
  )
16
- process.exit(1)
17
- }
18
+ } else {
19
+ // Validate js-yaml is available
20
+ try {
21
+ await import('js-yaml')
22
+ } catch {
23
+ console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
24
+ }
18
25
 
19
- // Validate js-yaml is available
20
- try {
21
- await import('js-yaml')
22
- } catch {
23
- console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
26
+ console.log('')
27
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
28
+ console.log(' Pantheon OpenCode — npm dependencies installed ✅')
29
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
30
+ console.log('')
31
+ console.log(' Next steps:')
32
+ console.log(' 1. Configure Pantheon:')
33
+ console.log(' npm run setup')
34
+ console.log(' # or: npx pantheon-opencode init')
35
+ console.log(' 2. Verify installation:')
36
+ console.log(' npm run doctor')
37
+ console.log('')
24
38
  }
25
-
26
- console.log('')
27
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
28
- console.log(' Pantheon OpenCode — npm dependencies installed ✅')
29
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
30
- console.log('')
31
- console.log(' Next steps:')
32
- console.log(' 1. Configure Pantheon:')
33
- console.log(' npm run setup')
34
- console.log(' # or: npx pantheon-opencode init')
35
- console.log(' 2. Verify installation:')
36
- console.log(' npm run doctor')
37
- console.log('')
38
39
  } catch (err) {
39
40
  console.error(`❌ Post-install check failed: ${err.message}`)
40
41
  process.exit(1)
@@ -15,6 +15,7 @@ import { homedir } from 'node:os'
15
15
  import { join } from 'node:path'
16
16
  import { fileURLToPath, pathToFileURL } from 'node:url'
17
17
  import { resolveOpenCodeVersion } from './install/opencode-version.mjs'
18
+ import { isSupportedNodeVersion } from './node-engine-policy.mjs'
18
19
 
19
20
  const __dirname = fileURLToPath(new URL('.', import.meta.url))
20
21
  const ROOT = join(__dirname, '..')
@@ -257,7 +258,10 @@ async function main() {
257
258
  const isDirectRun =
258
259
  Boolean(process.argv[1]) && pathToFileURL(process.argv[1]).href === import.meta.url
259
260
 
260
- if (isDirectRun) {
261
+ if (isDirectRun && !isSupportedNodeVersion(process.versions.node)) {
262
+ // postinstall.mjs already reports the engine mismatch. Do not call npm ci or
263
+ // touch the user's config with an incompatible lifecycle runtime.
264
+ } else if (isDirectRun) {
261
265
  main().catch((err) => {
262
266
  console.error(`❌ TUI sync failed: ${err.message}`)
263
267
  process.exitCode = 1
@@ -477,7 +477,7 @@ function Sidebar(props) {
477
477
  setProp(_el$, "width", "100%");
478
478
  setProp(_el$, "padding", 1);
479
479
  setProp(_el$2, "attributes", 1);
480
- insert(_el$2, `Pantheon v1.6.0-beta.12`);
480
+ insert(_el$2, `Pantheon v1.6.0-beta.13`);
481
481
  insertNode(_el$3, createTextNode(`OpenCode V2 · plugin TUI ativo`));
482
482
  insert(_el$, createComponent(Show, {
483
483
  get when() {
@@ -623,7 +623,7 @@ function HomeStatus(props) {
623
623
  const muted = () => props.context.theme?.text?.muted ?? props.context.theme?.text?.base;
624
624
  return (() => {
625
625
  var _el$20 = createElement("text");
626
- insert(_el$20, () => `Pantheon v1.6.0-beta.12${preset().name ? ` ⚡ ${preset().name}` : ""}`);
626
+ insert(_el$20, () => `Pantheon v1.6.0-beta.13${preset().name ? ` ⚡ ${preset().name}` : ""}`);
627
627
  effect((_$p) => setProp(_el$20, "fg", muted(), _$p));
628
628
  return _el$20;
629
629
  })();
@@ -167,7 +167,7 @@ async function detectVersion(api) {
167
167
  if (ver) return ver;
168
168
  }
169
169
  } catch {}
170
- return "1.6.0-beta.12";
170
+ return "1.6.0-beta.13";
171
171
  }
172
172
  /**
173
173
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
- "version": "1.6.0-beta.12",
3
+ "version": "1.6.0-beta.13",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pantheon-tui",
9
- "version": "1.6.0-beta.12",
9
+ "version": "1.6.0-beta.13",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@opencode-ai/plugin": "1.18.33",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon-tui",
3
3
  "type": "module",
4
- "version": "1.6.0-beta.12",
4
+ "version": "1.6.0-beta.13",
5
5
  "description": "Pantheon TUI plugin — V1 rich sidebar and usage gauges; native V2 status, session and delegation sidebar",
6
6
  "license": "MIT",
7
7
  "engines": {