Files
makelore/electron/coding-packages/device-package-manager.ts

656 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { cp, mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { atomicWriteJson } from '../coding-projects/atomic-json';
import {
devicePackageKind,
inspectDevicePackage,
parseDevicePackageSource,
relativeContainedPath,
safeStorageSegment,
type DevicePackageIndexV1,
type DevicePackageRecordV1,
type InstallPreviewV1,
} from './device-package-format';
export * from './device-package-format';
const PLAN_TTL_MS = 10 * 60 * 1000;
const INSTALL_OUTPUT_BYTES = 64 * 1024;
const INSTALL_TIMEOUT_MS = 2 * 60 * 1000;
const INDEX_SCHEMA_VERSION = 1;
const EXECUTABLE_WARNING = '此包包含可执行代码,将获得完整桌面权限,包括当前进程可用的文件、网络和进程权限。';
const SKILL_SCRIPT_WARNING = '此包包含可执行 Skill 脚本Skill 被调用时,脚本可能使用当前桌面用户可用的文件、网络和进程权限。';
function executableWarning(kind: InstallPreviewV1['kind'], includesExecutableCode: boolean): string | null {
if (!includesExecutableCode) return null;
return kind === 'skill-only' ? SKILL_SCRIPT_WARNING : EXECUTABLE_WARNING;
}
export class DevicePackageError extends Error {
constructor(
readonly code:
| 'local_package_source_unsupported'
| 'local_package_not_found'
| 'local_package_manifest_invalid'
| 'local_package_dependency_failed'
| 'local_package_confirmation_required'
| 'local_package_plan_expired'
| 'local_package_install_failed'
| 'local_package_not_installed'
| 'local_package_in_use',
message: string,
) {
super(message);
this.name = 'DevicePackageError';
}
}
export interface DevicePackageInstallInput {
source: string;
executablePath: string;
cliPath: string;
npmCliPath?: string;
agentDir: string;
cwd: string;
env: NodeJS.ProcessEnv;
}
export type DevicePackageInstallRunner = (input: DevicePackageInstallInput) => Promise<void>;
interface PiPackageManagerModule {
DefaultPackageManager: new (input: {
cwd: string;
agentDir: string;
settingsManager: unknown;
}) => { getInstalledPath(source: string, scope: 'user'): string | undefined };
SettingsManager: {
inMemory(user: Record<string, unknown>, project: { projectTrusted: boolean }): unknown;
};
}
export interface DevicePackageManagerOptions {
rootDir: string;
executablePath?: string;
cliPath?: string;
npmCliPath?: string;
runInstall?: DevicePackageInstallRunner;
now?: () => number;
createId?: () => string;
writeIndex?: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
onGenerationChanged?(index: DevicePackageIndexV1): Promise<void> | void;
}
interface StagedPlanV1 {
schemaVersion: 1;
requestedByTurnId: string;
preview: InstallPreviewV1;
record: DevicePackageRecordV1;
}
export interface EnabledDevicePackageResources {
generation: number;
packageIds: string[];
packageRefs: Array<{ packageId: string; resolvedVersion: string }>;
skillEntries: Array<{ id: string; entryPath: string; packageRoot: string }>;
extensionPaths: string[];
}
function installEnvironment(agentDir: string): NodeJS.ProcessEnv {
return {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: agentDir,
PI_TELEMETRY: '0',
GIT_TERMINAL_PROMPT: '0',
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
CI: '1',
npm_config_ignore_scripts: 'true',
npm_config_update_notifier: 'false',
npm_config_audit: 'false',
npm_config_fund: 'false',
};
}
export async function runPiPackageInstall(input: DevicePackageInstallInput): Promise<void> {
await mkdir(input.agentDir, { recursive: true });
await mkdir(input.cwd, { recursive: true });
if (input.npmCliPath) {
await atomicWriteJson(path.join(input.agentDir, 'settings.json'), {
npmCommand: [input.executablePath, input.npmCliPath],
});
}
await new Promise<void>((resolve, reject) => {
execFile(
input.executablePath,
[input.cliPath, 'install', input.source, '--no-approve'],
{
cwd: input.cwd,
env: input.env,
windowsHide: true,
timeout: INSTALL_TIMEOUT_MS,
maxBuffer: INSTALL_OUTPUT_BYTES,
},
(error) => error ? reject(error) : resolve(),
);
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function string(value: unknown, field: string, maximum = 2048): string {
if (typeof value !== 'string' || !value.trim() || value.length > maximum) {
throw new DevicePackageError('local_package_install_failed', `${field} is invalid`);
}
return value;
}
function parseRecord(value: unknown): DevicePackageRecordV1 {
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.source)
|| !Array.isArray(value.skillEntries) || !Array.isArray(value.extensionEntries)
|| typeof value.enabled !== 'boolean' || typeof value.confirmedExecutableCode !== 'boolean') {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const sourceKind = value.source.kind;
if (sourceKind !== 'npm' && sourceKind !== 'git' && sourceKind !== 'file') {
throw new DevicePackageError('local_package_install_failed', 'Device package source is invalid');
}
const kind = value.kind;
if (kind !== 'skill-only' && kind !== 'pi-extension' && kind !== 'mixed') {
throw new DevicePackageError('local_package_install_failed', 'Device package kind is invalid');
}
const skillEntries = value.skillEntries.map((entry) => {
if (!isRecord(entry)) throw new DevicePackageError('local_package_install_failed', 'Skill entry is invalid');
return { id: string(entry.id, 'Skill id', 128), entryPath: string(entry.entryPath, 'Skill path') };
});
return {
schemaVersion: 1,
packageId: string(value.packageId, 'Package id', 128),
displayName: string(value.displayName, 'Display name', 256),
resolvedVersion: string(value.resolvedVersion, 'Resolved version', 128),
source: {
kind: sourceKind,
requested: string(value.source.requested, 'Requested source'),
resolved: string(value.source.resolved, 'Resolved source'),
},
kind,
skillEntries,
extensionEntries: value.extensionEntries.map((entry) => string(entry, 'Extension path')),
enabled: value.enabled,
confirmedExecutableCode: value.confirmedExecutableCode,
installedAt: string(value.installedAt, 'Installed at', 64),
};
}
function parseIndex(value: unknown): DevicePackageIndexV1 {
if (!isRecord(value) || value.schemaVersion !== INDEX_SCHEMA_VERSION
|| !Number.isSafeInteger(value.generation) || (value.generation as number) < 0
|| !Array.isArray(value.packages)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
const packages = value.packages.map(parseRecord);
const ids = new Set<string>();
for (const record of packages) {
if (ids.has(record.packageId)) {
throw new DevicePackageError('local_package_install_failed', 'Device package index contains duplicate ids');
}
ids.add(record.packageId);
}
return { schemaVersion: 1, generation: value.generation as number, packages };
}
function cloneIndex(index: DevicePackageIndexV1): DevicePackageIndexV1 {
return structuredClone(index);
}
async function pathMetadata(target: string): Promise<'file' | 'directory' | null> {
return await stat(target).then((value) => value.isDirectory() ? 'directory' : value.isFile() ? 'file' : null)
.catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
}
async function gitHead(directory: string): Promise<string> {
return await new Promise<string>((resolve, reject) => {
execFile('git', ['rev-parse', 'HEAD'], {
cwd: directory,
windowsHide: true,
timeout: 10_000,
maxBuffer: 4096,
}, (error, stdout) => error ? reject(error) : resolve(stdout.trim()));
});
}
async function loadBundledPiPackageManager(cliPath: string): Promise<PiPackageManagerModule> {
const entryPath = path.join(path.dirname(path.resolve(cliPath)), 'index.js');
try {
const loaded = createRequire(entryPath)(entryPath) as Partial<PiPackageManagerModule>;
if (typeof loaded.DefaultPackageManager !== 'function'
|| typeof loaded.SettingsManager?.inMemory !== 'function') {
throw new Error('Pi package manager exports are unavailable');
}
return loaded as PiPackageManagerModule;
} catch {
throw new DevicePackageError(
'local_package_dependency_failed',
'Bundled Pi package manager is unavailable',
);
}
}
export class DevicePackageManager {
private readonly rootDir: string;
private readonly packagesDir: string;
private readonly stagingDir: string;
private readonly trashDir: string;
private readonly indexPath: string;
private readonly now: () => number;
private readonly createId: () => string;
private readonly runInstall: DevicePackageInstallRunner;
private readonly writeIndex: (filePath: string, value: DevicePackageIndexV1) => Promise<void>;
private readonly activePackages = new Map<string, number>();
private readonly pendingCleanup = new Set<string>();
private operation: Promise<void> = Promise.resolve();
constructor(private readonly options: DevicePackageManagerOptions) {
this.rootDir = path.resolve(options.rootDir);
this.packagesDir = path.join(this.rootDir, 'packages');
this.stagingDir = path.join(this.rootDir, 'staging');
this.trashDir = path.join(this.rootDir, 'trash');
this.indexPath = path.join(this.rootDir, 'index.json');
this.now = options.now ?? Date.now;
this.createId = options.createId ?? randomUUID;
this.runInstall = options.runInstall ?? runPiPackageInstall;
this.writeIndex = options.writeIndex ?? atomicWriteJson;
}
async prepare(sourceValue: string, requestedByTurnId: string): Promise<InstallPreviewV1> {
if (!requestedByTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Request turn id is required');
}
let source;
try {
source = parseDevicePackageSource(sourceValue);
} catch (error) {
throw new DevicePackageError('local_package_source_unsupported', (error as Error).message);
}
const planId = this.createId();
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_install_failed', 'Install plan id is invalid');
}
const planRoot = path.join(this.stagingDir, planId);
const packageRoot = path.join(planRoot, 'package');
await mkdir(planRoot, { recursive: true });
try {
let resolvedSource = sourceValue.trim();
if (source.kind === 'file') {
const metadata = await pathMetadata(source.absolutePath);
if (!metadata) throw new DevicePackageError('local_package_not_found', 'Local package source does not exist');
if (metadata === 'directory') await cp(source.absolutePath, packageRoot, { recursive: true, force: false });
else {
await mkdir(packageRoot, { recursive: true });
await cp(source.absolutePath, path.join(packageRoot, path.basename(source.absolutePath)), { force: false });
}
resolvedSource = source.absolutePath;
} else {
const executablePath = this.options.executablePath;
const cliPath = this.options.cliPath;
if (!executablePath || !cliPath) {
throw new DevicePackageError('local_package_dependency_failed', 'Bundled Pi installer is unavailable');
}
const agentDir = path.join(planRoot, 'pi-agent');
const cwd = path.join(planRoot, 'project');
try {
await this.runInstall({
source: source.spec,
executablePath,
cliPath,
...(this.options.npmCliPath ? { npmCliPath: this.options.npmCliPath } : {}),
agentDir,
cwd,
env: installEnvironment(agentDir),
});
} catch {
throw new DevicePackageError(
'local_package_dependency_failed',
'Pi could not install this package source',
);
}
const { DefaultPackageManager, SettingsManager } = await loadBundledPiPackageManager(cliPath);
const manager = new DefaultPackageManager({
cwd,
agentDir,
settingsManager: SettingsManager.inMemory({}, { projectTrusted: false }),
});
const installedPath = manager.getInstalledPath(source.spec, 'user');
if (!installedPath) throw new DevicePackageError('local_package_dependency_failed', 'Pi did not install the package');
if (source.kind === 'npm') {
const installRoot = path.join(agentDir, 'npm');
await cp(installRoot, packageRoot, { recursive: true, force: false });
const relativePackage = relativeContainedPath(installRoot, installedPath);
const nested = path.join(packageRoot, ...relativePackage.split('/'));
const inspected = await inspectDevicePackage(nested);
const prefix = relativeContainedPath(packageRoot, nested);
const remapped = {
...inspected,
skillEntries: inspected.skillEntries.map((entry) => ({
...entry, entryPath: `${prefix}/${entry.entryPath}`,
})),
extensionEntries: inspected.extensionEntries.map((entry) => `${prefix}/${entry}`),
};
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId,
resolvedSource: `npm:${inspected.displayName}@${inspected.resolvedVersion ?? 'unknown'}`,
inspected: remapped,
});
}
await cp(installedPath, packageRoot, { recursive: true, force: false });
const commit = await gitHead(installedPath).catch(() => {
throw new DevicePackageError('local_package_dependency_failed', 'Git package commit cannot be resolved');
});
resolvedSource = `${source.spec.replace(/@[^/@]+$/u, '')}@${commit}`;
}
return await this.finishPrepare({
planId, planRoot, packageRoot, sourceValue, source, requestedByTurnId, resolvedSource,
inspected: await inspectDevicePackage(
packageRoot,
source.kind === 'file'
? path.basename(source.absolutePath, path.extname(source.absolutePath))
: path.basename(packageRoot),
),
});
} catch (error) {
await rm(planRoot, { recursive: true, force: true }).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_manifest_invalid', (error as Error).message);
}
}
private async finishPrepare(input: {
planId: string;
planRoot: string;
packageRoot: string;
sourceValue: string;
source: ReturnType<typeof parseDevicePackageSource>;
requestedByTurnId: string;
resolvedSource: string;
inspected: Awaited<ReturnType<typeof inspectDevicePackage>>;
}): Promise<InstallPreviewV1> {
const kind = devicePackageKind(input.inspected);
const includesExecutableCode = input.inspected.extensionEntries.length > 0
|| input.inspected.hasSkillScripts;
const warning = executableWarning(kind, includesExecutableCode);
const resolvedVersion = input.inspected.resolvedVersion
?? `local-${new Date(this.now()).toISOString().replace(/[-:.]/gu, '')}`;
safeStorageSegment(resolvedVersion);
const preview: InstallPreviewV1 = {
schemaVersion: 1,
planId: input.planId,
expiresAt: new Date(this.now() + PLAN_TTL_MS).toISOString(),
requestedSource: input.sourceValue.trim(),
resolvedSource: input.resolvedSource,
packageId: input.inspected.packageId,
displayName: input.inspected.displayName,
resolvedVersion,
kind,
skillEntries: input.inspected.skillEntries,
extensionEntries: input.inspected.extensionEntries,
includesExecutableCode,
ignoredLifecycleScripts: input.inspected.ignoredLifecycleScripts,
warnings: warning ? [warning] : [],
scope: 'device-parent-workers',
};
const record: DevicePackageRecordV1 = {
schemaVersion: 1,
packageId: preview.packageId,
displayName: preview.displayName,
resolvedVersion,
source: {
kind: input.source.kind,
requested: preview.requestedSource,
resolved: preview.resolvedSource,
},
kind: preview.kind,
skillEntries: preview.skillEntries,
extensionEntries: preview.extensionEntries,
enabled: true,
confirmedExecutableCode: includesExecutableCode,
installedAt: new Date(this.now()).toISOString(),
};
const plan: StagedPlanV1 = {
schemaVersion: 1,
requestedByTurnId: input.requestedByTurnId,
preview,
record,
};
await atomicWriteJson(path.join(input.planRoot, 'plan.json'), plan);
return structuredClone(preview);
}
async commit(planId: string, confirmed: true, confirmationTurnId: string): Promise<DevicePackageIndexV1> {
if (confirmed !== true || !confirmationTurnId.trim()) {
throw new DevicePackageError('local_package_confirmation_required', 'Literal confirmation is required');
}
return await this.withOperation(async () => {
const plan = await this.readPlan(planId);
if (plan.requestedByTurnId === confirmationTurnId) {
throw new DevicePackageError('local_package_confirmation_required', 'Confirm installation in a new user turn');
}
if (Date.parse(plan.preview.expiresAt) <= this.now()) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview has expired');
}
const warning = executableWarning(plan.preview.kind, plan.preview.includesExecutableCode);
if (warning && !plan.preview.warnings.includes(warning)) {
throw new DevicePackageError('local_package_confirmation_required', 'Executable permission warning is missing');
}
const stagedPackage = path.join(this.stagingDir, planId, 'package');
await this.assertRecordEntries(stagedPackage, plan.record);
const index = await this.readIndex();
const existing = index.packages.find(({ packageId }) => packageId === plan.record.packageId);
if (existing && existing.source.requested !== plan.record.source.requested) {
throw new DevicePackageError('local_package_install_failed', 'Package id belongs to another source');
}
const destination = this.packageDirectory(plan.record);
const activeKey = this.packageKey(plan.record);
if (await pathMetadata(destination) && (this.activePackages.get(activeKey) ?? 0) > 0) {
throw new DevicePackageError('local_package_in_use', 'This package version is active in a worker');
}
await mkdir(path.dirname(destination), { recursive: true });
await mkdir(this.trashDir, { recursive: true });
const displaced = await pathMetadata(destination)
? path.join(this.trashDir, `${plan.record.packageId}-${this.createId()}`)
: null;
if (displaced) await rename(destination, displaced);
let moved = false;
try {
await rename(stagedPackage, destination);
moved = true;
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: [...index.packages.filter(({ packageId }) => packageId !== plan.record.packageId), plan.record]
.sort((left, right) => left.packageId.localeCompare(right.packageId)),
};
await this.writeIndex(this.indexPath, next);
await rm(path.join(this.stagingDir, planId), { recursive: true, force: true });
if (existing && existing.resolvedVersion !== plan.record.resolvedVersion) {
await this.cleanupPackage(existing);
}
if (displaced) await rm(displaced, { recursive: true, force: true }).catch(() => undefined);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
} catch (error) {
if (moved) await rm(destination, { recursive: true, force: true }).catch(() => undefined);
if (displaced) await rename(displaced, destination).catch(() => undefined);
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package commit failed');
}
});
}
async list(): Promise<DevicePackageIndexV1> {
return cloneIndex(await this.readIndex());
}
async setEnabled(packageId: string, enabled: boolean): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) throw new DevicePackageError('local_package_not_installed', 'Device package is not installed');
if (existing.enabled === enabled) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.map((record) => record.packageId === packageId
? { ...record, enabled }
: record),
};
await this.writeIndex(this.indexPath, next);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async uninstall(packageId: string): Promise<DevicePackageIndexV1> {
return await this.withOperation(async () => {
const index = await this.readIndex();
const existing = index.packages.find((record) => record.packageId === packageId);
if (!existing) return cloneIndex(index);
const next: DevicePackageIndexV1 = {
schemaVersion: 1,
generation: index.generation + 1,
packages: index.packages.filter((record) => record.packageId !== packageId),
};
await this.writeIndex(this.indexPath, next);
await this.cleanupPackage(existing);
await this.options.onGenerationChanged?.(cloneIndex(next));
return cloneIndex(next);
});
}
async resolveEnabledResources(): Promise<EnabledDevicePackageResources> {
const index = await this.readIndex();
const enabled = index.packages.filter(({ enabled }) => enabled);
const skillEntries: EnabledDevicePackageResources['skillEntries'] = [];
const extensionPaths: string[] = [];
for (const record of enabled) {
const packageRoot = this.packageDirectory(record);
await this.assertRecordEntries(packageRoot, record);
for (const entry of record.skillEntries) skillEntries.push({ ...entry, packageRoot });
for (const entry of record.extensionEntries) {
extensionPaths.push(path.join(packageRoot, ...entry.split('/')));
}
}
return {
generation: index.generation,
packageIds: enabled.map(({ packageId }) => packageId),
packageRefs: enabled.map(({ packageId, resolvedVersion }) => ({ packageId, resolvedVersion })),
skillEntries,
extensionPaths,
};
}
registerActiveWorker(refs: readonly { packageId: string; resolvedVersion: string }[]): () => Promise<void> {
const keys = [...new Set(refs.map((record) => this.packageKey(record)))];
for (const key of keys) this.activePackages.set(key, (this.activePackages.get(key) ?? 0) + 1);
let released = false;
return async () => {
if (released) return;
released = true;
for (const key of keys) {
const count = this.activePackages.get(key) ?? 0;
if (count <= 1) this.activePackages.delete(key);
else this.activePackages.set(key, count - 1);
}
for (const target of [...this.pendingCleanup]) {
const [key, packagePath] = target.split('\u0000', 2);
if (!key || !packagePath || (this.activePackages.get(key) ?? 0) > 0) continue;
this.pendingCleanup.delete(target);
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
};
}
private async readPlan(planId: string): Promise<StagedPlanV1> {
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u.test(planId)) {
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
try {
const value = JSON.parse(await readFile(path.join(this.stagingDir, planId, 'plan.json'), 'utf8')) as unknown;
if (!isRecord(value) || value.schemaVersion !== 1 || !isRecord(value.preview)) throw new Error();
const record = parseRecord(value.record);
const preview = value.preview as unknown as InstallPreviewV1;
if (preview.schemaVersion !== 1 || preview.planId !== planId
|| !Array.isArray(preview.warnings) || !Array.isArray(preview.skillEntries)
|| !Array.isArray(preview.extensionEntries)) throw new Error();
return {
schemaVersion: 1,
requestedByTurnId: string(value.requestedByTurnId, 'Request turn id', 256),
preview,
record,
};
} catch (error) {
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_plan_expired', 'Install preview is unavailable');
}
}
private async readIndex(): Promise<DevicePackageIndexV1> {
try {
return parseIndex(JSON.parse(await readFile(this.indexPath, 'utf8')) as unknown);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { schemaVersion: 1, generation: 0, packages: [] };
}
if (error instanceof DevicePackageError) throw error;
throw new DevicePackageError('local_package_install_failed', 'Device package index is invalid');
}
}
private packageDirectory(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return path.join(this.packagesDir, record.packageId, safeStorageSegment(record.resolvedVersion));
}
private packageKey(record: Pick<DevicePackageRecordV1, 'packageId' | 'resolvedVersion'>): string {
return `${record.packageId}@${record.resolvedVersion}`;
}
private async assertRecordEntries(packageRoot: string, record: DevicePackageRecordV1): Promise<void> {
if (await pathMetadata(packageRoot) !== 'directory') {
throw new DevicePackageError('local_package_install_failed', 'Device package bytes are unavailable');
}
for (const entry of [...record.skillEntries.map(({ entryPath }) => entryPath), ...record.extensionEntries]) {
const candidate = path.resolve(packageRoot, ...entry.split('/'));
try {
relativeContainedPath(packageRoot, candidate);
} catch {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry escapes its root');
}
if (await pathMetadata(candidate) !== 'file') {
throw new DevicePackageError('local_package_manifest_invalid', 'Device package entry is unavailable');
}
}
}
private async cleanupPackage(record: DevicePackageRecordV1): Promise<void> {
const packagePath = this.packageDirectory(record);
const key = this.packageKey(record);
if ((this.activePackages.get(key) ?? 0) > 0) {
this.pendingCleanup.add(`${key}\u0000${packagePath}`);
return;
}
await rm(packagePath, { recursive: true, force: true }).catch(() => undefined);
}
private withOperation<T>(operation: () => Promise<T>): Promise<T> {
const result = this.operation.then(operation, operation);
this.operation = result.then(() => undefined, () => undefined);
return result;
}
}