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.
- 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 -78
- 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 +603 -137
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +317 -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 +317 -127
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-config.mjs — Standalone migration CLI with checkpoint crash recovery
|
|
4
|
+
*
|
|
5
|
+
* Runs configuration migrations with an atomic checkpoint system.
|
|
6
|
+
* If the process crashes mid-migration, the checkpoint file persists
|
|
7
|
+
* so that --resume picks up where it left off.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/install/migrate-config.mjs --from=1.1.0 --to=1.1.1 --target=~/.config/opencode
|
|
11
|
+
* node scripts/install/migrate-config.mjs --resume --target=~/.config/opencode
|
|
12
|
+
* node scripts/install/migrate-config.mjs --list --target=~/.config/opencode
|
|
13
|
+
* node scripts/install/migrate-config.mjs --dry-run --from=1.1.0 --to=1.1.1
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import {
|
|
19
|
+
readState,
|
|
20
|
+
writeState,
|
|
21
|
+
createInitialState,
|
|
22
|
+
addMigrationRecord,
|
|
23
|
+
updateVersion,
|
|
24
|
+
} from './state.mjs'
|
|
25
|
+
import {
|
|
26
|
+
readCheckpoint,
|
|
27
|
+
writeCheckpoint,
|
|
28
|
+
advanceCheckpoint,
|
|
29
|
+
removeCheckpoint,
|
|
30
|
+
resumePendingMigration,
|
|
31
|
+
listAppliedMigrations,
|
|
32
|
+
} from './migrate.mjs'
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Migration step definitions
|
|
36
|
+
// Each step is an idempotent function(target, state, { dryRun }) => void
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const MIGRATION_STEPS = {
|
|
40
|
+
/**
|
|
41
|
+
* migrate_state_v2: Upgrade install-state.json from schema v1 to v2.
|
|
42
|
+
* Adds schema_version field, components tracking, and migration history.
|
|
43
|
+
*/
|
|
44
|
+
migrate_state_v2: (target, state, { dryRun }) => {
|
|
45
|
+
if (state.schema_version && state.schema_version >= 2) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (!dryRun) {
|
|
49
|
+
state.schema_version = 2
|
|
50
|
+
state.components = state.components || {
|
|
51
|
+
agents: { version: 1, status: 'pending' },
|
|
52
|
+
skills: { version: 1, status: 'pending' },
|
|
53
|
+
plugins: { version: 1, status: 'pending' },
|
|
54
|
+
runtime: { version: 1, status: 'pending' },
|
|
55
|
+
}
|
|
56
|
+
state.applied_migrations = state.applied_migrations || []
|
|
57
|
+
state.migration_checkpoint = null
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* update_schema: Ensure all v2 schema fields exist with defaults.
|
|
63
|
+
*/
|
|
64
|
+
update_schema: (target, state, { dryRun }) => {
|
|
65
|
+
if (!dryRun) {
|
|
66
|
+
if (!state.components) {
|
|
67
|
+
state.components = {
|
|
68
|
+
agents: { version: 1, status: 'pending' },
|
|
69
|
+
skills: { version: 1, status: 'pending' },
|
|
70
|
+
plugins: { version: 1, status: 'pending' },
|
|
71
|
+
runtime: { version: 1, status: 'pending' },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const key of ['agents', 'skills', 'plugins', 'runtime']) {
|
|
75
|
+
if (!state.components[key]) {
|
|
76
|
+
state.components[key] = { version: 1, status: 'pending' }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
state.applied_migrations = state.applied_migrations || []
|
|
80
|
+
if (state.migration_checkpoint === undefined) {
|
|
81
|
+
state.migration_checkpoint = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* migrate_plugins: Normalize plugin component tracking.
|
|
88
|
+
*/
|
|
89
|
+
migrate_plugins: (target, state, { dryRun }) => {
|
|
90
|
+
if (!dryRun) {
|
|
91
|
+
if (!state.components) state.components = {}
|
|
92
|
+
const p = state.components.plugins || { version: 1, status: 'pending' }
|
|
93
|
+
p.version = Math.max(p.version || 0, 1)
|
|
94
|
+
state.components.plugins = p
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Migration step ordering (a step runs ONLY after all predecessors complete)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const STEP_ORDER = Object.keys(MIGRATION_STEPS)
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve which steps to run based on from/to version comparison.
|
|
107
|
+
* For now, runs all steps. In the future this can be gated by version.
|
|
108
|
+
* @param {string} fromVersion
|
|
109
|
+
* @param {string} toVersion
|
|
110
|
+
* @returns {string[]} Ordered list of step names
|
|
111
|
+
*/
|
|
112
|
+
function resolveSteps(fromVersion, toVersion) {
|
|
113
|
+
return [...STEP_ORDER]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// CLI
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function parseArgs() {
|
|
121
|
+
const args = process.argv.slice(2)
|
|
122
|
+
const result = {
|
|
123
|
+
from: null,
|
|
124
|
+
to: null,
|
|
125
|
+
target: process.cwd(),
|
|
126
|
+
dryRun: false,
|
|
127
|
+
resume: false,
|
|
128
|
+
list: false,
|
|
129
|
+
help: false,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < args.length; i++) {
|
|
133
|
+
const arg = args[i]
|
|
134
|
+
if (arg === '--dry-run') result.dryRun = true
|
|
135
|
+
else if (arg === '--resume') result.resume = true
|
|
136
|
+
else if (arg === '--list') result.list = true
|
|
137
|
+
else if (arg === '--help' || arg === '-h') result.help = true
|
|
138
|
+
else if (arg.startsWith('--from=')) result.from = arg.split('=', 2)[1]
|
|
139
|
+
else if (arg.startsWith('--to=')) result.to = arg.split('=', 2)[1]
|
|
140
|
+
else if (arg.startsWith('--target=')) result.target = arg.split('=', 2)[1]
|
|
141
|
+
else if (arg === '--from') result.from = args[++i]
|
|
142
|
+
else if (arg === '--to') result.to = args[++i]
|
|
143
|
+
else if (arg === '--target') result.target = args[++i]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function showHelp() {
|
|
150
|
+
console.log(`
|
|
151
|
+
migrate-config.mjs — Pantheon configuration migration CLI
|
|
152
|
+
|
|
153
|
+
Usage:
|
|
154
|
+
node scripts/install/migrate-config.mjs --from=1.1.0 --to=1.1.1 --target=~/.config/opencode
|
|
155
|
+
node scripts/install/migrate-config.mjs --resume --target=~/.config/opencode
|
|
156
|
+
node scripts/install/migrate-config.mjs --list --target=~/.config/opencode
|
|
157
|
+
node scripts/install/migrate-config.mjs --dry-run --from=1.1.0 --to=1.1.1
|
|
158
|
+
node scripts/install/migrate-config.mjs --help
|
|
159
|
+
|
|
160
|
+
Options:
|
|
161
|
+
--from=<version> Source version (required for new migrations)
|
|
162
|
+
--to=<version> Target version (required for new migrations)
|
|
163
|
+
--target=<path> Installation directory (default: cwd)
|
|
164
|
+
--dry-run Preview without writing
|
|
165
|
+
--resume Resume from pending checkpoint (crash recovery)
|
|
166
|
+
--list Show applied migration history
|
|
167
|
+
--help Show this help
|
|
168
|
+
|
|
169
|
+
Crash Recovery:
|
|
170
|
+
If the process is interrupted mid-migration, a checkpoint file is left
|
|
171
|
+
at .pantheon/.migration-checkpoint. Run with --resume to continue.
|
|
172
|
+
`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function main() {
|
|
176
|
+
const config = parseArgs()
|
|
177
|
+
|
|
178
|
+
if (config.help) {
|
|
179
|
+
showHelp()
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── List mode ──
|
|
184
|
+
if (config.list) {
|
|
185
|
+
const history = listAppliedMigrations(config.target)
|
|
186
|
+
if (!history.currentVersion) {
|
|
187
|
+
console.log(' \u2139\uFE0F No install state found in: ' + config.target)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
console.log(` Current version: ${history.currentVersion}`)
|
|
191
|
+
console.log(` Previous version: ${history.previousVersion || 'N/A'}`)
|
|
192
|
+
console.log(` Applied migrations: ${history.migrations.length}`)
|
|
193
|
+
for (const m of history.migrations) {
|
|
194
|
+
const icon =
|
|
195
|
+
m.status === 'completed'
|
|
196
|
+
? '\u2705'
|
|
197
|
+
: m.status === 'rolled_back'
|
|
198
|
+
? '\u{1F504}'
|
|
199
|
+
: '\u26A0\uFE0F'
|
|
200
|
+
console.log(` ${icon} ${m.from} \u2192 ${m.to} [${m.status}] (${m.applied_at})`)
|
|
201
|
+
}
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Resume mode ──
|
|
206
|
+
if (config.resume) {
|
|
207
|
+
const cp = resumePendingMigration(config.target)
|
|
208
|
+
if (!cp) {
|
|
209
|
+
console.log(' \u23ED\uFE0F No pending checkpoint found — nothing to resume')
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
config.from = cp.from
|
|
213
|
+
config.to = cp.to
|
|
214
|
+
} else {
|
|
215
|
+
if (!config.from || !config.to) {
|
|
216
|
+
console.error('\u274C --from and --to are required (or use --resume)')
|
|
217
|
+
showHelp()
|
|
218
|
+
process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Read state ──
|
|
223
|
+
let state = readState(config.target)
|
|
224
|
+
if (!state) {
|
|
225
|
+
state = createInitialState(config.from)
|
|
226
|
+
console.log(` \u{1F4DD} Creating new install state (v${state.schema_version})`)
|
|
227
|
+
if (!config.dryRun) writeState(config.target, state)
|
|
228
|
+
} else {
|
|
229
|
+
console.log(
|
|
230
|
+
` \u{1F4CB} Current state: v${state.schema_version}, version ${state.pantheon_version}`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Resolve checkpoint ──
|
|
235
|
+
let cp = readCheckpoint(config.target)
|
|
236
|
+
|
|
237
|
+
if (!cp) {
|
|
238
|
+
const steps = resolveSteps(config.from, config.to)
|
|
239
|
+
cp = writeCheckpoint(config.target, config.from, config.to, steps)
|
|
240
|
+
console.log(` \u{1F4DD} Checkpoint written: ${config.from} \u2192 ${config.to}`)
|
|
241
|
+
} else {
|
|
242
|
+
console.log(
|
|
243
|
+
` \u{1F504} Resuming from checkpoint (${cp.steps_completed.length} steps done, ${cp.steps_pending.length} remaining)`,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Run pending steps ──
|
|
248
|
+
const stepsToRun = [...cp.steps_pending]
|
|
249
|
+
let failed = false
|
|
250
|
+
|
|
251
|
+
for (const step of stepsToRun) {
|
|
252
|
+
const stepFn = MIGRATION_STEPS[step]
|
|
253
|
+
if (!stepFn) {
|
|
254
|
+
console.warn(` \u26A0\uFE0F Unknown migration step: ${step} — skipping`)
|
|
255
|
+
if (!config.dryRun) {
|
|
256
|
+
advanceCheckpoint(config.target, step)
|
|
257
|
+
}
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(` \u25B6\uFE0F Running: ${step}`)
|
|
262
|
+
try {
|
|
263
|
+
stepFn(config.target, state, { dryRun: config.dryRun })
|
|
264
|
+
|
|
265
|
+
if (!config.dryRun) {
|
|
266
|
+
advanceCheckpoint(config.target, step)
|
|
267
|
+
writeState(config.target, state)
|
|
268
|
+
console.log(` \u2705 ${step} completed`)
|
|
269
|
+
} else {
|
|
270
|
+
console.log(` \u2705 ${step} would complete (dry-run)`)
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(` \u274C ${step} failed: ${err.message}`)
|
|
274
|
+
failed = true
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Finalize ──
|
|
280
|
+
if (failed) {
|
|
281
|
+
console.error(`\n \u274C Migration failed at checkpoint.`)
|
|
282
|
+
console.error(
|
|
283
|
+
` Resume with: node scripts/install/migrate-config.mjs --resume --target=${config.target}`,
|
|
284
|
+
)
|
|
285
|
+
process.exit(1)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!config.dryRun) {
|
|
289
|
+
updateVersion(state, config.to)
|
|
290
|
+
addMigrationRecord(state, config.from, config.to, 'completed')
|
|
291
|
+
removeCheckpoint(config.target)
|
|
292
|
+
writeState(config.target, state)
|
|
293
|
+
console.log(
|
|
294
|
+
`\n \u2705 Migration complete: ${config.from} \u2192 ${config.to}`,
|
|
295
|
+
)
|
|
296
|
+
console.log(
|
|
297
|
+
` State written to: ${join(config.target, '.pantheon', 'install-state.json')}`,
|
|
298
|
+
)
|
|
299
|
+
} else {
|
|
300
|
+
console.log(
|
|
301
|
+
`\n \u2705 Migration would complete (dry-run): ${config.from} \u2192 ${config.to}`,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
307
|
+
main()
|
|
308
|
+
}
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* node scripts/install/migrate.mjs --target ~/.config/opencode --force
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { dirname, join } from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { readState, writeState, createInitialState } from './state.mjs'
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
17
|
const ROOT = join(__dirname, '..', '..')
|
|
@@ -33,16 +34,9 @@ function readPackageVersion() {
|
|
|
33
34
|
* @returns {string|null} - version string or null for fresh install
|
|
34
35
|
*/
|
|
35
36
|
export function detectVersion(target) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
const state = JSON.parse(readFileSync(stateFile, 'utf8'))
|
|
40
|
-
return state.pantheon_version || state.version || 'unknown'
|
|
41
|
-
} catch {
|
|
42
|
-
/* corrupted state — treat as unknown */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return null // fresh install
|
|
37
|
+
const state = readState(target)
|
|
38
|
+
if (state) return state.pantheon_version || state.version || 'unknown'
|
|
39
|
+
return null
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
/**
|
|
@@ -96,9 +90,42 @@ export function runMigrations(target, currentVersion, { dryRun = false } = {}) {
|
|
|
96
90
|
semver[2] = 1
|
|
97
91
|
}
|
|
98
92
|
|
|
93
|
+
// ── Migration: v3.19.1 → v3.19.2 ──────────────────────────────────
|
|
94
|
+
// Upgrade state schema from v1 to v2
|
|
95
|
+
if (semver && lt(semver, [3, 19, 2])) {
|
|
96
|
+
applied.push({
|
|
97
|
+
from: formatSemver(semver),
|
|
98
|
+
to: '3.19.2',
|
|
99
|
+
description: 'Upgrade install state schema to v2 with component tracking',
|
|
100
|
+
})
|
|
101
|
+
semver[0] = 3
|
|
102
|
+
semver[1] = 19
|
|
103
|
+
semver[2] = 2
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
// ── Write updated state ───────────────────────────────────────────
|
|
100
107
|
if (applied.length > 0 && !dryRun) {
|
|
101
|
-
|
|
108
|
+
const newVersion = formatSemver(semver)
|
|
109
|
+
let state = readState(target)
|
|
110
|
+
if (!state) state = createInitialState(newVersion)
|
|
111
|
+
state.pantheon_version = newVersion
|
|
112
|
+
for (const m of applied) {
|
|
113
|
+
state.applied_migrations.push({
|
|
114
|
+
from: m.from,
|
|
115
|
+
to: m.to,
|
|
116
|
+
applied_at: new Date().toISOString(),
|
|
117
|
+
status: 'completed',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
if (!state.components) {
|
|
121
|
+
state.components = {
|
|
122
|
+
agents: { version: 1, status: 'installed' },
|
|
123
|
+
skills: { version: 1, status: 'installed' },
|
|
124
|
+
plugins: { version: 1, status: 'installed' },
|
|
125
|
+
runtime: { version: 1, status: 'installed' },
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
writeState(target, state)
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
return {
|
|
@@ -107,20 +134,150 @@ export function runMigrations(target, currentVersion, { dryRun = false } = {}) {
|
|
|
107
134
|
}
|
|
108
135
|
}
|
|
109
136
|
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Checkpoint system for crash recovery
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
const CHECKPOINT_FILE = '.migration-checkpoint'
|
|
142
|
+
const STATE_DIR = '.pantheon'
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read the migration checkpoint file if it exists.
|
|
146
|
+
* @param {string} target
|
|
147
|
+
* @returns {object|null}
|
|
148
|
+
*/
|
|
149
|
+
export function readCheckpoint(target) {
|
|
150
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
151
|
+
if (!existsSync(cpFile)) return null
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(readFileSync(cpFile, 'utf8'))
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Write a migration checkpoint before starting migrations.
|
|
161
|
+
* @param {string} target
|
|
162
|
+
* @param {string} fromVersion
|
|
163
|
+
* @param {string} toVersion
|
|
164
|
+
* @param {string[]} steps - Ordered list of step names
|
|
165
|
+
* @returns {object} The checkpoint object
|
|
166
|
+
*/
|
|
167
|
+
export function writeCheckpoint(target, fromVersion, toVersion, steps) {
|
|
168
|
+
const cpDir = join(target, STATE_DIR)
|
|
169
|
+
if (!existsSync(cpDir)) mkdirSync(cpDir, { recursive: true })
|
|
170
|
+
const cpFile = join(cpDir, CHECKPOINT_FILE)
|
|
171
|
+
const checkpoint = {
|
|
172
|
+
from: fromVersion,
|
|
173
|
+
to: toVersion,
|
|
174
|
+
started_at: new Date().toISOString(),
|
|
175
|
+
steps_completed: [],
|
|
176
|
+
steps_pending: [...steps],
|
|
177
|
+
}
|
|
178
|
+
writeFileSync(cpFile, `${JSON.stringify(checkpoint, null, 2)}\n`, 'utf8')
|
|
179
|
+
return checkpoint
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Advance the checkpoint: mark a step as completed and remove from pending.
|
|
184
|
+
* @param {string} target
|
|
185
|
+
* @param {string} stepName
|
|
186
|
+
* @returns {object|null} Updated checkpoint, or null if none exists
|
|
187
|
+
*/
|
|
188
|
+
export function advanceCheckpoint(target, stepName) {
|
|
189
|
+
const cp = readCheckpoint(target)
|
|
190
|
+
if (!cp) return null
|
|
191
|
+
cp.steps_completed.push(stepName)
|
|
192
|
+
cp.steps_pending = cp.steps_pending.filter((s) => s !== stepName)
|
|
193
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
194
|
+
writeFileSync(cpFile, `${JSON.stringify(cp, null, 2)}\n`, 'utf8')
|
|
195
|
+
return cp
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove the checkpoint file on successful completion.
|
|
200
|
+
* @param {string} target
|
|
201
|
+
*/
|
|
202
|
+
export function removeCheckpoint(target) {
|
|
203
|
+
const cpFile = join(target, STATE_DIR, CHECKPOINT_FILE)
|
|
204
|
+
if (existsSync(cpFile)) {
|
|
205
|
+
rmSync(cpFile, { force: true })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resume from a pending checkpoint.
|
|
211
|
+
* Reads the checkpoint and reports what steps remain.
|
|
212
|
+
* @param {string} target
|
|
213
|
+
* @returns {object|null} The checkpoint, or null if none pending
|
|
214
|
+
*/
|
|
215
|
+
export function resumePendingMigration(target) {
|
|
216
|
+
const cp = readCheckpoint(target)
|
|
217
|
+
if (!cp) return null
|
|
218
|
+
console.log(` \u{1F504} Resuming migration from checkpoint: ${cp.from} \u2192 ${cp.to}`)
|
|
219
|
+
console.log(` Completed: ${cp.steps_completed.join(', ') || 'none'}`)
|
|
220
|
+
console.log(` Pending: ${cp.steps_pending.join(', ') || 'none'}`)
|
|
221
|
+
return cp
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Rollback support
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
110
228
|
/**
|
|
111
|
-
*
|
|
229
|
+
* Roll back the last completed migration for a given version.
|
|
230
|
+
* Each migration step should define a corresponding down-migration.
|
|
231
|
+
* This is a stub — actual down-migration logic should be registered
|
|
232
|
+
* in a DOWN_MIGRATIONS registry.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} target
|
|
235
|
+
* @param {string} version - The version to roll back from
|
|
236
|
+
* @returns {{ success: boolean, reason?: string, migration?: object }}
|
|
112
237
|
*/
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
238
|
+
export function rollbackMigration(target, version) {
|
|
239
|
+
const state = readState(target)
|
|
240
|
+
if (!state) {
|
|
241
|
+
console.log(' \u26A0\uFE0F No install state found — nothing to roll back')
|
|
242
|
+
return { success: false, reason: 'No install state' }
|
|
118
243
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
244
|
+
|
|
245
|
+
const migrations = state.applied_migrations || []
|
|
246
|
+
const targetMigration = migrations.find(
|
|
247
|
+
(m) => m.to === version && m.status === 'completed',
|
|
248
|
+
)
|
|
249
|
+
if (!targetMigration) {
|
|
250
|
+
console.log(` \u26A0\uFE0F No completed migration found for version ${version}`)
|
|
251
|
+
return { success: false, reason: 'Migration not found' }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(
|
|
255
|
+
` \u{1F504} Rolling back migration: ${targetMigration.from} \u2192 ${targetMigration.to}`,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
targetMigration.status = 'rolled_back'
|
|
259
|
+
state.pantheon_version = targetMigration.from
|
|
260
|
+
writeState(target, state)
|
|
261
|
+
|
|
262
|
+
console.log(` \u2705 Rolled back to ${targetMigration.from}`)
|
|
263
|
+
return { success: true, migration: targetMigration }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* List all applied migrations from the install state.
|
|
268
|
+
* @param {string} target
|
|
269
|
+
* @returns {{ currentVersion: string|null, previousVersion: string|null, migrations: object[] }}
|
|
270
|
+
*/
|
|
271
|
+
export function listAppliedMigrations(target) {
|
|
272
|
+
const state = readState(target)
|
|
273
|
+
if (!state) {
|
|
274
|
+
return { currentVersion: null, previousVersion: null, migrations: [] }
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
currentVersion: state.pantheon_version,
|
|
278
|
+
previousVersion: state.previous_version,
|
|
279
|
+
migrations: state.applied_migrations || [],
|
|
122
280
|
}
|
|
123
|
-
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
124
281
|
}
|
|
125
282
|
|
|
126
283
|
// ---------------------------------------------------------------------------
|
|
@@ -172,15 +329,38 @@ function main() {
|
|
|
172
329
|
const target = targetIdx !== -1 ? args[targetIdx + 1] : process.cwd()
|
|
173
330
|
const dryRun = args.includes('--dry-run')
|
|
174
331
|
const force = args.includes('--force')
|
|
332
|
+
const list = args.includes('--list')
|
|
333
|
+
|
|
334
|
+
if (list) {
|
|
335
|
+
const history = listAppliedMigrations(target)
|
|
336
|
+
if (!history.currentVersion) {
|
|
337
|
+
console.log(' \u2139\uFE0F No install state found in: ' + target)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
console.log(` Current version: ${history.currentVersion}`)
|
|
341
|
+
console.log(` Previous version: ${history.previousVersion || 'N/A'}`)
|
|
342
|
+
console.log(` Applied migrations: ${history.migrations.length}`)
|
|
343
|
+
for (const m of history.migrations) {
|
|
344
|
+
const icon = m.status === 'completed' ? '\u2705' : m.status === 'rolled_back' ? '\u{1F504}' : '\u26A0\uFE0F'
|
|
345
|
+
console.log(` ${icon} ${m.from} \u2192 ${m.to} [${m.status}] (${m.applied_at})`)
|
|
346
|
+
}
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const rollbackTo = args.find((a) => a.startsWith('--rollback='))?.split('=', 2)[1]
|
|
351
|
+
if (rollbackTo) {
|
|
352
|
+
rollbackMigration(target, rollbackTo)
|
|
353
|
+
return
|
|
354
|
+
}
|
|
175
355
|
|
|
176
356
|
const currentVersion = detectVersion(target)
|
|
177
357
|
if (!currentVersion) {
|
|
178
|
-
// Fresh install — write state and exit
|
|
179
358
|
if (!dryRun) {
|
|
180
|
-
|
|
181
|
-
|
|
359
|
+
const state = createInitialState(PANTHON_VERSION)
|
|
360
|
+
writeState(target, state)
|
|
361
|
+
console.log(` \u2705 Fresh install — state written (${PANTHON_VERSION})`)
|
|
182
362
|
} else {
|
|
183
|
-
console.log(`
|
|
363
|
+
console.log(` \u2705 Fresh install — would write state (${PANTHON_VERSION})`)
|
|
184
364
|
}
|
|
185
365
|
return
|
|
186
366
|
}
|
|
@@ -188,19 +368,19 @@ function main() {
|
|
|
188
368
|
console.log(` Current version: ${currentVersion}`)
|
|
189
369
|
|
|
190
370
|
if (!force && currentVersion === PANTHON_VERSION) {
|
|
191
|
-
console.log('
|
|
371
|
+
console.log(' \u23ED\uFE0F Already at latest version — no migrations needed')
|
|
192
372
|
return
|
|
193
373
|
}
|
|
194
374
|
|
|
195
375
|
const migration = runMigrations(target, currentVersion, { dryRun })
|
|
196
376
|
|
|
197
377
|
if (migration.applied > 0) {
|
|
198
|
-
console.log(`
|
|
378
|
+
console.log(` \u2705 Applied ${migration.applied} migration(s):`)
|
|
199
379
|
for (const msg of migration.messages) {
|
|
200
|
-
console.log(`
|
|
380
|
+
console.log(` \u2022 ${msg}`)
|
|
201
381
|
}
|
|
202
382
|
} else {
|
|
203
|
-
console.log('
|
|
383
|
+
console.log(' \u23ED\uFE0F No applicable migrations')
|
|
204
384
|
}
|
|
205
385
|
}
|
|
206
386
|
|