Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/server/src/project/ProjectSetupScriptRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,75 @@ describe("ProjectSetupScriptRunner", () => {
);
});

it.effect("gives a multi-repo cousin its own terminal and its original repo root", () => {
const open = vi.fn(() =>
Effect.succeed({
threadId: "thread-1",
terminalId: "setup-default-setup-web",
cwd: "/repo/worktrees/t/web",
worktreePath: "/repo/worktrees/t/web",
status: "running" as const,
pid: 123,
history: "",
exitCode: null,
exitSignal: null,
label: "setup-default-setup-web",
updatedAt: "2026-01-01T00:00:00.000Z",
}),
);
return Effect.gen(function* () {
const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
yield* runner.runForThread({
threadId: "thread-1",
projectId: "project-1",
worktreePath: "/repo/worktrees/t/web",
repoRoot: "/repo/web",
terminalIdSuffix: "web",
});
expect(open).toHaveBeenCalledWith({
threadId: "thread-1",
terminalId: "setup-default-setup-web",
cwd: "/repo/worktrees/t/web",
worktreePath: "/repo/worktrees/t/web",
env: {
T3CODE_PROJECT_ROOT: "/repo/project",
T3CODE_WORKTREE_PATH: "/repo/worktrees/t/web",
T3CODE_REPO_ROOT: "/repo/web",
NO_COLOR: "1",
FORCE_COLOR: "0",
},
});
}).pipe(
Effect.provide(
testLayer(
makeProject([]),
{ open, write: () => Effect.void },
ServerSettings.layerTest({
defaultProjectScripts: [
{
id: "default-setup",
name: "Setup",
command: "npm install",
icon: "configure",
runOnWorktreeCreate: true,
},
],
}),
),
),
);
});

it("keeps long setup terminal ids distinct within the terminal id limit", () => {
const repoPrefix = "service-".repeat(20);
const api = ProjectSetupScriptRunner.setupTerminalId("setup", `${repoPrefix}api`);
const web = ProjectSetupScriptRunner.setupTerminalId("setup", `${repoPrefix}web`);
expect(api).not.toBe(web);
expect(api.length).toBeLessThanOrEqual(128);
expect(web.length).toBeLessThanOrEqual(128);
expect(ProjectSetupScriptRunner.setupTerminalId("setup", "web")).toBe("setup-setup-web");
});

it.effect("returns no-script when no setup script exists", () => {
const open = vi.fn(() => Effect.die("unexpected open"));
const write = vi.fn(() => Effect.die("unexpected write"));
Expand Down
19 changes: 15 additions & 4 deletions apps/server/src/project/ProjectSetupScriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ function wrapCommandForCompletion(
}
}

const TERMINAL_ID_MAX_LENGTH = 128;

/**
* The setup terminal's id. Ids over the terminal limit keep a hash of the full
* id, so two repos whose suffixes differ only past the limit still get their
* own terminal instead of one stopping the other's script.
*/
export function setupTerminalId(scriptId: string, suffix?: string): string {
const id = suffix ? `setup-${scriptId}-${suffix}` : `setup-${scriptId}`;
if (id.length <= TERMINAL_ID_MAX_LENGTH) return id;
const hash = NodeCrypto.createHash("sha256").update(id).digest("hex").slice(0, 12);
return `${id.slice(0, TERMINAL_ID_MAX_LENGTH - hash.length - 1)}-${hash}`;
}

/** @public Service construction is part of the canonical Effect module API. */
export const make = Effect.gen(function* () {
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
Expand Down Expand Up @@ -354,10 +368,7 @@ export const make = Effect.gen(function* () {
}

const terminalId =
input.preferredTerminalId ??
(input.terminalIdSuffix
? `setup-${script.id}-${input.terminalIdSuffix}`.slice(0, 128)
: `setup-${script.id}`);
input.preferredTerminalId ?? setupTerminalId(script.id, input.terminalIdSuffix);
const cwd = input.worktreePath;
const env = projectScriptRuntimeEnv({
project: { cwd: project.workspaceRoot },
Expand Down
Loading