Skip to content

Commit a560c1f

Browse files
committed
feat(worktree): enhance worktree management with new functions and improved path resolution
- Introduced functions for resolving worktree instance roles and reading worktree registries. - Added utilities for finding worktree roots and validating worktree CLI scripts. - Updated promotion functions to utilize new worktree management capabilities, improving overall workflow and error handling. - Refactored existing code to streamline worktree path resolution and enhance maintainability.
1 parent 9556e9e commit a560c1f

3 files changed

Lines changed: 186 additions & 24 deletions

File tree

apps/server/src/local/promotionApply.ts

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ import {
2121
} from "./promotionStatus.ts";
2222
import { writeRelaunchQuiesce } from "./relaunchQuiesce.ts";
2323
import { writePromotionPreSnapshot } from "./promotionSnapshot.ts";
24-
import {
25-
resolveWorktreeInstanceRole,
26-
type WorktreeRegistry,
27-
tryResolveWorktreeRecordFromEnv,
28-
} from "../../../../scripts/worktree-instance.ts";
2924

3025
export type LocalDesktopPromotionApplyResult =
3126
| {
@@ -62,6 +57,124 @@ const CONVEX_PROMOTION_PUSH_RELATIVE_PATH = NodePath.join(
6257
"scripts",
6358
"convex-promotion-push.mjs",
6459
);
60+
const WORKTREE_CLI_RELATIVE_PATH = NodePath.join("scripts", "worktree-cli.ts");
61+
const WORKTREE_DEV_INSTANCE_PREFIX = "t3code-local-worktree-";
62+
const WORKTREE_REGISTRY_RELATIVE_PATH = NodePath.join(
63+
".local",
64+
"share",
65+
"t3code-local",
66+
"worktrees.json",
67+
);
68+
69+
type WorktreeInstanceRole = "main" | "staging";
70+
71+
interface WorktreeInstanceRecord {
72+
readonly slug: string;
73+
readonly role?: WorktreeInstanceRole;
74+
}
75+
76+
export interface WorktreeRegistry {
77+
readonly instances: readonly WorktreeInstanceRecord[];
78+
}
79+
80+
function resolveWorktreeInstanceRole(record: WorktreeInstanceRecord): WorktreeInstanceRole {
81+
const role = record.role ?? "staging";
82+
if (role === "main" || role === "staging") {
83+
return role;
84+
}
85+
throw new Error(`Invalid worktree role "${record.role}".`);
86+
}
87+
88+
function findWorktreeRecord(
89+
slug: string,
90+
registry: WorktreeRegistry,
91+
): WorktreeInstanceRecord | undefined {
92+
return registry.instances.find((record) => record.slug === slug);
93+
}
94+
95+
function tryResolveWorktreeRecordFromEnv(
96+
env: NodeJS.ProcessEnv,
97+
registry: WorktreeRegistry,
98+
): WorktreeInstanceRecord | undefined {
99+
const slugFromEnv = env.T3CODE_WORKTREE_SLUG?.trim();
100+
if (slugFromEnv) {
101+
return findWorktreeRecord(slugFromEnv, registry);
102+
}
103+
104+
const devInstance = env.T3CODE_DEV_INSTANCE?.trim();
105+
if (!devInstance?.startsWith(WORKTREE_DEV_INSTANCE_PREFIX)) {
106+
return undefined;
107+
}
108+
109+
return findWorktreeRecord(devInstance.slice(WORKTREE_DEV_INSTANCE_PREFIX.length), registry);
110+
}
111+
112+
function readWorktreeRegistry(homeDirectory = NodeOS.homedir()): WorktreeRegistry {
113+
const registryPath = NodePath.join(homeDirectory, WORKTREE_REGISTRY_RELATIVE_PATH);
114+
try {
115+
const parsed = JSON.parse(NodeFS.readFileSync(registryPath, "utf8")) as {
116+
readonly instances?: unknown;
117+
};
118+
if (!Array.isArray(parsed.instances)) {
119+
return { instances: [] };
120+
}
121+
return {
122+
instances: parsed.instances.flatMap((entry) => {
123+
if (!entry || typeof entry !== "object") {
124+
return [];
125+
}
126+
const record = entry as Partial<WorktreeInstanceRecord>;
127+
if (typeof record.slug !== "string") {
128+
return [];
129+
}
130+
if (record.role !== undefined && record.role !== "main" && record.role !== "staging") {
131+
return [];
132+
}
133+
return [record as WorktreeInstanceRecord];
134+
}),
135+
};
136+
} catch {
137+
return { instances: [] };
138+
}
139+
}
140+
141+
function candidateSiblingStagingWorktreePath(worktreePath: string | undefined): string | undefined {
142+
const trimmed = worktreePath?.trim();
143+
if (!trimmed) {
144+
return undefined;
145+
}
146+
return NodePath.join(NodePath.dirname(NodePath.resolve(trimmed)), "t3code-staging");
147+
}
148+
149+
function findNearestWorktreeRoot(startPath: string): string | undefined {
150+
let current = NodePath.resolve(startPath);
151+
while (true) {
152+
if (NodeFS.existsSync(NodePath.join(current, WORKTREE_CLI_RELATIVE_PATH))) {
153+
return current;
154+
}
155+
const parent = NodePath.dirname(current);
156+
if (parent === current) {
157+
return undefined;
158+
}
159+
current = parent;
160+
}
161+
}
162+
163+
function hasWorktreeCli(stagingWorktreePath: string): boolean {
164+
return NodeFS.existsSync(resolveWorktreeCliScriptPath(stagingWorktreePath));
165+
}
166+
167+
export function resolveWorktreeCliScriptPath(stagingWorktreePath: string): string {
168+
return NodePath.join(stagingWorktreePath, WORKTREE_CLI_RELATIVE_PATH);
169+
}
170+
171+
function requireWorktreeCliScriptPath(stagingWorktreePath: string): string {
172+
const scriptPath = resolveWorktreeCliScriptPath(stagingWorktreePath);
173+
if (!NodeFS.existsSync(scriptPath)) {
174+
throw new Error(`Missing worktree launcher script: ${scriptPath}`);
175+
}
176+
return scriptPath;
177+
}
65178

66179
/**
67180
* Default promotion pushes schema/functions in place; only full DB copy needs a backend restart.
@@ -928,21 +1041,39 @@ export type MainPromotionRelaunchTarget =
9281041
readonly stagingWorktreePath: string;
9291042
};
9301043

931-
function resolveDefaultStagingWorktreePath(): string {
932-
return NodePath.resolve(NodePath.dirname(fileURLToPath(import.meta.url)), "../../../..");
1044+
export function resolveDefaultStagingWorktreePath(env: NodeJS.ProcessEnv = process.env): string {
1045+
const moduleWorktreeRoot = findNearestWorktreeRoot(
1046+
NodePath.dirname(fileURLToPath(import.meta.url)),
1047+
);
1048+
const cwdWorktreeRoot = findNearestWorktreeRoot(process.cwd());
1049+
const candidates = [
1050+
candidateSiblingStagingWorktreePath(env.T3CODE_APP_ROOT),
1051+
candidateSiblingStagingWorktreePath(env.T3CODE_LOCAL_MAIN_WORKTREE),
1052+
candidateSiblingStagingWorktreePath(cwdWorktreeRoot),
1053+
candidateSiblingStagingWorktreePath(moduleWorktreeRoot),
1054+
NodePath.resolve(NodePath.dirname(fileURLToPath(import.meta.url)), "../../../.."),
1055+
].filter((value): value is string => Boolean(value));
1056+
1057+
for (const candidate of candidates) {
1058+
if (hasWorktreeCli(candidate)) {
1059+
return candidate;
1060+
}
1061+
}
1062+
1063+
return candidates[0] ?? NodePath.resolve(process.cwd(), "../t3code-staging");
9331064
}
9341065

9351066
export function resolveMainPromotionRelaunchTarget(
9361067
env: NodeJS.ProcessEnv = process.env,
937-
registry?: WorktreeRegistry,
1068+
registry: WorktreeRegistry = readWorktreeRegistry(),
9381069
): MainPromotionRelaunchTarget {
9391070
const record = tryResolveWorktreeRecordFromEnv(env, registry);
9401071
if (record && resolveWorktreeInstanceRole(record) === "main") {
9411072
return {
9421073
kind: "worktree",
9431074
slug: record.slug,
9441075
stagingWorktreePath:
945-
env.T3CODE_LOCAL_STAGING_WORKTREE?.trim() || resolveDefaultStagingWorktreePath(),
1076+
env.T3CODE_LOCAL_STAGING_WORKTREE?.trim() || resolveDefaultStagingWorktreePath(env),
9461077
};
9471078
}
9481079

@@ -957,7 +1088,7 @@ export function spawnDetachedWorktreeLauncher(
9571088
return;
9581089
}
9591090

960-
const worktreeCli = NodePath.join(input.stagingWorktreePath, "scripts/worktree-cli.ts");
1091+
const worktreeCli = requireWorktreeCliScriptPath(input.stagingWorktreePath);
9611092
const child = NodeChildProcess.spawn(process.execPath, [worktreeCli, "start", input.slug], {
9621093
cwd: input.stagingWorktreePath,
9631094
detached: true,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as NodePath from "node:path";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { resolvePromotionPreflightCheckScriptPath } from "./promotionPreflight.ts";
5+
6+
describe("promotionPreflight script paths", () => {
7+
it("resolves the UI/model check script from the staging worktree", () => {
8+
const stagingWorktree = "/worktrees/t3code-staging";
9+
10+
expect(resolvePromotionPreflightCheckScriptPath(stagingWorktree)).toBe(
11+
NodePath.join(stagingWorktree, "scripts", "local-promotion-preflight-check.ts"),
12+
);
13+
});
14+
});

apps/server/src/local/promotionPreflight.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,19 @@ import {
2323
writeWorktreeAgentAutomation,
2424
writeWorktreeDesktopSettings,
2525
} from "../../../../scripts/worktree-instance.ts";
26-
import { autoCommitStagingChangesForPromotion } from "./promotionApply.ts";
26+
import {
27+
autoCommitStagingChangesForPromotion,
28+
resolveWorktreeCliScriptPath,
29+
} from "./promotionApply.ts";
2730
import { readLocalWorktreePathsFromEnv } from "./promotionStatus.ts";
2831

2932
const TEST_SLUG = "test";
3033
const TOKEN_TTL_MS = 30 * 60 * 1_000;
3134
const PREFLIGHT_STATUS_FILENAME = "promotion-preflight-status.json";
3235
const PREFLIGHT_TOKEN_FILENAME = "promotion-preflight-passed.json";
33-
const WORKTREE_CLI_SCRIPT = NodePath.resolve(
34-
NodePath.dirname(new URL(import.meta.url).pathname),
35-
"../../../../scripts/worktree-cli.ts",
36-
);
37-
const PREFLIGHT_CHECK_SCRIPT = NodePath.resolve(
38-
NodePath.dirname(new URL(import.meta.url).pathname),
39-
"../../../../scripts/local-promotion-preflight-check.ts",
36+
const PREFLIGHT_CHECK_RELATIVE_PATH = NodePath.join(
37+
"scripts",
38+
"local-promotion-preflight-check.ts",
4039
);
4140

4241
type PromotionPreflightToken = {
@@ -395,13 +394,28 @@ function runConvexValidation(input: {
395394
});
396395
}
397396

397+
export function resolvePromotionPreflightCheckScriptPath(stagingWorktreePath: string): string {
398+
return NodePath.join(stagingWorktreePath, PREFLIGHT_CHECK_RELATIVE_PATH);
399+
}
400+
401+
function requireScriptPath(path: string, label: string): string {
402+
if (!NodeFS.existsSync(path)) {
403+
throw new Error(`Missing ${label}: ${path}`);
404+
}
405+
return path;
406+
}
407+
398408
function startTestStack(input: {
399409
readonly stagingWorktreePath: string;
400410
readonly urls: ReturnType<typeof resolveWorktreeInstanceUrls>;
401411
}): void {
402412
writeWorktreeDesktopSettings(input.urls.t3Home, input.urls.tailscaleServePath);
403413
writeWorktreeAgentAutomation(input.urls.t3Home, input.urls.cdpPort);
404-
runScript(process.execPath, [WORKTREE_CLI_SCRIPT, "start", TEST_SLUG, "--wait"], {
414+
const worktreeCliScript = requireScriptPath(
415+
resolveWorktreeCliScriptPath(input.stagingWorktreePath),
416+
"worktree launcher script",
417+
);
418+
runScript(process.execPath, [worktreeCliScript, "start", TEST_SLUG, "--wait"], {
405419
cwd: input.stagingWorktreePath,
406420
env: {
407421
...process.env,
@@ -414,15 +428,17 @@ function startTestStack(input: {
414428
}
415429

416430
function runUiAndModelChecks(input: {
431+
readonly stagingWorktreePath: string;
417432
readonly testWorktreePath: string;
418433
readonly urls: ReturnType<typeof resolveWorktreeInstanceUrls>;
419434
}): string {
420-
if (!NodeFS.existsSync(PREFLIGHT_CHECK_SCRIPT)) {
421-
throw new Error(`Missing preflight UI/model check script: ${PREFLIGHT_CHECK_SCRIPT}`);
422-
}
435+
const preflightCheckScript = requireScriptPath(
436+
resolvePromotionPreflightCheckScriptPath(input.stagingWorktreePath),
437+
"preflight UI/model check script",
438+
);
423439

424-
const stdout = runScript(process.execPath, [PREFLIGHT_CHECK_SCRIPT], {
425-
cwd: NodePath.resolve(NodePath.dirname(new URL(import.meta.url).pathname), "../../../../"),
440+
const stdout = runScript(process.execPath, [preflightCheckScript], {
441+
cwd: input.stagingWorktreePath,
426442
env: {
427443
...process.env,
428444
T3CODE_PREFLIGHT_VITE_URL: `http://127.0.0.1:${input.urls.webPort}`,
@@ -567,6 +583,7 @@ async function runPreflight(
567583
startStep("verify-desktop-and-vite", "Checking projects and messages on Vite and Desktop.");
568584
startStep("send-real-model-check", "Sending Hi through the real configured provider.");
569585
const modelCheckThreadId = runUiAndModelChecks({
586+
stagingWorktreePath: paths.localStagingWorktreePath,
570587
testWorktreePath: prepared.testWorktreePath,
571588
urls,
572589
});

0 commit comments

Comments
 (0)