@saeed42/worktree-worker 1.0.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/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # @orgn/worktree-worker
2
+
3
+ Git worktree management service for AI agent trials. Runs as an npm package in CodeSandbox or any Node.js environment.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g @orgn/worktree-worker
10
+
11
+ # Or add to your project
12
+ npm install @orgn/worktree-worker
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### Run as CLI
18
+
19
+ ```bash
20
+ # Set environment variables
21
+ export WORKER_TOKEN=your-secret-token
22
+ export PORT=8787
23
+
24
+ # Run the service
25
+ worktree-worker
26
+ ```
27
+
28
+ ### Run in CodeSandbox
29
+
30
+ Add to your `package.json`:
31
+
32
+ ```json
33
+ {
34
+ "dependencies": {
35
+ "@orgn/worktree-worker": "^1.0.0"
36
+ },
37
+ "scripts": {
38
+ "worktree-worker": "worktree-worker"
39
+ }
40
+ }
41
+ ```
42
+
43
+ Or run directly:
44
+
45
+ ```bash
46
+ npx @orgn/worktree-worker
47
+ ```
48
+
49
+ ### Run Programmatically
50
+
51
+ ```typescript
52
+ import { app } from '@orgn/worktree-worker';
53
+
54
+ // The app is a Hono instance
55
+ // You can extend it or use it as middleware
56
+ ```
57
+
58
+ ## Architecture
59
+
60
+ ```
61
+ /project/
62
+ ├── sandbox/ # BASE_WORKSPACE_DIR - Main repo clone
63
+ └── workspaces/
64
+ └── trials/ # TRIALS_WORKSPACE_DIR - Worktrees
65
+ ├── feature-auth-a1b2c3d4/
66
+ ├── fix-bug-xyz-5e6f7g8h/
67
+ └── ...
68
+ ```
69
+
70
+ ## API Endpoints
71
+
72
+ ### Health Checks
73
+
74
+ | Endpoint | Description |
75
+ |----------|-------------|
76
+ | `GET /health` | Basic health check |
77
+ | `GET /health/ready` | Readiness check |
78
+ | `GET /health/live` | Liveness check |
79
+
80
+ ### Repository Management
81
+
82
+ | Endpoint | Description |
83
+ |----------|-------------|
84
+ | `POST /v1/repo/init` | Clone repository |
85
+ | `POST /v1/repo/update` | Fetch/pull latest |
86
+ | `GET /v1/repo/status` | Get repo status |
87
+ | `GET /v1/repo/branches` | List branches |
88
+
89
+ ### Worktree Management
90
+
91
+ | Endpoint | Description |
92
+ |----------|-------------|
93
+ | `POST /v1/trials/:trialId/worktree/reset` | Create/reset worktree |
94
+ | `DELETE /v1/trials/:trialId/worktree` | Delete worktree |
95
+ | `GET /v1/trials/:trialId/worktree/status` | Get status |
96
+ | `GET /v1/trials/:trialId/worktree/diff` | Get diff |
97
+ | `POST /v1/trials/:trialId/worktree/commit` | Commit changes |
98
+ | `POST /v1/trials/:trialId/worktree/push` | Push to remote |
99
+
100
+ ## Configuration
101
+
102
+ | Variable | Default | Description |
103
+ |----------|---------|-------------|
104
+ | `PORT` | `8787` | HTTP server port |
105
+ | `NODE_ENV` | `development` | Environment mode |
106
+ | `WORKER_TOKEN` | `` | Auth token (empty = no auth) |
107
+ | `BASE_WORKSPACE_DIR` | `/project/sandbox` | Main repo path |
108
+ | `TRIALS_WORKSPACE_DIR` | `/project/workspaces/trials` | Worktrees path |
109
+ | `DEFAULT_BRANCH` | `main` | Default base branch |
110
+ | `GIT_TIMEOUT_MS` | `60000` | Git timeout |
111
+ | `CLEANUP_AFTER_HOURS` | `24` | Stale worktree cleanup |
112
+
113
+ ## Usage Examples
114
+
115
+ ### Initialize Repository
116
+
117
+ ```bash
118
+ curl -X POST http://localhost:8787/v1/repo/init \
119
+ -H "Authorization: Bearer your-token" \
120
+ -H "Content-Type: application/json" \
121
+ -d '{
122
+ "repoUrl": "https://github.com/owner/repo",
123
+ "branch": "main",
124
+ "githubToken": "ghp_xxx"
125
+ }'
126
+ ```
127
+
128
+ ### Create Worktree
129
+
130
+ ```bash
131
+ curl -X POST http://localhost:8787/v1/trials/abc123/worktree/reset \
132
+ -H "Authorization: Bearer your-token" \
133
+ -H "Content-Type: application/json" \
134
+ -d '{
135
+ "baseBranch": "main",
136
+ "trialBranch": "feature/my-feature"
137
+ }'
138
+ ```
139
+
140
+ Response:
141
+
142
+ ```json
143
+ {
144
+ "success": true,
145
+ "data": {
146
+ "worktreePath": "/project/workspaces/trials/feature-my-feature-abc12345",
147
+ "branch": "feature/my-feature",
148
+ "headSha": "a1b2c3d4e5f6..."
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Commit Changes
154
+
155
+ ```bash
156
+ curl -X POST http://localhost:8787/v1/trials/abc123/worktree/commit \
157
+ -H "Authorization: Bearer your-token" \
158
+ -H "Content-Type: application/json" \
159
+ -d '{
160
+ "message": "feat: implement auth",
161
+ "author": {
162
+ "name": "AI Agent",
163
+ "email": "agent@origin.ai"
164
+ }
165
+ }'
166
+ ```
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ # Clone the repo
172
+ cd worker-service-node
173
+
174
+ # Install dependencies
175
+ npm install
176
+
177
+ # Run in dev mode (with watch)
178
+ npm run dev
179
+
180
+ # Type check
181
+ npm run typecheck
182
+
183
+ # Build
184
+ npm run build
185
+
186
+ # Run production build
187
+ npm start
188
+ ```
189
+
190
+ ## Publishing
191
+
192
+ ```bash
193
+ # Build and publish to npm
194
+ npm publish --access public
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
200
+
package/dist/main.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ declare const app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
5
+
6
+ export { app };
package/dist/main.js ADDED
@@ -0,0 +1,1121 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.ts
4
+ import { serve } from "@hono/node-server";
5
+ import { Hono as Hono4 } from "hono";
6
+ import { cors } from "hono/cors";
7
+ import { logger as honoLogger } from "hono/logger";
8
+ import { secureHeaders } from "hono/secure-headers";
9
+
10
+ // src/config/env.ts
11
+ var env = {
12
+ PORT: parseInt(process.env.PORT || "8787", 10),
13
+ NODE_ENV: process.env.NODE_ENV || "development",
14
+ WORKER_TOKEN: process.env.WORKER_TOKEN || "",
15
+ BASE_WORKSPACE_DIR: process.env.BASE_WORKSPACE_DIR || "/project/sandbox",
16
+ WORKSPACES_ROOT: process.env.WORKSPACES_ROOT || "/project/workspaces",
17
+ TRIALS_WORKSPACE_DIR: process.env.TRIALS_WORKSPACE_DIR || "/project/workspaces/trials",
18
+ DEFAULT_BRANCH: process.env.DEFAULT_BRANCH || "main",
19
+ GIT_TIMEOUT_MS: parseInt(process.env.GIT_TIMEOUT_MS || "60000", 10),
20
+ CLEANUP_AFTER_HOURS: parseInt(process.env.CLEANUP_AFTER_HOURS || "24", 10)
21
+ };
22
+
23
+ // src/config/logger.ts
24
+ import pino from "pino";
25
+ var isDev = env.NODE_ENV === "development";
26
+ var baseLogger = pino({
27
+ level: isDev ? "debug" : "info",
28
+ transport: isDev ? {
29
+ target: "pino-pretty",
30
+ options: {
31
+ colorize: true,
32
+ translateTime: "SYS:standard",
33
+ ignore: "pid,hostname"
34
+ }
35
+ } : void 0,
36
+ base: {
37
+ service: "worktree-worker"
38
+ }
39
+ });
40
+ var logger = {
41
+ info: (msgOrObj, obj) => {
42
+ if (typeof msgOrObj === "string") {
43
+ baseLogger.info(obj || {}, msgOrObj);
44
+ } else {
45
+ const { msg, ...rest } = msgOrObj;
46
+ baseLogger.info(rest, msg);
47
+ }
48
+ },
49
+ error: (msgOrObj, obj) => {
50
+ if (typeof msgOrObj === "string") {
51
+ baseLogger.error(obj || {}, msgOrObj);
52
+ } else {
53
+ const { msg, ...rest } = msgOrObj;
54
+ baseLogger.error(rest, msg);
55
+ }
56
+ },
57
+ warn: (msgOrObj, obj) => {
58
+ if (typeof msgOrObj === "string") {
59
+ baseLogger.warn(obj || {}, msgOrObj);
60
+ } else {
61
+ const { msg, ...rest } = msgOrObj;
62
+ baseLogger.warn(rest, msg);
63
+ }
64
+ },
65
+ debug: (msgOrObj, obj) => {
66
+ if (typeof msgOrObj === "string") {
67
+ baseLogger.debug(obj || {}, msgOrObj);
68
+ } else {
69
+ const { msg, ...rest } = msgOrObj;
70
+ baseLogger.debug(rest, msg);
71
+ }
72
+ },
73
+ child: (bindings) => {
74
+ const childLogger = baseLogger.child(bindings);
75
+ return {
76
+ info: (msgOrObj, obj) => {
77
+ if (typeof msgOrObj === "string") {
78
+ childLogger.info(obj || {}, msgOrObj);
79
+ } else {
80
+ const { msg, ...rest } = msgOrObj;
81
+ childLogger.info(rest, msg);
82
+ }
83
+ },
84
+ error: (msgOrObj, obj) => {
85
+ if (typeof msgOrObj === "string") {
86
+ childLogger.error(obj || {}, msgOrObj);
87
+ } else {
88
+ const { msg, ...rest } = msgOrObj;
89
+ childLogger.error(rest, msg);
90
+ }
91
+ },
92
+ warn: (msgOrObj, obj) => {
93
+ if (typeof msgOrObj === "string") {
94
+ childLogger.warn(obj || {}, msgOrObj);
95
+ } else {
96
+ const { msg, ...rest } = msgOrObj;
97
+ childLogger.warn(rest, msg);
98
+ }
99
+ },
100
+ debug: (msgOrObj, obj) => {
101
+ if (typeof msgOrObj === "string") {
102
+ childLogger.debug(obj || {}, msgOrObj);
103
+ } else {
104
+ const { msg, ...rest } = msgOrObj;
105
+ childLogger.debug(rest, msg);
106
+ }
107
+ }
108
+ };
109
+ }
110
+ };
111
+
112
+ // src/middleware/auth.ts
113
+ async function authMiddleware(c, next) {
114
+ if (!env.WORKER_TOKEN) {
115
+ return next();
116
+ }
117
+ const authHeader = c.req.header("Authorization");
118
+ if (!authHeader) {
119
+ return c.json({ success: false, error: { code: "UNAUTHORIZED", message: "Missing Authorization header" } }, 401);
120
+ }
121
+ const [scheme, token] = authHeader.split(" ");
122
+ if (scheme !== "Bearer" || !token) {
123
+ return c.json({ success: false, error: { code: "UNAUTHORIZED", message: "Invalid Authorization format" } }, 401);
124
+ }
125
+ if (token !== env.WORKER_TOKEN) {
126
+ return c.json({ success: false, error: { code: "UNAUTHORIZED", message: "Invalid token" } }, 401);
127
+ }
128
+ return next();
129
+ }
130
+
131
+ // src/routes/health.routes.ts
132
+ import { Hono } from "hono";
133
+ import { stat, mkdir } from "fs/promises";
134
+
135
+ // src/services/git.service.ts
136
+ import { spawn } from "child_process";
137
+ var GitService = class {
138
+ /**
139
+ * Execute a git command
140
+ */
141
+ async exec(args, cwd) {
142
+ const workDir = cwd || env.BASE_WORKSPACE_DIR;
143
+ const log = logger.child({ git: args.slice(0, 2).join(" "), cwd: workDir });
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn("git", args, {
146
+ cwd: workDir,
147
+ timeout: env.GIT_TIMEOUT_MS,
148
+ env: {
149
+ ...process.env,
150
+ GIT_TERMINAL_PROMPT: "0",
151
+ GIT_ASKPASS: ""
152
+ }
153
+ });
154
+ let stdout = "";
155
+ let stderr = "";
156
+ child.stdout.on("data", (data) => {
157
+ stdout += data.toString();
158
+ });
159
+ child.stderr.on("data", (data) => {
160
+ stderr += data.toString();
161
+ });
162
+ child.on("close", (code) => {
163
+ const exitCode = code ?? 1;
164
+ log.debug("Git command completed", { exitCode, stdoutLen: stdout.length });
165
+ resolve({ code: exitCode, stdout, stderr });
166
+ });
167
+ child.on("error", (err) => {
168
+ log.error("Git command error", { error: err.message });
169
+ reject(err);
170
+ });
171
+ });
172
+ }
173
+ /**
174
+ * Execute git command and throw on error
175
+ */
176
+ async execOrThrow(args, cwd) {
177
+ const result = await this.exec(args, cwd);
178
+ if (result.code !== 0) {
179
+ throw new Error(`Git command failed: ${result.stderr || result.stdout}`);
180
+ }
181
+ return result;
182
+ }
183
+ /**
184
+ * Get git version
185
+ */
186
+ async getVersion() {
187
+ const result = await this.execOrThrow(["--version"]);
188
+ return result.stdout.trim();
189
+ }
190
+ /**
191
+ * Fetch from remote
192
+ */
193
+ async fetch(remote = "origin", cwd) {
194
+ await this.execOrThrow(["fetch", remote], cwd);
195
+ }
196
+ /**
197
+ * Clone a repository
198
+ */
199
+ async cloneRepo(repoUrl, targetDir, options) {
200
+ const args = ["clone"];
201
+ if (options?.blobless) {
202
+ args.push("--filter=blob:none");
203
+ } else if (options?.depth) {
204
+ args.push("--depth", String(options.depth));
205
+ }
206
+ if (options?.branch) {
207
+ args.push("--branch", options.branch);
208
+ }
209
+ args.push(repoUrl, targetDir);
210
+ const parentDir = targetDir.split("/").slice(0, -1).join("/") || "/";
211
+ await this.execOrThrow(args, parentDir);
212
+ }
213
+ /**
214
+ * List worktrees
215
+ */
216
+ async listWorktrees(cwd) {
217
+ const result = await this.exec(["worktree", "list", "--porcelain"], cwd);
218
+ if (result.code !== 0) return [];
219
+ const worktrees = [];
220
+ let current = {};
221
+ for (const line of result.stdout.split("\n")) {
222
+ if (line.startsWith("worktree ")) {
223
+ if (current.path) {
224
+ worktrees.push({
225
+ path: current.path,
226
+ branch: current.branch || "",
227
+ head: current.head || ""
228
+ });
229
+ }
230
+ current = { path: line.slice(9) };
231
+ } else if (line.startsWith("HEAD ")) {
232
+ current.head = line.slice(5);
233
+ } else if (line.startsWith("branch ")) {
234
+ current.branch = line.slice(7).replace("refs/heads/", "");
235
+ }
236
+ }
237
+ if (current.path) {
238
+ worktrees.push({
239
+ path: current.path,
240
+ branch: current.branch || "",
241
+ head: current.head || ""
242
+ });
243
+ }
244
+ return worktrees;
245
+ }
246
+ /**
247
+ * Add a worktree with a new branch
248
+ */
249
+ async addWorktreeWithNewBranch(path, newBranch, fromRef, cwd) {
250
+ const branchResult = await this.exec(["branch", newBranch, fromRef], cwd);
251
+ if (branchResult.code === 0) {
252
+ await this.execOrThrow(["worktree", "add", path, newBranch], cwd);
253
+ } else {
254
+ await this.execOrThrow(["worktree", "add", "-b", newBranch, path, fromRef], cwd);
255
+ }
256
+ }
257
+ /**
258
+ * Remove a worktree
259
+ */
260
+ async removeWorktree(path, force = false, cwd) {
261
+ const args = ["worktree", "remove", path];
262
+ if (force) args.push("--force");
263
+ await this.exec(args, cwd);
264
+ }
265
+ /**
266
+ * Prune worktrees
267
+ */
268
+ async pruneWorktrees(cwd) {
269
+ await this.exec(["worktree", "prune"], cwd);
270
+ }
271
+ /**
272
+ * Delete a branch
273
+ */
274
+ async deleteBranch(branch, force = false, cwd) {
275
+ const args = ["branch", force ? "-D" : "-d", branch];
276
+ await this.exec(args, cwd);
277
+ }
278
+ /**
279
+ * Get HEAD SHA
280
+ */
281
+ async getHeadSha(cwd) {
282
+ const result = await this.execOrThrow(["rev-parse", "HEAD"], cwd);
283
+ return result.stdout.trim();
284
+ }
285
+ /**
286
+ * Get current branch
287
+ */
288
+ async getCurrentBranch(cwd) {
289
+ const result = await this.execOrThrow(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
290
+ return result.stdout.trim();
291
+ }
292
+ /**
293
+ * Check if branch exists
294
+ */
295
+ async branchExists(branch, cwd) {
296
+ const result = await this.exec(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], cwd);
297
+ return result.code === 0;
298
+ }
299
+ /**
300
+ * Get status
301
+ */
302
+ async getStatus(cwd) {
303
+ const result = await this.exec(["status", "--porcelain"], cwd);
304
+ const files = result.stdout.split("\n").filter((line) => line.trim()).map((line) => line.slice(3));
305
+ return { hasChanges: files.length > 0, files };
306
+ }
307
+ /**
308
+ * Get diff
309
+ */
310
+ async getDiff(cwd) {
311
+ const result = await this.exec(["diff", "--no-color"], cwd);
312
+ return result.stdout;
313
+ }
314
+ /**
315
+ * Add all files
316
+ */
317
+ async addAll(cwd) {
318
+ await this.execOrThrow(["add", "-A"], cwd);
319
+ }
320
+ /**
321
+ * Commit
322
+ */
323
+ async commit(message, author, cwd) {
324
+ const args = ["commit", "-m", message];
325
+ if (author) {
326
+ args.push("--author", `${author.name} <${author.email}>`);
327
+ }
328
+ await this.execOrThrow(args, cwd);
329
+ return this.getHeadSha(cwd);
330
+ }
331
+ /**
332
+ * Push
333
+ */
334
+ async push(remote, branch, force = false, cwd) {
335
+ const args = ["push", remote, branch];
336
+ if (force) args.push("--force");
337
+ await this.execOrThrow(args, cwd);
338
+ }
339
+ /**
340
+ * List branches
341
+ */
342
+ async listBranches(cwd) {
343
+ const localResult = await this.exec(["branch", "--format=%(refname:short)"], cwd);
344
+ const remoteResult = await this.exec(["branch", "-r", "--format=%(refname:short)"], cwd);
345
+ return {
346
+ local: localResult.stdout.split("\n").filter((b) => b.trim()),
347
+ remote: remoteResult.stdout.split("\n").filter((b) => b.trim())
348
+ };
349
+ }
350
+ /**
351
+ * Get remote URL
352
+ */
353
+ async getRemoteUrl(remote = "origin", cwd) {
354
+ const result = await this.exec(["remote", "get-url", remote], cwd);
355
+ return result.code === 0 ? result.stdout.trim() : null;
356
+ }
357
+ };
358
+ var gitService = new GitService();
359
+
360
+ // src/routes/health.routes.ts
361
+ var health = new Hono();
362
+ health.get("/", (c) => {
363
+ return c.json({
364
+ status: "ok",
365
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
366
+ service: "worktree-worker",
367
+ version: "1.0.0"
368
+ });
369
+ });
370
+ health.get("/ready", async (c) => {
371
+ const checks = {};
372
+ try {
373
+ const version = await gitService.getVersion();
374
+ checks.git = { status: "ok", message: version };
375
+ } catch (err) {
376
+ checks.git = { status: "error", message: err.message };
377
+ }
378
+ try {
379
+ const stats = await stat(env.BASE_WORKSPACE_DIR);
380
+ checks.baseWorkspaceDir = {
381
+ status: stats.isDirectory() ? "ok" : "error",
382
+ message: env.BASE_WORKSPACE_DIR
383
+ };
384
+ } catch {
385
+ checks.baseWorkspaceDir = {
386
+ status: "warn",
387
+ message: `${env.BASE_WORKSPACE_DIR} not found (repo not cloned yet)`
388
+ };
389
+ }
390
+ try {
391
+ await mkdir(env.TRIALS_WORKSPACE_DIR, { recursive: true });
392
+ checks.trialsWorkspaceDir = { status: "ok", message: env.TRIALS_WORKSPACE_DIR };
393
+ } catch (err) {
394
+ checks.trialsWorkspaceDir = { status: "error", message: err.message };
395
+ }
396
+ const hasError = Object.values(checks).some((c2) => c2.status === "error");
397
+ const hasWarn = Object.values(checks).some((c2) => c2.status === "warn");
398
+ return c.json(
399
+ {
400
+ status: hasError ? "degraded" : hasWarn ? "ready-warn" : "ready",
401
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
402
+ checks
403
+ },
404
+ hasError ? 503 : 200
405
+ );
406
+ });
407
+ health.get("/live", (c) => {
408
+ return c.json({
409
+ status: "alive",
410
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
411
+ uptime: Math.floor(process.uptime())
412
+ });
413
+ });
414
+
415
+ // src/routes/repo.routes.ts
416
+ import { Hono as Hono2 } from "hono";
417
+ import { z } from "zod";
418
+
419
+ // src/services/repo.service.ts
420
+ import { mkdir as mkdir2, rm, stat as stat2, writeFile } from "fs/promises";
421
+ var RepoService = class {
422
+ /**
423
+ * Check if the repository is initialized
424
+ */
425
+ async getStatus() {
426
+ const repoRoot = env.BASE_WORKSPACE_DIR;
427
+ try {
428
+ const stats = await stat2(repoRoot);
429
+ if (!stats.isDirectory()) {
430
+ return {
431
+ initialized: false,
432
+ path: repoRoot,
433
+ branch: null,
434
+ remote: null,
435
+ headSha: null
436
+ };
437
+ }
438
+ const gitDir = await stat2(`${repoRoot}/.git`).catch(() => null);
439
+ if (!gitDir) {
440
+ return {
441
+ initialized: false,
442
+ path: repoRoot,
443
+ branch: null,
444
+ remote: null,
445
+ headSha: null
446
+ };
447
+ }
448
+ const branch = await gitService.getCurrentBranch(repoRoot).catch(() => null);
449
+ const headSha = await gitService.getHeadSha(repoRoot).catch(() => null);
450
+ const remote = await gitService.getRemoteUrl("origin", repoRoot);
451
+ return {
452
+ initialized: true,
453
+ path: repoRoot,
454
+ branch,
455
+ remote,
456
+ headSha
457
+ };
458
+ } catch {
459
+ return {
460
+ initialized: false,
461
+ path: repoRoot,
462
+ branch: null,
463
+ remote: null,
464
+ headSha: null
465
+ };
466
+ }
467
+ }
468
+ /**
469
+ * Initialize/clone the repository
470
+ */
471
+ async initRepo(options) {
472
+ const log = logger.child({ service: "repo", action: "init" });
473
+ const repoRoot = env.BASE_WORKSPACE_DIR;
474
+ log.info("Initializing repository", {
475
+ repoUrl: options.repoUrl.replace(/ghp_[a-zA-Z0-9]+/, "ghp_***"),
476
+ branch: options.branch,
477
+ force: options.force
478
+ });
479
+ const status = await this.getStatus();
480
+ if (status.initialized && !options.force) {
481
+ log.info("Repository already initialized");
482
+ return {
483
+ path: repoRoot,
484
+ branch: status.branch || env.DEFAULT_BRANCH,
485
+ headSha: status.headSha || "",
486
+ remote: status.remote
487
+ };
488
+ }
489
+ if (options.force && status.initialized) {
490
+ log.info("Force flag set, removing existing repo");
491
+ await rm(repoRoot, { recursive: true, force: true });
492
+ }
493
+ const parentDir = repoRoot.split("/").slice(0, -1).join("/");
494
+ await mkdir2(parentDir, { recursive: true });
495
+ await mkdir2(env.TRIALS_WORKSPACE_DIR, { recursive: true });
496
+ let cloneUrl = options.repoUrl;
497
+ if (options.githubToken) {
498
+ const urlWithoutProtocol = options.repoUrl.replace(/^https?:\/\//, "");
499
+ cloneUrl = `https://x-access-token:${options.githubToken}@${urlWithoutProtocol}`;
500
+ }
501
+ const branch = options.branch || env.DEFAULT_BRANCH;
502
+ log.info("Cloning repository", { branch });
503
+ await gitService.cloneRepo(cloneUrl, repoRoot, {
504
+ branch,
505
+ blobless: true
506
+ });
507
+ await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], repoRoot);
508
+ await gitService.exec(["config", "--local", "user.email", "origin-agent[bot]@users.noreply.github.com"], repoRoot);
509
+ await gitService.exec(["config", "--local", "safe.directory", repoRoot], repoRoot);
510
+ const cleanUrl = options.repoUrl.replace(/^https:\/\/[^@]+@/, "https://");
511
+ await gitService.exec(["remote", "set-url", "origin", cleanUrl], repoRoot);
512
+ if (options.githubToken) {
513
+ await this.updateCredentials(options.githubToken);
514
+ }
515
+ const headSha = await gitService.getHeadSha(repoRoot);
516
+ const remote = await gitService.getRemoteUrl("origin", repoRoot);
517
+ log.info("Repository initialized", { branch, headSha });
518
+ return { path: repoRoot, branch, headSha, remote };
519
+ }
520
+ /**
521
+ * Update/pull the repository
522
+ */
523
+ async updateRepo(options) {
524
+ const log = logger.child({ service: "repo", action: "update" });
525
+ const repoRoot = env.BASE_WORKSPACE_DIR;
526
+ const status = await this.getStatus();
527
+ if (!status.initialized) {
528
+ throw new Error("Repository not initialized");
529
+ }
530
+ if (options?.githubToken) {
531
+ await this.updateCredentials(options.githubToken);
532
+ }
533
+ log.info("Fetching from remote");
534
+ await gitService.fetch("origin", repoRoot);
535
+ if (options?.branch && options.branch !== status.branch) {
536
+ log.info("Checking out branch", { branch: options.branch });
537
+ await gitService.execOrThrow(["checkout", options.branch], repoRoot);
538
+ }
539
+ if (options?.pull) {
540
+ log.info("Pulling latest changes");
541
+ await gitService.exec(["pull", "--ff-only"], repoRoot);
542
+ }
543
+ const headSha = await gitService.getHeadSha(repoRoot);
544
+ const branch = await gitService.getCurrentBranch(repoRoot);
545
+ const remote = await gitService.getRemoteUrl("origin", repoRoot);
546
+ return { path: repoRoot, branch, headSha, remote };
547
+ }
548
+ /**
549
+ * Update git credentials
550
+ */
551
+ async updateCredentials(githubToken) {
552
+ const log = logger.child({ service: "repo", action: "updateCredentials" });
553
+ const repoRoot = env.BASE_WORKSPACE_DIR;
554
+ await gitService.exec(["config", "--global", "credential.helper", "store --file=/tmp/.git-credentials"], repoRoot);
555
+ await gitService.exec(["config", "--global", "core.askPass", ""], repoRoot);
556
+ const credentialsContent = `https://x-access-token:${githubToken}@github.com
557
+ `;
558
+ await writeFile("/tmp/.git-credentials", credentialsContent, { mode: 384 });
559
+ log.info("Credentials updated");
560
+ }
561
+ /**
562
+ * List branches
563
+ */
564
+ async listBranches() {
565
+ const repoRoot = env.BASE_WORKSPACE_DIR;
566
+ const status = await this.getStatus();
567
+ if (!status.initialized) {
568
+ return { local: [], remote: [] };
569
+ }
570
+ return gitService.listBranches(repoRoot);
571
+ }
572
+ };
573
+ var repoService = new RepoService();
574
+
575
+ // src/routes/repo.routes.ts
576
+ var repo = new Hono2();
577
+ var initRepoSchema = z.object({
578
+ repoUrl: z.string().url(),
579
+ branch: z.string().optional(),
580
+ githubToken: z.string().optional(),
581
+ force: z.boolean().default(false)
582
+ });
583
+ var updateRepoSchema = z.object({
584
+ branch: z.string().optional(),
585
+ pull: z.boolean().default(false),
586
+ githubToken: z.string().optional()
587
+ });
588
+ repo.get("/status", async (c) => {
589
+ try {
590
+ const status = await repoService.getStatus();
591
+ return c.json({ success: true, data: status });
592
+ } catch (err) {
593
+ const error = err;
594
+ logger.error("Failed to get repo status", { error: error.message });
595
+ return c.json({ success: false, error: { code: "INTERNAL_ERROR", message: error.message } }, 500);
596
+ }
597
+ });
598
+ repo.get("/branches", async (c) => {
599
+ try {
600
+ const branches = await repoService.listBranches();
601
+ return c.json({ success: true, data: branches });
602
+ } catch (err) {
603
+ const error = err;
604
+ logger.error("Failed to list branches", { error: error.message });
605
+ return c.json({ success: false, error: { code: "INTERNAL_ERROR", message: error.message } }, 500);
606
+ }
607
+ });
608
+ repo.post("/init", async (c) => {
609
+ const log = logger.child({ route: "repo/init" });
610
+ try {
611
+ const body = await c.req.json();
612
+ const parsed = initRepoSchema.safeParse(body);
613
+ if (!parsed.success) {
614
+ return c.json(
615
+ { success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
616
+ 400
617
+ );
618
+ }
619
+ log.info("Initializing repository", { repoUrl: parsed.data.repoUrl, branch: parsed.data.branch });
620
+ const result = await repoService.initRepo(parsed.data);
621
+ return c.json({ success: true, data: result });
622
+ } catch (err) {
623
+ const error = err;
624
+ log.error("Failed to initialize repo", { error: error.message });
625
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
626
+ }
627
+ });
628
+ repo.post("/update", async (c) => {
629
+ const log = logger.child({ route: "repo/update" });
630
+ try {
631
+ const body = await c.req.json().catch(() => ({}));
632
+ const parsed = updateRepoSchema.safeParse(body);
633
+ if (!parsed.success) {
634
+ return c.json(
635
+ { success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
636
+ 400
637
+ );
638
+ }
639
+ log.info("Updating repository", { branch: parsed.data.branch, pull: parsed.data.pull });
640
+ const result = await repoService.updateRepo(parsed.data);
641
+ return c.json({ success: true, data: result });
642
+ } catch (err) {
643
+ const error = err;
644
+ log.error("Failed to update repo", { error: error.message });
645
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
646
+ }
647
+ });
648
+
649
+ // src/routes/worktree.routes.ts
650
+ import { Hono as Hono3 } from "hono";
651
+ import { z as z2 } from "zod";
652
+
653
+ // src/services/worktree.service.ts
654
+ import { mkdir as mkdir3, rm as rm2, stat as stat3, readdir } from "fs/promises";
655
+ var WorktreeService = class {
656
+ /**
657
+ * Get the isolated workspace directory for a specific trial.
658
+ * Matches orgn branch-manager.ts getTrialWorkspaceDir()
659
+ */
660
+ getWorktreePath(trialId, trialBranch) {
661
+ const branch = trialBranch || `trial/${trialId}`;
662
+ const safeBranchName = branch.replace(/[^a-zA-Z0-9-_]/g, "-");
663
+ const shortTrialId = trialId.replace(/-/g, "").slice(0, 8);
664
+ if (!safeBranchName.endsWith(shortTrialId)) {
665
+ return `${env.TRIALS_WORKSPACE_DIR}/${safeBranchName}-${shortTrialId}`;
666
+ }
667
+ return `${env.TRIALS_WORKSPACE_DIR}/${safeBranchName}`;
668
+ }
669
+ /**
670
+ * Get the branch name for a trial
671
+ */
672
+ getBranchName(trialId) {
673
+ return `trial/${trialId}`;
674
+ }
675
+ /**
676
+ * Reset or create a worktree for a trial
677
+ */
678
+ async resetWorktree(trialId, options) {
679
+ const log = logger.child({ trialId, service: "worktree" });
680
+ const branchName = options.trialBranch || this.getBranchName(trialId);
681
+ const worktreePath = this.getWorktreePath(trialId, branchName);
682
+ const baseBranch = options.baseBranch || env.DEFAULT_BRANCH;
683
+ const baseRepoDir = env.BASE_WORKSPACE_DIR;
684
+ log.info("Starting worktree reset (v3 architecture)", {
685
+ worktreePath,
686
+ branchName,
687
+ baseBranch,
688
+ baseRepoDir
689
+ });
690
+ await mkdir3(env.TRIALS_WORKSPACE_DIR, { recursive: true });
691
+ try {
692
+ const stats = await stat3(worktreePath);
693
+ if (stats.isDirectory()) {
694
+ const gitFile = await stat3(`${worktreePath}/.git`).catch(() => null);
695
+ if (gitFile) {
696
+ log.info("Worktree already exists, fetching latest");
697
+ await gitService.fetch("origin", worktreePath).catch(() => {
698
+ });
699
+ const headSha2 = await gitService.getHeadSha(worktreePath);
700
+ return { worktreePath, branch: branchName, headSha: headSha2 };
701
+ }
702
+ }
703
+ } catch {
704
+ }
705
+ log.info("Fetching from remote");
706
+ await gitService.fetch("origin", baseRepoDir);
707
+ try {
708
+ await gitService.removeWorktree(worktreePath, true, baseRepoDir);
709
+ } catch {
710
+ }
711
+ await rm2(worktreePath, { recursive: true, force: true }).catch(() => {
712
+ });
713
+ const worktreeFolderName = worktreePath.split("/").pop() || branchName;
714
+ await rm2(`${baseRepoDir}/.git/worktrees/${worktreeFolderName}`, {
715
+ recursive: true,
716
+ force: true
717
+ }).catch(() => {
718
+ });
719
+ await gitService.pruneWorktrees(baseRepoDir);
720
+ const localBranchExists = await gitService.branchExists(branchName, baseRepoDir);
721
+ const remoteBranchExists = await gitService.branchExists(`origin/${branchName}`, baseRepoDir);
722
+ if (localBranchExists) {
723
+ log.info("Creating worktree from existing local branch", { branchName });
724
+ await gitService.execOrThrow(["worktree", "add", worktreePath, branchName], baseRepoDir);
725
+ } else if (remoteBranchExists) {
726
+ log.info("Creating worktree from remote branch", { branchName });
727
+ await gitService.exec(["fetch", "origin", `${branchName}:${branchName}`], baseRepoDir);
728
+ await gitService.execOrThrow(["worktree", "add", worktreePath, branchName], baseRepoDir);
729
+ } else {
730
+ const baseRef = `origin/${baseBranch}`;
731
+ log.info("Creating worktree with new branch from base", { baseRef, branchName });
732
+ await gitService.addWorktreeWithNewBranch(worktreePath, branchName, baseRef, baseRepoDir);
733
+ }
734
+ await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], worktreePath).catch(() => {
735
+ });
736
+ await gitService.exec(["config", "--local", "user.email", "origin-agent[bot]@users.noreply.github.com"], worktreePath).catch(() => {
737
+ });
738
+ await gitService.exec(["config", "--local", "safe.directory", worktreePath], worktreePath).catch(() => {
739
+ });
740
+ const headSha = await gitService.getHeadSha(worktreePath);
741
+ log.info("Worktree created successfully", { headSha });
742
+ return { worktreePath, branch: branchName, headSha };
743
+ }
744
+ /**
745
+ * Delete a worktree for a trial
746
+ */
747
+ async deleteWorktree(trialId, trialBranch) {
748
+ const log = logger.child({ trialId, service: "worktree" });
749
+ const branchName = trialBranch || this.getBranchName(trialId);
750
+ const worktreePath = this.getWorktreePath(trialId, branchName);
751
+ const baseRepoDir = env.BASE_WORKSPACE_DIR;
752
+ log.info("Deleting worktree", { worktreePath, branchName });
753
+ try {
754
+ await gitService.removeWorktree(worktreePath, true, baseRepoDir);
755
+ } catch {
756
+ log.warn("Worktree removal failed, cleaning up manually");
757
+ await rm2(worktreePath, { recursive: true, force: true }).catch(() => {
758
+ });
759
+ const worktreeFolderName = worktreePath.split("/").pop() || branchName;
760
+ await rm2(`${baseRepoDir}/.git/worktrees/${worktreeFolderName}`, {
761
+ recursive: true,
762
+ force: true
763
+ }).catch(() => {
764
+ });
765
+ await gitService.pruneWorktrees(baseRepoDir);
766
+ }
767
+ try {
768
+ await gitService.deleteBranch(branchName, true, baseRepoDir);
769
+ } catch {
770
+ }
771
+ log.info("Worktree deleted");
772
+ }
773
+ /**
774
+ * Get worktree status
775
+ */
776
+ async getWorktreeStatus(trialId, trialBranch) {
777
+ const branchName = trialBranch || this.getBranchName(trialId);
778
+ const worktreePath = this.getWorktreePath(trialId, branchName);
779
+ try {
780
+ const stats = await stat3(worktreePath);
781
+ if (!stats.isDirectory()) {
782
+ return {
783
+ exists: false,
784
+ worktreePath: null,
785
+ branch: null,
786
+ headSha: null,
787
+ hasChanges: false,
788
+ changedFiles: 0
789
+ };
790
+ }
791
+ const gitFile = await stat3(`${worktreePath}/.git`).catch(() => null);
792
+ if (!gitFile) {
793
+ return {
794
+ exists: false,
795
+ worktreePath: null,
796
+ branch: null,
797
+ headSha: null,
798
+ hasChanges: false,
799
+ changedFiles: 0
800
+ };
801
+ }
802
+ const headSha = await gitService.getHeadSha(worktreePath);
803
+ const branch = await gitService.getCurrentBranch(worktreePath);
804
+ const status = await gitService.getStatus(worktreePath);
805
+ return {
806
+ exists: true,
807
+ worktreePath,
808
+ branch,
809
+ headSha,
810
+ hasChanges: status.hasChanges,
811
+ changedFiles: status.files.length
812
+ };
813
+ } catch {
814
+ return {
815
+ exists: false,
816
+ worktreePath: null,
817
+ branch: null,
818
+ headSha: null,
819
+ hasChanges: false,
820
+ changedFiles: 0
821
+ };
822
+ }
823
+ }
824
+ /**
825
+ * Get worktree diff
826
+ */
827
+ async getWorktreeDiff(trialId, trialBranch) {
828
+ const branchName = trialBranch || this.getBranchName(trialId);
829
+ const worktreePath = this.getWorktreePath(trialId, branchName);
830
+ const status = await gitService.getStatus(worktreePath);
831
+ const diff = await gitService.getDiff(worktreePath);
832
+ return {
833
+ hasChanges: status.hasChanges,
834
+ changedFiles: status.files,
835
+ diff
836
+ };
837
+ }
838
+ /**
839
+ * Commit changes in worktree
840
+ */
841
+ async commitChanges(trialId, message, author, trialBranch) {
842
+ const branchName = trialBranch || this.getBranchName(trialId);
843
+ const worktreePath = this.getWorktreePath(trialId, branchName);
844
+ await gitService.addAll(worktreePath);
845
+ const commitSha = await gitService.commit(message, author, worktreePath);
846
+ const branch = await gitService.getCurrentBranch(worktreePath);
847
+ return { commitSha, branch };
848
+ }
849
+ /**
850
+ * Push branch
851
+ */
852
+ async pushBranch(trialId, remote = "origin", force = false, trialBranch) {
853
+ const branchName = trialBranch || this.getBranchName(trialId);
854
+ const worktreePath = this.getWorktreePath(trialId, branchName);
855
+ const branch = await gitService.getCurrentBranch(worktreePath);
856
+ await gitService.push(remote, branch, force, worktreePath);
857
+ return { branch, pushed: true };
858
+ }
859
+ /**
860
+ * Cleanup stale worktrees
861
+ */
862
+ async cleanupStaleWorktrees() {
863
+ const log = logger.child({ service: "worktree", action: "cleanup" });
864
+ let cleaned = 0;
865
+ const errors = [];
866
+ const cutoffTime = Date.now() - env.CLEANUP_AFTER_HOURS * 60 * 60 * 1e3;
867
+ try {
868
+ const entries = await readdir(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
869
+ for (const entry of entries) {
870
+ if (!entry.isDirectory()) continue;
871
+ const worktreePath = `${env.TRIALS_WORKSPACE_DIR}/${entry.name}`;
872
+ try {
873
+ const stats = await stat3(worktreePath);
874
+ if (stats.mtimeMs < cutoffTime) {
875
+ log.info("Cleaning up stale worktree", { path: worktreePath });
876
+ await rm2(worktreePath, { recursive: true, force: true });
877
+ cleaned++;
878
+ }
879
+ } catch (err) {
880
+ const errMsg = err instanceof Error ? err.message : String(err);
881
+ errors.push(`${worktreePath}: ${errMsg}`);
882
+ }
883
+ }
884
+ await gitService.pruneWorktrees(env.BASE_WORKSPACE_DIR);
885
+ } catch (err) {
886
+ const errMsg = err instanceof Error ? err.message : String(err);
887
+ errors.push(`readdir: ${errMsg}`);
888
+ }
889
+ log.info("Cleanup completed", { cleaned, errorCount: errors.length });
890
+ return { cleaned, errors };
891
+ }
892
+ };
893
+ var worktreeService = new WorktreeService();
894
+
895
+ // src/routes/worktree.routes.ts
896
+ var worktree = new Hono3();
897
+ var resetWorktreeSchema = z2.object({
898
+ baseBranch: z2.string().default("main"),
899
+ trialBranch: z2.string().optional(),
900
+ force: z2.boolean().default(false)
901
+ });
902
+ var commitSchema = z2.object({
903
+ message: z2.string().min(1),
904
+ author: z2.object({
905
+ name: z2.string(),
906
+ email: z2.string().email()
907
+ }).optional()
908
+ });
909
+ var pushSchema = z2.object({
910
+ remote: z2.string().default("origin"),
911
+ force: z2.boolean().default(false)
912
+ });
913
+ worktree.post("/trials/:trialId/worktree/reset", async (c) => {
914
+ const trialId = c.req.param("trialId");
915
+ const log = logger.child({ route: "worktree/reset", trialId });
916
+ try {
917
+ const body = await c.req.json().catch(() => ({}));
918
+ const parsed = resetWorktreeSchema.safeParse(body);
919
+ if (!parsed.success) {
920
+ return c.json(
921
+ { success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
922
+ 400
923
+ );
924
+ }
925
+ const input = parsed.data;
926
+ log.info("Creating/resetting worktree", {
927
+ baseBranch: input.baseBranch,
928
+ trialBranch: input.trialBranch,
929
+ force: input.force
930
+ });
931
+ const result = await worktreeService.resetWorktree(trialId, {
932
+ baseBranch: input.baseBranch,
933
+ trialBranch: input.trialBranch,
934
+ force: input.force
935
+ });
936
+ return c.json({ success: true, data: result });
937
+ } catch (err) {
938
+ const error = err;
939
+ log.error("Failed to reset worktree", { error: error.message });
940
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
941
+ }
942
+ });
943
+ worktree.delete("/trials/:trialId/worktree", async (c) => {
944
+ const trialId = c.req.param("trialId");
945
+ const log = logger.child({ route: "worktree/delete", trialId });
946
+ try {
947
+ log.info("Deleting worktree");
948
+ await worktreeService.deleteWorktree(trialId);
949
+ return c.json({ success: true, data: { deleted: true } });
950
+ } catch (err) {
951
+ const error = err;
952
+ log.error("Failed to delete worktree", { error: error.message });
953
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
954
+ }
955
+ });
956
+ worktree.get("/trials/:trialId/worktree/status", async (c) => {
957
+ const trialId = c.req.param("trialId");
958
+ const log = logger.child({ route: "worktree/status", trialId });
959
+ try {
960
+ const status = await worktreeService.getWorktreeStatus(trialId);
961
+ return c.json({ success: true, data: status });
962
+ } catch (err) {
963
+ const error = err;
964
+ log.error("Failed to get worktree status", { error: error.message });
965
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
966
+ }
967
+ });
968
+ worktree.get("/trials/:trialId/worktree/diff", async (c) => {
969
+ const trialId = c.req.param("trialId");
970
+ const log = logger.child({ route: "worktree/diff", trialId });
971
+ try {
972
+ const diff = await worktreeService.getWorktreeDiff(trialId);
973
+ return c.json({ success: true, data: diff });
974
+ } catch (err) {
975
+ const error = err;
976
+ log.error("Failed to get worktree diff", { error: error.message });
977
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
978
+ }
979
+ });
980
+ worktree.post("/trials/:trialId/worktree/commit", async (c) => {
981
+ const trialId = c.req.param("trialId");
982
+ const log = logger.child({ route: "worktree/commit", trialId });
983
+ try {
984
+ const body = await c.req.json();
985
+ const parsed = commitSchema.safeParse(body);
986
+ if (!parsed.success) {
987
+ return c.json(
988
+ { success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
989
+ 400
990
+ );
991
+ }
992
+ log.info("Committing changes", { message: parsed.data.message.slice(0, 50) });
993
+ const result = await worktreeService.commitChanges(
994
+ trialId,
995
+ parsed.data.message,
996
+ parsed.data.author
997
+ );
998
+ return c.json({ success: true, data: result });
999
+ } catch (err) {
1000
+ const error = err;
1001
+ log.error("Failed to commit changes", { error: error.message });
1002
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
1003
+ }
1004
+ });
1005
+ worktree.post("/trials/:trialId/worktree/push", async (c) => {
1006
+ const trialId = c.req.param("trialId");
1007
+ const log = logger.child({ route: "worktree/push", trialId });
1008
+ try {
1009
+ const body = await c.req.json().catch(() => ({}));
1010
+ const parsed = pushSchema.safeParse(body);
1011
+ if (!parsed.success) {
1012
+ return c.json(
1013
+ { success: false, error: { code: "VALIDATION_ERROR", message: parsed.error.message } },
1014
+ 400
1015
+ );
1016
+ }
1017
+ log.info("Pushing to remote", { remote: parsed.data.remote, force: parsed.data.force });
1018
+ const result = await worktreeService.pushBranch(
1019
+ trialId,
1020
+ parsed.data.remote,
1021
+ parsed.data.force
1022
+ );
1023
+ return c.json({ success: true, data: result });
1024
+ } catch (err) {
1025
+ const error = err;
1026
+ log.error("Failed to push", { error: error.message });
1027
+ return c.json({ success: false, error: { code: "GIT_ERROR", message: error.message } }, 500);
1028
+ }
1029
+ });
1030
+ worktree.post("/worktrees/cleanup", async (c) => {
1031
+ const log = logger.child({ route: "worktrees/cleanup" });
1032
+ try {
1033
+ log.info("Starting worktree cleanup");
1034
+ const result = await worktreeService.cleanupStaleWorktrees();
1035
+ return c.json({ success: true, data: result });
1036
+ } catch (err) {
1037
+ const error = err;
1038
+ log.error("Failed to cleanup worktrees", { error: error.message });
1039
+ return c.json({ success: false, error: { code: "INTERNAL_ERROR", message: error.message } }, 500);
1040
+ }
1041
+ });
1042
+
1043
+ // src/main.ts
1044
+ var app = new Hono4();
1045
+ app.use("*", secureHeaders());
1046
+ app.use(
1047
+ "*",
1048
+ cors({
1049
+ origin: "*",
1050
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
1051
+ allowHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
1052
+ exposeHeaders: ["X-Request-ID"],
1053
+ maxAge: 86400
1054
+ })
1055
+ );
1056
+ if (env.NODE_ENV === "development") {
1057
+ app.use("*", honoLogger());
1058
+ }
1059
+ app.use("*", async (c, next) => {
1060
+ const requestId = c.req.header("X-Request-ID") || crypto.randomUUID();
1061
+ c.res.headers.set("X-Request-ID", requestId);
1062
+ await next();
1063
+ });
1064
+ app.route("/health", health);
1065
+ app.get("/", (c) => {
1066
+ return c.json({
1067
+ service: "worktree-worker",
1068
+ version: "1.0.0",
1069
+ status: "ok",
1070
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1071
+ });
1072
+ });
1073
+ app.use("/v1/*", authMiddleware);
1074
+ app.route("/v1/repo", repo);
1075
+ app.route("/v1", worktree);
1076
+ app.onError((err, c) => {
1077
+ const requestId = c.req.header("X-Request-ID") || "unknown";
1078
+ logger.error("Unhandled error", {
1079
+ requestId,
1080
+ error: err.message,
1081
+ stack: err.stack,
1082
+ path: c.req.path,
1083
+ method: c.req.method
1084
+ });
1085
+ return c.json(
1086
+ {
1087
+ success: false,
1088
+ error: {
1089
+ code: "INTERNAL_ERROR",
1090
+ message: env.NODE_ENV === "development" ? err.message : "Internal server error",
1091
+ requestId
1092
+ }
1093
+ },
1094
+ 500
1095
+ );
1096
+ });
1097
+ app.notFound((c) => {
1098
+ return c.json(
1099
+ {
1100
+ success: false,
1101
+ error: {
1102
+ code: "NOT_FOUND",
1103
+ message: `Route ${c.req.method} ${c.req.path} not found`
1104
+ }
1105
+ },
1106
+ 404
1107
+ );
1108
+ });
1109
+ var port = env.PORT;
1110
+ logger.info(`Worktree Worker starting on port ${port}`, {
1111
+ port,
1112
+ baseWorkspaceDir: env.BASE_WORKSPACE_DIR,
1113
+ trialsWorkspaceDir: env.TRIALS_WORKSPACE_DIR
1114
+ });
1115
+ serve({
1116
+ fetch: app.fetch,
1117
+ port
1118
+ });
1119
+ export {
1120
+ app
1121
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@saeed42/worktree-worker",
3
+ "version": "1.0.0",
4
+ "description": "Git worktree management service for AI agent trials",
5
+ "type": "module",
6
+ "main": "dist/main.js",
7
+ "bin": {
8
+ "opencode-ai": "./dist/main.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsx watch src/main.ts",
13
+ "start": "node dist/main.js",
14
+ "typecheck": "tsc --noEmit",
15
+ "lint": "eslint src/",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "git",
20
+ "worktree",
21
+ "codesandbox",
22
+ "agent",
23
+ "trial"
24
+ ],
25
+ "author": "Origin",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@hono/node-server": "^1.13.7",
29
+ "hono": "^4.6.0",
30
+ "pino": "^9.5.0",
31
+ "pino-pretty": "^13.0.0",
32
+ "zod": "^3.24.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.0",
36
+ "tsup": "^8.3.0",
37
+ "tsx": "^4.19.0",
38
+ "typescript": "^5.7.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
51
+