import { z } from "zod"; export const PROJECT_SCRIPT_ICON_VALUES = [ "play", "test", "lint", "configure", "build", "debug", ] as const; export const projectScriptIconSchema = z.enum(PROJECT_SCRIPT_ICON_VALUES); export const projectScriptSchema = z .object({ id: z .string() .trim() .min(1) .max(64) .regex(/^[a-z0-9][a-z0-9-]*$/), name: z.string().trim().min(1).max(64), command: z.string().trim().min(1).max(10_000), icon: projectScriptIconSchema, runOnWorktreeCreate: z.boolean().default(false), }) .strict(); export const projectScriptsSchema = z.array(projectScriptSchema).max(64); export const projectRecordSchema = z.object({ id: z.string().min(1), cwd: z.string().min(1), name: z.string().min(1), scripts: projectScriptsSchema.default([]), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }); export const projectListResultSchema = z.array(projectRecordSchema); export const projectAddInputSchema = z.object({ cwd: z.string().trim().min(1), }); export const projectAddResultSchema = z.object({ project: projectRecordSchema, created: z.boolean(), }); export const projectRemoveInputSchema = z.object({ id: z.string().min(1), }); export const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200; export const projectSearchEntriesInputSchema = z.object({ cwd: z.string().trim().min(1), query: z.string().trim().max(256).default(""), limit: z.number().int().min(1).max(PROJECT_SEARCH_ENTRIES_MAX_LIMIT).default(80), }); export const projectEntryKindSchema = z.enum(["file", "directory"]); export const projectEntrySchema = z.object({ path: z.string().min(1), kind: projectEntryKindSchema, parentPath: z.string().optional(), }); export const projectSearchEntriesResultSchema = z.object({ entries: z.array(projectEntrySchema), truncated: z.boolean(), }); export const projectUpdateScriptsInputSchema = z.object({ id: z.string().min(1), scripts: projectScriptsSchema, }); export const projectUpdateScriptsResultSchema = z.object({ project: projectRecordSchema, }); export type ProjectScriptIcon = z.infer; export type ProjectScript = z.infer; /** * Deduplicate scripts by ID and enforce at most one `runOnWorktreeCreate: true` * flag (the first one wins). Fields are trimmed defensively so this is safe to * call on data that hasn't been through Zod parsing. */ export function normalizeProjectScripts(scripts: readonly ProjectScript[]): ProjectScript[] { const seenIds = new Set(); const deduped: ProjectScript[] = []; for (const script of scripts) { const id = script.id.trim(); if (id.length === 0 || seenIds.has(id)) continue; seenIds.add(id); deduped.push({ ...script, id, name: script.name.trim(), command: script.command.trim(), }); } let setupAssigned = false; const normalized: ProjectScript[] = []; for (const script of deduped) { if (!script.runOnWorktreeCreate) { normalized.push(script); continue; } if (!setupAssigned) { setupAssigned = true; normalized.push(script); continue; } normalized.push({ ...script, runOnWorktreeCreate: false }); } return normalized; } export type ProjectRecord = z.infer; export type ProjectListResult = z.infer; export type ProjectAddInput = z.input; export type ProjectAddResult = z.infer; export type ProjectRemoveInput = z.input; export type ProjectSearchEntriesInput = z.input; export type ProjectEntryKind = z.infer; export type ProjectEntry = z.infer; export type ProjectSearchEntriesResult = z.infer; export type ProjectUpdateScriptsInput = z.input; export type ProjectUpdateScriptsResult = z.infer;