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
@@ -0,0 +1,128 @@
1
+ import {
2
+ closeSync,
3
+ fsyncSync,
4
+ lstatSync,
5
+ openSync,
6
+ readFileSync,
7
+ renameSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from 'node:fs'
11
+ import { isAbsolute, parse, relative, resolve, sep } from 'node:path'
12
+
13
+ /** Reject symlinked path components before a managed filesystem operation. */
14
+ export function assertNoSymlinkAncestors(filePath, { includeLeaf = true } = {}) {
15
+ const absolute = resolve(filePath)
16
+ const root = parse(absolute).root
17
+ const parts = absolute.slice(root.length).split(sep).filter(Boolean)
18
+ const checked = parts.slice(0, includeLeaf ? parts.length : Math.max(0, parts.length - 1))
19
+ let current = root
20
+ for (const [index, part] of checked.entries()) {
21
+ current = current ? `${current}${current.endsWith(sep) ? '' : sep}${part}` : part
22
+ let info
23
+ try {
24
+ info = lstatSync(current)
25
+ } catch (error) {
26
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return true
27
+ throw error
28
+ }
29
+ if (info.isSymbolicLink()) throw new Error(`Path contains a symlink: ${current}`)
30
+ if (index < checked.length - 1 && !info.isDirectory()) {
31
+ throw new Error(`Path ancestor is not a directory: ${current}`)
32
+ }
33
+ }
34
+ return true
35
+ }
36
+
37
+ /** Require a destination to remain lexically inside its selected target. */
38
+ export function assertPathWithin(rootPath, destinationPath) {
39
+ const root = resolve(rootPath)
40
+ const destination = resolve(destinationPath)
41
+ const fromRoot = relative(root, destination)
42
+ if (fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
43
+ throw new Error(`Path escapes selected target: ${destinationPath}`)
44
+ }
45
+ return true
46
+ }
47
+
48
+ /** Validate lexical containment and reject links before deleting a path. */
49
+ export function assertSafeRemoval(rootPath, destinationPath) {
50
+ assertPathWithin(rootPath, destinationPath)
51
+ assertNoSymlinkAncestors(destinationPath)
52
+ return true
53
+ }
54
+
55
+ /** Read back a regular-file write without ever including file contents in errors. */
56
+ export function verifyRegularFile(filePath, expected) {
57
+ const info = lstatSync(filePath)
58
+ if (!info.isFile() || info.isSymbolicLink()) {
59
+ throw new Error(`Managed-file write did not produce a regular file: ${filePath}`)
60
+ }
61
+ const actual = readFileSync(filePath)
62
+ const expectedBuffer = Buffer.isBuffer(expected) ? expected : Buffer.from(expected)
63
+ if (!actual.equals(expectedBuffer)) {
64
+ throw new Error(`Managed-file read-back verification failed: ${filePath}`)
65
+ }
66
+ return true
67
+ }
68
+
69
+ /**
70
+ * Atomically write a managed file using a private, exclusively-created temp.
71
+ * Existing destinations are replaced only after path checks and a temp read-back.
72
+ */
73
+ export function writeFileAtomically(filePath, content) {
74
+ const temporary = `${filePath}.tmp-${process.pid}`
75
+ assertNoSymlinkAncestors(filePath)
76
+ assertRegularFileOrMissing(filePath)
77
+ assertNoSymlinkAncestors(temporary)
78
+
79
+ let fd
80
+ let createdTemporary = false
81
+ try {
82
+ fd = openSync(temporary, 'wx', 0o600)
83
+ createdTemporary = true
84
+ writeFileSync(fd, content)
85
+ fsyncSync(fd)
86
+ closeSync(fd)
87
+ fd = undefined
88
+
89
+ verifyRegularFile(temporary, content)
90
+ assertNoSymlinkAncestors(filePath)
91
+ assertRegularFileOrMissing(filePath)
92
+ renameSync(temporary, filePath)
93
+ createdTemporary = false
94
+ verifyRegularFile(filePath, content)
95
+ } finally {
96
+ if (fd !== undefined) closeSync(fd)
97
+ if (createdTemporary) {
98
+ try {
99
+ assertNoSymlinkAncestors(temporary)
100
+ unlinkSync(temporary)
101
+ } catch {
102
+ // Preserve the write error; do not follow or remove an unsafe path.
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function assertRegularFileOrMissing(filePath) {
109
+ try {
110
+ const info = lstatSync(filePath)
111
+ if (!info.isFile() || info.isSymbolicLink()) {
112
+ throw new Error(`Refusing to replace non-regular file: ${filePath}`)
113
+ }
114
+ } catch (error) {
115
+ if (error?.code !== 'ENOENT') throw error
116
+ }
117
+ }
118
+
119
+ /** Verify a managed path was removed, without following symlinks. */
120
+ export function verifyPathRemoved(filePath) {
121
+ try {
122
+ lstatSync(filePath)
123
+ } catch (error) {
124
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return true
125
+ throw error
126
+ }
127
+ throw new Error(`Managed-path removal verification failed: ${filePath}`)
128
+ }
@@ -20,6 +20,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
20
20
  import { homedir } from 'node:os'
21
21
  import { dirname, join } from 'node:path'
22
22
  import { writeConfigWithBackup } from './shared.mjs'
23
+ import { verifyRegularFile } from './verify.mjs'
23
24
 
24
25
  const CONFIG_FILENAME = 'opencode-vision.json'
25
26
  /** `provider/model` — exactly one slash, no surrounding whitespace. */
@@ -87,6 +88,7 @@ export function mergeVisionModel(
87
88
  if (existing === null) return
88
89
  try {
89
90
  copyFileSync(configPath, `${configPath}.bak`)
91
+ verifyRegularFile(`${configPath}.bak`, readFileSync(configPath))
90
92
  } catch {
91
93
  // Best-effort: a backup failure must not block the write.
92
94
  }
@@ -238,7 +238,7 @@ export const MCPS = {
238
238
  tier: 1,
239
239
  name: 'Pantheon Memory',
240
240
  description:
241
- 'Pantheon Memory MCP Server — persistent memory with semantic search, recall, knowledge graph, compression, and verification',
241
+ 'Pantheon Memory MCP Server — persistent SQLite memory with FTS5/BM25 keyword search, exact-key recall, and explicit maintenance; no embedding/vector search',
242
242
  platforms: {
243
243
  opencode: {
244
244
  type: 'local',
@@ -57,6 +57,18 @@ export const MANIFEST_INVENTORY = Object.freeze([
57
57
  name: 'pantheon-tui',
58
58
  valuePath: ['name'],
59
59
  }),
60
+ Object.freeze({
61
+ id: 'zenodo-release-metadata',
62
+ role: 'metadata',
63
+ file: '.zenodo.json',
64
+ kind: 'json',
65
+ }),
66
+ Object.freeze({
67
+ id: 'citation-release-metadata',
68
+ role: 'metadata',
69
+ file: 'CITATION.cff',
70
+ kind: 'cff',
71
+ }),
60
72
  Object.freeze({
61
73
  id: 'root-lock',
62
74
  role: 'lock',
@@ -76,7 +88,7 @@ export const MANIFEST_INVENTORY = Object.freeze([
76
88
  ])
77
89
 
78
90
  export const VERSION_MANIFEST_INVENTORY = Object.freeze(
79
- MANIFEST_INVENTORY.filter(({ role }) => role === 'manifest'),
91
+ MANIFEST_INVENTORY.filter(({ role }) => role !== 'lock'),
80
92
  )
81
93
 
82
94
  export const LOCK_INVENTORY = Object.freeze(
@@ -99,11 +111,18 @@ function readToml(file, text) {
99
111
  return parsed
100
112
  }
101
113
 
114
+ function readCff(file, text) {
115
+ const match = /^version:\s*([^\s#]+)\s*(?:#.*)?$/m.exec(text)
116
+ if (!match) throw new Error(`${file} is missing top-level version metadata`)
117
+ return { version: match[1] }
118
+ }
119
+
102
120
  function readDocument(root, entry) {
103
121
  const path = join(root, entry.file)
104
122
  if (!existsSync(path)) throw new Error(`manifest inventory file is missing: ${entry.file}`)
105
123
  const text = readFileSync(path, 'utf8')
106
124
  if (entry.kind === 'toml') return { entry, path, text, data: readToml(entry.file, text) }
125
+ if (entry.kind === 'cff') return { entry, path, text, data: readCff(entry.file, text) }
107
126
  let data
108
127
  try {
109
128
  data = JSON.parse(text)
@@ -175,6 +194,24 @@ function validateLock(document, manifestsByFile, allowVersionDivergence) {
175
194
  if (!allowVersionDivergence && rootPackage.version !== manifestVersionValue) {
176
195
  throw new Error(`${entry.file} packages[""] version diverges from ${entry.packageFile}`)
177
196
  }
197
+
198
+ if (entry.file === 'package-lock.json') {
199
+ const tuiManifest = manifestsByFile.get('src/plugins/tui/package.json')
200
+ const tuiWorkspace = data.packages?.['src/plugins/tui']
201
+ if (!tuiManifest) throw new Error('package-lock.json has no TUI workspace manifest partner')
202
+ if (!tuiWorkspace || typeof tuiWorkspace !== 'object' || Array.isArray(tuiWorkspace)) {
203
+ throw new Error('package-lock.json is missing packages["src/plugins/tui"]')
204
+ }
205
+ if (tuiWorkspace.name !== 'pantheon-tui') {
206
+ throw new Error('package-lock.json TUI workspace name must be pantheon-tui')
207
+ }
208
+ assertSemver(tuiWorkspace.version, 'package-lock.json TUI workspace version')
209
+ if (!allowVersionDivergence && tuiWorkspace.version !== manifestVersion(tuiManifest)) {
210
+ throw new Error(
211
+ 'package-lock.json TUI workspace version diverges from src/plugins/tui/package.json',
212
+ )
213
+ }
214
+ }
178
215
  }
179
216
 
180
217
  /** Read and validate every entry before a caller is allowed to write anything. */
@@ -186,7 +223,7 @@ export function validateInventory(root, { allowVersionDivergence = false } = {})
186
223
  .map((document) => [document.entry.file, document]),
187
224
  )
188
225
  const versions = documents
189
- .filter(({ entry }) => entry.role === 'manifest')
226
+ .filter(({ entry }) => entry.role === 'manifest' || entry.role === 'metadata')
190
227
  .map((document) => ({ file: document.entry.file, version: manifestVersion(document) }))
191
228
  documents.filter(({ entry }) => entry.role === 'manifest').forEach(manifestName)
192
229
  const sourceVersion = versions[0].version
@@ -309,8 +346,17 @@ function replaceTomlProjectVersion(text, version, file) {
309
346
 
310
347
  function prepareDocumentVersion(document, version) {
311
348
  const { entry, text } = document
349
+ if (entry.role === 'metadata') {
350
+ const current = manifestVersion(document)
351
+ return text.split(current).join(version)
352
+ }
312
353
  if (entry.kind === 'toml') return replaceTomlProjectVersion(text, version, entry.file)
313
- const paths = entry.role === 'lock' ? [['version'], ['packages', '', 'version']] : [['version']]
354
+ const paths =
355
+ entry.role === 'lock'
356
+ ? entry.file === 'package-lock.json'
357
+ ? [['version'], ['packages', '', 'version'], ['packages', 'src/plugins/tui', 'version']]
358
+ : [['version'], ['packages', '', 'version']]
359
+ : [['version']]
314
360
  return replaceJsonStringValues(text, paths, version, entry.file)
315
361
  }
316
362
 
@@ -0,0 +1,85 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const MIN_SUPPORTED_NODE_VERSION = '26.4.0'
5
+ export const REQUIRED_NODE_ENGINE_RANGE = `>=${MIN_SUPPORTED_NODE_VERSION}`
6
+
7
+ /** Return actionable, user-managed upgrade guidance for an unsupported runtime. */
8
+ export function unsupportedNodeVersionMessage(version) {
9
+ return [
10
+ `❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${version})`,
11
+ ' Install and switch with a user-managed Node version manager:',
12
+ ` nvm install ${MIN_SUPPORTED_NODE_VERSION} && nvm use ${MIN_SUPPORTED_NODE_VERSION}`,
13
+ ` fnm install ${MIN_SUPPORTED_NODE_VERSION} && fnm use ${MIN_SUPPORTED_NODE_VERSION}`,
14
+ ` volta install node@${MIN_SUPPORTED_NODE_VERSION}`,
15
+ ].join('\n')
16
+ }
17
+
18
+ function parseNodeVersion(version) {
19
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+([\w.-]+))?$/.exec(String(version))
20
+ if (!match) return null
21
+ return { parts: match.slice(1, 4).map(Number), prerelease: match[4] !== undefined }
22
+ }
23
+
24
+ /** Return whether a runtime version meets Pantheon's supported Node floor. */
25
+ export function isSupportedNodeVersion(version) {
26
+ const parsed = parseNodeVersion(version)
27
+ const minimum = parseNodeVersion(MIN_SUPPORTED_NODE_VERSION)
28
+ if (!parsed || !minimum || parsed.prerelease) return false
29
+ for (let index = 0; index < minimum.parts.length; index += 1) {
30
+ if (parsed.parts[index] !== minimum.parts[index]) {
31
+ return parsed.parts[index] > minimum.parts[index]
32
+ }
33
+ }
34
+ return true
35
+ }
36
+
37
+ function readJson(root, file, errors) {
38
+ try {
39
+ const document = JSON.parse(readFileSync(join(root, file), 'utf8'))
40
+ if (!document || typeof document !== 'object' || Array.isArray(document)) {
41
+ throw new Error('expected a JSON object')
42
+ }
43
+ return document
44
+ } catch (error) {
45
+ errors.push(`${file}: ${error.message}`)
46
+ return null
47
+ }
48
+ }
49
+
50
+ function checkEngineRange(value, label, errors) {
51
+ if (value !== REQUIRED_NODE_ENGINE_RANGE) {
52
+ errors.push(`${label} must declare engines.node as "${REQUIRED_NODE_ENGINE_RANGE}"`)
53
+ }
54
+ }
55
+
56
+ /** Validate Node support claims in package manifests and their npm locks. */
57
+ export function validateNodeEnginePolicy(root) {
58
+ const errors = []
59
+ const rootPackage = readJson(root, 'package.json', errors)
60
+ const tuiPackage = readJson(root, 'src/plugins/tui/package.json', errors)
61
+ const rootLock = readJson(root, 'package-lock.json', errors)
62
+ const tuiLock = readJson(root, 'src/plugins/tui/package-lock.json', errors)
63
+
64
+ if (rootPackage) checkEngineRange(rootPackage.engines?.node, 'package.json', errors)
65
+ if (tuiPackage) checkEngineRange(tuiPackage.engines?.node, 'src/plugins/tui/package.json', errors)
66
+
67
+ const rootLockPackage = rootLock?.packages?.['']
68
+ if (!rootLockPackage) errors.push('package-lock.json is missing packages[""]')
69
+ else checkEngineRange(rootLockPackage.engines?.node, 'package-lock.json packages[""]', errors)
70
+
71
+ const workspacePackage = rootLock?.packages?.['src/plugins/tui']
72
+ if (!workspacePackage) errors.push('package-lock.json is missing the TUI workspace package')
73
+ else checkEngineRange(workspacePackage.engines?.node, 'package-lock.json TUI workspace', errors)
74
+
75
+ const tuiLockPackage = tuiLock?.packages?.['']
76
+ if (!tuiLockPackage) errors.push('src/plugins/tui/package-lock.json is missing packages[""]')
77
+ else
78
+ checkEngineRange(
79
+ tuiLockPackage.engines?.node,
80
+ 'src/plugins/tui/package-lock.json packages[""]',
81
+ errors,
82
+ )
83
+
84
+ return errors
85
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import { tmpdir } from 'node:os'
18
18
  import { isAbsolute, join } from 'node:path'
19
19
  import { validateInventory } from './manifest-inventory.mjs'
20
+ import { validateNodeEnginePolicy } from './node-engine-policy.mjs'
20
21
 
21
22
  const CI_ARGS = ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund']
22
23
  const REQUIRED_PAYLOAD = [
@@ -32,6 +33,8 @@ const REQUIRED_PAYLOAD = [
32
33
  'package/src/plugins/tui/dist/server.js',
33
34
  'package/bin/pantheon-init.mjs',
34
35
  'package/scripts/doctor.mjs',
36
+ 'package/scripts/node-engine-policy.mjs',
37
+ 'package/scripts/validate-node-engine-policy.mjs',
35
38
  'package/.pantheon/code-mode/compress-inline.py',
36
39
  ]
37
40
 
@@ -132,6 +135,7 @@ function validatePayload(packageRoot, inventory) {
132
135
  }
133
136
  for (const document of inventory.documents) {
134
137
  const { entry, path: sourcePath } = document
138
+ if (entry.role === 'metadata') continue
135
139
  const packagedPath = join(packageRoot, entry.file)
136
140
  if (entry.role === 'lock' && entry.file === 'package-lock.json' && !existsSync(packagedPath)) {
137
141
  // npm-packlist excludes the package root lockfile by policy. It remains
@@ -181,6 +185,8 @@ function sha256(path) {
181
185
 
182
186
  export function createPackageEvidence(root, { outputDir, targetSha } = {}) {
183
187
  const inventory = validateInventory(root)
188
+ const engineErrors = validateNodeEnginePolicy(root)
189
+ if (engineErrors.length) fail(engineErrors.join('; '))
184
190
  const sha = resolveTargetSha(root, targetSha)
185
191
  const output = createOutputDirectory(outputDir)
186
192
  let workDir
@@ -5,37 +5,37 @@
5
5
  * Runs after `npm install` completes. Validates environment and prints
6
6
  * setup instructions. Does NOT install Pantheon platform components.
7
7
  */
8
- const REQUIRED_NODE_MAJOR = 18
9
- const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
8
+ import { isSupportedNodeVersion, unsupportedNodeVersionMessage } from './node-engine-policy.mjs'
10
9
 
11
10
  try {
12
- // Validate Node.js version
13
- if (nodeMajor < REQUIRED_NODE_MAJOR) {
14
- console.error(
15
- `❌ Node.js >= ${REQUIRED_NODE_MAJOR} required (current: ${process.versions.node})`,
11
+ // npm may warn about `engines.node`, but must still finish installing so the
12
+ // bin can print the actionable hard-gate message. No Node-dependent setup is
13
+ // attempted from an incompatible lifecycle runtime.
14
+ if (!isSupportedNodeVersion(process.versions.node)) {
15
+ console.warn(
16
+ `⚠️ Pantheon postinstall skipped because this runtime is unsupported.\n${unsupportedNodeVersionMessage(process.versions.node)}\n Switch to Node.js >= 26.4.0 before running Pantheon setup.`,
16
17
  )
17
- process.exit(1)
18
- }
18
+ } else {
19
+ // Validate js-yaml is available
20
+ try {
21
+ await import('js-yaml')
22
+ } catch {
23
+ console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
24
+ }
19
25
 
20
- // Validate js-yaml is available
21
- try {
22
- await import('js-yaml')
23
- } catch {
24
- console.warn('⚠️ js-yaml not found — run `npm install` to install dependencies')
26
+ console.log('')
27
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
28
+ console.log(' Pantheon OpenCode — npm dependencies installed ✅')
29
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
30
+ console.log('')
31
+ console.log(' Next steps:')
32
+ console.log(' 1. Configure Pantheon:')
33
+ console.log(' npm run setup')
34
+ console.log(' # or: npx pantheon-opencode init')
35
+ console.log(' 2. Verify installation:')
36
+ console.log(' npm run doctor')
37
+ console.log('')
25
38
  }
26
-
27
- console.log('')
28
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
29
- console.log(' Pantheon OpenCode — npm dependencies installed ✅')
30
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
31
- console.log('')
32
- console.log(' Next steps:')
33
- console.log(' 1. Configure Pantheon:')
34
- console.log(' npm run setup')
35
- console.log(' # or: npx pantheon-opencode init')
36
- console.log(' 2. Verify installation:')
37
- console.log(' npm run doctor')
38
- console.log('')
39
39
  } catch (err) {
40
40
  console.error(`❌ Post-install check failed: ${err.message}`)
41
41
  process.exit(1)