Skip to content
Source
Sign inSign up

projectScripts.ts

108 lines3.5 KB

1import {
2 MAX_SCRIPT_ID_LENGTH,
3 SCRIPT_RUN_COMMAND_PATTERN,
4 type KeybindingCommand,
5 type ProjectScript,
6} from "@t3tools/contracts";
7import * as Schema from "effect/Schema";
8const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN);
9​
10export interface ProjectScriptInput {
11 readonly name: ProjectScript["name"];
12 readonly command: ProjectScript["command"];
13 readonly icon: ProjectScript["icon"];
14 readonly runOnWorktreeCreate: ProjectScript["runOnWorktreeCreate"];
15 readonly waitForSetup: boolean;
16 readonly runOnSettle: boolean;
17 readonly stopOnSettle?: boolean;
18 readonly previewUrl: Exclude<ProjectScript["previewUrl"], undefined> | null;
19 readonly autoOpenPreview: boolean;
21​
22export function buildProjectScript(id: string, input: ProjectScriptInput): ProjectScript {
23 return {
24 id,
25 name: input.name,
26 command: input.command,
27 icon: input.icon,
28 runOnWorktreeCreate: input.runOnWorktreeCreate,
29 ...(input.runOnWorktreeCreate && input.waitForSetup ? { async: false } : {}),
30 ...(input.runOnSettle ? { runOnSettle: true } : {}),
31 ...(input.stopOnSettle ? { stopOnSettle: true } : {}),
32 ...(input.previewUrl === null
33 ? {}
34 : {
35 previewUrl: input.previewUrl,
36 autoOpenPreview: input.autoOpenPreview,
37 }),
38 };
40​
41export function releaseClaimedRoles(
42 script: ProjectScript,
43 saved: ProjectScriptInput,
44): ProjectScript {
45 const releaseSetup = saved.runOnWorktreeCreate && script.runOnWorktreeCreate;
46 const releaseSettle = saved.runOnSettle && script.runOnSettle === true;
47 if (!releaseSetup && !releaseSettle) return script;
48 return {
49 ...script,
50 ...(releaseSetup ? { runOnWorktreeCreate: false } : {}),
51 ...(releaseSettle ? { runOnSettle: false } : {}),
52 };
54​
55function normalizeScriptId(value: string): string {
56 const cleaned = value
57 .trim()
58 .toLowerCase()
59 .replace(/[^a-z0-9]+/g, "-")
60 .replace(/^-+|-+$/g, "");
61 if (cleaned.length === 0) {
62 return "script";
63 }
64 if (cleaned.length <= MAX_SCRIPT_ID_LENGTH) {
65 return cleaned;
66 }
67 return cleaned.slice(0, MAX_SCRIPT_ID_LENGTH).replace(/-+$/g, "") || "script";
69​
70export function commandForProjectScript(scriptId: string): KeybindingCommand | null {
71 const command = `script.${scriptId}.run`;
72 return isScriptRunCommand(command) ? command : null;
74​
75export function projectScriptIdFromCommand(command: string): string | null {
76 const trimmed = command.trim();
77 if (!isScriptRunCommand(trimmed)) {
78 return null;
79 }
80 const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts;
81 return trimmed.slice(prefix.literal.length, -suffix.literal.length);
83​
84export function nextProjectScriptId(name: string, existingIds: Iterable<string>): string {
85 const taken = new Set(Array.from(existingIds));
86 const baseId = normalizeScriptId(name);
87 if (!taken.has(baseId)) return baseId;
88​
89 let suffix = 2;
90 while (suffix < 10_000) {
91 const candidate = `${baseId}-${suffix}`;
92 const safeCandidate =
93 candidate.length <= MAX_SCRIPT_ID_LENGTH
94 ? candidate
95 : `${baseId.slice(0, Math.max(1, MAX_SCRIPT_ID_LENGTH - String(suffix).length - 1))}-${suffix}`;
96 if (!taken.has(safeCandidate)) {
97 return safeCandidate;
98 }
99 suffix += 1;
100 }
101​
102 return `${baseId}-${Date.now()}`.slice(0, MAX_SCRIPT_ID_LENGTH);
104​
105export function primaryProjectScript(scripts: ReadonlyArray<ProjectScript>): ProjectScript | null {
106 const regular = scripts.find((script) => !script.runOnWorktreeCreate && !script.runOnSettle);
107 return regular ?? scripts.find((script) => !script.runOnSettle) ?? null;