Skip to content

Commit cc50c38

Browse files
committed
feat: array.json and scripts qol
1 parent e133653 commit cc50c38

File tree

26 files changed

+474
-235
lines changed

26 files changed

+474
-235
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ AGENTS.md
3636
.envrc
3737

3838
# Session store used for example-client.ts
39-
.session-store.json
39+
.session-store.json
40+
41+
.turbo
42+
.agent-trigger

apps/array/src/main/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
// Legacy type kept for backwards compatibility with taskControllers map
2121
type TaskController = unknown;
2222

23+
import { shellManager } from "./lib/shellManager.js";
2324
import { setupAgentHotReload } from "./services/dev-reload.js";
2425
import { registerFileWatcherIpc } from "./services/fileWatcher.js";
2526
import { registerFoldersIpc } from "./services/folders.js";
@@ -57,8 +58,15 @@ const taskControllers = new Map<string, TaskController>();
5758
// instead of ::1. This matches how the renderer already reaches the PostHog API.
5859
dns.setDefaultResultOrder("ipv4first");
5960

60-
// Set app name to ensure consistent userData path across platforms
61-
app.setName("Array");
61+
// Set app name based on workspace (for unique userData paths per workspace)
62+
const workspaceName = process.env.ARRAY_WORKSPACE_NAME;
63+
const appName = workspaceName ? `Array (${workspaceName})` : "Array";
64+
app.setName(appName);
65+
66+
// Use workspace-specific data directory if provided
67+
if (process.env.ARRAY_WORKSPACE_DATA_DIR) {
68+
app.setPath("userData", process.env.ARRAY_WORKSPACE_DATA_DIR);
69+
}
6270

6371
function ensureClaudeConfigDir(): void {
6472
const existing = process.env.CLAUDE_CONFIG_DIR;
@@ -87,13 +95,16 @@ function setupExternalLinkHandlers(window: BrowserWindow): void {
8795
}
8896

8997
function createWindow(): void {
98+
const windowTitle = workspaceName ? `Array (${workspaceName})` : "Array";
99+
90100
mainWindow = new BrowserWindow({
91101
width: 900,
92102
height: 600,
93103
minWidth: 900,
94104
minHeight: 600,
95105
backgroundColor: "#0a0a0a",
96106
titleBarStyle: "hiddenInset",
107+
title: windowTitle,
97108
show: false,
98109
webPreferences: {
99110
nodeIntegration: true,
@@ -111,6 +122,13 @@ function createWindow(): void {
111122

112123
setupExternalLinkHandlers(mainWindow);
113124

125+
// Kill all shell sessions when renderer reloads (dev hot reload or CMD R)
126+
mainWindow.webContents.on("did-start-loading", () => {
127+
if (mainWindow?.webContents) {
128+
shellManager.destroyByWebContents(mainWindow.webContents);
129+
}
130+
});
131+
114132
// Set up menu for keyboard shortcuts
115133
const template: MenuItemConstructorOptions[] = [
116134
{
@@ -249,6 +267,10 @@ app.on("activate", () => {
249267
registerAutoUpdater(() => mainWindow);
250268

251269
ipcMain.handle("app:get-version", () => app.getVersion());
270+
ipcMain.handle(
271+
"app:get-workspace-name",
272+
() => process.env.ARRAY_WORKSPACE_NAME || null,
273+
);
252274

253275
// Register IPC handlers via services
254276
registerPosthogIpc();

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ class ShellManagerImpl {
8383
);
8484

8585
const env = buildShellEnv(additionalEnv);
86-
const ptyProcess = pty.spawn(shell, ["-l"], {
86+
87+
// If there's an initial command, spawn shell with -c to run it directly (no echo)
88+
// Otherwise spawn an interactive login shell
89+
const shellArgs = initialCommand ? ["-c", initialCommand] : ["-l"];
90+
91+
const ptyProcess = pty.spawn(shell, shellArgs, {
8792
name: "xterm-256color",
8893
cols: 80,
8994
rows: 24,
@@ -108,12 +113,6 @@ class ShellManagerImpl {
108113
resolveExit({ exitCode });
109114
});
110115

111-
if (initialCommand) {
112-
setTimeout(() => {
113-
ptyProcess.write(`${initialCommand}\n`);
114-
}, 100);
115-
}
116-
117116
const session: ShellSession = {
118117
pty: ptyProcess,
119118
webContents,
@@ -136,15 +135,15 @@ class ShellManagerImpl {
136135
write(sessionId: string, data: string): void {
137136
const session = this.sessions.get(sessionId);
138137
if (!session) {
139-
throw new Error(`Shell session ${sessionId} not found`);
138+
return;
140139
}
141140
session.pty.write(data);
142141
}
143142

144143
resize(sessionId: string, cols: number, rows: number): void {
145144
const session = this.sessions.get(sessionId);
146145
if (!session) {
147-
throw new Error(`Shell session ${sessionId} not found`);
146+
return;
148147
}
149148
session.pty.resize(cols, rows);
150149
}
@@ -180,6 +179,21 @@ class ShellManagerImpl {
180179
}
181180
}
182181
}
182+
183+
destroyByWebContents(webContents: WebContents): void {
184+
for (const [sessionId, session] of this.sessions.entries()) {
185+
if (session.webContents === webContents) {
186+
log.info(`Destroying shell session ${sessionId} (webContents reload)`);
187+
this.destroy(sessionId);
188+
}
189+
}
190+
}
191+
192+
destroyAll(): void {
193+
for (const sessionId of this.sessions.keys()) {
194+
this.destroy(sessionId);
195+
}
196+
}
183197
}
184198

185199
export const shellManager = new ShellManagerImpl();

apps/array/src/main/preload.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
313313
onClearStorage: (listener: () => void): (() => void) =>
314314
createVoidIpcListener("clear-storage", listener),
315315
getAppVersion: (): Promise<string> => ipcRenderer.invoke("app:get-version"),
316+
getWorkspaceName: (): Promise<string | null> =>
317+
ipcRenderer.invoke("app:get-workspace-name"),
316318
onUpdateReady: (listener: () => void): (() => void) =>
317319
createVoidIpcListener("updates:ready", listener),
318320
installUpdate: (): Promise<{ installed: boolean }> =>
@@ -465,6 +467,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
465467
ipcRenderer.invoke("workspace:is-running", taskId),
466468
getTerminals: (taskId: string): Promise<WorkspaceTerminalInfo[]> =>
467469
ipcRenderer.invoke("workspace:get-terminals", taskId),
470+
stop: (taskId: string): Promise<void> =>
471+
ipcRenderer.invoke("workspace:stop", taskId),
472+
restart: (taskId: string): Promise<ScriptExecutionResult> =>
473+
ipcRenderer.invoke("workspace:restart", taskId),
468474
onTerminalCreated: (
469475
listener: IpcEventListener<WorkspaceTerminalInfo & { taskId: string }>,
470476
): (() => void) =>

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,18 @@ class FileService {
209209
}
210210

211211
private emit(channel: string, data: unknown): void {
212-
this.mainWindow?.webContents.send(channel, data);
212+
try {
213+
if (
214+
this.mainWindow &&
215+
!this.mainWindow.isDestroyed() &&
216+
this.mainWindow.webContents &&
217+
!this.mainWindow.webContents.isDestroyed()
218+
) {
219+
this.mainWindow.webContents.send(channel, data);
220+
}
221+
} catch {
222+
// Window or webContents was destroyed, ignore
223+
}
213224
}
214225
}
215226

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ export interface LoadConfigResult {
1919
export async function loadConfig(
2020
worktreePath: string,
2121
worktreeName: string,
22+
mainRepoPath?: string,
2223
): Promise<LoadConfigResult> {
2324
// Search order:
24-
// 1. .array/{WORKSPACE_NAME}/array.json (workspace-specific)
25-
// 2. {repo-root}/array.json (repository root)
25+
// 1. .array/{WORKSPACE_NAME}/array.json (workspace-specific in worktree)
26+
// 2. {worktree}/array.json (worktree root)
27+
// 3. {main-repo}/array.json (original repo root, for uncommitted configs)
2628

2729
const workspaceConfigPath = path.join(
2830
worktreePath,
@@ -32,6 +34,9 @@ export async function loadConfig(
3234
);
3335

3436
const repoConfigPath = path.join(worktreePath, "array.json");
37+
const mainRepoConfigPath = mainRepoPath
38+
? path.join(mainRepoPath, "array.json")
39+
: null;
3540

3641
// Try workspace-specific config first
3742
const workspaceResult = await tryLoadConfig(workspaceConfigPath);
@@ -46,7 +51,7 @@ export async function loadConfig(
4651
return { config: null, source: null };
4752
}
4853

49-
// Try repo root config
54+
// Try repo root config (worktree path)
5055
const repoResult = await tryLoadConfig(repoConfigPath);
5156
if (repoResult.config) {
5257
log.info(`Loaded config from repo root: ${repoConfigPath}`);
@@ -59,6 +64,21 @@ export async function loadConfig(
5964
return { config: null, source: null };
6065
}
6166

67+
// Try main repo root config (for uncommitted configs in worktree scenarios)
68+
if (mainRepoConfigPath && mainRepoConfigPath !== repoConfigPath) {
69+
const mainRepoResult = await tryLoadConfig(mainRepoConfigPath);
70+
if (mainRepoResult.config) {
71+
log.info(`Loaded config from main repo: ${mainRepoConfigPath}`);
72+
return { config: mainRepoResult.config, source: "repo" };
73+
}
74+
if (mainRepoResult.errors) {
75+
log.warn(
76+
`Invalid config at ${mainRepoConfigPath}: ${mainRepoResult.errors.join(", ")}`,
77+
);
78+
return { config: null, source: null };
79+
}
80+
}
81+
6282
return { config: null, source: null };
6383
}
6484

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ export function registerWorkspaceIpc(
7575
async () => workspaceService?.getAllWorkspaces() ?? {},
7676
{ rethrow: false, fallback: {} },
7777
);
78+
79+
handle<[string], void>("workspace:stop", (_event, taskId) =>
80+
getService().stopWorkspace(taskId),
81+
);
82+
83+
handle<[string], ScriptExecutionResult>(
84+
"workspace:restart",
85+
async (_event, taskId) => getService().restartWorkspace(taskId),
86+
{
87+
rethrow: false,
88+
fallback: { success: false, terminalSessionIds: [], errors: ["Failed"] },
89+
},
90+
);
7891
}
7992

8093
export { loadConfig, normalizeScripts } from "./configLoader";

0 commit comments

Comments
 (0)