pantheon-opencode 1.1.1-beta.0 → 1.1.1-beta.10

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.
@@ -8,11 +8,11 @@ import { homedir } from 'node:os'
8
8
  import { join, resolve } from 'node:path'
9
9
  import { healthCheck } from './health-check.mjs'
10
10
  import { detectVersion, runMigrations } from './migrate.mjs'
11
+ import { colors, configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
11
12
  import {
12
13
  collectSkillNames,
13
14
  copyFiles,
14
15
  installSkills,
15
- PLATFORM_DIR,
16
16
  parseFrontmatter,
17
17
  ROOT,
18
18
  sourceDirValid,
@@ -21,6 +21,10 @@ import {
21
21
  writeIfChanged,
22
22
  } from './shared.mjs'
23
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']
24
28
 
25
29
  function deepClone(value) {
26
30
  return JSON.parse(JSON.stringify(value))
@@ -70,11 +74,12 @@ function isGlobalConfigDir(target) {
70
74
  return resolve(target) === xdgDir
71
75
  }
72
76
 
73
- export function installOpenCode(
77
+ export async function installOpenCode(
74
78
  target,
75
79
  dryRun = false,
76
80
  clean = false,
77
81
  components = ['agents', 'skills', 'instructions', 'commands', 'plugins', 'runtime'],
82
+ opts = {},
78
83
  ) {
79
84
  // Default target if not provided (global ~/.config/opencode)
80
85
  if (!target) {
@@ -84,6 +89,35 @@ export function installOpenCode(
84
89
  const componentSet = new Set(components)
85
90
  const stats = summary.opencode
86
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
+
87
121
  // Determine layout based on install scope.
88
122
  // Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
89
123
  // agents/ skills/ commands/ plugins/
@@ -93,21 +127,23 @@ export function installOpenCode(
93
127
  const _subDir = isGlobal ? '' : '.opencode'
94
128
  const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
95
129
 
130
+ configure({ dryRun })
131
+
96
132
  if (isGlobal) {
97
- console.log(
98
- ' 🌐 Global config directory detected — using flat layout (agents/, skills/, commands/)',
99
- )
133
+ info('Global config directory detected — using flat layout (agents/, skills/, commands/)')
100
134
  }
101
135
 
102
136
  // -----------------------------------------------------------------------
103
137
  // 1. Install agents (--components agents)
104
138
  // -----------------------------------------------------------------------
105
139
  if (componentSet.has('agents')) {
140
+ section('\uD83D\uDCE6 Agents')
106
141
  const srcDir = join(ROOT, 'src', 'agents')
107
142
  if (!sourceDirValid(srcDir)) {
108
- console.warn(` ⚠️ Agent source directory not found: ${srcDir}`)
143
+ warning(`Agent source directory not found: ${srcDir}`)
109
144
  stats.errors++
110
145
  } else {
146
+ const agentStep = step('Installing agents')
111
147
  const dstDir = isGlobal ? join(target, 'agents') : join(target, '.opencode', 'agents')
112
148
  if (!dryRun) mkdirSync(dstDir, { recursive: true })
113
149
  if (clean && existsSync(dstDir) && !dryRun) {
@@ -119,6 +155,7 @@ export function installOpenCode(
119
155
  const { created, skipped } = copyFiles(srcDir, dstDir, dryRun)
120
156
  stats.created += created
121
157
  stats.skipped += skipped
158
+ agentStep(true)
122
159
  }
123
160
  }
124
161
 
@@ -138,9 +175,10 @@ export function installOpenCode(
138
175
  // 2. Install skills (--components skills)
139
176
  // -----------------------------------------------------------------------
140
177
  if (componentSet.has('skills')) {
178
+ section('\uD83D\uDCDA Skills')
141
179
  const skillNames = collectSkillNames()
142
180
  if (skillNames.length > 0) {
143
- console.log(` 📚 Installing ${skillNames.length} skills...`)
181
+ const skillStep = step(`Installing ${skillNames.length} skills`)
144
182
  const dstSkillsDir = isGlobal ? join(target, 'skills') : join(target, '.opencode', 'skills')
145
183
  if (clean && existsSync(dstSkillsDir) && !dryRun) {
146
184
  const existing = readdirSync(dstSkillsDir)
@@ -157,6 +195,7 @@ export function installOpenCode(
157
195
  )
158
196
  stats.created += sCreated
159
197
  stats.skipped += sSkipped
198
+ skillStep(true)
160
199
  }
161
200
  }
162
201
 
@@ -164,6 +203,7 @@ export function installOpenCode(
164
203
  // 2.5 Install instructions: AGENTS.md + instructions/ (--components instructions)
165
204
  // -----------------------------------------------------------------------
166
205
  if (componentSet.has('instructions')) {
206
+ section('\uD83D\uDCCB Instructions')
167
207
  // AGENTS.md
168
208
  const srcAgentsMd = join(ROOT, 'AGENTS.md')
169
209
  const dstAgentsMd = join(target, 'AGENTS.md')
@@ -187,6 +227,7 @@ export function installOpenCode(
187
227
  // 2.6 Install prompts (--components prompts)
188
228
  // -----------------------------------------------------------------------
189
229
  if (componentSet.has('prompts')) {
230
+ section('\uD83D\uDCAC Prompts')
190
231
  const srcPrompts = join(ROOT, 'prompts')
191
232
  const dstPrompts = join(target, 'prompts')
192
233
  if (existsSync(srcPrompts)) {
@@ -200,6 +241,7 @@ export function installOpenCode(
200
241
  // 2.7 Install commands (--components commands)
201
242
  // -----------------------------------------------------------------------
202
243
  if (componentSet.has('commands')) {
244
+ section('\u26A1 Commands')
203
245
  const srcCmds = join(ROOT, 'commands')
204
246
  const dstCmds = isGlobal ? join(target, 'commands') : join(target, '.opencode', 'commands')
205
247
  if (existsSync(srcCmds)) {
@@ -222,10 +264,10 @@ export function installOpenCode(
222
264
  if (cfg.subagent_depth === undefined) { cfg.subagent_depth = 2; changed = true }
223
265
  if (changed && !dryRun) {
224
266
  writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
225
- console.log(' ✅ subagent_depth: 2 added to opencode.json')
267
+ success('subagent_depth: 2 added to opencode.json')
226
268
  }
227
269
  } catch (e) {
228
- console.warn(` ⚠️ Could not update opencode.json: ${e.message}`)
270
+ warning(`Could not update opencode.json: ${e.message}`)
229
271
  }
230
272
  }
231
273
  }
@@ -233,46 +275,15 @@ export function installOpenCode(
233
275
  // 2.8 Install TUI plugins (--components plugins)
234
276
  // -----------------------------------------------------------------------
235
277
  if (componentSet.has('plugins')) {
278
+ section('\uD83D\uDD0C Plugins')
236
279
  const srcPluginDir = join(ROOT, 'src', 'plugins', 'tui')
237
280
  const dstPluginDir = isGlobal
238
281
  ? join(target, 'plugins', 'pantheon-tui')
239
282
  : join(target, '.opencode', 'plugins', 'pantheon-tui')
240
- if (!dryRun) mkdirSync(dstPluginDir, { recursive: true })
241
- if (clean && existsSync(dstPluginDir) && !dryRun) {
242
- rmSync(dstPluginDir, { recursive: true, force: true })
243
- mkdirSync(dstPluginDir, { recursive: true })
244
- }
245
- if (!dryRun) mkdirSync(join(dstPluginDir, "dist"), { recursive: true })
246
- // Copy src/index.tsx -> index.tsx
247
- const srcIdx = join(srcPluginDir, "src", "index.tsx")
248
- if (existsSync(srcIdx)) {
249
- writeIfChanged(join(dstPluginDir, "index.tsx"), readFileSync(srcIdx, "utf8"), dryRun)
250
- stats.created++
251
- }
252
- // Copy dist files
253
- const distSrc = join(srcPluginDir, "dist")
254
- if (existsSync(distSrc)) {
255
- for (const f of readdirSync(distSrc)) {
256
- const c = readFileSync(join(distSrc, f), "utf8")
257
- writeIfChanged(join(dstPluginDir, "dist", f), c, dryRun)
258
- stats.created++
259
- }
260
- }
261
- // Copy package.json
262
- const pkgSrc = join(srcPluginDir, "package.json")
263
- if (existsSync(pkgSrc)) {
264
- writeIfChanged(join(dstPluginDir, "package.json"), readFileSync(pkgSrc, "utf8"), dryRun)
265
- stats.created++
266
- }
267
- // Install plugin dependencies (@opentui/core, @opentui/solid, solid-js)
268
- if (!dryRun) {
269
- try {
270
- execSync('npm install --production --no-audit --no-fund', { cwd: dstPluginDir, stdio: 'pipe' })
271
- } catch (e) {
272
- console.warn(' ⚠️ Failed to install TUI plugin dependencies:', e.message)
273
- }
274
- }
275
-
283
+ const pluginStats = installPlugin(srcPluginDir, dstPluginDir, { dryRun, clean })
284
+ stats.created += pluginStats.created
285
+ stats.skipped += pluginStats.skipped
286
+ stats.errors += pluginStats.errors
276
287
  }
277
288
 
278
289
  // -----------------------------------------------------------------------
@@ -281,30 +292,9 @@ export function installOpenCode(
281
292
  const targetTuiConfigPath = isGlobal
282
293
  ? join(target, 'tui.json')
283
294
  : join(target, '.opencode', 'tui.json')
284
- let tuiConfig = { plugin: [] }
285
- if (existsSync(targetTuiConfigPath)) {
286
- try {
287
- tuiConfig = JSON.parse(readFileSync(targetTuiConfigPath, 'utf8'))
288
- } catch {
289
- /* use default */
290
- }
291
- }
292
- if (!Array.isArray(tuiConfig.plugin)) {
293
- tuiConfig.plugin = []
294
- }
295
- // Remove stale plugin refs (old dist/tui.tsx path)
296
- const staleRefs = ["plugins/pantheon-tui/dist/tui.tsx", "plugins/pantheon-tui/dist/tui.js"]
297
- for (const stale of staleRefs) {
298
- const idx = tuiConfig.plugin.indexOf(stale)
299
- if (idx !== -1) tuiConfig.plugin.splice(idx, 1)
300
- }
301
- // Add our plugin if not already present
302
295
  const pluginRef = 'plugins/pantheon-tui'
303
- if (!tuiConfig.plugin.includes(pluginRef)) {
304
- tuiConfig.plugin.push(pluginRef)
305
- }
306
- const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
307
- const tuiStatus = writeIfChanged(targetTuiConfigPath, tuiContent, dryRun)
296
+ unregisterPlugin(targetTuiConfigPath, pluginRef, { dryRun })
297
+ const tuiStatus = registerPlugin(targetTuiConfigPath, pluginRef, { dryRun })
308
298
  if (tuiStatus === 'created') stats.created++
309
299
  else stats.skipped++
310
300
 
@@ -313,6 +303,7 @@ export function installOpenCode(
313
303
  // MCP server scripts, code-mode scripts, tiers.json
314
304
  // -----------------------------------------------------------------------
315
305
  if (componentSet.has('runtime')) {
306
+ section('\u2699\uFE0F Runtime')
316
307
  const runtimeTarget = isGlobal ? target : join(target, '.opencode')
317
308
 
318
309
  // ── MCP server scripts ──
@@ -330,7 +321,7 @@ export function installOpenCode(
330
321
  for (const script of mcpScripts) {
331
322
  const src = join(srcScriptsDir, script)
332
323
  if (!existsSync(src)) {
333
- console.warn(` ⚠️ Script not found: ${src}`)
324
+ warning(`Script not found: ${src}`)
334
325
  stats.errors++
335
326
  continue
336
327
  }
@@ -682,9 +673,9 @@ export function installOpenCode(
682
673
  if (currentVersion) {
683
674
  const migration = runMigrations(target, currentVersion, { dryRun })
684
675
  if (migration.applied > 0) {
685
- console.log(` ✅ Applied ${migration.applied} migration(s)`)
676
+ success(`Applied ${migration.applied} migration(s)`)
686
677
  for (const msg of migration.messages) {
687
- console.log(` • ${msg}`)
678
+ bullet(msg, 1)
688
679
  }
689
680
  }
690
681
  }
@@ -695,22 +686,50 @@ export function installOpenCode(
695
686
  // -----------------------------------------------------------------------
696
687
  if (componentSet.has('runtime')) {
697
688
  try {
689
+ const venvSpinner = spinner('Setting up Python virtual environment')
698
690
  setupVenv(target, { dryRun, force: clean })
691
+ venvSpinner(true)
699
692
  const health = healthCheck(target, { dryRun })
700
693
 
701
694
  // Print health summary
702
- console.log('\n 📊 Health Check:')
703
- for (const p of health.passed) console.log(` ✅ ${p.check}: ${p.detail}`)
704
- for (const w of health.warnings) console.log(` ⚠️ ${w.check}: ${w.detail}`)
705
- 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}`)
706
700
 
707
701
  if (health.failed.length > 0) {
708
- console.log(` ⚠️ ${health.failed.length} check(s) failed — review above`)
702
+ healthStep(false)
703
+ warning(`${health.failed.length} check(s) failed — review above`)
709
704
  stats.errors += health.failed.length
705
+ } else {
706
+ healthStep(true)
710
707
  }
711
708
  } catch (err) {
712
- console.error(` ❌ Setup failed: ${err.message}`)
709
+ error(`Setup failed: ${err.message}`)
713
710
  throw err // Fatal — abort installation
714
711
  }
715
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
+ }
716
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":";;cA2WM,QAAQ;EAAoB"}