@ebay-dt/bonfire-node 0.1.0 → 0.2.0-alpha.10

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.
Files changed (45) hide show
  1. package/README.md +10 -4
  2. package/dist/agent-service.d.ts +4 -1
  3. package/dist/agent-service.d.ts.map +1 -1
  4. package/dist/agent-service.js +18 -2
  5. package/dist/agent-service.js.map +1 -1
  6. package/dist/claude.d.ts +12 -11
  7. package/dist/claude.d.ts.map +1 -1
  8. package/dist/claude.js +166 -11
  9. package/dist/claude.js.map +1 -1
  10. package/dist/deployment-provider.d.ts +1 -0
  11. package/dist/deployment-provider.d.ts.map +1 -1
  12. package/dist/deployment-provider.js +1 -0
  13. package/dist/deployment-provider.js.map +1 -1
  14. package/dist/git-repository.d.ts +4 -2
  15. package/dist/git-repository.d.ts.map +1 -1
  16. package/dist/git-repository.js +35 -2
  17. package/dist/git-repository.js.map +1 -1
  18. package/dist/github-cli.d.ts.map +1 -1
  19. package/dist/github-cli.js +3 -1
  20. package/dist/github-cli.js.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/lifecycle/deployment-worktree.d.ts +35 -0
  26. package/dist/lifecycle/deployment-worktree.d.ts.map +1 -0
  27. package/dist/lifecycle/deployment-worktree.js +302 -0
  28. package/dist/lifecycle/deployment-worktree.js.map +1 -0
  29. package/dist/lifecycle/local-prototype-lifecycle.d.ts +11 -1
  30. package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
  31. package/dist/lifecycle/local-prototype-lifecycle.js +272 -76
  32. package/dist/lifecycle/local-prototype-lifecycle.js.map +1 -1
  33. package/dist/lifecycle/repository-summary.d.ts +19 -0
  34. package/dist/lifecycle/repository-summary.d.ts.map +1 -0
  35. package/dist/lifecycle/repository-summary.js +191 -0
  36. package/dist/lifecycle/repository-summary.js.map +1 -0
  37. package/dist/lifecycle/repository-synchronization.d.ts +36 -0
  38. package/dist/lifecycle/repository-synchronization.d.ts.map +1 -0
  39. package/dist/lifecycle/repository-synchronization.js +314 -0
  40. package/dist/lifecycle/repository-synchronization.js.map +1 -0
  41. package/dist/lifecycle/runtime-context.d.ts +5 -1
  42. package/dist/lifecycle/runtime-context.d.ts.map +1 -1
  43. package/dist/lifecycle/runtime-context.js +7 -0
  44. package/dist/lifecycle/runtime-context.js.map +1 -1
  45. package/package.json +2 -2
@@ -2,8 +2,8 @@ import { constants } from "node:fs";
2
2
  import { lstat, open, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { join, relative, sep } from "node:path";
5
- import { definePrototypeRef, getPrototypeUrl, LifecycleInputError, lifecycleFailure, lifecycleSuccess, parseCreatePrototypeInput, parseDuplicatePrototypeInput, parseEditPrototypeInput, parseBonfireIdentity, parsePrototypeCommandInput, parsePrototypeMetadata, slugifyPrototypeTitle, PrototypeRefError, } from "@ebay-dt/bonfire-core";
6
- import { commitPath, discardPath, GitRepositoryError, prepareIdentityBranch, pushBranch, } from "../git-repository.js";
5
+ import { definePrototypeRef, getPrototypeUrl, LifecycleInputError, lifecycleFailure, lifecycleSuccess, parseCreatePrototypeInput, parseDuplicatePrototypeInput, parseEditPrototypeInput, parseBonfireIdentity, parsePrototypeCommandInput, parsePrototypeMetadata, slugifyPrototypeTitle, PrototypeRefError, parseGitModeSetting, } from "@ebay-dt/bonfire-core";
6
+ import { commitPath, discardPath, GitRepositoryError, pushBranch, pushCommitToBranch, } from "../git-repository.js";
7
7
  import { duplicatePrototype, PrototypeDuplicateError } from "../duplicate.js";
8
8
  import { PrototypeCatalog, } from "../prototype-catalog.js";
9
9
  import { PrototypeScaffoldError, scaffoldPrototype } from "../scaffold.js";
@@ -11,6 +11,9 @@ import { createPrototypeLifecycleRuntime, } from "./runtime-context.js";
11
11
  import { cleanupOwnedDestination, validateOwnedDestination, } from "../internal/owned-destination.js";
12
12
  import { RepositoryBusyError, withRepositoryOperation, } from "./operation-gate.js";
13
13
  import { DeploymentProviderError, } from "../deployment-provider.js";
14
+ import { assertIdentityBranchReady, synchronizeIdentityBranch, } from "./repository-synchronization.js";
15
+ import { getRepositorySummary } from "./repository-summary.js";
16
+ import { DeploymentWorktreeError, preflightDeploymentSource, publishDeploymentSource, reportDeploymentIssue, } from "./deployment-worktree.js";
14
17
  function repositoryRelativePath(runtime, ref) {
15
18
  const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);
16
19
  return relative(runtime.repositoryRoot, absolutePath).split(sep).join("/");
@@ -29,6 +32,10 @@ class PrototypeMetadataFileError extends Error {
29
32
  }
30
33
  class DeploymentUnavailableError extends Error {
31
34
  }
35
+ class DeploymentSourcePushError extends Error {
36
+ }
37
+ class DeploymentNotAcceptedError extends Error {
38
+ }
32
39
  function metadataPath(runtime, ref) {
33
40
  return join(runtime.prototypeRoot, ref.login, ref.slug, "metadata.json");
34
41
  }
@@ -112,26 +119,6 @@ async function writeMetadataDocument(runtime, ref, document) {
112
119
  await rm(temporaryPath, { force: true }).catch(() => undefined);
113
120
  }
114
121
  }
115
- async function mergeIdentityBranchToPrimary(repository, options) {
116
- if (options.branch === options.primaryBranch)
117
- return;
118
- const hasRemote = (await repository.remotes()).some((remote) => remote.name === options.remote);
119
- if (!hasRemote)
120
- return;
121
- await repository.checkout([options.primaryBranch]);
122
- try {
123
- await repository.merge([
124
- options.branch,
125
- "--no-ff",
126
- "-m",
127
- `merge: ${options.branch}`,
128
- ]);
129
- await repository.push(options.remote, options.primaryBranch);
130
- }
131
- finally {
132
- await repository.checkout([options.branch]);
133
- }
134
- }
135
122
  function mapError(error) {
136
123
  if (error instanceof PrototypeNotFoundError)
137
124
  return "not-found";
@@ -173,6 +160,11 @@ function mapError(error) {
173
160
  if (error instanceof DeploymentUnavailableError) {
174
161
  return "deployment-unavailable";
175
162
  }
163
+ if (error instanceof DeploymentSourcePushError ||
164
+ error instanceof DeploymentNotAcceptedError ||
165
+ error instanceof DeploymentWorktreeError) {
166
+ return "deployment-failure";
167
+ }
176
168
  if (error instanceof DeploymentProviderError) {
177
169
  return error.kind === "timeout" ? "timeout" : "deployment-failure";
178
170
  }
@@ -183,19 +175,35 @@ function resultFrom(operation) {
183
175
  .then(lifecycleSuccess)
184
176
  .catch((error) => lifecycleFailure(mapError(error)));
185
177
  }
178
+ async function diagnoseMetadataStep(step, operation) {
179
+ try {
180
+ return await operation();
181
+ }
182
+ catch (error) {
183
+ reportDeploymentIssue("publication", step, error);
184
+ throw error;
185
+ }
186
+ }
186
187
  export class LocalPrototypeLifecycle {
187
188
  #runtime;
189
+ gitMode;
188
190
  #catalog;
189
191
  #repository;
190
192
  #preparePrototypeFiles;
191
193
  #deploymentProvider;
192
194
  #applicationId;
193
195
  #themeRegistry;
196
+ #restartRequired = false;
197
+ #pendingPublication;
194
198
  constructor(options) {
195
199
  const internalOptions = options;
196
- this.#runtime =
197
- internalOptions.runtime ??
198
- createPrototypeLifecycleRuntime(internalOptions);
200
+ const runtime = internalOptions.runtime ??
201
+ createPrototypeLifecycleRuntime(internalOptions);
202
+ this.#runtime = {
203
+ ...runtime,
204
+ gitMode: parseGitModeSetting(runtime.gitMode),
205
+ };
206
+ this.gitMode = this.#runtime.gitMode;
199
207
  this.#themeRegistry = options.themeRegistry;
200
208
  this.#catalog =
201
209
  internalOptions.catalog ??
@@ -241,14 +249,7 @@ export class LocalPrototypeLifecycle {
241
249
  };
242
250
  }
243
251
  async #prepareMutationBranch(login) {
244
- if (this.#runtime.automaticBranchSwitch === "disabled") {
245
- return this.#currentBranch();
246
- }
247
- await prepareIdentityBranch(this.#repository, {
248
- identityBranch: login,
249
- primaryBranch: this.#runtime.primaryBranch,
250
- remote: this.#runtime.remote,
251
- });
252
+ await assertIdentityBranchReady(this.#repository, login);
252
253
  return login;
253
254
  }
254
255
  async #prepareDestination(receipt, expectedFiles, request) {
@@ -278,16 +279,35 @@ export class LocalPrototypeLifecycle {
278
279
  return resultFrom(async () => {
279
280
  const verifiedIdentity = await this.#verifiedIdentity(identity);
280
281
  return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
281
- if (this.#runtime.automaticBranchSwitch === "disabled") {
282
+ if (this.#runtime.gitMode === "manual" ||
283
+ this.#runtime.automaticBranchSwitch === "disabled") {
282
284
  await this.#currentBranch();
283
285
  return { status: "disabled" };
284
286
  }
285
- await prepareIdentityBranch(this.#repository, {
287
+ const synchronization = await synchronizeIdentityBranch(this.#repository, {
286
288
  identityBranch: verifiedIdentity.login,
287
289
  primaryBranch: this.#runtime.primaryBranch,
288
290
  remote: this.#runtime.remote,
291
+ managedPaths: this.#runtime.managedRepositoryPaths,
289
292
  });
290
- return { status: "ready" };
293
+ if (synchronization.status !== "synchronized") {
294
+ return {
295
+ status: synchronization.status,
296
+ reason: synchronization.reason,
297
+ ...(synchronization.affectedPaths.length > 0
298
+ ? { affectedPaths: synchronization.affectedPaths }
299
+ : {}),
300
+ };
301
+ }
302
+ if (synchronization.affectedPaths.some((path) => this.#runtime.managedRepositoryPaths.includes(path))) {
303
+ this.#restartRequired = true;
304
+ }
305
+ return {
306
+ status: "ready",
307
+ ...(synchronization.affectedPaths.length > 0
308
+ ? { affectedPaths: synchronization.affectedPaths }
309
+ : {}),
310
+ };
291
311
  });
292
312
  });
293
313
  }
@@ -300,7 +320,9 @@ export class LocalPrototypeLifecycle {
300
320
  if (await this.#catalog.getSummary(ref)) {
301
321
  throw new PrototypeScaffoldError("collision");
302
322
  }
303
- const branch = await this.#prepareMutationBranch(verifiedIdentity.login);
323
+ const branch = this.#runtime.gitMode === "managed"
324
+ ? await this.#prepareMutationBranch(verifiedIdentity.login)
325
+ : undefined;
304
326
  const prepared = await scaffoldPrototype({
305
327
  paths: this.#runtime,
306
328
  ref,
@@ -320,6 +342,13 @@ export class LocalPrototypeLifecycle {
320
342
  title: normalizedInput.title,
321
343
  description: normalizedInput.description,
322
344
  });
345
+ if (branch === undefined) {
346
+ return {
347
+ ref,
348
+ url: getPrototypeUrl(ref),
349
+ remoteStatus: "manual",
350
+ };
351
+ }
323
352
  await commitPath(this.#repository, repositoryRelativePath(this.#runtime, ref), `init: ${ref.slug}`);
324
353
  const push = await pushBranch(this.#repository, this.#runtime.remote, branch);
325
354
  return {
@@ -338,7 +367,9 @@ export class LocalPrototypeLifecycle {
338
367
  const { ref, path } = await this.#ownedPrototype(verifiedIdentity, {
339
368
  ref: normalizedInput.ref,
340
369
  });
341
- const branch = await this.#prepareMutationBranch(verifiedIdentity.login);
370
+ const branch = this.#runtime.gitMode === "managed"
371
+ ? await this.#prepareMutationBranch(verifiedIdentity.login)
372
+ : undefined;
342
373
  await assertPrototypeDirectory(this.#runtime, ref);
343
374
  const document = await readMetadataDocument(this.#runtime, ref, this.#themeRegistry);
344
375
  const metadata = parsePrototypeMetadata(document, {
@@ -356,8 +387,10 @@ export class LocalPrototypeLifecycle {
356
387
  ? {}
357
388
  : { theme: normalizedInput.theme }),
358
389
  });
359
- await commitPath(this.#repository, path, `edit: ${ref.slug}`);
360
- await pushBranch(this.#repository, this.#runtime.remote, branch);
390
+ if (branch !== undefined) {
391
+ await commitPath(this.#repository, path, `edit: ${ref.slug}`);
392
+ await pushBranch(this.#repository, this.#runtime.remote, branch);
393
+ }
361
394
  return { edited: true, ref };
362
395
  });
363
396
  });
@@ -368,10 +401,14 @@ export class LocalPrototypeLifecycle {
368
401
  const normalizedInput = parsePrototypeCommandInput(input);
369
402
  return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
370
403
  const { ref, path } = await this.#ownedPrototype(verifiedIdentity, normalizedInput);
371
- const branch = await this.#prepareMutationBranch(verifiedIdentity.login);
404
+ const branch = this.#runtime.gitMode === "managed"
405
+ ? await this.#prepareMutationBranch(verifiedIdentity.login)
406
+ : undefined;
372
407
  await removePrototypeDirectory(this.#runtime, ref);
373
- await commitPath(this.#repository, path, `delete: ${ref.slug}`);
374
- await pushBranch(this.#repository, this.#runtime.remote, branch);
408
+ if (branch !== undefined) {
409
+ await commitPath(this.#repository, path, `delete: ${ref.slug}`);
410
+ await pushBranch(this.#repository, this.#runtime.remote, branch);
411
+ }
375
412
  return { deleted: true, ref };
376
413
  });
377
414
  });
@@ -388,7 +425,113 @@ export class LocalPrototypeLifecycle {
388
425
  };
389
426
  });
390
427
  }
428
+ async getRepositorySummary(identity) {
429
+ return resultFrom(async () => {
430
+ const verifiedIdentity = await this.#verifiedIdentity(identity);
431
+ return getRepositorySummary({
432
+ repository: this.#repository,
433
+ catalog: this.#catalog,
434
+ repositoryRoot: this.#runtime.repositoryRoot,
435
+ prototypeRoot: this.#runtime.prototypeRoot,
436
+ identity: verifiedIdentity,
437
+ mode: this.#runtime.gitMode,
438
+ remote: this.#runtime.remote,
439
+ primaryBranch: this.#runtime.primaryBranch,
440
+ restartRequired: this.#restartRequired,
441
+ });
442
+ });
443
+ }
444
+ async checkRepositorySummary(identity) {
445
+ return resultFrom(async () => {
446
+ const verifiedIdentity = await this.#verifiedIdentity(identity);
447
+ return getRepositorySummary({
448
+ repository: this.#repository,
449
+ catalog: this.#catalog,
450
+ repositoryRoot: this.#runtime.repositoryRoot,
451
+ prototypeRoot: this.#runtime.prototypeRoot,
452
+ identity: verifiedIdentity,
453
+ mode: this.#runtime.gitMode,
454
+ remote: this.#runtime.remote,
455
+ primaryBranch: this.#runtime.primaryBranch,
456
+ restartRequired: this.#restartRequired,
457
+ refresh: true,
458
+ });
459
+ });
460
+ }
461
+ async #headCommit() {
462
+ const commit = (await this.#repository.raw(["rev-parse", "HEAD"])).trim();
463
+ if (!/^[0-9a-f]{40,64}$/iu.test(commit)) {
464
+ throw new GitRepositoryError("malformed-output", "head-commit");
465
+ }
466
+ return commit.toLowerCase();
467
+ }
468
+ async #prepareDeploymentBranch(login) {
469
+ if (this.#runtime.automaticBranchSwitch === "disabled") {
470
+ await assertIdentityBranchReady(this.#repository, login);
471
+ return "updates-not-synchronized";
472
+ }
473
+ const synchronization = await synchronizeIdentityBranch(this.#repository, {
474
+ identityBranch: login,
475
+ primaryBranch: this.#runtime.primaryBranch,
476
+ remote: this.#runtime.remote,
477
+ managedPaths: this.#runtime.managedRepositoryPaths,
478
+ });
479
+ if (synchronization.status === "unsafe") {
480
+ throw new GitRepositoryError("conflict", "deployment-preparation");
481
+ }
482
+ await assertIdentityBranchReady(this.#repository, login);
483
+ return synchronization.status === "deferred"
484
+ ? "updates-not-synchronized"
485
+ : undefined;
486
+ }
487
+ #workspaceRuntime(workspacePath, workspaceRepository) {
488
+ return {
489
+ ...this.#runtime,
490
+ repositoryRoot: workspacePath,
491
+ repositoryRealPath: workspacePath,
492
+ prototypeRoot: join(workspacePath, relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot)),
493
+ gitRepository: workspaceRepository,
494
+ };
495
+ }
496
+ async #publishDeploymentMetadata(pending) {
497
+ return publishDeploymentSource(this.#repository, {
498
+ remote: this.#runtime.remote,
499
+ primaryBranch: this.#runtime.primaryBranch,
500
+ sourceSha: pending.sourceSha,
501
+ commitIdentity: this.#runtime.commitIdentity,
502
+ }, async ({ path, repository }) => {
503
+ const workspaceRuntime = this.#workspaceRuntime(path, repository);
504
+ const mergedDocument = await diagnoseMetadataStep("metadata-read", () => readMetadataDocument(workspaceRuntime, pending.ref, this.#themeRegistry));
505
+ await diagnoseMetadataStep("metadata-write", () => writeMetadataDocument(workspaceRuntime, pending.ref, {
506
+ ...mergedDocument,
507
+ previewUrl: pending.previewUrl,
508
+ updatedAt: new Date().toISOString(),
509
+ }));
510
+ await diagnoseMetadataStep("metadata-commit", () => commitPath(repository, pending.path, `deploy: ${pending.ref.slug} (metadata)`));
511
+ });
512
+ }
513
+ async #deploymentValue(pending, publicationWarnings = []) {
514
+ const warnings = [
515
+ ...(pending.synchronizationWarning === undefined
516
+ ? []
517
+ : [pending.synchronizationWarning]),
518
+ ...publicationWarnings,
519
+ ];
520
+ return {
521
+ deployed: true,
522
+ ref: pending.ref,
523
+ previewUrl: pending.previewUrl,
524
+ deploymentId: pending.deploymentId,
525
+ sourceSha: pending.sourceSha,
526
+ readyState: pending.readyState,
527
+ warning: publicationWarnings[0] ?? pending.synchronizationWarning,
528
+ ...(warnings.length === 0 ? {} : { warnings }),
529
+ };
530
+ }
391
531
  async deploy(identity, input) {
532
+ if (this.#runtime.gitMode === "manual") {
533
+ return lifecycleFailure("forbidden");
534
+ }
392
535
  return resultFrom(async () => {
393
536
  const verifiedIdentity = await this.#verifiedIdentity(identity);
394
537
  const normalizedInput = parsePrototypeCommandInput(input);
@@ -397,7 +540,7 @@ export class LocalPrototypeLifecycle {
397
540
  }
398
541
  return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
399
542
  const { ref, path } = await this.#ownedPrototype(verifiedIdentity, normalizedInput);
400
- const branch = await this.#prepareMutationBranch(verifiedIdentity.login);
543
+ const synchronizationWarning = await this.#prepareDeploymentBranch(verifiedIdentity.login);
401
544
  await assertPrototypeDirectory(this.#runtime, ref);
402
545
  const document = await readMetadataDocument(this.#runtime, ref, this.#themeRegistry);
403
546
  const metadata = parsePrototypeMetadata(document, {
@@ -407,60 +550,104 @@ export class LocalPrototypeLifecycle {
407
550
  throw new PrototypeNotFoundError();
408
551
  }
409
552
  await commitPath(this.#repository, path, `deploy: ${ref.slug}`);
410
- await pushBranch(this.#repository, this.#runtime.remote, branch);
553
+ const sourceSha = await this.#headCommit();
554
+ const sourcePush = await pushCommitToBranch(this.#repository, this.#runtime.remote, sourceSha, verifiedIdentity.login);
555
+ if (sourcePush.remoteStatus !== "pushed") {
556
+ throw new DeploymentSourcePushError();
557
+ }
558
+ await preflightDeploymentSource(this.#repository, {
559
+ remote: this.#runtime.remote,
560
+ primaryBranch: this.#runtime.primaryBranch,
561
+ sourceSha,
562
+ commitIdentity: this.#runtime.commitIdentity,
563
+ });
411
564
  const deployment = await this.#deploymentProvider.deploy({
412
565
  applicationId: this.#applicationId,
413
566
  login: verifiedIdentity.login,
414
567
  });
415
- const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
416
- let warning;
417
- try {
418
- await writeMetadataDocument(this.#runtime, ref, {
419
- ...document,
420
- previewUrl,
421
- updatedAt: new Date().toISOString(),
422
- });
423
- await commitPath(this.#repository, path, `deploy: ${ref.slug} (metadata)`);
424
- await pushBranch(this.#repository, this.#runtime.remote, branch);
425
- }
426
- catch {
427
- warning = "metadata-persistence-failed";
568
+ if (deployment.commitSha.toLowerCase() !== sourceSha ||
569
+ !["READY", "QUEUED", "INITIALIZING", "BUILDING"].includes(deployment.readyState)) {
570
+ throw new DeploymentNotAcceptedError();
428
571
  }
429
- if (warning === undefined) {
430
- try {
431
- await mergeIdentityBranchToPrimary(this.#repository, {
432
- branch,
433
- primaryBranch: this.#runtime.primaryBranch,
434
- remote: this.#runtime.remote,
435
- });
436
- }
437
- catch {
438
- warning = "primary-merge-failed";
439
- }
440
- }
441
- return {
442
- deployed: true,
572
+ const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
573
+ const pending = {
574
+ identity: verifiedIdentity,
575
+ input: normalizedInput,
443
576
  ref,
577
+ path,
444
578
  previewUrl,
445
579
  deploymentId: deployment.deploymentId,
580
+ sourceSha,
446
581
  readyState: deployment.readyState,
447
- warning,
582
+ synchronizationWarning,
448
583
  };
584
+ try {
585
+ const publication = await this.#publishDeploymentMetadata(pending);
586
+ this.#pendingPublication = undefined;
587
+ return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : []);
588
+ }
589
+ catch (error) {
590
+ // Keep the accepted deployment retryable for every publication
591
+ // failure, including metadata preparation failures.
592
+ this.#pendingPublication = pending;
593
+ return this.#deploymentValue(pending, [
594
+ error instanceof DeploymentWorktreeError &&
595
+ error.kind !== "cleanup-failure"
596
+ ? "primary-merge-failed"
597
+ : "metadata-persistence-failed",
598
+ ...(error instanceof DeploymentWorktreeError &&
599
+ error.cleanupFailed
600
+ ? ["publication-cleanup-failed"]
601
+ : []),
602
+ ]);
603
+ }
604
+ });
605
+ });
606
+ }
607
+ async retryPublish(identity, input) {
608
+ if (this.#runtime.gitMode === "manual") {
609
+ return lifecycleFailure("forbidden");
610
+ }
611
+ return resultFrom(async () => {
612
+ const verifiedIdentity = await this.#verifiedIdentity(identity);
613
+ const normalizedInput = parsePrototypeCommandInput(input);
614
+ const pending = this.#pendingPublication;
615
+ if (pending === undefined ||
616
+ pending.identity.login !== verifiedIdentity.login ||
617
+ pending.ref.login !== normalizedInput.ref.login ||
618
+ pending.ref.slug !== normalizedInput.ref.slug) {
619
+ throw new DeploymentUnavailableError();
620
+ }
621
+ return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
622
+ await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
623
+ const publication = await this.#publishDeploymentMetadata(pending);
624
+ this.#pendingPublication = undefined;
625
+ return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : []);
449
626
  });
450
627
  });
451
628
  }
452
629
  async save(identity, input) {
630
+ if (this.#runtime.gitMode === "manual") {
631
+ return lifecycleFailure("forbidden");
632
+ }
453
633
  return resultFrom(async () => {
454
- const { ref, path } = await this.#ownedPrototype(identity, input);
634
+ const verifiedIdentity = await this.#verifiedIdentity(identity);
635
+ const { ref, path } = await this.#ownedPrototype(verifiedIdentity, input);
455
636
  return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
637
+ await this.#prepareMutationBranch(verifiedIdentity.login);
456
638
  await commitPath(this.#repository, path, `save: ${ref.slug}`);
639
+ await pushBranch(this.#repository, this.#runtime.remote, verifiedIdentity.login);
457
640
  return { saved: true };
458
641
  });
459
642
  });
460
643
  }
461
644
  async discard(identity, input) {
645
+ if (this.#runtime.gitMode === "manual") {
646
+ return lifecycleFailure("forbidden");
647
+ }
462
648
  return resultFrom(async () => {
463
- const { path } = await this.#ownedPrototype(identity, input);
649
+ const verifiedIdentity = await this.#verifiedIdentity(identity);
650
+ const { path } = await this.#ownedPrototype(verifiedIdentity, input);
464
651
  return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
465
652
  await discardPath(this.#repository, path);
466
653
  return { discarded: true };
@@ -479,7 +666,9 @@ export class LocalPrototypeLifecycle {
479
666
  if (await this.#catalog.getSummary(ref)) {
480
667
  throw new PrototypeDuplicateError("collision");
481
668
  }
482
- const branch = await this.#prepareMutationBranch(verifiedIdentity.login);
669
+ const branch = this.#runtime.gitMode === "managed"
670
+ ? await this.#prepareMutationBranch(verifiedIdentity.login)
671
+ : undefined;
483
672
  const prepared = await duplicatePrototype({
484
673
  paths: this.#runtime,
485
674
  source: normalizedInput.source,
@@ -500,6 +689,13 @@ export class LocalPrototypeLifecycle {
500
689
  title: normalizedInput.title,
501
690
  description: normalizedInput.description,
502
691
  });
692
+ if (branch === undefined) {
693
+ return {
694
+ ref,
695
+ url: getPrototypeUrl(ref),
696
+ remoteStatus: "manual",
697
+ };
698
+ }
503
699
  await commitPath(this.#repository, repositoryRelativePath(this.#runtime, ref), `init: ${ref.slug}`);
504
700
  const push = await pushBranch(this.#repository, this.#runtime.remote, branch);
505
701
  return {