hikoutei 0.5.19 → 0.5.21

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 (116) hide show
  1. package/README.md +17 -0
  2. package/dist/adapter/persistence/contracts/scalar.d.ts +34 -0
  3. package/dist/adapter/persistence/contracts/scalar.d.ts.map +1 -1
  4. package/dist/adapter/persistence/contracts/scalar.js +6 -1
  5. package/dist/adapter/persistence/contracts/scalar.js.map +1 -1
  6. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts +19 -20
  7. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts.map +1 -1
  8. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js +117 -67
  9. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js.map +1 -1
  10. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.d.ts +31 -0
  11. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.d.ts.map +1 -0
  12. package/dist/adapter/persistence/providers/mikro-orm/engine/{MikroOrmMappedTypedSheets.js → MikroOrmMappedRuntime.js} +10 -31
  13. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.js.map +1 -0
  14. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarEntityRuntime.js.map +1 -1
  15. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarRuntime.js.map +1 -1
  16. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.d.ts +12 -0
  17. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.d.ts.map +1 -0
  18. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.js +20 -0
  19. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.js.map +1 -0
  20. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.d.ts +2 -7
  21. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.d.ts.map +1 -1
  22. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.js +5 -11
  23. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.js.map +1 -1
  24. package/dist/api/Hikoutei.d.ts.map +1 -1
  25. package/dist/api/Hikoutei.js +3 -4
  26. package/dist/api/Hikoutei.js.map +1 -1
  27. package/dist/api/errors.d.ts +2 -0
  28. package/dist/api/errors.d.ts.map +1 -1
  29. package/dist/api/errors.js +2 -0
  30. package/dist/api/errors.js.map +1 -1
  31. package/dist/api/internalEntityManager.d.ts.map +1 -1
  32. package/dist/api/internalEntityManager.js +31 -9
  33. package/dist/api/internalEntityManager.js.map +1 -1
  34. package/dist/application/orm/mapping/contracts.d.ts +8 -3
  35. package/dist/application/orm/mapping/contracts.d.ts.map +1 -1
  36. package/dist/application/orm/mapping/values.d.ts +2 -0
  37. package/dist/application/orm/mapping/values.d.ts.map +1 -1
  38. package/dist/application/orm/mapping/values.js +10 -3
  39. package/dist/application/orm/mapping/values.js.map +1 -1
  40. package/dist/application/orm/persistence/flush/flushCoordinator.d.ts +2 -2
  41. package/dist/application/orm/persistence/flush/flushCoordinator.d.ts.map +1 -1
  42. package/dist/application/orm/persistence/flush/flushCoordinator.js +9 -16
  43. package/dist/application/orm/persistence/flush/flushCoordinator.js.map +1 -1
  44. package/dist/application/orm/persistence/lifecycle/entityLifecycle.d.ts.map +1 -1
  45. package/dist/application/orm/persistence/lifecycle/entityLifecycle.js +26 -13
  46. package/dist/application/orm/persistence/lifecycle/entityLifecycle.js.map +1 -1
  47. package/dist/application/orm/persistence/projection/projectionEffects.d.ts +2 -2
  48. package/dist/application/orm/persistence/projection/projectionEffects.d.ts.map +1 -1
  49. package/dist/application/orm/persistence/projection/projectionEffects.js +6 -6
  50. package/dist/application/orm/persistence/projection/projectionEffects.js.map +1 -1
  51. package/dist/application/orm/persistence/scalar/unitOfWork.d.ts +21 -2
  52. package/dist/application/orm/persistence/scalar/unitOfWork.d.ts.map +1 -1
  53. package/dist/application/orm/persistence/scalar/unitOfWork.js +107 -8
  54. package/dist/application/orm/persistence/scalar/unitOfWork.js.map +1 -1
  55. package/dist/application/orm/persistence/support/contracts.d.ts +6 -6
  56. package/dist/application/orm/persistence/support/contracts.d.ts.map +1 -1
  57. package/dist/application/orm/persistence/support/contracts.js +0 -2
  58. package/dist/application/orm/persistence/support/contracts.js.map +1 -1
  59. package/dist/application/orm/persistence/support/helpers.d.ts +3 -3
  60. package/dist/application/orm/persistence/support/helpers.d.ts.map +1 -1
  61. package/dist/application/orm/persistence/support/helpers.js +12 -9
  62. package/dist/application/orm/persistence/support/helpers.js.map +1 -1
  63. package/dist/application/orm/persistence/support/timing.d.ts +2 -2
  64. package/dist/application/orm/persistence/support/timing.d.ts.map +1 -1
  65. package/dist/application/orm/persistence/support/timing.js +4 -4
  66. package/dist/application/orm/persistence/support/timing.js.map +1 -1
  67. package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
  68. package/dist/application/sync/service/SyncServiceBootstrap.js +3 -3
  69. package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
  70. package/dist/cli/args.d.ts.map +1 -1
  71. package/dist/cli/args.js +16 -0
  72. package/dist/cli/args.js.map +1 -1
  73. package/dist/cli/keyProvision.d.ts +38 -0
  74. package/dist/cli/keyProvision.d.ts.map +1 -1
  75. package/dist/cli/keyProvision.js +86 -13
  76. package/dist/cli/keyProvision.js.map +1 -1
  77. package/dist/cli/saVerify.d.ts +37 -0
  78. package/dist/cli/saVerify.d.ts.map +1 -1
  79. package/dist/cli/saVerify.js +46 -0
  80. package/dist/cli/saVerify.js.map +1 -1
  81. package/dist/cli/setup.d.ts +23 -2
  82. package/dist/cli/setup.d.ts.map +1 -1
  83. package/dist/cli/setup.js +40 -4
  84. package/dist/cli/setup.js.map +1 -1
  85. package/dist/cli/setupFlow.d.ts +8 -0
  86. package/dist/cli/setupFlow.d.ts.map +1 -1
  87. package/dist/cli/setupFlow.js +206 -49
  88. package/dist/cli/setupFlow.js.map +1 -1
  89. package/dist/cli/setupProgress.d.ts +385 -0
  90. package/dist/cli/setupProgress.d.ts.map +1 -0
  91. package/dist/cli/setupProgress.js +1009 -0
  92. package/dist/cli/setupProgress.js.map +1 -0
  93. package/package.json +1 -1
  94. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.d.ts +0 -55
  95. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.d.ts.map +0 -1
  96. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.js +0 -120
  97. package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.js.map +0 -1
  98. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts +0 -56
  99. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts.map +0 -1
  100. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.js.map +0 -1
  101. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.d.ts +0 -38
  102. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.d.ts.map +0 -1
  103. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.js +0 -57
  104. package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.js.map +0 -1
  105. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.d.ts +0 -19
  106. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.d.ts.map +0 -1
  107. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.js +0 -67
  108. package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.js.map +0 -1
  109. package/dist/application/orm/api/TypedSheetsOrm.d.ts +0 -71
  110. package/dist/application/orm/api/TypedSheetsOrm.d.ts.map +0 -1
  111. package/dist/application/orm/api/TypedSheetsOrm.js +0 -116
  112. package/dist/application/orm/api/TypedSheetsOrm.js.map +0 -1
  113. package/dist/application/orm/api/contracts.d.ts +0 -118
  114. package/dist/application/orm/api/contracts.d.ts.map +0 -1
  115. package/dist/application/orm/api/contracts.js +0 -21
  116. package/dist/application/orm/api/contracts.js.map +0 -1
@@ -73,6 +73,7 @@ import { defaultSetupStateWriteFs } from "./checkpoint.js";
73
73
  import { SETUP_ERROR_CODES } from "./errors.js";
74
74
  import { describeGcloudFailure, errorResult, outcomeOf } from "./flowResult.js";
75
75
  import { createSafeRunner } from "./gcloudRunner.js";
76
+ import { boundedCheckReporter, safeProgressSink, SETUP_PROGRESS_OPERATIONS, } from "./setupProgress.js";
76
77
  import { checkHumanDriveAccess, DRIVE_ACCESS_COMMAND, } from "./humanAuth.js";
77
78
  import { KEY_STAGE_PLACEHOLDER, listUserManagedServiceAccountKeys, realSleeper, settleServiceAccountKey, } from "./keyProvision.js";
78
79
  import { httpStatusOf, safeReasonOf } from "./sdkError.js";
@@ -92,6 +93,44 @@ const PROJECT_NOT_FOUND_MARKER = "not found";
92
93
  export function defaultSpreadsheetTitle(projectId) {
93
94
  return `${DEFAULT_SPREADSHEET_TITLE_PREFIX}-${projectId}`;
94
95
  }
96
+ /**
97
+ * Setup phases a checkpoint status guarantees as already complete.
98
+ *
99
+ * Resume semantics: `project_selected` guarantees nothing (the project is
100
+ * only decided, not yet verified), and the in-progress write-ahead states
101
+ * (`key_create_started`, `spreadsheet_create_started`,
102
+ * `spreadsheet_share_started`) guarantee everything BEFORE the phase they
103
+ * started but not the phase itself. The cloud-auth and Drive-access phases
104
+ * are NEVER checkpoint-complete — every run re-runs them fresh — and the
105
+ * output phase is never checkpoint-complete because the `.env` write runs
106
+ * on every successful run (even a `complete` resume rewrites `.env`).
107
+ */
108
+ function checkpointCompletedPhases(status) {
109
+ switch (status) {
110
+ case "project_selected":
111
+ return [];
112
+ case "key_create_started":
113
+ return ["project", "apis", "service_account"];
114
+ case "key_ready":
115
+ case "spreadsheet_create_started":
116
+ return ["project", "apis", "service_account", "service_account_key"];
117
+ case "spreadsheet_created":
118
+ case "spreadsheet_share_started":
119
+ return ["project", "apis", "service_account", "service_account_key", "spreadsheet"];
120
+ case "spreadsheet_shared":
121
+ return ["project", "apis", "service_account", "service_account_key", "spreadsheet", "share"];
122
+ case "complete":
123
+ return [
124
+ "project",
125
+ "apis",
126
+ "service_account",
127
+ "service_account_key",
128
+ "spreadsheet",
129
+ "share",
130
+ "sa_access",
131
+ ];
132
+ }
133
+ }
95
134
  /**
96
135
  * Generates a `hikoutei-<timestamp>-<random>` project id.
97
136
  *
@@ -499,8 +538,14 @@ export async function runSetup(options) {
499
538
  // secrets) never reaches a message or the CLI `unexpected` handler.
500
539
  const runner = createSafeRunner(options.runner);
501
540
  const keySleeper = options.sleeper ?? realSleeper;
541
+ // The progress sink is swallowed-safe: an absent sink or a throwing
542
+ // renderer callback never affects the setup result or mutation order.
543
+ const progress = safeProgressSink(options.progress);
502
544
  // Preflight: gcloud must exist and an active account must be logged in.
545
+ progress.report({ type: "phase_started", phase: "cloud_auth" });
546
+ progress.report({ type: "operation_started", phase: "cloud_auth", operation: SETUP_PROGRESS_OPERATIONS.GCLOUD_PRESENCE });
503
547
  const version = await runner.run(["--version"]);
548
+ progress.report({ type: "operation_completed", phase: "cloud_auth", operation: SETUP_PROGRESS_OPERATIONS.GCLOUD_PRESENCE });
504
549
  executed.push({ kind: "gcloud", command: ["--version"], outcome: outcomeOf(version, "gcloud is installed") });
505
550
  if (version.status === "not_found") {
506
551
  return errorResult(SETUP_ERROR_CODES.GCLOUD_MISSING, "gcloud CLI was not found on PATH; install it from https://cloud.google.com/sdk and try again");
@@ -508,7 +553,9 @@ export async function runSetup(options) {
508
553
  if (version.status === "failed") {
509
554
  return errorResult(SETUP_ERROR_CODES.GCLOUD_MISSING, `gcloud --version failed: ${describeGcloudFailure(version)}`);
510
555
  }
556
+ progress.report({ type: "operation_started", phase: "cloud_auth", operation: SETUP_PROGRESS_OPERATIONS.ACTIVE_ACCOUNT });
511
557
  const auth = await runner.run([...AUTH_LIST_ARGS]);
558
+ progress.report({ type: "operation_completed", phase: "cloud_auth", operation: SETUP_PROGRESS_OPERATIONS.ACTIVE_ACCOUNT });
512
559
  executed.push({ kind: "gcloud", command: [...AUTH_LIST_ARGS], outcome: outcomeOf(auth, "active account found") });
513
560
  if (auth.status !== "ok") {
514
561
  // Both auth-list failure branches (invocation failure here, empty list
@@ -531,9 +578,13 @@ export async function runSetup(options) {
531
578
  // Drive-enabled login, never the bare `gcloud auth login`).
532
579
  `no active gcloud account; run \`gcloud ${DRIVE_ACCESS_COMMAND.join(" ")}\` and try again`);
533
580
  }
581
+ progress.report({ type: "phase_completed", phase: "cloud_auth", source: "run" });
534
582
  // Human auth: retrieve the user token and require Drive scope BEFORE any
535
583
  // cloud or file mutation. The token stays in memory for this run only.
584
+ progress.report({ type: "phase_started", phase: "drive_access" });
585
+ progress.report({ type: "operation_started", phase: "drive_access", operation: SETUP_PROGRESS_OPERATIONS.DRIVE_SCOPE });
536
586
  const human = await checkHumanDriveAccess(runner, options.validateToken);
587
+ progress.report({ type: "operation_completed", phase: "drive_access", operation: SETUP_PROGRESS_OPERATIONS.DRIVE_SCOPE });
537
588
  executed.push({
538
589
  kind: "gcloud",
539
590
  command: ["auth", "print-access-token"],
@@ -547,6 +598,7 @@ export async function runSetup(options) {
547
598
  label: "POST oauth2.googleapis.com/tokeninfo",
548
599
  outcome: `access token valid for ${human.ownerEmail} (memory only)`,
549
600
  });
601
+ progress.report({ type: "phase_completed", phase: "drive_access", source: "run" });
550
602
  // Exclusive setup lock: acquired after the human preflight, before any
551
603
  // checkpoint/cloud/file mutation, and released on every exit (success,
552
604
  // error, or throw). An existing lock entry — file, directory, or symlink —
@@ -563,7 +615,7 @@ export async function runSetup(options) {
563
615
  return errorResult(SETUP_ERROR_CODES.SETUP_LOCK_FAILED, lock.message);
564
616
  }
565
617
  try {
566
- return await runSetupLocked(options, executed, human, runner, keySleeper);
618
+ return await runSetupLocked(options, executed, human, runner, keySleeper, progress);
567
619
  }
568
620
  finally {
569
621
  releaseSetupLock(lockPath, lock.identity, options.lockFs);
@@ -577,7 +629,7 @@ export async function runSetup(options) {
577
629
  * `runner` is the safe-wrapped runner and `keySleeper` the propagation
578
630
  * poll timer established by `runSetup`.
579
631
  */
580
- async function runSetupLocked(options, executed, human, runner, keySleeper) {
632
+ async function runSetupLocked(options, executed, human, runner, keySleeper, progress) {
581
633
  const accessToken = human.accessToken;
582
634
  const ownerEmail = human.ownerEmail;
583
635
  // Checkpoint: load and validate; resume skips completed work, mismatched
@@ -587,6 +639,16 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
587
639
  return errorResult(SETUP_ERROR_CODES.SETUP_STATE_INVALID, checkpointResult.message);
588
640
  }
589
641
  const checkpoint = checkpointResult.status === "loaded" ? checkpointResult.state : undefined;
642
+ // Report the resume context once: the phases a checkpoint guarantees as
643
+ // already complete. cloud_auth and drive_access are never
644
+ // checkpoint-complete (every run re-runs them fresh), and the output phase
645
+ // is never checkpoint-complete (the .env write runs on every success).
646
+ if (checkpoint !== undefined) {
647
+ progress.report({
648
+ type: "resumed",
649
+ completedFromCheckpoint: checkpointCompletedPhases(checkpoint.status),
650
+ });
651
+ }
590
652
  // Key file: on resume it must exist and match the checkpoint once the
591
653
  // key phase was reached (key_ready and later); a `key_create_started`
592
654
  // checkpoint defers key handling to the reconciliation step because the
@@ -666,6 +728,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
666
728
  const needsProjectPhase = checkpoint === undefined || checkpoint.status === "project_selected";
667
729
  let projectId;
668
730
  let projectReused = false;
731
+ if (needsProjectPhase) {
732
+ progress.report({ type: "phase_started", phase: "project" });
733
+ }
669
734
  if (!needsProjectPhase) {
670
735
  projectId = checkpoint.projectId;
671
736
  // Resuming past project selection: the project was decided (and created
@@ -677,7 +742,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
677
742
  if (requested === undefined) {
678
743
  return errorResult(SETUP_ERROR_CODES.PROJECT_NOT_FOUND, "no project id is available for this setup run; pass --project <id>");
679
744
  }
745
+ progress.report({ type: "operation_started", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_VERIFY });
680
746
  const describe = await runner.run(["projects", "describe", requested]);
747
+ progress.report({ type: "operation_completed", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_VERIFY });
681
748
  executed.push({
682
749
  kind: "gcloud",
683
750
  command: ["projects", "describe", requested],
@@ -709,7 +776,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
709
776
  if (saveError !== null) {
710
777
  return saveError;
711
778
  }
779
+ progress.report({ type: "operation_started", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_CREATE });
712
780
  const createOutcome = await createProjectOnce(runner, executed, projectId);
781
+ progress.report({ type: "operation_completed", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_CREATE });
713
782
  if (createOutcome.status === "error") {
714
783
  return createOutcome.error;
715
784
  }
@@ -721,7 +790,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
721
790
  // create only when the describe confirms the project is absent. With a
722
791
  // matching --project the effective mode is explicit, so this branch
723
792
  // never runs for it and projects create is never called.
793
+ progress.report({ type: "operation_started", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_VERIFY });
724
794
  const describe = await runner.run(["projects", "describe", projectId]);
795
+ progress.report({ type: "operation_completed", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_VERIFY });
725
796
  executed.push({
726
797
  kind: "gcloud",
727
798
  command: ["projects", "describe", projectId],
@@ -731,7 +802,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
731
802
  projectReused = true;
732
803
  }
733
804
  else if (describe.status === "failed" && describe.stderr.includes(PROJECT_NOT_FOUND_MARKER)) {
805
+ progress.report({ type: "operation_started", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_CREATE });
734
806
  const createOutcome = await createProjectOnce(runner, executed, projectId);
807
+ progress.report({ type: "operation_completed", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_CREATE });
735
808
  if (createOutcome.status === "error") {
736
809
  return createOutcome.error;
737
810
  }
@@ -749,7 +822,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
749
822
  const email = serviceAccountEmail(options.saName, projectId);
750
823
  let serviceAccountReused = false;
751
824
  if (needsProjectPhase) {
825
+ progress.report({ type: "operation_started", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_SELECT });
752
826
  const configSet = await runner.run(["config", "set", "project", projectId]);
827
+ progress.report({ type: "operation_completed", phase: "project", operation: SETUP_PROGRESS_OPERATIONS.PROJECT_SELECT });
753
828
  executed.push({
754
829
  kind: "gcloud",
755
830
  command: ["config", "set", "project", projectId],
@@ -758,6 +833,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
758
833
  if (configSet.status !== "ok") {
759
834
  return errorResult(SETUP_ERROR_CODES.PROJECT_SELECT_FAILED, `could not select project "${projectId}": ${describeGcloudFailure(configSet)}`);
760
835
  }
836
+ progress.report({ type: "phase_completed", phase: "project", source: "run" });
837
+ progress.report({ type: "phase_started", phase: "apis" });
838
+ progress.report({ type: "operation_started", phase: "apis", operation: SETUP_PROGRESS_OPERATIONS.API_ENABLE });
761
839
  const enable = await runner.run([
762
840
  "services",
763
841
  "enable",
@@ -766,6 +844,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
766
844
  "--project",
767
845
  projectId,
768
846
  ]);
847
+ progress.report({ type: "operation_completed", phase: "apis", operation: SETUP_PROGRESS_OPERATIONS.API_ENABLE });
769
848
  executed.push({
770
849
  kind: "gcloud",
771
850
  command: ["services", "enable", "sheets.googleapis.com", "drive.googleapis.com", "--project", projectId],
@@ -774,8 +853,12 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
774
853
  if (enable.status !== "ok") {
775
854
  return errorResult(SETUP_ERROR_CODES.API_ENABLE_FAILED, `could not enable sheets.googleapis.com and drive.googleapis.com: ${describeGcloudFailure(enable)}`);
776
855
  }
856
+ progress.report({ type: "phase_completed", phase: "apis", source: "run" });
857
+ progress.report({ type: "phase_started", phase: "service_account" });
777
858
  // Service account: reuse by email when it already exists.
859
+ progress.report({ type: "operation_started", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
778
860
  const saList = await runner.run(["iam", "service-accounts", "list", "--project", projectId, "--format=value(email)"]);
861
+ progress.report({ type: "operation_completed", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
779
862
  executed.push({
780
863
  kind: "gcloud",
781
864
  command: ["iam", "service-accounts", "list", "--project", projectId, "--format=value(email)"],
@@ -789,6 +872,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
789
872
  serviceAccountReused = true;
790
873
  }
791
874
  else {
875
+ progress.report({ type: "operation_started", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
792
876
  const saCreate = await runner.run([
793
877
  "iam",
794
878
  "service-accounts",
@@ -799,6 +883,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
799
883
  "--display-name",
800
884
  "hikoutei setup",
801
885
  ]);
886
+ progress.report({ type: "operation_completed", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
802
887
  executed.push({
803
888
  kind: "gcloud",
804
889
  command: [
@@ -817,6 +902,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
817
902
  return errorResult(SETUP_ERROR_CODES.SA_CREATE_FAILED, `could not create service account "${options.saName}": ${describeGcloudFailure(saCreate)}`);
818
903
  }
819
904
  }
905
+ progress.report({ type: "phase_completed", phase: "service_account", source: "run" });
820
906
  }
821
907
  // Key: reuse an existing validated key file, or run the write-ahead
822
908
  // create. `key_create_started` resumes by RECONCILING only — the
@@ -829,55 +915,83 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
829
915
  // `key_create_started`, then creates only at the deterministic staging
830
916
  // path. Every path persists `key_ready` immediately after the key is
831
917
  // secured, before the spreadsheet phase; spreadsheet statuses imply key
832
- // readiness.
833
- if (checkpoint?.status === "key_create_started") {
834
- // A resumed checkpoint is RECONCILE-ONLY: the create was already issued
835
- // (or not) by the run that persisted it, so this invocation must never
836
- // create, even when no stage/final/delta is visible; it polls through
837
- // the bounded propagation window instead (see keyProvision.ts).
838
- const settled = await settleServiceAccountKey(runner, executed, {
839
- keyPath: options.keyPath,
840
- projectId,
841
- saEmail: email,
842
- keyMarker: checkpoint.keyMarker,
843
- baseline: checkpoint.keyBaseline,
844
- createPermission: "reconcile",
845
- sleeper: keySleeper,
846
- });
847
- if (settled.status === "error") {
848
- return settled.error;
849
- }
850
- keyReused = settled.keyReused;
851
- }
852
- else if (!keyReused) {
853
- const keyList = await listUserManagedServiceAccountKeys(runner, executed, {
854
- projectId,
855
- saEmail: email,
856
- purpose: "baseline",
857
- });
858
- if (keyList.status === "error") {
859
- return keyList.error;
860
- }
861
- const keyMarker = generateCreationMarker();
862
- const saveError = persistState(options, executed, keyStartedState(options, projectId, ownerEmail, title, email, persistProjectMode, keyMarker, keyList.names));
863
- if (saveError !== null) {
864
- return saveError;
918
+ // readiness. The key phase only EXECUTES WORK when a key must be
919
+ // created or reconciled this run; a resume past `key_ready` already
920
+ // validated and reused the key (marked complete by the resume event).
921
+ // Progress-wise the phase REPORTS on every run the checkpoint does not
922
+ // already guarantee as complete (a key_ready-or-later status does): a
923
+ // fresh run or a `project_selected` resume that reuses an existing key
924
+ // performs no settle/create work, but the phase still starts and
925
+ // completes so the validating tracker keeps phase order and the later
926
+ // phases stay visible. Emitting these events for a checkpoint-guaranteed
927
+ // key phase would be rejected by the tracker and must never double-count
928
+ // it, so the two flags are deliberately separate.
929
+ const keyPhaseGuaranteedByCheckpoint = checkpoint !== undefined &&
930
+ checkpoint.status !== "project_selected" &&
931
+ checkpoint.status !== "key_create_started";
932
+ const keyPhaseReports = !keyPhaseGuaranteedByCheckpoint;
933
+ const keyWorkRuns = checkpoint?.status === "key_create_started" || !keyReused;
934
+ const keySettleReporter = boundedCheckReporter(progress, "service_account_key", "key_settlement");
935
+ if (keyPhaseReports) {
936
+ progress.report({ type: "phase_started", phase: "service_account_key" });
937
+ }
938
+ if (keyWorkRuns) {
939
+ if (checkpoint?.status === "key_create_started") {
940
+ // A resumed checkpoint is RECONCILE-ONLY: the create was already issued
941
+ // (or not) by the run that persisted it, so this invocation must never
942
+ // create, even when no stage/final/delta is visible; it polls through
943
+ // the bounded propagation window instead (see keyProvision.ts).
944
+ const settled = await settleServiceAccountKey(runner, executed, {
945
+ keyPath: options.keyPath,
946
+ projectId,
947
+ saEmail: email,
948
+ keyMarker: checkpoint.keyMarker,
949
+ baseline: checkpoint.keyBaseline,
950
+ createPermission: "reconcile",
951
+ sleeper: keySleeper,
952
+ onSettleProgress: keySettleReporter,
953
+ });
954
+ if (settled.status === "error") {
955
+ return settled.error;
956
+ }
957
+ keyReused = settled.keyReused;
865
958
  }
866
- // This invocation JUST persisted the fresh checkpoint, so it is the
867
- // only one allowed to issue the single key create (fresh permission).
868
- const settled = await settleServiceAccountKey(runner, executed, {
869
- keyPath: options.keyPath,
870
- projectId,
871
- saEmail: email,
872
- keyMarker,
873
- baseline: keyList.names,
874
- createPermission: "fresh",
875
- sleeper: keySleeper,
876
- });
877
- if (settled.status === "error") {
878
- return settled.error;
959
+ else {
960
+ // Fresh (or project_selected resume without a key): this branch only
961
+ // runs when the key was NOT reused (keyWorkRuns is true and the
962
+ // checkpoint is not key_create_started).
963
+ progress.report({ type: "operation_started", phase: "service_account_key", operation: SETUP_PROGRESS_OPERATIONS.KEY_LIST });
964
+ const keyList = await listUserManagedServiceAccountKeys(runner, executed, {
965
+ projectId,
966
+ saEmail: email,
967
+ purpose: "baseline",
968
+ });
969
+ progress.report({ type: "operation_completed", phase: "service_account_key", operation: SETUP_PROGRESS_OPERATIONS.KEY_LIST });
970
+ if (keyList.status === "error") {
971
+ return keyList.error;
972
+ }
973
+ const keyMarker = generateCreationMarker();
974
+ const saveError = persistState(options, executed, keyStartedState(options, projectId, ownerEmail, title, email, persistProjectMode, keyMarker, keyList.names));
975
+ if (saveError !== null) {
976
+ return saveError;
977
+ }
978
+ // This invocation JUST persisted the fresh checkpoint, so it is the
979
+ // only one allowed to issue the single key create (fresh permission).
980
+ const settled = await settleServiceAccountKey(runner, executed, {
981
+ keyPath: options.keyPath,
982
+ projectId,
983
+ saEmail: email,
984
+ keyMarker,
985
+ baseline: keyList.names,
986
+ createPermission: "fresh",
987
+ sleeper: keySleeper,
988
+ onSettleProgress: keySettleReporter,
989
+ });
990
+ if (settled.status === "error") {
991
+ return settled.error;
992
+ }
993
+ keyReused = settled.keyReused;
879
994
  }
880
- keyReused = settled.keyReused;
881
995
  }
882
996
  // Key provenance for the verify phase: whether the key was CREATED by
883
997
  // the setup (vs reused from a pre-existing credential). The checkpoint
@@ -906,6 +1020,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
906
1020
  return saveError;
907
1021
  }
908
1022
  }
1023
+ if (keyPhaseReports) {
1024
+ progress.report({ type: "phase_completed", phase: "service_account_key", source: "run" });
1025
+ }
909
1026
  // Spreadsheet: generate a local opaque creation marker and persist it as
910
1027
  // `spreadsheet_create_started` BEFORE the one and only remote create
911
1028
  // attempt, which carries the marker as a private `appProperties` entry.
@@ -914,6 +1031,21 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
914
1031
  // state. Resuming a started state reconciles by marker only.
915
1032
  const api = options.createHumanApi(accessToken);
916
1033
  let spreadsheet;
1034
+ // Spreadsheet phase reporting: a status from `spreadsheet_created`
1035
+ // onward is checkpoint-guaranteed complete (the `resumed` event already
1036
+ // marked it), so this run must NOT re-emit its start/completion — the
1037
+ // validating tracker would reject the duplicates and the count must
1038
+ // never double-count. `spreadsheet_create_started` (and every earlier
1039
+ // status) leaves the phase current: the create/reconcile work actually
1040
+ // runs there, so the phase still reports start/completion as a run
1041
+ // phase. Derived from the same checkpoint-phase list the `resumed`
1042
+ // event uses, so the reporting decision and the resume list can never
1043
+ // drift apart.
1044
+ const spreadsheetPhaseGuaranteedByCheckpoint = checkpoint !== undefined && checkpointCompletedPhases(checkpoint.status).includes("spreadsheet");
1045
+ const spreadsheetPhaseReports = !spreadsheetPhaseGuaranteedByCheckpoint;
1046
+ if (spreadsheetPhaseReports) {
1047
+ progress.report({ type: "phase_started", phase: "spreadsheet" });
1048
+ }
917
1049
  // Spreadsheet statuses imply key readiness; `key_create_started` and
918
1050
  // `key_ready` both start (or continue) spreadsheet creation from a fresh
919
1051
  // marker. Only a spreadsheet status resumes by marker or by stored id.
@@ -925,6 +1057,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
925
1057
  checkpoint.status === "complete");
926
1058
  if (resumeSpreadsheet) {
927
1059
  if (checkpoint.status === "spreadsheet_create_started") {
1060
+ progress.report({ type: "operation_started", phase: "spreadsheet", operation: SETUP_PROGRESS_OPERATIONS.SHEET_RECONCILE });
928
1061
  const reconciled = await reconcileSpreadsheetByMarker(api, options, executed, {
929
1062
  projectId,
930
1063
  ownerEmail,
@@ -934,6 +1067,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
934
1067
  projectMode: persistProjectMode,
935
1068
  keyOrigin,
936
1069
  });
1070
+ progress.report({ type: "operation_completed", phase: "spreadsheet", operation: SETUP_PROGRESS_OPERATIONS.SHEET_RECONCILE });
937
1071
  if (reconciled.status === "error") {
938
1072
  return reconciled.error;
939
1073
  }
@@ -949,6 +1083,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
949
1083
  if (saveError !== null) {
950
1084
  return saveError;
951
1085
  }
1086
+ progress.report({ type: "operation_started", phase: "spreadsheet", operation: SETUP_PROGRESS_OPERATIONS.SHEET_CREATE });
952
1087
  const ensured = await createSpreadsheetWithMarker(api, options, executed, {
953
1088
  projectId,
954
1089
  ownerEmail,
@@ -958,11 +1093,15 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
958
1093
  projectMode: persistProjectMode,
959
1094
  keyOrigin,
960
1095
  });
1096
+ progress.report({ type: "operation_completed", phase: "spreadsheet", operation: SETUP_PROGRESS_OPERATIONS.SHEET_CREATE });
961
1097
  if (ensured.status === "error") {
962
1098
  return ensured.error;
963
1099
  }
964
1100
  spreadsheet = ensured.spreadsheet;
965
1101
  }
1102
+ if (spreadsheetPhaseReports) {
1103
+ progress.report({ type: "phase_completed", phase: "spreadsheet", source: "run" });
1104
+ }
966
1105
  // Share: ensure the service account is a writer and verify Drive metadata.
967
1106
  // The share is a write-ahead: `spreadsheet_share_started` (spreadsheet id
968
1107
  // + keyOrigin, no shareOrigin) is persisted BEFORE the idempotent ensure
@@ -978,6 +1117,9 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
978
1117
  let saWriterRole = "unchanged";
979
1118
  const alreadyShared = checkpoint !== undefined &&
980
1119
  (checkpoint.status === "spreadsheet_shared" || checkpoint.status === "complete");
1120
+ if (!alreadyShared) {
1121
+ progress.report({ type: "phase_started", phase: "share" });
1122
+ }
981
1123
  // Share provenance for the verify phase: whether the SA writer permission
982
1124
  // was created/upgraded by the setup (fresh) or reused. Persisted from
983
1125
  // `spreadsheet_shared` onward so a resumed shared-but-unverified state
@@ -1000,6 +1142,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1000
1142
  }
1001
1143
  }
1002
1144
  let outcome;
1145
+ progress.report({ type: "operation_started", phase: "share", operation: SETUP_PROGRESS_OPERATIONS.SHARE });
1003
1146
  try {
1004
1147
  outcome = await api.ensureSaWriter({
1005
1148
  spreadsheetId: spreadsheet.spreadsheetId,
@@ -1010,6 +1153,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1010
1153
  catch (error) {
1011
1154
  return errorResult(SETUP_ERROR_CODES.SHEET_SHARE_FAILED, `could not share spreadsheet ${spreadsheet.spreadsheetId} with ${email}: ${safeReasonOf(error)}`);
1012
1155
  }
1156
+ progress.report({ type: "operation_completed", phase: "share", operation: SETUP_PROGRESS_OPERATIONS.SHARE });
1013
1157
  saWriterRole = outcome.writerRole;
1014
1158
  shareOrigin =
1015
1159
  outcome.writerRole === "created" || outcome.writerRole === "upgraded" || shareStartedLoaded
@@ -1025,13 +1169,18 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1025
1169
  return saveError;
1026
1170
  }
1027
1171
  }
1172
+ if (!alreadyShared) {
1173
+ progress.report({ type: "phase_completed", phase: "share", source: "run" });
1174
+ }
1028
1175
  // SA verify: the key must read the spreadsheet (retried only for
1029
1176
  // propagation-class failures of resources created THIS run) before .env
1030
1177
  // is written. The validated key credential is promoted into memory at the
1031
1178
  // secure descriptor boundary and handed to the verifier IN MEMORY: the
1032
1179
  // verifier never reopens the key pathname, so a mid-run replacement of
1033
1180
  // the key file cannot redirect verification to a different credential.
1181
+ const saVerifyReporter = boundedCheckReporter(progress, "sa_access", "sa_access");
1034
1182
  if (checkpoint?.status !== "complete") {
1183
+ progress.report({ type: "phase_started", phase: "sa_access" });
1035
1184
  const keyCredential = readServiceAccountKeyCredentialSecurely(options.keyPath);
1036
1185
  if (keyCredential.status === "absent") {
1037
1186
  return errorResult(SETUP_ERROR_CODES.SA_ACCESS_VERIFY_FAILED, `the service-account key file ${options.keyPath} recorded in the setup state is missing`);
@@ -1058,6 +1207,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1058
1207
  // for a fresh key, 403/404 for a fresh share).
1059
1208
  keyFresh: keyOrigin === "created",
1060
1209
  shareFresh: shareOrigin === "fresh",
1210
+ onVerifyProgress: saVerifyReporter,
1061
1211
  // In-memory validated credentials for the run: the private key
1062
1212
  // exists only in process memory and is never stored or logged.
1063
1213
  credentials: {
@@ -1074,6 +1224,7 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1074
1224
  label: `spreadsheets.get with the ${email} key`,
1075
1225
  outcome: "service-account access verified",
1076
1226
  });
1227
+ progress.report({ type: "phase_completed", phase: "sa_access", source: "run" });
1077
1228
  }
1078
1229
  // .env: update only the two managed keys, preserving unrelated lines. The
1079
1230
  // spreadsheet URL is derived from the id — never trusted from storage.
@@ -1084,6 +1235,8 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1084
1235
  if (envPreflight !== null) {
1085
1236
  return envPreflight;
1086
1237
  }
1238
+ progress.report({ type: "phase_started", phase: "output" });
1239
+ progress.report({ type: "operation_started", phase: "output", operation: SETUP_PROGRESS_OPERATIONS.ENV_WRITE });
1087
1240
  let envResult;
1088
1241
  try {
1089
1242
  envResult = writeSetupEnvFile(options.outputPath, options.keyPath, spreadsheetEditUrl(spreadsheet.spreadsheetId), [options.statePath, setupLockPath(options.statePath), setupStateTempPath(options.statePath)]);
@@ -1091,15 +1244,19 @@ async function runSetupLocked(options, executed, human, runner, keySleeper) {
1091
1244
  catch (error) {
1092
1245
  return errorResult(SETUP_ERROR_CODES.OUTPUT_WRITE_FAILED, `could not write ${options.outputPath}: ${messageOf(error)}`);
1093
1246
  }
1247
+ progress.report({ type: "operation_completed", phase: "output", operation: SETUP_PROGRESS_OPERATIONS.ENV_WRITE });
1094
1248
  // Complete checkpoint: retained so reruns stay no-ops. Starting fresh
1095
1249
  // requires removing BOTH the checkpoint and the key file (or passing the
1096
1250
  // matching --project to recover); cloud resources are never deleted.
1097
1251
  if (checkpoint?.status !== "complete") {
1252
+ progress.report({ type: "operation_started", phase: "output", operation: SETUP_PROGRESS_OPERATIONS.CHECKPOINT_PERSIST });
1098
1253
  const saveError = persistState(options, executed, spreadsheetState(options, projectId, ownerEmail, title, email, spreadsheet, "complete", persistProjectMode, keyOrigin, shareOrigin));
1254
+ progress.report({ type: "operation_completed", phase: "output", operation: SETUP_PROGRESS_OPERATIONS.CHECKPOINT_PERSIST });
1099
1255
  if (saveError !== null) {
1100
1256
  return saveError;
1101
1257
  }
1102
1258
  }
1259
+ progress.report({ type: "phase_completed", phase: "output", source: "run" });
1103
1260
  return {
1104
1261
  status: "ok",
1105
1262
  dryRun: false,