Skip to content

Commit f405291

Browse files
authored
feat: Array to Twig migration part 1 (#534)
IMPORTANT: This is part 1 of a 2 part change. After this is merged, we rename the repo and env variables then merge part 2 ### TL;DR Renamed the product from "Array" to "Twig" across the codebase, including configuration files, protocol handlers, and user-facing text. ### What changed? - Renamed all user-facing instances of "Array" to "Twig" in the UI, window titles, and documentation - Updated configuration file paths from `.array/` to `.twig/` and `array.json` to `twig.json` - Added support for both `twig://` and legacy `array://` protocol handlers for deep linking - Implemented migration logic to automatically move existing `.array` directories to `.twig` - Maintained backward compatibility with legacy paths and configuration files - Updated environment variables (keeping `ARRAY_` prefix for now with a note about future migration to `TWIG_`) - Preserved app data paths for compatibility with existing installations ### How to test? 1. Verify the app name appears as "Twig" in window titles and UI elements 2. Confirm both `twig://` and `array://` deep links work correctly 3. Test that existing workspaces in `.array` are properly migrated to `.twig` 4. Verify that both `twig.json` and legacy `array.json` configuration files are recognized 5. Check that environment variables are correctly set in workspace terminals ### Why make this change? This rename from "Array" to "Twig" represents a product rebranding decision. The implementation maintains backward compatibility with existing installations while establishing the new brand identity across the codebase. The migration logic ensures a smooth transition for existing users by automatically moving directories and supporting legacy configuration paths.
1 parent bfb9fd1 commit f405291

17 files changed

Lines changed: 185 additions & 82 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
# Array Development Guide
1+
# Twig Development Guide
22

33
## Project Structure
44

55
- Monorepo with pnpm workspaces and turbo
6-
- `apps/array` - Electron desktop app (React + Vite)
6+
- `apps/array` - Twig Electron desktop app (React + Vite)
77
- `packages/agent` - TypeScript agent framework wrapping Claude Agent SDK
88

99
## Commands
1010

1111
- `pnpm install` - Install all dependencies
12-
- `pnpm dev` - Run both agent (watch) and array app via mprocs
12+
- `pnpm dev` - Run both agent (watch) and twig app via mprocs
1313
- `pnpm dev:agent` - Run agent package in watch mode only
14-
- `pnpm dev:array` - Run array desktop app only
14+
- `pnpm dev:array` - Run twig desktop app only
1515
- `pnpm build` - Build all packages (turbo)
1616
- `pnpm typecheck` - Type check all packages
1717
- `pnpm lint` - Lint and auto-fix with biome
1818
- `pnpm format` - Format with biome
1919
- `pnpm test` - Run tests across all packages
2020

21-
### Array App Specific
21+
### Twig App Specific
2222

2323
- `pnpm --filter array test` - Run vitest tests
24-
- `pnpm --filter array typecheck` - Type check array app
24+
- `pnpm --filter array typecheck` - Type check twig app
2525
- `pnpm --filter array package` - Package electron app
2626
- `pnpm --filter array make` - Make distributable
2727

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
> [!IMPORTANT]
2-
> Array is pre-alpha and not production-ready. Interested? Email jonathan@posthog.com
2+
> Twig is pre-alpha and not production-ready. Interested? Email jonathan@posthog.com
33
44

5-
# PostHog Array Monorepo
5+
# PostHog Twig Monorepo
66

7-
This is the monorepo for PostHog's Array apps and the agent framework that powers them.
7+
This is the monorepo for PostHog's Twig apps and the agent framework that powers them.
88

99
## Projects
1010

11-
- **[apps/array](./apps/array)** - Array desktop application (Electron)
11+
- **[apps/array](./apps/array)** - Twig desktop application (Electron)
1212
- **[apps/mobile](./apps/mobile)** - PostHog mobile app (React Native / Expo)
1313
- **[packages/agent](./packages/agent)** - The TypeScript agent framework
1414

@@ -90,16 +90,19 @@ array-monorepo/
9090
└── package.json # Root package.json
9191
```
9292

93-
## Workspace Configuration (array.json)
93+
## Workspace Configuration (twig.json)
9494

95-
Array supports per-repository configuration through an `array.json` file. This lets you define scripts that run automatically when workspaces are created or destroyed.
95+
Twig supports per-repository configuration through a `twig.json` file (or legacy `array.json`). This lets you define scripts that run automatically when workspaces are created or destroyed.
9696

9797
### File Locations
9898

99-
Array searches for configuration in this order:
99+
Twig searches for configuration in this order (first match wins):
100100

101-
1. `.array/{workspace-name}/array.json` - Workspace-specific config
102-
2. `array.json` - Repository root config
101+
1. `.twig/{workspace-name}/twig.json` - Workspace-specific config (new)
102+
2. `.twig/{workspace-name}/array.json` - Workspace-specific config (legacy)
103+
3. `.array/{workspace-name}/array.json` - Workspace-specific config (legacy location)
104+
4. `twig.json` - Repository root config (new)
105+
5. `array.json` - Repository root config (legacy)
103106

104107
### Schema
105108

@@ -153,7 +156,7 @@ Clean up Docker containers:
153156

154157
## Workspace Environment Variables
155158

156-
Array automatically sets environment variables in all workspace terminals and scripts. These are available in `init`, `start`, and `destroy` scripts, as well as any terminal sessions opened within a workspace.
159+
Twig automatically sets environment variables in all workspace terminals and scripts. These are available in `init`, `start`, and `destroy` scripts, as well as any terminal sessions opened within a workspace.
157160

158161
| Variable | Description | Example |
159162
|----------|-------------|---------|

apps/array/forge.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ const config: ForgeConfig = {
131131
"{**/*.node,**/spawn-helper,**/.vite/build/claude-cli/**,**/node_modules/node-pty/**,**/node_modules/@parcel/**,**/node_modules/file-icon/**}",
132132
},
133133
prune: false,
134-
name: "Array",
135-
executableName: "Array",
134+
name: "Twig",
135+
executableName: "Twig",
136136
icon: "./build/app-icon", // Forge adds .icns/.ico/.png based on platform
137137
appBundleId: "com.posthog.array",
138138
appCategoryType: "public.app-category.productivity",

apps/array/src/main/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const isDev = !app.isPackaged;
1313

1414
// Set different app names for separate single-instance locks
1515
const appName = isDev ? "array-dev" : "Array";
16-
app.setName(isDev ? "Array (Development)" : "Array");
16+
app.setName(isDev ? "Twig (Development)" : "Twig");
1717

1818
const userDataPath = path.join(app.getPath("appData"), "@posthog", appName);
1919
app.setPath("userData", userDataPath);

apps/array/src/main/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ app.on("open-url", (event, url) => {
9595

9696
// Handle deep link URLs on Windows/Linux (second instance sends URL via command line)
9797
app.on("second-instance", (_event, commandLine) => {
98-
const url = commandLine.find((arg) => arg.startsWith("array://"));
98+
const url = commandLine.find(
99+
(arg) => arg.startsWith("twig://") || arg.startsWith("array://"),
100+
);
99101
if (url) {
100102
const deepLinkService = container.get<DeepLinkService>(
101103
MAIN_TOKENS.DeepLinkService,
@@ -166,10 +168,10 @@ function createWindow(): void {
166168
// Set up menu for keyboard shortcuts
167169
const template: MenuItemConstructorOptions[] = [
168170
{
169-
label: "Array",
171+
label: "Twig",
170172
submenu: [
171173
{
172-
label: "About Array",
174+
label: "About Twig",
173175
click: () => {
174176
const commit = __BUILD_COMMIT__ ?? "dev";
175177
const buildDate = __BUILD_DATE__ ?? "dev";
@@ -187,8 +189,8 @@ function createWindow(): void {
187189
dialog
188190
.showMessageBox({
189191
type: "info",
190-
title: "About Array",
191-
message: "Array",
192+
title: "About Twig",
193+
message: "Twig",
192194
detail: info,
193195
buttons: ["Copy", "OK"],
194196
defaultId: 1,
@@ -348,7 +350,9 @@ app.whenReady().then(() => {
348350
}
349351
} else {
350352
// On Windows/Linux, the URL comes via command line arguments
351-
const deepLinkUrl = process.argv.find((arg) => arg.startsWith("array://"));
353+
const deepLinkUrl = process.argv.find(
354+
(arg) => arg.startsWith("twig://") || arg.startsWith("array://"),
355+
);
352356
if (deepLinkUrl) {
353357
deepLinkService.handleUrl(deepLinkUrl);
354358
}

apps/array/src/main/lib/shellManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function buildShellEnv(
3636
env.LC_MONETARY = locale;
3737
}
3838

39-
env.TERM_PROGRAM = "Array";
39+
env.TERM_PROGRAM = "Twig";
4040
env.COLORTERM = "truecolor";
4141
env.FORCE_COLOR = "3";
4242

apps/array/src/main/services/deep-link/service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { logger } from "../../lib/logger.js";
44

55
const log = logger.scope("deep-link-service");
66

7-
const PROTOCOL = "array";
7+
const PROTOCOL = "twig";
8+
const LEGACY_PROTOCOL = "array";
89

910
export type DeepLinkHandler = (
1011
path: string,
@@ -30,11 +31,14 @@ export class DeepLinkService {
3031
return;
3132
}
3233

33-
// Production: register the protocol
34+
// Production: register both new and legacy protocols
3435
app.setAsDefaultProtocolClient(PROTOCOL);
36+
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL);
3537

3638
this.protocolRegistered = true;
37-
log.info(`Registered '${PROTOCOL}' protocol handler`);
39+
log.info(
40+
`Registered '${PROTOCOL}' and '${LEGACY_PROTOCOL}' protocol handlers`,
41+
);
3842
}
3943

4044
public registerHandler(key: string, handler: DeepLinkHandler): void {
@@ -53,11 +57,15 @@ export class DeepLinkService {
5357
* Handle an incoming deep link URL
5458
*
5559
* NOTE: Strips the protocol and main key, passing only dynamic segments to handlers.
60+
* Supports both twig:// and legacy array:// protocols.
5661
*/
5762
public handleUrl(url: string): boolean {
5863
log.info("Received deep link:", url);
5964

60-
if (!url.startsWith(`${PROTOCOL}://`)) {
65+
const isTwigProtocol = url.startsWith(`${PROTOCOL}://`);
66+
const isLegacyProtocol = url.startsWith(`${LEGACY_PROTOCOL}://`);
67+
68+
if (!isTwigProtocol && !isLegacyProtocol) {
6169
log.warn("URL does not match protocol:", url);
6270
return false;
6371
}

apps/array/src/main/services/settingsStore.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { existsSync, renameSync } from "node:fs";
12
import * as os from "node:os";
23
import * as path from "node:path";
34
import { app } from "electron";
@@ -7,10 +8,38 @@ interface SettingsSchema {
78
worktreeLocation: string;
89
}
910

11+
const LEGACY_DIR_NAME = ".array";
12+
const CURRENT_DIR_NAME = ".twig";
13+
1014
function getDefaultWorktreeLocation(): string {
11-
return path.join(os.homedir(), ".array");
15+
return path.join(os.homedir(), CURRENT_DIR_NAME);
16+
}
17+
18+
function getLegacyWorktreeLocation(): string {
19+
return path.join(os.homedir(), LEGACY_DIR_NAME);
20+
}
21+
22+
/**
23+
* Migrate ~/.array to ~/.twig if needed (one-time migration)
24+
*/
25+
function migrateWorktreeDirectory(): void {
26+
const legacyPath = getLegacyWorktreeLocation();
27+
const newPath = getDefaultWorktreeLocation();
28+
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
36+
}
37+
}
1238
}
1339

40+
// Run migration before store initialization
41+
migrateWorktreeDirectory();
42+
1443
const schema = {
1544
worktreeLocation: {
1645
type: "string" as const,
@@ -27,6 +56,23 @@ export const settingsStore = new Store<SettingsSchema>({
2756
},
2857
});
2958

59+
/**
60+
* Migrate stored worktree setting from ~/.array to ~/.twig if it was the default
61+
*/
62+
function migrateWorktreeSetting(): void {
63+
const stored = settingsStore.get("worktreeLocation");
64+
const legacyDefault = getLegacyWorktreeLocation();
65+
const newDefault = getDefaultWorktreeLocation();
66+
67+
// If user had the legacy default, update to new default
68+
if (stored === legacyDefault && existsSync(newDefault)) {
69+
settingsStore.set("worktreeLocation", newDefault);
70+
}
71+
}
72+
73+
// Run setting migration after store initialization
74+
migrateWorktreeSetting();
75+
3076
export function getWorktreeLocation(): string {
3177
return settingsStore.get("worktreeLocation", getDefaultWorktreeLocation());
3278
}

apps/array/src/main/services/shell/service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function buildShellEnv(
4040
}
4141

4242
Object.assign(env, {
43-
TERM_PROGRAM: "Array",
43+
TERM_PROGRAM: "Twig",
4444
COLORTERM: "truecolor",
4545
FORCE_COLOR: "3",
4646
...additionalEnv,

apps/array/src/main/services/workspace/configLoader.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,43 +20,48 @@ export async function loadConfig(
2020
worktreePath: string,
2121
worktreeName: string,
2222
): Promise<LoadConfigResult> {
23-
// Search order:
24-
// 1. .array/{WORKSPACE_NAME}/array.json (workspace-specific)
25-
// 2. {repo-root}/array.json (repository root)
23+
// Search order (twig.json takes precedence over legacy array.json):
24+
// 1. .twig/{WORKSPACE_NAME}/twig.json (workspace-specific, new)
25+
// 2. .twig/{WORKSPACE_NAME}/array.json (workspace-specific, legacy)
26+
// 3. .array/{WORKSPACE_NAME}/array.json (workspace-specific, legacy location)
27+
// 4. {repo-root}/twig.json (repository root, new)
28+
// 5. {repo-root}/array.json (repository root, legacy)
2629

27-
const workspaceConfigPath = path.join(
28-
worktreePath,
29-
".array",
30-
worktreeName,
31-
"array.json",
32-
);
30+
const workspaceConfigPaths = [
31+
path.join(worktreePath, ".twig", worktreeName, "twig.json"),
32+
path.join(worktreePath, ".twig", worktreeName, "array.json"),
33+
path.join(worktreePath, ".array", worktreeName, "array.json"),
34+
];
3335

34-
const repoConfigPath = path.join(worktreePath, "array.json");
36+
const repoConfigPaths = [
37+
path.join(worktreePath, "twig.json"),
38+
path.join(worktreePath, "array.json"),
39+
];
3540

36-
// Try workspace-specific config first
37-
const workspaceResult = await tryLoadConfig(workspaceConfigPath);
38-
if (workspaceResult.config) {
39-
log.info(`Loaded config from workspace: ${workspaceConfigPath}`);
40-
return { config: workspaceResult.config, source: "workspace" };
41-
}
42-
if (workspaceResult.errors) {
43-
log.warn(
44-
`Invalid config at ${workspaceConfigPath}: ${workspaceResult.errors.join(", ")}`,
45-
);
46-
return { config: null, source: null };
41+
// Try workspace-specific configs first
42+
for (const configPath of workspaceConfigPaths) {
43+
const result = await tryLoadConfig(configPath);
44+
if (result.config) {
45+
log.info(`Loaded config from workspace: ${configPath}`);
46+
return { config: result.config, source: "workspace" };
47+
}
48+
if (result.errors) {
49+
log.warn(`Invalid config at ${configPath}: ${result.errors.join(", ")}`);
50+
return { config: null, source: null };
51+
}
4752
}
4853

49-
// Try repo root config
50-
const repoResult = await tryLoadConfig(repoConfigPath);
51-
if (repoResult.config) {
52-
log.info(`Loaded config from repo root: ${repoConfigPath}`);
53-
return { config: repoResult.config, source: "repo" };
54-
}
55-
if (repoResult.errors) {
56-
log.warn(
57-
`Invalid config at ${repoConfigPath}: ${repoResult.errors.join(", ")}`,
58-
);
59-
return { config: null, source: null };
54+
// Try repo root configs
55+
for (const configPath of repoConfigPaths) {
56+
const result = await tryLoadConfig(configPath);
57+
if (result.config) {
58+
log.info(`Loaded config from repo root: ${configPath}`);
59+
return { config: result.config, source: "repo" };
60+
}
61+
if (result.errors) {
62+
log.warn(`Invalid config at ${configPath}: ${result.errors.join(", ")}`);
63+
return { config: null, source: null };
64+
}
6065
}
6166

6267
return { config: null, source: null };

0 commit comments

Comments
 (0)