Ctrl K
ComunidadGuías…

scripts/manager-tooling.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/** Build only the manager closure; authors' plugin sources never enter this workspace. */
import { createHash } from 'node:crypto';
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { commandSpec, normalizeEnvironment } from '../packages/plugin-manager/src/process.mjs';

const isolated = ['--config.node-linker=isolated', '--config.dedupe-peer-dependents=false'];
export const managerToolInputs = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'packages/plugin-kit', 'packages/plugin-manager'];

/**
 * 管理器工具构建输入的哈希:上面每个路径下所有已跟踪文件的 Git 对象号。
 *
 * 输入没变时,上一次成功发布的管理器归档可以直接复用:构建一次实测 18.6 秒(pnpm 安装 +
 * kit 与 manager 两次构建 + 打包),而复用只需按摘要核验再安装一次(约 1.2 秒)。
 *
 * 哈希取自 Git 树而不是工作区:内容一致、不受时间戳影响,也不把未跟踪文件算成输入。
 */
export function managerToolInputsHash(git, revision) {
  const entries = [];
  for (const record of git(['ls-tree', '-r', '-z', revision, '--', ...managerToolInputs]).split('\0')) {
    if (!record) continue;
    const separator = record.indexOf('\t');
    const [, type, object] = record.slice(0, separator).split(' ');
    if (type === 'blob') entries.push(`${record.slice(separator + 1)}\0${object}`);
  }
  return createHash('sha256').update(entries.sort().join('\n')).digest('hex');
}

function command(bin, args, options) {
  const cli = commandSpec(bin, options);
  const result = spawnSync(cli.command, [...cli.prefix, ...args], { windowsHide: true, stdio: 'inherit', ...options });
  if (result.error) throw result.error;
  if (result.status !== 0) throw Object.assign(new Error(`管理器工具准备失败:${bin} ${args[0]} (${result.status ?? result.signal})`), { signal: result.signal });
  return result.stdout?.trim() ?? '';
}

/** Shared by the disposable local workspace and the Docker manager-builder stage. */
export function buildManagerArchive({ root, archive, execute = command, env = normalizeEnvironment(process.env) }) {
  if (!root || !archive) throw new Error('Manager tooling requires explicit root and archive paths.');
  root = resolve(root); archive = resolve(archive);
  if (existsSync(archive)) throw new Error(`管理器归档已存在,请使用新的输出位置:${archive}`);
  mkdirSync(dirname(archive), { recursive: true });
  const run = args => execute('pnpm', [...isolated, ...args], { cwd: root, env });
  run(['--filter', 'dsh-plugin-manager-workspace', '--filter', '@dsh-plugin-manager/plugin-manager...', 'install', '--frozen-lockfile']);
  for (const name of ['plugin-kit', 'plugin-manager']) run(['--filter', `@dsh-plugin-manager/${name}`, 'build']);
  run(['--filter', '@dsh-plugin-manager/plugin-manager', 'pack', '--out', archive]);
  if (!existsSync(archive)) throw new Error('管理器构建未生成归档。');
  return archive;
}

/** Source builds and release packaging install the same verified, relocatable archive. */
export function installManagerArchive({ archive, output, version, execute = command, env = normalizeEnvironment(process.env) }) {
  if (!archive || !output || !version) throw new Error('Manager installation requires archive, output and version.');
  archive = resolve(archive); output = resolve(output);
  if (!lstatSync(archive).isFile() || lstatSync(archive).isSymbolicLink()) throw new Error('Manager archive must be a regular file.');
  mkdirSync(output, { recursive: true });
  const installedArchive = resolve(output, 'plugin-manager.tgz');
  if (readdirSync(output).some(name => name !== 'plugin-manager.tgz') || (existsSync(installedArchive) && installedArchive !== archive)) throw new Error(`工具输出目录必须为空:${output}`);
  const bytes = readFileSync(archive);
  if (installedArchive !== archive) copyFileSync(archive, installedArchive);
  // npm otherwise walks to a parent package and can modify the source workspace.
  writeFileSync(resolve(output, 'package.json'), '{"private":true,"type":"module"}\n', { flag: 'wx' });
  execute('npm', ['install', '--prefix', output, '--offline', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--bin-links=false', './plugin-manager.tgz'], { cwd: output, env });
  const packageRoot = resolve(output, 'node_modules/@dsh-plugin-manager/plugin-manager');
  const cli = resolve(packageRoot, 'dist/cli.mjs');
  const metadata = JSON.parse(readFileSync(resolve(packageRoot, 'package.json')));
  if (metadata.name !== '@dsh-plugin-manager/plugin-manager' || metadata.version !== version || !existsSync(cli)) throw new Error('管理器归档名称、版本或入口不匹配。');
  const actual = execute(process.execPath, [cli, '--version'], { cwd: output, env, stdio: 'pipe', encoding: 'utf8' });
  if (actual.trim() !== version || !readFileSync(installedArchive).equals(bytes) || !readFileSync(archive).equals(bytes)) throw new Error('已安装工具版本或归档内容不一致。');
  execute(process.execPath, [cli, 'paths', '--root', resolve(output, 'verification-project')], { cwd: output, env, stdio: 'pipe', encoding: 'utf8' });
  return { archive: installedArchive, sha256: createHash('sha256').update(bytes).digest('hex'), cli, toolRoot: output };
}

/**
 * A real installation produces the same self-contained tooling layout as the deployment zip.
 *
 * `archive` 传入一个已核验的归档时跳过重建:把它装进本次的工具目录并做同样的版本与入口核验。
 * 当前源码发布路径不传它(`deploy/scripts/source-input.mjs` 每次按本次输入重建工具);归档的身份由
 * 调用方核验,这里返回的 `sha256` 是实际安装内容重新算出来的摘要,调用方据此再核对一次。
 */
export function prepareManagerTooling({ root, output, execute = command, env = normalizeEnvironment(process.env), archive: reused } = {}) {
  if (!root || !output) throw new Error('Manager tooling requires explicit root and output paths.');
  root = resolve(root); output = resolve(output);
  if (existsSync(output) && readdirSync(output).length) throw new Error(`工具输出目录必须为空:${output}`);
  mkdirSync(output, { recursive: true });
  const version = JSON.parse(readFileSync(resolve(root, 'packages/plugin-manager/package.json'))).version;
  if (reused !== undefined) {
    const source = resolve(reused);
    if (!existsSync(source) || !lstatSync(source).isFile() || lstatSync(source).isSymbolicLink()) throw new Error('复用的管理器归档必须是普通文件。');
    const installed = resolve(output, 'plugin-manager.tgz');
    copyFileSync(source, installed);
    return installManagerArchive({ archive: installed, output, version, execute, env });
  }
  const parent = realpathSync(tmpdir());
  const source = realpathSync(mkdtempSync(resolve(parent, 'dsh-manager-source-')));
  const archive = resolve(output, 'plugin-manager.tgz');
  try {
    for (const input of managerToolInputs) cpSync(resolve(root, input), resolve(source, input), {
      recursive: true,
      filter: path => {
        if (['node_modules', 'dist', 'coverage', '.git', '.local'].includes(path.split(/[\\/]/u).at(-1))) return false;
        if (lstatSync(path).isSymbolicLink()) throw new Error(`管理器源码输入不能是链接:${path}`);
        return true;
      },
    });
    buildManagerArchive({ root: source, archive, execute, env });
    return installManagerArchive({ archive, output, version, execute, env });
  } finally {
    if (dirname(source) !== parent || !source.startsWith(resolve(parent, 'dsh-manager-source-'))) throw new Error('Unsafe temporary tooling cleanup path.');
    rmSync(source, { recursive: true, force: true });
  }
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  try {
    const args = process.argv.slice(2), values = {};
    while (args.length) {
      const flag = args.shift();
      if (!['--root', '--archive', '--output'].includes(flag) || values[flag] || !args[0] || args[0].startsWith('--')) throw new Error('用法:manager-tooling.mjs --root <源码根> (--archive <tgz> | --output <工具目录>)');
      values[flag] = args.shift();
    }
    if (!values['--root'] || Boolean(values['--archive']) === Boolean(values['--output'])) throw new Error('必须明确源码根,并选择 archive 或 output。');
    if (values['--archive']) buildManagerArchive({ root: values['--root'], archive: values['--archive'] });
    else prepareManagerTooling({ root: values['--root'], output: values['--output'] });
  } catch (error) { console.error(error.message); process.exitCode = 1; }
}