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.
- package/README.md +17 -0
- package/dist/adapter/persistence/contracts/scalar.d.ts +34 -0
- package/dist/adapter/persistence/contracts/scalar.d.ts.map +1 -1
- package/dist/adapter/persistence/contracts/scalar.js +6 -1
- package/dist/adapter/persistence/contracts/scalar.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts +19 -20
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js +117 -67
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmScalarPersistenceProvider.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.d.ts +31 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.d.ts.map +1 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/{MikroOrmMappedTypedSheets.js → MikroOrmMappedRuntime.js} +10 -31
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedRuntime.js.map +1 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarEntityRuntime.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarRuntime.js.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.d.ts +12 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.d.ts.map +1 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.js +20 -0
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmScalarStorage.js.map +1 -0
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.d.ts +2 -7
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.d.ts.map +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.js +5 -11
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmSqliteAdapter.js.map +1 -1
- package/dist/api/Hikoutei.d.ts.map +1 -1
- package/dist/api/Hikoutei.js +3 -4
- package/dist/api/Hikoutei.js.map +1 -1
- package/dist/api/errors.d.ts +2 -0
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +2 -0
- package/dist/api/errors.js.map +1 -1
- package/dist/api/internalEntityManager.d.ts.map +1 -1
- package/dist/api/internalEntityManager.js +31 -9
- package/dist/api/internalEntityManager.js.map +1 -1
- package/dist/application/orm/mapping/contracts.d.ts +8 -3
- package/dist/application/orm/mapping/contracts.d.ts.map +1 -1
- package/dist/application/orm/mapping/values.d.ts +2 -0
- package/dist/application/orm/mapping/values.d.ts.map +1 -1
- package/dist/application/orm/mapping/values.js +10 -3
- package/dist/application/orm/mapping/values.js.map +1 -1
- package/dist/application/orm/persistence/flush/flushCoordinator.d.ts +2 -2
- package/dist/application/orm/persistence/flush/flushCoordinator.d.ts.map +1 -1
- package/dist/application/orm/persistence/flush/flushCoordinator.js +9 -16
- package/dist/application/orm/persistence/flush/flushCoordinator.js.map +1 -1
- package/dist/application/orm/persistence/lifecycle/entityLifecycle.d.ts.map +1 -1
- package/dist/application/orm/persistence/lifecycle/entityLifecycle.js +26 -13
- package/dist/application/orm/persistence/lifecycle/entityLifecycle.js.map +1 -1
- package/dist/application/orm/persistence/projection/projectionEffects.d.ts +2 -2
- package/dist/application/orm/persistence/projection/projectionEffects.d.ts.map +1 -1
- package/dist/application/orm/persistence/projection/projectionEffects.js +6 -6
- package/dist/application/orm/persistence/projection/projectionEffects.js.map +1 -1
- package/dist/application/orm/persistence/scalar/unitOfWork.d.ts +21 -2
- package/dist/application/orm/persistence/scalar/unitOfWork.d.ts.map +1 -1
- package/dist/application/orm/persistence/scalar/unitOfWork.js +107 -8
- package/dist/application/orm/persistence/scalar/unitOfWork.js.map +1 -1
- package/dist/application/orm/persistence/support/contracts.d.ts +6 -6
- package/dist/application/orm/persistence/support/contracts.d.ts.map +1 -1
- package/dist/application/orm/persistence/support/contracts.js +0 -2
- package/dist/application/orm/persistence/support/contracts.js.map +1 -1
- package/dist/application/orm/persistence/support/helpers.d.ts +3 -3
- package/dist/application/orm/persistence/support/helpers.d.ts.map +1 -1
- package/dist/application/orm/persistence/support/helpers.js +12 -9
- package/dist/application/orm/persistence/support/helpers.js.map +1 -1
- package/dist/application/orm/persistence/support/timing.d.ts +2 -2
- package/dist/application/orm/persistence/support/timing.d.ts.map +1 -1
- package/dist/application/orm/persistence/support/timing.js +4 -4
- package/dist/application/orm/persistence/support/timing.js.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
- package/dist/application/sync/service/SyncServiceBootstrap.js +3 -3
- package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/keyProvision.d.ts +38 -0
- package/dist/cli/keyProvision.d.ts.map +1 -1
- package/dist/cli/keyProvision.js +86 -13
- package/dist/cli/keyProvision.js.map +1 -1
- package/dist/cli/saVerify.d.ts +37 -0
- package/dist/cli/saVerify.d.ts.map +1 -1
- package/dist/cli/saVerify.js +46 -0
- package/dist/cli/saVerify.js.map +1 -1
- package/dist/cli/setup.d.ts +23 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +40 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/setupFlow.d.ts +8 -0
- package/dist/cli/setupFlow.d.ts.map +1 -1
- package/dist/cli/setupFlow.js +206 -49
- package/dist/cli/setupFlow.js.map +1 -1
- package/dist/cli/setupProgress.d.ts +385 -0
- package/dist/cli/setupProgress.d.ts.map +1 -0
- package/dist/cli/setupProgress.js +1009 -0
- package/dist/cli/setupProgress.js.map +1 -0
- package/package.json +1 -1
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.d.ts +0 -55
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.d.ts.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.js +0 -120
- package/dist/adapter/persistence/providers/mikro-orm/api/MikroOrmTypedSheetsEntityManager.js.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts +0 -56
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.d.ts.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmMappedTypedSheets.js.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.d.ts +0 -38
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.d.ts.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.js +0 -57
- package/dist/adapter/persistence/providers/mikro-orm/engine/MikroOrmTypedSheetsEngine.js.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.d.ts +0 -19
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.d.ts.map +0 -1
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.js +0 -67
- package/dist/adapter/persistence/providers/mikro-orm/storage/MikroOrmFlushChanges.js.map +0 -1
- package/dist/application/orm/api/TypedSheetsOrm.d.ts +0 -71
- package/dist/application/orm/api/TypedSheetsOrm.d.ts.map +0 -1
- package/dist/application/orm/api/TypedSheetsOrm.js +0 -116
- package/dist/application/orm/api/TypedSheetsOrm.js.map +0 -1
- package/dist/application/orm/api/contracts.d.ts +0 -118
- package/dist/application/orm/api/contracts.d.ts.map +0 -1
- package/dist/application/orm/api/contracts.js +0 -21
- package/dist/application/orm/api/contracts.js.map +0 -1
package/dist/cli/setupFlow.js
CHANGED
|
@@ -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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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,
|