@@ -21,11 +21,6 @@ import {
2121} from "./promotionStatus.ts" ;
2222import { writeRelaunchQuiesce } from "./relaunchQuiesce.ts" ;
2323import { writePromotionPreSnapshot } from "./promotionSnapshot.ts" ;
24- import {
25- resolveWorktreeInstanceRole ,
26- type WorktreeRegistry ,
27- tryResolveWorktreeRecordFromEnv ,
28- } from "../../../../scripts/worktree-instance.ts" ;
2924
3025export 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
9351066export 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 ,
0 commit comments