@hellcoder/companion 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ─── Mock sandbox-manager ─────────────────────────────────────────────────
|
|
4
|
+
vi.mock("../sandbox-manager.js", () => ({
|
|
5
|
+
listSandboxes: vi.fn(() => []),
|
|
6
|
+
getSandbox: vi.fn(() => null),
|
|
7
|
+
createSandbox: vi.fn(),
|
|
8
|
+
updateSandbox: vi.fn(),
|
|
9
|
+
deleteSandbox: vi.fn(() => false),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// ─── Mock container-manager ───────────────────────────────────────────────
|
|
13
|
+
vi.mock("../container-manager.js", () => ({
|
|
14
|
+
containerManager: {
|
|
15
|
+
checkDocker: vi.fn(() => true),
|
|
16
|
+
createContainer: vi.fn(() => ({ containerId: "test-container-123", name: "companion-test" })),
|
|
17
|
+
copyWorkspaceToContainer: vi.fn(async () => {}),
|
|
18
|
+
execInContainerAsync: vi.fn(async () => ({ exitCode: 0, output: "ok\n" })),
|
|
19
|
+
removeContainer: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// ─── Mock image-pull-manager ──────────────────────────────────────────────
|
|
24
|
+
vi.mock("../image-pull-manager.js", () => ({
|
|
25
|
+
imagePullManager: {
|
|
26
|
+
isReady: vi.fn(() => true),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import { Hono } from "hono";
|
|
31
|
+
import * as sandboxManager from "../sandbox-manager.js";
|
|
32
|
+
import { containerManager } from "../container-manager.js";
|
|
33
|
+
import { imagePullManager } from "../image-pull-manager.js";
|
|
34
|
+
import { registerSandboxRoutes } from "./sandbox-routes.js";
|
|
35
|
+
|
|
36
|
+
// ─── Test setup ───────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
let app: Hono;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
|
|
43
|
+
app = new Hono().basePath("/api");
|
|
44
|
+
registerSandboxRoutes(app);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Minimal sandbox fixture matching the CompanionSandbox shape. */
|
|
50
|
+
function makeSandbox(overrides: Record<string, unknown> = {}) {
|
|
51
|
+
return {
|
|
52
|
+
name: "My Sandbox",
|
|
53
|
+
slug: "my-sandbox",
|
|
54
|
+
createdAt: 1000,
|
|
55
|
+
updatedAt: 2000,
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
// GET /api/sandboxes
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
describe("GET /api/sandboxes", () => {
|
|
65
|
+
it("returns an empty list when no sandboxes exist", async () => {
|
|
66
|
+
// Validates that the endpoint returns 200 with an empty array
|
|
67
|
+
// when there are no sandboxes on disk.
|
|
68
|
+
vi.mocked(sandboxManager.listSandboxes).mockReturnValue([]);
|
|
69
|
+
|
|
70
|
+
const res = await app.request("/api/sandboxes");
|
|
71
|
+
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
expect(await res.json()).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns a list of sandboxes", async () => {
|
|
77
|
+
// Validates that multiple sandboxes are returned correctly.
|
|
78
|
+
const sandboxes = [
|
|
79
|
+
makeSandbox(),
|
|
80
|
+
makeSandbox({ slug: "second", name: "Second" }),
|
|
81
|
+
];
|
|
82
|
+
vi.mocked(sandboxManager.listSandboxes).mockReturnValue(sandboxes as any);
|
|
83
|
+
|
|
84
|
+
const res = await app.request("/api/sandboxes");
|
|
85
|
+
|
|
86
|
+
expect(res.status).toBe(200);
|
|
87
|
+
const json = await res.json();
|
|
88
|
+
expect(json).toHaveLength(2);
|
|
89
|
+
expect(json[0].slug).toBe("my-sandbox");
|
|
90
|
+
expect(json[1].slug).toBe("second");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns 500 when listSandboxes throws", async () => {
|
|
94
|
+
// Validates that internal errors in sandbox-manager are surfaced
|
|
95
|
+
// as 500 responses with the error message in the body.
|
|
96
|
+
vi.mocked(sandboxManager.listSandboxes).mockImplementation(() => {
|
|
97
|
+
throw new Error("disk failure");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const res = await app.request("/api/sandboxes");
|
|
101
|
+
|
|
102
|
+
expect(res.status).toBe(500);
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
expect(json.error).toBe("disk failure");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// GET /api/sandboxes/:slug
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
describe("GET /api/sandboxes/:slug", () => {
|
|
113
|
+
it("returns the sandbox when it exists", async () => {
|
|
114
|
+
// Validates that a single sandbox is returned by slug and that
|
|
115
|
+
// getSandbox is called with the correct slug parameter.
|
|
116
|
+
const sandbox = makeSandbox();
|
|
117
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
118
|
+
|
|
119
|
+
const res = await app.request("/api/sandboxes/my-sandbox");
|
|
120
|
+
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
expect(await res.json()).toEqual(sandbox);
|
|
123
|
+
expect(sandboxManager.getSandbox).toHaveBeenCalledWith("my-sandbox");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns 404 when the sandbox does not exist", async () => {
|
|
127
|
+
// Validates that requesting a non-existent slug returns 404
|
|
128
|
+
// with an appropriate error message.
|
|
129
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(null);
|
|
130
|
+
|
|
131
|
+
const res = await app.request("/api/sandboxes/missing");
|
|
132
|
+
|
|
133
|
+
expect(res.status).toBe(404);
|
|
134
|
+
const json = await res.json();
|
|
135
|
+
expect(json.error).toMatch(/not found/i);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
140
|
+
// POST /api/sandboxes
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
describe("POST /api/sandboxes", () => {
|
|
144
|
+
it("creates a sandbox with name and initScript and returns 201", async () => {
|
|
145
|
+
// Validates that a new sandbox is created with optional initScript
|
|
146
|
+
// and that the response status is 201 (Created).
|
|
147
|
+
const created = makeSandbox({
|
|
148
|
+
initScript: "npm install",
|
|
149
|
+
});
|
|
150
|
+
vi.mocked(sandboxManager.createSandbox).mockReturnValue(created as any);
|
|
151
|
+
|
|
152
|
+
const res = await app.request("/api/sandboxes", {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "Content-Type": "application/json" },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
name: "My Sandbox",
|
|
157
|
+
initScript: "npm install",
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(res.status).toBe(201);
|
|
162
|
+
expect(await res.json()).toEqual(created);
|
|
163
|
+
// Verify createSandbox was called with name and options object
|
|
164
|
+
expect(sandboxManager.createSandbox).toHaveBeenCalledWith("My Sandbox", {
|
|
165
|
+
initScript: "npm install",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("returns 400 when createSandbox throws a validation error", async () => {
|
|
170
|
+
// Validates that errors thrown by createSandbox (e.g. duplicate slug)
|
|
171
|
+
// are surfaced as 400 responses with the error message.
|
|
172
|
+
vi.mocked(sandboxManager.createSandbox).mockImplementation(() => {
|
|
173
|
+
throw new Error('A sandbox with a similar name already exists ("my-sandbox")');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const res = await app.request("/api/sandboxes", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({ name: "My Sandbox" }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(res.status).toBe(400);
|
|
183
|
+
const json = await res.json();
|
|
184
|
+
expect(json.error).toMatch(/already exists/i);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns 400 when name is missing", async () => {
|
|
188
|
+
// Validates that omitting the required "name" field causes a 400 error.
|
|
189
|
+
// The sandbox-manager throws "Sandbox name is required" for empty names.
|
|
190
|
+
vi.mocked(sandboxManager.createSandbox).mockImplementation(() => {
|
|
191
|
+
throw new Error("Sandbox name is required");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const res = await app.request("/api/sandboxes", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({}),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(res.status).toBe(400);
|
|
201
|
+
const json = await res.json();
|
|
202
|
+
expect(json.error).toBe("Sandbox name is required");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// PUT /api/sandboxes/:slug
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
describe("PUT /api/sandboxes/:slug", () => {
|
|
211
|
+
it("updates an existing sandbox", async () => {
|
|
212
|
+
// Validates that an existing sandbox can be updated with new fields
|
|
213
|
+
// and that updateSandbox is called with the correct slug and update payload.
|
|
214
|
+
const updated = makeSandbox({ name: "Updated Name" });
|
|
215
|
+
vi.mocked(sandboxManager.updateSandbox).mockReturnValue(updated as any);
|
|
216
|
+
|
|
217
|
+
const res = await app.request("/api/sandboxes/my-sandbox", {
|
|
218
|
+
method: "PUT",
|
|
219
|
+
headers: { "Content-Type": "application/json" },
|
|
220
|
+
body: JSON.stringify({ name: "Updated Name", initScript: "echo hi" }),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(res.status).toBe(200);
|
|
224
|
+
expect(await res.json()).toEqual(updated);
|
|
225
|
+
expect(sandboxManager.updateSandbox).toHaveBeenCalledWith(
|
|
226
|
+
"my-sandbox",
|
|
227
|
+
expect.objectContaining({ name: "Updated Name", initScript: "echo hi" }),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("returns 404 when the sandbox does not exist", async () => {
|
|
232
|
+
// Validates that updating a non-existent sandbox returns 404.
|
|
233
|
+
// updateSandbox returns null when the slug is not found.
|
|
234
|
+
vi.mocked(sandboxManager.updateSandbox).mockReturnValue(null);
|
|
235
|
+
|
|
236
|
+
const res = await app.request("/api/sandboxes/missing", {
|
|
237
|
+
method: "PUT",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify({ name: "X" }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(res.status).toBe(404);
|
|
243
|
+
const json = await res.json();
|
|
244
|
+
expect(json.error).toMatch(/not found/i);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("returns 400 when updateSandbox throws a slug collision error", async () => {
|
|
248
|
+
// Validates that renaming a sandbox to a name that collides with
|
|
249
|
+
// an existing slug results in a 400 error.
|
|
250
|
+
vi.mocked(sandboxManager.updateSandbox).mockImplementation(() => {
|
|
251
|
+
throw new Error('A sandbox with a similar name already exists ("other-sandbox")');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const res = await app.request("/api/sandboxes/my-sandbox", {
|
|
255
|
+
method: "PUT",
|
|
256
|
+
headers: { "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify({ name: "Other Sandbox" }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(res.status).toBe(400);
|
|
261
|
+
const json = await res.json();
|
|
262
|
+
expect(json.error).toMatch(/already exists/i);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
267
|
+
// DELETE /api/sandboxes/:slug
|
|
268
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
|
+
|
|
270
|
+
describe("DELETE /api/sandboxes/:slug", () => {
|
|
271
|
+
it("deletes a sandbox and returns ok", async () => {
|
|
272
|
+
// Validates successful deletion returns { ok: true } and that
|
|
273
|
+
// deleteSandbox is called with the correct slug.
|
|
274
|
+
vi.mocked(sandboxManager.deleteSandbox).mockReturnValue(true);
|
|
275
|
+
|
|
276
|
+
const res = await app.request("/api/sandboxes/my-sandbox", {
|
|
277
|
+
method: "DELETE",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(res.status).toBe(200);
|
|
281
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
282
|
+
expect(sandboxManager.deleteSandbox).toHaveBeenCalledWith("my-sandbox");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("returns 404 when the sandbox does not exist", async () => {
|
|
286
|
+
// Validates that deleting a non-existent sandbox returns 404.
|
|
287
|
+
vi.mocked(sandboxManager.deleteSandbox).mockReturnValue(false);
|
|
288
|
+
|
|
289
|
+
const res = await app.request("/api/sandboxes/missing", {
|
|
290
|
+
method: "DELETE",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(res.status).toBe(404);
|
|
294
|
+
const json = await res.json();
|
|
295
|
+
expect(json.error).toMatch(/not found/i);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
300
|
+
// POST /api/sandboxes/:slug/test-init
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
302
|
+
|
|
303
|
+
describe("POST /api/sandboxes/:slug/test-init", () => {
|
|
304
|
+
it("executes the init script in an ephemeral container and returns success", async () => {
|
|
305
|
+
// Happy path: sandbox exists, has init script, Docker available, image ready.
|
|
306
|
+
// Should create container, copy workspace, exec script, cleanup.
|
|
307
|
+
const sandbox = makeSandbox({ initScript: "echo hello" });
|
|
308
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
309
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
310
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(true);
|
|
311
|
+
vi.mocked(containerManager.execInContainerAsync).mockResolvedValue({
|
|
312
|
+
exitCode: 0,
|
|
313
|
+
output: "hello\n",
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: JSON.stringify({ cwd: "/home/user/project" }),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(res.status).toBe(200);
|
|
323
|
+
const json = await res.json();
|
|
324
|
+
expect(json.success).toBe(true);
|
|
325
|
+
expect(json.exitCode).toBe(0);
|
|
326
|
+
expect(json.output).toBe("hello\n");
|
|
327
|
+
|
|
328
|
+
// Container should be cleaned up
|
|
329
|
+
expect(containerManager.removeContainer).toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("returns 404 when sandbox not found", async () => {
|
|
333
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(null);
|
|
334
|
+
|
|
335
|
+
const res = await app.request("/api/sandboxes/missing/test-init", {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "Content-Type": "application/json" },
|
|
338
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(res.status).toBe(404);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("returns 400 when sandbox has no init script", async () => {
|
|
345
|
+
// A sandbox without an init script cannot be tested
|
|
346
|
+
const sandbox = makeSandbox(); // no initScript
|
|
347
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
348
|
+
|
|
349
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "Content-Type": "application/json" },
|
|
352
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(res.status).toBe(400);
|
|
356
|
+
const json = await res.json();
|
|
357
|
+
expect(json.error).toMatch(/no init script/i);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("returns 400 when cwd is missing", async () => {
|
|
361
|
+
const sandbox = makeSandbox({ initScript: "echo test" });
|
|
362
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
363
|
+
|
|
364
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
body: JSON.stringify({}),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(res.status).toBe(400);
|
|
371
|
+
const json = await res.json();
|
|
372
|
+
expect(json.error).toMatch(/cwd/i);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("returns 503 when Docker is not available", async () => {
|
|
376
|
+
const sandbox = makeSandbox({ initScript: "echo test" });
|
|
377
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
378
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(false);
|
|
379
|
+
|
|
380
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
383
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
expect(res.status).toBe(503);
|
|
387
|
+
const json = await res.json();
|
|
388
|
+
expect(json.error).toMatch(/docker/i);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("returns 503 when Docker image is not ready", async () => {
|
|
392
|
+
const sandbox = makeSandbox({ initScript: "echo test" });
|
|
393
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
394
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
395
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(false);
|
|
396
|
+
|
|
397
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: { "Content-Type": "application/json" },
|
|
400
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(res.status).toBe(503);
|
|
404
|
+
const json = await res.json();
|
|
405
|
+
expect(json.error).toMatch(/not available/i);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("returns failure when init script exits with non-zero code", async () => {
|
|
409
|
+
// The init script failed — report the exit code and captured output
|
|
410
|
+
const sandbox = makeSandbox({ initScript: "exit 1" });
|
|
411
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
412
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
413
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(true);
|
|
414
|
+
vi.mocked(containerManager.execInContainerAsync).mockResolvedValue({
|
|
415
|
+
exitCode: 1,
|
|
416
|
+
output: "command not found\n",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { "Content-Type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(res.status).toBe(200);
|
|
426
|
+
const json = await res.json();
|
|
427
|
+
expect(json.success).toBe(false);
|
|
428
|
+
expect(json.exitCode).toBe(1);
|
|
429
|
+
expect(json.output).toContain("command not found");
|
|
430
|
+
|
|
431
|
+
// Container should still be cleaned up
|
|
432
|
+
expect(containerManager.removeContainer).toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("cleans up container even when execInContainerAsync throws", async () => {
|
|
436
|
+
// Ensures the finally block removes the container on unexpected errors
|
|
437
|
+
const sandbox = makeSandbox({ initScript: "echo crash" });
|
|
438
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
439
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
440
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(true);
|
|
441
|
+
vi.mocked(containerManager.execInContainerAsync).mockRejectedValue(
|
|
442
|
+
new Error("Container crashed"),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
446
|
+
method: "POST",
|
|
447
|
+
headers: { "Content-Type": "application/json" },
|
|
448
|
+
body: JSON.stringify({ cwd: "/tmp" }),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
expect(res.status).toBe(500);
|
|
452
|
+
const json = await res.json();
|
|
453
|
+
expect(json.success).toBe(false);
|
|
454
|
+
expect(json.output).toBe("Container crashed");
|
|
455
|
+
|
|
456
|
+
// Container should be cleaned up even on error
|
|
457
|
+
expect(containerManager.removeContainer).toHaveBeenCalled();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("uses body initScript over stored initScript when provided", async () => {
|
|
461
|
+
// The endpoint accepts an optional initScript body param so the frontend
|
|
462
|
+
// can test unsaved draft content without persisting first.
|
|
463
|
+
const sandbox = makeSandbox({ initScript: "stored script" });
|
|
464
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
465
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
466
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(true);
|
|
467
|
+
vi.mocked(containerManager.execInContainerAsync).mockResolvedValue({
|
|
468
|
+
exitCode: 0,
|
|
469
|
+
output: "draft ok\n",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
473
|
+
method: "POST",
|
|
474
|
+
headers: { "Content-Type": "application/json" },
|
|
475
|
+
body: JSON.stringify({ cwd: "/tmp", initScript: "echo draft" }),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(res.status).toBe(200);
|
|
479
|
+
// Should exec the body initScript, not the stored one
|
|
480
|
+
expect(containerManager.execInContainerAsync).toHaveBeenCalledWith(
|
|
481
|
+
"test-container-123",
|
|
482
|
+
["sh", "-lc", "echo draft"],
|
|
483
|
+
expect.any(Object),
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("normalizes cwd to prevent path traversal", async () => {
|
|
488
|
+
// The cwd should be resolved to an absolute path to collapse
|
|
489
|
+
// traversal sequences like ../../etc.
|
|
490
|
+
const sandbox = makeSandbox({ initScript: "echo test" });
|
|
491
|
+
vi.mocked(sandboxManager.getSandbox).mockReturnValue(sandbox as any);
|
|
492
|
+
vi.mocked(containerManager.checkDocker).mockReturnValue(true);
|
|
493
|
+
vi.mocked(imagePullManager.isReady).mockReturnValue(true);
|
|
494
|
+
vi.mocked(containerManager.execInContainerAsync).mockResolvedValue({
|
|
495
|
+
exitCode: 0,
|
|
496
|
+
output: "ok\n",
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const res = await app.request("/api/sandboxes/my-sandbox/test-init", {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: { "Content-Type": "application/json" },
|
|
502
|
+
body: JSON.stringify({ cwd: "/home/user/../../../etc" }),
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
expect(res.status).toBe(200);
|
|
506
|
+
// The cwd passed to createContainer should be the resolved path
|
|
507
|
+
expect(containerManager.createContainer).toHaveBeenCalledWith(
|
|
508
|
+
expect.any(String),
|
|
509
|
+
"/etc",
|
|
510
|
+
expect.any(Object),
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import * as sandboxManager from "../sandbox-manager.js";
|
|
4
|
+
import { containerManager, type ContainerConfig } from "../container-manager.js";
|
|
5
|
+
import { imagePullManager } from "../image-pull-manager.js";
|
|
6
|
+
|
|
7
|
+
export function registerSandboxRoutes(
|
|
8
|
+
api: Hono,
|
|
9
|
+
): void {
|
|
10
|
+
api.get("/sandboxes", (c) => {
|
|
11
|
+
try {
|
|
12
|
+
return c.json(sandboxManager.listSandboxes());
|
|
13
|
+
} catch (e: unknown) {
|
|
14
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
api.get("/sandboxes/:slug", (c) => {
|
|
19
|
+
const sandbox = sandboxManager.getSandbox(c.req.param("slug"));
|
|
20
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
21
|
+
return c.json(sandbox);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
api.post("/sandboxes", async (c) => {
|
|
25
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26
|
+
try {
|
|
27
|
+
const sandbox = sandboxManager.createSandbox(body.name, {
|
|
28
|
+
initScript: body.initScript,
|
|
29
|
+
});
|
|
30
|
+
return c.json(sandbox, 201);
|
|
31
|
+
} catch (e: unknown) {
|
|
32
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
api.put("/sandboxes/:slug", async (c) => {
|
|
37
|
+
const slug = c.req.param("slug");
|
|
38
|
+
const body = await c.req.json().catch(() => ({}));
|
|
39
|
+
try {
|
|
40
|
+
const sandbox = sandboxManager.updateSandbox(slug, {
|
|
41
|
+
name: body.name,
|
|
42
|
+
initScript: body.initScript,
|
|
43
|
+
});
|
|
44
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
45
|
+
return c.json(sandbox);
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
api.delete("/sandboxes/:slug", (c) => {
|
|
52
|
+
try {
|
|
53
|
+
const deleted = sandboxManager.deleteSandbox(c.req.param("slug"));
|
|
54
|
+
if (!deleted) return c.json({ error: "Sandbox not found" }, 404);
|
|
55
|
+
return c.json({ ok: true });
|
|
56
|
+
} catch (e: unknown) {
|
|
57
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Test the init script of a sandbox in an ephemeral container.
|
|
62
|
+
// Accepts an optional `initScript` body param to test unsaved content
|
|
63
|
+
// without persisting it first. Falls back to the stored script.
|
|
64
|
+
api.post("/sandboxes/:slug/test-init", async (c) => {
|
|
65
|
+
const slug = c.req.param("slug");
|
|
66
|
+
const body = await c.req.json().catch(() => ({}));
|
|
67
|
+
const rawCwd = body.cwd;
|
|
68
|
+
|
|
69
|
+
const sandbox = sandboxManager.getSandbox(slug);
|
|
70
|
+
if (!sandbox) return c.json({ error: "Sandbox not found" }, 404);
|
|
71
|
+
|
|
72
|
+
// Prefer body initScript (unsaved draft) over stored value
|
|
73
|
+
const initScript = (typeof body.initScript === "string" ? body.initScript : sandbox.initScript ?? "").trim();
|
|
74
|
+
if (!initScript) return c.json({ error: "No init script configured for this sandbox" }, 400);
|
|
75
|
+
if (!rawCwd) return c.json({ error: "Working directory (cwd) is required" }, 400);
|
|
76
|
+
|
|
77
|
+
// Require an absolute path from the caller, then normalize
|
|
78
|
+
const cwdStr = String(rawCwd);
|
|
79
|
+
if (!cwdStr.startsWith("/")) return c.json({ error: "Working directory must be an absolute path" }, 400);
|
|
80
|
+
const cwd = resolve(cwdStr);
|
|
81
|
+
|
|
82
|
+
if (!containerManager.checkDocker()) return c.json({ error: "Docker is not available" }, 503);
|
|
83
|
+
|
|
84
|
+
const effectiveImage = "the-companion:latest";
|
|
85
|
+
if (!imagePullManager.isReady(effectiveImage)) {
|
|
86
|
+
return c.json({ error: `Docker image ${effectiveImage} is not available. Pull it first.` }, 503);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tempId = `t${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
90
|
+
let containerId: string | undefined;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const config: ContainerConfig = {
|
|
94
|
+
image: effectiveImage,
|
|
95
|
+
ports: [],
|
|
96
|
+
};
|
|
97
|
+
const containerInfo = containerManager.createContainer(tempId, cwd, config);
|
|
98
|
+
containerId = containerInfo.containerId;
|
|
99
|
+
|
|
100
|
+
await containerManager.copyWorkspaceToContainer(containerId, cwd);
|
|
101
|
+
|
|
102
|
+
const initTimeout = Number(process.env.COMPANION_INIT_SCRIPT_TIMEOUT) || 120_000;
|
|
103
|
+
const result = await containerManager.execInContainerAsync(
|
|
104
|
+
containerId,
|
|
105
|
+
["sh", "-lc", initScript],
|
|
106
|
+
{ timeout: initTimeout },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const output = result.output.length > 2000
|
|
110
|
+
? result.output.slice(0, 500) + "\n...[truncated]...\n" + result.output.slice(-1500)
|
|
111
|
+
: result.output;
|
|
112
|
+
|
|
113
|
+
return c.json({
|
|
114
|
+
success: result.exitCode === 0,
|
|
115
|
+
exitCode: result.exitCode,
|
|
116
|
+
output,
|
|
117
|
+
});
|
|
118
|
+
} catch (e: unknown) {
|
|
119
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
120
|
+
return c.json({ success: false, exitCode: -1, output: msg }, 500);
|
|
121
|
+
} finally {
|
|
122
|
+
if (containerId) {
|
|
123
|
+
try { containerManager.removeContainer(tempId); } catch { /* best effort cleanup */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|