git-workspace-service 0.2.0 → 0.3.1

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/dist/index.cjs CHANGED
@@ -324,24 +324,28 @@ var WorkspaceService = class {
324
324
  credential = parent.credential;
325
325
  } else {
326
326
  await fs3__namespace.mkdir(workspacePath, { recursive: true });
327
- credential = await this.credentialService.getCredentials({
328
- repo: config.repo,
329
- access: "write",
330
- context: {
327
+ if (config.userCredentials) {
328
+ credential = await this.credentialService.getCredentials({
329
+ repo: config.repo,
330
+ access: "write",
331
+ context: {
332
+ executionId: config.execution.id,
333
+ taskId: config.task.id,
334
+ userId: config.user?.id,
335
+ reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
336
+ },
337
+ userProvided: config.userCredentials
338
+ });
339
+ }
340
+ if (credential) {
341
+ await this.emitEvent({
342
+ type: "credential:granted",
343
+ workspaceId,
344
+ credentialId: credential.id,
331
345
  executionId: config.execution.id,
332
- taskId: config.task.id,
333
- userId: config.user?.id,
334
- reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
335
- },
336
- userProvided: config.userCredentials
337
- });
338
- await this.emitEvent({
339
- type: "credential:granted",
340
- workspaceId,
341
- credentialId: credential.id,
342
- executionId: config.execution.id,
343
- timestamp: /* @__PURE__ */ new Date()
344
- });
346
+ timestamp: /* @__PURE__ */ new Date()
347
+ });
348
+ }
345
349
  }
346
350
  const branchInfo = createBranchInfo(
347
351
  {
@@ -358,15 +362,55 @@ var WorkspaceService = class {
358
362
  repo: config.repo,
359
363
  branch: branchInfo,
360
364
  credential,
365
+ // Will be set before any write operations
361
366
  provisionedAt: /* @__PURE__ */ new Date(),
362
367
  status: "provisioning",
363
368
  strategy,
364
- parentWorkspaceId: config.parentWorkspace
369
+ parentWorkspaceId: config.parentWorkspace,
370
+ onComplete: config.onComplete,
371
+ progress: {
372
+ phase: "initializing",
373
+ message: "Initializing workspace",
374
+ updatedAt: /* @__PURE__ */ new Date()
375
+ }
365
376
  };
366
377
  this.workspaces.set(workspaceId, workspace);
367
378
  try {
368
379
  if (strategy === "clone") {
369
- await this.cloneRepo(workspace, credential.token);
380
+ this.updateProgress(workspace, "cloning", "Cloning repository");
381
+ if (!credential) {
382
+ const cloneResult = await this.tryUnauthenticatedClone(workspace);
383
+ if (!cloneResult.success) {
384
+ this.log(
385
+ "info",
386
+ { workspaceId, error: cloneResult.error },
387
+ "Unauthenticated clone failed, requesting credentials"
388
+ );
389
+ credential = await this.credentialService.getCredentials({
390
+ repo: config.repo,
391
+ access: "write",
392
+ context: {
393
+ executionId: config.execution.id,
394
+ taskId: config.task.id,
395
+ userId: config.user?.id,
396
+ reason: `Workspace for ${config.task.role} in ${config.execution.patternName}`
397
+ }
398
+ });
399
+ workspace.credential = credential;
400
+ this.workspaces.set(workspaceId, workspace);
401
+ await this.emitEvent({
402
+ type: "credential:granted",
403
+ workspaceId,
404
+ credentialId: credential.id,
405
+ executionId: config.execution.id,
406
+ timestamp: /* @__PURE__ */ new Date()
407
+ });
408
+ await this.cloneRepo(workspace, credential.token);
409
+ }
410
+ } else {
411
+ await this.cloneRepo(workspace, credential.token);
412
+ }
413
+ this.updateProgress(workspace, "creating_branch", "Creating branch");
370
414
  await this.createBranch(workspace);
371
415
  } else {
372
416
  const parent = this.workspaces.get(config.parentWorkspace);
@@ -384,9 +428,12 @@ var WorkspaceService = class {
384
428
  data: { parentWorkspaceId: parent.id }
385
429
  });
386
430
  }
431
+ this.updateProgress(workspace, "configuring", "Configuring git");
387
432
  await this.configureGit(workspace);
388
433
  workspace.status = "ready";
434
+ this.updateProgress(workspace, "ready", "Workspace ready");
389
435
  this.workspaces.set(workspaceId, workspace);
436
+ await this.executeCompletionHook(workspace, "success");
390
437
  this.log(
391
438
  "info",
392
439
  {
@@ -406,8 +453,9 @@ var WorkspaceService = class {
406
453
  return workspace;
407
454
  } catch (error) {
408
455
  workspace.status = "error";
409
- this.workspaces.set(workspaceId, workspace);
410
456
  const errorMessage = error instanceof Error ? error.message : String(error);
457
+ this.updateProgress(workspace, "error", errorMessage);
458
+ this.workspaces.set(workspaceId, workspace);
411
459
  this.log("error", { workspaceId, error: errorMessage }, "Failed to provision workspace");
412
460
  await this.emitEvent({
413
461
  type: "workspace:error",
@@ -416,6 +464,7 @@ var WorkspaceService = class {
416
464
  timestamp: /* @__PURE__ */ new Date(),
417
465
  error: errorMessage
418
466
  });
467
+ await this.executeCompletionHook(workspace, "error");
419
468
  throw error;
420
469
  }
421
470
  }
@@ -581,7 +630,7 @@ var WorkspaceService = class {
581
630
  });
582
631
  }
583
632
  }
584
- if (workspace.strategy === "clone") {
633
+ if (workspace.strategy === "clone" && workspace.credential) {
585
634
  await this.credentialService.revokeCredential(workspace.credential.id);
586
635
  await this.emitEvent({
587
636
  type: "credential:revoked",
@@ -617,6 +666,36 @@ var WorkspaceService = class {
617
666
  // ─────────────────────────────────────────────────────────────
618
667
  // Private Methods
619
668
  // ─────────────────────────────────────────────────────────────
669
+ /**
670
+ * Try to clone a public repository without authentication
671
+ */
672
+ async tryUnauthenticatedClone(workspace) {
673
+ let cloneUrl = workspace.repo;
674
+ if (cloneUrl.startsWith("git@github.com:")) {
675
+ cloneUrl = cloneUrl.replace("git@github.com:", "https://github.com/");
676
+ }
677
+ if (!cloneUrl.endsWith(".git")) {
678
+ cloneUrl = `${cloneUrl}.git`;
679
+ }
680
+ if (!cloneUrl.startsWith("https://")) {
681
+ cloneUrl = `https://${cloneUrl}`;
682
+ }
683
+ try {
684
+ await this.execInDir(
685
+ workspace.path,
686
+ `git clone --depth 1 --branch ${workspace.branch.baseBranch} ${cloneUrl} .`
687
+ );
688
+ this.log("info", { workspaceId: workspace.id }, "Public repository cloned without authentication");
689
+ return { success: true };
690
+ } catch (error) {
691
+ const errorMessage = error instanceof Error ? error.message : String(error);
692
+ const isAuthError = errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("terminal prompts disabled");
693
+ if (isAuthError) {
694
+ return { success: false, error: "Authentication required" };
695
+ }
696
+ throw error;
697
+ }
698
+ }
620
699
  async cloneRepo(workspace, token) {
621
700
  const cloneUrl = token ? this.buildAuthenticatedUrl(workspace.repo, token) : workspace.repo;
622
701
  await this.execInDir(
@@ -640,11 +719,11 @@ var WorkspaceService = class {
640
719
  async configureGit(workspace) {
641
720
  await this.execInDir(workspace.path, 'git config user.name "Workspace Agent"');
642
721
  await this.execInDir(workspace.path, 'git config user.email "agent@workspace.local"');
643
- if (!workspace.credential.token) {
722
+ if (!workspace.credential || !workspace.credential.token) {
644
723
  this.log(
645
724
  "debug",
646
725
  { workspaceId: workspace.id },
647
- "Using SSH authentication, skipping credential helper"
726
+ workspace.credential ? "Using SSH authentication, skipping credential helper" : "No credentials (public repo), skipping credential helper"
648
727
  );
649
728
  return;
650
729
  }
@@ -670,9 +749,19 @@ var WorkspaceService = class {
670
749
  );
671
750
  }
672
751
  async pushBranch(workspace) {
752
+ if (!workspace.credential) {
753
+ throw new Error(
754
+ "Push requires authentication. This workspace was cloned from a public repository without credentials."
755
+ );
756
+ }
673
757
  await this.execInDir(workspace.path, `git push -u origin ${workspace.branch.name}`);
674
758
  }
675
759
  async createPullRequest(workspace, config) {
760
+ if (!workspace.credential) {
761
+ throw new Error(
762
+ "Pull request creation requires authentication. This workspace was cloned from a public repository without credentials."
763
+ );
764
+ }
676
765
  const repoInfo = this.parseRepo(workspace.repo);
677
766
  if (!repoInfo) {
678
767
  throw new Error(`Invalid repository format: ${workspace.repo}`);
@@ -752,6 +841,72 @@ var WorkspaceService = class {
752
841
  }
753
842
  }
754
843
  }
844
+ /**
845
+ * Update workspace progress
846
+ */
847
+ updateProgress(workspace, phase, message) {
848
+ workspace.progress = {
849
+ phase,
850
+ message,
851
+ updatedAt: /* @__PURE__ */ new Date()
852
+ };
853
+ this.workspaces.set(workspace.id, workspace);
854
+ this.log(
855
+ "debug",
856
+ { workspaceId: workspace.id, phase, message },
857
+ "Progress updated"
858
+ );
859
+ }
860
+ /**
861
+ * Execute completion hook if configured
862
+ */
863
+ async executeCompletionHook(workspace, status) {
864
+ const hook = workspace.onComplete;
865
+ if (!hook) return;
866
+ if (status === "error" && hook.runOnError === false) {
867
+ return;
868
+ }
869
+ const env = {
870
+ ...process.env,
871
+ WORKSPACE_ID: workspace.id,
872
+ REPO: workspace.repo,
873
+ BRANCH: workspace.branch.name,
874
+ STATUS: status,
875
+ WORKSPACE_PATH: workspace.path
876
+ };
877
+ if (hook.command) {
878
+ try {
879
+ this.log("info", { workspaceId: workspace.id, command: hook.command }, "Executing completion hook command");
880
+ await execAsync(hook.command, { env });
881
+ } catch (error) {
882
+ const errorMessage = error instanceof Error ? error.message : String(error);
883
+ this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion hook command failed");
884
+ }
885
+ }
886
+ if (hook.webhook) {
887
+ try {
888
+ this.log("info", { workspaceId: workspace.id, webhook: hook.webhook }, "Calling completion webhook");
889
+ const payload = {
890
+ workspaceId: workspace.id,
891
+ repo: workspace.repo,
892
+ branch: workspace.branch.name,
893
+ status,
894
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
895
+ };
896
+ await fetch(hook.webhook, {
897
+ method: "POST",
898
+ headers: {
899
+ "Content-Type": "application/json",
900
+ ...hook.webhookHeaders
901
+ },
902
+ body: JSON.stringify(payload)
903
+ });
904
+ } catch (error) {
905
+ const errorMessage = error instanceof Error ? error.message : String(error);
906
+ this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion webhook failed");
907
+ }
908
+ }
909
+ }
755
910
  };
756
911
 
757
912
  // src/oauth/device-flow.ts