Skip to content

Commit 38f1268

Browse files
committed
fix: restore Docker container name for existing workspaces
The DockerRuntime now accepts containerName in its config for existing workspaces. When createRuntime is called with projectPath and workspaceName options, the container name is derived and passed to DockerRuntime, allowing exec operations to work without calling createWorkspace first. - Added containerName to DockerRuntimeConfig (optional) - Added workspaceName to CreateRuntimeOptions - Updated all createRuntime call sites to pass workspaceName - Added tests for getContainerName and containerName config
1 parent 1eaca8a commit 38f1268

File tree

7 files changed

+86
-29
lines changed

7 files changed

+86
-29
lines changed

src/node/runtime/DockerRuntime.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "bun:test";
2-
import { DockerRuntime } from "./DockerRuntime";
2+
import { DockerRuntime, getContainerName } from "./DockerRuntime";
33

44
/**
55
* DockerRuntime constructor tests (run with bun test)
@@ -29,4 +29,35 @@ describe("DockerRuntime constructor", () => {
2929
const runtime = new DockerRuntime({ image: "ubuntu:22.04" });
3030
expect(runtime.getWorkspacePath("/any/project", "any-branch")).toBe("/src");
3131
});
32+
33+
it("should accept containerName for existing workspaces", () => {
34+
// When recreating runtime for existing workspace, containerName is passed in config
35+
const runtime = new DockerRuntime({
36+
image: "ubuntu:22.04",
37+
containerName: "mux-myproject-my-feature",
38+
});
39+
expect(runtime.getImage()).toBe("ubuntu:22.04");
40+
// Runtime should be ready for exec operations without calling createWorkspace
41+
});
42+
});
43+
44+
describe("getContainerName", () => {
45+
it("should generate container name from project and workspace", () => {
46+
expect(getContainerName("/home/user/myproject", "feature-branch")).toBe(
47+
"mux-myproject-feature-branch"
48+
);
49+
});
50+
51+
it("should sanitize special characters", () => {
52+
expect(getContainerName("/home/user/my@project", "feature/branch")).toBe(
53+
"mux-my-project-feature-branch"
54+
);
55+
});
56+
57+
it("should handle long names", () => {
58+
const longName = "a".repeat(100);
59+
const result = getContainerName("/project", longName);
60+
// Docker has 64 char limit, function uses 63 to be safe
61+
expect(result.length).toBeLessThanOrEqual(63);
62+
});
3263
});

src/node/runtime/DockerRuntime.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ function runDockerCommand(command: string, timeoutMs = 30000): Promise<DockerCom
103103
export interface DockerRuntimeConfig {
104104
/** Docker image to use (e.g., ubuntu:22.04) */
105105
image: string;
106+
/**
107+
* Container name for existing workspaces.
108+
* When creating a new workspace, this is computed during createWorkspace().
109+
* When recreating runtime for an existing workspace, this should be passed
110+
* to allow exec operations without calling createWorkspace again.
111+
*/
112+
containerName?: string;
106113
}
107114

108115
/**
@@ -121,7 +128,7 @@ function sanitizeContainerName(name: string): string {
121128
* Generate container name from project path and workspace name.
122129
* Format: mux-{projectName}-{workspaceName}
123130
*/
124-
function getContainerName(projectPath: string, workspaceName: string): string {
131+
export function getContainerName(projectPath: string, workspaceName: string): string {
125132
const projectName = getProjectName(projectPath);
126133
return sanitizeContainerName(`mux-${projectName}-${workspaceName}`);
127134
}
@@ -131,9 +138,15 @@ function getContainerName(projectPath: string, workspaceName: string): string {
131138
*/
132139
export class DockerRuntime implements Runtime {
133140
private readonly config: DockerRuntimeConfig;
141+
/** Container name - set during construction (for existing) or createWorkspace (for new) */
142+
private containerName?: string;
134143

135144
constructor(config: DockerRuntimeConfig) {
136145
this.config = config;
146+
// If container name is provided (existing workspace), store it
147+
if (config.containerName) {
148+
this.containerName = config.containerName;
149+
}
137150
}
138151

139152
/**
@@ -154,16 +167,17 @@ export class DockerRuntime implements Runtime {
154167
throw new RuntimeError("Operation aborted before execution", "exec");
155168
}
156169

157-
// Extract container name from cwd (assumes cwd is /src for workspace operations)
158-
// For Docker, we need the container name which we get from the workspace context
159-
// The container name is stored on the runtime instance during workspace creation
160-
const containerName = (this as DockerRuntimeWithContainer).containerName;
161-
if (!containerName) {
170+
// Verify container name is available (set in constructor for existing workspaces,
171+
// or set in createWorkspace for new workspaces)
172+
if (!this.containerName) {
162173
throw new RuntimeError(
163-
"Docker runtime not initialized with container name. Use createWorkspace first.",
174+
"Docker runtime not initialized with container name. " +
175+
"For existing workspaces, pass containerName in config. " +
176+
"For new workspaces, call createWorkspace first.",
164177
"exec"
165178
);
166179
}
180+
const containerName = this.containerName;
167181

168182
// Build command parts
169183
const parts: string[] = [];
@@ -474,7 +488,7 @@ export class DockerRuntime implements Runtime {
474488
}
475489

476490
// Store container name on runtime instance for exec operations
477-
(this as DockerRuntimeWithContainer).containerName = containerName;
491+
this.containerName = containerName;
478492

479493
initLogger.logStep("Container created successfully");
480494

@@ -494,13 +508,13 @@ export class DockerRuntime implements Runtime {
494508
const { projectPath, branchName, trunkBranch, workspacePath, initLogger, abortSignal } = params;
495509

496510
try {
497-
const containerName = (this as DockerRuntimeWithContainer).containerName;
498-
if (!containerName) {
511+
if (!this.containerName) {
499512
return {
500513
success: false,
501514
error: "Container not initialized. Call createWorkspace first.",
502515
};
503516
}
517+
const containerName = this.containerName;
504518

505519
// 1. Sync project to container using git bundle + docker cp
506520
initLogger.logStep("Syncing project files to container...");
@@ -863,14 +877,6 @@ export class DockerRuntime implements Runtime {
863877
return Promise.resolve("/tmp");
864878
}
865879
}
866-
867-
/**
868-
* Extended interface for runtime with container name set
869-
*/
870-
interface DockerRuntimeWithContainer extends DockerRuntime {
871-
containerName?: string;
872-
}
873-
874880
/**
875881
* Helper to convert a ReadableStream to a string
876882
*/

src/node/runtime/runtimeFactory.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Runtime, RuntimeAvailability } from "./Runtime";
44
import { LocalRuntime } from "./LocalRuntime";
55
import { WorktreeRuntime } from "./WorktreeRuntime";
66
import { SSHRuntime } from "./SSHRuntime";
7-
import { DockerRuntime } from "./DockerRuntime";
7+
import { DockerRuntime, getContainerName } from "./DockerRuntime";
88
import type { RuntimeConfig, RuntimeMode } from "@/common/types/runtime";
99
import { hasSrcBaseDir } from "@/common/types/runtime";
1010
import { isIncompatibleRuntimeConfig } from "@/common/utils/runtimeCompatibility";
@@ -30,9 +30,15 @@ export class IncompatibleRuntimeError extends Error {
3030
export interface CreateRuntimeOptions {
3131
/**
3232
* Project path - required for project-dir local runtimes (type: "local" without srcBaseDir).
33+
* For Docker runtimes with existing workspaces, used together with workspaceName to derive container name.
3334
* For other runtime types, this is optional and used only for getWorkspacePath calculations.
3435
*/
3536
projectPath?: string;
37+
/**
38+
* Workspace name - required for Docker runtimes when connecting to an existing workspace.
39+
* Used together with projectPath to derive the container name.
40+
*/
41+
workspaceName?: string;
3642
}
3743

3844
/**
@@ -82,10 +88,17 @@ export function createRuntime(config: RuntimeConfig, options?: CreateRuntimeOpti
8288
port: config.port,
8389
});
8490

85-
case "docker":
91+
case "docker": {
92+
// For existing workspaces, derive container name from project+workspace
93+
const containerName =
94+
options?.projectPath && options?.workspaceName
95+
? getContainerName(options.projectPath, options.workspaceName)
96+
: undefined;
8697
return new DockerRuntime({
8798
image: config.image,
99+
containerName,
88100
});
101+
}
89102

90103
default: {
91104
const unknownConfig = config as { type?: string };

src/node/services/agentSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export class AgentSession {
269269
: (() => {
270270
const runtime = createRuntime(
271271
metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir },
272-
{ projectPath: metadata.projectPath }
272+
{ projectPath: metadata.projectPath, workspaceName: metadata.name }
273273
);
274274
return runtime.getWorkspacePath(metadata.projectPath, metadata.name);
275275
})();

src/node/services/aiService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,7 +975,7 @@ export class AIService extends EventEmitter {
975975
// Get workspace path - handle both worktree and in-place modes
976976
const runtime = createRuntime(
977977
metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir },
978-
{ projectPath: metadata.projectPath }
978+
{ projectPath: metadata.projectPath, workspaceName: metadata.name }
979979
);
980980
// In-place workspaces (CLI/benchmarks) have projectPath === name
981981
// Use path directly instead of reconstructing via getWorkspacePath

src/node/services/terminalService.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ export class TerminalService {
6666
throw new Error(`Workspace not found: ${params.workspaceId}`);
6767
}
6868

69-
// 2. Create runtime
69+
// 2. Create runtime (pass workspace info for Docker container name derivation)
7070
const runtime = createRuntime(
71-
workspaceMetadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir }
71+
workspaceMetadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir },
72+
{ projectPath: workspaceMetadata.projectPath, workspaceName: workspaceMetadata.name }
7273
);
7374

7475
// 3. Compute workspace path

src/node/services/workspaceService.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ export class WorkspaceService extends EventEmitter {
428428

429429
const runtime = createRuntime(
430430
metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir },
431-
{ projectPath }
431+
{ projectPath, workspaceName: metadata.name }
432432
);
433433

434434
// Delete workspace from runtime
@@ -539,7 +539,7 @@ export class WorkspaceService extends EventEmitter {
539539

540540
const runtime = createRuntime(
541541
oldMetadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir },
542-
{ projectPath }
542+
{ projectPath, workspaceName: oldName }
543543
);
544544

545545
const renameResult = await runtime.renameWorkspace(projectPath, oldName, newName);
@@ -656,7 +656,10 @@ export class WorkspaceService extends EventEmitter {
656656
type: "local",
657657
srcBaseDir: this.config.srcDir,
658658
};
659-
const runtime = createRuntime(sourceRuntimeConfig);
659+
const runtime = createRuntime(sourceRuntimeConfig, {
660+
projectPath: foundProjectPath,
661+
workspaceName: sourceMetadata.name,
662+
});
660663

661664
const newWorkspaceId = this.config.generateStableId();
662665

@@ -1039,7 +1042,10 @@ export class WorkspaceService extends EventEmitter {
10391042
type: "local" as const,
10401043
srcBaseDir: this.config.srcDir,
10411044
};
1042-
const runtime = createRuntime(runtimeConfig, { projectPath: metadata.projectPath });
1045+
const runtime = createRuntime(runtimeConfig, {
1046+
projectPath: metadata.projectPath,
1047+
workspaceName: metadata.name,
1048+
});
10431049
const workspacePath = runtime.getWorkspacePath(metadata.projectPath, metadata.name);
10441050

10451051
// Create bash tool

0 commit comments

Comments
 (0)