Files

144 lines
11 KiB
JavaScript

import { execFile } from 'node:child_process';
import { access, copyFile, mkdir, readFile, realpath, readdir, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { withoutRedundantAgentTeamProfile } from './preview-agent-team.mjs';
const root = fileURLToPath(new URL('..', import.meta.url));
const rootManifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
const baseVersion = rootManifest.pnpm?.overrides?.['@deepseek-ai/dsh-base'];
const webAppVersion = rootManifest.pnpm?.overrides?.['@deepseek-ai/dsh-web-app'];
if (typeof baseVersion !== 'string') throw new Error('Missing pinned @deepseek-ai/dsh-base version in package.json pnpm.overrides.');
if (typeof webAppVersion !== 'string') throw new Error('Missing pinned @deepseek-ai/dsh-web-app version in package.json pnpm.overrides.');
const baseSpec = `@deepseek-ai/dsh-base@${baseVersion}`;
const webAppSpec = `@deepseek-ai/dsh-web-app@${webAppVersion}`;
const marketVersion = '1.66.1';
const marketSpec = `dshmarket@${marketVersion}`;
const cliVersion = JSON.parse(await readFile(join(root, 'node_modules/@deepseek-ai/dsh/package.json'), 'utf8')).version;
if (cliVersion !== baseVersion) throw new Error('Preview CLI and Base must use the same pinned version.');
const home = resolve(process.env.WORKDSH_PREVIEW_HOME ?? join(root, '.test-runtime/preview'));
const artifacts = join(root, '.artifacts');
// pnpm includes local archive paths in its store index filename. Keep the
// immutable archive path short even when this checkout is a nested worktree.
const previewPacks = resolve(process.env.WORKDSH_PREVIEW_PACKS_HOME ?? join(homedir(), '.cache/workdsh-preview-packs'));
const env = { ...process.env, DSH_HOME: home, DSH_AGENTS_HOME: process.env.DSH_AGENTS_HOME ?? join(home, 'agents'), PATH: `${join(root, 'node_modules/.bin')}:${dirname(process.execPath)}:${process.env.PATH}` };
const exec = promisify(execFile);
const run = async (tool, args) => {
await exec(process.execPath, [join(root, 'node_modules', tool), ...args], { cwd: root, env, timeout: 600_000, maxBuffer: 8 * 1024 * 1024 });
};
await mkdir(home, { recursive: true }); await mkdir(artifacts, { recursive: true });
const tarballs = [];
const packages = [];
for (const directory of ['packages/providers/identity-local', 'packages/providers/browser-session', 'packages/plugins/audit', 'packages/plugins/access', 'packages/plugins/skills', 'packages/plugins/experts', 'packages/plugins/connectors', 'packages/plugins/office', 'packages/plugins/library', 'packages/plugins/projects', 'packages/plugins/activity', 'packages/bundle']) {
const manifest = JSON.parse(await readFile(join(root, directory, 'package.json'), 'utf8'));
await access(join(root, directory, manifest.exports['.'].default));
await run('pnpm/bin/pnpm.cjs', ['--filter', manifest.name, 'pack', '--pack-destination', artifacts]);
const packed = join(artifacts, `${manifest.name}-${manifest.version}.tgz`);
// Preview candidates can change before their next release. A stable file:
// address lets the package manager reuse an older archive, even after pack.
// Keep official CLI installation, but address each archive by its content.
const digest = createHash('sha256').update(await readFile(packed)).digest('hex');
const destination = join(previewPacks, digest.slice(0, 16));
await mkdir(destination, { recursive: true });
const immutableArchive = join(destination, `${manifest.name}-${manifest.version}.tgz`);
await copyFile(packed, immutableArchive);
tarballs.push(immutableArchive);
packages.push({ directory, manifest });
}
let initialized = false;
try { await access(join(home, 'profiles/preview/package.json')); initialized = true; }
catch (error) { if (error.code !== 'ENOENT') throw error; }
if (!initialized) await run('@deepseek-ai/dsh/lib/bin.js', ['--profile', 'preview', '--from-default-profile', 'web', '--dump-config']);
// Experts already contributes the official Team service, tools and Web action.
// The standalone Team profile adds the same loader ids again and can be toggled
// on from the upstream plugin gallery. Keep this preview's bundle list singular.
const previewManifestPath = join(home, 'profiles/preview/package.json');
const previewManifest = JSON.parse(await readFile(previewManifestPath, 'utf8'));
const normalizedManifest = withoutRedundantAgentTeamProfile(previewManifest);
if (normalizedManifest) {
await writeFile(previewManifestPath, `${JSON.stringify(normalizedManifest, null, 2)}\n`);
console.log('Removed redundant standalone Agent Team profile from preview; WorkDSH experts owns Team composition.');
}
// Reinstall the pinned official Web bundle as well as the WorkDSH layers. An
// existing preview Profile may have been created by an older DSH release; its
// bundle list alone does not upgrade the packages that provide newly added Web
// surfaces such as Terminal and archived-session recovery.
const currentDependencies = JSON.parse(await readFile(previewManifestPath, 'utf8')).dependencies ?? {};
const layersMatch = currentDependencies['@deepseek-ai/dsh-base'] === baseVersion
&& currentDependencies['@deepseek-ai/dsh-web-app'] === webAppVersion
&& currentDependencies.dshmarket === marketVersion
&& packages.every(({ manifest }, index) => currentDependencies[manifest.name] === `file:${tarballs[index]}`);
if (!layersMatch) {
await run('@deepseek-ai/dsh/lib/bin.js', ['plugin', '--profile', 'preview', 'add', baseSpec, webAppSpec, marketSpec, ...tarballs]);
}
// Boot and ConfigEditor share module-local registration in dsh-app-boot.
// Keep the official CLI in the Profile dependency graph as well: launching the
// workspace CLI beside separately installed Profile packages splits that state.
// Profiles disable automatic peer installation. The platform account adapter
// also needs its declared native account peer in this standalone runtime.
const installedVersion = async (name) => {
try { return JSON.parse(await readFile(join(home, 'profiles/preview/node_modules/@deepseek-ai', name, 'package.json'), 'utf8')).version; }
catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
};
if (await installedVersion('dsh') !== cliVersion || await installedVersion('dsh-deepseek-account') !== cliVersion || await installedVersion('cordis-plugin-group') !== '1.0.4') {
await run('pnpm/bin/pnpm.cjs', ['--dir', join(home, 'profiles/preview'), 'add', '--save-exact', `@deepseek-ai/dsh@${cliVersion}`, `@deepseek-ai/dsh-deepseek-account@${cliVersion}`, '@deepseek-ai/[email protected]']);
}
// DSH Profiles disable automatic peer installation. A fresh preview must
// install the same official runtime peer closure as the release installer.
const profile = join(home, 'profiles/preview');
const officialScope = join(profile, 'node_modules/@deepseek-ai');
for (let pass = 0; pass < 9; pass++) {
const missing = new Map();
for (const entry of await readdir(officialScope)) {
const manifestPath = join(officialScope, entry, 'package.json');
let manifest;
try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')); }
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
if (!name.startsWith('@deepseek-ai/') || manifest.peerDependenciesMeta?.[name]?.optional) continue;
if (await installedVersion(name.slice('@deepseek-ai/'.length))) continue;
const version = rootManifest.pnpm?.overrides?.[name] ?? (name.startsWith('@deepseek-ai/dsh-') ? baseVersion : range);
missing.set(name, `${name}@${version}`);
}
}
if (!missing.size) break;
if (pass === 8) throw new Error('Official preview runtime peer dependencies did not converge.');
await run('pnpm/bin/pnpm.cjs', ['--dir', profile, 'add', '--save-exact', ...missing.values()]);
}
const installedBase = JSON.parse(await readFile(join(home, 'profiles/preview/node_modules/@deepseek-ai/dsh-base/package.json'), 'utf8'));
if (installedBase.version !== baseVersion) throw new Error(`Installed @deepseek-ai/dsh-base ${installedBase.version} does not match pinned ${baseVersion}.`);
const installedWebApp = JSON.parse(await readFile(join(home, 'profiles/preview/node_modules/@deepseek-ai/dsh-web-app/package.json'), 'utf8'));
if (installedWebApp.version !== webAppVersion) throw new Error(`Installed @deepseek-ai/dsh-web-app ${installedWebApp.version} does not match pinned ${webAppVersion}.`);
// DSH scopes are module-instance local. Installing only the Web bundle
// beside a CLI-provided Base bundle can load two physical dsh-scope copies: the
// Agent consumers must resolve one shared scope module; otherwise new
// session fails as an "unscoped context". Resolve both consumers from the
// Profile and fail installation unless they share the exact same module file.
const profileRequire = createRequire(join(home, 'profiles/preview/package.json'));
const resolveProfileDependency = async (consumer, dependency) => {
const consumerManifest = profileRequire.resolve(`${consumer}/package.json`);
const consumerRequire = createRequire(consumerManifest);
return realpath(consumerRequire.resolve(`${dependency}/package.json`));
};
const cliBoot = await resolveProfileDependency('@deepseek-ai/dsh', '@deepseek-ai/dsh-app-boot');
const settingsBoot = await resolveProfileDependency('@deepseek-ai/dsh-config-editor', '@deepseek-ai/dsh-app-boot');
if (cliBoot !== settingsBoot) throw new Error('Preview CLI and ConfigEditor resolve different dsh-app-boot instances; settings cannot persist.');
const loopScope = await resolveProfileDependency('@deepseek-ai/dsh-agent-loop', '@deepseek-ai/dsh-scope');
const presetScope = await resolveProfileDependency('@deepseek-ai/dsh-agent-preset-registry', '@deepseek-ai/dsh-scope');
if (loopScope !== presetScope) throw new Error(`Preview loaded split @deepseek-ai/dsh-scope instances: agent-loop=${loopScope}; agent-preset-registry=${presetScope}.`);
for (const { directory, manifest } of packages) {
for (const face of ['.', './client']) {
const entry = manifest.exports[face]?.default;
if (!entry) continue;
const expected = await readFile(join(root, directory, entry));
const installed = await readFile(join(home, 'profiles/preview/node_modules', manifest.name, entry));
if (!expected.equals(installed)) throw new Error(`Installed ${manifest.name} ${face} differs from the current build; refusing to report a successful preview update.`);
}
}
console.log('Installed Skill, Expert, Connector, Office, Library, Projects and WorkDSH presentation as separate official Profile layers.');
console.log('Start the stopped preview with: corepack pnpm preview');