@saeed42/worktree-worker 1.0.0 → 1.1.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 +28 -7
- package/dist/main.js +155 -68
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @saeed42/worktree-worker
|
|
2
2
|
|
|
3
3
|
Git worktree management service for AI agent trials. Runs as an npm package in CodeSandbox or any Node.js environment.
|
|
4
4
|
|
|
5
|
+
## Security
|
|
6
|
+
|
|
7
|
+
**GitHub tokens are NEVER persisted to disk.** All authenticated operations require the token to be passed in the request body. This is critical for sandbox environments where credentials should not be stored.
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
# Install globally
|
|
9
|
-
npm install -g @
|
|
13
|
+
npm install -g @saeed42/worktree-worker
|
|
10
14
|
|
|
11
15
|
# Or add to your project
|
|
12
|
-
npm install @
|
|
16
|
+
npm install @saeed42/worktree-worker
|
|
13
17
|
```
|
|
14
18
|
|
|
15
19
|
## Quick Start
|
|
@@ -32,7 +36,7 @@ Add to your `package.json`:
|
|
|
32
36
|
```json
|
|
33
37
|
{
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@
|
|
39
|
+
"@saeed42/worktree-worker": "^1.0.0"
|
|
36
40
|
},
|
|
37
41
|
"scripts": {
|
|
38
42
|
"worktree-worker": "worktree-worker"
|
|
@@ -43,13 +47,13 @@ Add to your `package.json`:
|
|
|
43
47
|
Or run directly:
|
|
44
48
|
|
|
45
49
|
```bash
|
|
46
|
-
npx @
|
|
50
|
+
npx @saeed42/worktree-worker
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
### Run Programmatically
|
|
50
54
|
|
|
51
55
|
```typescript
|
|
52
|
-
import { app } from '@
|
|
56
|
+
import { app } from '@saeed42/worktree-worker';
|
|
53
57
|
|
|
54
58
|
// The app is a Hono instance
|
|
55
59
|
// You can extend it or use it as middleware
|
|
@@ -133,10 +137,13 @@ curl -X POST http://localhost:8787/v1/trials/abc123/worktree/reset \
|
|
|
133
137
|
-H "Content-Type: application/json" \
|
|
134
138
|
-d '{
|
|
135
139
|
"baseBranch": "main",
|
|
136
|
-
"trialBranch": "feature/my-feature"
|
|
140
|
+
"trialBranch": "feature/my-feature",
|
|
141
|
+
"githubToken": "ghp_xxx"
|
|
137
142
|
}'
|
|
138
143
|
```
|
|
139
144
|
|
|
145
|
+
> **Note:** `githubToken` is required for private repositories. The token is used inline and **never stored on disk**.
|
|
146
|
+
|
|
140
147
|
Response:
|
|
141
148
|
|
|
142
149
|
```json
|
|
@@ -165,6 +172,20 @@ curl -X POST http://localhost:8787/v1/trials/abc123/worktree/commit \
|
|
|
165
172
|
}'
|
|
166
173
|
```
|
|
167
174
|
|
|
175
|
+
### Push Changes
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
curl -X POST http://localhost:8787/v1/trials/abc123/worktree/push \
|
|
179
|
+
-H "Authorization: Bearer your-token" \
|
|
180
|
+
-H "Content-Type: application/json" \
|
|
181
|
+
-d '{
|
|
182
|
+
"githubToken": "ghp_xxx",
|
|
183
|
+
"force": false
|
|
184
|
+
}'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Note:** `githubToken` is **required** for push operations. The token is used inline and **never stored on disk**.
|
|
188
|
+
|
|
168
189
|
## Development
|
|
169
190
|
|
|
170
191
|
```bash
|
package/dist/main.js
CHANGED
|
@@ -188,13 +188,36 @@ var GitService = class {
|
|
|
188
188
|
return result.stdout.trim();
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
|
-
*
|
|
191
|
+
* Build authenticated URL (token is NEVER persisted)
|
|
192
192
|
*/
|
|
193
|
-
|
|
193
|
+
buildAuthUrl(repoUrl, githubToken) {
|
|
194
|
+
if (!githubToken) return repoUrl;
|
|
195
|
+
const urlWithoutProtocol = repoUrl.replace(/^https?:\/\//, "");
|
|
196
|
+
return `https://x-access-token:${githubToken}@${urlWithoutProtocol}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get remote URL and inject auth if token provided
|
|
200
|
+
*/
|
|
201
|
+
async getAuthenticatedRemoteUrl(remote, cwd, githubToken) {
|
|
202
|
+
const url = await this.getRemoteUrl(remote, cwd);
|
|
203
|
+
if (!url) return null;
|
|
204
|
+
return githubToken ? this.buildAuthUrl(url, githubToken) : url;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Fetch from remote (token used inline, never persisted)
|
|
208
|
+
*/
|
|
209
|
+
async fetch(remote = "origin", cwd, auth) {
|
|
210
|
+
if (auth?.githubToken) {
|
|
211
|
+
const authUrl = await this.getAuthenticatedRemoteUrl(remote, cwd || env.BASE_WORKSPACE_DIR, auth.githubToken);
|
|
212
|
+
if (authUrl) {
|
|
213
|
+
await this.execOrThrow(["fetch", authUrl], cwd);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
194
217
|
await this.execOrThrow(["fetch", remote], cwd);
|
|
195
218
|
}
|
|
196
219
|
/**
|
|
197
|
-
* Clone a repository
|
|
220
|
+
* Clone a repository (token used inline, never persisted)
|
|
198
221
|
*/
|
|
199
222
|
async cloneRepo(repoUrl, targetDir, options) {
|
|
200
223
|
const args = ["clone"];
|
|
@@ -206,7 +229,8 @@ var GitService = class {
|
|
|
206
229
|
if (options?.branch) {
|
|
207
230
|
args.push("--branch", options.branch);
|
|
208
231
|
}
|
|
209
|
-
|
|
232
|
+
const cloneUrl = options?.githubToken ? this.buildAuthUrl(repoUrl, options.githubToken) : repoUrl;
|
|
233
|
+
args.push(cloneUrl, targetDir);
|
|
210
234
|
const parentDir = targetDir.split("/").slice(0, -1).join("/") || "/";
|
|
211
235
|
await this.execOrThrow(args, parentDir);
|
|
212
236
|
}
|
|
@@ -329,9 +353,18 @@ var GitService = class {
|
|
|
329
353
|
return this.getHeadSha(cwd);
|
|
330
354
|
}
|
|
331
355
|
/**
|
|
332
|
-
* Push
|
|
356
|
+
* Push (token used inline, never persisted)
|
|
333
357
|
*/
|
|
334
|
-
async push(remote, branch, force = false, cwd) {
|
|
358
|
+
async push(remote, branch, force = false, cwd, auth) {
|
|
359
|
+
if (auth?.githubToken) {
|
|
360
|
+
const authUrl = await this.getAuthenticatedRemoteUrl(remote, cwd || env.BASE_WORKSPACE_DIR, auth.githubToken);
|
|
361
|
+
if (authUrl) {
|
|
362
|
+
const args2 = ["push", authUrl, branch];
|
|
363
|
+
if (force) args2.push("--force");
|
|
364
|
+
await this.execOrThrow(args2, cwd);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
335
368
|
const args = ["push", remote, branch];
|
|
336
369
|
if (force) args.push("--force");
|
|
337
370
|
await this.execOrThrow(args, cwd);
|
|
@@ -417,7 +450,7 @@ import { Hono as Hono2 } from "hono";
|
|
|
417
450
|
import { z } from "zod";
|
|
418
451
|
|
|
419
452
|
// src/services/repo.service.ts
|
|
420
|
-
import { mkdir as mkdir2, rm, stat as stat2
|
|
453
|
+
import { mkdir as mkdir2, readdir, rm, stat as stat2 } from "fs/promises";
|
|
421
454
|
var RepoService = class {
|
|
422
455
|
/**
|
|
423
456
|
* Check if the repository is initialized
|
|
@@ -467,71 +500,113 @@ var RepoService = class {
|
|
|
467
500
|
}
|
|
468
501
|
/**
|
|
469
502
|
* Initialize/clone the repository
|
|
503
|
+
* SECURITY: githubToken is used inline and NEVER persisted
|
|
504
|
+
*
|
|
505
|
+
* Smart init logic:
|
|
506
|
+
* - No repo exists → Clone it
|
|
507
|
+
* - Repo exists, same URL → Fetch latest (idempotent)
|
|
508
|
+
* - Repo exists, different URL, force=false → Error
|
|
509
|
+
* - Repo exists, different URL, force=true → Clean all worktrees → Delete repo → Re-clone
|
|
470
510
|
*/
|
|
471
511
|
async initRepo(options) {
|
|
472
512
|
const log = logger.child({ service: "repo", action: "init" });
|
|
473
513
|
const repoRoot = env.BASE_WORKSPACE_DIR;
|
|
514
|
+
const auth = options.githubToken ? { githubToken: options.githubToken } : void 0;
|
|
515
|
+
const normalizeUrl = (url) => {
|
|
516
|
+
return url.replace(/^https:\/\/[^@]+@/, "https://").replace(/\.git$/, "").replace(/\/$/, "").toLowerCase();
|
|
517
|
+
};
|
|
518
|
+
const requestedUrl = normalizeUrl(options.repoUrl);
|
|
474
519
|
log.info("Initializing repository", {
|
|
475
520
|
repoUrl: options.repoUrl.replace(/ghp_[a-zA-Z0-9]+/, "ghp_***"),
|
|
476
521
|
branch: options.branch,
|
|
477
|
-
force: options.force
|
|
522
|
+
force: options.force,
|
|
523
|
+
hasToken: !!options.githubToken
|
|
478
524
|
});
|
|
479
525
|
const status = await this.getStatus();
|
|
480
|
-
if (status.initialized
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
526
|
+
if (status.initialized) {
|
|
527
|
+
const currentUrl = normalizeUrl(status.remote || "");
|
|
528
|
+
const isSameRepo = currentUrl === requestedUrl;
|
|
529
|
+
if (isSameRepo) {
|
|
530
|
+
log.info("Repository already initialized with same URL, fetching latest");
|
|
531
|
+
await gitService.fetch("origin", repoRoot, auth);
|
|
532
|
+
const targetBranch = options.branch || env.DEFAULT_BRANCH;
|
|
533
|
+
const currentBranch = status.branch || "";
|
|
534
|
+
if (targetBranch !== currentBranch) {
|
|
535
|
+
log.info("Switching to requested branch", { from: currentBranch, to: targetBranch });
|
|
536
|
+
const checkoutResult = await gitService.exec(["checkout", targetBranch], repoRoot);
|
|
537
|
+
if (checkoutResult.code !== 0) {
|
|
538
|
+
await gitService.exec(["checkout", "-b", targetBranch, `origin/${targetBranch}`], repoRoot);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const headSha2 = await gitService.getHeadSha(repoRoot);
|
|
542
|
+
const branch2 = await gitService.getCurrentBranch(repoRoot);
|
|
543
|
+
return { path: repoRoot, branch: branch2, headSha: headSha2, remote: status.remote };
|
|
544
|
+
}
|
|
545
|
+
if (!options.force) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`Repository already initialized with different URL. Current: ${currentUrl}, Requested: ${requestedUrl}. Use force=true to re-initialize (this will delete all worktrees).`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
log.warn("Force re-init: cleaning all worktrees and repo");
|
|
551
|
+
await this.cleanAllWorktrees();
|
|
491
552
|
await rm(repoRoot, { recursive: true, force: true });
|
|
492
553
|
}
|
|
493
554
|
const parentDir = repoRoot.split("/").slice(0, -1).join("/");
|
|
494
555
|
await mkdir2(parentDir, { recursive: true });
|
|
495
556
|
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
557
|
const branch = options.branch || env.DEFAULT_BRANCH;
|
|
502
558
|
log.info("Cloning repository", { branch });
|
|
503
|
-
await gitService.cloneRepo(
|
|
559
|
+
await gitService.cloneRepo(options.repoUrl, repoRoot, {
|
|
504
560
|
branch,
|
|
505
|
-
blobless: true
|
|
561
|
+
blobless: true,
|
|
562
|
+
githubToken: options.githubToken
|
|
506
563
|
});
|
|
507
564
|
await gitService.exec(["config", "--local", "user.name", "origin-agent[bot]"], repoRoot);
|
|
508
565
|
await gitService.exec(["config", "--local", "user.email", "origin-agent[bot]@users.noreply.github.com"], repoRoot);
|
|
509
566
|
await gitService.exec(["config", "--local", "safe.directory", repoRoot], repoRoot);
|
|
510
567
|
const cleanUrl = options.repoUrl.replace(/^https:\/\/[^@]+@/, "https://");
|
|
511
568
|
await gitService.exec(["remote", "set-url", "origin", cleanUrl], repoRoot);
|
|
512
|
-
if (options.githubToken) {
|
|
513
|
-
await this.updateCredentials(options.githubToken);
|
|
514
|
-
}
|
|
515
569
|
const headSha = await gitService.getHeadSha(repoRoot);
|
|
516
570
|
const remote = await gitService.getRemoteUrl("origin", repoRoot);
|
|
517
571
|
log.info("Repository initialized", { branch, headSha });
|
|
518
572
|
return { path: repoRoot, branch, headSha, remote };
|
|
519
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Clean all worktrees (used before force re-init)
|
|
576
|
+
*/
|
|
577
|
+
async cleanAllWorktrees() {
|
|
578
|
+
const log = logger.child({ service: "repo", action: "cleanAllWorktrees" });
|
|
579
|
+
try {
|
|
580
|
+
const entries = await readdir(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
if (entry.isDirectory()) {
|
|
583
|
+
const worktreePath = `${env.TRIALS_WORKSPACE_DIR}/${entry.name}`;
|
|
584
|
+
log.info("Removing worktree", { path: worktreePath });
|
|
585
|
+
await rm(worktreePath, { recursive: true, force: true });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await gitService.pruneWorktrees(env.BASE_WORKSPACE_DIR).catch(() => {
|
|
589
|
+
});
|
|
590
|
+
log.info("All worktrees cleaned");
|
|
591
|
+
} catch (err) {
|
|
592
|
+
log.debug("No worktrees to clean", { error: err.message });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
520
595
|
/**
|
|
521
596
|
* Update/pull the repository
|
|
597
|
+
* SECURITY: githubToken is used inline and NEVER persisted
|
|
522
598
|
*/
|
|
523
599
|
async updateRepo(options) {
|
|
524
600
|
const log = logger.child({ service: "repo", action: "update" });
|
|
525
601
|
const repoRoot = env.BASE_WORKSPACE_DIR;
|
|
602
|
+
const auth = options?.githubToken ? { githubToken: options.githubToken } : void 0;
|
|
526
603
|
const status = await this.getStatus();
|
|
527
604
|
if (!status.initialized) {
|
|
528
605
|
throw new Error("Repository not initialized");
|
|
529
606
|
}
|
|
530
|
-
|
|
531
|
-
await this.updateCredentials(options.githubToken);
|
|
532
|
-
}
|
|
607
|
+
log.info("Updating repository", { hasToken: !!options?.githubToken });
|
|
533
608
|
log.info("Fetching from remote");
|
|
534
|
-
await gitService.fetch("origin", repoRoot);
|
|
609
|
+
await gitService.fetch("origin", repoRoot, auth);
|
|
535
610
|
if (options?.branch && options.branch !== status.branch) {
|
|
536
611
|
log.info("Checking out branch", { branch: options.branch });
|
|
537
612
|
await gitService.execOrThrow(["checkout", options.branch], repoRoot);
|
|
@@ -545,19 +620,6 @@ var RepoService = class {
|
|
|
545
620
|
const remote = await gitService.getRemoteUrl("origin", repoRoot);
|
|
546
621
|
return { path: repoRoot, branch, headSha, remote };
|
|
547
622
|
}
|
|
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
623
|
/**
|
|
562
624
|
* List branches
|
|
563
625
|
*/
|
|
@@ -651,7 +713,7 @@ import { Hono as Hono3 } from "hono";
|
|
|
651
713
|
import { z as z2 } from "zod";
|
|
652
714
|
|
|
653
715
|
// src/services/worktree.service.ts
|
|
654
|
-
import { mkdir as mkdir3, rm as rm2, stat as stat3, readdir } from "fs/promises";
|
|
716
|
+
import { mkdir as mkdir3, rm as rm2, stat as stat3, readdir as readdir2 } from "fs/promises";
|
|
655
717
|
var WorktreeService = class {
|
|
656
718
|
/**
|
|
657
719
|
* Get the isolated workspace directory for a specific trial.
|
|
@@ -674,6 +736,7 @@ var WorktreeService = class {
|
|
|
674
736
|
}
|
|
675
737
|
/**
|
|
676
738
|
* Reset or create a worktree for a trial
|
|
739
|
+
* SECURITY: githubToken is used inline and NEVER persisted
|
|
677
740
|
*/
|
|
678
741
|
async resetWorktree(trialId, options) {
|
|
679
742
|
const log = logger.child({ trialId, service: "worktree" });
|
|
@@ -681,11 +744,13 @@ var WorktreeService = class {
|
|
|
681
744
|
const worktreePath = this.getWorktreePath(trialId, branchName);
|
|
682
745
|
const baseBranch = options.baseBranch || env.DEFAULT_BRANCH;
|
|
683
746
|
const baseRepoDir = env.BASE_WORKSPACE_DIR;
|
|
747
|
+
const auth = options.githubToken ? { githubToken: options.githubToken } : void 0;
|
|
684
748
|
log.info("Starting worktree reset (v3 architecture)", {
|
|
685
749
|
worktreePath,
|
|
686
750
|
branchName,
|
|
687
751
|
baseBranch,
|
|
688
|
-
baseRepoDir
|
|
752
|
+
baseRepoDir,
|
|
753
|
+
hasToken: !!options.githubToken
|
|
689
754
|
});
|
|
690
755
|
await mkdir3(env.TRIALS_WORKSPACE_DIR, { recursive: true });
|
|
691
756
|
try {
|
|
@@ -694,7 +759,7 @@ var WorktreeService = class {
|
|
|
694
759
|
const gitFile = await stat3(`${worktreePath}/.git`).catch(() => null);
|
|
695
760
|
if (gitFile) {
|
|
696
761
|
log.info("Worktree already exists, fetching latest");
|
|
697
|
-
await gitService.fetch("origin", worktreePath).catch(() => {
|
|
762
|
+
await gitService.fetch("origin", worktreePath, auth).catch(() => {
|
|
698
763
|
});
|
|
699
764
|
const headSha2 = await gitService.getHeadSha(worktreePath);
|
|
700
765
|
return { worktreePath, branch: branchName, headSha: headSha2 };
|
|
@@ -703,7 +768,7 @@ var WorktreeService = class {
|
|
|
703
768
|
} catch {
|
|
704
769
|
}
|
|
705
770
|
log.info("Fetching from remote");
|
|
706
|
-
await gitService.fetch("origin", baseRepoDir);
|
|
771
|
+
await gitService.fetch("origin", baseRepoDir, auth);
|
|
707
772
|
try {
|
|
708
773
|
await gitService.removeWorktree(worktreePath, true, baseRepoDir);
|
|
709
774
|
} catch {
|
|
@@ -848,12 +913,14 @@ var WorktreeService = class {
|
|
|
848
913
|
}
|
|
849
914
|
/**
|
|
850
915
|
* Push branch
|
|
916
|
+
* SECURITY: githubToken is used inline and NEVER persisted
|
|
851
917
|
*/
|
|
852
|
-
async pushBranch(trialId, remote = "origin", force = false, trialBranch) {
|
|
918
|
+
async pushBranch(trialId, remote = "origin", force = false, trialBranch, githubToken) {
|
|
853
919
|
const branchName = trialBranch || this.getBranchName(trialId);
|
|
854
920
|
const worktreePath = this.getWorktreePath(trialId, branchName);
|
|
921
|
+
const auth = githubToken ? { githubToken } : void 0;
|
|
855
922
|
const branch = await gitService.getCurrentBranch(worktreePath);
|
|
856
|
-
await gitService.push(remote, branch, force, worktreePath);
|
|
923
|
+
await gitService.push(remote, branch, force, worktreePath, auth);
|
|
857
924
|
return { branch, pushed: true };
|
|
858
925
|
}
|
|
859
926
|
/**
|
|
@@ -865,7 +932,7 @@ var WorktreeService = class {
|
|
|
865
932
|
const errors = [];
|
|
866
933
|
const cutoffTime = Date.now() - env.CLEANUP_AFTER_HOURS * 60 * 60 * 1e3;
|
|
867
934
|
try {
|
|
868
|
-
const entries = await
|
|
935
|
+
const entries = await readdir2(env.TRIALS_WORKSPACE_DIR, { withFileTypes: true });
|
|
869
936
|
for (const entry of entries) {
|
|
870
937
|
if (!entry.isDirectory()) continue;
|
|
871
938
|
const worktreePath = `${env.TRIALS_WORKSPACE_DIR}/${entry.name}`;
|
|
@@ -897,18 +964,23 @@ var worktree = new Hono3();
|
|
|
897
964
|
var resetWorktreeSchema = z2.object({
|
|
898
965
|
baseBranch: z2.string().default("main"),
|
|
899
966
|
trialBranch: z2.string().optional(),
|
|
900
|
-
force: z2.boolean().default(false)
|
|
967
|
+
force: z2.boolean().default(false),
|
|
968
|
+
githubToken: z2.string().optional()
|
|
969
|
+
// Required for private repos
|
|
901
970
|
});
|
|
902
971
|
var commitSchema = z2.object({
|
|
903
972
|
message: z2.string().min(1),
|
|
904
973
|
author: z2.object({
|
|
905
974
|
name: z2.string(),
|
|
906
975
|
email: z2.string().email()
|
|
907
|
-
}).optional()
|
|
976
|
+
}).optional(),
|
|
977
|
+
trialBranch: z2.string().optional()
|
|
908
978
|
});
|
|
909
979
|
var pushSchema = z2.object({
|
|
910
980
|
remote: z2.string().default("origin"),
|
|
911
|
-
force: z2.boolean().default(false)
|
|
981
|
+
force: z2.boolean().default(false),
|
|
982
|
+
githubToken: z2.string().min(1, "GitHub token is required for push operations"),
|
|
983
|
+
trialBranch: z2.string().optional()
|
|
912
984
|
});
|
|
913
985
|
worktree.post("/trials/:trialId/worktree/reset", async (c) => {
|
|
914
986
|
const trialId = c.req.param("trialId");
|
|
@@ -926,12 +998,14 @@ worktree.post("/trials/:trialId/worktree/reset", async (c) => {
|
|
|
926
998
|
log.info("Creating/resetting worktree", {
|
|
927
999
|
baseBranch: input.baseBranch,
|
|
928
1000
|
trialBranch: input.trialBranch,
|
|
929
|
-
force: input.force
|
|
1001
|
+
force: input.force,
|
|
1002
|
+
hasToken: !!input.githubToken
|
|
930
1003
|
});
|
|
931
1004
|
const result = await worktreeService.resetWorktree(trialId, {
|
|
932
1005
|
baseBranch: input.baseBranch,
|
|
933
1006
|
trialBranch: input.trialBranch,
|
|
934
|
-
force: input.force
|
|
1007
|
+
force: input.force,
|
|
1008
|
+
githubToken: input.githubToken
|
|
935
1009
|
});
|
|
936
1010
|
return c.json({ success: true, data: result });
|
|
937
1011
|
} catch (err) {
|
|
@@ -942,10 +1016,11 @@ worktree.post("/trials/:trialId/worktree/reset", async (c) => {
|
|
|
942
1016
|
});
|
|
943
1017
|
worktree.delete("/trials/:trialId/worktree", async (c) => {
|
|
944
1018
|
const trialId = c.req.param("trialId");
|
|
945
|
-
const
|
|
1019
|
+
const trialBranch = c.req.query("trialBranch");
|
|
1020
|
+
const log = logger.child({ route: "worktree/delete", trialId, trialBranch });
|
|
946
1021
|
try {
|
|
947
1022
|
log.info("Deleting worktree");
|
|
948
|
-
await worktreeService.deleteWorktree(trialId);
|
|
1023
|
+
await worktreeService.deleteWorktree(trialId, trialBranch);
|
|
949
1024
|
return c.json({ success: true, data: { deleted: true } });
|
|
950
1025
|
} catch (err) {
|
|
951
1026
|
const error = err;
|
|
@@ -955,9 +1030,10 @@ worktree.delete("/trials/:trialId/worktree", async (c) => {
|
|
|
955
1030
|
});
|
|
956
1031
|
worktree.get("/trials/:trialId/worktree/status", async (c) => {
|
|
957
1032
|
const trialId = c.req.param("trialId");
|
|
958
|
-
const
|
|
1033
|
+
const trialBranch = c.req.query("trialBranch");
|
|
1034
|
+
const log = logger.child({ route: "worktree/status", trialId, trialBranch });
|
|
959
1035
|
try {
|
|
960
|
-
const status = await worktreeService.getWorktreeStatus(trialId);
|
|
1036
|
+
const status = await worktreeService.getWorktreeStatus(trialId, trialBranch);
|
|
961
1037
|
return c.json({ success: true, data: status });
|
|
962
1038
|
} catch (err) {
|
|
963
1039
|
const error = err;
|
|
@@ -967,9 +1043,10 @@ worktree.get("/trials/:trialId/worktree/status", async (c) => {
|
|
|
967
1043
|
});
|
|
968
1044
|
worktree.get("/trials/:trialId/worktree/diff", async (c) => {
|
|
969
1045
|
const trialId = c.req.param("trialId");
|
|
970
|
-
const
|
|
1046
|
+
const trialBranch = c.req.query("trialBranch");
|
|
1047
|
+
const log = logger.child({ route: "worktree/diff", trialId, trialBranch });
|
|
971
1048
|
try {
|
|
972
|
-
const diff = await worktreeService.getWorktreeDiff(trialId);
|
|
1049
|
+
const diff = await worktreeService.getWorktreeDiff(trialId, trialBranch);
|
|
973
1050
|
return c.json({ success: true, data: diff });
|
|
974
1051
|
} catch (err) {
|
|
975
1052
|
const error = err;
|
|
@@ -989,11 +1066,15 @@ worktree.post("/trials/:trialId/worktree/commit", async (c) => {
|
|
|
989
1066
|
400
|
|
990
1067
|
);
|
|
991
1068
|
}
|
|
992
|
-
log.info("Committing changes", {
|
|
1069
|
+
log.info("Committing changes", {
|
|
1070
|
+
message: parsed.data.message.slice(0, 50),
|
|
1071
|
+
trialBranch: parsed.data.trialBranch
|
|
1072
|
+
});
|
|
993
1073
|
const result = await worktreeService.commitChanges(
|
|
994
1074
|
trialId,
|
|
995
1075
|
parsed.data.message,
|
|
996
|
-
parsed.data.author
|
|
1076
|
+
parsed.data.author,
|
|
1077
|
+
parsed.data.trialBranch
|
|
997
1078
|
);
|
|
998
1079
|
return c.json({ success: true, data: result });
|
|
999
1080
|
} catch (err) {
|
|
@@ -1014,11 +1095,17 @@ worktree.post("/trials/:trialId/worktree/push", async (c) => {
|
|
|
1014
1095
|
400
|
|
1015
1096
|
);
|
|
1016
1097
|
}
|
|
1017
|
-
log.info("Pushing to remote", {
|
|
1098
|
+
log.info("Pushing to remote", {
|
|
1099
|
+
remote: parsed.data.remote,
|
|
1100
|
+
force: parsed.data.force,
|
|
1101
|
+
trialBranch: parsed.data.trialBranch
|
|
1102
|
+
});
|
|
1018
1103
|
const result = await worktreeService.pushBranch(
|
|
1019
1104
|
trialId,
|
|
1020
1105
|
parsed.data.remote,
|
|
1021
|
-
parsed.data.force
|
|
1106
|
+
parsed.data.force,
|
|
1107
|
+
parsed.data.trialBranch,
|
|
1108
|
+
parsed.data.githubToken
|
|
1022
1109
|
);
|
|
1023
1110
|
return c.json({ success: true, data: result });
|
|
1024
1111
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saeed42/worktree-worker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Git worktree management service for AI agent trials",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"worktree-worker": "./dist/main.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsup",
|