Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 11 additions & 73 deletions packages/runtime-playground/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
import { createHash } from "node:crypto"
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises"
import { mkdir, realpath, writeFile } from "node:fs/promises"
import { dirname, join, resolve } from "node:path"
import { RUNTIME_EPISODE_OBSERVATION_SCHEMA, RUNTIME_EPISODE_SNAPSHOT_SCHEMA, assertRuntimeCommandAllowed, runtimeEpisodeDigest } from "@chubes4/wp-codebox-core"
import {
ArtifactRedactor,
fileEntry,
} from "./artifacts.js"
import { ArtifactBundleBuilder } from "./artifact-bundle-builder.js"
import { browserManifestFiles as browserArtifactManifestFiles, browserRedactionPaths, browserReviewSummary as browserArtifactReviewSummary, type BrowserProbeArtifact } from "./browser-artifacts.js"
import { browserReviewSummary as browserArtifactReviewSummary, type BrowserProbeArtifact } from "./browser-artifacts.js"
import { runBrowserActionsCommand, runBrowserProbeCommand } from "./browser-command-runners.js"
import { pluginCheckManifestFiles, redactPluginCheckArtifacts, redactThemeCheckArtifacts, themeCheckManifestFiles, type PluginCheckArtifact, type ThemeCheckArtifact } from "./check-artifacts.js"
import type { PluginCheckArtifact, ThemeCheckArtifact } from "./check-artifacts.js"
import { executePlaygroundCommand, playgroundRuntimeCommandIds } from "./command-router.js"
export { playgroundRuntimeCommandIds } from "./command-router.js"
import { cleanWpCliOutput, shellArgv, wpCliCommandFromArgs, wpCliPhpScript } from "./commands.js"
import { bootstrapPhpCode } from "./php-bootstrap.js"
import { captureMountedFiles, captureMountDiffs } from "./mounted-artifact-capture.js"
import { observeHttpResponse as observeHttpResponseArtifact, observeWordPressState as observeWordPressStateArtifact } from "./observation-artifacts.js"
import { PlaygroundCommandCrashError, assertPlaygroundResponseOk, errorMessage, type PlaygroundRunResponse } from "./playground-command-errors.js"
import { startPlaygroundCliServer } from "./playground-cli-runner.js"
import type { PlaygroundCliServer } from "./preview-server.js"
import { collectPlaygroundArtifacts } from "./runtime-artifact-helpers.js"
import { runAbilityCommand, runBenchCommand, runCorePhpunitCommand, runPhpCommand, runPhpunitCommand, runPluginCheckCommand, runThemeCheckCommand } from "./wordpress-command-runners.js"
import { PlaygroundSnapshotRestoreError, contentDigest, mountsFromSnapshot, runtimeSnapshotExportPhp, runtimeSnapshotPayload, runtimeSnapshotRestorePhp, runtimeSpecFromSnapshot, snapshotDigest, type RuntimeSnapshotArtifact } from "./runtime-snapshot.js"
import { createRuntimeWpCliBridge, type RuntimeWpCliBridge } from "./runtime-wp-cli-bridge.js"
import type {
ArtifactBundle,
ArtifactManifestFile,
ArtifactPreview,
ArtifactSpec,
ExecutionResult,
Expand Down Expand Up @@ -318,10 +312,10 @@ class PlaygroundRuntime implements Runtime {
}

async collectArtifacts(spec: ArtifactSpec = {}): Promise<ArtifactBundle> {
return new ArtifactBundleBuilder({
return collectPlaygroundArtifacts({
artifactRoot: this.artifactRoot,
runtimeId: this.runtimeId,
runtimeCreatedAt: this.createdAt,
createdAt: this.createdAt,
spec: this.spec,
mounts: this.mounts,
commands: this.commands,
Expand All @@ -330,27 +324,16 @@ class PlaygroundRuntime implements Runtime {
events: this.events,
info: () => this.info(),
previewInfo: (createdAt, previewHoldSeconds) => this.previewInfo(createdAt, previewHoldSeconds),
browserReviewSummary: () => this.browserReviewSummary(),
captureMountedFiles: (filesDirectory, redactor) => captureMountedFiles(filesDirectory, this.mounts, redactor),
captureMountDiffs: (filesDirectory, redactor) => captureMountDiffs(this.artifactRoot, filesDirectory, this.mounts, redactor),
redactBrowserArtifacts: (redactor) => this.redactBrowserArtifacts(redactor),
redactPluginCheckArtifacts: (redactor) => redactPluginCheckArtifacts(this.artifactRoot, this.pluginChecks, redactor),
redactThemeCheckArtifacts: (redactor) => redactThemeCheckArtifacts(this.artifactRoot, this.themeChecks, redactor),
browserManifestFiles: () => this.browserManifestFiles(),
pluginCheckArtifactPaths: () => this.pluginChecks.map((check) => check.files.normalized),
themeCheckArtifactPaths: () => this.themeChecks.map((check) => check.files.normalized),
observationManifestFiles: () => this.observationManifestFiles(),
pluginCheckManifestFiles: () => pluginCheckManifestFiles(this.artifactRoot, this.pluginChecks),
themeCheckManifestFiles: () => themeCheckManifestFiles(this.artifactRoot, this.themeChecks),
formatRuntimeLog: () => this.formatRuntimeLog(),
formatCommandsLog: () => this.formatCommandsLog(),
recordArtifactsCollected: (bundleId, createdAt, artifactSpec) => this.recordEvent("runtime.artifacts.collected", {
id: bundleId,
directory: this.artifactRoot,
createdAt,
spec: artifactSpec,
}),
}).build(spec)
browserProbes: this.browserProbes,
pluginChecks: this.pluginChecks,
themeChecks: this.themeChecks,
}, spec)
}

async destroy(): Promise<void> {
Expand Down Expand Up @@ -404,22 +387,6 @@ class PlaygroundRuntime implements Runtime {
return event
}

private formatRuntimeLog(): string {
return this.events.map((event) => `[${event.timestamp}] ${event.type} ${JSON.stringify(event.data ?? {})}`).join("\n") + "\n"
}

private formatCommandsLog(): string {
return (
this.commands
.map((command) => {
const header = `[${command.startedAt}] ${command.command} ${command.args.join(" ")}`.trim()
const output = [command.stdout, command.stderr].filter(Boolean).join("\n")
return `${header}\nexitCode=${command.exitCode}\n${output}`
})
.join("\n---\n") + "\n"
)
}

async runBrowserProbe(spec: ExecutionSpec): Promise<string> {
const server = await this.bootPlayground()
const result = await runBrowserProbeCommand({ artifactRoot: this.artifactRoot, server, spec })
Expand Down Expand Up @@ -637,7 +604,7 @@ echo json_encode(array('command' => 'inspect-mounted-inputs', 'mounts' => $inspe
}

if (spec.type === "browser-result") {
return { data: this.browserReviewSummary() ?? { probes: [] }, artifactRefs }
return { data: browserArtifactReviewSummary(this.browserProbes) ?? { probes: [] }, artifactRefs }
}

if (spec.type === "runtime-events" || spec.type === "runtime-logs") {
Expand Down Expand Up @@ -669,35 +636,6 @@ echo json_encode(array('command' => 'inspect-mounted-inputs', 'mounts' => $inspe
return new URL(url, baseUrl).toString()
}

private browserReviewSummary() {
return browserArtifactReviewSummary(this.browserProbes)
}

private browserManifestFiles(): ArtifactManifestFile[] {
return browserArtifactManifestFiles(this.artifactRoot, this.browserProbes)
}

private observationManifestFiles(): ArtifactManifestFile[] {
return this.observations.flatMap((observation) =>
(observation.artifactRefs ?? [])
.filter((ref): ref is RuntimeEpisodeTraceRef & { path: string } => typeof ref.path === "string" && ref.path.length > 0)
.map((ref) => fileEntry(join(this.artifactRoot, ref.path), ref.kind, ref.path.endsWith(".json") ? "application/json" : "text/plain")),
)
}

private async redactBrowserArtifacts(redactor: ArtifactRedactor): Promise<void> {
for (const probe of this.browserProbes) {
for (const path of browserRedactionPaths(probe)) {
const absolutePath = join(this.artifactRoot, path)
try {
await writeFile(absolutePath, redactor.redact(path, await readFile(absolutePath, "utf8")))
} catch {
// Browser capture is best-effort; preserve artifact collection if a file vanished.
}
}
}
}

}

export function createPlaygroundRuntimeBackend(): RuntimeBackend {
Expand Down
121 changes: 121 additions & 0 deletions packages/runtime-playground/src/runtime-artifact-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { readFile, writeFile } from "node:fs/promises"
import { join } from "node:path"
import type {
ArtifactBundle,
ArtifactManifestFile,
ArtifactPreview,
ArtifactSpec,
ExecutionResult,
LifecycleEvent,
MountSpec,
ObservationResult,
RuntimeCreateSpec,
RuntimeEpisodeTraceRef,
RuntimeInfo,
Snapshot,
} from "@chubes4/wp-codebox-core"
import { ArtifactBundleBuilder } from "./artifact-bundle-builder.js"
import { fileEntry, type ArtifactRedactor } from "./artifacts.js"
import { browserManifestFiles as browserArtifactManifestFiles, browserRedactionPaths, browserReviewSummary as browserArtifactReviewSummary, type BrowserProbeArtifact } from "./browser-artifacts.js"
import { pluginCheckManifestFiles, redactPluginCheckArtifacts, redactThemeCheckArtifacts, themeCheckManifestFiles, type PluginCheckArtifact, type ThemeCheckArtifact } from "./check-artifacts.js"
import { captureMountDiffs, captureMountedFiles } from "./mounted-artifact-capture.js"

export async function collectPlaygroundArtifacts({
artifactRoot,
browserProbes,
commands,
createdAt,
events,
info,
mounts,
observations,
pluginChecks,
previewInfo,
recordArtifactsCollected,
runtimeId,
snapshots,
spec,
themeChecks,
}: {
artifactRoot: string
browserProbes: BrowserProbeArtifact[]
commands: ExecutionResult[]
createdAt: string
events: LifecycleEvent[]
info: () => Promise<RuntimeInfo>
mounts: MountSpec[]
observations: ObservationResult[]
pluginChecks: PluginCheckArtifact[]
previewInfo: (createdAt: string, holdSeconds: number) => Promise<ArtifactPreview>
recordArtifactsCollected: (bundleId: string, createdAt: string, artifactSpec: ArtifactSpec) => void
runtimeId: string
snapshots: Snapshot[]
spec: RuntimeCreateSpec
themeChecks: ThemeCheckArtifact[]
}, artifactSpec: ArtifactSpec = {}): Promise<ArtifactBundle> {
return new ArtifactBundleBuilder({
artifactRoot,
runtimeId,
runtimeCreatedAt: createdAt,
spec,
mounts,
commands,
observations,
snapshots,
events,
info,
previewInfo,
browserReviewSummary: () => browserArtifactReviewSummary(browserProbes),
captureMountedFiles: (filesDirectory, redactor) => captureMountedFiles(filesDirectory, mounts, redactor),
captureMountDiffs: (filesDirectory, redactor) => captureMountDiffs(artifactRoot, filesDirectory, mounts, redactor),
redactBrowserArtifacts: (redactor) => redactBrowserArtifacts(artifactRoot, browserProbes, redactor),
redactPluginCheckArtifacts: (redactor) => redactPluginCheckArtifacts(artifactRoot, pluginChecks, redactor),
redactThemeCheckArtifacts: (redactor) => redactThemeCheckArtifacts(artifactRoot, themeChecks, redactor),
browserManifestFiles: () => browserArtifactManifestFiles(artifactRoot, browserProbes),
pluginCheckArtifactPaths: () => pluginChecks.map((check) => check.files.normalized),
themeCheckArtifactPaths: () => themeChecks.map((check) => check.files.normalized),
observationManifestFiles: () => observationManifestFiles(artifactRoot, observations),
pluginCheckManifestFiles: () => pluginCheckManifestFiles(artifactRoot, pluginChecks),
themeCheckManifestFiles: () => themeCheckManifestFiles(artifactRoot, themeChecks),
formatRuntimeLog: () => formatRuntimeLog(events),
formatCommandsLog: () => formatCommandsLog(commands),
recordArtifactsCollected,
}).build(artifactSpec)
}

export function formatRuntimeLog(events: LifecycleEvent[]): string {
return events.map((event) => `[${event.timestamp}] ${event.type} ${JSON.stringify(event.data ?? {})}`).join("\n") + "\n"
}

export function formatCommandsLog(commands: ExecutionResult[]): string {
return (
commands
.map((command) => {
const header = `[${command.startedAt}] ${command.command} ${command.args.join(" ")}`.trim()
const output = [command.stdout, command.stderr].filter(Boolean).join("\n")
return `${header}\nexitCode=${command.exitCode}\n${output}`
})
.join("\n---\n") + "\n"
)
}

function observationManifestFiles(artifactRoot: string, observations: ObservationResult[]): ArtifactManifestFile[] {
return observations.flatMap((observation) =>
(observation.artifactRefs ?? [])
.filter((ref): ref is RuntimeEpisodeTraceRef & { path: string } => typeof ref.path === "string" && ref.path.length > 0)
.map((ref) => fileEntry(join(artifactRoot, ref.path), ref.kind, ref.path.endsWith(".json") ? "application/json" : "text/plain")),
)
}

async function redactBrowserArtifacts(artifactRoot: string, browserProbes: BrowserProbeArtifact[], redactor: ArtifactRedactor): Promise<void> {
for (const probe of browserProbes) {
for (const path of browserRedactionPaths(probe)) {
const absolutePath = join(artifactRoot, path)
try {
await writeFile(absolutePath, redactor.redact(path, await readFile(absolutePath, "utf8")))
} catch {
// Browser capture is best-effort; preserve artifact collection if a file vanished.
}
}
}
}