diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index da18773..c65348e 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -98,6 +98,20 @@ export interface ArtifactReviewBrowserSummary { probes: Array<{ url: string requestedUrl?: string + preview?: { + requestedMode: "local" | "public" | "secure" + effectiveMode: "local" | "public" | "secure" + localOrigin: string + effectiveOrigin: string + publicOrigin?: string + secureContext?: boolean + diagnostics: Array<{ + code: string + severity: "error" | "warning" | "info" + message: string + details?: Record + }> + } localPreviewOrigin?: string requestedPreviewOrigin?: string effectivePreviewOrigin?: string diff --git a/packages/runtime-playground/src/browser-artifacts.ts b/packages/runtime-playground/src/browser-artifacts.ts index 846253f..e4f8b92 100644 --- a/packages/runtime-playground/src/browser-artifacts.ts +++ b/packages/runtime-playground/src/browser-artifacts.ts @@ -5,6 +5,7 @@ import type { Request } from "playwright" export interface BrowserProbeArtifact { requestedUrl: string url: string + preview: BrowserProbePreviewRouting localPreviewOrigin?: string requestedPreviewOrigin?: string effectivePreviewOrigin?: string @@ -80,6 +81,25 @@ export interface BrowserProbePermissionState { state: "granted" | "denied" | "prompt" | "unsupported" | "error" } +export type BrowserProbePreviewMode = "local" | "public" | "secure" + +export interface BrowserProbePreviewRouting { + requestedMode: BrowserProbePreviewMode + effectiveMode: BrowserProbePreviewMode + localOrigin: string + effectiveOrigin: string + publicOrigin?: string + secureContext?: boolean + diagnostics: BrowserProbePreviewDiagnostic[] +} + +export interface BrowserProbePreviewDiagnostic { + code: string + severity: "error" | "warning" | "info" + message: string + details?: Record +} + export interface BrowserProbeContextDetails { requested: { browser?: string @@ -366,6 +386,7 @@ export function browserReviewSummary(probes: BrowserProbeArtifact[]): ArtifactRe probes: probes.map((probe) => ({ url: probe.url, requestedUrl: probe.requestedUrl, + preview: probe.preview, localPreviewOrigin: probe.localPreviewOrigin, requestedPreviewOrigin: probe.requestedPreviewOrigin, effectivePreviewOrigin: probe.effectivePreviewOrigin, diff --git a/packages/runtime-playground/src/browser-command-runners.ts b/packages/runtime-playground/src/browser-command-runners.ts index 6860f00..5cef3f8 100644 --- a/packages/runtime-playground/src/browser-command-runners.ts +++ b/packages/runtime-playground/src/browser-command-runners.ts @@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { join } from "node:path" import { assertRuntimeCommandAllowed, browserInteractionScriptUsesEvaluate, type ExecutionSpec, type RuntimeCreateSpec } from "@automattic/wp-codebox-core" import { browserInteractionStepsFromArgs, durationStringMs } from "./browser-actions.js" -import type { BrowserProbeArtifact, BrowserProbeCapabilityDiagnostics, BrowserProbeCheckpointRecord, BrowserProbeContextDetails, BrowserProbeErrorRecord, BrowserProbeLifecycleArtifact, BrowserProbeMemoryArtifact, BrowserProbeNetworkRecord, BrowserProbePerformanceArtifact, BrowserProbeScriptMetadata, BrowserProbeViewport, BrowserStepRecord } from "./browser-artifacts.js" +import type { BrowserProbeArtifact, BrowserProbeCapabilityDiagnostics, BrowserProbeCheckpointRecord, BrowserProbeContextDetails, BrowserProbeErrorRecord, BrowserProbeLifecycleArtifact, BrowserProbeMemoryArtifact, BrowserProbeNetworkRecord, BrowserProbePerformanceArtifact, BrowserProbePreviewMode, BrowserProbePreviewRouting, BrowserProbeScriptMetadata, BrowserProbeViewport, BrowserStepRecord } from "./browser-artifacts.js" import { browserAssertionsSummary, browserStepRecord, executeBrowserInteractionStep } from "./browser-interactions.js" import { browserProbeLifecycleArtifact, browserProbeLifecycleInitScript, collectBrowserProbeLifecycle } from "./browser-lifecycle.js" import { browserProbeBenchMetrics, jsonLines, serializeBrowserConsoleMessage, serializeBrowserError, serializeBrowserFinishedRequest, serializeBrowserRequestFailure } from "./browser-metrics.js" @@ -171,8 +171,9 @@ async function runSingleBrowserProbeCommand({ const capturesNetworkForAssertions = browserProbeAssertionsNeedNetwork(assertions) const capturesBrowserMetrics = capture.has("performance") || capture.has("memory") || browserProbeAssertionsNeedMetrics(assertions) const prePageScriptMetadata = prePageScript ? browserProbeScriptMetadata(prePageScript) : undefined - const previewOrigins = browserProbePreviewOrigins(runtimeSpec, server.serverUrl) - const targetUrl = resolveBrowserProbeUrl(urlArg, previewOrigins.effectivePreviewOrigin) + const preview = browserProbePreviewRouting(args, runtimeSpec, server.serverUrl) + const previewOrigins = browserProbePreviewOrigins(preview) + const targetUrl = resolveBrowserProbeUrl(urlArg, preview.effectiveOrigin) const browserDirectory = join(artifactRoot, browserFilesDirectory) await mkdir(browserDirectory, { recursive: true }) @@ -273,8 +274,18 @@ async function runSingleBrowserProbeCommand({ }) } + const previewReadinessError = browserProbePreviewReadinessError(preview) + if (previewReadinessError) { + throw previewReadinessError + } + await withBrowserProbeLiveness(page, progress, failFast, navigateBrowserProbe(page, targetUrl, waitFor, durationMs)) progress.mark("navigation") + preview.secureContext = await page.evaluate(() => window.isSecureContext).catch(() => undefined) + const secureContextError = browserProbeSecureContextError(preview) + if (secureContextError) { + throw secureContextError + } if (capturesBrowserMetrics) { checkpoints.push(await browserProbeCheckpoint(page, "after-navigation")) } @@ -393,6 +404,7 @@ async function runSingleBrowserProbeCommand({ artifact = { requestedUrl: targetUrl, url: targetUrl, + preview, ...previewOrigins, ...(prePageScriptMetadata ? { prePageScript: prePageScriptMetadata } : {}), files: { @@ -430,6 +442,7 @@ async function runSingleBrowserProbeCommand({ await writeFile(summaryPath, `${JSON.stringify({ schema: "wp-codebox/browser-probe/v1", requestedUrl: targetUrl, + preview, ...previewOrigins, finalUrl, waitFor, @@ -466,6 +479,7 @@ async function runSingleBrowserProbeCommand({ output: `${JSON.stringify({ command, requestedUrl: targetUrl, + preview, ...previewOrigins, finalUrl: artifact.summary.finalUrl ?? targetUrl, files: artifact.files, @@ -707,7 +721,8 @@ export async function runBrowserActionsCommand({ const startedAtMs = Date.now() const { chromium } = await import("playwright") const browser = await chromium.launch() - let requestedUrl = initialUrl ? resolveBrowserProbeUrl(initialUrl, server.serverUrl) : server.serverUrl + const preview = browserProbePreviewRouting([], runtimeSpec, server.serverUrl) + let requestedUrl = initialUrl ? resolveBrowserProbeUrl(initialUrl, preview.effectiveOrigin) : preview.effectiveOrigin let finalUrl = requestedUrl let htmlSha256: string | undefined let screenshotSha256: string | undefined @@ -822,6 +837,7 @@ export async function runBrowserActionsCommand({ artifact = { requestedUrl, url: requestedUrl, + preview, files: { ...(capture.has("steps") ? { steps: "files/browser/steps.jsonl" } : {}), ...(capture.has("console") ? { console: "files/browser/console.jsonl" } : {}), @@ -848,6 +864,7 @@ export async function runBrowserActionsCommand({ await writeFile(summaryPath, `${JSON.stringify({ schema: "wp-codebox/browser-actions/v1", requestedUrl, + preview, finalUrl, capture: [...capture].sort(), stepTimeoutMs, @@ -875,6 +892,7 @@ export async function runBrowserActionsCommand({ output: `${JSON.stringify({ command: "wordpress.browser-actions", requestedUrl, + preview, finalUrl: artifact.summary.finalUrl ?? finalUrl, files: artifact.files, summary: artifact.summary, @@ -1176,7 +1194,8 @@ export async function runEditorOpenCommand({ } const waitTimeoutMs = durationArg(args, "wait-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS) - const targetUrl = resolveBrowserProbeUrl(target.url, server.serverUrl) + const preview = browserProbePreviewRouting([], runtimeSpec, server.serverUrl) + const targetUrl = resolveBrowserProbeUrl(target.url, preview.effectiveOrigin) const browserDirectory = join(artifactRoot, "files", "browser") await mkdir(browserDirectory, { recursive: true }) @@ -1269,6 +1288,7 @@ export async function runEditorOpenCommand({ artifact = { requestedUrl: targetUrl, url: targetUrl, + preview, files: { ...(capture.has("steps") ? { steps: "files/browser/editor-steps.jsonl" } : {}), ...(capture.has("console") ? { console: "files/browser/editor-console.jsonl" } : {}), @@ -1295,6 +1315,7 @@ export async function runEditorOpenCommand({ schema: "wp-codebox/editor-open/v1", target, requestedUrl: targetUrl, + preview, finalUrl, capture: [...capture].sort(), waitTimeoutMs, @@ -1321,6 +1342,7 @@ export async function runEditorOpenCommand({ command: "wordpress.editor-open", target, requestedUrl: targetUrl, + preview, finalUrl: artifact.summary.finalUrl ?? finalUrl, files: artifact.files, summary: artifact.summary, @@ -1363,7 +1385,8 @@ export async function runEditorActionsCommand({ const waitTimeoutMs = durationArg(args, "wait-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS) const stepTimeoutMs = durationArg(args, "step-timeout", BROWSER_STEP_DEFAULT_TIMEOUT_MS) const totalTimeoutMs = durationArg(args, "timeout", BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS) - const targetUrl = resolveBrowserProbeUrl(target.url, server.serverUrl) + const preview = browserProbePreviewRouting([], runtimeSpec, server.serverUrl) + const targetUrl = resolveBrowserProbeUrl(target.url, preview.effectiveOrigin) const browserDirectory = join(artifactRoot, "files", "browser") await mkdir(browserDirectory, { recursive: true }) @@ -1480,6 +1503,7 @@ export async function runEditorActionsCommand({ artifact = { requestedUrl: targetUrl, url: targetUrl, + preview, files: { ...(capture.has("steps") ? { steps: "files/browser/editor-action-steps.jsonl" } : {}), ...(capture.has("console") ? { console: "files/browser/editor-action-console.jsonl" } : {}), @@ -1508,6 +1532,7 @@ export async function runEditorActionsCommand({ target, actions: actionSteps, requestedUrl: targetUrl, + preview, finalUrl, capture: [...capture].sort(), waitTimeoutMs, @@ -1537,6 +1562,7 @@ export async function runEditorActionsCommand({ target, actions: actionSteps.length, requestedUrl: targetUrl, + preview, finalUrl: artifact.summary.finalUrl ?? finalUrl, files: artifact.files, summary: artifact.summary, @@ -1801,11 +1827,90 @@ function now(): string { return new Date().toISOString() } -function browserProbePreviewOrigins(runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string): { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string } { +function browserProbePreviewOrigins(preview: BrowserProbePreviewRouting): { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string } { + return { + localPreviewOrigin: preview.localOrigin, + requestedPreviewOrigin: preview.publicOrigin, + effectivePreviewOrigin: preview.effectiveOrigin, + } +} + +function browserProbePreviewRouting(args: string[], runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string): BrowserProbePreviewRouting { + const requestedMode = browserProbePreviewMode(args) + const publicOrigin = runtimeSpec?.preview?.publicUrl + const effectiveMode: BrowserProbePreviewMode = requestedMode === "local" || !publicOrigin ? "local" : requestedMode + const effectiveOrigin = effectiveMode === "local" ? localPreviewOrigin : (publicOrigin ?? localPreviewOrigin) + const diagnostics: BrowserProbePreviewRouting["diagnostics"] = [] + + if ((requestedMode === "public" || requestedMode === "secure") && !publicOrigin) { + diagnostics.push({ + code: "preview-public-origin-missing", + severity: "error", + message: `wordpress.browser-probe preview-mode=${requestedMode} requires runtime.preview.publicUrl or --preview-public-url`, + details: { requestedMode, localOrigin: localPreviewOrigin }, + }) + } + + if (requestedMode === "secure" && publicOrigin) { + const protocol = urlProtocol(publicOrigin) + if (protocol !== "https:") { + diagnostics.push({ + code: "preview-public-origin-not-https", + severity: "error", + message: "wordpress.browser-probe preview-mode=secure requires an HTTPS public preview origin", + details: { publicOrigin, protocol }, + }) + } + } + return { - localPreviewOrigin, - requestedPreviewOrigin: runtimeSpec?.preview?.publicUrl, - effectivePreviewOrigin: runtimeSpec?.preview?.publicUrl ?? localPreviewOrigin, + requestedMode, + effectiveMode, + localOrigin: localPreviewOrigin, + effectiveOrigin, + ...(publicOrigin ? { publicOrigin } : {}), + diagnostics, + } +} + +function browserProbePreviewMode(args: string[]): BrowserProbePreviewMode { + const raw = argValue(args, "preview-mode")?.trim() || "local" + if (raw === "local" || raw === "public" || raw === "secure") { + return raw + } + + throw new Error(`wordpress.browser-probe preview-mode supports local, public, secure: ${raw}`) +} + +function browserProbePreviewReadinessError(preview: BrowserProbePreviewRouting): Error | undefined { + const diagnostic = preview.diagnostics.find((item) => item.severity === "error") + if (!diagnostic) { + return undefined + } + + return new Error(diagnostic.message) +} + +function browserProbeSecureContextError(preview: BrowserProbePreviewRouting): Error | undefined { + if (preview.requestedMode !== "secure" || preview.secureContext !== false) { + return undefined + } + + const diagnostic = { + code: "preview-secure-context-unavailable", + severity: "error" as const, + message: "wordpress.browser-probe preview-mode=secure reached the preview, but the page did not report a secure browser context", + details: { effectiveOrigin: preview.effectiveOrigin, secureContext: preview.secureContext }, + } + preview.diagnostics.push(diagnostic) + return new Error(diagnostic.message) +} + +function urlProtocol(url: string): string | undefined { + try { + return new URL(url).protocol + } catch { + return undefined } } diff --git a/scripts/artifact-contract-smoke.ts b/scripts/artifact-contract-smoke.ts index 4cc73fb..bb1c89b 100644 --- a/scripts/artifact-contract-smoke.ts +++ b/scripts/artifact-contract-smoke.ts @@ -141,7 +141,7 @@ try { assert.equal(metadata.provenance.packages.environment.nodeVersion, process.versions.node) assert.equal(metadata.provenance.task.kind, "recipe-run") assert.equal(metadata.provenance.task.recipePath.endsWith("examples/recipes/seeded-plugin-workspace.json"), true) - assert.equal(metadata.provenance.task.previewPublicUrl, "https://preview.example.test/codebox/") + assert.equal(metadata.provenance.task.preview.effective.publicUrl, "https://preview.example.test/codebox/") assert.ok(metadata.provenance.task.inputs.workspaces.length > 0) assert.equal(metadata.provenance.workspace.schema, "wp-codebox/sandbox-workspace/v1") assert.equal(metadata.provenance.workspace.root, "/workspace") diff --git a/scripts/recipe-preview-routing-browser-probe-smoke.ts b/scripts/recipe-preview-routing-browser-probe-smoke.ts index d5bbddc..0116fe1 100644 --- a/scripts/recipe-preview-routing-browser-probe-smoke.ts +++ b/scripts/recipe-preview-routing-browser-probe-smoke.ts @@ -19,6 +19,9 @@ try { assert.equal(recipeOutput.success, true, recipeOutput.error?.message ?? "recipe-run failed") const recipeSummary = await browserSummary(recipeOutput) assert.equal(recipeSummary.requestedUrl, `${recipePublicUrl}relative-probe`) + assert.equal(recipeSummary.preview.requestedMode, "public") + assert.equal(recipeSummary.preview.effectiveMode, "public") + assert.equal(recipeSummary.preview.secureContext, true) assert.equal(recipeSummary.localPreviewOrigin.startsWith("http://127.0.0.1:"), true) assert.equal(recipeSummary.requestedPreviewOrigin, recipePublicUrl) assert.equal(recipeSummary.effectivePreviewOrigin, recipePublicUrl) @@ -27,10 +30,25 @@ try { const recipeReview = await review(recipeOutput) assert.equal(recipeReview.browser?.probes?.[0]?.requestedUrl, `${recipePublicUrl}relative-probe`) + assert.equal(recipeReview.browser?.probes?.[0]?.preview?.requestedMode, "public") + assert.equal(recipeReview.browser?.probes?.[0]?.preview?.effectiveMode, "public") assert.equal(recipeReview.browser?.probes?.[0]?.localPreviewOrigin?.startsWith("http://127.0.0.1:"), true) assert.equal(recipeReview.browser?.probes?.[0]?.requestedPreviewOrigin, recipePublicUrl) assert.equal(recipeReview.browser?.probes?.[0]?.effectivePreviewOrigin, recipePublicUrl) + const localDefaultPort = await reserveFreePort() + const localDefaultPublicUrl = `http://127.0.0.1:${localDefaultPort}/public-route/` + const localDefaultRecipePath = await writeRecipe("recipe-preview-local-default.json", localDefaultPort, localDefaultPublicUrl, []) + const localDefaultOutput = await runCliJson(["recipe-run", "--recipe", localDefaultRecipePath, "--json"]) + + assert.equal(localDefaultOutput.success, true, localDefaultOutput.error?.message ?? "recipe-run with default local preview failed") + const localDefaultSummary = await browserSummary(localDefaultOutput) + assert.equal(localDefaultSummary.preview.requestedMode, "local") + assert.equal(localDefaultSummary.preview.effectiveMode, "local") + assert.equal(localDefaultSummary.preview.publicOrigin, localDefaultPublicUrl) + assert.equal(localDefaultSummary.effectivePreviewOrigin.startsWith("http://127.0.0.1:"), true) + assert.notEqual(localDefaultSummary.effectivePreviewOrigin, localDefaultPublicUrl) + const overridePort = await reserveFreePort() const staleRecipePublicUrl = "https://stale-preview.example.test/" const overridePublicUrl = `http://127.0.0.1:${overridePort}/cli-route/` @@ -47,6 +65,8 @@ try { assert.equal(overrideOutput.success, true, overrideOutput.error?.message ?? "recipe-run with CLI preview override failed") const overrideSummary = await browserSummary(overrideOutput) assert.equal(overrideSummary.requestedUrl, `${overridePublicUrl}relative-probe`) + assert.equal(overrideSummary.preview.requestedMode, "public") + assert.equal(overrideSummary.preview.effectiveMode, "public") assert.equal(overrideSummary.requestedPreviewOrigin, overridePublicUrl) assert.equal(overrideSummary.effectivePreviewOrigin, overridePublicUrl) assert.equal(overrideOutput.artifacts.preview.url, overridePublicUrl) @@ -56,12 +76,36 @@ try { assert.equal(overrideMetadata.provenance?.task?.preview?.effective?.publicUrl, overridePublicUrl) assert.equal(overrideMetadata.provenance?.task?.preview?.cliOverrides?.publicUrl, overridePublicUrl) + const missingSecurePort = await reserveFreePort() + const missingSecureRecipePath = await writeRecipe("recipe-preview-secure-missing.json", missingSecurePort, undefined, ["preview-mode=secure"]) + const missingSecureOutput = await runCliJson(["recipe-run", "--recipe", missingSecureRecipePath, "--json"]) + const missingSecureSummary = await browserSummary(missingSecureOutput) + + assert.equal(missingSecureOutput.success, false, "secure preview without a public URL should fail") + assert.match(missingSecureOutput.error?.message ?? "", /preview-mode=secure requires runtime\.preview\.publicUrl/) + assert.equal(missingSecureSummary.preview.requestedMode, "secure") + assert.equal(missingSecureSummary.preview.effectiveMode, "local") + assert.equal(missingSecureSummary.preview.diagnostics[0]?.code, "preview-public-origin-missing") + + const insecureSecurePort = await reserveFreePort() + const insecureSecurePublicUrl = `http://127.0.0.1:${insecureSecurePort}/secure-route/` + const insecureSecureRecipePath = await writeRecipe("recipe-preview-secure-insecure.json", insecureSecurePort, insecureSecurePublicUrl, ["preview-mode=secure"]) + const insecureSecureOutput = await runCliJson(["recipe-run", "--recipe", insecureSecureRecipePath, "--json"]) + const insecureSecureSummary = await browserSummary(insecureSecureOutput) + + assert.equal(insecureSecureOutput.success, false, "secure preview with an HTTP public URL should fail") + assert.match(insecureSecureOutput.error?.message ?? "", /preview-mode=secure requires an HTTPS public preview origin/) + assert.equal(insecureSecureSummary.preview.requestedMode, "secure") + assert.equal(insecureSecureSummary.preview.effectiveMode, "secure") + assert.equal(insecureSecureSummary.preview.publicOrigin, insecureSecurePublicUrl) + assert.equal(insecureSecureSummary.preview.diagnostics[0]?.code, "preview-public-origin-not-https") + console.log("Recipe preview routing browser probe smoke passed") } finally { await rm(workspace, { recursive: true, force: true }) } -async function writeRecipe(name: string, port: number, publicUrl: string): Promise { +async function writeRecipe(name: string, port: number, publicUrl: string | undefined, browserProbeArgs: string[] = ["preview-mode=public"]): Promise { const recipePath = join(workspace, name) const artifactsDirectory = join(workspace, `${name}-artifacts`) await writeFile(recipePath, `${JSON.stringify({ @@ -69,7 +113,7 @@ async function writeRecipe(name: string, port: number, publicUrl: string): Promi runtime: { preview: { port, - publicUrl, + ...(publicUrl ? { publicUrl } : {}), }, }, workflow: { @@ -80,6 +124,7 @@ async function writeRecipe(name: string, port: number, publicUrl: string): Promi "url=relative-probe", "wait-for=domcontentloaded", "capture=html", + ...browserProbeArgs, ], }, ], @@ -92,21 +137,21 @@ async function writeRecipe(name: string, port: number, publicUrl: string): Promi return recipePath } -async function browserSummary(output: { artifacts?: { directory?: string } }): Promise<{ requestedUrl: string; localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string }> { +async function browserSummary(output: { artifacts?: { directory?: string } }): Promise<{ requestedUrl: string; preview: { requestedMode: string; effectiveMode: string; localOrigin: string; publicOrigin?: string; effectiveOrigin: string; secureContext?: boolean; diagnostics: Array<{ code: string }> }; localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string }> { assert.ok(output.artifacts?.directory, "recipe-run should return an artifact directory") return JSON.parse(await readFile(join(output.artifacts.directory, "files", "browser", "summary.json"), "utf8")) } -async function review(output: { artifacts?: { directory?: string } }): Promise<{ browser?: { probes?: Array<{ requestedUrl?: string; localPreviewOrigin?: string; requestedPreviewOrigin?: string; effectivePreviewOrigin?: string }> } }> { +async function review(output: { artifacts?: { directory?: string } }): Promise<{ browser?: { probes?: Array<{ requestedUrl?: string; preview?: { requestedMode?: string; effectiveMode?: string }; localPreviewOrigin?: string; requestedPreviewOrigin?: string; effectivePreviewOrigin?: string }> } }> { assert.ok(output.artifacts?.directory, "recipe-run should return an artifact directory") return JSON.parse(await readFile(join(output.artifacts.directory, "files", "review.json"), "utf8")) } async function runCliJson(args: string[]): Promise { - const { stdout } = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", ...args], { + const stdout = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", ...args], { cwd: repoRoot, maxBuffer: 1024 * 1024 * 10, - }) + }).then((result) => result.stdout).catch((error: { stdout?: string }) => error.stdout ?? "") return JSON.parse(stdout) }