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

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 (53) hide show
  1. package/AGENTS.global.md +3 -3
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +85 -31
  5. package/README.pt-BR.md +7 -4
  6. package/bin/pantheon-init.mjs +278 -102
  7. package/package.json +5 -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/interactive.mjs +69 -391
  17. package/scripts/install/migrate.mjs +45 -38
  18. package/scripts/install/model-picker.mjs +63 -22
  19. package/scripts/install/opencode-version.mjs +9 -1
  20. package/scripts/install/opencode.mjs +279 -79
  21. package/scripts/install/plan-artifact.mjs +517 -0
  22. package/scripts/install/plan.mjs +165 -3
  23. package/scripts/install/plugin.mjs +118 -14
  24. package/scripts/install/prompt-result.mjs +2 -0
  25. package/scripts/install/shared.mjs +188 -65
  26. package/scripts/install/state.mjs +63 -8
  27. package/scripts/install/venv.mjs +15 -6
  28. package/scripts/install/verify.mjs +128 -0
  29. package/scripts/install/vision-prompt.mjs +2 -0
  30. package/scripts/install-mcp.mjs +1 -1
  31. package/scripts/manifest-inventory.mjs +49 -3
  32. package/scripts/node-engine-policy.mjs +85 -0
  33. package/scripts/package-evidence.mjs +6 -0
  34. package/scripts/postinstall.mjs +26 -26
  35. package/scripts/reconcile.mjs +325 -0
  36. package/scripts/sync-tui.mjs +5 -1
  37. package/scripts/uninstall.mjs +258 -34
  38. package/scripts/update.mjs +164 -0
  39. package/scripts/validate-node-engine-policy.mjs +13 -0
  40. package/scripts/version-check.mjs +34 -1
  41. package/scripts/versioning.mjs +9 -1
  42. package/skills-lock.json +3 -3
  43. package/src/agents/mnemosyne.md +7 -2
  44. package/src/instructions/memory-protocol.instructions.md +2 -2
  45. package/src/instructions/zeus-memory-operations.instructions.md +4 -3
  46. package/src/pantheon/v2-unsupported.mjs +2 -3
  47. package/src/plugins/tui/dist/tui-v2.js +2 -2
  48. package/src/plugins/tui/dist/tui.js +1 -1
  49. package/src/plugins/tui/package-lock.json +3 -3
  50. package/src/plugins/tui/package.json +2 -2
  51. package/src/skills/artifact-management/SKILL.md +5 -1
  52. package/src/skills/memory-bank/SKILL.md +5 -1
  53. package/src/skills/worktrees/SKILL.md +30 -11
@@ -14,18 +14,13 @@
14
14
  * "herdar do chat" (inherit) é o default da Q1: não grava active-preset.json,
15
15
  * então os delegates herdam o modelo do chat pai (herança nativa, sem preset).
16
16
  */
17
- import {
18
- copyFileSync,
19
- existsSync,
20
- mkdirSync,
21
- readFileSync,
22
- renameSync,
23
- writeFileSync,
24
- } from 'node:fs'
17
+ import { lstatSync, mkdirSync, readFileSync } from 'node:fs'
25
18
  import { homedir } from 'node:os'
26
19
  import { join } from 'node:path'
27
20
  import { createInterface } from 'node:readline/promises'
28
21
  import { loadPresetDefs } from '../../src/pantheon/presets.mjs'
22
+ import { ACTIONS, fingerprintOutput, recordChange, snapshot } from './plan.mjs'
23
+ import { assertNoSymlinkAncestors, writeFileAtomically } from './verify.mjs'
29
24
 
30
25
  /** Preset pricing table (2026 verified). */
31
26
  export const PRESET_PRICE = {
@@ -176,12 +171,16 @@ export function buildInitQuestions(presetDefs) {
176
171
  * @param {string} [source] 'cli' | 'interactive'
177
172
  * @returns {{version: number, preset: string, source: string, updated_at: string}}
178
173
  */
179
- export function buildActivePresetFile(presetName, source = 'cli') {
174
+ export function buildActivePresetFile(
175
+ presetName,
176
+ source = 'cli',
177
+ updatedAt = new Date().toISOString(),
178
+ ) {
180
179
  return {
181
180
  version: 1,
182
181
  preset: presetName,
183
182
  source,
184
- updated_at: new Date().toISOString(),
183
+ updated_at: updatedAt,
185
184
  }
186
185
  }
187
186
 
@@ -197,28 +196,70 @@ export function buildActivePresetFile(presetName, source = 'cli') {
197
196
  export function writeActivePreset(
198
197
  presetDir,
199
198
  presetName,
200
- { dryRun = false, source = 'cli', logger = console } = {},
199
+ { dryRun = false, source = 'cli', logger = console, updatedAt, recorder = null } = {},
201
200
  ) {
202
201
  const dir = join(presetDir, '.pantheon')
203
202
  const filePath = join(dir, 'active-preset.json')
204
203
  const backupPath = join(dir, 'active-preset.json.bak')
205
204
 
205
+ const content = `${JSON.stringify(buildActivePresetFile(presetName, source, updatedAt), null, 2)}\n`
206
+ assertNoSymlinkAncestors(filePath)
207
+ assertNoSymlinkAncestors(backupPath)
208
+ assertNoSymlinkAncestors(`${filePath}.tmp-${process.pid}`)
209
+ assertNoSymlinkAncestors(`${backupPath}.tmp-${process.pid}`)
210
+ let current = null
211
+ try {
212
+ const info = lstatSync(filePath)
213
+ if (info.isSymbolicLink() || !info.isFile()) {
214
+ throw new Error(`Refusing to write through non-regular file: ${filePath}`)
215
+ }
216
+ current = readFileSync(filePath)
217
+ } catch (error) {
218
+ if (error?.code !== 'ENOENT') throw error
219
+ }
220
+ if (current !== null) {
221
+ const backupSnapshot = snapshot(backupPath)
222
+ recordChange(
223
+ {
224
+ path: backupPath,
225
+ action: !backupSnapshot.exists
226
+ ? ACTIONS.CREATE
227
+ : backupSnapshot.sha256 === fingerprintOutput(current)
228
+ ? ACTIONS.UNCHANGED
229
+ : ACTIONS.UPDATE,
230
+ bytes: current.length,
231
+ outputFingerprint: fingerprintOutput(current),
232
+ scored: false,
233
+ },
234
+ recorder,
235
+ )
236
+ if (!dryRun && backupSnapshot.sha256 !== fingerprintOutput(current)) {
237
+ writeFileAtomically(backupPath, current)
238
+ }
239
+ }
240
+ const before = snapshot(filePath)
241
+ recordChange(
242
+ {
243
+ path: filePath,
244
+ action: !before.exists
245
+ ? ACTIONS.CREATE
246
+ : current?.toString('utf8') === content
247
+ ? ACTIONS.UNCHANGED
248
+ : ACTIONS.UPDATE,
249
+ bytes: Buffer.byteLength(content, 'utf8'),
250
+ outputFingerprint: fingerprintOutput(content),
251
+ },
252
+ recorder,
253
+ )
206
254
  if (dryRun) {
207
255
  logger.log?.(`[dry-run] Would write model preset "${presetName}" to ${filePath}`)
208
256
  return { written: false, path: filePath, backupPath: null }
209
257
  }
210
258
 
211
259
  mkdirSync(dir, { recursive: true })
212
- let backupMade = false
213
- if (existsSync(filePath)) {
214
- copyFileSync(filePath, backupPath)
215
- backupMade = true
216
- }
217
- writeFileSync(
218
- `${filePath}.tmp-${process.pid}`,
219
- `${JSON.stringify(buildActivePresetFile(presetName, source), null, 2)}\n`,
220
- )
221
- renameSync(`${filePath}.tmp-${process.pid}`, filePath)
260
+ assertNoSymlinkAncestors(filePath)
261
+ assertNoSymlinkAncestors(backupPath)
262
+ writeFileAtomically(filePath, content)
222
263
  // health-check: verify file is readable and valid JSON with preset
223
264
  try {
224
265
  const content = readFileSync(filePath, 'utf8')
@@ -227,7 +268,7 @@ export function writeActivePreset(
227
268
  } catch (err) {
228
269
  logger.warn?.(`health-check: active-preset.json verification failed: ${err.message}`)
229
270
  }
230
- return { written: true, path: filePath, backupPath: backupMade ? backupPath : null }
271
+ return { written: true, path: filePath, backupPath: current !== null ? backupPath : null }
231
272
  }
232
273
 
233
274
  /**
@@ -220,10 +220,18 @@ export function resolveOpenCodeVersion(requested = 'auto', options = {}) {
220
220
  return env.OPENCODE_VERSION
221
221
  }
222
222
  const binary = options.binary ?? env.OPENCODE_BIN ?? ''
223
+ const warn = options.warn ?? warning
223
224
  if (/(?:^|[\\/])opencode2(?:\.exe)?$/i.test(binary)) return 'v2'
224
225
 
226
+ if (options.skipProbe) {
227
+ warn(
228
+ 'Dry-run skipped the OpenCode host version probe to avoid host-side writes; assuming V1. ' +
229
+ 'Pass --opencode-version v2 (or v1) to review a specific generation.',
230
+ )
231
+ return 'v1'
232
+ }
233
+
225
234
  const probe = options.probe ?? (() => probeHostVersion(binary || 'opencode'))
226
- const warn = options.warn ?? warning
227
235
  let raw
228
236
  try {
229
237
  raw = probe()