/** * Orchestration for `hikoutei setup`. * * `runSetup` is the pure-ish core of the CLI: it receives injected gcloud * runner, token validator, human-token sheet API factory, and service-account * verifier, and drives the bootstrap sequence (preflight, Drive-scoped human * auth, exclusive setup lock, checkpoint, project, API enable, service * account, key, human-owned spreadsheet, SA writer share, SA access verify, * .env, complete checkpoint) and returns an explicit result union. Nothing * here reads process state; the thin entry in `setup.ts` resolves defaults * and prints output. `--dry-run` builds the exact command plan: it performs * only read-only local path-safety resolution (the reserved-path collision * check) and never invokes a subprocess, the network, or the cloud, and * never mutates the filesystem (no lock, checkpoint, key, or .env writes). * * The spreadsheet is created by the logged-in human account (service accounts * cannot own Workspace assets) and shared with the service account as a * writer. The access token exists only in memory: it is never written to the * checkpoint, the .env file, or any message. Drive `files.generateIds` ids * cannot create Google Workspace files, so creation uses an honest * marker-based write-ahead contract instead: a local opaque creation marker * (a UUID) is generated and persisted as `spreadsheet_create_started` BEFORE * the one and only remote create attempt, and that same create request * carries the marker as a private `appProperties` entry. A lost response or * failed create is reconciled by querying Drive for that exact marker; when * the outcome cannot be confirmed the run fails with `sheet_create_uncertain` * and NEVER creates a second spreadsheet — the next run reconciles the * started state by marker again. The spreadsheet URL is derived from the id * and never stored. Sharing is a write-ahead too: `spreadsheet_share_started` * (spreadsheet id + keyOrigin, no shareOrigin) is persisted BEFORE the * idempotent writer-permission ensure can create/upgrade the permission, * and `spreadsheet_shared` only after the ensure completes — a crash * between the remote permission mutation and that write leaves * `spreadsheet_share_started` and resumes safely; a loaded * share-started state conservatively persists `shareOrigin: "fresh"` * (the prior attempt may have created/upgraded before crashing) so the * 403/404 propagation retries are preserved. * * An exclusive setup lock (`.lock`) is acquired after the human * Drive-scope preflight and held through the whole run, so concurrent runs * cannot double-create cloud resources; the lock is an EMPTY DIRECTORY * created with `mkdir` (mode 0700) and released with `rmdir` on every exit. * A crash leaves the empty directory behind for manual removal only; it is * never removed automatically. The service-account key is created by * gcloud under a REAL write-ahead state: the flow lists the user-managed * keys of the service account, persists a `key_create_started` checkpoint * (UUID marker + baseline) BEFORE the single gcloud key create, and the * marker derives a deterministic private sibling staging directory so a * crash at any key boundary resumes by reconciliation instead of creating * a second key (an unmatched cloud key fails with `key_create_uncertain`; * nothing is ever deleted automatically). The key is validated in the * staging directory and installed at the final key path with an atomic * same-filesystem hard link, so the final path is never pointed at by * gcloud and a planted entry there fails closed (`key_create_failed`) * instead of being overwritten; reused keys are enforced to owner-only * mode 0600 through one secure descriptor. Whether the key was CREATED * by the setup or REUSED is persisted as the non-secret `keyOrigin` * discriminant from `key_ready` onward, so a resumed run keeps the * verify-phase propagation freshness (Invalid JWT Signature retries) * without conflating it with the current run's reuse summary. The `.env` * write is also atomic (unique private temp file + rename) and never * reads or follows a symlink/hardlink alias of the key or checkpoint. * Automatic setup runs on macOS and Linux; on Windows a non-dry-run fails * with `unsupported_platform` before any subprocess, network, cloud, * lock, checkpoint, key, or env mutation, and manual setup remains * available. */ import { type LockFs } from "./checkpoint.js"; import { type SetupFailure } from "./errors.js"; import { type PlannedCommand, type SetupErrorResult } from "./flowResult.js"; import { poolKeyPath } from "./setupPaths.js"; export { writeSetupEnvFile, atomicWritePrivateFile, SETUP_ENV_KEYS } from "./envFileWriter.js"; export { findSetupPathCollision, type SetupPathCollision } from "./setupPathCollision.js"; export { poolKeyPath }; import { type GcloudRunner } from "./gcloudRunner.js"; import { type SetupProgressSink } from "./setupProgress.js"; import { type TokenValidator } from "./humanAuth.js"; import { type Sleeper } from "./keyProvision.js"; import type { SaAccessVerifier } from "./saVerify.js"; import type { HumanSheetApiFactory, ShareOutcome } from "./sheetsFactory.js"; /** Default service-account key file name, resolved against the current directory. */ export declare const DEFAULT_KEY_FILE_NAME = "hikoutei-service-account.json"; /** Prefix of the default spreadsheet title (`hikoutei-sync-`). */ export declare const DEFAULT_SPREADSHEET_TITLE_PREFIX = "hikoutei-sync"; /** Service-account key file permission after setup (owner read/write only). */ export declare const KEY_FILE_MODE = 384; /** Options for one setup run; paths are absolute and resolved by the entry. */ export interface RunSetupOptions { readonly runner: GcloudRunner; /** Validates the user access token through tokeninfo (Drive scope check). */ readonly validateToken: TokenValidator; /** Builds the human-token Sheets/Drive API for a run. */ readonly createHumanApi: HumanSheetApiFactory; /** Verifies the service-account key can read the spreadsheet (with retries). */ readonly verifySaAccess: SaAccessVerifier; /** Existing project id, or undefined to create `hikoutei-`. */ readonly projectId: string | undefined; readonly saName: string; /** Spreadsheet title override; defaults to `hikoutei-sync-`. */ readonly spreadsheetTitle: string | undefined; /** Absolute service-account key path. */ readonly keyPath: string; /** Absolute .env output path. */ readonly outputPath: string; /** Absolute setup checkpoint path (`.hikoutei-setup-state.json`). */ readonly statePath: string; readonly dryRun: boolean; /** * Service accounts to provision as a credential pool. Defaults to 1 * (single-SA behavior, unchanged); N > 1 provisions `-` * accounts after the primary flow, sharing the same spreadsheet and * recorded in `HIKOUTEI_SYNC_CREDENTIALS`. Values outside 1..10 are an * `invalid_args` usage error. Resuming with a smaller count keeps every * existing pool entry (the pool never shrinks); the summary reports the * actual pool size. */ readonly saCount?: number; /** * Optional progress sink for the CLI renderer. When omitted the run is * unaffected; when present a throwing callback is swallowed so progress * can never change the setup result, the mutation order, or the exit * code. Internal CLI machinery only — never part of the public API. */ readonly progress?: SetupProgressSink; /** Filesystem operations for the exclusive setup lock; injectable for tests. */ readonly lockFs?: LockFs; /** Platform of the run; defaults to `process.platform`. A non-dry-run on * `win32` is refused with `unsupported_platform` before any subprocess, * network, cloud, lock, checkpoint, key, or env mutation (Windows cannot * guarantee no-follow or owner-only ACL semantics); dry runs remain pure * on every platform. Injectable so tests can exercise the gate on macOS. */ readonly platform?: string; /** * Timer for the bounded key-settlement propagation poll; defaults to a * real `setTimeout` sleeper so production actually waits. Injectable so * tests are instant. */ readonly sleeper?: Sleeper; } /** One planned or executed step of the setup flow. */ export type { PlannedCommand } from "./flowResult.js"; /** The error branch of a setup result. */ export type { SetupErrorResult } from "./flowResult.js"; /** Outcome summary returned for a successful (non-dry-run) setup. */ export interface SetupSummary { readonly projectId: string; /** Human account that owns the spreadsheet (from the validated token). */ readonly ownerEmail: string; readonly serviceAccountEmail: string; readonly keyPath: string; readonly spreadsheetId: string; readonly spreadsheetUrl: string; readonly spreadsheetTitle: string; readonly outputPath: string; /** Absolute checkpoint path; status reflects the persisted progression. */ readonly statePath: string; readonly stateStatus: string; readonly envFileCreated: boolean; readonly envFileModified: boolean; readonly projectReused: boolean; readonly serviceAccountReused: boolean; readonly keyReused: boolean; /** How the SA writer permission was ensured; `unchanged` when resumed past sharing. */ readonly saWriterRole: ShareOutcome["writerRole"] | "unchanged"; /** True when this run resumed from an existing checkpoint. */ readonly resumed: boolean; /** Provisioned pool size (1 for single-SA runs). */ readonly poolSize: number; /** Key paths of the provisioned pool, entry 1 first ([keyPath] for N=1). */ readonly poolPaths: readonly string[]; /** * Pool entries kept from a previous run (absent/0 for fresh runs and * N=1). A resume never removes entries, so resuming with a smaller * `--sa-count` reports the actual (larger) pool with this kept count. */ readonly poolKeptEntries?: number; } /** Discriminated result of a setup run. */ export type SetupResult = { readonly status: "ok"; readonly dryRun: false; readonly summary: SetupSummary; readonly commands: readonly PlannedCommand[]; } | { readonly status: "ok"; readonly dryRun: true; readonly commands: readonly PlannedCommand[]; } | SetupErrorResult; /** Default spreadsheet title for a project. */ export declare function defaultSpreadsheetTitle(projectId: string): string; /** * Generates a `hikoutei--` project id. * * The timestamp base-36 component sorts lexically and the random suffix makes * parallel runs collision-resistant; if the id already exists the flow reuses * the project instead of failing. */ export declare function generateProjectId(now?: number, random?: () => number): string; /** * Builds the exact command plan for a dry run. * * Pure: does not execute anything and performs no filesystem mutation — no * subprocess, network, or cloud calls, and no lock/checkpoint/key/.env * writes. The caller has already run the read-only reserved-path collision * resolution before planning, so a dry run may perform read-only local * path-safety checks but never reads checkpoint or key file contents. * Each step carries a simulated outcome so `--dry-run` previews the scope * check, the exclusive setup lock, both API enables, the marker write-ahead * and human-owned sheet creation, SA share/verify, checkpoint, and .env * write. */ export declare function planSetupCommands(options: RunSetupOptions, slug: string): readonly PlannedCommand[]; /** Renders a plan or executed-command list for `--dry-run` output. */ export declare function formatPlan(commands: readonly PlannedCommand[]): string; /** Renders the human summary; never includes key contents or tokens. */ export declare function formatSummary(summary: SetupSummary): string; /** * Runs the full setup bootstrap. * * In dry-run mode returns the command plan after the read-only reserved-path * collision resolution, without invoking the runner or mutating the * filesystem: no subprocess, network, or cloud calls, and no lock, * checkpoint, key, or .env writes. Otherwise the sequence is: path collision * check (rejects `--output` aliasing the key/checkpoint/temp/lock paths * before anything runs), preflight (gcloud present, active account), human * auth (Drive scope verified through tokeninfo, memory-only token), exclusive * setup lock (held until every exit), checkpoint load (resume skips * completed work; mismatches fail with `setup_state_conflict`), project * verify/create (checkpoint persisted before creation; an explicit project * is never created, a resumed generated project is described first and * created only when confirmed absent), `config set project`, enable the * Sheets and Drive APIs, service account list/create, staged key create * (gcloud writes a private sibling staging path, the staged key is * validated there and atomically hard-linked to the final path with mode * 600; an existing validated key is reused; only the invocation that just * persisted the fresh key_create_started checkpoint may create — resumed * key states are reconcile-only and poll through a bounded propagation * window before key_create_uncertain), marker write-ahead + human-owned * spreadsheet creation (the creation marker is persisted as * `spreadsheet_create_started` BEFORE the single create attempt; a lost * response is reconciled by querying Drive for that marker and a second * create is never attempted), SA writer share + Drive ownership verification * (a `spreadsheet_share_started` write-ahead is persisted before the * idempotent permission ensure and `spreadsheet_shared` after it), * SA-key access verification with retries (propagation retries only for * resources created this run, tracked by the persisted keyOrigin and * shareOrigin discriminants), the .env write, and finally the `complete` * checkpoint. Every phase maps to a stable error code on failure; key * material and the access token are never included in messages. */ export declare function runSetup(options: RunSetupOptions): Promise; /** Prompt text for the interactive service-account count question. */ export declare const SA_COUNT_PROMPT = "Service accounts to create? [1]: "; /** Reads one line of prompt input; null on end-of-input. Injectable for tests. */ export type SaCountLineReader = () => Promise; /** Inputs for resolving the service-account count of a run. */ export interface ResolveSaCountOptions { /** Parsed `--sa-count` value, or undefined when the flag was not given. */ readonly saCount: number | undefined; readonly yes: boolean; readonly dryRun: boolean; /** True when the session is an interactive terminal (stdin and stdout TTY). */ readonly isTTY: boolean; /** Writes the prompt text; required when prompting. */ readonly write?: (text: string) => void; /** Reads one input line; required when prompting. */ readonly readLine?: SaCountLineReader; } /** Outcome of resolving the service-account count of a run. */ export type SaCountResolution = { readonly status: "ok"; readonly saCount: number; } | { readonly status: "invalid"; readonly failure: SetupFailure; }; /** * Resolves how many service accounts to provision. * * `--yes`, `--dry-run`, and non-TTY sessions never prompt and use the * flag (default 1). An interactive TTY session without `--sa-count` asks * once (`Service accounts to create? [1]: `): an empty answer means 1, a * valid 1..10 integer wins, and invalid input re-asks exactly once before * failing with an `invalid_args` usage error. End-of-input on the first * read counts as empty (1); end-of-input on the re-ask fails closed with * an `invalid_args` usage error (a typo followed by Ctrl-D must never * silently provision 1). A missing line reader degrades to the default * without prompting. */ export declare function resolveSaCountForSetup(options: ResolveSaCountOptions): Promise; //# sourceMappingURL=setupFlow.d.ts.map