git-workspace-service 0.1.0 → 0.3.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 +70 -0
- package/dist/index.cjs +243 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -2
- package/dist/index.d.ts +121 -2
- package/dist/index.js +243 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -258,6 +258,22 @@ var WorkspaceService = class {
|
|
|
258
258
|
* Provision a new workspace for a task
|
|
259
259
|
*/
|
|
260
260
|
async provision(config) {
|
|
261
|
+
const strategy = config.strategy || "clone";
|
|
262
|
+
if (strategy === "worktree") {
|
|
263
|
+
if (!config.parentWorkspace) {
|
|
264
|
+
throw new Error('parentWorkspace is required when strategy is "worktree"');
|
|
265
|
+
}
|
|
266
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
267
|
+
if (!parent) {
|
|
268
|
+
throw new Error(`Parent workspace not found: ${config.parentWorkspace}`);
|
|
269
|
+
}
|
|
270
|
+
if (parent.strategy !== "clone") {
|
|
271
|
+
throw new Error("Parent workspace must be a clone, not a worktree");
|
|
272
|
+
}
|
|
273
|
+
if (parent.repo !== config.repo) {
|
|
274
|
+
throw new Error("Worktree must be for the same repository as parent");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
261
277
|
const workspaceId = randomUUID();
|
|
262
278
|
this.log(
|
|
263
279
|
"info",
|
|
@@ -265,7 +281,8 @@ var WorkspaceService = class {
|
|
|
265
281
|
workspaceId,
|
|
266
282
|
repo: config.repo,
|
|
267
283
|
executionId: config.execution.id,
|
|
268
|
-
role: config.task.role
|
|
284
|
+
role: config.task.role,
|
|
285
|
+
strategy
|
|
269
286
|
},
|
|
270
287
|
"Provisioning workspace"
|
|
271
288
|
);
|
|
@@ -276,26 +293,31 @@ var WorkspaceService = class {
|
|
|
276
293
|
timestamp: /* @__PURE__ */ new Date()
|
|
277
294
|
});
|
|
278
295
|
const workspacePath = path.join(this.baseDir, workspaceId);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
296
|
+
let credential;
|
|
297
|
+
if (strategy === "worktree" && config.parentWorkspace) {
|
|
298
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
299
|
+
credential = parent.credential;
|
|
300
|
+
} else {
|
|
301
|
+
await fs3.mkdir(workspacePath, { recursive: true });
|
|
302
|
+
credential = await this.credentialService.getCredentials({
|
|
303
|
+
repo: config.repo,
|
|
304
|
+
access: "write",
|
|
305
|
+
context: {
|
|
306
|
+
executionId: config.execution.id,
|
|
307
|
+
taskId: config.task.id,
|
|
308
|
+
userId: config.user?.id,
|
|
309
|
+
reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
|
|
310
|
+
},
|
|
311
|
+
userProvided: config.userCredentials
|
|
312
|
+
});
|
|
313
|
+
await this.emitEvent({
|
|
314
|
+
type: "credential:granted",
|
|
315
|
+
workspaceId,
|
|
316
|
+
credentialId: credential.id,
|
|
284
317
|
executionId: config.execution.id,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
},
|
|
289
|
-
// Pass user-provided credentials if available
|
|
290
|
-
userProvided: config.userCredentials
|
|
291
|
-
});
|
|
292
|
-
await this.emitEvent({
|
|
293
|
-
type: "credential:granted",
|
|
294
|
-
workspaceId,
|
|
295
|
-
credentialId: credential.id,
|
|
296
|
-
executionId: config.execution.id,
|
|
297
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
298
|
-
});
|
|
318
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
319
|
+
});
|
|
320
|
+
}
|
|
299
321
|
const branchInfo = createBranchInfo(
|
|
300
322
|
{
|
|
301
323
|
executionId: config.execution.id,
|
|
@@ -312,21 +334,52 @@ var WorkspaceService = class {
|
|
|
312
334
|
branch: branchInfo,
|
|
313
335
|
credential,
|
|
314
336
|
provisionedAt: /* @__PURE__ */ new Date(),
|
|
315
|
-
status: "provisioning"
|
|
337
|
+
status: "provisioning",
|
|
338
|
+
strategy,
|
|
339
|
+
parentWorkspaceId: config.parentWorkspace,
|
|
340
|
+
onComplete: config.onComplete,
|
|
341
|
+
progress: {
|
|
342
|
+
phase: "initializing",
|
|
343
|
+
message: "Initializing workspace",
|
|
344
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
345
|
+
}
|
|
316
346
|
};
|
|
317
347
|
this.workspaces.set(workspaceId, workspace);
|
|
318
348
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
349
|
+
if (strategy === "clone") {
|
|
350
|
+
this.updateProgress(workspace, "cloning", "Cloning repository");
|
|
351
|
+
await this.cloneRepo(workspace, credential.token);
|
|
352
|
+
this.updateProgress(workspace, "creating_branch", "Creating branch");
|
|
353
|
+
await this.createBranch(workspace);
|
|
354
|
+
} else {
|
|
355
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
356
|
+
await this.addWorktreeFromParent(parent, workspace);
|
|
357
|
+
if (!parent.worktreeIds) {
|
|
358
|
+
parent.worktreeIds = [];
|
|
359
|
+
}
|
|
360
|
+
parent.worktreeIds.push(workspaceId);
|
|
361
|
+
this.workspaces.set(parent.id, parent);
|
|
362
|
+
await this.emitEvent({
|
|
363
|
+
type: "worktree:added",
|
|
364
|
+
workspaceId,
|
|
365
|
+
executionId: config.execution.id,
|
|
366
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
367
|
+
data: { parentWorkspaceId: parent.id }
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
this.updateProgress(workspace, "configuring", "Configuring git");
|
|
321
371
|
await this.configureGit(workspace);
|
|
322
372
|
workspace.status = "ready";
|
|
373
|
+
this.updateProgress(workspace, "ready", "Workspace ready");
|
|
323
374
|
this.workspaces.set(workspaceId, workspace);
|
|
375
|
+
await this.executeCompletionHook(workspace, "success");
|
|
324
376
|
this.log(
|
|
325
377
|
"info",
|
|
326
378
|
{
|
|
327
379
|
workspaceId,
|
|
328
380
|
path: workspacePath,
|
|
329
|
-
branch: branchInfo.name
|
|
381
|
+
branch: branchInfo.name,
|
|
382
|
+
strategy
|
|
330
383
|
},
|
|
331
384
|
"Workspace provisioned"
|
|
332
385
|
);
|
|
@@ -339,8 +392,9 @@ var WorkspaceService = class {
|
|
|
339
392
|
return workspace;
|
|
340
393
|
} catch (error) {
|
|
341
394
|
workspace.status = "error";
|
|
342
|
-
this.workspaces.set(workspaceId, workspace);
|
|
343
395
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
396
|
+
this.updateProgress(workspace, "error", errorMessage);
|
|
397
|
+
this.workspaces.set(workspaceId, workspace);
|
|
344
398
|
this.log("error", { workspaceId, error: errorMessage }, "Failed to provision workspace");
|
|
345
399
|
await this.emitEvent({
|
|
346
400
|
type: "workspace:error",
|
|
@@ -349,9 +403,54 @@ var WorkspaceService = class {
|
|
|
349
403
|
timestamp: /* @__PURE__ */ new Date(),
|
|
350
404
|
error: errorMessage
|
|
351
405
|
});
|
|
406
|
+
await this.executeCompletionHook(workspace, "error");
|
|
352
407
|
throw error;
|
|
353
408
|
}
|
|
354
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Add a worktree to an existing clone workspace (convenience method)
|
|
412
|
+
*/
|
|
413
|
+
async addWorktree(parentWorkspaceId, options) {
|
|
414
|
+
const parent = this.workspaces.get(parentWorkspaceId);
|
|
415
|
+
if (!parent) {
|
|
416
|
+
throw new Error(`Parent workspace not found: ${parentWorkspaceId}`);
|
|
417
|
+
}
|
|
418
|
+
return this.provision({
|
|
419
|
+
repo: parent.repo,
|
|
420
|
+
strategy: "worktree",
|
|
421
|
+
parentWorkspace: parentWorkspaceId,
|
|
422
|
+
branchStrategy: "feature_branch",
|
|
423
|
+
baseBranch: options.branch,
|
|
424
|
+
execution: options.execution,
|
|
425
|
+
task: options.task
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* List all worktrees for a parent workspace
|
|
430
|
+
*/
|
|
431
|
+
listWorktrees(parentWorkspaceId) {
|
|
432
|
+
const parent = this.workspaces.get(parentWorkspaceId);
|
|
433
|
+
if (!parent) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
if (!parent.worktreeIds || parent.worktreeIds.length === 0) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
return parent.worktreeIds.map((id) => this.workspaces.get(id)).filter((w) => w !== void 0);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Remove a worktree (alias for cleanup with worktree-specific handling)
|
|
443
|
+
*/
|
|
444
|
+
async removeWorktree(workspaceId) {
|
|
445
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
446
|
+
if (!workspace) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (workspace.strategy !== "worktree") {
|
|
450
|
+
throw new Error("Workspace is not a worktree. Use cleanup() instead.");
|
|
451
|
+
}
|
|
452
|
+
await this.cleanup(workspaceId);
|
|
453
|
+
}
|
|
355
454
|
/**
|
|
356
455
|
* Finalize a workspace (push, create PR, cleanup)
|
|
357
456
|
*/
|
|
@@ -431,21 +530,55 @@ var WorkspaceService = class {
|
|
|
431
530
|
if (!workspace) {
|
|
432
531
|
return;
|
|
433
532
|
}
|
|
434
|
-
this.log("info", { workspaceId }, "Cleaning up workspace");
|
|
533
|
+
this.log("info", { workspaceId, strategy: workspace.strategy }, "Cleaning up workspace");
|
|
534
|
+
if (workspace.strategy === "clone" && workspace.worktreeIds?.length) {
|
|
535
|
+
this.log(
|
|
536
|
+
"info",
|
|
537
|
+
{ workspaceId, worktreeCount: workspace.worktreeIds.length },
|
|
538
|
+
"Cleaning up child worktrees first"
|
|
539
|
+
);
|
|
540
|
+
for (const worktreeId of workspace.worktreeIds) {
|
|
541
|
+
await this.cleanup(worktreeId);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
435
544
|
try {
|
|
436
545
|
await cleanupCredentialFiles(workspace.path);
|
|
437
546
|
} catch (error) {
|
|
438
547
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
439
548
|
this.log("warn", { workspaceId, error: errorMessage }, "Failed to clean up credential files");
|
|
440
549
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
550
|
+
if (workspace.strategy === "worktree" && workspace.parentWorkspaceId) {
|
|
551
|
+
const parent = this.workspaces.get(workspace.parentWorkspaceId);
|
|
552
|
+
if (parent) {
|
|
553
|
+
try {
|
|
554
|
+
await this.execInDir(parent.path, `git worktree remove "${workspace.path}" --force`);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
557
|
+
this.log("warn", { workspaceId, error: errorMessage }, "Failed to remove worktree via git");
|
|
558
|
+
}
|
|
559
|
+
if (parent.worktreeIds) {
|
|
560
|
+
parent.worktreeIds = parent.worktreeIds.filter((id) => id !== workspaceId);
|
|
561
|
+
this.workspaces.set(parent.id, parent);
|
|
562
|
+
}
|
|
563
|
+
await this.emitEvent({
|
|
564
|
+
type: "worktree:removed",
|
|
565
|
+
workspaceId,
|
|
566
|
+
executionId: workspace.branch.executionId,
|
|
567
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
568
|
+
data: { parentWorkspaceId: parent.id }
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (workspace.strategy === "clone") {
|
|
573
|
+
await this.credentialService.revokeCredential(workspace.credential.id);
|
|
574
|
+
await this.emitEvent({
|
|
575
|
+
type: "credential:revoked",
|
|
576
|
+
workspaceId,
|
|
577
|
+
credentialId: workspace.credential.id,
|
|
578
|
+
executionId: workspace.branch.executionId,
|
|
579
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
580
|
+
});
|
|
581
|
+
}
|
|
449
582
|
try {
|
|
450
583
|
await fs3.rm(workspace.path, { recursive: true, force: true });
|
|
451
584
|
} catch (error) {
|
|
@@ -482,6 +615,16 @@ var WorkspaceService = class {
|
|
|
482
615
|
async createBranch(workspace) {
|
|
483
616
|
await this.execInDir(workspace.path, `git checkout -b ${workspace.branch.name}`);
|
|
484
617
|
}
|
|
618
|
+
async addWorktreeFromParent(parent, workspace) {
|
|
619
|
+
try {
|
|
620
|
+
await this.execInDir(parent.path, `git fetch origin ${workspace.branch.baseBranch}`);
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
await this.execInDir(
|
|
624
|
+
parent.path,
|
|
625
|
+
`git worktree add -b ${workspace.branch.name} "${workspace.path}" origin/${workspace.branch.baseBranch}`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
485
628
|
async configureGit(workspace) {
|
|
486
629
|
await this.execInDir(workspace.path, 'git config user.name "Workspace Agent"');
|
|
487
630
|
await this.execInDir(workspace.path, 'git config user.email "agent@workspace.local"');
|
|
@@ -597,6 +740,72 @@ var WorkspaceService = class {
|
|
|
597
740
|
}
|
|
598
741
|
}
|
|
599
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Update workspace progress
|
|
745
|
+
*/
|
|
746
|
+
updateProgress(workspace, phase, message) {
|
|
747
|
+
workspace.progress = {
|
|
748
|
+
phase,
|
|
749
|
+
message,
|
|
750
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
751
|
+
};
|
|
752
|
+
this.workspaces.set(workspace.id, workspace);
|
|
753
|
+
this.log(
|
|
754
|
+
"debug",
|
|
755
|
+
{ workspaceId: workspace.id, phase, message },
|
|
756
|
+
"Progress updated"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Execute completion hook if configured
|
|
761
|
+
*/
|
|
762
|
+
async executeCompletionHook(workspace, status) {
|
|
763
|
+
const hook = workspace.onComplete;
|
|
764
|
+
if (!hook) return;
|
|
765
|
+
if (status === "error" && hook.runOnError === false) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const env = {
|
|
769
|
+
...process.env,
|
|
770
|
+
WORKSPACE_ID: workspace.id,
|
|
771
|
+
REPO: workspace.repo,
|
|
772
|
+
BRANCH: workspace.branch.name,
|
|
773
|
+
STATUS: status,
|
|
774
|
+
WORKSPACE_PATH: workspace.path
|
|
775
|
+
};
|
|
776
|
+
if (hook.command) {
|
|
777
|
+
try {
|
|
778
|
+
this.log("info", { workspaceId: workspace.id, command: hook.command }, "Executing completion hook command");
|
|
779
|
+
await execAsync(hook.command, { env });
|
|
780
|
+
} catch (error) {
|
|
781
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
782
|
+
this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion hook command failed");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (hook.webhook) {
|
|
786
|
+
try {
|
|
787
|
+
this.log("info", { workspaceId: workspace.id, webhook: hook.webhook }, "Calling completion webhook");
|
|
788
|
+
const payload = {
|
|
789
|
+
workspaceId: workspace.id,
|
|
790
|
+
repo: workspace.repo,
|
|
791
|
+
branch: workspace.branch.name,
|
|
792
|
+
status,
|
|
793
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
794
|
+
};
|
|
795
|
+
await fetch(hook.webhook, {
|
|
796
|
+
method: "POST",
|
|
797
|
+
headers: {
|
|
798
|
+
"Content-Type": "application/json",
|
|
799
|
+
...hook.webhookHeaders
|
|
800
|
+
},
|
|
801
|
+
body: JSON.stringify(payload)
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
805
|
+
this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion webhook failed");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
600
809
|
};
|
|
601
810
|
|
|
602
811
|
// src/oauth/device-flow.ts
|