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,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
|
}
|
package/scripts/install-mcp.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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,74 @@
|
|
|
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
|
+
function parseNodeVersion(version) {
|
|
8
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+([\w.-]+))?$/.exec(String(version))
|
|
9
|
+
if (!match) return null
|
|
10
|
+
return { parts: match.slice(1, 4).map(Number), prerelease: match[4] !== undefined }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Return whether a runtime version meets Pantheon's supported Node floor. */
|
|
14
|
+
export function isSupportedNodeVersion(version) {
|
|
15
|
+
const parsed = parseNodeVersion(version)
|
|
16
|
+
const minimum = parseNodeVersion(MIN_SUPPORTED_NODE_VERSION)
|
|
17
|
+
if (!parsed || !minimum || parsed.prerelease) return false
|
|
18
|
+
for (let index = 0; index < minimum.parts.length; index += 1) {
|
|
19
|
+
if (parsed.parts[index] !== minimum.parts[index]) {
|
|
20
|
+
return parsed.parts[index] > minimum.parts[index]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readJson(root, file, errors) {
|
|
27
|
+
try {
|
|
28
|
+
const document = JSON.parse(readFileSync(join(root, file), 'utf8'))
|
|
29
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
30
|
+
throw new Error('expected a JSON object')
|
|
31
|
+
}
|
|
32
|
+
return document
|
|
33
|
+
} catch (error) {
|
|
34
|
+
errors.push(`${file}: ${error.message}`)
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function checkEngineRange(value, label, errors) {
|
|
40
|
+
if (value !== REQUIRED_NODE_ENGINE_RANGE) {
|
|
41
|
+
errors.push(`${label} must declare engines.node as "${REQUIRED_NODE_ENGINE_RANGE}"`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Validate Node support claims in package manifests and their npm locks. */
|
|
46
|
+
export function validateNodeEnginePolicy(root) {
|
|
47
|
+
const errors = []
|
|
48
|
+
const rootPackage = readJson(root, 'package.json', errors)
|
|
49
|
+
const tuiPackage = readJson(root, 'src/plugins/tui/package.json', errors)
|
|
50
|
+
const rootLock = readJson(root, 'package-lock.json', errors)
|
|
51
|
+
const tuiLock = readJson(root, 'src/plugins/tui/package-lock.json', errors)
|
|
52
|
+
|
|
53
|
+
if (rootPackage) checkEngineRange(rootPackage.engines?.node, 'package.json', errors)
|
|
54
|
+
if (tuiPackage) checkEngineRange(tuiPackage.engines?.node, 'src/plugins/tui/package.json', errors)
|
|
55
|
+
|
|
56
|
+
const rootLockPackage = rootLock?.packages?.['']
|
|
57
|
+
if (!rootLockPackage) errors.push('package-lock.json is missing packages[""]')
|
|
58
|
+
else checkEngineRange(rootLockPackage.engines?.node, 'package-lock.json packages[""]', errors)
|
|
59
|
+
|
|
60
|
+
const workspacePackage = rootLock?.packages?.['src/plugins/tui']
|
|
61
|
+
if (!workspacePackage) errors.push('package-lock.json is missing the TUI workspace package')
|
|
62
|
+
else checkEngineRange(workspacePackage.engines?.node, 'package-lock.json TUI workspace', errors)
|
|
63
|
+
|
|
64
|
+
const tuiLockPackage = tuiLock?.packages?.['']
|
|
65
|
+
if (!tuiLockPackage) errors.push('src/plugins/tui/package-lock.json is missing packages[""]')
|
|
66
|
+
else
|
|
67
|
+
checkEngineRange(
|
|
68
|
+
tuiLockPackage.engines?.node,
|
|
69
|
+
'src/plugins/tui/package-lock.json packages[""]',
|
|
70
|
+
errors,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return errors
|
|
74
|
+
}
|
|
@@ -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
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
* Runs after `npm install` completes. Validates environment and prints
|
|
6
6
|
* setup instructions. Does NOT install Pantheon platform components.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
|
|
8
|
+
import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_VERSION } from './node-engine-policy.mjs'
|
|
10
9
|
|
|
11
10
|
try {
|
|
12
11
|
// Validate Node.js version
|
|
13
|
-
if (
|
|
12
|
+
if (!isSupportedNodeVersion(process.versions.node)) {
|
|
14
13
|
console.error(
|
|
15
|
-
`❌ Node.js >= ${
|
|
14
|
+
`❌ Pantheon requires Node.js >= ${MIN_SUPPORTED_NODE_VERSION} (current: ${process.versions.node})`,
|
|
16
15
|
)
|
|
17
16
|
process.exit(1)
|
|
18
17
|
}
|