Files
dsh-web/image/reconcile-profile.mjs
T

122 lines
5.3 KiB
JavaScript

import { lstatSync, readFileSync, writeFileSync, openSync, closeSync, fsyncSync, renameSync, unlinkSync } from 'node:fs';
import { dirname, join, resolve, basename } from 'node:path';
import { randomUUID } from 'node:crypto';
export const BUILTIN_DEPENDENCIES = Object.freeze({
'dsh-preview-download': 'file:/usr/local/lib/dsh-mgr/dsh-preview-download-1.0.1.tgz',
'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.16-dsh-mgr.1.tgz',
'dsh-mgr-workspace': 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz',
'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.1.0.tgz',
'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.9.2.tgz',
});
export const BUILTIN_OVERRIDES = Object.freeze({
'dsh-chat-manager>clsx': 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz',
'dsh-free-search>@deepseek-ai/schemastery': 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz',
'@deepseek-ai/schemastery>@deepseek-ai/cosmokit': 'file:/usr/local/lib/dsh-mgr/cosmokit-1.8.5.tgz',
'@deepseek-ai/schemastery>@standard-schema/spec': 'file:/usr/local/lib/dsh-mgr/spec-1.1.0.tgz',
});
const sections = ['dependencies', 'devDependencies', 'optionalDependencies'];
const overrideOwners = ['dsh-chat-manager', 'dsh-free-search', 'dsh-free-search', 'dsh-free-search'];
const semver = '(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?';
function object(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`invalid ${label}`);
}
export function reconcileManifest(input) {
object(input, 'manifest');
const manifest = structuredClone(input);
const active = new Set();
const archives = new Set();
let changed = false;
for (const section of sections) {
if (manifest[section] === undefined) continue;
object(manifest[section], section);
for (const [name, target] of Object.entries(BUILTIN_DEPENDENCIES)) {
const current = manifest[section][name];
if (typeof current !== 'string' || !new RegExp(`^file:/usr/local/lib/dsh-mgr/${name}-${semver}\\.tgz$`).test(current)) continue;
active.add(name);
archives.add(target);
if (current !== target) {
manifest[section][name] = target;
changed = true;
}
}
}
if (manifest.pnpm !== undefined) {
object(manifest.pnpm, 'pnpm');
if (manifest.pnpm.overrides !== undefined) object(manifest.pnpm.overrides, 'pnpm.overrides');
}
Object.entries(BUILTIN_OVERRIDES).forEach(([key, target], index) => {
if (!active.has(overrideOwners[index])) return;
manifest.pnpm ??= {};
manifest.pnpm.overrides ??= {};
if (!Object.hasOwn(manifest.pnpm.overrides, key)) {
manifest.pnpm.overrides[key] = target;
changed = true;
}
if (manifest.pnpm.overrides[key] === target) archives.add(target);
});
return { manifest, changed, archives: [...archives] };
}
function statIfPresent(path) {
try { return lstatSync(path); } catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
export function assertSafeProfile(profile) {
for (let dir = resolve(profile); ; dir = dirname(dir)) {
const stat = statIfPresent(dir);
if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) throw new Error(`unsafe profile directory: ${dir}`);
if (dirname(dir) === dir) break;
}
for (const name of ['package.json', 'pnpm-workspace.yaml']) {
const path = join(profile, name);
const stat = statIfPresent(path);
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error(`unsafe profile file: ${path}`);
}
}
export function reconcileProfile(profile, { archiveDirectory = '/usr/local/lib/dsh-mgr' } = {}) {
assertSafeProfile(profile);
const path = join(profile, 'package.json');
if (!statIfPresent(path)) return { changed: false };
const original = readFileSync(path);
const result = reconcileManifest(JSON.parse(original.toString('utf8')));
for (const reference of result.archives) {
const archive = join(archiveDirectory, basename(reference.slice(5)));
const stat = lstatSync(archive);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`unsafe builtin archive: ${archive}`);
}
if (!result.changed) return { changed: false };
const backupPath = `${path}.backup-${randomUUID()}`;
const temporaryPath = `${path}.tmp-${randomUUID()}`;
let temporaryCreated = false;
try {
const backupFd = openSync(backupPath, 'wx', 0o600);
try { writeFileSync(backupFd, original); fsyncSync(backupFd); }
finally { closeSync(backupFd); }
const syncDirectory = () => {
const directoryFd = openSync(profile, 'r');
try { fsyncSync(directoryFd); } finally { closeSync(directoryFd); }
};
// Make the recovery copy durable before replacing the active manifest.
syncDirectory();
const fd = openSync(temporaryPath, 'wx', 0o600);
temporaryCreated = true;
try {
writeFileSync(fd, JSON.stringify(result.manifest, null, 2) + '\n');
fsyncSync(fd);
} finally { closeSync(fd); }
assertSafeProfile(profile);
renameSync(temporaryPath, path);
temporaryCreated = false;
syncDirectory();
} finally {
if (temporaryCreated) unlinkSync(temporaryPath);
}
return { changed: true, backupPath };
}