Skip to content

Commit 05bb2c1

Browse files
committed
Centralized branch/directory constants and migration fix
1 parent 845a0a7 commit 05bb2c1

10 files changed

Lines changed: 148 additions & 48 deletions

File tree

apps/twig/src/main/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from "./services/posthog-analytics.js";
4444
import type { TaskLinkService } from "./services/task-link/service";
4545
import type { UpdatesService } from "./services/updates/service.js";
46+
import { migrateStoredWorktreePaths } from "./utils/store.js";
4647

4748
const __filename = fileURLToPath(import.meta.url);
4849
const __dirname = path.dirname(__filename);
@@ -320,6 +321,9 @@ function createWindow(): void {
320321
}
321322

322323
app.whenReady().then(() => {
324+
// Migrate stored worktree paths from legacy directories (e.g., ~/.array -> ~/.twig)
325+
migrateStoredWorktreePaths();
326+
323327
createWindow();
324328
ensureClaudeConfigDir();
325329

apps/twig/src/main/services/git/service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { exec, execFile, spawn } from "node:child_process";
22
import fs from "node:fs";
33
import path from "node:path";
44
import { promisify } from "node:util";
5+
import { isTwigBranch } from "@shared/constants";
56
import { injectable } from "inversify";
67
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
78
import type {
@@ -190,7 +191,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
190191
.split("\n")
191192
.filter(Boolean)
192193
.map((branch) => branch.trim())
193-
.filter((branch) => !branch.startsWith("array/"));
194+
.filter((branch) => !isTwigBranch(branch));
194195
} catch {
195196
return [];
196197
}
Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,43 @@
11
import { existsSync, renameSync } from "node:fs";
22
import * as os from "node:os";
33
import * as path from "node:path";
4+
import { DATA_DIR, LEGACY_DATA_DIRS } from "@shared/constants";
45
import { app } from "electron";
56
import Store from "electron-store";
67

78
interface SettingsSchema {
89
worktreeLocation: string;
910
}
1011

11-
const LEGACY_DIR_NAME = ".array";
12-
const CURRENT_DIR_NAME = ".twig";
13-
1412
function getDefaultWorktreeLocation(): string {
15-
return path.join(os.homedir(), CURRENT_DIR_NAME);
13+
return path.join(os.homedir(), DATA_DIR);
1614
}
1715

18-
function getLegacyWorktreeLocation(): string {
19-
return path.join(os.homedir(), LEGACY_DIR_NAME);
16+
function getLegacyWorktreeLocations(): string[] {
17+
return LEGACY_DATA_DIRS.map((dir) => path.join(os.homedir(), dir));
2018
}
2119

2220
/**
23-
* Migrate ~/.array to ~/.twig if needed (one-time migration)
21+
* Migrate legacy directories to current if needed (one-time migration)
2422
*/
2523
function migrateWorktreeDirectory(): void {
26-
const legacyPath = getLegacyWorktreeLocation();
2724
const newPath = getDefaultWorktreeLocation();
2825

29-
// Only migrate if legacy exists and new doesn't
30-
if (existsSync(legacyPath) && !existsSync(newPath)) {
31-
try {
32-
renameSync(legacyPath, newPath);
33-
} catch {
34-
// If rename fails (e.g., cross-device), leave as-is
35-
// User can manually migrate or continue using legacy location
26+
// Only migrate if new path doesn't exist yet
27+
if (existsSync(newPath)) {
28+
return;
29+
}
30+
31+
// Try to migrate from each legacy location (first one found wins)
32+
for (const legacyPath of getLegacyWorktreeLocations()) {
33+
if (existsSync(legacyPath)) {
34+
try {
35+
renameSync(legacyPath, newPath);
36+
return;
37+
} catch {
38+
// If rename fails (e.g., cross-device), leave as-is
39+
// User can manually migrate or continue using legacy location
40+
}
3641
}
3742
}
3843
}
@@ -57,16 +62,18 @@ export const settingsStore = new Store<SettingsSchema>({
5762
});
5863

5964
/**
60-
* Migrate stored worktree setting from ~/.array to ~/.twig if it was the default
65+
* Migrate stored worktree setting from legacy to current if it was a legacy default
6166
*/
6267
function migrateWorktreeSetting(): void {
6368
const stored = settingsStore.get("worktreeLocation");
64-
const legacyDefault = getLegacyWorktreeLocation();
6569
const newDefault = getDefaultWorktreeLocation();
6670

67-
// If user had the legacy default, update to new default
68-
if (stored === legacyDefault && existsSync(newDefault)) {
69-
settingsStore.set("worktreeLocation", newDefault);
71+
// If user had a legacy default, update to new default
72+
for (const legacyPath of getLegacyWorktreeLocations()) {
73+
if (stored === legacyPath && existsSync(newDefault)) {
74+
settingsStore.set("worktreeLocation", newDefault);
75+
return;
76+
}
7077
}
7178
}
7279

@@ -77,6 +84,24 @@ export function getWorktreeLocation(): string {
7784
return settingsStore.get("worktreeLocation", getDefaultWorktreeLocation());
7885
}
7986

87+
/**
88+
* Get all worktree locations to check (current + legacy).
89+
* Use this when searching for existing worktrees for backwards compatibility.
90+
*/
91+
export function getAllWorktreeLocations(): string[] {
92+
const primary = getWorktreeLocation();
93+
const locations = [primary];
94+
95+
// Add legacy locations if they exist and aren't the primary
96+
for (const legacyPath of getLegacyWorktreeLocations()) {
97+
if (legacyPath !== primary && existsSync(legacyPath)) {
98+
locations.push(legacyPath);
99+
}
100+
}
101+
102+
return locations;
103+
}
104+
80105
export function setWorktreeLocation(location: string): void {
81106
settingsStore.set("worktreeLocation", location);
82107
}

apps/twig/src/main/utils/store.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import * as os from "node:os";
12
import { WorktreeManager } from "@posthog/agent";
3+
import { LEGACY_DATA_DIRS } from "@shared/constants";
24
import { app } from "electron";
35
import Store from "electron-store";
46
import type {
@@ -75,6 +77,49 @@ export const foldersStore = new Store<FoldersSchema>({
7577

7678
const log = logger.scope("store");
7779

80+
/**
81+
* Migrate stored worktree paths from legacy directories to current.
82+
* This updates taskAssociations that have paths like ~/.array/... to ~/.twig/...
83+
*/
84+
export function migrateStoredWorktreePaths(): void {
85+
const currentLocation = getWorktreeLocation();
86+
const associations = foldersStore.get("taskAssociations", []);
87+
let migrated = false;
88+
89+
const legacyPaths = LEGACY_DATA_DIRS.map((dir) => `${os.homedir()}/${dir}/`);
90+
const currentPath = `${currentLocation}/`;
91+
92+
const updatedAssociations = associations.map((assoc) => {
93+
if (!assoc.worktree?.worktreePath) return assoc;
94+
95+
for (const legacyPath of legacyPaths) {
96+
if (assoc.worktree.worktreePath.startsWith(legacyPath)) {
97+
const newWorktreePath = assoc.worktree.worktreePath.replace(
98+
legacyPath,
99+
currentPath,
100+
);
101+
log.info(
102+
`Migrating worktree path: ${assoc.worktree.worktreePath} -> ${newWorktreePath}`,
103+
);
104+
migrated = true;
105+
return {
106+
...assoc,
107+
worktree: {
108+
...assoc.worktree,
109+
worktreePath: newWorktreePath,
110+
},
111+
};
112+
}
113+
}
114+
return assoc;
115+
});
116+
117+
if (migrated) {
118+
foldersStore.set("taskAssociations", updatedAssociations);
119+
log.info("Worktree path migration complete");
120+
}
121+
}
122+
78123
export async function clearAllStoreData(): Promise<void> {
79124
const associations = foldersStore.get("taskAssociations", []);
80125
for (const assoc of associations) {

apps/twig/src/renderer/components/StatusBarMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export function StatusBarMenu() {
44
return (
55
<Button size="1" variant="ghost">
66
<Code size="1" color="gray" variant="ghost">
7-
ARRAY
7+
TWIG
88
</Code>
99
</Button>
1010
);

apps/twig/src/renderer/features/sidebar/components/items/TaskItem.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { Tooltip } from "@radix-ui/themes";
1111
import { trpcVanilla } from "@renderer/trpc";
1212
import { formatRelativeTime } from "@renderer/utils/time";
13+
import { isTwigBranch } from "@shared/constants";
1314
import type { WorkspaceMode } from "@shared/types";
1415
import { selectFocusedBranch, useFocusStore } from "@stores/focusStore";
1516
import { useQuery } from "@tanstack/react-query";
@@ -156,15 +157,12 @@ export function TaskItem({
156157
const focusedBranch = useFocusStore(selectFocusedBranch(mainRepoPath ?? ""));
157158

158159
const isCloudTask = workspaceMode === "cloud";
159-
const isTwigBranch =
160-
branchName?.startsWith("twig/") ||
161-
branchName?.startsWith("array/") ||
162-
branchName?.startsWith("posthog/");
160+
const hasTwigBranch = branchName ? isTwigBranch(branchName) : false;
163161
// Only show "Watching" indicator for twig-created branches, not borrowed ones
164162
const isWatching = !!(
165163
branchName &&
166164
focusedBranch === branchName &&
167-
isTwigBranch
165+
hasTwigBranch
168166
);
169167

170168
const activityText = isGenerating

apps/twig/src/renderer/features/workspace/components/FocusWorkspaceButton.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ArrowLeft, ArrowsClockwise, GitBranch } from "@phosphor-icons/react";
22
import { Button, Spinner, Text, Tooltip } from "@radix-ui/themes";
3+
import { isTwigBranch } from "@shared/constants";
34
import {
45
selectIsFocusedOnWorktree,
56
selectIsLoading,
@@ -10,15 +11,6 @@ import { toast } from "@utils/toast";
1011
import { useCallback } from "react";
1112
import { selectWorkspace, useWorkspaceStore } from "../stores/workspaceStore";
1213

13-
/** Check if branch is a twig-created branch (not borrowed) */
14-
function isTwigBranch(branchName: string): boolean {
15-
return (
16-
branchName.startsWith("twig/") ||
17-
branchName.startsWith("array/") ||
18-
branchName.startsWith("posthog/")
19-
);
20-
}
21-
2214
interface FocusWorkspaceButtonProps {
2315
taskId: string;
2416
repoPath?: string;

apps/twig/src/shared/constants.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Branch naming conventions.
3+
* - Reading: Accept all prefixes for backwards compatibility
4+
* - Writing: Always use BRANCH_PREFIX (twig/)
5+
*/
6+
export const BRANCH_PREFIX = "twig/";
7+
export const LEGACY_BRANCH_PREFIXES = ["array/", "posthog/"];
8+
9+
export function isTwigBranch(branchName: string): boolean {
10+
return (
11+
branchName.startsWith(BRANCH_PREFIX) ||
12+
LEGACY_BRANCH_PREFIXES.some((p) => branchName.startsWith(p))
13+
);
14+
}
15+
16+
/**
17+
* Data directory conventions.
18+
* - Reading: Accept all directories for backwards compatibility
19+
* - Writing: Always use DATA_DIR (.twig)
20+
*/
21+
export const DATA_DIR = ".twig";
22+
export const LEGACY_DATA_DIRS = [".array"];

packages/agent/src/constants.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Branch naming conventions.
3+
* - Reading: Accept all prefixes for backwards compatibility
4+
* - Writing: Always use BRANCH_PREFIX (twig/)
5+
*/
6+
export const BRANCH_PREFIX = "twig/";
7+
export const LEGACY_BRANCH_PREFIXES = ["array/", "posthog/"];
8+
9+
export function isTwigBranch(branchName: string): boolean {
10+
return (
11+
branchName.startsWith(BRANCH_PREFIX) ||
12+
LEGACY_BRANCH_PREFIXES.some((p) => branchName.startsWith(p))
13+
);
14+
}
15+
16+
export function makeBranchName(name: string): string {
17+
return `${BRANCH_PREFIX}${name}`;
18+
}

packages/agent/src/worktree-manager.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as crypto from "node:crypto";
33
import * as fs from "node:fs/promises";
44
import * as path from "node:path";
55
import { promisify } from "node:util";
6+
import { isTwigBranch, makeBranchName } from "./constants.js";
67
import type { WorktreeInfo } from "./types.js";
78
import { Logger } from "./utils/logger.js";
89

@@ -783,7 +784,7 @@ export class WorktreeManager {
783784
const worktreeName = await worktreeNamePromise;
784785
const baseBranch = await baseBranchPromise;
785786
const worktreePath = this.getWorktreePath(worktreeName);
786-
const branchName = `twig/${worktreeName}`;
787+
const branchName = makeBranchName(worktreeName);
787788

788789
this.logger.info("Creating worktree", {
789790
worktreeName,
@@ -1073,25 +1074,19 @@ export class WorktreeManager {
10731074
worktreePath &&
10741075
path.resolve(worktreePath) === path.resolve(this.mainRepoPath);
10751076
const isInWorktreeFolder = worktreePath?.startsWith(worktreeFolderPath);
1076-
const isTwigBranch =
1077-
branchName?.startsWith("twig/") ||
1078-
branchName?.startsWith("array/") ||
1079-
branchName?.startsWith("posthog/");
1077+
const hasTwigBranch = branchName ? isTwigBranch(branchName) : false;
10801078

10811079
if (
10821080
worktreePath &&
10831081
branchName &&
10841082
!isMainRepo &&
1085-
(isInWorktreeFolder || isTwigBranch)
1083+
(isInWorktreeFolder || hasTwigBranch)
10861084
) {
10871085
const worktreeName = path.basename(worktreePath);
10881086
// Infer ownership: twig/ prefixed branches are "created", others are "borrowed"
1089-
const branchOwnership =
1090-
branchName.startsWith("twig/") ||
1091-
branchName.startsWith("array/") ||
1092-
branchName.startsWith("posthog/")
1093-
? "created"
1094-
: "borrowed";
1087+
const branchOwnership = isTwigBranch(branchName)
1088+
? "created"
1089+
: "borrowed";
10951090
worktrees.push({
10961091
worktreePath,
10971092
worktreeName,

0 commit comments

Comments
 (0)