hikoutei 0.10.11-dev → 0.10.14-dev
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/dist/auth/serviceAccountAuthPool.d.ts +72 -0
- package/dist/auth/serviceAccountAuthPool.d.ts.map +1 -0
- package/dist/auth/serviceAccountAuthPool.js +92 -0
- package/dist/auth/serviceAccountAuthPool.js.map +1 -0
- package/dist/auth/serviceAccountKey.d.ts +85 -0
- package/dist/auth/serviceAccountKey.d.ts.map +1 -0
- package/dist/auth/serviceAccountKey.js +104 -0
- package/dist/auth/serviceAccountKey.js.map +1 -0
- package/dist/cli/args.d.ts +19 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +50 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/checkpoint.d.ts +37 -1
- package/dist/cli/checkpoint.d.ts.map +1 -1
- package/dist/cli/checkpoint.js +94 -14
- package/dist/cli/checkpoint.js.map +1 -1
- package/dist/cli/confirm.d.ts +17 -0
- package/dist/cli/confirm.d.ts.map +1 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/confirm.js.map +1 -1
- package/dist/cli/envFileWriter.d.ts +15 -2
- package/dist/cli/envFileWriter.d.ts.map +1 -1
- package/dist/cli/envFileWriter.js +18 -3
- package/dist/cli/envFileWriter.js.map +1 -1
- package/dist/cli/setup.d.ts +2 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +20 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/setupFlow.d.ts +67 -0
- package/dist/cli/setupFlow.d.ts.map +1 -1
- package/dist/cli/setupFlow.js +401 -65
- package/dist/cli/setupFlow.js.map +1 -1
- package/dist/cli/setupProgress.d.ts +16 -1
- package/dist/cli/setupProgress.d.ts.map +1 -1
- package/dist/cli/setupProgress.js +47 -1
- package/dist/cli/setupProgress.js.map +1 -1
- package/dist/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js +1 -1
- package/dist/sync-engine/sync/service/syncAutoStart.js +1 -1
- package/package.json +1 -1
package/dist/cli/setupFlow.d.ts
CHANGED
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
* available.
|
|
67
67
|
*/
|
|
68
68
|
import { type LockFs } from "./checkpoint.js";
|
|
69
|
+
import { type SetupFailure } from "./errors.js";
|
|
69
70
|
import { type PlannedCommand, type SetupErrorResult } from "./flowResult.js";
|
|
70
71
|
export { writeSetupEnvFile, atomicWritePrivateFile, SETUP_ENV_KEYS } from "./envFileWriter.js";
|
|
71
72
|
export { findSetupPathCollision, type SetupPathCollision } from "./setupPathCollision.js";
|
|
@@ -102,6 +103,16 @@ export interface RunSetupOptions {
|
|
|
102
103
|
/** Absolute setup checkpoint path (`.hikoutei-setup-state.json`). */
|
|
103
104
|
readonly statePath: string;
|
|
104
105
|
readonly dryRun: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Service accounts to provision as a credential pool. Defaults to 1
|
|
108
|
+
* (single-SA behavior, unchanged); N > 1 provisions `<saName>-<i>`
|
|
109
|
+
* accounts after the primary flow, sharing the same spreadsheet and
|
|
110
|
+
* recorded in `HIKOUTEI_SYNC_CREDENTIALS`. Values outside 1..10 are an
|
|
111
|
+
* `invalid_args` usage error. Resuming with a smaller count keeps every
|
|
112
|
+
* existing pool entry (the pool never shrinks); the summary reports the
|
|
113
|
+
* actual pool size.
|
|
114
|
+
*/
|
|
115
|
+
readonly saCount?: number;
|
|
105
116
|
/**
|
|
106
117
|
* Optional progress sink for the CLI renderer. When omitted the run is
|
|
107
118
|
* unaffected; when present a throwing callback is swallowed so progress
|
|
@@ -152,6 +163,16 @@ export interface SetupSummary {
|
|
|
152
163
|
readonly saWriterRole: ShareOutcome["writerRole"] | "unchanged";
|
|
153
164
|
/** True when this run resumed from an existing checkpoint. */
|
|
154
165
|
readonly resumed: boolean;
|
|
166
|
+
/** Provisioned pool size (1 for single-SA runs). */
|
|
167
|
+
readonly poolSize: number;
|
|
168
|
+
/** Key paths of the provisioned pool, entry 1 first ([keyPath] for N=1). */
|
|
169
|
+
readonly poolPaths: readonly string[];
|
|
170
|
+
/**
|
|
171
|
+
* Pool entries kept from a previous run (absent/0 for fresh runs and
|
|
172
|
+
* N=1). A resume never removes entries, so resuming with a smaller
|
|
173
|
+
* `--sa-count` reports the actual (larger) pool with this kept count.
|
|
174
|
+
*/
|
|
175
|
+
readonly poolKeptEntries?: number;
|
|
155
176
|
}
|
|
156
177
|
/** Discriminated result of a setup run. */
|
|
157
178
|
export type SetupResult = {
|
|
@@ -227,4 +248,50 @@ export declare function formatSummary(summary: SetupSummary): string;
|
|
|
227
248
|
* material and the access token are never included in messages.
|
|
228
249
|
*/
|
|
229
250
|
export declare function runSetup(options: RunSetupOptions): Promise<SetupResult>;
|
|
251
|
+
/**
|
|
252
|
+
* Derives the key path for pool entry `index` (2-based) next to the
|
|
253
|
+
* primary key path, mirroring the primary naming: `name-2.json` beside
|
|
254
|
+
* `name.json` (or `name-2` beside an extensionless `name`). The directory
|
|
255
|
+
* is preserved and no existing file is ever touched here.
|
|
256
|
+
*/
|
|
257
|
+
export declare function poolKeyPath(primaryKeyPath: string, index: number): string;
|
|
258
|
+
/** Prompt text for the interactive service-account count question. */
|
|
259
|
+
export declare const SA_COUNT_PROMPT = "Service accounts to create? [1]: ";
|
|
260
|
+
/** Reads one line of prompt input; null on end-of-input. Injectable for tests. */
|
|
261
|
+
export type SaCountLineReader = () => Promise<string | null>;
|
|
262
|
+
/** Inputs for resolving the service-account count of a run. */
|
|
263
|
+
export interface ResolveSaCountOptions {
|
|
264
|
+
/** Parsed `--sa-count` value, or undefined when the flag was not given. */
|
|
265
|
+
readonly saCount: number | undefined;
|
|
266
|
+
readonly yes: boolean;
|
|
267
|
+
readonly dryRun: boolean;
|
|
268
|
+
/** True when the session is an interactive terminal (stdin and stdout TTY). */
|
|
269
|
+
readonly isTTY: boolean;
|
|
270
|
+
/** Writes the prompt text; required when prompting. */
|
|
271
|
+
readonly write?: (text: string) => void;
|
|
272
|
+
/** Reads one input line; required when prompting. */
|
|
273
|
+
readonly readLine?: SaCountLineReader;
|
|
274
|
+
}
|
|
275
|
+
/** Outcome of resolving the service-account count of a run. */
|
|
276
|
+
export type SaCountResolution = {
|
|
277
|
+
readonly status: "ok";
|
|
278
|
+
readonly saCount: number;
|
|
279
|
+
} | {
|
|
280
|
+
readonly status: "invalid";
|
|
281
|
+
readonly failure: SetupFailure;
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Resolves how many service accounts to provision.
|
|
285
|
+
*
|
|
286
|
+
* `--yes`, `--dry-run`, and non-TTY sessions never prompt and use the
|
|
287
|
+
* flag (default 1). An interactive TTY session without `--sa-count` asks
|
|
288
|
+
* once (`Service accounts to create? [1]: `): an empty answer means 1, a
|
|
289
|
+
* valid 1..10 integer wins, and invalid input re-asks exactly once before
|
|
290
|
+
* failing with an `invalid_args` usage error. End-of-input on the first
|
|
291
|
+
* read counts as empty (1); end-of-input on the re-ask fails closed with
|
|
292
|
+
* an `invalid_args` usage error (a typo followed by Ctrl-D must never
|
|
293
|
+
* silently provision 1). A missing line reader degrades to the default
|
|
294
|
+
* without prompting.
|
|
295
|
+
*/
|
|
296
|
+
export declare function resolveSaCountForSetup(options: ResolveSaCountOptions): Promise<SaCountResolution>;
|
|
230
297
|
//# sourceMappingURL=setupFlow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupFlow.d.ts","sourceRoot":"","sources":["../../../../../../src/setupFlow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;
|
|
1
|
+
{"version":3,"file":"setupFlow.d.ts","sourceRoot":"","sources":["../../../../../../src/setupFlow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAGH,OAAO,EAeL,KAAK,MAAM,EAKZ,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAqE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACnH,OAAO,EAAiD,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAgB5H,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC1F,OAAO,EAAoB,KAAK,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAC9F,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAKL,KAAK,OAAO,EACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAEV,oBAAoB,EAEpB,YAAY,EAEb,MAAM,oBAAoB,CAAC;AAG5B,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAErE,2EAA2E;AAC3E,eAAO,MAAM,gCAAgC,kBAAkB,CAAC;AAEhE,+EAA+E;AAC/E,eAAO,MAAM,aAAa,MAAgC,CAAC;AAE3D,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,cAAc,CAAC;IACvC,yDAAyD;IACzD,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAC9C,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;IAC1C,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IACtC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,sDAAsD;AACtD,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,0CAA0C;AAC1C,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC,YAAY,CAAC,GAAG,WAAW,CAAC;IAChE,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GACnB;IACA,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C,GACC;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAA;CAAE,GAC9F,gBAAgB,CAAC;AAUrB,+CAA+C;AAC/C,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjE;AAyCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAmB,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAItG;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,cAAc,EAAE,CA6JnG;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAatE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CA2B3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAwK7E;AAs4BD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzE;AAED,sEAAsE;AACtE,eAAO,MAAM,eAAe,sCAAsC,CAAC;AAEnE,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAE7D,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuCvG"}
|
package/dist/cli/setupFlow.js
CHANGED
|
@@ -65,8 +65,10 @@
|
|
|
65
65
|
* lock, checkpoint, key, or env mutation, and manual setup remains
|
|
66
66
|
* available.
|
|
67
67
|
*/
|
|
68
|
+
import { basename, dirname, join } from "node:path";
|
|
68
69
|
import { acquireSetupLock, checkStateCompatibility, generateCreationMarker, isValidGcpProjectId, isValidServiceAccountName, loadSetupState, releaseSetupLock, saveSetupState, serviceAccountEmail, SERVICE_ACCOUNT_KEY_FILE_MODE, SETUP_STATE_VERSION, setupLockPath, setupStateTempPath, } from "./checkpoint.js";
|
|
69
|
-
import {
|
|
70
|
+
import { MAX_SETUP_SA_COUNT, parseSaCount } from "./args.js";
|
|
71
|
+
import { SETUP_ERROR_CODES, carrierCode, setupFailure } from "./errors.js";
|
|
70
72
|
import { describeGcloudFailure, errorResult, outcomeOf } from "./flowResult.js";
|
|
71
73
|
import { revalidateSetupPaths, writeSetupEnvFile, } from "./envFileWriter.js";
|
|
72
74
|
import { findSetupPathCollision } from "./setupPathCollision.js";
|
|
@@ -76,7 +78,7 @@ import { readServiceAccountKeyCredentialSecurely, readServiceAccountKeySecurely,
|
|
|
76
78
|
export { writeSetupEnvFile, atomicWritePrivateFile, SETUP_ENV_KEYS } from "./envFileWriter.js";
|
|
77
79
|
export { findSetupPathCollision } from "./setupPathCollision.js";
|
|
78
80
|
import { createSafeRunner } from "./gcloudRunner.js";
|
|
79
|
-
import { boundedCheckReporter, safeProgressSink, SETUP_PROGRESS_OPERATIONS, } from "./setupProgress.js";
|
|
81
|
+
import { boundedCheckReporter, POOL_EXPANSION_PHASE, safeProgressSink, SETUP_PROGRESS_OPERATIONS, } from "./setupProgress.js";
|
|
80
82
|
import { checkHumanDriveAccess, DRIVE_ACCESS_COMMAND, } from "./humanAuth.js";
|
|
81
83
|
import { KEY_STAGE_PLACEHOLDER, listUserManagedServiceAccountKeys, realSleeper, settleServiceAccountKey, } from "./keyProvision.js";
|
|
82
84
|
import { httpStatusOf, safeReasonOf } from "./sdkError.js";
|
|
@@ -319,7 +321,7 @@ export function formatSummary(summary) {
|
|
|
319
321
|
: summary.envFileModified
|
|
320
322
|
? "updated"
|
|
321
323
|
: "unchanged";
|
|
322
|
-
|
|
324
|
+
const lines = [
|
|
323
325
|
"Hikoutei setup complete.",
|
|
324
326
|
` project: ${summary.projectId} (${summary.projectReused ? "reused" : "created"})`,
|
|
325
327
|
` human owner: ${summary.ownerEmail}`,
|
|
@@ -330,7 +332,13 @@ export function formatSummary(summary) {
|
|
|
330
332
|
` spreadsheet URL: ${summary.spreadsheetUrl}`,
|
|
331
333
|
` checkpoint: ${summary.statePath} (${summary.stateStatus}${summary.resumed ? ", resumed" : ""})`,
|
|
332
334
|
` env file: ${summary.outputPath} (${envState})`,
|
|
333
|
-
]
|
|
335
|
+
];
|
|
336
|
+
// Pool lines appear only for multi-SA runs, so N=1 output is unchanged.
|
|
337
|
+
if (summary.poolSize > 1) {
|
|
338
|
+
const kept = summary.poolKeptEntries ?? 0;
|
|
339
|
+
lines.push(` credential pool: ${summary.poolSize} service accounts${kept > 0 ? ` (kept: ${kept} existing)` : ""}`, ` credential pool keys: ${summary.poolPaths.join(",")}`);
|
|
340
|
+
}
|
|
341
|
+
return lines.join("\n");
|
|
334
342
|
}
|
|
335
343
|
/**
|
|
336
344
|
* Runs the full setup bootstrap.
|
|
@@ -381,6 +389,12 @@ export async function runSetup(options) {
|
|
|
381
389
|
return errorResult(SETUP_ERROR_CODES.INVALID_ARGS, `invalid --sa-name value: service-account names must start with a lowercase letter and ` +
|
|
382
390
|
`contain only lowercase letters, digits, and hyphens (6-30 characters)`);
|
|
383
391
|
}
|
|
392
|
+
// Fail closed on a programmatic out-of-range pool size (the CLI parser
|
|
393
|
+
// already enforces 1..10): the pool expansion below trusts this bound.
|
|
394
|
+
if (options.saCount !== undefined &&
|
|
395
|
+
(!Number.isSafeInteger(options.saCount) || options.saCount < 1 || options.saCount > MAX_SETUP_SA_COUNT)) {
|
|
396
|
+
return errorResult(SETUP_ERROR_CODES.INVALID_ARGS, `invalid sa-count value: expected a positive integer between 1 and ${MAX_SETUP_SA_COUNT}`);
|
|
397
|
+
}
|
|
384
398
|
// Reject canonical path collisions before anything else runs: `--output`
|
|
385
399
|
// must never alias the key, checkpoint, checkpoint temp, or lock path, and
|
|
386
400
|
// the key must never alias the checkpoint, temp, or lock. This is a usage
|
|
@@ -735,52 +749,15 @@ async function runSetupLocked(options, executed, human, runner, keySleeper, prog
|
|
|
735
749
|
progress.report({ type: "phase_completed", phase: "apis", source: "run" });
|
|
736
750
|
progress.report({ type: "phase_started", phase: "service_account" });
|
|
737
751
|
// Service account: reuse by email when it already exists.
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
kind: "gcloud",
|
|
743
|
-
command: ["iam", "service-accounts", "list", "--project", projectId, "--format=value(email)"],
|
|
744
|
-
outcome: outcomeOf(saList, "service account lookup complete"),
|
|
752
|
+
const ensured = await ensureServiceAccount(runner, executed, progress, "service_account", {
|
|
753
|
+
projectId,
|
|
754
|
+
saName: options.saName,
|
|
755
|
+
saEmail: email,
|
|
745
756
|
});
|
|
746
|
-
if (
|
|
747
|
-
return
|
|
748
|
-
}
|
|
749
|
-
const existingEmails = saList.stdout.split("\n").map((line) => line.trim());
|
|
750
|
-
if (existingEmails.includes(email)) {
|
|
751
|
-
serviceAccountReused = true;
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
progress.report({ type: "operation_started", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
|
|
755
|
-
const saCreate = await runner.run([
|
|
756
|
-
"iam",
|
|
757
|
-
"service-accounts",
|
|
758
|
-
"create",
|
|
759
|
-
options.saName,
|
|
760
|
-
"--project",
|
|
761
|
-
projectId,
|
|
762
|
-
"--display-name",
|
|
763
|
-
"hikoutei setup",
|
|
764
|
-
]);
|
|
765
|
-
progress.report({ type: "operation_completed", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
|
|
766
|
-
executed.push({
|
|
767
|
-
kind: "gcloud",
|
|
768
|
-
command: [
|
|
769
|
-
"iam",
|
|
770
|
-
"service-accounts",
|
|
771
|
-
"create",
|
|
772
|
-
options.saName,
|
|
773
|
-
"--project",
|
|
774
|
-
projectId,
|
|
775
|
-
"--display-name",
|
|
776
|
-
"hikoutei setup",
|
|
777
|
-
],
|
|
778
|
-
outcome: outcomeOf(saCreate, "service account created"),
|
|
779
|
-
});
|
|
780
|
-
if (saCreate.status !== "ok") {
|
|
781
|
-
return errorResult(SETUP_ERROR_CODES.SA_CREATE_FAILED, `could not create service account "${options.saName}": ${describeGcloudFailure(saCreate)}`);
|
|
782
|
-
}
|
|
757
|
+
if (ensured.status === "error") {
|
|
758
|
+
return ensured.error;
|
|
783
759
|
}
|
|
760
|
+
serviceAccountReused = ensured.reused;
|
|
784
761
|
progress.report({ type: "phase_completed", phase: "service_account", source: "run" });
|
|
785
762
|
}
|
|
786
763
|
// Key: reuse an existing validated key file, or run the write-ahead
|
|
@@ -1136,28 +1113,61 @@ async function runSetupLocked(options, executed, human, runner, keySleeper, prog
|
|
|
1136
1113
|
}
|
|
1137
1114
|
}
|
|
1138
1115
|
progress.report({ type: "phase_completed", phase: "output", source: "run" });
|
|
1116
|
+
const baseSummary = {
|
|
1117
|
+
projectId,
|
|
1118
|
+
ownerEmail,
|
|
1119
|
+
serviceAccountEmail: email,
|
|
1120
|
+
keyPath: options.keyPath,
|
|
1121
|
+
spreadsheetId: spreadsheet.spreadsheetId,
|
|
1122
|
+
spreadsheetUrl: spreadsheetEditUrl(spreadsheet.spreadsheetId),
|
|
1123
|
+
spreadsheetTitle: title,
|
|
1124
|
+
outputPath: options.outputPath,
|
|
1125
|
+
statePath: options.statePath,
|
|
1126
|
+
stateStatus: "complete",
|
|
1127
|
+
envFileCreated: envResult.created,
|
|
1128
|
+
envFileModified: envResult.modified,
|
|
1129
|
+
projectReused,
|
|
1130
|
+
serviceAccountReused: needsProjectPhase ? serviceAccountReused : true,
|
|
1131
|
+
keyReused,
|
|
1132
|
+
saWriterRole,
|
|
1133
|
+
resumed: checkpoint !== undefined,
|
|
1134
|
+
};
|
|
1135
|
+
// Single-SA runs return exactly as before (no pool checkpoint key, no
|
|
1136
|
+
// pool env line, no pool progress); the pool expansion below runs only
|
|
1137
|
+
// when more than one service account was requested.
|
|
1138
|
+
const saCount = options.saCount ?? 1;
|
|
1139
|
+
if (saCount <= 1) {
|
|
1140
|
+
return {
|
|
1141
|
+
status: "ok",
|
|
1142
|
+
dryRun: false,
|
|
1143
|
+
commands: executed,
|
|
1144
|
+
summary: { ...baseSummary, poolSize: 1, poolPaths: [options.keyPath] },
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const expanded = await expandCredentialPool(runner, executed, progress, api, options, keySleeper, {
|
|
1148
|
+
projectId,
|
|
1149
|
+
spreadsheetId: spreadsheet.spreadsheetId,
|
|
1150
|
+
title,
|
|
1151
|
+
ownerEmail,
|
|
1152
|
+
primaryEmail: email,
|
|
1153
|
+
projectMode: persistProjectMode,
|
|
1154
|
+
keyOrigin,
|
|
1155
|
+
shareOrigin,
|
|
1156
|
+
saCount,
|
|
1157
|
+
});
|
|
1158
|
+
if (expanded.status === "error") {
|
|
1159
|
+
return expanded.error;
|
|
1160
|
+
}
|
|
1139
1161
|
return {
|
|
1140
1162
|
status: "ok",
|
|
1141
1163
|
dryRun: false,
|
|
1142
1164
|
commands: executed,
|
|
1143
1165
|
summary: {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
spreadsheetUrl: spreadsheetEditUrl(spreadsheet.spreadsheetId),
|
|
1150
|
-
spreadsheetTitle: title,
|
|
1151
|
-
outputPath: options.outputPath,
|
|
1152
|
-
statePath: options.statePath,
|
|
1153
|
-
stateStatus: "complete",
|
|
1154
|
-
envFileCreated: envResult.created,
|
|
1155
|
-
envFileModified: envResult.modified,
|
|
1156
|
-
projectReused,
|
|
1157
|
-
serviceAccountReused: needsProjectPhase ? serviceAccountReused : true,
|
|
1158
|
-
keyReused,
|
|
1159
|
-
saWriterRole,
|
|
1160
|
-
resumed: checkpoint !== undefined,
|
|
1166
|
+
...baseSummary,
|
|
1167
|
+
envFileModified: envResult.modified || expanded.envModified,
|
|
1168
|
+
poolSize: expanded.pool.length,
|
|
1169
|
+
poolPaths: expanded.pool.map((entry) => entry.keyPath),
|
|
1170
|
+
poolKeptEntries: expanded.keptPoolEntries,
|
|
1161
1171
|
},
|
|
1162
1172
|
};
|
|
1163
1173
|
}
|
|
@@ -1186,6 +1196,332 @@ async function createProjectOnce(runner, executed, projectId) {
|
|
|
1186
1196
|
executed.push({ kind: "gcloud", command: ["projects", "create", projectId], outcome: "created" });
|
|
1187
1197
|
return { status: "ok", reused: false };
|
|
1188
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Ensures one service account exists: reuse by email when it already
|
|
1201
|
+
* exists, create it otherwise.
|
|
1202
|
+
*
|
|
1203
|
+
* Shared verbatim by the primary flow and the credential-pool expansion:
|
|
1204
|
+
* the same list/create commands, outcomes, and `sa_create_failed` codes
|
|
1205
|
+
* run under the caller's phase (the primary `service_account` phase or
|
|
1206
|
+
* `pool_expansion`), so the primary path emits byte-identical events.
|
|
1207
|
+
*/
|
|
1208
|
+
async function ensureServiceAccount(runner, executed, progress, phase, input) {
|
|
1209
|
+
progress.report({ type: "operation_started", phase, operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
|
|
1210
|
+
const saList = await runner.run(["iam", "service-accounts", "list", "--project", input.projectId, "--format=value(email)"]);
|
|
1211
|
+
progress.report({ type: "operation_completed", phase, operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
|
|
1212
|
+
executed.push({
|
|
1213
|
+
kind: "gcloud",
|
|
1214
|
+
command: ["iam", "service-accounts", "list", "--project", input.projectId, "--format=value(email)"],
|
|
1215
|
+
outcome: outcomeOf(saList, "service account lookup complete"),
|
|
1216
|
+
});
|
|
1217
|
+
if (saList.status !== "ok") {
|
|
1218
|
+
return {
|
|
1219
|
+
status: "error",
|
|
1220
|
+
error: errorResult(SETUP_ERROR_CODES.SA_CREATE_FAILED, `could not list service accounts: ${describeGcloudFailure(saList)}`),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
const existingEmails = saList.stdout.split("\n").map((line) => line.trim());
|
|
1224
|
+
if (existingEmails.includes(input.saEmail)) {
|
|
1225
|
+
return { status: "ok", reused: true };
|
|
1226
|
+
}
|
|
1227
|
+
progress.report({ type: "operation_started", phase, operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
|
|
1228
|
+
const saCreate = await runner.run([
|
|
1229
|
+
"iam",
|
|
1230
|
+
"service-accounts",
|
|
1231
|
+
"create",
|
|
1232
|
+
input.saName,
|
|
1233
|
+
"--project",
|
|
1234
|
+
input.projectId,
|
|
1235
|
+
"--display-name",
|
|
1236
|
+
"hikoutei setup",
|
|
1237
|
+
]);
|
|
1238
|
+
progress.report({ type: "operation_completed", phase, operation: SETUP_PROGRESS_OPERATIONS.SA_CREATE });
|
|
1239
|
+
executed.push({
|
|
1240
|
+
kind: "gcloud",
|
|
1241
|
+
command: [
|
|
1242
|
+
"iam",
|
|
1243
|
+
"service-accounts",
|
|
1244
|
+
"create",
|
|
1245
|
+
input.saName,
|
|
1246
|
+
"--project",
|
|
1247
|
+
input.projectId,
|
|
1248
|
+
"--display-name",
|
|
1249
|
+
"hikoutei setup",
|
|
1250
|
+
],
|
|
1251
|
+
outcome: outcomeOf(saCreate, "service account created"),
|
|
1252
|
+
});
|
|
1253
|
+
if (saCreate.status !== "ok") {
|
|
1254
|
+
return {
|
|
1255
|
+
status: "error",
|
|
1256
|
+
error: errorResult(SETUP_ERROR_CODES.SA_CREATE_FAILED, `could not create service account "${input.saName}": ${describeGcloudFailure(saCreate)}`),
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
return { status: "ok", reused: false };
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Derives the key path for pool entry `index` (2-based) next to the
|
|
1263
|
+
* primary key path, mirroring the primary naming: `name-2.json` beside
|
|
1264
|
+
* `name.json` (or `name-2` beside an extensionless `name`). The directory
|
|
1265
|
+
* is preserved and no existing file is ever touched here.
|
|
1266
|
+
*/
|
|
1267
|
+
export function poolKeyPath(primaryKeyPath, index) {
|
|
1268
|
+
const base = basename(primaryKeyPath);
|
|
1269
|
+
const dot = base.lastIndexOf(".");
|
|
1270
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
1271
|
+
const ext = dot > 0 ? base.slice(dot) : "";
|
|
1272
|
+
return join(dirname(primaryKeyPath), `${stem}-${index}${ext}`);
|
|
1273
|
+
}
|
|
1274
|
+
/** Prompt text for the interactive service-account count question. */
|
|
1275
|
+
export const SA_COUNT_PROMPT = "Service accounts to create? [1]: ";
|
|
1276
|
+
/**
|
|
1277
|
+
* Resolves how many service accounts to provision.
|
|
1278
|
+
*
|
|
1279
|
+
* `--yes`, `--dry-run`, and non-TTY sessions never prompt and use the
|
|
1280
|
+
* flag (default 1). An interactive TTY session without `--sa-count` asks
|
|
1281
|
+
* once (`Service accounts to create? [1]: `): an empty answer means 1, a
|
|
1282
|
+
* valid 1..10 integer wins, and invalid input re-asks exactly once before
|
|
1283
|
+
* failing with an `invalid_args` usage error. End-of-input on the first
|
|
1284
|
+
* read counts as empty (1); end-of-input on the re-ask fails closed with
|
|
1285
|
+
* an `invalid_args` usage error (a typo followed by Ctrl-D must never
|
|
1286
|
+
* silently provision 1). A missing line reader degrades to the default
|
|
1287
|
+
* without prompting.
|
|
1288
|
+
*/
|
|
1289
|
+
export async function resolveSaCountForSetup(options) {
|
|
1290
|
+
if (options.yes || options.dryRun || !options.isTTY || options.saCount !== undefined) {
|
|
1291
|
+
return { status: "ok", saCount: options.saCount ?? 1 };
|
|
1292
|
+
}
|
|
1293
|
+
const readLine = options.readLine;
|
|
1294
|
+
if (readLine === undefined) {
|
|
1295
|
+
return { status: "ok", saCount: 1 };
|
|
1296
|
+
}
|
|
1297
|
+
const write = options.write ?? (() => undefined);
|
|
1298
|
+
const invalidArgs = (detail) => ({
|
|
1299
|
+
status: "invalid",
|
|
1300
|
+
failure: setupFailure(SETUP_ERROR_CODES.INVALID_ARGS, `invalid service-account count: ${detail}; run with --sa-count <n> (1-${MAX_SETUP_SA_COUNT}) or --yes`),
|
|
1301
|
+
});
|
|
1302
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1303
|
+
write(SA_COUNT_PROMPT);
|
|
1304
|
+
const line = await readLine();
|
|
1305
|
+
if (line === null) {
|
|
1306
|
+
if (attempt === 0) {
|
|
1307
|
+
return { status: "ok", saCount: 1 };
|
|
1308
|
+
}
|
|
1309
|
+
return invalidArgs("no input on retry");
|
|
1310
|
+
}
|
|
1311
|
+
const trimmed = line.trim();
|
|
1312
|
+
if (trimmed === "") {
|
|
1313
|
+
return { status: "ok", saCount: 1 };
|
|
1314
|
+
}
|
|
1315
|
+
const parsed = parseSaCount(trimmed);
|
|
1316
|
+
if (parsed.status === "ok") {
|
|
1317
|
+
return { status: "ok", saCount: parsed.saCount };
|
|
1318
|
+
}
|
|
1319
|
+
if (attempt === 1) {
|
|
1320
|
+
return invalidArgs(parsed.message);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
// Unreachable: the loop above returns on every path.
|
|
1324
|
+
return { status: "ok", saCount: 1 };
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Provisions the additional credential-pool accounts (entries 2..N).
|
|
1328
|
+
*
|
|
1329
|
+
* Runs after the primary flow reached `complete`: for each missing entry
|
|
1330
|
+
* it ensures the `<saName>-<i>` service account exists (the same
|
|
1331
|
+
* ensure/create path as the primary), issues its key with the same
|
|
1332
|
+
* settlement loop (`settleServiceAccountKey` with `fresh` permission and
|
|
1333
|
+
* its own marker/baseline), grants it writer access, verifies access with
|
|
1334
|
+
* the same verifier, and persists the entry to the `complete` checkpoint
|
|
1335
|
+
* IMMEDIATELY — a mid-pool failure keeps entries 1..i-1 so the resume
|
|
1336
|
+
* skips them. Failures use the same error codes as the primary path.
|
|
1337
|
+
* Finally the `.env` file is rewritten with the comma-separated
|
|
1338
|
+
* `HIKOUTEI_SYNC_CREDENTIALS` pool line. The bounded key/verify progress
|
|
1339
|
+
* reporters stay unwired here (the tracker hosts one bounded kind per
|
|
1340
|
+
* phase): only generic operation events are emitted under
|
|
1341
|
+
* `pool_expansion`, which never advances the overall ten-phase count.
|
|
1342
|
+
*
|
|
1343
|
+
* // ponytail: no per-pool-SA key write-ahead checkpoint; a crash between
|
|
1344
|
+
* // a pool key create and its pool-entry persist leaves an orphaned cloud
|
|
1345
|
+
* // key that the retry treats as baseline (fails uncertain, never
|
|
1346
|
+
* // duplicates silently). Persist per-SA key markers if this ever matters.
|
|
1347
|
+
*/
|
|
1348
|
+
async function expandCredentialPool(runner, executed, progress, api, options, keySleeper, context) {
|
|
1349
|
+
progress.report({ type: "phase_started", phase: POOL_EXPANSION_PHASE });
|
|
1350
|
+
// The primary flow just persisted `complete`; reload it as the
|
|
1351
|
+
// append-reconcile base (saCount is a run option, never a checkpoint
|
|
1352
|
+
// conflict: entries already present are skipped).
|
|
1353
|
+
const reloaded = loadSetupState(options.statePath);
|
|
1354
|
+
if (reloaded.status !== "loaded" || reloaded.state.status !== "complete") {
|
|
1355
|
+
return {
|
|
1356
|
+
status: "error",
|
|
1357
|
+
error: errorResult(SETUP_ERROR_CODES.SETUP_STATE_INVALID, `the setup state at ${options.statePath} is not complete; remove it and rerun setup`),
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const primaryEntry = {
|
|
1361
|
+
saName: options.saName,
|
|
1362
|
+
saEmail: context.primaryEmail,
|
|
1363
|
+
keyPath: options.keyPath,
|
|
1364
|
+
};
|
|
1365
|
+
const storedPool = reloaded.state.pool;
|
|
1366
|
+
if (storedPool !== undefined) {
|
|
1367
|
+
const first = storedPool[0];
|
|
1368
|
+
if (first === undefined ||
|
|
1369
|
+
first.saName !== primaryEntry.saName ||
|
|
1370
|
+
first.saEmail !== primaryEntry.saEmail ||
|
|
1371
|
+
first.keyPath !== primaryEntry.keyPath) {
|
|
1372
|
+
return {
|
|
1373
|
+
status: "error",
|
|
1374
|
+
error: errorResult(SETUP_ERROR_CODES.SETUP_STATE_INVALID, `the credential pool in ${options.statePath} does not start with the primary service account; ` +
|
|
1375
|
+
`remove the setup state and rerun setup`),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// Keep-all resume: resuming with a smaller --sa-count never removes
|
|
1380
|
+
// entries — the loop below only appends missing entries 2..N, so the
|
|
1381
|
+
// pool only grows and the summary reports the actual (larger) size.
|
|
1382
|
+
const keptPoolEntries = storedPool?.length ?? 0;
|
|
1383
|
+
const pool = storedPool !== undefined ? [...storedPool] : [primaryEntry];
|
|
1384
|
+
for (let index = pool.length + 1; index <= context.saCount; index += 1) {
|
|
1385
|
+
const saName = `${options.saName}-${index}`;
|
|
1386
|
+
if (!isValidServiceAccountName(saName)) {
|
|
1387
|
+
return {
|
|
1388
|
+
status: "error",
|
|
1389
|
+
error: errorResult(SETUP_ERROR_CODES.INVALID_ARGS, `service-account name "${options.saName}" is too long to expand into a ${context.saCount}-account ` +
|
|
1390
|
+
`pool ("${saName}" is not a valid service-account name); use a shorter --sa-name`),
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const saEmail = serviceAccountEmail(saName, context.projectId);
|
|
1394
|
+
const keyPath = poolKeyPath(options.keyPath, index);
|
|
1395
|
+
const ensured = await ensureServiceAccount(runner, executed, progress, POOL_EXPANSION_PHASE, {
|
|
1396
|
+
projectId: context.projectId,
|
|
1397
|
+
saName,
|
|
1398
|
+
saEmail,
|
|
1399
|
+
});
|
|
1400
|
+
if (ensured.status === "error") {
|
|
1401
|
+
return ensured;
|
|
1402
|
+
}
|
|
1403
|
+
progress.report({ type: "operation_started", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.KEY_LIST });
|
|
1404
|
+
const keyList = await listUserManagedServiceAccountKeys(runner, executed, {
|
|
1405
|
+
projectId: context.projectId,
|
|
1406
|
+
saEmail,
|
|
1407
|
+
purpose: "baseline",
|
|
1408
|
+
});
|
|
1409
|
+
progress.report({ type: "operation_completed", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.KEY_LIST });
|
|
1410
|
+
if (keyList.status === "error") {
|
|
1411
|
+
return { status: "error", error: keyList.error };
|
|
1412
|
+
}
|
|
1413
|
+
const settled = await settleServiceAccountKey(runner, executed, {
|
|
1414
|
+
keyPath,
|
|
1415
|
+
projectId: context.projectId,
|
|
1416
|
+
saEmail,
|
|
1417
|
+
keyMarker: generateCreationMarker(),
|
|
1418
|
+
baseline: keyList.names,
|
|
1419
|
+
createPermission: "fresh",
|
|
1420
|
+
sleeper: keySleeper,
|
|
1421
|
+
});
|
|
1422
|
+
if (settled.status === "error") {
|
|
1423
|
+
return settled;
|
|
1424
|
+
}
|
|
1425
|
+
progress.report({ type: "operation_started", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.SHARE });
|
|
1426
|
+
let outcome;
|
|
1427
|
+
try {
|
|
1428
|
+
outcome = await api.ensureSaWriter({
|
|
1429
|
+
spreadsheetId: context.spreadsheetId,
|
|
1430
|
+
saEmail,
|
|
1431
|
+
ownerEmail: context.ownerEmail,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
return {
|
|
1436
|
+
status: "error",
|
|
1437
|
+
error: errorResult(SETUP_ERROR_CODES.SHEET_SHARE_FAILED, `could not share spreadsheet ${context.spreadsheetId} with ${saEmail}: ${safeReasonOf(error)}`),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
progress.report({ type: "operation_completed", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.SHARE });
|
|
1441
|
+
executed.push({
|
|
1442
|
+
kind: "api",
|
|
1443
|
+
label: `drive: share ${saEmail} as writer on ${context.spreadsheetId}`,
|
|
1444
|
+
outcome: `role ${outcome.writerRole}; ownership verified`,
|
|
1445
|
+
});
|
|
1446
|
+
const keyCredential = readServiceAccountKeyCredentialSecurely(keyPath);
|
|
1447
|
+
if (keyCredential.status === "absent") {
|
|
1448
|
+
return {
|
|
1449
|
+
status: "error",
|
|
1450
|
+
error: errorResult(SETUP_ERROR_CODES.SA_ACCESS_VERIFY_FAILED, `the service-account key file ${keyPath} recorded in the setup state is missing`),
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
if (keyCredential.status === "invalid") {
|
|
1454
|
+
return {
|
|
1455
|
+
status: "error",
|
|
1456
|
+
error: errorResult(SETUP_ERROR_CODES.SA_ACCESS_VERIFY_FAILED, `could not read the service-account key at ${keyPath}: ${keyCredential.message}`),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
if (keyCredential.credentials.projectId !== context.projectId || keyCredential.credentials.clientEmail !== saEmail) {
|
|
1460
|
+
return {
|
|
1461
|
+
status: "error",
|
|
1462
|
+
error: errorResult(SETUP_ERROR_CODES.SA_ACCESS_VERIFY_FAILED, `the service-account key at ${keyPath} belongs to ` +
|
|
1463
|
+
`${keyCredential.credentials.clientEmail} (project ` +
|
|
1464
|
+
`${keyCredential.credentials.projectId}); expected ${saEmail} in project ${context.projectId}`),
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
await options.verifySaAccess.verify({
|
|
1469
|
+
keyPath,
|
|
1470
|
+
spreadsheetId: context.spreadsheetId,
|
|
1471
|
+
keyFresh: !settled.keyReused,
|
|
1472
|
+
shareFresh: outcome.writerRole === "created" || outcome.writerRole === "upgraded",
|
|
1473
|
+
credentials: {
|
|
1474
|
+
client_email: keyCredential.credentials.clientEmail,
|
|
1475
|
+
private_key: keyCredential.credentials.privateKey,
|
|
1476
|
+
},
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
return {
|
|
1481
|
+
status: "error",
|
|
1482
|
+
error: errorResult(SETUP_ERROR_CODES.SA_ACCESS_VERIFY_FAILED, `could not verify service-account access to the spreadsheet: ${safeReasonOf(error)}`),
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
executed.push({
|
|
1486
|
+
kind: "api",
|
|
1487
|
+
label: `spreadsheets.get with the ${saEmail} key`,
|
|
1488
|
+
outcome: "service-account access verified",
|
|
1489
|
+
});
|
|
1490
|
+
// Persisted IMMEDIATELY: a later failure keeps this entry and the
|
|
1491
|
+
// resume skips it.
|
|
1492
|
+
pool.push({ saName, saEmail, keyPath });
|
|
1493
|
+
progress.report({ type: "operation_started", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.CHECKPOINT_PERSIST });
|
|
1494
|
+
const completeState = spreadsheetState(options, context.projectId, context.ownerEmail, context.title, context.primaryEmail, { spreadsheetId: context.spreadsheetId }, "complete", context.projectMode, context.keyOrigin, context.shareOrigin);
|
|
1495
|
+
if (completeState.status !== "complete") {
|
|
1496
|
+
// Unreachable: the builder returns the requested status; fail
|
|
1497
|
+
// closed rather than persist a wrong-status checkpoint.
|
|
1498
|
+
return {
|
|
1499
|
+
status: "error",
|
|
1500
|
+
error: errorResult(SETUP_ERROR_CODES.SETUP_STATE_INVALID, `could not persist the credential pool in ${options.statePath}; remove the setup state and rerun setup`),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const saveError = persistState(options, executed, { ...completeState, pool: [...pool] });
|
|
1504
|
+
progress.report({ type: "operation_completed", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.CHECKPOINT_PERSIST });
|
|
1505
|
+
if (saveError !== null) {
|
|
1506
|
+
return { status: "error", error: saveError };
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
progress.report({ type: "operation_started", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.ENV_WRITE });
|
|
1510
|
+
let envModified = false;
|
|
1511
|
+
try {
|
|
1512
|
+
const poolWrite = writeSetupEnvFile(options.outputPath, options.keyPath, spreadsheetEditUrl(context.spreadsheetId), [options.statePath, setupLockPath(options.statePath), setupStateTempPath(options.statePath)], pool.map((entry) => entry.keyPath));
|
|
1513
|
+
envModified = poolWrite.modified;
|
|
1514
|
+
}
|
|
1515
|
+
catch (error) {
|
|
1516
|
+
return {
|
|
1517
|
+
status: "error",
|
|
1518
|
+
error: errorResult(carrierCode(error) ?? SETUP_ERROR_CODES.OUTPUT_WRITE_FAILED, `could not write ${options.outputPath}: ${messageOf(error)}`),
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
progress.report({ type: "operation_completed", phase: POOL_EXPANSION_PHASE, operation: SETUP_PROGRESS_OPERATIONS.ENV_WRITE });
|
|
1522
|
+
progress.report({ type: "phase_completed", phase: POOL_EXPANSION_PHASE, source: "run" });
|
|
1523
|
+
return { status: "ok", pool, envModified, keptPoolEntries };
|
|
1524
|
+
}
|
|
1189
1525
|
/**
|
|
1190
1526
|
* Describes a failed gcloud invocation with phase context and exit status
|
|
1191
1527
|
* only.
|