Distinguish services from scripts with type system and runtime tracking

This commit is contained in:
Mohamed Boudra
2026-04-09 23:45:00 +07:00
parent c7399084a4
commit b1b85833bb
27 changed files with 1265 additions and 493 deletions
@@ -3,7 +3,9 @@ import {
createWorktree,
deriveWorktreeProjectHash,
deletePaseoWorktree,
getScriptConfigs,
getWorktreeTerminalSpecs,
isServiceScript,
isPaseoOwnedWorktreeCwd,
listPaseoWorktrees,
resolveWorktreeRuntimeEnv,
@@ -483,6 +485,91 @@ describe("createWorktree", () => {
{ command: "npm run test" },
]);
});
it("parses omitted script type as a plain script", async () => {
writeFileSync(
join(repoDir, "paseo.json"),
JSON.stringify({
scripts: {
typecheck: {
command: " npm run typecheck ",
},
},
}),
);
const scriptConfigs = getScriptConfigs(repoDir);
const typecheck = scriptConfigs.get("typecheck");
expect(typecheck).toEqual({
command: "npm run typecheck",
});
expect(typecheck).toBeDefined();
expect(isServiceScript(typecheck!)).toBe(false);
});
it("parses service scripts and preserves optional port", async () => {
writeFileSync(
join(repoDir, "paseo.json"),
JSON.stringify({
scripts: {
server: {
type: "service",
command: "npm run dev",
port: 4321,
},
},
}),
);
const scriptConfigs = getScriptConfigs(repoDir);
const server = scriptConfigs.get("server");
expect(server).toEqual({
type: "service",
command: "npm run dev",
port: 4321,
});
expect(server).toBeDefined();
expect(isServiceScript(server!)).toBe(true);
});
it("ignores invalid script entries gracefully", async () => {
writeFileSync(
join(repoDir, "paseo.json"),
JSON.stringify({
scripts: {
valid: {
command: "npm run valid",
},
invalidType: {
type: "worker",
command: "npm run worker",
},
missingCommand: {
type: "service",
},
blankCommand: {
command: " ",
},
nonObject: "npm run nope",
invalidPort: {
type: "service",
command: "npm run dev",
port: "3000",
},
},
}),
);
expect(getScriptConfigs(repoDir)).toEqual(
new Map([
["valid", { command: "npm run valid" }],
["invalidType", { command: "npm run worker" }],
["invalidPort", { type: "service", command: "npm run dev" }],
]),
);
});
});
describe("paseo worktree manager", () => {
+30 -4
View File
@@ -24,7 +24,14 @@ interface PaseoConfig {
teardown?: string[];
terminals?: WorktreeTerminalConfig[];
};
scripts?: Record<string, ScriptConfig>;
scripts?: Record<
string,
{
type?: unknown;
command?: unknown;
port?: unknown;
}
>;
}
const execAsync = promisify(exec);
@@ -90,11 +97,24 @@ export interface WorktreeTerminalConfig {
command: string;
}
export interface ScriptConfig {
export interface PlainScriptConfig {
type?: undefined;
command: string;
port?: undefined;
}
export interface ServiceScriptConfig {
type: "service";
command: string;
port?: number; // explicit port override, otherwise auto-assigned
}
export type ScriptConfig = PlainScriptConfig | ServiceScriptConfig;
export function isServiceScript(config: ScriptConfig): config is ServiceScriptConfig {
return "type" in config && config.type === "service";
}
export class WorktreeSetupError extends Error {
readonly results: WorktreeSetupCommandResult[];
@@ -227,9 +247,15 @@ export function getScriptConfigs(repoRoot: string): Map<string, ScriptConfig> {
continue;
}
const scriptConfig: ScriptConfig = { command };
const scriptConfig: ScriptConfig =
entry.type === "service"
? {
type: "service",
command,
}
: { command };
if (typeof entry.port === "number" && Number.isFinite(entry.port)) {
if (isServiceScript(scriptConfig) && typeof entry.port === "number" && Number.isFinite(entry.port)) {
scriptConfig.port = entry.port;
}