dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,203 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]{0,126}\/[a-z0-9][a-z0-9._-]{0,126}|[a-z0-9][a-z0-9._-]{0,213})$/i
const SELECTION_SCHEMA = 1
function packagePath(root, packageName) {
if (!PACKAGE_NAME_PATTERN.test(packageName)) return undefined
return join(root, 'node_modules', ...packageName.split('/'), 'package.json')
}
async function optionalBytes(path) {
return readFile(path).then(
bytes => ({ present: true, bytes }),
error => error?.code === 'ENOENT' ? { present: false, bytes: Buffer.alloc(0) } : Promise.reject(error),
)
}
function parseObject(bytes, label) {
let value
try {
value = JSON.parse(bytes.toString('utf8'))
} catch (error) {
throw new Error(`${label} is not valid JSON`, { cause: error })
}
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must contain a JSON object`)
}
return value
}
function profileFields(manifest) {
const dependencies = manifest.dependencies ?? {}
if (dependencies === null || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
throw new Error('Web Profile dependencies must be an object')
}
for (const [name, spec] of Object.entries(dependencies)) {
if (typeof spec !== 'string' || spec.length === 0) {
throw new Error(`Web Profile dependency ${name} must have a non-empty string spec`)
}
}
const bundles = manifest.dsh?.profile?.bundles ?? []
if (!Array.isArray(bundles) || bundles.some(name => typeof name !== 'string')) {
throw new Error('Web Profile dsh.profile.bundles must be a string array')
}
return { dependencies, bundles }
}
function disabledPositions(value) {
if (value === undefined) return new Map()
if (value === null || typeof value !== 'object' || Array.isArray(value)
|| value.schema !== SELECTION_SCHEMA || !Array.isArray(value.disabled)) {
throw new Error('User Plugin selection state is invalid')
}
const positions = new Map()
for (const entry of value.disabled) {
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)
|| !PACKAGE_NAME_PATTERN.test(entry.name) || !Number.isSafeInteger(entry.index) || entry.index < 0) {
throw new Error('User Plugin disabled-order entry is invalid')
}
if (positions.has(entry.name)) throw new Error(`User Plugin ${entry.name} has duplicate disabled-order entries`)
positions.set(entry.name, entry.index)
}
return positions
}
function sourceForSpec(spec) {
if (/^(?:file|link):/i.test(spec) || /^(?:\.{1,2}\/|\/)/.test(spec)) return 'file'
if (/^(?:git(?:\+[^:]+)?|github|gitlab|bitbucket):/i.test(spec) || /\.git(?:#|$)/i.test(spec)) return 'git'
if (/^https?:/i.test(spec)) return 'url'
if (/^npm:/i.test(spec) || /^(?:[~^<>=*]|\d|v\d)/i.test(spec) || spec === 'latest' || spec === 'next') return 'registry'
return 'other'
}
function updateRevision(hash, label, file) {
hash.update(`${label}:${file.present ? '1' : '0'}:${String(file.bytes.length)}:`)
hash.update(file.bytes)
hash.update('\0')
}
function metadataFailure(error) {
if (error?.code === 'ENOENT') return 'installed package metadata is missing'
if (error instanceof SyntaxError) return 'installed package metadata is not valid JSON'
return error instanceof Error ? error.message : String(error)
}
async function inspectDependency(profileRoot, name, spec, enabledBundles, disabled, reservedNames) {
const path = packagePath(profileRoot, name)
let metadata
let metadataError = null
if (path === undefined) {
metadataError = 'dependency name is invalid'
} else {
try {
metadata = JSON.parse(await readFile(path, 'utf8'))
if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
throw new Error('installed package metadata must be an object')
}
if (metadata.name !== name) throw new Error(`installed package name is ${String(metadata.name)}`)
} catch (error) {
metadataError = metadataFailure(error)
}
}
if (metadataError === null && metadata.version !== undefined
&& (typeof metadata.version !== 'string' || metadata.version.length === 0)) {
metadataError = 'installed package version is invalid'
}
const bundleDeclaration = metadata?.dsh?.bundle
const patch = bundleDeclaration?.patch
const isBundle = typeof patch === 'string' && patch.length > 0
const reservedNameConflict = reservedNames.has(name)
if (metadataError === null && !isBundle
&& (bundleDeclaration !== undefined || enabledBundles.has(name))) {
metadataError = 'installed package Bundle metadata is invalid'
}
if (!isBundle && metadataError === null && !reservedNameConflict) return undefined
return Object.freeze({
name,
spec,
source: sourceForSpec(spec),
version: typeof metadata?.version === 'string' ? metadata.version : null,
description: typeof metadata?.description === 'string' && metadata.description.trim() !== '' ? metadata.description.trim() : null,
enabled: !reservedNameConflict && enabledBundles.has(name),
previousIndex: disabled.get(name) ?? null,
damaged: metadataError !== null,
metadataError,
reservedNameConflict,
})
}
function loadedIdentity(plugin) {
return JSON.stringify([
plugin.name, plugin.spec, plugin.version, plugin.enabled, plugin.damaged,
plugin.metadataError, plugin.reservedNameConflict,
])
}
export function markUserPluginRestartState(current, loaded) {
if (loaded === undefined) return current
const loadedPlugins = new Map(loaded.plugins.map(plugin => [plugin.name, loadedIdentity(plugin)]))
const plugins = current.plugins.map(plugin => Object.freeze({
...plugin,
pendingRestart: loadedPlugins.get(plugin.name) !== loadedIdentity(plugin),
}))
const currentNames = new Set(current.plugins.map(plugin => plugin.name))
const removedLoadedPlugin = loaded.plugins.some(plugin => !currentNames.has(plugin.name))
return Object.freeze({
...current,
plugins: Object.freeze(plugins),
restartRequired: removedLoadedPlugin || plugins.some(plugin => plugin.pendingRestart),
})
}
export class UserPluginInventory {
constructor({ dshHome = '/data/dsh', profile = 'web', selectionPath, systemPluginNames = async () => [] } = {}) {
if (profile !== 'web') throw new Error('Only the web DSH Profile is supported')
this.profile = profile
this.profileRoot = join(resolve(dshHome), 'profiles', profile)
this.selectionPath = selectionPath === undefined
? join(resolve(dshHome), '..', 'platform', 'state', 'management', 'user-plugins.json')
: resolve(selectionPath)
this.systemPluginNames = systemPluginNames
}
async read() {
const manifestPath = join(this.profileRoot, 'package.json')
const lockfilePath = join(this.profileRoot, 'pnpm-lock.yaml')
const [manifestBytes, lockfile, selectionFile, names] = await Promise.all([
readFile(manifestPath),
optionalBytes(lockfilePath),
optionalBytes(this.selectionPath),
this.systemPluginNames(),
])
const manifest = parseObject(manifestBytes, 'Web Profile package.json')
const { dependencies, bundles } = profileFields(manifest)
const selection = !selectionFile.present
? undefined
: parseObject(selectionFile.bytes, 'User Plugin selection state')
const disabled = disabledPositions(selection)
const reservedNames = new Set(names)
if ([...reservedNames].some(name => typeof name !== 'string' || !PACKAGE_NAME_PATTERN.test(name))) {
throw new Error('Verified System Plugin names are invalid')
}
const enabledBundles = new Set(bundles)
const inspected = await Promise.all(Object.entries(dependencies).map(([name, spec]) => (
inspectDependency(this.profileRoot, name, spec, enabledBundles, disabled, reservedNames)
)))
const revisionHash = createHash('sha256')
updateRevision(revisionHash, 'manifest', { present: true, bytes: manifestBytes })
updateRevision(revisionHash, 'lockfile', lockfile)
updateRevision(revisionHash, 'selection', selectionFile)
const revision = revisionHash.digest('hex')
return Object.freeze({
schema: 1,
profile: this.profile,
revision: `sha256:${revision}`,
plugins: Object.freeze(inspected.filter(Boolean)),
})
}
}
export const userPluginInternals = Object.freeze({ PACKAGE_NAME_PATTERN, sourceForSpec, loadedIdentity })