pantheon-opencode 1.1.1-beta.1 → 1.1.1-beta.11

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.
@@ -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 { colors, 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,10 @@ 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'
26
+
27
+ const COMPONENT_NAMES = ['agents', 'skills', 'instructions', 'prompts', 'commands', 'plugins', 'runtime']
25
28
 
26
29
  function deepClone(value) {
27
30
  return JSON.parse(JSON.stringify(value))
@@ -71,11 +74,12 @@ function isGlobalConfigDir(target) {
71
74
  return resolve(target) === xdgDir
72
75
  }
73
76
 
74
- export function installOpenCode(
77
+ export async function installOpenCode(
75
78
  target,
76
79
  dryRun = false,
77
80
  clean = false,
78
81
  components = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'],
82
+ opts = {},
79
83
  ) {
80
84
  // Default target if not provided (global ~/.config/opencode)
81
85
  if (!target) {
@@ -85,6 +89,35 @@ export function installOpenCode(
85
89
  const componentSet = new Set(components)
86
90
  const stats = summary.opencode
87
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
+
88
121
  // Determine layout based on install scope.
89
122
  // Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
90
123
  // agents/ skills/ commands/ plugins/
@@ -94,21 +127,23 @@ export function installOpenCode(
94
127
  const _subDir = isGlobal ? '' : '.opencode'
95
128
  const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
96
129
 
130
+ configure({ dryRun })
131
+
97
132
  if (isGlobal) {
98
- console.log(
99
- ' 🌐 Global config directory detected — using flat layout (agents/, skills/, commands/)',
100
- )
133
+ info('Global config directory detected — using flat layout (agents/, skills/, commands/)')
101
134
  }
102
135
 
103
136
  // -----------------------------------------------------------------------
104
137
  // 1. Install agents (--components agents)
105
138
  // -----------------------------------------------------------------------
106
139
  if (componentSet.has('agents')) {
140
+ section('\uD83D\uDCE6 Agents')
107
141
  const srcDir = join(ROOT, 'src', 'agents')
108
142
  if (!sourceDirValid(srcDir)) {
109
- console.warn(` ⚠️ Agent source directory not found: ${srcDir}`)
143
+ warning(`Agent source directory not found: ${srcDir}`)
110
144
  stats.errors++
111
145
  } else {
146
+ const agentStep = step('Installing agents')
112
147
  const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
113
148
  if (!dryRun) mkdirSync(dstDir, { recursive: true })
114
149
  if (clean && existsSync(dstDir) && !dryRun) {
@@ -120,6 +155,7 @@ export function installOpenCode(
120
155
  const { created, skipped } = copyFiles(srcDir, dstDir, dryRun)
121
156
  stats.created += created
122
157
  stats.skipped += skipped
158
+ agentStep(true)
123
159
  }
124
160
  }
125
161
 
@@ -139,9 +175,10 @@ export function installOpenCode(
139
175
  // 2. Install skills (--components skills)
140
176
  // -----------------------------------------------------------------------
141
177
  if (componentSet.has('skills')) {
178
+ section('\uD83D\uDCDA Skills')
142
179
  const skillNames = collectSkillNames()
143
180
  if (skillNames.length > 0) {
144
- console.log(` 📚 Installing ${skillNames.length} skills...`)
181
+ const skillStep = step(`Installing ${skillNames.length} skills`)
145
182
  const dstSkillsDir = isGlobal ? join(target, 'skills') : join(target, '.opencode', 'skills')
146
183
  if (clean && existsSync(dstSkillsDir) && !dryRun) {
147
184
  const existing = readdirSync(dstSkillsDir)
@@ -158,6 +195,7 @@ export function installOpenCode(
158
195
  )
159
196
  stats.created += sCreated
160
197
  stats.skipped += sSkipped
198
+ skillStep(true)
161
199
  }
162
200
  }
163
201
 
@@ -165,6 +203,7 @@ export function installOpenCode(
165
203
  // 2.5 Install instructions: AGENTS.md + instructions/ (--components instructions)
166
204
  // -----------------------------------------------------------------------
167
205
  if (componentSet.has('instructions')) {
206
+ section('\uD83D\uDCCB Instructions')
168
207
  // AGENTS.md
169
208
  const srcAgentsMd = join(ROOT, 'AGENTS.md')
170
209
  const dstAgentsMd = join(target, 'AGENTS.md')
@@ -188,6 +227,7 @@ export function installOpenCode(
188
227
  // 2.6 Install prompts (--components prompts)
189
228
  // -----------------------------------------------------------------------
190
229
  if (componentSet.has('prompts')) {
230
+ section('\uD83D\uDCAC Prompts')
191
231
  const srcPrompts = join(ROOT, 'prompts')
192
232
  const dstPrompts = join(target, 'prompts')
193
233
  if (existsSync(srcPrompts)) {
@@ -201,6 +241,7 @@ export function installOpenCode(
201
241
  // 2.7 Install commands (--components commands)
202
242
  // -----------------------------------------------------------------------
203
243
  if (componentSet.has('commands')) {
244
+ section('\u26A1 Commands')
204
245
  const srcCmds = join(ROOT, 'commands')
205
246
  const dstCmds = isGlobal ? join(target, 'commands') : join(target, '.opencode', 'commands')
206
247
  if (existsSync(srcCmds)) {
@@ -223,10 +264,10 @@ export function installOpenCode(
223
264
  if (cfg.subagent_depth === undefined) { cfg.subagent_depth = 2; changed = true }
224
265
  if (changed && !dryRun) {
225
266
  writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
226
- console.log(' ✅ subagent_depth: 2 added to opencode.json')
267
+ success('subagent_depth: 2 added to opencode.json')
227
268
  }
228
269
  } catch (e) {
229
- console.warn(` ⚠️ Could not update opencode.json: ${e.message}`)
270
+ warning(`Could not update opencode.json: ${e.message}`)
230
271
  }
231
272
  }
232
273
  }
@@ -234,46 +275,15 @@ export function installOpenCode(
234
275
  // 2.8 Install TUI plugins (--components plugins)
235
276
  // -----------------------------------------------------------------------
236
277
  if (componentSet.has('plugins')) {
278
+ section('\uD83D\uDD0C Plugins')
237
279
  const srcPluginDir = join(ROOT, 'src', 'plugins', 'tui')
238
280
  const dstPluginDir = isGlobal
239
281
  ? join(target, 'plugins', 'pantheon-tui')
240
282
  : join(target, '.opencode', 'plugins', 'pantheon-tui')
241
- if (!dryRun) mkdirSync(dstPluginDir, { recursive: true })
242
- if (clean && existsSync(dstPluginDir) && !dryRun) {
243
- rmSync(dstPluginDir, { recursive: true, force: true })
244
- mkdirSync(dstPluginDir, { recursive: true })
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 --production --no-audit --no-fund', { cwd: dstPluginDir, stdio: 'pipe' })
272
- } catch (e) {
273
- console.warn(' ⚠️ Failed to install TUI plugin dependencies:', e.message)
274
- }
275
- }
276
-
283
+ const pluginStats = installPlugin(srcPluginDir, dstPluginDir, { dryRun, clean })
284
+ stats.created += pluginStats.created
285
+ stats.skipped += pluginStats.skipped
286
+ stats.errors += pluginStats.errors
277
287
  }
278
288
 
279
289
  // -----------------------------------------------------------------------
@@ -282,30 +292,9 @@ export function installOpenCode(
282
292
  const targetTuiConfigPath = isGlobal
283
293
  ? join(target, 'tui.json')
284
294
  : 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
295
  const pluginRef = 'plugins/pantheon-tui'
304
- if (!tuiConfig.plugin.includes(pluginRef)) {
305
- tuiConfig.plugin.push(pluginRef)
306
- }
307
- const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
308
- const tuiStatus = writeIfChanged(targetTuiConfigPath, tuiContent, dryRun)
296
+ unregisterPlugin(targetTuiConfigPath, pluginRef, { dryRun })
297
+ const tuiStatus = registerPlugin(targetTuiConfigPath, pluginRef, { dryRun })
309
298
  if (tuiStatus === 'created') stats.created++
310
299
  else stats.skipped++
311
300
 
@@ -314,6 +303,7 @@ export function installOpenCode(
314
303
  // MCP server scripts, code-mode scripts, tiers.json
315
304
  // -----------------------------------------------------------------------
316
305
  if (componentSet.has('runtime')) {
306
+ section('\u2699\uFE0F Runtime')
317
307
  const runtimeTarget = isGlobal ? target : join(target, '.opencode')
318
308
 
319
309
  // ── MCP server scripts ──
@@ -331,7 +321,7 @@ export function installOpenCode(
331
321
  for (const script of mcpScripts) {
332
322
  const src = join(srcScriptsDir, script)
333
323
  if (!existsSync(src)) {
334
- console.warn(` ⚠️ Script not found: ${src}`)
324
+ warning(`Script not found: ${src}`)
335
325
  stats.errors++
336
326
  continue
337
327
  }
@@ -683,9 +673,9 @@ export function installOpenCode(
683
673
  if (currentVersion) {
684
674
  const migration = runMigrations(target, currentVersion, { dryRun })
685
675
  if (migration.applied > 0) {
686
- console.log(` ✅ Applied ${migration.applied} migration(s)`)
676
+ success(`Applied ${migration.applied} migration(s)`)
687
677
  for (const msg of migration.messages) {
688
- console.log(` • ${msg}`)
678
+ bullet(msg, 1)
689
679
  }
690
680
  }
691
681
  }
@@ -696,22 +686,50 @@ export function installOpenCode(
696
686
  // -----------------------------------------------------------------------
697
687
  if (componentSet.has('runtime')) {
698
688
  try {
689
+ const venvSpinner = spinner('Setting up Python virtual environment')
699
690
  setupVenv(target, { dryRun, force: clean })
691
+ venvSpinner(true)
700
692
  const health = healthCheck(target, { dryRun })
701
693
 
702
694
  // Print health summary
703
- console.log('\n 📊 Health Check:')
704
- for (const p of health.passed) console.log(` ✅ ${p.check}: ${p.detail}`)
705
- for (const w of health.warnings) console.log(` ⚠️ ${w.check}: ${w.detail}`)
706
- for (const f of health.failed) console.log(` ❌ ${f.check}: ${f.detail}`)
695
+ section('\uD83D\uDD0D Health Check')
696
+ const healthStep = step('Running health checks')
697
+ for (const p of health.passed) success(`${p.check}: ${p.detail}`)
698
+ for (const w of health.warnings) warning(`${w.check}: ${w.detail}`)
699
+ for (const f of health.failed) error(`${f.check}: ${f.detail}`)
707
700
 
708
701
  if (health.failed.length > 0) {
709
- console.log(` ⚠️ ${health.failed.length} check(s) failed — review above`)
702
+ healthStep(false)
703
+ warning(`${health.failed.length} check(s) failed — review above`)
710
704
  stats.errors += health.failed.length
705
+ } else {
706
+ healthStep(true)
711
707
  }
712
708
  } catch (err) {
713
- console.error(` ❌ Setup failed: ${err.message}`)
709
+ error(`Setup failed: ${err.message}`)
714
710
  throw err // Fatal — abort installation
715
711
  }
716
712
  }
713
+
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
+ }
717
735
  }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * plugin.mjs — Plugin installation utilities for the OpenCode installer
3
+ */
4
+
5
+ import { execSync } from 'node:child_process'
6
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import { warning } from './cli-ui.mjs'
9
+ import { writeIfChanged } from './shared.mjs'
10
+
11
+ /**
12
+ * Install a plugin from source to destination.
13
+ * @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
14
+ * @param {string} dstDir - Destination plugin directory
15
+ * @param {object} [options]
16
+ * @param {boolean} [options.dryRun=false] - If true, don't write files
17
+ * @param {boolean} [options.clean=false] - If true, remove existing before install
18
+ * @returns {{ created: number, skipped: number, errors: number }}
19
+ */
20
+ export function installPlugin(srcDir, dstDir, { dryRun = false, clean = false } = {}) {
21
+ const result = { created: 0, skipped: 0, errors: 0 }
22
+
23
+ if (!dryRun) mkdirSync(dstDir, { recursive: true })
24
+ if (clean && existsSync(dstDir) && !dryRun) {
25
+ rmSync(dstDir, { recursive: true, force: true })
26
+ mkdirSync(dstDir, { recursive: true })
27
+ }
28
+ if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
29
+
30
+ // Copy src/index.tsx -> index.tsx
31
+ const srcIdx = join(srcDir, 'src', 'index.tsx')
32
+ if (existsSync(srcIdx)) {
33
+ const idxStatus = writeIfChanged(join(dstDir, 'index.tsx'), readFileSync(srcIdx, 'utf8'), dryRun)
34
+ if (idxStatus === 'created') result.created++
35
+ else result.skipped++
36
+ }
37
+
38
+ // Copy dist/* files
39
+ const distSrc = join(srcDir, 'dist')
40
+ if (existsSync(distSrc)) {
41
+ for (const f of readdirSync(distSrc)) {
42
+ const c = readFileSync(join(distSrc, f), 'utf8')
43
+ writeIfChanged(join(dstDir, 'dist', f), c, dryRun)
44
+ result.created++
45
+ }
46
+ }
47
+
48
+ // Copy package.json
49
+ const pkgSrc = join(srcDir, 'package.json')
50
+ if (existsSync(pkgSrc)) {
51
+ const pkgStatus = writeIfChanged(join(dstDir, 'package.json'), readFileSync(pkgSrc, 'utf8'), dryRun)
52
+ if (pkgStatus === 'created') result.created++
53
+ else result.skipped++
54
+ }
55
+
56
+ // Install plugin dependencies (@opentui/core, @opentui/solid, solid-js)
57
+ if (!dryRun) {
58
+ try {
59
+ execSync('npm install --omit=dev --no-audit', { cwd: dstDir, stdio: 'pipe' })
60
+ } catch (e) {
61
+ warning('Failed to install TUI plugin dependencies: ' + e.message)
62
+ result.errors++
63
+ }
64
+ }
65
+
66
+ return result
67
+ }
68
+
69
+ /**
70
+ * Install plugin dependencies via npm.
71
+ * @param {string} pluginDir - Directory containing package.json
72
+ * @param {object} [options]
73
+ * @param {boolean} [options.dryRun=false] - If true, skip npm install
74
+ */
75
+ export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
76
+ if (dryRun) return
77
+ try {
78
+ execSync('npm install --omit=dev --no-audit', { cwd: pluginDir, stdio: 'pipe' })
79
+ } catch (e) {
80
+ warning('Failed to install plugin dependencies: ' + e.message)
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Register a plugin in tui.json.
86
+ * @param {string} tuiJsonPath - Path to tui.json
87
+ * @param {string} pluginId - Plugin identifier string
88
+ * @param {object} [options]
89
+ * @param {boolean} [options.dryRun=false]
90
+ * @returns {'created'|'skipped'}
91
+ */
92
+ export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
93
+ let tuiConfig = { plugin: [] }
94
+ if (existsSync(tuiJsonPath)) {
95
+ try {
96
+ tuiConfig = JSON.parse(readFileSync(tuiJsonPath, 'utf8'))
97
+ } catch {
98
+ /* use default */
99
+ }
100
+ }
101
+ if (!Array.isArray(tuiConfig.plugin)) {
102
+ tuiConfig.plugin = []
103
+ }
104
+ if (!tuiConfig.plugin.includes(pluginId)) {
105
+ tuiConfig.plugin.push(pluginId)
106
+ }
107
+ const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
108
+ return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
109
+ }
110
+
111
+ /**
112
+ * Unregister stale plugin references from tui.json.
113
+ * @param {string} tuiJsonPath - Path to tui.json
114
+ * @param {string} pluginId - Plugin to unregister
115
+ * @param {object} [options]
116
+ * @param {boolean} [options.dryRun=false]
117
+ * @returns {'created'|'skipped'}
118
+ */
119
+ export function unregisterPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
120
+ let tuiConfig = { plugin: [] }
121
+ if (existsSync(tuiJsonPath)) {
122
+ try {
123
+ tuiConfig = JSON.parse(readFileSync(tuiJsonPath, 'utf8'))
124
+ } catch {
125
+ /* use default */
126
+ }
127
+ }
128
+ if (!Array.isArray(tuiConfig.plugin)) {
129
+ tuiConfig.plugin = []
130
+ }
131
+ // Remove stale plugin refs (old dist/tui.tsx path)
132
+ const staleRefs = [`${pluginId}/dist/tui.tsx`, `${pluginId}/dist/tui.js`]
133
+ for (const stale of staleRefs) {
134
+ const idx = tuiConfig.plugin.indexOf(stale)
135
+ if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
136
+ }
137
+ const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
138
+ return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
139
+ }
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * state.mjs — State manifest for Pantheon installations (schema v2)
4
+ *
5
+ * Manages the install-state.json manifest with version tracking,
6
+ * component state, and applied migration history.
7
+ *
8
+ * Schema v2:
9
+ * {
10
+ * "schema_version": 2,
11
+ * "pantheon_version": "1.1.1",
12
+ * "previous_version": "1.1.0",
13
+ * "updated_at": "2026-07-26T10:30:00Z",
14
+ * "migration_checkpoint": null,
15
+ * "applied_migrations": [
16
+ * { "from": "1.1.0", "to": "1.1.1", "applied_at": "2026-07-26T10:30:00Z", "status": "completed" }
17
+ * ],
18
+ * "components": {
19
+ * "agents": { "version": 1, "status": "installed" },
20
+ * "skills": { "version": 1, "status": "installed" },
21
+ * "plugins": { "version": 1, "status": "installed" },
22
+ * "runtime": { "version": 1, "status": "installed" }
23
+ * }
24
+ * }
25
+ */
26
+
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
28
+ import { join } from 'node:path'
29
+
30
+ const STATE_FILE = 'install-state.json'
31
+ const STATE_DIR = '.pantheon'
32
+
33
+ /**
34
+ * Create a fresh v2 state object for a new install.
35
+ * @param {string} version - Pantheon version string
36
+ * @returns {object} v2 state
37
+ */
38
+ export function createInitialState(version) {
39
+ return {
40
+ schema_version: 2,
41
+ pantheon_version: version || '0.0.0',
42
+ previous_version: null,
43
+ updated_at: new Date().toISOString(),
44
+ migration_checkpoint: null,
45
+ applied_migrations: [],
46
+ components: {
47
+ agents: { version: 1, status: 'pending' },
48
+ skills: { version: 1, status: 'pending' },
49
+ plugins: { version: 1, status: 'pending' },
50
+ runtime: { version: 1, status: 'pending' },
51
+ },
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Read install-state.json from a target directory.
57
+ * Backward-compatible with v1 schema (auto-upgrades on read).
58
+ * @param {string} target - Installation directory
59
+ * @returns {object|null} v2 state object, or null if not found
60
+ */
61
+ export function readState(target) {
62
+ const stateFile = join(target, STATE_DIR, STATE_FILE)
63
+ if (!existsSync(stateFile)) return null
64
+ try {
65
+ const raw = JSON.parse(readFileSync(stateFile, 'utf8'))
66
+ return normalizeState(raw)
67
+ } catch {
68
+ return null
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Detect whether the raw state is v1 format (pre-schema_version).
74
+ * v1 state has pantheon_version (or version) but no schema_version.
75
+ * @param {object} state
76
+ * @returns {boolean}
77
+ */
78
+ export function isV1State(state) {
79
+ if (!state || typeof state !== 'object') return false
80
+ return !state.schema_version && (!!state.pantheon_version || !!state.version)
81
+ }
82
+
83
+ /**
84
+ * Upgrade a v1 state to v2 schema.
85
+ * Preserves all existing fields, adds defaults for new ones.
86
+ * @param {object} state - v1 state object
87
+ * @returns {object} v2 state object
88
+ */
89
+ export function upgradeStateV1toV2(state) {
90
+ const now = new Date().toISOString()
91
+ return {
92
+ schema_version: 2,
93
+ pantheon_version: state.pantheon_version || state.version || 'unknown',
94
+ previous_version: state.previous_version || null,
95
+ updated_at: state.updated_at || now,
96
+ migration_checkpoint: null,
97
+ applied_migrations: Array.isArray(state.applied_migrations) ? state.applied_migrations : [],
98
+ components: state.components && typeof state.components === 'object' ? state.components : {
99
+ agents: { version: 1, status: 'installed' },
100
+ skills: { version: 1, status: 'installed' },
101
+ plugins: { version: 1, status: 'unknown' },
102
+ runtime: { version: 1, status: 'unknown' },
103
+ },
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Normalize any state to v2 (handles v1 → v2 upgrade).
109
+ * @param {object} state
110
+ * @returns {object|null}
111
+ */
112
+ function normalizeState(state) {
113
+ if (!state || typeof state !== 'object') return null
114
+ if (isV1State(state)) return upgradeStateV1toV2(state)
115
+ if (!state.schema_version || state.schema_version < 2) return upgradeStateV1toV2(state)
116
+ return state
117
+ }
118
+
119
+ /**
120
+ * Write state to target's install-state.json (always v2 format).
121
+ * @param {string} target - Installation directory
122
+ * @param {object} state - v2 state object
123
+ */
124
+ export function writeState(target, state) {
125
+ const stateDir = join(target, STATE_DIR)
126
+ if (!existsSync(stateDir)) {
127
+ mkdirSync(stateDir, { recursive: true })
128
+ }
129
+ const stateFile = join(stateDir, STATE_FILE)
130
+ state.updated_at = new Date().toISOString()
131
+ state.schema_version = 2
132
+ writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
133
+ }
134
+
135
+ /**
136
+ * Check whether install-state.json exists in target.
137
+ * @param {string} target
138
+ * @returns {boolean}
139
+ */
140
+ export function hasState(target) {
141
+ return existsSync(join(target, STATE_DIR, STATE_FILE))
142
+ }
143
+
144
+ /**
145
+ * Add a migration record to the applied_migrations array.
146
+ * @param {object} state
147
+ * @param {string} fromVersion
148
+ * @param {string} toVersion
149
+ * @param {string} [status='completed']
150
+ */
151
+ export function addMigrationRecord(state, fromVersion, toVersion, status = 'completed') {
152
+ if (!Array.isArray(state.applied_migrations)) {
153
+ state.applied_migrations = []
154
+ }
155
+ state.applied_migrations.push({
156
+ from: fromVersion,
157
+ to: toVersion,
158
+ applied_at: new Date().toISOString(),
159
+ status,
160
+ })
161
+ }
162
+
163
+ /**
164
+ * Update the pantheon_version and previous_version.
165
+ * @param {object} state
166
+ * @param {string} newVersion
167
+ */
168
+ export function updateVersion(state, newVersion) {
169
+ state.previous_version = state.pantheon_version || null
170
+ state.pantheon_version = newVersion
171
+ }
172
+
173
+ /**
174
+ * Get a component's state object.
175
+ * @param {object} state
176
+ * @param {string} component - Component name (agents, skills, plugins, runtime)
177
+ * @returns {{ version: number, status: string }|null}
178
+ */
179
+ export function getComponentState(state, component) {
180
+ if (!state.components) return null
181
+ return state.components[component] || null
182
+ }
183
+
184
+ /**
185
+ * Set a component's version and status.
186
+ * @param {object} state
187
+ * @param {string} component
188
+ * @param {number} version
189
+ * @param {string} status
190
+ */
191
+ export function setComponentState(state, component, version, status) {
192
+ if (!state.components) state.components = {}
193
+ state.components[component] = { version, status }
194
+ }
@@ -0,0 +1,8 @@
1
+ import { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ //#region src/index.d.ts
3
+ declare const plugin: TuiPluginModule & {
4
+ id: string;
5
+ };
6
+ //#endregion
7
+ export { plugin as default };
8
+ //# sourceMappingURL=tui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cAsbM,QAAQ;EAAoB"}