chore: crush git history - reborn from consolidation on 2026-03-10
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Workspace runner for APOPHIS CLI commands.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Fan out a command across all workspace packages
|
||||
* - Collect per-package artifacts with package attribution
|
||||
* - Aggregate results into a single workspace result
|
||||
* - Support json, ndjson, and human output formats
|
||||
* - Preserve exit codes: fail if any package fails
|
||||
*
|
||||
* Architecture:
|
||||
* - Dependency injection: all dependencies passed explicitly
|
||||
* - No optional imports — everything is required or injected
|
||||
* - Inline comments for documentation
|
||||
*/
|
||||
|
||||
import type { CliContext } from './context.js';
|
||||
import { findWorkspacePackages } from './config-loader.js';
|
||||
import type { Artifact, WorkspaceRun, WorkspaceResult, ExitCode } from './types.js';
|
||||
import { SUCCESS } from './exit-codes.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type RunCommandFn = (ctx: CliContext) => Promise<{ exitCode: number; artifact?: Artifact; warnings?: string[] }>;
|
||||
|
||||
export interface WorkspaceRunnerDeps {
|
||||
runCommand: RunCommandFn;
|
||||
findPackages?: (cwd: string) => string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace package discovery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Discover workspace packages using config-loader.
|
||||
* Falls back to empty array if no workspaces found.
|
||||
*/
|
||||
function discoverPackages(cwd: string, findPackages?: (cwd: string) => string[]): string[] {
|
||||
if (findPackages) {
|
||||
return findPackages(cwd);
|
||||
}
|
||||
return findWorkspacePackages(cwd);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Package name extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract package name from absolute path.
|
||||
* Uses basename of the directory.
|
||||
*/
|
||||
function getPackageName(pkgPath: string): string {
|
||||
const parts = pkgPath.split('/');
|
||||
return parts[parts.length - 1] || 'unknown';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Run a command across all workspace packages.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Discover workspace packages
|
||||
* 2. Run command for each package with package-attributed context
|
||||
* 3. Collect artifacts and warnings
|
||||
* 4. Determine overall exit code (fail if any package fails)
|
||||
* 5. Return workspace result with all runs
|
||||
*/
|
||||
export async function runWorkspace(
|
||||
deps: WorkspaceRunnerDeps,
|
||||
ctx: CliContext,
|
||||
): Promise<WorkspaceResult> {
|
||||
const packages = discoverPackages(ctx.cwd, deps.findPackages);
|
||||
|
||||
if (packages.length === 0) {
|
||||
return {
|
||||
exitCode: SUCCESS as ExitCode,
|
||||
runs: [],
|
||||
message: 'No workspace packages found.',
|
||||
};
|
||||
}
|
||||
|
||||
const runs: WorkspaceRun[] = [];
|
||||
let overallExitCode = SUCCESS;
|
||||
const allWarnings: string[] = [];
|
||||
|
||||
for (const pkgPath of packages) {
|
||||
const pkgName = getPackageName(pkgPath);
|
||||
|
||||
// Create a context scoped to this package's directory
|
||||
const pkgCtx: CliContext = {
|
||||
...ctx,
|
||||
cwd: pkgPath,
|
||||
};
|
||||
|
||||
const pkgResult = await deps.runCommand(pkgCtx);
|
||||
|
||||
if (pkgResult.artifact) {
|
||||
// Attach package name to artifact for attribution
|
||||
const attributedArtifact: Artifact = {
|
||||
...pkgResult.artifact,
|
||||
package: pkgName,
|
||||
};
|
||||
runs.push({
|
||||
package: pkgName,
|
||||
cwd: pkgPath,
|
||||
artifact: attributedArtifact,
|
||||
});
|
||||
}
|
||||
|
||||
if (pkgResult.exitCode !== SUCCESS) {
|
||||
overallExitCode = pkgResult.exitCode as ExitCode;
|
||||
}
|
||||
|
||||
if (pkgResult.warnings) {
|
||||
allWarnings.push(...pkgResult.warnings.map(w => `[${pkgName}] ${w}`));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: overallExitCode as ExitCode,
|
||||
runs,
|
||||
warnings: allWarnings.length > 0 ? allWarnings : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Format workspace results for human-readable output.
|
||||
* Shows per-package summary with pass/fail status.
|
||||
*/
|
||||
export function formatWorkspaceHuman(result: WorkspaceResult): string {
|
||||
const lines: string[] = [];
|
||||
lines.push('Workspace results');
|
||||
lines.push('');
|
||||
|
||||
for (const run of result.runs) {
|
||||
const a = run.artifact;
|
||||
const status = a.exitReason === 'success' ? '✓' : '✗';
|
||||
lines.push(` ${status} ${run.package}: ${a.summary.passed}/${a.summary.total} passed`);
|
||||
if (a.summary.failed > 0) {
|
||||
lines.push(` ${a.summary.failed} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(`Overall: ${result.exitCode === SUCCESS ? 'passed' : 'failed'}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format workspace results as JSON.
|
||||
* Includes all runs with full artifacts.
|
||||
*/
|
||||
export function formatWorkspaceJson(result: WorkspaceResult): string {
|
||||
return JSON.stringify({
|
||||
exitCode: result.exitCode,
|
||||
runs: result.runs.map(r => ({
|
||||
package: r.package,
|
||||
cwd: r.cwd,
|
||||
artifact: r.artifact,
|
||||
})),
|
||||
warnings: result.warnings,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format workspace results as NDJSON.
|
||||
* Emits one event per package plus a completion event.
|
||||
*/
|
||||
export function formatWorkspaceNdjson(result: WorkspaceResult): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const run of result.runs) {
|
||||
lines.push(JSON.stringify({
|
||||
type: 'workspace.run.completed',
|
||||
package: run.package,
|
||||
cwd: run.cwd,
|
||||
summary: run.artifact.summary,
|
||||
exitReason: run.artifact.exitReason,
|
||||
}));
|
||||
}
|
||||
|
||||
lines.push(JSON.stringify({
|
||||
type: 'workspace.completed',
|
||||
exitCode: result.exitCode,
|
||||
packages: result.runs.length,
|
||||
}));
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user