import { MAX_SCRIPT_ID_LENGTH, SCRIPT_RUN_COMMAND_PATTERN, type KeybindingCommand, type ProjectScript, } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN); export interface ProjectScriptInput { readonly name: ProjectScript["name"]; readonly command: ProjectScript["command"]; readonly icon: ProjectScript["icon"]; readonly runOnWorktreeCreate: ProjectScript["runOnWorktreeCreate"]; readonly previewUrl: Exclude | null; readonly autoOpenPreview: boolean; } export function buildProjectScript(id: string, input: ProjectScriptInput): ProjectScript { return { id, name: input.name, command: input.command, icon: input.icon, runOnWorktreeCreate: input.runOnWorktreeCreate, ...(input.previewUrl === null ? {} : { previewUrl: input.previewUrl, autoOpenPreview: input.autoOpenPreview, }), }; } function normalizeScriptId(value: string): string { const cleaned = value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); if (cleaned.length === 0) { return "script"; } if (cleaned.length <= MAX_SCRIPT_ID_LENGTH) { return cleaned; } return cleaned.slice(0, MAX_SCRIPT_ID_LENGTH).replace(/-+$/g, "") || "script"; } export const commandForProjectScript = (scriptId: string): KeybindingCommand => SCRIPT_RUN_COMMAND_PATTERN.make(`script.${scriptId}.run`); export function projectScriptIdFromCommand(command: string): string | null { const trimmed = command.trim(); if (!isScriptRunCommand(trimmed)) { return null; } const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts; return trimmed.slice(prefix.literal.length, -suffix.literal.length); } export function nextProjectScriptId(name: string, existingIds: Iterable): string { const taken = new Set(Array.from(existingIds)); const baseId = normalizeScriptId(name); if (!taken.has(baseId)) return baseId; let suffix = 2; while (suffix < 10_000) { const candidate = `${baseId}-${suffix}`; const safeCandidate = candidate.length <= MAX_SCRIPT_ID_LENGTH ? candidate : `${baseId.slice(0, Math.max(1, MAX_SCRIPT_ID_LENGTH - String(suffix).length - 1))}-${suffix}`; if (!taken.has(safeCandidate)) { return safeCandidate; } suffix += 1; } // This last-resort fallback only triggers after exhausting thousands of suffixes. return `${baseId}-${Date.now()}`.slice(0, MAX_SCRIPT_ID_LENGTH); } export function primaryProjectScript(scripts: ReadonlyArray): ProjectScript | null { const regular = scripts.find((script) => !script.runOnWorktreeCreate); return regular ?? scripts[0] ?? null; }