/**
 * Launch metadata generation for buildpack-compatible output.
 *
 * Generates the metadata that tells the runtime how to start the application.
 * This is analogous to CNB's launch.toml / Docker CMD.
 */
import { join } from 'node:path';
import { mkdirSync, writeFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { z } from 'zod';
import { pathExists } from "../../../node-compat/fs.js";
/**
 * Filename the CLI looks for at the project root when a user wants to
 * override or fully supply the launch metadata. Same name as the file
 * we emit into the build output so users can copy/edit one we generated.
 */
export const USER_LAUNCH_FILENAME = 'launch.json';
/**
 * Structural shape of a user-supplied `launch.json`. `.passthrough()`
 * everywhere so unknown extra keys — including the machine-generated
 * `build` field users copy from an emitted launch.json — pass through
 * unrejected. Wrong *types* on known fields still fail validation.
 *
 * `processes[].default` is optional here even though the internal
 * `ProcessDefinition` requires it: files written before this field
 * existed must keep working. Callers coerce with `default ?? false`.
 *
 * Every optional field is `.nullish()` + a `?? undefined` transform, not
 * plain `.optional()`: the pre-Zod code read these fields with `?.`,
 * which tolerated an explicit JSON `null` as well as absence. Collapsing
 * `null` to `undefined` here (rather than leaving it in the parsed
 * shape) keeps `UserLaunchOverride` — derived via `z.infer` — free of
 * `| null`, so downstream consumers only ever handle "absent".
 */
const UserLaunchProcessSchema = z
    .object({
    type: z.string(),
    command: z.string(),
    default: z
        .boolean()
        .nullish()
        .transform((v) => v ?? undefined),
    workingDirectory: z
        .string()
        .nullish()
        .transform((v) => v ?? undefined),
})
    .passthrough();
const UserLaunchOverrideSchema = z
    .object({
    processes: z
        .array(UserLaunchProcessSchema)
        .nullish()
        .transform((v) => v ?? undefined),
    framework: z
        .object({
        name: z
            .string()
            .nullish()
            .transform((v) => v ?? undefined),
        version: z
            .string()
            .nullish()
            .transform((v) => v ?? undefined),
    })
        .passthrough()
        .nullish()
        .transform((v) => v ?? undefined),
    runtime: z
        .object({
        name: z
            .string()
            .nullish()
            .transform((v) => v ?? undefined),
        port: z
            .number()
            .nullish()
            .transform((v) => v ?? undefined),
    })
        .passthrough()
        .nullish()
        .transform((v) => v ?? undefined),
})
    .passthrough();
/**
 * Thrown by `readUserLaunchOverride` for both invalid JSON and
 * schema-invalid `launch.json` files. Callers that own a `CommandContext`
 * (inspect, build) catch this and translate it into a `CONFIG_INVALID`
 * structured error instead of letting a raw crash reach the user.
 */
export class LaunchConfigError extends Error {
    filePath;
    issues;
    constructor(filePath, issues, message) {
        super(message);
        this.name = 'LaunchConfigError';
        this.filePath = filePath;
        this.issues = issues;
    }
}
/**
 * Read a user-supplied `launch.json` from the project root, if any.
 *
 * Returns `null` when the file is missing. Throws `LaunchConfigError` on
 * invalid JSON or a structurally invalid shape — a malformed override is
 * a user error worth surfacing rather than silently falling back to
 * inference (or, worse, crashing deep inside a consumer that assumed the
 * shape was already validated).
 */
export async function readUserLaunchOverride(projectDir) {
    const path = join(projectDir, USER_LAUNCH_FILENAME);
    if (!(await pathExists(path)))
        return null;
    let parsed;
    try {
        parsed = JSON.parse(await readFile(path, 'utf-8'));
    }
    catch (ex) {
        const message = ex.message;
        throw new LaunchConfigError(path, [{ path: 'root', message }], `Invalid ${USER_LAUNCH_FILENAME} at ${path}: ${message}`);
    }
    const result = UserLaunchOverrideSchema.safeParse(parsed);
    if (!result.success) {
        const issues = result.error.issues.map((issue) => ({
            path: issue.path.join('.') || 'root',
            message: issue.message,
        }));
        const summary = issues.map((issue) => `${issue.path}: ${issue.message}`).join('; ');
        throw new LaunchConfigError(path, issues, `Invalid ${USER_LAUNCH_FILENAME} at ${path}: ${summary}`);
    }
    return result.data;
}
/**
 * Generate launch metadata from a build result and detected framework.
 *
 * If a user-supplied override is provided, its fields take precedence:
 * `processes` (whole array replaces ours), `framework.{name,version}`,
 * and `runtime.{name,port}`. `build.{date,duration}` is always emitted
 * from the actual build and cannot be overridden.
 */
export function generateLaunchMetadata(framework, buildResult, override, monorepo) {
    const processes = [];
    // Primary web process
    const startCommand = buildResult.startCommand ?? framework.startCommand;
    if (startCommand) {
        processes.push({
            type: 'web',
            command: startCommand,
            default: true,
            // Pilot interprets a relative `workingDirectory` against the
            // container's deploy root (`/home/agentuity/app`). The
            // monorepo subpath places the process inside the workspace
            // subpackage so paths in the start command (`dist/index.js`,
            // `.next/standalone/server.js`, ...) resolve unchanged.
            ...(monorepo?.subpath ? { workingDirectory: monorepo.subpath } : {}),
        });
    }
    // Reconcile runtime against the actual launch command.
    // `framework.runtime` reflects what the detector inferred from the
    // project (lockfile, engines, the start script before adapters had
    // a chance to rewrite it). But adapters can override the start
    // command — e.g. the Next.js adapter writes `node server.js`
    // because Next.js standalone is Node-only, even if the user's
    // project uses Bun for everything else. The runtime in launch.json
    // must match what gets executed; pilot's memory tuning depends on
    // it.
    const runtimeName = (() => {
        if (startCommand && /^\s*bun(\s+run)?\s+/.test(startCommand))
            return 'bun';
        if (startCommand && /^\s*node(\s|$)/.test(startCommand))
            return 'node';
        return framework.runtime;
    })();
    // The user schema keeps `default` optional for backward compat with
    // files written before this field existed; the emitted metadata's
    // `ProcessDefinition` requires it, so coerce here at the boundary.
    const finalProcesses = override?.processes && override.processes.length > 0
        ? override.processes.map((p) => ({ ...p, default: p.default ?? false }))
        : processes;
    return {
        processes: finalProcesses,
        framework: {
            name: override?.framework?.name ?? framework.name,
            version: override?.framework?.version ?? framework.version,
        },
        runtime: {
            name: override?.runtime?.name ?? runtimeName,
            port: override?.runtime?.port ?? buildResult.port ?? framework.port,
        },
        build: {
            date: new Date().toISOString(),
            duration: buildResult.duration,
        },
    };
}
/**
 * Write launch metadata to the output directory.
 *
 * Writes launch.json — machine-readable launch metadata.
 */
export function writeLaunchMetadata(outputDir, metadata) {
    mkdirSync(outputDir, { recursive: true });
    const jsonPath = join(outputDir, 'launch.json');
    writeFileSync(jsonPath, JSON.stringify(metadata, null, 2), 'utf-8');
}
//# sourceMappingURL=launch.js.map