feat(runner): install stage with progress events

Install stage: package-manager detection (lockfile beats the
packageManager field, npm default — vite-kitchen's shape), install run
inside the clone with staged status heartbeats, and every failure —
including install-before-clone — mapped to the named install-failed
cause with a bounded output tail. The spawned-runner suite now runs the
full clone → install pipeline on the daemon-served vite-kitchen (real
npm registry install) and greps the child's entire stdout+stderr plus
the clone's .git/config for the PAT and workspace token. Shared test
doubles extracted (exec.mock.ts, tests/runner-session.ts) to keep the
suites duplication-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 22:44:39 +02:00
parent 750ab44379
commit ec7bf948af
10 changed files with 612 additions and 65 deletions

View File

@@ -11,6 +11,7 @@ import {
import { runCommand } from "./exec";
import type { Logger } from "./log";
import { cloneRepository } from "./stages/clone";
import { installDependencies } from "./stages/install";
import { runStage, type StageEmitter } from "./stages/stage-runner";
/**
@@ -142,10 +143,9 @@ export type CommandMessage = Extract<
/** Stages that later walking-skeleton stories wire up (05+). */
const NOT_IMPLEMENTED: Record<
Exclude<CommandMessage["type"], "clone">,
Exclude<CommandMessage["type"], "clone" | "install">,
{ cause: RunnerErrorCause; stage?: RunnerStage }
> = {
install: { cause: "install-failed", stage: "installing" },
scan: { cause: "scan-failed", stage: "scanning" },
"adapter-start": { cause: "adapter-start-failed", stage: "starting-preview" },
"render-frame": { cause: "render-failed" },
@@ -184,6 +184,20 @@ async function handleCommand(
if (result.ok) ctx.ready();
return;
}
case "install": {
const result = await runStage(
"installing",
"install-failed",
stageOptions,
installDependencies({
workspaceDir: options.workspaceDir,
runCommand,
log: options.log,
}),
);
if (result.ok) ctx.ready();
return;
}
default: {
const notImplemented = NOT_IMPLEMENTED[command.type];
emit.error(