pantheon-opencode 1.1.1-beta.2 → 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
- package/src/plugins/tui/package.json +4 -4
|
@@ -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
|
+
}
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
20
20
|
"@opencode-ai/sdk": ">=1.14.0",
|
|
21
|
-
"@opentui/core": ">=0.
|
|
22
|
-
"@opentui/solid": ">=0.
|
|
21
|
+
"@opentui/core": ">=0.4.5",
|
|
22
|
+
"@opentui/solid": ">=0.4.5",
|
|
23
23
|
"solid-js": ">=1.9.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@opencode-ai/plugin": "^1.14.0",
|
|
28
|
-
"@opentui/core": "^0.
|
|
29
|
-
"@opentui/solid": "^0.
|
|
28
|
+
"@opentui/core": "^0.4.5",
|
|
29
|
+
"@opentui/solid": "^0.4.5",
|
|
30
30
|
"solid-js": "^1.9.0"
|
|
31
31
|
}
|
|
32
32
|
}
|