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.
- package/AGENTS.global.md +3 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +69 -30
- package/README.pt-BR.md +5 -4
- package/bin/pantheon-init.mjs +279 -101
- package/package.json +4 -3
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +77 -13
- package/scripts/check-plugin-v2-coverage.mjs +7 -7
- package/scripts/doctor.mjs +242 -19
- package/scripts/install/backup.mjs +191 -18
- package/scripts/install/cli-ui.mjs +73 -0
- package/scripts/install/file-inventory.mjs +555 -0
- package/scripts/install/migrate.mjs +45 -38
- package/scripts/install/model-picker.mjs +63 -22
- package/scripts/install/opencode-version.mjs +9 -1
- package/scripts/install/opencode.mjs +265 -79
- package/scripts/install/plan-artifact.mjs +517 -0
- package/scripts/install/plan.mjs +165 -3
- package/scripts/install/plugin.mjs +118 -14
- package/scripts/install/shared.mjs +186 -65
- package/scripts/install/state.mjs +63 -8
- package/scripts/install/venv.mjs +15 -6
- package/scripts/install/verify.mjs +128 -0
- package/scripts/install/vision-prompt.mjs +2 -0
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/manifest-inventory.mjs +49 -3
- package/scripts/node-engine-policy.mjs +74 -0
- package/scripts/package-evidence.mjs +6 -0
- package/scripts/postinstall.mjs +3 -4
- package/scripts/reconcile.mjs +325 -0
- package/scripts/uninstall.mjs +258 -34
- package/scripts/update.mjs +164 -0
- package/scripts/validate-node-engine-policy.mjs +13 -0
- package/scripts/version-check.mjs +34 -1
- package/scripts/versioning.mjs +9 -1
- package/skills-lock.json +3 -3
- package/src/agents/mnemosyne.md +7 -2
- package/src/instructions/memory-protocol.instructions.md +2 -2
- package/src/instructions/zeus-memory-operations.instructions.md +4 -3
- package/src/pantheon/v2-unsupported.mjs +2 -3
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +3 -3
- package/src/plugins/tui/package.json +2 -2
- package/src/skills/artifact-management/SKILL.md +5 -1
- package/src/skills/memory-bank/SKILL.md +5 -1
- 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
|
+
}
|