pantheon-opencode 1.1.1-beta.3 → 1.1.1-beta.4
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/package.json +1 -1
- package/scripts/install/cli-ui.mjs +160 -0
- package/scripts/install/component.mjs +24 -0
- package/scripts/install/migrate-config.mjs +308 -0
- package/scripts/install/migrate.mjs +210 -30
- package/scripts/install/opencode.mjs +43 -77
- package/scripts/install/plugin.mjs +139 -0
- package/scripts/install/state.mjs +194 -0
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* node scripts/install/migrate.mjs --target ~/.config/opencode --force
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
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 { readState, writeState, createInitialState } from './state.mjs'
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
17
|
const ROOT = join(__dirname, '..', '..')
|
|
@@ -33,16 +34,9 @@ function readPackageVersion() {
|
|
|
33
34
|
* @returns {string|null} - version string or null for fresh install
|
|
34
35
|
*/
|
|
35
36
|
export function detectVersion(target) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
const state = JSON.parse(readFileSync(stateFile, 'utf8'))
|
|
40
|
-
return state.pantheon_version || state.version || 'unknown'
|
|
41
|
-
} catch {
|
|
42
|
-
/* corrupted state — treat as unknown */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return null // fresh install
|
|
37
|
+
const state = readState(target)
|
|
38
|
+
if (state) return state.pantheon_version || state.version || 'unknown'
|
|
39
|
+
return null
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
/**
|
|
@@ -96,9 +90,42 @@ export function runMigrations(target, currentVersion, { dryRun = false } = {}) {
|
|
|
96
90
|
semver[2] = 1
|
|
97
91
|
}
|
|
98
92
|
|
|
93
|
+
// ── Migration: v3.19.1 → v3.19.2 ──────────────────────────────────
|
|
94
|
+
// Upgrade state schema from v1 to v2
|
|
95
|
+
if (semver && lt(semver, [3, 19, 2])) {
|
|
96
|
+
applied.push({
|
|
97
|
+
from: formatSemver(semver),
|
|
98
|
+
to: '3.19.2',
|
|
99
|
+
description: 'Upgrade install state schema to v2 with component tracking',
|
|
100
|
+
})
|
|
101
|
+
semver[0] = 3
|
|
102
|
+
semver[1] = 19
|
|
103
|
+
semver[2] = 2
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
// ── Write updated state ───────────────────────────────────────────
|
|
100
107
|
if (applied.length > 0 && !dryRun) {
|
|
101
|
-
|
|
108
|
+
const newVersion = formatSemver(semver)
|
|
109
|
+
let state = readState(target)
|
|
110
|
+
if (!state) state = createInitialState(newVersion)
|
|
111
|
+
state.pantheon_version = newVersion
|
|
112
|
+
for (const m of applied) {
|
|
113
|
+
state.applied_migrations.push({
|
|
114
|
+
from: m.from,
|
|
115
|
+
to: m.to,
|
|
116
|
+
applied_at: new Date().toISOString(),
|
|
117
|
+
status: 'completed',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
if (!state.components) {
|
|
121
|
+
state.components = {
|
|
122
|
+
agents: { version: 1, status: 'installed' },
|
|
123
|
+
skills: { version: 1, status: 'installed' },
|
|
124
|
+
plugins: { version: 1, status: 'installed' },
|
|
125
|
+
runtime: { version: 1, status: 'installed' },
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
writeState(target, state)
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
return {
|
|
@@ -107,20 +134,150 @@ export function runMigrations(target, currentVersion, { dryRun = false } = {}) {
|
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Checkpoint system for crash recovery
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const CHECKPOINT_FILE = '.migration-checkpoint'
|
|
142
|
+
const STATE_DIR = '.pantheon'
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read the migration checkpoint file if it exists.
|
|
146
|
+
* @param {string} target
|
|
147
|
+
* @returns {object|null}
|
|
148
|
+
*/
|
|
149
|
+
export function readCheckpoint(target) {
|
|
150
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
151
|
+
if (!existsSync(cpFile)) return null
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(readFileSync(cpFile, 'utf8'))
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Write a migration checkpoint before starting migrations.
|
|
161
|
+
* @param {string} target
|
|
162
|
+
* @param {string} fromVersion
|
|
163
|
+
* @param {string} toVersion
|
|
164
|
+
* @param {string[]} steps - Ordered list of step names
|
|
165
|
+
* @returns {object} The checkpoint object
|
|
166
|
+
*/
|
|
167
|
+
export function writeCheckpoint(target, fromVersion, toVersion, steps) {
|
|
168
|
+
const cpDir = join(target, STATE_DIR)
|
|
169
|
+
if (!existsSync(cpDir)) mkdirSync(cpDir, { recursive: true })
|
|
170
|
+
const cpFile = join(cpDir, CHECKPOINT_FILE)
|
|
171
|
+
const checkpoint = {
|
|
172
|
+
from: fromVersion,
|
|
173
|
+
to: toVersion,
|
|
174
|
+
started_at: new Date().toISOString(),
|
|
175
|
+
steps_completed: [],
|
|
176
|
+
steps_pending: [...steps],
|
|
177
|
+
}
|
|
178
|
+
writeFileSync(cpFile, `${JSON.stringify(checkpoint, null, 2)}\n`, 'utf8')
|
|
179
|
+
return checkpoint
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Advance the checkpoint: mark a step as completed and remove from pending.
|
|
184
|
+
* @param {string} target
|
|
185
|
+
* @param {string} stepName
|
|
186
|
+
* @returns {object|null} Updated checkpoint, or null if none exists
|
|
187
|
+
*/
|
|
188
|
+
export function advanceCheckpoint(target, stepName) {
|
|
189
|
+
const cp = readCheckpoint(target)
|
|
190
|
+
if (!cp) return null
|
|
191
|
+
cp.steps_completed.push(stepName)
|
|
192
|
+
cp.steps_pending = cp.steps_pending.filter((s) => s !== stepName)
|
|
193
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
194
|
+
writeFileSync(cpFile, `${JSON.stringify(cp, null, 2)}\n`, 'utf8')
|
|
195
|
+
return cp
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove the checkpoint file on successful completion.
|
|
200
|
+
* @param {string} target
|
|
201
|
+
*/
|
|
202
|
+
export function removeCheckpoint(target) {
|
|
203
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
204
|
+
if (existsSync(cpFile)) {
|
|
205
|
+
rmSync(cpFile, { force: true })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resume from a pending checkpoint.
|
|
211
|
+
* Reads the checkpoint and reports what steps remain.
|
|
212
|
+
* @param {string} target
|
|
213
|
+
* @returns {object|null} The checkpoint, or null if none pending
|
|
214
|
+
*/
|
|
215
|
+
export function resumePendingMigration(target) {
|
|
216
|
+
const cp = readCheckpoint(target)
|
|
217
|
+
if (!cp) return null
|
|
218
|
+
console.log(` \u{1F504} Resuming migration from checkpoint: ${cp.from} \u2192 ${cp.to}`)
|
|
219
|
+
console.log(` Completed: ${cp.steps_completed.join(', ') || 'none'}`)
|
|
220
|
+
console.log(` Pending: ${cp.steps_pending.join(', ') || 'none'}`)
|
|
221
|
+
return cp
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Rollback support
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
110
228
|
/**
|
|
111
|
-
*
|
|
229
|
+
* Roll back the last completed migration for a given version.
|
|
230
|
+
* Each migration step should define a corresponding down-migration.
|
|
231
|
+
* This is a stub — actual down-migration logic should be registered
|
|
232
|
+
* in a DOWN_MIGRATIONS registry.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} target
|
|
235
|
+
* @param {string} version - The version to roll back from
|
|
236
|
+
* @returns {{ success: boolean, reason?: string, migration?: object }}
|
|
112
237
|
*/
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
238
|
+
export function rollbackMigration(target, version) {
|
|
239
|
+
const state = readState(target)
|
|
240
|
+
if (!state) {
|
|
241
|
+
console.log(' \u26A0\uFE0F No install state found — nothing to roll back')
|
|
242
|
+
return { success: false, reason: 'No install state' }
|
|
118
243
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
244
|
+
|
|
245
|
+
const migrations = state.applied_migrations || []
|
|
246
|
+
const targetMigration = migrations.find(
|
|
247
|
+
(m) => m.to === version && m.status === 'completed',
|
|
248
|
+
)
|
|
249
|
+
if (!targetMigration) {
|
|
250
|
+
console.log(` \u26A0\uFE0F No completed migration found for version ${version}`)
|
|
251
|
+
return { success: false, reason: 'Migration not found' }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(
|
|
255
|
+
` \u{1F504} Rolling back migration: ${targetMigration.from} \u2192 ${targetMigration.to}`,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
targetMigration.status = 'rolled_back'
|
|
259
|
+
state.pantheon_version = targetMigration.from
|
|
260
|
+
writeState(target, state)
|
|
261
|
+
|
|
262
|
+
console.log(` \u2705 Rolled back to ${targetMigration.from}`)
|
|
263
|
+
return { success: true, migration: targetMigration }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* List all applied migrations from the install state.
|
|
268
|
+
* @param {string} target
|
|
269
|
+
* @returns {{ currentVersion: string|null, previousVersion: string|null, migrations: object[] }}
|
|
270
|
+
*/
|
|
271
|
+
export function listAppliedMigrations(target) {
|
|
272
|
+
const state = readState(target)
|
|
273
|
+
if (!state) {
|
|
274
|
+
return { currentVersion: null, previousVersion: null, migrations: [] }
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
currentVersion: state.pantheon_version,
|
|
278
|
+
previousVersion: state.previous_version,
|
|
279
|
+
migrations: state.applied_migrations || [],
|
|
122
280
|
}
|
|
123
|
-
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
124
281
|
}
|
|
125
282
|
|
|
126
283
|
// ---------------------------------------------------------------------------
|
|
@@ -172,15 +329,38 @@ function main() {
|
|
|
172
329
|
const target = targetIdx !== -1 ? args[targetIdx + 1] : process.cwd()
|
|
173
330
|
const dryRun = args.includes('--dry-run')
|
|
174
331
|
const force = args.includes('--force')
|
|
332
|
+
const list = args.includes('--list')
|
|
333
|
+
|
|
334
|
+
if (list) {
|
|
335
|
+
const history = listAppliedMigrations(target)
|
|
336
|
+
if (!history.currentVersion) {
|
|
337
|
+
console.log(' \u2139\uFE0F No install state found in: ' + target)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
console.log(` Current version: ${history.currentVersion}`)
|
|
341
|
+
console.log(` Previous version: ${history.previousVersion || 'N/A'}`)
|
|
342
|
+
console.log(` Applied migrations: ${history.migrations.length}`)
|
|
343
|
+
for (const m of history.migrations) {
|
|
344
|
+
const icon = m.status === 'completed' ? '\u2705' : m.status === 'rolled_back' ? '\u{1F504}' : '\u26A0\uFE0F'
|
|
345
|
+
console.log(` ${icon} ${m.from} \u2192 ${m.to} [${m.status}] (${m.applied_at})`)
|
|
346
|
+
}
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const rollbackTo = args.find((a) => a.startsWith('--rollback='))?.split('=', 2)[1]
|
|
351
|
+
if (rollbackTo) {
|
|
352
|
+
rollbackMigration(target, rollbackTo)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
175
355
|
|
|
176
356
|
const currentVersion = detectVersion(target)
|
|
177
357
|
if (!currentVersion) {
|
|
178
|
-
// Fresh install — write state and exit
|
|
179
358
|
if (!dryRun) {
|
|
180
|
-
|
|
181
|
-
|
|
359
|
+
const state = createInitialState(PANTHON_VERSION)
|
|
360
|
+
writeState(target, state)
|
|
361
|
+
console.log(` \u2705 Fresh install — state written (${PANTHON_VERSION})`)
|
|
182
362
|
} else {
|
|
183
|
-
console.log(`
|
|
363
|
+
console.log(` \u2705 Fresh install — would write state (${PANTHON_VERSION})`)
|
|
184
364
|
}
|
|
185
365
|
return
|
|
186
366
|
}
|
|
@@ -188,19 +368,19 @@ function main() {
|
|
|
188
368
|
console.log(` Current version: ${currentVersion}`)
|
|
189
369
|
|
|
190
370
|
if (!force && currentVersion === PANTHON_VERSION) {
|
|
191
|
-
console.log('
|
|
371
|
+
console.log(' \u23ED\uFE0F Already at latest version — no migrations needed')
|
|
192
372
|
return
|
|
193
373
|
}
|
|
194
374
|
|
|
195
375
|
const migration = runMigrations(target, currentVersion, { dryRun })
|
|
196
376
|
|
|
197
377
|
if (migration.applied > 0) {
|
|
198
|
-
console.log(`
|
|
378
|
+
console.log(` \u2705 Applied ${migration.applied} migration(s):`)
|
|
199
379
|
for (const msg of migration.messages) {
|
|
200
|
-
console.log(`
|
|
380
|
+
console.log(` \u2022 ${msg}`)
|
|
201
381
|
}
|
|
202
382
|
} else {
|
|
203
|
-
console.log('
|
|
383
|
+
console.log(' \u23ED\uFE0F No applicable migrations')
|
|
204
384
|
}
|
|
205
385
|
}
|
|
206
386
|
|
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
* opencode.mjs — OpenCode platform installer
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { execSync } from 'node:child_process'
|
|
7
6
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
7
|
import { homedir } from 'node:os'
|
|
9
8
|
import { join, resolve } from 'node:path'
|
|
10
9
|
import { healthCheck } from './health-check.mjs'
|
|
11
10
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
11
|
+
import { configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
|
|
12
12
|
import {
|
|
13
13
|
collectSkillNames,
|
|
14
14
|
copyFiles,
|
|
15
15
|
installSkills,
|
|
16
|
-
PLATFORM_DIR,
|
|
17
16
|
parseFrontmatter,
|
|
18
17
|
ROOT,
|
|
19
18
|
sourceDirValid,
|
|
@@ -22,6 +21,8 @@ import {
|
|
|
22
21
|
writeIfChanged,
|
|
23
22
|
} from './shared.mjs'
|
|
24
23
|
import { setupVenv } from './venv.mjs'
|
|
24
|
+
import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
25
|
+
import { resolveComponents } from './component.mjs'
|
|
25
26
|
|
|
26
27
|
function deepClone(value) {
|
|
27
28
|
return JSON.parse(JSON.stringify(value))
|
|
@@ -94,21 +95,23 @@ export function installOpenCode(
|
|
|
94
95
|
const _subDir = isGlobal ? '' : '.opencode'
|
|
95
96
|
const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
|
|
96
97
|
|
|
98
|
+
configure({ dryRun })
|
|
99
|
+
|
|
97
100
|
if (isGlobal) {
|
|
98
|
-
|
|
99
|
-
' 🌐 Global config directory detected — using flat layout (agents/, skills/, commands/)',
|
|
100
|
-
)
|
|
101
|
+
info('Global config directory detected — using flat layout (agents/, skills/, commands/)')
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// -----------------------------------------------------------------------
|
|
104
105
|
// 1. Install agents (--components agents)
|
|
105
106
|
// -----------------------------------------------------------------------
|
|
106
107
|
if (componentSet.has('agents')) {
|
|
108
|
+
section('\uD83D\uDCE6 Agents')
|
|
107
109
|
const srcDir = join(ROOT, 'src', 'agents')
|
|
108
110
|
if (!sourceDirValid(srcDir)) {
|
|
109
|
-
|
|
111
|
+
warning(`Agent source directory not found: ${srcDir}`)
|
|
110
112
|
stats.errors++
|
|
111
113
|
} else {
|
|
114
|
+
const agentStep = step('Installing agents')
|
|
112
115
|
const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
|
|
113
116
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
114
117
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
@@ -120,6 +123,7 @@ export function installOpenCode(
|
|
|
120
123
|
const { created, skipped } = copyFiles(srcDir, dstDir, dryRun)
|
|
121
124
|
stats.created += created
|
|
122
125
|
stats.skipped += skipped
|
|
126
|
+
agentStep(true)
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -139,9 +143,10 @@ export function installOpenCode(
|
|
|
139
143
|
// 2. Install skills (--components skills)
|
|
140
144
|
// -----------------------------------------------------------------------
|
|
141
145
|
if (componentSet.has('skills')) {
|
|
146
|
+
section('\uD83D\uDCDA Skills')
|
|
142
147
|
const skillNames = collectSkillNames()
|
|
143
148
|
if (skillNames.length > 0) {
|
|
144
|
-
|
|
149
|
+
const skillStep = step(`Installing ${skillNames.length} skills`)
|
|
145
150
|
const dstSkillsDir = isGlobal ? join(target, 'skills') : join(target, '.opencode', 'skills')
|
|
146
151
|
if (clean && existsSync(dstSkillsDir) && !dryRun) {
|
|
147
152
|
const existing = readdirSync(dstSkillsDir)
|
|
@@ -158,6 +163,7 @@ export function installOpenCode(
|
|
|
158
163
|
)
|
|
159
164
|
stats.created += sCreated
|
|
160
165
|
stats.skipped += sSkipped
|
|
166
|
+
skillStep(true)
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -165,6 +171,7 @@ export function installOpenCode(
|
|
|
165
171
|
// 2.5 Install instructions: AGENTS.md + instructions/ (--components instructions)
|
|
166
172
|
// -----------------------------------------------------------------------
|
|
167
173
|
if (componentSet.has('instructions')) {
|
|
174
|
+
section('\uD83D\uDCCB Instructions')
|
|
168
175
|
// AGENTS.md
|
|
169
176
|
const srcAgentsMd = join(ROOT, 'AGENTS.md')
|
|
170
177
|
const dstAgentsMd = join(target, 'AGENTS.md')
|
|
@@ -188,6 +195,7 @@ export function installOpenCode(
|
|
|
188
195
|
// 2.6 Install prompts (--components prompts)
|
|
189
196
|
// -----------------------------------------------------------------------
|
|
190
197
|
if (componentSet.has('prompts')) {
|
|
198
|
+
section('\uD83D\uDCAC Prompts')
|
|
191
199
|
const srcPrompts = join(ROOT, 'prompts')
|
|
192
200
|
const dstPrompts = join(target, 'prompts')
|
|
193
201
|
if (existsSync(srcPrompts)) {
|
|
@@ -201,6 +209,7 @@ export function installOpenCode(
|
|
|
201
209
|
// 2.7 Install commands (--components commands)
|
|
202
210
|
// -----------------------------------------------------------------------
|
|
203
211
|
if (componentSet.has('commands')) {
|
|
212
|
+
section('\u26A1 Commands')
|
|
204
213
|
const srcCmds = join(ROOT, 'commands')
|
|
205
214
|
const dstCmds = isGlobal ? join(target, 'commands') : join(target, '.opencode', 'commands')
|
|
206
215
|
if (existsSync(srcCmds)) {
|
|
@@ -223,10 +232,10 @@ export function installOpenCode(
|
|
|
223
232
|
if (cfg.subagent_depth === undefined) { cfg.subagent_depth = 2; changed = true }
|
|
224
233
|
if (changed && !dryRun) {
|
|
225
234
|
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
|
|
226
|
-
|
|
235
|
+
success('subagent_depth: 2 added to opencode.json')
|
|
227
236
|
}
|
|
228
237
|
} catch (e) {
|
|
229
|
-
|
|
238
|
+
warning(`Could not update opencode.json: ${e.message}`)
|
|
230
239
|
}
|
|
231
240
|
}
|
|
232
241
|
}
|
|
@@ -234,46 +243,15 @@ export function installOpenCode(
|
|
|
234
243
|
// 2.8 Install TUI plugins (--components plugins)
|
|
235
244
|
// -----------------------------------------------------------------------
|
|
236
245
|
if (componentSet.has('plugins')) {
|
|
246
|
+
section('\uD83D\uDD0C Plugins')
|
|
237
247
|
const srcPluginDir = join(ROOT, 'src', 'plugins', 'tui')
|
|
238
248
|
const dstPluginDir = isGlobal
|
|
239
249
|
? join(target, 'plugins', 'pantheon-tui')
|
|
240
250
|
: join(target, '.opencode', 'plugins', 'pantheon-tui')
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
if (!dryRun) mkdirSync(join(dstPluginDir, "dist"), { recursive: true })
|
|
247
|
-
// Copy src/index.tsx -> index.tsx
|
|
248
|
-
const srcIdx = join(srcPluginDir, "src", "index.tsx")
|
|
249
|
-
if (existsSync(srcIdx)) {
|
|
250
|
-
writeIfChanged(join(dstPluginDir, "index.tsx"), readFileSync(srcIdx, "utf8"), dryRun)
|
|
251
|
-
stats.created++
|
|
252
|
-
}
|
|
253
|
-
// Copy dist files
|
|
254
|
-
const distSrc = join(srcPluginDir, "dist")
|
|
255
|
-
if (existsSync(distSrc)) {
|
|
256
|
-
for (const f of readdirSync(distSrc)) {
|
|
257
|
-
const c = readFileSync(join(distSrc, f), "utf8")
|
|
258
|
-
writeIfChanged(join(dstPluginDir, "dist", f), c, dryRun)
|
|
259
|
-
stats.created++
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// Copy package.json
|
|
263
|
-
const pkgSrc = join(srcPluginDir, "package.json")
|
|
264
|
-
if (existsSync(pkgSrc)) {
|
|
265
|
-
writeIfChanged(join(dstPluginDir, "package.json"), readFileSync(pkgSrc, "utf8"), dryRun)
|
|
266
|
-
stats.created++
|
|
267
|
-
}
|
|
268
|
-
// Install plugin dependencies (@opentui/core, @opentui/solid, solid-js)
|
|
269
|
-
if (!dryRun) {
|
|
270
|
-
try {
|
|
271
|
-
execSync('npm install --omit=dev --no-audit', { cwd: dstPluginDir, stdio: 'pipe' })
|
|
272
|
-
} catch (e) {
|
|
273
|
-
console.warn(' ⚠️ Failed to install TUI plugin dependencies:', e.message)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
251
|
+
const pluginStats = installPlugin(srcPluginDir, dstPluginDir, { dryRun, clean })
|
|
252
|
+
stats.created += pluginStats.created
|
|
253
|
+
stats.skipped += pluginStats.skipped
|
|
254
|
+
stats.errors += pluginStats.errors
|
|
277
255
|
}
|
|
278
256
|
|
|
279
257
|
// -----------------------------------------------------------------------
|
|
@@ -282,30 +260,9 @@ export function installOpenCode(
|
|
|
282
260
|
const targetTuiConfigPath = isGlobal
|
|
283
261
|
? join(target, 'tui.json')
|
|
284
262
|
: join(target, '.opencode', 'tui.json')
|
|
285
|
-
let tuiConfig = { plugin: [] }
|
|
286
|
-
if (existsSync(targetTuiConfigPath)) {
|
|
287
|
-
try {
|
|
288
|
-
tuiConfig = JSON.parse(readFileSync(targetTuiConfigPath, 'utf8'))
|
|
289
|
-
} catch {
|
|
290
|
-
/* use default */
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (!Array.isArray(tuiConfig.plugin)) {
|
|
294
|
-
tuiConfig.plugin = []
|
|
295
|
-
}
|
|
296
|
-
// Remove stale plugin refs (old dist/tui.tsx path)
|
|
297
|
-
const staleRefs = ["plugins/pantheon-tui/dist/tui.tsx", "plugins/pantheon-tui/dist/tui.js"]
|
|
298
|
-
for (const stale of staleRefs) {
|
|
299
|
-
const idx = tuiConfig.plugin.indexOf(stale)
|
|
300
|
-
if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
|
|
301
|
-
}
|
|
302
|
-
// Add our plugin if not already present
|
|
303
263
|
const pluginRef = 'plugins/pantheon-tui'
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
|
|
308
|
-
const tuiStatus = writeIfChanged(targetTuiConfigPath, tuiContent, dryRun)
|
|
264
|
+
unregisterPlugin(targetTuiConfigPath, pluginRef, { dryRun })
|
|
265
|
+
const tuiStatus = registerPlugin(targetTuiConfigPath, pluginRef, { dryRun })
|
|
309
266
|
if (tuiStatus === 'created') stats.created++
|
|
310
267
|
else stats.skipped++
|
|
311
268
|
|
|
@@ -314,6 +271,7 @@ export function installOpenCode(
|
|
|
314
271
|
// MCP server scripts, code-mode scripts, tiers.json
|
|
315
272
|
// -----------------------------------------------------------------------
|
|
316
273
|
if (componentSet.has('runtime')) {
|
|
274
|
+
section('\u2699\uFE0F Runtime')
|
|
317
275
|
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
318
276
|
|
|
319
277
|
// ── MCP server scripts ──
|
|
@@ -331,7 +289,7 @@ export function installOpenCode(
|
|
|
331
289
|
for (const script of mcpScripts) {
|
|
332
290
|
const src = join(srcScriptsDir, script)
|
|
333
291
|
if (!existsSync(src)) {
|
|
334
|
-
|
|
292
|
+
warning(`Script not found: ${src}`)
|
|
335
293
|
stats.errors++
|
|
336
294
|
continue
|
|
337
295
|
}
|
|
@@ -683,9 +641,9 @@ export function installOpenCode(
|
|
|
683
641
|
if (currentVersion) {
|
|
684
642
|
const migration = runMigrations(target, currentVersion, { dryRun })
|
|
685
643
|
if (migration.applied > 0) {
|
|
686
|
-
|
|
644
|
+
success(`Applied ${migration.applied} migration(s)`)
|
|
687
645
|
for (const msg of migration.messages) {
|
|
688
|
-
|
|
646
|
+
bullet(msg, 1)
|
|
689
647
|
}
|
|
690
648
|
}
|
|
691
649
|
}
|
|
@@ -696,22 +654,30 @@ export function installOpenCode(
|
|
|
696
654
|
// -----------------------------------------------------------------------
|
|
697
655
|
if (componentSet.has('runtime')) {
|
|
698
656
|
try {
|
|
657
|
+
const venvSpinner = spinner('Setting up Python virtual environment')
|
|
699
658
|
setupVenv(target, { dryRun, force: clean })
|
|
659
|
+
venvSpinner(true)
|
|
700
660
|
const health = healthCheck(target, { dryRun })
|
|
701
661
|
|
|
702
662
|
// Print health summary
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
for (const
|
|
706
|
-
for (const
|
|
663
|
+
section('\uD83D\uDD0D Health Check')
|
|
664
|
+
const healthStep = step('Running health checks')
|
|
665
|
+
for (const p of health.passed) success(`${p.check}: ${p.detail}`)
|
|
666
|
+
for (const w of health.warnings) warning(`${w.check}: ${w.detail}`)
|
|
667
|
+
for (const f of health.failed) error(`${f.check}: ${f.detail}`)
|
|
707
668
|
|
|
708
669
|
if (health.failed.length > 0) {
|
|
709
|
-
|
|
670
|
+
healthStep(false)
|
|
671
|
+
warning(`${health.failed.length} check(s) failed — review above`)
|
|
710
672
|
stats.errors += health.failed.length
|
|
673
|
+
} else {
|
|
674
|
+
healthStep(true)
|
|
711
675
|
}
|
|
712
676
|
} catch (err) {
|
|
713
|
-
|
|
677
|
+
error(`Setup failed: ${err.message}`)
|
|
714
678
|
throw err // Fatal — abort installation
|
|
715
679
|
}
|
|
716
680
|
}
|
|
681
|
+
|
|
682
|
+
printSummary(target, ['opencode'], stats)
|
|
717
683
|
}
|