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.
- package/README.md +7 -2
- package/bin/pantheon-init.mjs +19 -6
- package/package.json +1 -1
- package/scripts/install/cli-ui.mjs +160 -0
- package/scripts/install/component.mjs +24 -0
- package/scripts/install/interactive.mjs +395 -0
- package/scripts/install/migrate-config.mjs +308 -0
- package/scripts/install/migrate.mjs +210 -30
- package/scripts/install/opencode.mjs +96 -77
- package/scripts/install/plugin.mjs +139 -0
- package/scripts/install/state.mjs +194 -0
- package/src/plugins/tui/dist/tui.d.ts +8 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui.js +557 -137
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +242 -127
- package/src/plugins/tui/package-lock.json +2879 -0
- package/src/plugins/tui/package.json +11 -8
- package/src/plugins/tui/src/index.tsx +242 -127
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
success('subagent_depth: 2 added to opencode.json')
|
|
226
268
|
}
|
|
227
269
|
} catch (e) {
|
|
228
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
+
success(`Applied ${migration.applied} migration(s)`)
|
|
686
677
|
for (const msg of migration.messages) {
|
|
687
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
for (const
|
|
705
|
-
for (const
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cA2WM,QAAQ;EAAoB"}
|