pantheon-opencode 1.6.0-beta.11 → 1.6.0-beta.12

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.
Files changed (50) hide show
  1. package/AGENTS.global.md +3 -3
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +69 -30
  5. package/README.pt-BR.md +5 -4
  6. package/bin/pantheon-init.mjs +279 -101
  7. package/package.json +4 -3
  8. package/plugin.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/scripts/build-agents-md.mjs +77 -13
  11. package/scripts/check-plugin-v2-coverage.mjs +7 -7
  12. package/scripts/doctor.mjs +242 -19
  13. package/scripts/install/backup.mjs +191 -18
  14. package/scripts/install/cli-ui.mjs +73 -0
  15. package/scripts/install/file-inventory.mjs +555 -0
  16. package/scripts/install/migrate.mjs +45 -38
  17. package/scripts/install/model-picker.mjs +63 -22
  18. package/scripts/install/opencode-version.mjs +9 -1
  19. package/scripts/install/opencode.mjs +265 -79
  20. package/scripts/install/plan-artifact.mjs +517 -0
  21. package/scripts/install/plan.mjs +165 -3
  22. package/scripts/install/plugin.mjs +118 -14
  23. package/scripts/install/shared.mjs +186 -65
  24. package/scripts/install/state.mjs +63 -8
  25. package/scripts/install/venv.mjs +15 -6
  26. package/scripts/install/verify.mjs +128 -0
  27. package/scripts/install/vision-prompt.mjs +2 -0
  28. package/scripts/install-mcp.mjs +1 -1
  29. package/scripts/manifest-inventory.mjs +49 -3
  30. package/scripts/node-engine-policy.mjs +74 -0
  31. package/scripts/package-evidence.mjs +6 -0
  32. package/scripts/postinstall.mjs +3 -4
  33. package/scripts/reconcile.mjs +325 -0
  34. package/scripts/uninstall.mjs +258 -34
  35. package/scripts/update.mjs +164 -0
  36. package/scripts/validate-node-engine-policy.mjs +13 -0
  37. package/scripts/version-check.mjs +34 -1
  38. package/scripts/versioning.mjs +9 -1
  39. package/skills-lock.json +3 -3
  40. package/src/agents/mnemosyne.md +7 -2
  41. package/src/instructions/memory-protocol.instructions.md +2 -2
  42. package/src/instructions/zeus-memory-operations.instructions.md +4 -3
  43. package/src/pantheon/v2-unsupported.mjs +2 -3
  44. package/src/plugins/tui/dist/tui-v2.js +2 -2
  45. package/src/plugins/tui/dist/tui.js +1 -1
  46. package/src/plugins/tui/package-lock.json +3 -3
  47. package/src/plugins/tui/package.json +2 -2
  48. package/src/skills/artifact-management/SKILL.md +5 -1
  49. package/src/skills/memory-bank/SKILL.md +5 -1
  50. package/src/skills/worktrees/SKILL.md +30 -11
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /** Preview or remove only files proven stale by the install ownership inventory. */
3
+ import {
4
+ closeSync,
5
+ constants,
6
+ existsSync,
7
+ fsyncSync,
8
+ openSync,
9
+ readFileSync,
10
+ writeFileSync,
11
+ } from 'node:fs'
12
+ import { homedir } from 'node:os'
13
+ import { join, resolve } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { applyFileDriftPlan, classifyFileInventory } from './install/file-inventory.mjs'
16
+ import { readState } from './install/state.mjs'
17
+ import { assertNoSymlinkAncestors, verifyRegularFile } from './install/verify.mjs'
18
+
19
+ const VALID_COMPONENTS = [
20
+ 'agents',
21
+ 'skills',
22
+ 'instructions',
23
+ 'prompts',
24
+ 'commands',
25
+ 'plugins',
26
+ 'runtime',
27
+ ]
28
+ const DEFAULT_COMPONENTS = [...VALID_COMPONENTS]
29
+
30
+ export function parseArgs(argv) {
31
+ const args = {
32
+ project: false,
33
+ global: false,
34
+ target: null,
35
+ components: null,
36
+ apply: false,
37
+ dryRun: false,
38
+ yes: false,
39
+ savePlan: null,
40
+ applyPlan: null,
41
+ help: false,
42
+ }
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const arg = argv[i]
45
+ if (arg === '--project') args.project = true
46
+ else if (arg === '--global') args.global = true
47
+ else if (arg === '--target') {
48
+ const target = argv[++i]
49
+ if (!target || target.startsWith('--')) throw new Error('--target requires a path')
50
+ args.target = target
51
+ } else if (arg === '--components') args.components = argv[++i] ?? null
52
+ else if (arg === '--apply') args.apply = true
53
+ else if (arg === '--dry-run') args.dryRun = true
54
+ else if (arg === '--yes' || arg === '-y') args.yes = true
55
+ else if (arg === '--save-plan') args.savePlan = argv[++i] ?? null
56
+ else if (arg === '--apply-plan') args.applyPlan = argv[++i] ?? null
57
+ else if (arg === '--help' || arg === '-h') args.help = true
58
+ else throw new Error(`Unknown reconcile option: ${arg}`)
59
+ }
60
+ if (!args.applyPlan && !args.help) {
61
+ if (args.project && (args.global || args.target !== null)) {
62
+ throw new Error('--project cannot be combined with --global or --target')
63
+ }
64
+ if (!args.project && !args.global && args.target === null) {
65
+ throw new Error('Choose a target scope: --project, --global, or --target PATH')
66
+ }
67
+ }
68
+ if (args.components !== null) {
69
+ args.components = args.components
70
+ .split(',')
71
+ .map((item) => item.trim())
72
+ .filter(Boolean)
73
+ if (
74
+ args.components.length === 0 ||
75
+ args.components.some((item) => !VALID_COMPONENTS.includes(item))
76
+ ) {
77
+ throw new Error(
78
+ `--components must be a comma-separated list of: ${VALID_COMPONENTS.join(', ')}`,
79
+ )
80
+ }
81
+ args.components = [...new Set(args.components)]
82
+ }
83
+ if (args.apply && args.applyPlan) throw new Error('Use either --apply or --apply-plan, not both')
84
+ if (args.dryRun && (args.apply || args.applyPlan)) {
85
+ throw new Error('--dry-run cannot be combined with an apply option')
86
+ }
87
+ if (args.savePlan && (args.apply || args.applyPlan))
88
+ throw new Error('--save-plan is available only during preview')
89
+ if (args.yes && !args.apply && !args.applyPlan)
90
+ throw new Error('--yes requires --apply or --apply-plan')
91
+ if (args.applyPlan && (args.project || args.global || args.target || args.components)) {
92
+ throw new Error('--apply-plan uses the target and components embedded in the reviewed plan')
93
+ }
94
+ return args
95
+ }
96
+
97
+ function printHelp() {
98
+ console.log(`
99
+ pantheon-opencode reconcile — review and remove exact stale installer-owned files
100
+
101
+ Usage:
102
+ pantheon-opencode reconcile --project [--dry-run] [--components agents,skills] [--save-plan <file>]
103
+ pantheon-opencode reconcile --global [--target PATH] [--dry-run] [--components agents,skills]
104
+ pantheon-opencode reconcile --target PATH [--dry-run] [--components agents,skills]
105
+ pantheon-opencode reconcile --project --apply [--yes]
106
+ pantheon-opencode reconcile --apply-plan <file> --yes
107
+
108
+ Use --global for the global OpenCode config target, or --target PATH for an explicit
109
+ target. An explicit target is project-scoped unless combined with --global, which
110
+ selects the flat global layout for that path. Preview is read-only. Apply removes only unchanged, inventoried stale files;
111
+ legacy, modified, missing, symlinked, and ambiguous files are preserved.
112
+ `)
113
+ }
114
+
115
+ function resolveTarget(args) {
116
+ if (args.target) return resolve(args.target)
117
+ if (args.project) return resolve(process.cwd())
118
+ return resolve(join(homedir(), '.config', 'opencode'))
119
+ }
120
+
121
+ function layoutFor({ global, version }) {
122
+ return Object.fromEntries(
123
+ VALID_COMPONENTS.map((component) => [
124
+ component,
125
+ global ? 'global' : version === 'v2' ? 'project-v2' : 'project-v1',
126
+ ]),
127
+ )
128
+ }
129
+
130
+ async function buildReview(target, components, global) {
131
+ if (!existsSync(target)) throw new Error(`Target directory does not exist: ${target}`)
132
+ const state = readState(target)
133
+ if (!state)
134
+ throw new Error('No readable installer state found; unowned legacy files will not be removed')
135
+ const { installOpenCode } = await import('./install/opencode.mjs')
136
+ const desired = await installOpenCode(target, true, false, components, {
137
+ version: 'auto',
138
+ global,
139
+ quiet: true,
140
+ quietPlan: true,
141
+ planOnly: true,
142
+ headless: true,
143
+ })
144
+ const desiredFiles = new Map(
145
+ (desired?.files ?? [])
146
+ .filter((file) => typeof file.outputFingerprint === 'string')
147
+ .map((file) => [file.path, { sha256: file.outputFingerprint }]),
148
+ )
149
+ const selectedLayouts = layoutFor({ global, version: desired?.openCodeVersion })
150
+ const report = classifyFileInventory(target, state, {
151
+ desiredFiles,
152
+ selectedComponents: components,
153
+ selectedLayouts,
154
+ })
155
+ return {
156
+ state,
157
+ report,
158
+ desiredFiles,
159
+ selectedLayouts,
160
+ components,
161
+ plan: {
162
+ schema_version: 1,
163
+ command: 'reconcile',
164
+ target: resolve(target),
165
+ selected_components: components,
166
+ selected_layouts: selectedLayouts,
167
+ inventory_fingerprint: report.inventoryFingerprint,
168
+ manual_review: report.manualReview,
169
+ operations: report.operations,
170
+ },
171
+ }
172
+ }
173
+
174
+ function safePlanShape(plan) {
175
+ return (
176
+ plan &&
177
+ plan.schema_version === 1 &&
178
+ plan.command === 'reconcile' &&
179
+ typeof plan.target === 'string' &&
180
+ Array.isArray(plan.selected_components) &&
181
+ plan.selected_components.every((component) => VALID_COMPONENTS.includes(component)) &&
182
+ plan.selected_layouts &&
183
+ typeof plan.selected_layouts === 'object' &&
184
+ typeof plan.inventory_fingerprint === 'string' &&
185
+ Array.isArray(plan.operations)
186
+ )
187
+ }
188
+
189
+ function savePlan(filePath, plan) {
190
+ const destination = resolve(filePath)
191
+ assertNoSymlinkAncestors(destination)
192
+ const descriptor = openSync(
193
+ destination,
194
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
195
+ 0o600,
196
+ )
197
+ const contents = `${JSON.stringify(plan, null, 2)}\n`
198
+ try {
199
+ writeFileSync(descriptor, contents, 'utf8')
200
+ fsyncSync(descriptor)
201
+ } finally {
202
+ closeSync(descriptor)
203
+ }
204
+ verifyRegularFile(destination, contents)
205
+ return destination
206
+ }
207
+
208
+ async function authorize(plan, args) {
209
+ const count = plan.operations.length
210
+ console.log(`Reviewed ${count} exact stale-owned file(s) under ${plan.target}:`)
211
+ for (const operation of plan.operations) console.log(` remove ${operation.path}`)
212
+ if (plan.operations.length === 0) return true
213
+ if (args.yes) return true
214
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
215
+ console.error(
216
+ 'Apply requested without authorization. Re-run with --apply --yes or --apply-plan FILE --yes.',
217
+ )
218
+ process.exitCode = 2
219
+ return false
220
+ }
221
+ const readline = await import('node:readline/promises')
222
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout })
223
+ try {
224
+ const answer = await prompt.question('Remove exactly these files? [y/N] ')
225
+ return ['y', 'yes'].includes(answer.trim().toLowerCase())
226
+ } finally {
227
+ prompt.close()
228
+ }
229
+ }
230
+
231
+ function printReport(report) {
232
+ console.log('Installer file ownership report:')
233
+ if (report.legacyAmbiguous) {
234
+ console.log(
235
+ ' manual-review Legacy files without inventory are unknown and will never be auto-deleted.',
236
+ )
237
+ }
238
+ for (const entry of report.entries) {
239
+ const suffix = entry.reason ? ` (${entry.reason})` : ''
240
+ console.log(` ${entry.classification.padEnd(14)} ${entry.path}${suffix}`)
241
+ }
242
+ console.log(
243
+ ` ${report.operations.length} stale-owned regular file(s) eligible for reviewed removal.`,
244
+ )
245
+ }
246
+
247
+ async function runApplyPlan(plan, args) {
248
+ if (!safePlanShape(plan) || plan.manual_review === true) {
249
+ throw new Error('Reviewed reconcile plan is invalid or marked for manual review')
250
+ }
251
+ const target = resolve(plan.target)
252
+ const components = plan.selected_components
253
+ const review = await buildReview(target, components, plan.selected_layouts?.agents === 'global')
254
+ if (JSON.stringify(review.plan.operations) !== JSON.stringify(plan.operations)) {
255
+ throw new Error('Reviewed reconcile operation set drifted; no files were removed')
256
+ }
257
+ const reviewed = {
258
+ target,
259
+ operations: plan.operations,
260
+ inventoryFingerprint: plan.inventory_fingerprint,
261
+ manualReview: plan.manual_review,
262
+ }
263
+ if (!(await authorize(reviewed, args))) return
264
+ const result = applyFileDriftPlan(target, review.state, reviewed, {
265
+ desiredFiles: review.desiredFiles,
266
+ selectedComponents: components,
267
+ selectedLayouts: review.selectedLayouts,
268
+ })
269
+ console.log(`Removed ${result.removed.length} file(s).`)
270
+ if (result.backupDir) console.log(`Restorable backup: ${result.backupDir}`)
271
+ }
272
+
273
+ export async function main(argv = process.argv.slice(2)) {
274
+ let args
275
+ try {
276
+ args = parseArgs(argv)
277
+ } catch (error) {
278
+ console.error(`❌ ${error.message}`)
279
+ process.exitCode = 1
280
+ return
281
+ }
282
+ if (args.help) return printHelp()
283
+
284
+ if (args.applyPlan) {
285
+ try {
286
+ const plan = JSON.parse(readFileSync(resolve(args.applyPlan), 'utf8'))
287
+ await runApplyPlan(plan, args)
288
+ } catch (error) {
289
+ console.error(`❌ ${error.message}`)
290
+ process.exitCode = 3
291
+ }
292
+ return
293
+ }
294
+
295
+ const target = resolveTarget(args)
296
+ const components = args.components ?? DEFAULT_COMPONENTS
297
+ try {
298
+ const review = await buildReview(target, components, args.global)
299
+ printReport(review.report)
300
+ if (args.savePlan) {
301
+ const savedPath = savePlan(args.savePlan, review.plan)
302
+ console.log(`Reviewed plan saved: ${savedPath}`)
303
+ return
304
+ }
305
+ if (!args.apply) {
306
+ console.log('Preview only; no files were written or removed.')
307
+ return
308
+ }
309
+ if (!(await authorize(review.plan, args))) return
310
+ const result = applyFileDriftPlan(target, review.state, review.report, {
311
+ desiredFiles: review.desiredFiles,
312
+ selectedComponents: components,
313
+ selectedLayouts: review.selectedLayouts,
314
+ })
315
+ console.log(`Removed ${result.removed.length} file(s).`)
316
+ if (result.backupDir) console.log(`Restorable backup: ${result.backupDir}`)
317
+ } catch (error) {
318
+ console.error(`❌ ${error.message}`)
319
+ process.exitCode = 1
320
+ }
321
+ }
322
+
323
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
324
+ await main()
325
+ }