import { z } from "zod"; export function normalizeLifecycleCommands(commands: unknown): string[] { if (typeof commands === "string") { return commands.trim().length > 0 ? [commands] : []; } if (!Array.isArray(commands)) { return []; } return commands.filter((command): command is string => { return typeof command === "string" && command.trim().length > 0; }); } export const PaseoLifecycleCommandRawSchema = z.union([z.string(), z.array(z.string())]); export const PaseoScriptEntryRawSchema = z .object({ type: z.unknown().optional(), command: z.unknown().optional(), port: z.unknown().optional(), }) .passthrough(); export const PaseoWorktreeConfigRawSchema = z .object({ setup: PaseoLifecycleCommandRawSchema.optional(), teardown: PaseoLifecycleCommandRawSchema.optional(), terminals: z.unknown().optional(), }) .passthrough(); export const PaseoMetadataGenerationEntrySchema = z .object({ instructions: z.string().optional(), }) .passthrough() .catch({}); export const PaseoMetadataGenerationSchema = z .object({ title: PaseoMetadataGenerationEntrySchema.optional(), branchName: PaseoMetadataGenerationEntrySchema.optional(), commitMessage: PaseoMetadataGenerationEntrySchema.optional(), pullRequest: PaseoMetadataGenerationEntrySchema.optional(), }) // COMPAT(projectMetadataAgentTitle): `agentTitle` project metadata prompts were removed // in v0.1.96; keep legacy paseo.json parseable until 2026-12-16. .passthrough() .catch({}); export const PaseoConfigRawSchema = z .object({ worktree: PaseoWorktreeConfigRawSchema.optional(), scripts: z.record(z.string(), PaseoScriptEntryRawSchema).optional(), metadataGeneration: PaseoMetadataGenerationSchema.optional(), }) .passthrough(); export const WorktreeConfigSchema = PaseoWorktreeConfigRawSchema.extend({ setup: z.unknown().optional().transform(normalizeLifecycleCommands), teardown: z.unknown().optional().transform(normalizeLifecycleCommands), }) .passthrough() .catch({ setup: [], teardown: [] }); export const ScriptEntrySchema = PaseoScriptEntryRawSchema.catch({}); export const PaseoConfigSchema = PaseoConfigRawSchema.extend({ worktree: WorktreeConfigSchema.optional(), scripts: z.record(z.string(), ScriptEntrySchema).optional().catch({}), metadataGeneration: PaseoMetadataGenerationSchema.optional(), }) .passthrough() .catch({}); export const PaseoConfigRevisionSchema = z.object({ mtimeMs: z.number(), size: z.number(), }); export const ProjectConfigRpcErrorSchema = z.discriminatedUnion("code", [ z.object({ code: z.literal("project_not_found") }), z.object({ code: z.literal("invalid_project_config") }), z.object({ code: z.literal("stale_project_config"), currentRevision: PaseoConfigRevisionSchema.nullable(), }), z.object({ code: z.literal("write_failed") }), ]); export type PaseoScriptEntryRaw = z.infer; export type PaseoMetadataGenerationEntry = z.infer; export type PaseoMetadataGeneration = z.infer; export type PaseoConfigRaw = z.infer; export type PaseoConfig = z.infer; export type PaseoConfigRevision = z.infer; export type ProjectConfigRpcError = z.infer;