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.
Files changed (39) hide show
  1. package/dist/auth/serviceAccountAuthPool.d.ts +72 -0
  2. package/dist/auth/serviceAccountAuthPool.d.ts.map +1 -0
  3. package/dist/auth/serviceAccountAuthPool.js +92 -0
  4. package/dist/auth/serviceAccountAuthPool.js.map +1 -0
  5. package/dist/auth/serviceAccountKey.d.ts +85 -0
  6. package/dist/auth/serviceAccountKey.d.ts.map +1 -0
  7. package/dist/auth/serviceAccountKey.js +104 -0
  8. package/dist/auth/serviceAccountKey.js.map +1 -0
  9. package/dist/cli/args.d.ts +19 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +50 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/cli/checkpoint.d.ts +37 -1
  14. package/dist/cli/checkpoint.d.ts.map +1 -1
  15. package/dist/cli/checkpoint.js +94 -14
  16. package/dist/cli/checkpoint.js.map +1 -1
  17. package/dist/cli/confirm.d.ts +17 -0
  18. package/dist/cli/confirm.d.ts.map +1 -1
  19. package/dist/cli/confirm.js +1 -1
  20. package/dist/cli/confirm.js.map +1 -1
  21. package/dist/cli/envFileWriter.d.ts +15 -2
  22. package/dist/cli/envFileWriter.d.ts.map +1 -1
  23. package/dist/cli/envFileWriter.js +18 -3
  24. package/dist/cli/envFileWriter.js.map +1 -1
  25. package/dist/cli/setup.d.ts +2 -0
  26. package/dist/cli/setup.d.ts.map +1 -1
  27. package/dist/cli/setup.js +20 -2
  28. package/dist/cli/setup.js.map +1 -1
  29. package/dist/cli/setupFlow.d.ts +67 -0
  30. package/dist/cli/setupFlow.d.ts.map +1 -1
  31. package/dist/cli/setupFlow.js +401 -65
  32. package/dist/cli/setupFlow.js.map +1 -1
  33. package/dist/cli/setupProgress.d.ts +16 -1
  34. package/dist/cli/setupProgress.d.ts.map +1 -1
  35. package/dist/cli/setupProgress.js +47 -1
  36. package/dist/cli/setupProgress.js.map +1 -1
  37. package/dist/sheets/providers/google-sheets-api/transport/googleSheetsApiTransport.js +1 -1
  38. package/dist/sync-engine/sync/service/syncAutoStart.js +1 -1
  39. package/package.json +1 -1
@@ -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":"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"}
1
+ {"version":3,"file":"setupFlow.d.ts","sourceRoot":"","sources":["../../../../../../src/setupFlow.ts"],"names":[],"mappings":"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"}
@@ -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 { SETUP_ERROR_CODES, carrierCode } from "./errors.js";
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
- return [
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
- ].join("\n");
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
- progress.report({ type: "operation_started", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
739
- const saList = await runner.run(["iam", "service-accounts", "list", "--project", projectId, "--format=value(email)"]);
740
- progress.report({ type: "operation_completed", phase: "service_account", operation: SETUP_PROGRESS_OPERATIONS.SA_LIST });
741
- executed.push({
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 (saList.status !== "ok") {
747
- return errorResult(SETUP_ERROR_CODES.SA_CREATE_FAILED, `could not list service accounts: ${describeGcloudFailure(saList)}`);
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
- projectId,
1145
- ownerEmail,
1146
- serviceAccountEmail: email,
1147
- keyPath: options.keyPath,
1148
- spreadsheetId: spreadsheet.spreadsheetId,
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.