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
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-by-step progress reporting for `hikoutei setup`.
|
|
3
|
+
*
|
|
4
|
+
* The setup flow creates several remote Google Cloud resources and waits on
|
|
5
|
+
* asynchronous IAM/ACL propagation, so a run can take minutes with no stdout
|
|
6
|
+
* activity. This module renders that progress to stderr as two bars:
|
|
7
|
+
*
|
|
8
|
+
* - an OVERALL bar that advances one segment per completed setup phase
|
|
9
|
+
* (never a guessed percentage or an ETA — only finished work moves it);
|
|
10
|
+
* and
|
|
11
|
+
* - a DETAIL bar for the bounded retry polls inside the key-settlement and
|
|
12
|
+
* service-account access-verification phases (how many of the eight
|
|
13
|
+
* propagation checks have run, and during a known wait how far through
|
|
14
|
+
* that wait the clock is) plus a fixed `working…` label for
|
|
15
|
+
* unknown-duration steps.
|
|
16
|
+
*
|
|
17
|
+
* The flow reports progress through a discriminated
|
|
18
|
+
* {@link SetupProgressEvent} union: `resumed` (once, with the phases a
|
|
19
|
+
* checkpoint guarantees), `phase_started` / `phase_completed` at phase
|
|
20
|
+
* boundaries, `operation_started` / `operation_completed` around the
|
|
21
|
+
* notable remote/local steps (fixed safe labels; the bounded propagation
|
|
22
|
+
* checks additionally carry 1-based attempt info), `retry_wait_started`
|
|
23
|
+
* before each bounded sleep, and `phase_failed` (a stable
|
|
24
|
+
* {@link SetupErrorCode} only) emitted by the CLI controller when a run
|
|
25
|
+
* ends in error — the flow itself returns a stable error result and never
|
|
26
|
+
* knows it is the final attempt (the interactive login retry can still
|
|
27
|
+
* rescue an auth preflight failure).
|
|
28
|
+
*
|
|
29
|
+
* Two renderers share one validating state machine:
|
|
30
|
+
* - an interactive (TTY, color-capable) renderer that redraws a fixed
|
|
31
|
+
* four-line block in place with ANSI and animates a known wait through
|
|
32
|
+
* an `unref`-ed interval timer; and
|
|
33
|
+
* - an append-only renderer for CI / non-TTY / `NO_COLOR` that prints a
|
|
34
|
+
* static line per phase start / phase completion / bounded-check attempt
|
|
35
|
+
* / retry wait / failure and NEVER uses control sequences, a clock tick,
|
|
36
|
+
* or a line for ordinary operation events (no log spam). The final
|
|
37
|
+
* bounded attempt (8/8) has no following wait line, so it must print its
|
|
38
|
+
* own attempt line or it would be invisible before success/failure.
|
|
39
|
+
*
|
|
40
|
+
* Security contract: progress events and rendered text carry ONLY fixed
|
|
41
|
+
* labels, attempt/delay numbers, and stable error codes. Project ids,
|
|
42
|
+
* service-account emails, owner emails, paths, access tokens, private keys,
|
|
43
|
+
* key ids, raw gcloud output, and raw provider payloads are NEVER placed in
|
|
44
|
+
* an event or written by a renderer. A throwing renderer callback is
|
|
45
|
+
* swallowed by {@link safeProgressSink}, and the controller swallows its
|
|
46
|
+
* own write/scheduler failures, so progress can never change the setup
|
|
47
|
+
* result, the mutation order, or the process exit code. This module is
|
|
48
|
+
* internal CLI machinery only; it is not part of the application-facing
|
|
49
|
+
* API.
|
|
50
|
+
*/
|
|
51
|
+
import { SETUP_ERROR_CODES } from "./errors.js";
|
|
52
|
+
/** The ten setup phases in execution order. */
|
|
53
|
+
export const SETUP_PROGRESS_PHASES = [
|
|
54
|
+
"cloud_auth",
|
|
55
|
+
"drive_access",
|
|
56
|
+
"project",
|
|
57
|
+
"apis",
|
|
58
|
+
"service_account",
|
|
59
|
+
"service_account_key",
|
|
60
|
+
"spreadsheet",
|
|
61
|
+
"share",
|
|
62
|
+
"sa_access",
|
|
63
|
+
"output",
|
|
64
|
+
];
|
|
65
|
+
/** Total number of setup phases (drives the overall bar denominator). */
|
|
66
|
+
export const SETUP_PROGRESS_PHASE_COUNT = SETUP_PROGRESS_PHASES.length;
|
|
67
|
+
/**
|
|
68
|
+
* The phases a checkpoint can ever guarantee as already complete, in order.
|
|
69
|
+
*
|
|
70
|
+
* A valid `resumed` event lists a PREFIX of this sequence: cloud_auth and
|
|
71
|
+
* drive_access are never checkpoint-complete (every run re-runs them
|
|
72
|
+
* fresh) and the output phase is never checkpoint-complete (the `.env`
|
|
73
|
+
* write runs on every successful run). The tracker rejects any other
|
|
74
|
+
* shape (gaps, duplicates, out-of-order entries, unknown phases) as an
|
|
75
|
+
* invalid checkpoint phase list.
|
|
76
|
+
*/
|
|
77
|
+
const RESUMABLE_CHECKPOINT_PHASES = [
|
|
78
|
+
"project",
|
|
79
|
+
"apis",
|
|
80
|
+
"service_account",
|
|
81
|
+
"service_account_key",
|
|
82
|
+
"spreadsheet",
|
|
83
|
+
"share",
|
|
84
|
+
"sa_access",
|
|
85
|
+
];
|
|
86
|
+
/** Fixed, safe human labels for each phase (never secrets). */
|
|
87
|
+
export const SETUP_PROGRESS_LABELS = {
|
|
88
|
+
cloud_auth: "Google Cloud authentication",
|
|
89
|
+
drive_access: "Drive access",
|
|
90
|
+
project: "Project",
|
|
91
|
+
apis: "Sheets and Drive APIs",
|
|
92
|
+
service_account: "Service account",
|
|
93
|
+
service_account_key: "Service-account key",
|
|
94
|
+
spreadsheet: "Spreadsheet",
|
|
95
|
+
share: "Share and ownership",
|
|
96
|
+
sa_access: "Service-account access",
|
|
97
|
+
output: "Checkpoint and .env",
|
|
98
|
+
};
|
|
99
|
+
/** Short labels for the compact "done" line. */
|
|
100
|
+
export const SETUP_PROGRESS_SHORT_LABELS = {
|
|
101
|
+
cloud_auth: "Cloud auth",
|
|
102
|
+
drive_access: "Drive access",
|
|
103
|
+
project: "Project",
|
|
104
|
+
apis: "APIs",
|
|
105
|
+
service_account: "Service account",
|
|
106
|
+
service_account_key: "Key",
|
|
107
|
+
spreadsheet: "Spreadsheet",
|
|
108
|
+
share: "Share",
|
|
109
|
+
sa_access: "SA access",
|
|
110
|
+
output: "Output",
|
|
111
|
+
};
|
|
112
|
+
/** The retry kind a phase can host, if any. */
|
|
113
|
+
const RETRY_KIND_OF_PHASE = {
|
|
114
|
+
service_account_key: "key_settlement",
|
|
115
|
+
sa_access: "sa_access",
|
|
116
|
+
};
|
|
117
|
+
/** Human word for each retry kind, used in "before next <word> check". */
|
|
118
|
+
const RETRY_KIND_WORD = {
|
|
119
|
+
key_settlement: "key",
|
|
120
|
+
sa_access: "access",
|
|
121
|
+
};
|
|
122
|
+
/** Human noun for each retry kind shown between checks. */
|
|
123
|
+
const RETRY_CHECK_NOUN = {
|
|
124
|
+
key_settlement: "key settlement check",
|
|
125
|
+
sa_access: "access check",
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Fixed, safe operation labels for the notable setup steps.
|
|
129
|
+
*
|
|
130
|
+
* The flow emits `operation_started` / `operation_completed` around the
|
|
131
|
+
* unknown-duration calls (gcloud spawns, API calls) so the detail bar can
|
|
132
|
+
* show a fixed `working… <label>` instead of a guessed percentage. Labels
|
|
133
|
+
* are constant text and never carry identifiers, paths, or secrets.
|
|
134
|
+
*/
|
|
135
|
+
export const SETUP_PROGRESS_OPERATIONS = {
|
|
136
|
+
GCLOUD_PRESENCE: "checking gcloud CLI",
|
|
137
|
+
ACTIVE_ACCOUNT: "checking active gcloud account",
|
|
138
|
+
DRIVE_SCOPE: "verifying Drive access",
|
|
139
|
+
PROJECT_VERIFY: "verifying project",
|
|
140
|
+
PROJECT_CREATE: "creating project",
|
|
141
|
+
PROJECT_SELECT: "selecting default project",
|
|
142
|
+
API_ENABLE: "enabling Sheets and Drive APIs",
|
|
143
|
+
SA_LIST: "listing service accounts",
|
|
144
|
+
SA_CREATE: "creating service account",
|
|
145
|
+
KEY_LIST: "listing service-account keys",
|
|
146
|
+
SHEET_CREATE: "creating spreadsheet",
|
|
147
|
+
SHEET_RECONCILE: "reconciling spreadsheet",
|
|
148
|
+
SHARE: "sharing spreadsheet with service account",
|
|
149
|
+
ENV_WRITE: "writing .env",
|
|
150
|
+
CHECKPOINT_PERSIST: "persisting checkpoint",
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Fixed, safe operation labels for the bounded propagation checks.
|
|
154
|
+
*
|
|
155
|
+
* Operation events carrying these labels also carry a
|
|
156
|
+
* {@link BoundedCheckInfo} with the 1-based attempt within the bounded
|
|
157
|
+
* window, so the tracker can keep the detail bar on the propagation check
|
|
158
|
+
* while it runs.
|
|
159
|
+
*/
|
|
160
|
+
export const SETUP_PROGRESS_BOUNDED_OPERATIONS = {
|
|
161
|
+
KEY_SETTLE: "key settlement check",
|
|
162
|
+
SA_ACCESS: "access check",
|
|
163
|
+
};
|
|
164
|
+
/** The bounded-check operation label a phase hosts, if any. */
|
|
165
|
+
function boundedOperationOf(phase) {
|
|
166
|
+
if (RETRY_KIND_OF_PHASE[phase] === "key_settlement") {
|
|
167
|
+
return SETUP_PROGRESS_BOUNDED_OPERATIONS.KEY_SETTLE;
|
|
168
|
+
}
|
|
169
|
+
if (RETRY_KIND_OF_PHASE[phase] === "sa_access") {
|
|
170
|
+
return SETUP_PROGRESS_BOUNDED_OPERATIONS.SA_ACCESS;
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
/** A sink that drops every event; used when progress is disabled. */
|
|
175
|
+
export const NOOP_PROGRESS_SINK = {
|
|
176
|
+
report() {
|
|
177
|
+
/* progress disabled */
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Wraps a sink so a throwing callback (or an absent sink) can never affect
|
|
182
|
+
* the setup run. Returns {@link NOOP_PROGRESS_SINK} for `undefined` so flow
|
|
183
|
+
* call sites can call `progress.report(...)` unconditionally.
|
|
184
|
+
*/
|
|
185
|
+
export function safeProgressSink(sink) {
|
|
186
|
+
if (sink === undefined) {
|
|
187
|
+
return NOOP_PROGRESS_SINK;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
report(event) {
|
|
191
|
+
try {
|
|
192
|
+
sink.report(event);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Progress rendering must never change the setup result, the
|
|
196
|
+
// mutation order, or the exit code.
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Width (in cells) of both progress bars. */
|
|
202
|
+
const PROGRESS_BAR_WIDTH = 20;
|
|
203
|
+
/**
|
|
204
|
+
* Renders a `[████…░░░…]` bar for a 0..1 fill ratio.
|
|
205
|
+
*
|
|
206
|
+
* The fill is clamped to [0, 1] so a clock tick past the end of a known wait
|
|
207
|
+
* never overflows the bar.
|
|
208
|
+
*/
|
|
209
|
+
export function renderProgressBar(fill) {
|
|
210
|
+
const ratio = fill < 0 ? 0 : fill > 1 ? 1 : fill;
|
|
211
|
+
const filled = Math.round(ratio * PROGRESS_BAR_WIDTH);
|
|
212
|
+
return `[${"█".repeat(filled)}${"░".repeat(PROGRESS_BAR_WIDTH - filled)}]`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Whole-percent of completed phases over the total.
|
|
216
|
+
*
|
|
217
|
+
* The input is clamped to the ten logical phases (defense in depth: the
|
|
218
|
+
* tracker count is already clamped, but an out-of-range caller value must
|
|
219
|
+
* never be able to render 110% or a negative percent).
|
|
220
|
+
*/
|
|
221
|
+
export function overallPercent(completed) {
|
|
222
|
+
const clamped = completed < 0 ? 0 : completed > SETUP_PROGRESS_PHASE_COUNT ? SETUP_PROGRESS_PHASE_COUNT : completed;
|
|
223
|
+
return Math.floor((clamped / SETUP_PROGRESS_PHASE_COUNT) * 100);
|
|
224
|
+
}
|
|
225
|
+
/** True when the value names a known phase (runtime boundary guard). */
|
|
226
|
+
function isKnownPhase(value) {
|
|
227
|
+
return typeof value === "string" && SETUP_PROGRESS_PHASES.includes(value);
|
|
228
|
+
}
|
|
229
|
+
/** True when the value is a known generic or bounded operation label. */
|
|
230
|
+
function isKnownOperation(value) {
|
|
231
|
+
if (typeof value !== "string") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return (Object.values(SETUP_PROGRESS_OPERATIONS).includes(value) ||
|
|
235
|
+
Object.values(SETUP_PROGRESS_BOUNDED_OPERATIONS).includes(value));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Fixed size of every bounded propagation window.
|
|
239
|
+
*
|
|
240
|
+
* Both polls (key settlement and SA-access verification) run exactly eight
|
|
241
|
+
* checks (`1/8`..`8/8`); the tracker rejects any attempt/maxAttempts pair
|
|
242
|
+
* that claims a different window size, so impossible values can never
|
|
243
|
+
* reach the detail bar or a rendered line.
|
|
244
|
+
*/
|
|
245
|
+
export const BOUNDED_CHECK_MAX_ATTEMPTS = 8;
|
|
246
|
+
/** True when the value is a structurally valid bounded-check info. */
|
|
247
|
+
function isValidCheckInfo(value) {
|
|
248
|
+
if (typeof value !== "object" || value === null) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const { attempt, maxAttempts } = value;
|
|
252
|
+
return (typeof attempt === "number" &&
|
|
253
|
+
Number.isInteger(attempt) &&
|
|
254
|
+
attempt >= 1 &&
|
|
255
|
+
attempt <= BOUNDED_CHECK_MAX_ATTEMPTS &&
|
|
256
|
+
typeof maxAttempts === "number" &&
|
|
257
|
+
Number.isInteger(maxAttempts) &&
|
|
258
|
+
maxAttempts === BOUNDED_CHECK_MAX_ATTEMPTS &&
|
|
259
|
+
maxAttempts >= attempt);
|
|
260
|
+
}
|
|
261
|
+
/** True when the value is a stable setup error code. */
|
|
262
|
+
function isSetupErrorCode(value) {
|
|
263
|
+
return typeof value === "string" && Object.values(SETUP_ERROR_CODES).includes(value);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Validating progress state machine shared by both renderers.
|
|
267
|
+
*
|
|
268
|
+
* The tracker validates every event and REJECTS (ignores, never throws)
|
|
269
|
+
* invalid ones: phase order (a phase may only start when every earlier
|
|
270
|
+
* phase is complete and no other phase is in progress), duplicate phase
|
|
271
|
+
* completion (a completed phase never increments the count again),
|
|
272
|
+
* operation start/complete pairing (an operation may only complete the
|
|
273
|
+
* exact operation that is active, with matching check attempt info),
|
|
274
|
+
* retry kind/phase consistency, attempt bounds, and checkpoint phase
|
|
275
|
+
* lists (a `resumed` list must be a prefix of the resumable phases).
|
|
276
|
+
* Invalid events are silently dropped so a benign reporting quirk can
|
|
277
|
+
* never silence progress for the rest of a run and can never throw.
|
|
278
|
+
* {@link SetupProgressTracker.resetTransient} clears the in-progress
|
|
279
|
+
* state (current phase, active operation, retry, failure) before the
|
|
280
|
+
* interactive login retry so a re-run can re-emit phase events from
|
|
281
|
+
* scratch; completed phases and the resume flag are kept.
|
|
282
|
+
* {@link SetupProgressTracker.markFailed} records a failure for a phase
|
|
283
|
+
* that is no longer in progress (the controller's `fail()` fallback after
|
|
284
|
+
* a suspend cleared the current phase).
|
|
285
|
+
*/
|
|
286
|
+
export class SetupProgressTracker {
|
|
287
|
+
completed = new Map();
|
|
288
|
+
current;
|
|
289
|
+
operation;
|
|
290
|
+
retry;
|
|
291
|
+
failed;
|
|
292
|
+
resumed = false;
|
|
293
|
+
/**
|
|
294
|
+
* Number of completed phases (any source), clamped to the ten logical
|
|
295
|
+
* phases. Duplicate completions are rejected, so the count can never
|
|
296
|
+
* exceed the total; the clamp is defense in depth.
|
|
297
|
+
*/
|
|
298
|
+
get completedCount() {
|
|
299
|
+
return Math.min(this.completed.size, SETUP_PROGRESS_PHASE_COUNT);
|
|
300
|
+
}
|
|
301
|
+
/** True once a valid `resumed` event was processed. */
|
|
302
|
+
get isResumed() {
|
|
303
|
+
return this.resumed;
|
|
304
|
+
}
|
|
305
|
+
/** The currently in-progress phase, if any. */
|
|
306
|
+
get currentPhase() {
|
|
307
|
+
return this.current;
|
|
308
|
+
}
|
|
309
|
+
/** The active operation (generic step or bounded check), if any. */
|
|
310
|
+
get activeOperation() {
|
|
311
|
+
return this.operation;
|
|
312
|
+
}
|
|
313
|
+
/** The active retry (bounded check/wait context), if any. */
|
|
314
|
+
get activeRetry() {
|
|
315
|
+
return this.retry;
|
|
316
|
+
}
|
|
317
|
+
/** The failed phase/code on a `phase_failed` event, if any. */
|
|
318
|
+
get failure() {
|
|
319
|
+
return this.failed;
|
|
320
|
+
}
|
|
321
|
+
/** True when the phase was completed (by run or checkpoint). */
|
|
322
|
+
isComplete(phase) {
|
|
323
|
+
return this.completed.has(phase);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Resets the in-progress state so a retried run can re-emit phase events
|
|
327
|
+
* from scratch.
|
|
328
|
+
*
|
|
329
|
+
* Called by the renderer's `suspend()` before the inherited `gcloud auth
|
|
330
|
+
* login` handoff: the first attempt may have died mid-phase (for example
|
|
331
|
+
* during `drive_access`), and the retry re-runs the whole flow from
|
|
332
|
+
* `cloud_auth`. Completed phases and the resume flag are kept — the
|
|
333
|
+
* retry re-completes the same phases, and duplicate completion is
|
|
334
|
+
* rejected so the overall count never grows past the real work.
|
|
335
|
+
*/
|
|
336
|
+
resetTransient() {
|
|
337
|
+
this.current = undefined;
|
|
338
|
+
this.operation = undefined;
|
|
339
|
+
this.retry = undefined;
|
|
340
|
+
this.failed = undefined;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Records a failure for a phase that is no longer in progress.
|
|
344
|
+
*
|
|
345
|
+
* The controller uses this as the `fail()` fallback after `suspend()`
|
|
346
|
+
* cleared the current phase for the inherited login handoff — and for
|
|
347
|
+
* the deterministic next-pending phase when a run-level error has no
|
|
348
|
+
* current phase: the final failure frame must still name a phase.
|
|
349
|
+
* Validation matches the `phase_failed` event (known phase, stable
|
|
350
|
+
* code) minus the current-phase requirement; the transient
|
|
351
|
+
* operation/retry state is already clear after the suspend.
|
|
352
|
+
*/
|
|
353
|
+
markFailed(phase, code) {
|
|
354
|
+
if (!isKnownPhase(phase) || !isSetupErrorCode(code)) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
this.failed = { phase, code };
|
|
358
|
+
this.current = undefined;
|
|
359
|
+
this.retry = undefined;
|
|
360
|
+
this.operation = undefined;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
/** True when every phase before `phase` (in execution order) is complete. */
|
|
364
|
+
allEarlierCompleted(phase) {
|
|
365
|
+
const index = SETUP_PROGRESS_PHASES.indexOf(phase);
|
|
366
|
+
if (index === -1) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
for (let i = 0; i < index; i += 1) {
|
|
370
|
+
if (!this.completed.has(SETUP_PROGRESS_PHASES[i])) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
/** True when `phase` may start: no current phase, not completed, ordered. */
|
|
377
|
+
isValidNextPhase(phase) {
|
|
378
|
+
return this.current === undefined && !this.completed.has(phase) && this.allEarlierCompleted(phase);
|
|
379
|
+
}
|
|
380
|
+
/** True when the resumed list is a valid prefix of the resumable phases. */
|
|
381
|
+
isValidCheckpointList(phases) {
|
|
382
|
+
if (!Array.isArray(phases)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
if (phases.length > RESUMABLE_CHECKPOINT_PHASES.length) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
for (let i = 0; i < phases.length; i += 1) {
|
|
389
|
+
if (phases[i] !== RESUMABLE_CHECKPOINT_PHASES[i]) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Processes one event, updating internal state.
|
|
397
|
+
*
|
|
398
|
+
* Never throws: invalid events are rejected (ignored) and `false` is
|
|
399
|
+
* returned so the renderer can skip drawing for them.
|
|
400
|
+
*/
|
|
401
|
+
apply(event) {
|
|
402
|
+
switch (event.type) {
|
|
403
|
+
case "resumed": {
|
|
404
|
+
if (!this.isValidCheckpointList(event.completedFromCheckpoint)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
this.resumed = true;
|
|
408
|
+
for (const phase of event.completedFromCheckpoint) {
|
|
409
|
+
if (!this.completed.has(phase)) {
|
|
410
|
+
this.completed.set(phase, "checkpoint");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
case "phase_started": {
|
|
416
|
+
if (!isKnownPhase(event.phase) || !this.isValidNextPhase(event.phase)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
this.current = event.phase;
|
|
420
|
+
// A re-run of a phase clears any prior failure display (the login
|
|
421
|
+
// retry re-runs cloud_auth/drive_access after a suspended failure).
|
|
422
|
+
this.failed = undefined;
|
|
423
|
+
if (this.retry?.phase === event.phase) {
|
|
424
|
+
this.retry = undefined;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
case "phase_completed": {
|
|
429
|
+
if (!isKnownPhase(event.phase)) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (event.source !== "run" && event.source !== "checkpoint") {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
// Duplicate completion never increments the count again.
|
|
436
|
+
if (this.completed.has(event.phase)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (event.source === "run") {
|
|
440
|
+
if (this.current !== event.phase) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (!this.allEarlierCompleted(event.phase)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
this.completed.set(event.phase, event.source);
|
|
448
|
+
if (this.current === event.phase) {
|
|
449
|
+
this.current = undefined;
|
|
450
|
+
}
|
|
451
|
+
if (this.retry?.phase === event.phase) {
|
|
452
|
+
this.retry = undefined;
|
|
453
|
+
}
|
|
454
|
+
if (this.activeOperation?.phase === event.phase) {
|
|
455
|
+
this.operation = undefined;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
case "operation_started":
|
|
460
|
+
case "operation_completed": {
|
|
461
|
+
if (!isKnownPhase(event.phase) || this.current !== event.phase) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
if ("check" in event) {
|
|
465
|
+
// A bounded check must carry valid attempt info and the exact
|
|
466
|
+
// operation label its phase hosts (kind/phase consistency).
|
|
467
|
+
if (!isValidCheckInfo(event.check)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const bounded = boundedOperationOf(event.phase);
|
|
471
|
+
if (bounded === undefined || event.operation !== bounded) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else if (!isKnownOperation(event.operation)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
if (event.type === "operation_started") {
|
|
479
|
+
this.operation =
|
|
480
|
+
"check" in event
|
|
481
|
+
? { phase: event.phase, operation: event.operation, check: event.check }
|
|
482
|
+
: { phase: event.phase, operation: event.operation };
|
|
483
|
+
if ("check" in event) {
|
|
484
|
+
const kind = RETRY_KIND_OF_PHASE[event.phase];
|
|
485
|
+
if (kind === undefined) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
this.retry = {
|
|
489
|
+
phase: event.phase,
|
|
490
|
+
kind,
|
|
491
|
+
attempt: event.check.attempt,
|
|
492
|
+
maxAttempts: event.check.maxAttempts,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
// operation_completed must pair with the exact active operation
|
|
498
|
+
// (same phase, operation, and check attempt info).
|
|
499
|
+
const active = this.operation;
|
|
500
|
+
if (active === undefined || active.phase !== event.phase || active.operation !== event.operation) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
if ("check" in event) {
|
|
504
|
+
if (!("check" in active) ||
|
|
505
|
+
active.check.attempt !== event.check.attempt ||
|
|
506
|
+
active.check.maxAttempts !== event.check.maxAttempts) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const kind = RETRY_KIND_OF_PHASE[event.phase];
|
|
510
|
+
if (kind === undefined) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
this.retry = {
|
|
514
|
+
phase: event.phase,
|
|
515
|
+
kind,
|
|
516
|
+
attempt: event.check.attempt,
|
|
517
|
+
maxAttempts: event.check.maxAttempts,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
else if ("check" in active) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
this.operation = undefined;
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
case "retry_wait_started": {
|
|
527
|
+
if (!isKnownPhase(event.phase) || this.current !== event.phase) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
// The retry kind must match the phase that hosts it.
|
|
531
|
+
if (RETRY_KIND_OF_PHASE[event.phase] !== event.kind) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
if (!Number.isInteger(event.attempt) ||
|
|
535
|
+
!Number.isInteger(event.maxAttempts) ||
|
|
536
|
+
event.attempt < 1 ||
|
|
537
|
+
event.attempt > BOUNDED_CHECK_MAX_ATTEMPTS ||
|
|
538
|
+
event.maxAttempts !== BOUNDED_CHECK_MAX_ATTEMPTS ||
|
|
539
|
+
event.attempt > event.maxAttempts) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
if (!Number.isFinite(event.delayMs) || event.delayMs < 0) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
this.retry = {
|
|
546
|
+
phase: event.phase,
|
|
547
|
+
kind: event.kind,
|
|
548
|
+
attempt: event.attempt,
|
|
549
|
+
maxAttempts: event.maxAttempts,
|
|
550
|
+
};
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
case "phase_failed": {
|
|
554
|
+
if (!isKnownPhase(event.phase) || this.current !== event.phase) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
if (!isSetupErrorCode(event.code)) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
this.failed = { phase: event.phase, code: event.code };
|
|
561
|
+
this.current = undefined;
|
|
562
|
+
this.retry = undefined;
|
|
563
|
+
this.operation = undefined;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Builds a {@link BoundedCheckReporter} that forwards to a progress sink as
|
|
571
|
+
* operation events (with check attempt info) and `retry_wait_started`
|
|
572
|
+
* events for the given phase and kind. Returns a no-op when the sink is
|
|
573
|
+
* the {@link NOOP_PROGRESS_SINK}, so key provisioning and SA verification
|
|
574
|
+
* stay cheap when progress is disabled.
|
|
575
|
+
*/
|
|
576
|
+
export function boundedCheckReporter(sink, phase, kind) {
|
|
577
|
+
if (sink === NOOP_PROGRESS_SINK) {
|
|
578
|
+
return () => {
|
|
579
|
+
/* progress disabled */
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const operation = kind === "key_settlement" ? SETUP_PROGRESS_BOUNDED_OPERATIONS.KEY_SETTLE : SETUP_PROGRESS_BOUNDED_OPERATIONS.SA_ACCESS;
|
|
583
|
+
return (event) => {
|
|
584
|
+
switch (event.type) {
|
|
585
|
+
case "check_started":
|
|
586
|
+
sink.report({
|
|
587
|
+
type: "operation_started",
|
|
588
|
+
phase,
|
|
589
|
+
operation,
|
|
590
|
+
check: { attempt: event.attempt, maxAttempts: event.maxAttempts },
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
case "check_completed":
|
|
594
|
+
sink.report({
|
|
595
|
+
type: "operation_completed",
|
|
596
|
+
phase,
|
|
597
|
+
operation,
|
|
598
|
+
check: { attempt: event.attempt, maxAttempts: event.maxAttempts },
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
case "wait_started":
|
|
602
|
+
sink.report({
|
|
603
|
+
type: "retry_wait_started",
|
|
604
|
+
phase,
|
|
605
|
+
kind,
|
|
606
|
+
attempt: event.attempt,
|
|
607
|
+
maxAttempts: event.maxAttempts,
|
|
608
|
+
delayMs: event.delayMs,
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* True when the environment marks an automation session.
|
|
616
|
+
*
|
|
617
|
+
* Mirrors the de-facto CI convention: a NON-EMPTY `CI` value (GitHub
|
|
618
|
+
* Actions, GitLab CI, CircleCI, Travis, ...) marks automation, while an
|
|
619
|
+
* empty override keeps interactive behavior (the same empty-override rule
|
|
620
|
+
* as NO_COLOR). Production `main()` passes this into the CLI context so a
|
|
621
|
+
* CI pseudo-TTY can never prompt for the interactive login handoff or
|
|
622
|
+
* spawn the browser login.
|
|
623
|
+
*/
|
|
624
|
+
export function isCiEnvironment(env = process.env) {
|
|
625
|
+
return env.CI !== undefined && env.CI !== "";
|
|
626
|
+
}
|
|
627
|
+
/** True when ANSI color/control output should be used (TTY, not NO_COLOR, not CI). */
|
|
628
|
+
export function shouldUseInteractiveProgress(isTty, env = process.env) {
|
|
629
|
+
if (!isTty) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
// Respect the de-facto NO_COLOR convention (any non-empty value disables
|
|
633
|
+
// color/control output) and a TTY-only NO_COLOR=0/empty override. A CI
|
|
634
|
+
// pseudo-TTY must also stay static: setup progress is documented as one
|
|
635
|
+
// static line per event there — ANSI redraws and animation timers must
|
|
636
|
+
// never run in CI.
|
|
637
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
return !isCiEnvironment(env);
|
|
641
|
+
}
|
|
642
|
+
/** Interval (ms) between frames of the known-wait animation in TTY mode. */
|
|
643
|
+
const WAIT_ANIMATION_INTERVAL_MS = 250;
|
|
644
|
+
/** Marker lines emitted before the static block of the append-only renderer. */
|
|
645
|
+
const APPEND_HEADER_FRESH = "Hikoutei setup progress";
|
|
646
|
+
const APPEND_HEADER_RESUMED_PREFIX = "Hikoutei setup progress (resuming";
|
|
647
|
+
/**
|
|
648
|
+
* First phase (in execution order) that is not yet complete, if any.
|
|
649
|
+
*
|
|
650
|
+
* The deterministic fallback for a run-level error with no current phase
|
|
651
|
+
* (for example lock contention after the login retry's fresh auth phases):
|
|
652
|
+
* the failure frame names the phase the run was about to enter.
|
|
653
|
+
*/
|
|
654
|
+
function nextPendingPhase(tracker) {
|
|
655
|
+
for (const phase of SETUP_PROGRESS_PHASES) {
|
|
656
|
+
if (!tracker.isComplete(phase)) {
|
|
657
|
+
return phase;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Creates the production progress controller.
|
|
664
|
+
*
|
|
665
|
+
* In interactive mode it owns a single animation timer for the known-wait
|
|
666
|
+
* detail bar; the timer is cleared on every subsequent event, on
|
|
667
|
+
* {@link SetupProgressController.suspend} (before the inherited gcloud
|
|
668
|
+
* login), and on {@link SetupProgressController.finish}. The timer is
|
|
669
|
+
* `unref`-ed so it can never be what keeps the process alive (the setup
|
|
670
|
+
* sleep itself holds the run open). In append-only mode no timer is used
|
|
671
|
+
* and ordinary operation events update state without printing a line.
|
|
672
|
+
* All controller methods swallow their own write/scheduler failures so
|
|
673
|
+
* progress can never change the setup result or the exit code.
|
|
674
|
+
*/
|
|
675
|
+
export function createSetupProgressRenderer(options) {
|
|
676
|
+
const interactive = options.interactive ?? shouldUseInteractiveProgress(options.isTty);
|
|
677
|
+
const now = options.now ?? Date.now;
|
|
678
|
+
const setIntervalFn = options.setInterval ?? setInterval;
|
|
679
|
+
const clearIntervalFn = options.clearInterval ?? clearInterval;
|
|
680
|
+
const tracker = new SetupProgressTracker();
|
|
681
|
+
const output = options.output;
|
|
682
|
+
let headerShown = false;
|
|
683
|
+
let blockLines = 0;
|
|
684
|
+
let animationHandle;
|
|
685
|
+
// The known wait being animated (interactive mode only).
|
|
686
|
+
let wait;
|
|
687
|
+
// The phase that was in progress when `suspend()` cleared the tracker
|
|
688
|
+
// for the login handoff. `fail()` falls back to it when the handoff was
|
|
689
|
+
// cancelled or the login failed and no phase is current anymore, so the
|
|
690
|
+
// final failure frame still names the phase the run died in.
|
|
691
|
+
let suspendedPhase;
|
|
692
|
+
/** Writes to the output stream; a closed/erroring stream never fails the run. */
|
|
693
|
+
const safeWrite = (text) => {
|
|
694
|
+
try {
|
|
695
|
+
output.write(text);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// stderr closed or unwritable: progress must never change the
|
|
699
|
+
// setup result, the mutation order, or the exit code.
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
/** Clears the wait-animation timer if one is active. */
|
|
703
|
+
const clearAnimation = () => {
|
|
704
|
+
if (animationHandle !== undefined) {
|
|
705
|
+
try {
|
|
706
|
+
clearIntervalFn(animationHandle);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
// Best-effort cleanup; the process is finishing.
|
|
710
|
+
}
|
|
711
|
+
animationHandle = undefined;
|
|
712
|
+
}
|
|
713
|
+
wait = undefined;
|
|
714
|
+
};
|
|
715
|
+
/** Starts the known-wait animation (interactive only) for `delayMs`. */
|
|
716
|
+
const startAnimation = (delayMs) => {
|
|
717
|
+
clearAnimation();
|
|
718
|
+
if (!interactive || delayMs <= 0) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
wait = { start: now(), delay: delayMs };
|
|
722
|
+
try {
|
|
723
|
+
animationHandle = setIntervalFn(() => {
|
|
724
|
+
render();
|
|
725
|
+
}, WAIT_ANIMATION_INTERVAL_MS);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
// The scheduler failed: fall back to a static detail line; the
|
|
729
|
+
// setup wait itself is unaffected.
|
|
730
|
+
wait = undefined;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Never let the animation timer be the reason the process stays alive;
|
|
734
|
+
// the real setup sleep keeps the run open while it waits.
|
|
735
|
+
if (typeof animationHandle.unref === "function") {
|
|
736
|
+
animationHandle.unref();
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
/** Writes the interactive block in place (clears the previous lines first). */
|
|
740
|
+
const renderInteractive = () => {
|
|
741
|
+
const lines = buildBlockLines();
|
|
742
|
+
// Move the cursor up over the previously drawn block, then redraw each
|
|
743
|
+
// line on a cleared line. The first draw has no previous block.
|
|
744
|
+
if (blockLines > 0) {
|
|
745
|
+
safeWrite(`\x1b[${blockLines}A`);
|
|
746
|
+
}
|
|
747
|
+
let written = 0;
|
|
748
|
+
for (const line of lines) {
|
|
749
|
+
safeWrite(`\x1b[2K\r${line}\n`);
|
|
750
|
+
written += 1;
|
|
751
|
+
}
|
|
752
|
+
blockLines = written;
|
|
753
|
+
};
|
|
754
|
+
/**
|
|
755
|
+
* Computes the detail-bar ratio and text for the current state.
|
|
756
|
+
*
|
|
757
|
+
* Priority: a known wait animates its elapsed/total ratio; otherwise a
|
|
758
|
+
* bounded check shows its attempt/max ratio; otherwise a generic step
|
|
759
|
+
* (or the bare phase) shows a fixed `working…` label with a zero ratio;
|
|
760
|
+
* a failed phase shows its stable code; everything else reads "done".
|
|
761
|
+
* No fake percentage is ever shown for unknown-duration work.
|
|
762
|
+
*/
|
|
763
|
+
const detailState = () => {
|
|
764
|
+
const retry = tracker.activeRetry;
|
|
765
|
+
if (retry !== undefined) {
|
|
766
|
+
if (wait !== undefined) {
|
|
767
|
+
// Mid-wait: animate the elapsed portion of THIS wait.
|
|
768
|
+
const elapsed = now() - wait.start;
|
|
769
|
+
const ratio = wait.delay <= 0 ? 1 : elapsed / wait.delay;
|
|
770
|
+
return {
|
|
771
|
+
ratio,
|
|
772
|
+
text: `${retry.attempt}/${retry.maxAttempts} waiting ${Math.round(wait.delay / 1000)}s before next ${RETRY_KIND_WORD[retry.kind]} check`,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
// Between checks (brief): show check progress.
|
|
776
|
+
return {
|
|
777
|
+
ratio: retry.attempt / retry.maxAttempts,
|
|
778
|
+
text: `${retry.attempt}/${retry.maxAttempts} ${RETRY_CHECK_NOUN[retry.kind]}`,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const operation = tracker.activeOperation;
|
|
782
|
+
if (operation !== undefined) {
|
|
783
|
+
return { ratio: 0, text: `working… ${operation.operation}` };
|
|
784
|
+
}
|
|
785
|
+
const current = tracker.currentPhase;
|
|
786
|
+
if (current !== undefined) {
|
|
787
|
+
return { ratio: 0, text: `working… ${SETUP_PROGRESS_LABELS[current].toLowerCase()}` };
|
|
788
|
+
}
|
|
789
|
+
const failure = tracker.failure;
|
|
790
|
+
if (failure !== undefined) {
|
|
791
|
+
return { ratio: 0, text: `failed: ${failure.code}` };
|
|
792
|
+
}
|
|
793
|
+
return { ratio: 1, text: "done" };
|
|
794
|
+
};
|
|
795
|
+
/** Builds the fixed-height block (header + overall + detail + done). */
|
|
796
|
+
const buildBlockLines = () => {
|
|
797
|
+
const completed = tracker.completedCount;
|
|
798
|
+
const pct = overallPercent(completed);
|
|
799
|
+
const header = tracker.isResumed ? "Hikoutei setup (resuming)" : "Hikoutei setup";
|
|
800
|
+
const current = tracker.currentPhase ?? tracker.failure?.phase;
|
|
801
|
+
// The Overall label names the phase the run is working on; with no
|
|
802
|
+
// phase current it names the NEXT pending phase (a phase boundary at
|
|
803
|
+
// 10-90% must never read "complete"), and only a fully completed run
|
|
804
|
+
// (10/10) earns the literal "complete" label.
|
|
805
|
+
let overallLabel;
|
|
806
|
+
if (current !== undefined) {
|
|
807
|
+
overallLabel = SETUP_PROGRESS_LABELS[current];
|
|
808
|
+
}
|
|
809
|
+
else if (completed >= SETUP_PROGRESS_PHASE_COUNT) {
|
|
810
|
+
overallLabel = "complete";
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
const next = nextPendingPhase(tracker);
|
|
814
|
+
overallLabel = next === undefined ? "complete" : `next: ${SETUP_PROGRESS_LABELS[next]}`;
|
|
815
|
+
}
|
|
816
|
+
const overallLine = `Overall ${renderProgressBar(completed / SETUP_PROGRESS_PHASE_COUNT)} ${String(pct).padStart(3, " ")}% ${completed}/${SETUP_PROGRESS_PHASE_COUNT} ${overallLabel}`;
|
|
817
|
+
const detail = detailState();
|
|
818
|
+
const detailLine = `Detail ${renderProgressBar(detail.ratio)} ${detail.text}`;
|
|
819
|
+
const doneLine = `Done ${buildDoneSegment()}`;
|
|
820
|
+
return [header, overallLine, detailLine, doneLine];
|
|
821
|
+
};
|
|
822
|
+
/** Builds the compact checkmark list for the done line. */
|
|
823
|
+
const buildDoneSegment = () => {
|
|
824
|
+
const parts = [];
|
|
825
|
+
for (const phase of SETUP_PROGRESS_PHASES) {
|
|
826
|
+
if (tracker.isComplete(phase)) {
|
|
827
|
+
parts.push(`✓ ${SETUP_PROGRESS_SHORT_LABELS[phase]}`);
|
|
828
|
+
}
|
|
829
|
+
else if (tracker.failure !== undefined && tracker.failure.phase === phase) {
|
|
830
|
+
parts.push(`✗ ${SETUP_PROGRESS_SHORT_LABELS[phase]}`);
|
|
831
|
+
}
|
|
832
|
+
else if (tracker.currentPhase === phase) {
|
|
833
|
+
parts.push(`… ${SETUP_PROGRESS_SHORT_LABELS[phase]}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return parts.length === 0 ? "(starting)" : parts.join(" ");
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* Append-only renderer: one static line per phase/retry/failure event
|
|
840
|
+
* (bounded-check attempts included — the final 8/8 has no following wait
|
|
841
|
+
* line and must stay visible before success/failure), no control
|
|
842
|
+
* sequences, no clock ticks, and no line for ordinary operation events
|
|
843
|
+
* (CI log spam must stay bounded).
|
|
844
|
+
*/
|
|
845
|
+
const renderAppend = (event) => {
|
|
846
|
+
const completed = tracker.completedCount;
|
|
847
|
+
if (event.type === "resumed") {
|
|
848
|
+
// The resumed event arrives AFTER the fresh auth/drive phases in the
|
|
849
|
+
// real flow, so it always prints its own resume line with the
|
|
850
|
+
// checkpoint-guaranteed step count.
|
|
851
|
+
safeWrite(`${APPEND_HEADER_RESUMED_PREFIX}; ${completed}/${SETUP_PROGRESS_PHASE_COUNT} steps already complete)\n`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!headerShown) {
|
|
855
|
+
safeWrite(`${APPEND_HEADER_FRESH}\n`);
|
|
856
|
+
headerShown = true;
|
|
857
|
+
}
|
|
858
|
+
const pct = overallPercent(completed);
|
|
859
|
+
switch (event.type) {
|
|
860
|
+
case "phase_started": {
|
|
861
|
+
safeWrite(`[ ${String(pct).padStart(3, " ")}% | ${completed}/${SETUP_PROGRESS_PHASE_COUNT}] ${SETUP_PROGRESS_LABELS[event.phase]}\n`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
case "phase_completed": {
|
|
865
|
+
const suffix = event.source === "checkpoint" ? " (from checkpoint)" : " ready";
|
|
866
|
+
safeWrite(`[ ${String(pct).padStart(3, " ")}% | ${completed}/${SETUP_PROGRESS_PHASE_COUNT}] ✓ ${SETUP_PROGRESS_LABELS[event.phase]}${suffix}\n`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
case "retry_wait_started": {
|
|
870
|
+
const checkWord = RETRY_KIND_WORD[event.kind];
|
|
871
|
+
safeWrite(` [ ${event.attempt}/${event.maxAttempts}] waiting ${Math.round(event.delayMs / 1000)}s before next ${checkWord} check\n`);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
case "phase_failed": {
|
|
875
|
+
safeWrite(`[ FAIL | ${event.code}] ${SETUP_PROGRESS_LABELS[event.phase]}\n`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
case "operation_started":
|
|
879
|
+
case "operation_completed": {
|
|
880
|
+
if ("check" in event) {
|
|
881
|
+
// A bounded propagation check is visible in static mode: one line
|
|
882
|
+
// per attempt, including the final N/8 that has no following wait
|
|
883
|
+
// line (without it the last attempt would be invisible before
|
|
884
|
+
// success/failure). Only the start prints; the matching
|
|
885
|
+
// completion adds no extra line.
|
|
886
|
+
if (event.type === "operation_started") {
|
|
887
|
+
safeWrite(` [ ${event.check.attempt}/${event.check.maxAttempts}] ${event.operation}\n`);
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
// Ordinary operation events update state without printing a line.
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
/** Draws the current state in the active mode. */
|
|
897
|
+
const render = () => {
|
|
898
|
+
if (interactive) {
|
|
899
|
+
renderInteractive();
|
|
900
|
+
}
|
|
901
|
+
// The append-only renderer draws per event, not on a clock tick.
|
|
902
|
+
};
|
|
903
|
+
return {
|
|
904
|
+
report(event) {
|
|
905
|
+
// Invalid events are rejected by the tracker and never drawn.
|
|
906
|
+
if (!tracker.apply(event)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// A new event always ends any in-flight wait animation first.
|
|
910
|
+
clearAnimation();
|
|
911
|
+
if (interactive) {
|
|
912
|
+
if (event.type === "retry_wait_started") {
|
|
913
|
+
startAnimation(event.delayMs);
|
|
914
|
+
}
|
|
915
|
+
renderInteractive();
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
renderAppend(event);
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
suspend() {
|
|
922
|
+
// Remember the phase that was in progress BEFORE resetting: the
|
|
923
|
+
// inherited gcloud login owns the terminal cleanly and the retry
|
|
924
|
+
// re-emits phase events from scratch, but a cancelled handoff or a
|
|
925
|
+
// failed login must still render a stable failure frame naming the
|
|
926
|
+
// phase the run died in.
|
|
927
|
+
const phase = tracker.currentPhase;
|
|
928
|
+
if (phase !== undefined) {
|
|
929
|
+
suspendedPhase = phase;
|
|
930
|
+
}
|
|
931
|
+
// Stop the animation timer and reset the in-progress tracker state.
|
|
932
|
+
clearAnimation();
|
|
933
|
+
tracker.resetTransient();
|
|
934
|
+
if (interactive && blockLines > 0) {
|
|
935
|
+
// Move up over the block, erase each line, then park the cursor on
|
|
936
|
+
// the first cleared line so the next draw (or the login prompt)
|
|
937
|
+
// reuses the space.
|
|
938
|
+
safeWrite(`\x1b[${blockLines}A`);
|
|
939
|
+
for (let i = 0; i < blockLines; i += 1) {
|
|
940
|
+
safeWrite("\x1b[2K\r");
|
|
941
|
+
if (i < blockLines - 1) {
|
|
942
|
+
safeWrite("\n");
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (blockLines > 1) {
|
|
946
|
+
safeWrite(`\x1b[${blockLines - 1}A`);
|
|
947
|
+
}
|
|
948
|
+
blockLines = 0;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
resume() {
|
|
952
|
+
// The inherited login finished and the retry is live again: the
|
|
953
|
+
// suspended-phase fallback must never label a retry failure against
|
|
954
|
+
// the phase the FIRST attempt died in. No timer starts or stops
|
|
955
|
+
// here — the retry's own events restart the animation when a known
|
|
956
|
+
// wait runs, and nothing ever runs during the inherited login.
|
|
957
|
+
suspendedPhase = undefined;
|
|
958
|
+
},
|
|
959
|
+
fail(code) {
|
|
960
|
+
clearAnimation();
|
|
961
|
+
// The failure frame names the phase the run died in: the live
|
|
962
|
+
// current phase first, then the phase suspended for the login
|
|
963
|
+
// handoff, then the deterministic next pending phase (a run-level
|
|
964
|
+
// error with no current phase — for example lock contention after
|
|
965
|
+
// the retry's fresh auth phases — must still render a safe failure
|
|
966
|
+
// frame), and finally the last phase as a terminal fallback when
|
|
967
|
+
// every phase is complete. All fallbacks use fixed phase labels and
|
|
968
|
+
// stable codes only, so nothing about the failure can leak data.
|
|
969
|
+
const phase = tracker.currentPhase ??
|
|
970
|
+
suspendedPhase ??
|
|
971
|
+
nextPendingPhase(tracker) ??
|
|
972
|
+
SETUP_PROGRESS_PHASES[SETUP_PROGRESS_PHASES.length - 1];
|
|
973
|
+
// Consume the suspended fallback: once a failure is finalized, a
|
|
974
|
+
// later fail() or finish() can never reuse the pre-login phase.
|
|
975
|
+
suspendedPhase = undefined;
|
|
976
|
+
if (phase === undefined) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
// A phase that is still current fails through the validating
|
|
980
|
+
// tracker; a suspended or next-pending phase is no longer current
|
|
981
|
+
// (the login handoff or the phase boundary cleared it), so it is
|
|
982
|
+
// recorded directly with the same phase/code validation.
|
|
983
|
+
const applied = tracker.currentPhase === phase
|
|
984
|
+
? tracker.apply({ type: "phase_failed", phase, code })
|
|
985
|
+
: tracker.markFailed(phase, code);
|
|
986
|
+
if (!applied) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (interactive) {
|
|
990
|
+
renderInteractive();
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
// The append-only renderer printed the phase_started line already
|
|
994
|
+
// (or nothing, for a run-level error); emit an explicit failure
|
|
995
|
+
// line for the phase the run died in.
|
|
996
|
+
renderAppend({ type: "phase_failed", phase, code });
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
finish() {
|
|
1000
|
+
clearAnimation();
|
|
1001
|
+
suspendedPhase = undefined;
|
|
1002
|
+
if (interactive && blockLines > 0) {
|
|
1003
|
+
renderInteractive();
|
|
1004
|
+
}
|
|
1005
|
+
// Append-only mode needs no terminal frame.
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
//# sourceMappingURL=setupProgress.js.map
|