2026-10-24 16:58:18 +08:00
|
|
|
import {
|
2026-10-24 19:16:42 +08:00
|
|
|
createVerifierCommands,
|
|
|
|
|
VerifierCommandExitError,
|
|
|
|
|
type VerifierCommands,
|
|
|
|
|
type VerifierCommandsOptions,
|
|
|
|
|
type VerifierCommandOptions,
|
|
|
|
|
type VerifierCommandOutput,
|
|
|
|
|
type VerifierCleanupOwner,
|
|
|
|
|
} from "./verifier-command-owner.mjs";
|
2026-10-24 16:58:18 +08:00
|
|
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
|
|
|
|
|
|
|
|
export interface PackageVerifierWorkspace {
|
|
|
|
|
readonly path: string;
|
|
|
|
|
close(): Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 19:16:42 +08:00
|
|
|
export interface PackageVerifierCommands extends Pick<
|
|
|
|
|
VerifierCommands,
|
|
|
|
|
"exec"
|
|
|
|
|
> {
|
2026-10-24 16:58:18 +08:00
|
|
|
execNpm(
|
|
|
|
|
arguments_: readonly string[],
|
2026-10-24 19:16:42 +08:00
|
|
|
options?: VerifierCommandOptions,
|
|
|
|
|
): Promise<VerifierCommandOutput>;
|
2026-10-24 16:58:18 +08:00
|
|
|
run(
|
|
|
|
|
command: string,
|
|
|
|
|
arguments_: readonly string[],
|
|
|
|
|
environment?: NodeJS.ProcessEnv,
|
|
|
|
|
): Promise<string>;
|
|
|
|
|
runWithStatus(
|
|
|
|
|
command: string,
|
|
|
|
|
arguments_: readonly string[],
|
|
|
|
|
environment?: NodeJS.ProcessEnv,
|
|
|
|
|
): Promise<{ readonly stdout: string; readonly status: number }>;
|
2026-10-24 19:16:42 +08:00
|
|
|
adopt(owner: VerifierCleanupOwner, resource?: string): void;
|
2026-10-24 16:58:18 +08:00
|
|
|
close(): Promise<void>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The same command/workspace capability remains available after failed close. */
|
|
|
|
|
export class PackageVerifierCleanupFailure extends Error {
|
|
|
|
|
readonly path: string;
|
|
|
|
|
constructor(
|
|
|
|
|
cause: unknown,
|
|
|
|
|
readonly cleanupOwner: PackageVerifierCommands,
|
|
|
|
|
readonly workspace: PackageVerifierWorkspace,
|
|
|
|
|
) {
|
|
|
|
|
super(
|
|
|
|
|
`Package verification cleanup incomplete; retained ${workspace.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
|
|
|
{ cause },
|
|
|
|
|
);
|
|
|
|
|
this.name = "PackageVerifierCleanupFailure";
|
|
|
|
|
this.path = workspace.path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PackageCommands implements PackageVerifierCommands {
|
|
|
|
|
#workspaceReleaseStarted = false;
|
|
|
|
|
#closing: Promise<void> | undefined;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
readonly workspace: PackageVerifierWorkspace,
|
2026-10-24 19:16:42 +08:00
|
|
|
private readonly commands: VerifierCommands,
|
|
|
|
|
) {}
|
2026-10-24 16:58:18 +08:00
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
command: string,
|
|
|
|
|
arguments_: readonly string[],
|
2026-10-24 19:16:42 +08:00
|
|
|
options: VerifierCommandOptions = {},
|
|
|
|
|
): Promise<VerifierCommandOutput> {
|
|
|
|
|
return this.commands.exec(command, arguments_, options);
|
2026-10-24 16:58:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
execNpm(
|
|
|
|
|
arguments_: readonly string[],
|
2026-10-24 19:16:42 +08:00
|
|
|
options: VerifierCommandOptions = {},
|
|
|
|
|
): Promise<VerifierCommandOutput> {
|
2026-10-24 16:58:18 +08:00
|
|
|
const invocation = npmInvocation(arguments_, options.env ?? process.env);
|
|
|
|
|
return this.exec(invocation.command, invocation.arguments, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async run(
|
|
|
|
|
command: string,
|
|
|
|
|
arguments_: readonly string[],
|
|
|
|
|
environment?: NodeJS.ProcessEnv,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
return (await this.exec(command, arguments_, { env: environment })).stdout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runWithStatus(
|
|
|
|
|
command: string,
|
|
|
|
|
arguments_: readonly string[],
|
|
|
|
|
environment?: NodeJS.ProcessEnv,
|
|
|
|
|
): Promise<{ readonly stdout: string; readonly status: number }> {
|
|
|
|
|
try {
|
|
|
|
|
return {
|
|
|
|
|
stdout: await this.run(command, arguments_, environment),
|
|
|
|
|
status: 0,
|
|
|
|
|
};
|
|
|
|
|
} catch (cause: unknown) {
|
2026-10-24 19:16:42 +08:00
|
|
|
if (cause instanceof VerifierCommandExitError && cause.code === 1)
|
|
|
|
|
return { stdout: cause.snapshot.stdout.text, status: 1 };
|
2026-10-24 16:58:18 +08:00
|
|
|
throw cause;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 19:16:42 +08:00
|
|
|
adopt(owner: VerifierCleanupOwner, resource = this.workspace.path): void {
|
2026-10-24 16:58:18 +08:00
|
|
|
if (this.#workspaceReleaseStarted)
|
|
|
|
|
throw new Error("Package workspace release has started");
|
2026-10-24 19:16:42 +08:00
|
|
|
this.commands.adopt(owner, resource);
|
2026-10-24 16:58:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(): Promise<void> {
|
|
|
|
|
if (this.#closing !== undefined) return this.#closing;
|
|
|
|
|
let attempt: Promise<void>;
|
|
|
|
|
attempt = this.#release().catch((cause: unknown) => {
|
|
|
|
|
if (this.#closing === attempt) this.#closing = undefined;
|
|
|
|
|
throw new PackageVerifierCleanupFailure(cause, this, this.workspace);
|
|
|
|
|
});
|
|
|
|
|
this.#closing = attempt;
|
|
|
|
|
return attempt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async #release(): Promise<void> {
|
2026-10-24 19:16:42 +08:00
|
|
|
await this.commands.close();
|
2026-10-24 16:58:18 +08:00
|
|
|
this.#workspaceReleaseStarted = true;
|
|
|
|
|
await this.workspace.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 19:16:42 +08:00
|
|
|
/** Remove the package workspace only after its shared command owner releases. */
|
2026-10-24 16:58:18 +08:00
|
|
|
export async function createPackageVerifierCommands({
|
|
|
|
|
workspace,
|
2026-10-24 19:16:42 +08:00
|
|
|
launcher,
|
|
|
|
|
}: VerifierCommandsOptions & {
|
2026-10-24 16:58:18 +08:00
|
|
|
readonly workspace: PackageVerifierWorkspace;
|
|
|
|
|
}): Promise<PackageVerifierCommands> {
|
2026-10-24 19:16:42 +08:00
|
|
|
const commands = await createVerifierCommands(
|
|
|
|
|
launcher === undefined ? {} : { launcher },
|
|
|
|
|
);
|
|
|
|
|
return new PackageCommands(workspace, commands);
|
2026-10-24 16:58:18 +08:00
|
|
|
}
|