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/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Git workspace provisioning and credential management service. Handles cloning re
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Workspace provisioning** - Clone repos, create branches, configure git
|
|
8
|
+
- **Git Worktrees** - Fast parallel workspaces with shared .git directory
|
|
8
9
|
- **Credential management** - Secure credential handling with TTL and revocation
|
|
9
10
|
- **Multiple providers** - Support for GitHub, GitLab, Bitbucket, Azure DevOps
|
|
10
11
|
- **GitHub App support** - First-class GitHub App authentication
|
|
@@ -213,6 +214,73 @@ const permissions = {
|
|
|
213
214
|
};
|
|
214
215
|
```
|
|
215
216
|
|
|
217
|
+
## Git Worktrees
|
|
218
|
+
|
|
219
|
+
For parallel work on the same repository, use worktrees instead of clones. Worktrees share the `.git` directory, making them faster to create and using less disk space.
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
┌──────────────────────────┬────────────────────────────────────────┐
|
|
223
|
+
│ Clone │ Worktree │
|
|
224
|
+
├──────────────────────────┼────────────────────────────────────────┤
|
|
225
|
+
│ Full .git copy each time │ Shared .git directory │
|
|
226
|
+
│ Slower for same repo │ Fast - just checkout │
|
|
227
|
+
│ More disk space │ Minimal disk │
|
|
228
|
+
│ Good for different repos │ Perfect for parallel work on SAME repo │
|
|
229
|
+
└──────────────────────────┴────────────────────────────────────────┘
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Creating a Worktree
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// First, create a clone workspace (the parent)
|
|
236
|
+
const parent = await workspaceService.provision({
|
|
237
|
+
repo: 'https://github.com/owner/repo',
|
|
238
|
+
strategy: 'clone', // explicit, but this is the default
|
|
239
|
+
branchStrategy: 'feature_branch',
|
|
240
|
+
baseBranch: 'main',
|
|
241
|
+
execution: { id: 'exec-123', patternName: 'review' },
|
|
242
|
+
task: { id: 'task-1', role: 'architect' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Then create worktrees from it for parallel agents
|
|
246
|
+
const reviewerWorkspace = await workspaceService.provision({
|
|
247
|
+
repo: 'https://github.com/owner/repo',
|
|
248
|
+
strategy: 'worktree',
|
|
249
|
+
parentWorkspace: parent.id, // Required for worktrees
|
|
250
|
+
branchStrategy: 'feature_branch',
|
|
251
|
+
baseBranch: 'main',
|
|
252
|
+
execution: { id: 'exec-123', patternName: 'review' },
|
|
253
|
+
task: { id: 'task-2', role: 'reviewer' },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Or use the convenience method
|
|
257
|
+
const testerWorkspace = await workspaceService.addWorktree(parent.id, {
|
|
258
|
+
branch: 'main',
|
|
259
|
+
execution: { id: 'exec-123', patternName: 'review' },
|
|
260
|
+
task: { id: 'task-3', role: 'tester' },
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Managing Worktrees
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// List all worktrees for a parent
|
|
268
|
+
const worktrees = workspaceService.listWorktrees(parent.id);
|
|
269
|
+
|
|
270
|
+
// Remove a specific worktree
|
|
271
|
+
await workspaceService.removeWorktree(reviewerWorkspace.id);
|
|
272
|
+
|
|
273
|
+
// Cleanup parent also cleans up all its worktrees
|
|
274
|
+
await workspaceService.cleanup(parent.id);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Worktree Benefits for Multi-Agent Systems
|
|
278
|
+
|
|
279
|
+
- **Shared credentials**: Worktrees reuse the parent's credential
|
|
280
|
+
- **Faster provisioning**: No network clone, just local checkout
|
|
281
|
+
- **Less disk space**: Single .git directory shared across all worktrees
|
|
282
|
+
- **Isolated branches**: Each worktree can work on a different branch
|
|
283
|
+
|
|
216
284
|
## Event System
|
|
217
285
|
|
|
218
286
|
Subscribe to workspace lifecycle events:
|
|
@@ -406,6 +474,8 @@ class FileTokenStore extends TokenStore {
|
|
|
406
474
|
| `workspace:error` | Workspace provisioning failed |
|
|
407
475
|
| `workspace:finalizing` | Workspace finalization started |
|
|
408
476
|
| `workspace:cleaned_up` | Workspace has been cleaned up |
|
|
477
|
+
| `worktree:added` | Git worktree added to parent |
|
|
478
|
+
| `worktree:removed` | Git worktree removed |
|
|
409
479
|
| `credential:granted` | Credential was granted |
|
|
410
480
|
| `credential:revoked` | Credential was revoked |
|
|
411
481
|
| `pr:created` | Pull request was created |
|
package/dist/index.cjs
CHANGED
|
@@ -283,6 +283,22 @@ var WorkspaceService = class {
|
|
|
283
283
|
* Provision a new workspace for a task
|
|
284
284
|
*/
|
|
285
285
|
async provision(config) {
|
|
286
|
+
const strategy = config.strategy || "clone";
|
|
287
|
+
if (strategy === "worktree") {
|
|
288
|
+
if (!config.parentWorkspace) {
|
|
289
|
+
throw new Error('parentWorkspace is required when strategy is "worktree"');
|
|
290
|
+
}
|
|
291
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
292
|
+
if (!parent) {
|
|
293
|
+
throw new Error(`Parent workspace not found: ${config.parentWorkspace}`);
|
|
294
|
+
}
|
|
295
|
+
if (parent.strategy !== "clone") {
|
|
296
|
+
throw new Error("Parent workspace must be a clone, not a worktree");
|
|
297
|
+
}
|
|
298
|
+
if (parent.repo !== config.repo) {
|
|
299
|
+
throw new Error("Worktree must be for the same repository as parent");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
286
302
|
const workspaceId = crypto.randomUUID();
|
|
287
303
|
this.log(
|
|
288
304
|
"info",
|
|
@@ -290,7 +306,8 @@ var WorkspaceService = class {
|
|
|
290
306
|
workspaceId,
|
|
291
307
|
repo: config.repo,
|
|
292
308
|
executionId: config.execution.id,
|
|
293
|
-
role: config.task.role
|
|
309
|
+
role: config.task.role,
|
|
310
|
+
strategy
|
|
294
311
|
},
|
|
295
312
|
"Provisioning workspace"
|
|
296
313
|
);
|
|
@@ -301,26 +318,31 @@ var WorkspaceService = class {
|
|
|
301
318
|
timestamp: /* @__PURE__ */ new Date()
|
|
302
319
|
});
|
|
303
320
|
const workspacePath = path__namespace.join(this.baseDir, workspaceId);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
321
|
+
let credential;
|
|
322
|
+
if (strategy === "worktree" && config.parentWorkspace) {
|
|
323
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
324
|
+
credential = parent.credential;
|
|
325
|
+
} else {
|
|
326
|
+
await fs3__namespace.mkdir(workspacePath, { recursive: true });
|
|
327
|
+
credential = await this.credentialService.getCredentials({
|
|
328
|
+
repo: config.repo,
|
|
329
|
+
access: "write",
|
|
330
|
+
context: {
|
|
331
|
+
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,
|
|
309
342
|
executionId: config.execution.id,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
},
|
|
314
|
-
// Pass user-provided credentials if available
|
|
315
|
-
userProvided: config.userCredentials
|
|
316
|
-
});
|
|
317
|
-
await this.emitEvent({
|
|
318
|
-
type: "credential:granted",
|
|
319
|
-
workspaceId,
|
|
320
|
-
credentialId: credential.id,
|
|
321
|
-
executionId: config.execution.id,
|
|
322
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
323
|
-
});
|
|
343
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
344
|
+
});
|
|
345
|
+
}
|
|
324
346
|
const branchInfo = createBranchInfo(
|
|
325
347
|
{
|
|
326
348
|
executionId: config.execution.id,
|
|
@@ -337,21 +359,52 @@ var WorkspaceService = class {
|
|
|
337
359
|
branch: branchInfo,
|
|
338
360
|
credential,
|
|
339
361
|
provisionedAt: /* @__PURE__ */ new Date(),
|
|
340
|
-
status: "provisioning"
|
|
362
|
+
status: "provisioning",
|
|
363
|
+
strategy,
|
|
364
|
+
parentWorkspaceId: config.parentWorkspace,
|
|
365
|
+
onComplete: config.onComplete,
|
|
366
|
+
progress: {
|
|
367
|
+
phase: "initializing",
|
|
368
|
+
message: "Initializing workspace",
|
|
369
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
370
|
+
}
|
|
341
371
|
};
|
|
342
372
|
this.workspaces.set(workspaceId, workspace);
|
|
343
373
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
if (strategy === "clone") {
|
|
375
|
+
this.updateProgress(workspace, "cloning", "Cloning repository");
|
|
376
|
+
await this.cloneRepo(workspace, credential.token);
|
|
377
|
+
this.updateProgress(workspace, "creating_branch", "Creating branch");
|
|
378
|
+
await this.createBranch(workspace);
|
|
379
|
+
} else {
|
|
380
|
+
const parent = this.workspaces.get(config.parentWorkspace);
|
|
381
|
+
await this.addWorktreeFromParent(parent, workspace);
|
|
382
|
+
if (!parent.worktreeIds) {
|
|
383
|
+
parent.worktreeIds = [];
|
|
384
|
+
}
|
|
385
|
+
parent.worktreeIds.push(workspaceId);
|
|
386
|
+
this.workspaces.set(parent.id, parent);
|
|
387
|
+
await this.emitEvent({
|
|
388
|
+
type: "worktree:added",
|
|
389
|
+
workspaceId,
|
|
390
|
+
executionId: config.execution.id,
|
|
391
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
392
|
+
data: { parentWorkspaceId: parent.id }
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
this.updateProgress(workspace, "configuring", "Configuring git");
|
|
346
396
|
await this.configureGit(workspace);
|
|
347
397
|
workspace.status = "ready";
|
|
398
|
+
this.updateProgress(workspace, "ready", "Workspace ready");
|
|
348
399
|
this.workspaces.set(workspaceId, workspace);
|
|
400
|
+
await this.executeCompletionHook(workspace, "success");
|
|
349
401
|
this.log(
|
|
350
402
|
"info",
|
|
351
403
|
{
|
|
352
404
|
workspaceId,
|
|
353
405
|
path: workspacePath,
|
|
354
|
-
branch: branchInfo.name
|
|
406
|
+
branch: branchInfo.name,
|
|
407
|
+
strategy
|
|
355
408
|
},
|
|
356
409
|
"Workspace provisioned"
|
|
357
410
|
);
|
|
@@ -364,8 +417,9 @@ var WorkspaceService = class {
|
|
|
364
417
|
return workspace;
|
|
365
418
|
} catch (error) {
|
|
366
419
|
workspace.status = "error";
|
|
367
|
-
this.workspaces.set(workspaceId, workspace);
|
|
368
420
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
421
|
+
this.updateProgress(workspace, "error", errorMessage);
|
|
422
|
+
this.workspaces.set(workspaceId, workspace);
|
|
369
423
|
this.log("error", { workspaceId, error: errorMessage }, "Failed to provision workspace");
|
|
370
424
|
await this.emitEvent({
|
|
371
425
|
type: "workspace:error",
|
|
@@ -374,9 +428,54 @@ var WorkspaceService = class {
|
|
|
374
428
|
timestamp: /* @__PURE__ */ new Date(),
|
|
375
429
|
error: errorMessage
|
|
376
430
|
});
|
|
431
|
+
await this.executeCompletionHook(workspace, "error");
|
|
377
432
|
throw error;
|
|
378
433
|
}
|
|
379
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Add a worktree to an existing clone workspace (convenience method)
|
|
437
|
+
*/
|
|
438
|
+
async addWorktree(parentWorkspaceId, options) {
|
|
439
|
+
const parent = this.workspaces.get(parentWorkspaceId);
|
|
440
|
+
if (!parent) {
|
|
441
|
+
throw new Error(`Parent workspace not found: ${parentWorkspaceId}`);
|
|
442
|
+
}
|
|
443
|
+
return this.provision({
|
|
444
|
+
repo: parent.repo,
|
|
445
|
+
strategy: "worktree",
|
|
446
|
+
parentWorkspace: parentWorkspaceId,
|
|
447
|
+
branchStrategy: "feature_branch",
|
|
448
|
+
baseBranch: options.branch,
|
|
449
|
+
execution: options.execution,
|
|
450
|
+
task: options.task
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* List all worktrees for a parent workspace
|
|
455
|
+
*/
|
|
456
|
+
listWorktrees(parentWorkspaceId) {
|
|
457
|
+
const parent = this.workspaces.get(parentWorkspaceId);
|
|
458
|
+
if (!parent) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
if (!parent.worktreeIds || parent.worktreeIds.length === 0) {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
return parent.worktreeIds.map((id) => this.workspaces.get(id)).filter((w) => w !== void 0);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Remove a worktree (alias for cleanup with worktree-specific handling)
|
|
468
|
+
*/
|
|
469
|
+
async removeWorktree(workspaceId) {
|
|
470
|
+
const workspace = this.workspaces.get(workspaceId);
|
|
471
|
+
if (!workspace) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (workspace.strategy !== "worktree") {
|
|
475
|
+
throw new Error("Workspace is not a worktree. Use cleanup() instead.");
|
|
476
|
+
}
|
|
477
|
+
await this.cleanup(workspaceId);
|
|
478
|
+
}
|
|
380
479
|
/**
|
|
381
480
|
* Finalize a workspace (push, create PR, cleanup)
|
|
382
481
|
*/
|
|
@@ -456,21 +555,55 @@ var WorkspaceService = class {
|
|
|
456
555
|
if (!workspace) {
|
|
457
556
|
return;
|
|
458
557
|
}
|
|
459
|
-
this.log("info", { workspaceId }, "Cleaning up workspace");
|
|
558
|
+
this.log("info", { workspaceId, strategy: workspace.strategy }, "Cleaning up workspace");
|
|
559
|
+
if (workspace.strategy === "clone" && workspace.worktreeIds?.length) {
|
|
560
|
+
this.log(
|
|
561
|
+
"info",
|
|
562
|
+
{ workspaceId, worktreeCount: workspace.worktreeIds.length },
|
|
563
|
+
"Cleaning up child worktrees first"
|
|
564
|
+
);
|
|
565
|
+
for (const worktreeId of workspace.worktreeIds) {
|
|
566
|
+
await this.cleanup(worktreeId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
460
569
|
try {
|
|
461
570
|
await cleanupCredentialFiles(workspace.path);
|
|
462
571
|
} catch (error) {
|
|
463
572
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
464
573
|
this.log("warn", { workspaceId, error: errorMessage }, "Failed to clean up credential files");
|
|
465
574
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
575
|
+
if (workspace.strategy === "worktree" && workspace.parentWorkspaceId) {
|
|
576
|
+
const parent = this.workspaces.get(workspace.parentWorkspaceId);
|
|
577
|
+
if (parent) {
|
|
578
|
+
try {
|
|
579
|
+
await this.execInDir(parent.path, `git worktree remove "${workspace.path}" --force`);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
582
|
+
this.log("warn", { workspaceId, error: errorMessage }, "Failed to remove worktree via git");
|
|
583
|
+
}
|
|
584
|
+
if (parent.worktreeIds) {
|
|
585
|
+
parent.worktreeIds = parent.worktreeIds.filter((id) => id !== workspaceId);
|
|
586
|
+
this.workspaces.set(parent.id, parent);
|
|
587
|
+
}
|
|
588
|
+
await this.emitEvent({
|
|
589
|
+
type: "worktree:removed",
|
|
590
|
+
workspaceId,
|
|
591
|
+
executionId: workspace.branch.executionId,
|
|
592
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
593
|
+
data: { parentWorkspaceId: parent.id }
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (workspace.strategy === "clone") {
|
|
598
|
+
await this.credentialService.revokeCredential(workspace.credential.id);
|
|
599
|
+
await this.emitEvent({
|
|
600
|
+
type: "credential:revoked",
|
|
601
|
+
workspaceId,
|
|
602
|
+
credentialId: workspace.credential.id,
|
|
603
|
+
executionId: workspace.branch.executionId,
|
|
604
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
605
|
+
});
|
|
606
|
+
}
|
|
474
607
|
try {
|
|
475
608
|
await fs3__namespace.rm(workspace.path, { recursive: true, force: true });
|
|
476
609
|
} catch (error) {
|
|
@@ -507,6 +640,16 @@ var WorkspaceService = class {
|
|
|
507
640
|
async createBranch(workspace) {
|
|
508
641
|
await this.execInDir(workspace.path, `git checkout -b ${workspace.branch.name}`);
|
|
509
642
|
}
|
|
643
|
+
async addWorktreeFromParent(parent, workspace) {
|
|
644
|
+
try {
|
|
645
|
+
await this.execInDir(parent.path, `git fetch origin ${workspace.branch.baseBranch}`);
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
await this.execInDir(
|
|
649
|
+
parent.path,
|
|
650
|
+
`git worktree add -b ${workspace.branch.name} "${workspace.path}" origin/${workspace.branch.baseBranch}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
510
653
|
async configureGit(workspace) {
|
|
511
654
|
await this.execInDir(workspace.path, 'git config user.name "Workspace Agent"');
|
|
512
655
|
await this.execInDir(workspace.path, 'git config user.email "agent@workspace.local"');
|
|
@@ -622,6 +765,72 @@ var WorkspaceService = class {
|
|
|
622
765
|
}
|
|
623
766
|
}
|
|
624
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* Update workspace progress
|
|
770
|
+
*/
|
|
771
|
+
updateProgress(workspace, phase, message) {
|
|
772
|
+
workspace.progress = {
|
|
773
|
+
phase,
|
|
774
|
+
message,
|
|
775
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
776
|
+
};
|
|
777
|
+
this.workspaces.set(workspace.id, workspace);
|
|
778
|
+
this.log(
|
|
779
|
+
"debug",
|
|
780
|
+
{ workspaceId: workspace.id, phase, message },
|
|
781
|
+
"Progress updated"
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Execute completion hook if configured
|
|
786
|
+
*/
|
|
787
|
+
async executeCompletionHook(workspace, status) {
|
|
788
|
+
const hook = workspace.onComplete;
|
|
789
|
+
if (!hook) return;
|
|
790
|
+
if (status === "error" && hook.runOnError === false) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const env = {
|
|
794
|
+
...process.env,
|
|
795
|
+
WORKSPACE_ID: workspace.id,
|
|
796
|
+
REPO: workspace.repo,
|
|
797
|
+
BRANCH: workspace.branch.name,
|
|
798
|
+
STATUS: status,
|
|
799
|
+
WORKSPACE_PATH: workspace.path
|
|
800
|
+
};
|
|
801
|
+
if (hook.command) {
|
|
802
|
+
try {
|
|
803
|
+
this.log("info", { workspaceId: workspace.id, command: hook.command }, "Executing completion hook command");
|
|
804
|
+
await execAsync(hook.command, { env });
|
|
805
|
+
} catch (error) {
|
|
806
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
807
|
+
this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion hook command failed");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (hook.webhook) {
|
|
811
|
+
try {
|
|
812
|
+
this.log("info", { workspaceId: workspace.id, webhook: hook.webhook }, "Calling completion webhook");
|
|
813
|
+
const payload = {
|
|
814
|
+
workspaceId: workspace.id,
|
|
815
|
+
repo: workspace.repo,
|
|
816
|
+
branch: workspace.branch.name,
|
|
817
|
+
status,
|
|
818
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
819
|
+
};
|
|
820
|
+
await fetch(hook.webhook, {
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: {
|
|
823
|
+
"Content-Type": "application/json",
|
|
824
|
+
...hook.webhookHeaders
|
|
825
|
+
},
|
|
826
|
+
body: JSON.stringify(payload)
|
|
827
|
+
});
|
|
828
|
+
} catch (error) {
|
|
829
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
830
|
+
this.log("warn", { workspaceId: workspace.id, error: errorMessage }, "Completion webhook failed");
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
625
834
|
};
|
|
626
835
|
|
|
627
836
|
// src/oauth/device-flow.ts
|