#!/usr/bin/env node /** * `hikoutei setup` CLI entry. * * The orchestration is split into a thin `main()` that wires production * dependencies (gcloud runner, token validator, sheet factory, SA verifier, * interactive login runner) and a testable {@link runSetupCli} that owns the * sequence: resolve paths, reject canonical path collisions, ask for the * one-time y/N confirmation (skipped by `--yes`/`--dry-run`), run setup, and * on an auth preflight failure offer a single interactive Enter-to-login * handoff into `gcloud auth login` before retrying exactly once (interactive * TTY only, never in CI/`--yes`/`--dry-run`). Exit codes: * 0 success, 2 argument errors, 1 runtime failures. Errors are printed as * `hikoutei-setup:: ` for machine consumption, and key * material and the user access token are never printed. */ import { type SetupOptions } from "./args.js"; import { type GcloudLoginRunner } from "./gcloudRunner.js"; import { type SetupProgressController } from "./setupProgress.js"; import { type SetupResult } from "./setupFlow.js"; /** Pure parameters for one setup attempt (no injected infrastructure). */ export interface RunSetupParams { readonly projectId: string | undefined; readonly saName: string; readonly spreadsheetTitle: string | undefined; readonly keyPath: string; readonly outputPath: string; readonly statePath: string; readonly dryRun: boolean; /** Resolved service-account count (1 for single-SA runs). */ readonly saCount: number; } /** * Runs one setup attempt. * * Infrastructure (gcloud runner, token validator, sheet factory, SA verifier) * is closed over by the caller, so {@link runSetupCli} can be tested with a * scripted callable that returns auth preflight failures and successes * without wiring up the full harness. */ export type RunSetupCallable = (params: RunSetupParams) => Promise; /** Terminal line source for the CLI; `process.stdin` in production. */ export interface CliStdin extends AsyncIterable { /** True when stdin is attached to a terminal (gates the login handoff). */ readonly isTTY?: boolean; } /** Terminal output sink; `process.stdout` in production. */ export interface CliStdout { /** True when stdout is attached to a terminal (gates the login handoff). */ readonly isTTY?: boolean; readonly write: (text: string) => void; } /** Diagnostic output sink; `process.stderr` in production. */ export interface CliStderr { readonly write: (text: string) => void; } /** Context injected into {@link runSetupCli}; production values come from `main()`. */ export interface RunSetupCliContext { readonly options: SetupOptions; readonly cwd: string; /** Runs one setup attempt with all real/fake infrastructure closed over. */ readonly runSetup: RunSetupCallable; /** Runs the interactive `gcloud auth login` handoff attached to the terminal. */ readonly loginRunner: GcloudLoginRunner; readonly stdin: CliStdin; readonly stdout: CliStdout; readonly stderr: CliStderr; /** * True when the session runs in an automation environment (a non-empty * `CI` value). Gates the interactive login handoff so a CI pseudo-TTY * can never prompt, hang, or spawn the browser login; production main * passes the real process CI state via {@link isCiEnvironment}. * Optional so scripted tests that do not exercise the handoff run * unchanged (absent means not CI). */ readonly isCi?: boolean; /** * Optional progress controller (the stderr renderer in production). When * present it is suspended before the inherited `gcloud auth login` * handoff, marked failed on a final error, and finished on success. Tests * that drive a scripted `runSetup` callable omit it. */ readonly progress?: SetupProgressController; /** * Releases the shared stdin after every prompt and the inherited gcloud * login have finished; called exactly once on every setup outcome * (collision, declined confirmation, success, dry run, errors, login * cancel/failure, and retries). Optional so tests that do not exercise * the lifecycle run unchanged. * * The confirmation and login-handoff prompts read `process.stdin` through * its async iterator WITHOUT calling the iterator's `return()` so the two * sequential prompts share one stream; that open iterator keeps an internal * listener on the Readable and would hold the process alive after setup * finishes. The production finalizer destroys `process.stdin` to release * it. It is invoked from a `finally` AFTER the inherited login subprocess * (which needs the live terminal) resolves, never before it. */ readonly finalizeStdin?: () => void; } /** * Orchestrates the setup CLI: path resolution, collision guard, one-time * confirmation, the setup run, and the optional interactive login handoff. * * The handoff runs ONLY when the first attempt fails with an auth preflight * error (`gcloud_not_logged_in` or `gcloud_drive_access_required`) AND the * session is a real interactive terminal (`stdin.isTTY && stdout.isTTY`) AND * the session is not an automation run (`isCi` absent/false; production * passes the non-empty `CI` environment state, so a CI pseudo-TTY can never * prompt or spawn the browser login) AND neither `--yes` nor `--dry-run` * was given. The preflight runs before any * lock, checkpoint, cloud, or file mutation, so retrying after a successful * login cannot create duplicate resources. The login is attempted at most * once and the setup retried at most once; if the retry still lacks the * scope, the original sanitized error is reported with no further login. * * @returns the process exit code (0 success, 2 argument collision, 1 failure). */ export declare function runSetupCli(context: RunSetupCliContext): Promise; /** * Pure ESM entrypoint guard: true only when `entryArg` resolves (following * symlinks) to the same file as `moduleUrl`. * * In CommonJS the bin would use `require.main === module`; in this package's * pure-ESM layout (`"type": "module"`) this realpath comparison is the robust * equivalent. `npx hikoutei` runs the published bin through a symlink, so the * realpath step resolves `.bin/hikoutei` to `dist/cli/setup.js` and matches * `import.meta.url`. When this module is imported — for example by the unit * tests importing `runSetupCli` — the entry argument is the test runner binary * and never matches, so `main()` is not executed and nothing parses argv, * writes CLI errors, or mutates `process.exitCode` on import. */ export declare function isModuleMainEntry(entryArg: string | undefined, moduleUrl: string): boolean; /** * Production stdin finalizer for `hikoutei setup`. * * The confirmation and login-handoff prompts read `process.stdin` through * its async iterator WITHOUT calling the iterator's `return()`, so the two * sequential prompts can share one stream. That open iterator keeps an * internal listener on the Readable and would hold the process alive after * setup finishes; pausing the stream does not drop that listener. Destroying * the stream after every prompt and the inherited `gcloud auth login` * subprocess (which needs the live terminal) have finished is the definitive * release and is safe on every outcome — TTY, piped, never-read, or already * ended. Guarded and idempotent so it never throws or double-releases. */ /** * Destroys the process stdin exactly once so the CLI process can exit after * a single-chunk confirmation read leaves the shared async iterator open. * Exported for the adopt CLI, which prompts over the same shared stdin. */ export declare function createStdinFinalizer(): () => void; /** * Runs the `hikoutei setup` CLI with the given argument vector. Exported for * the bin router (src/cli/index.ts) and tests; the argv has already had a * leading "setup" subcommand stripped when routed through the router. */ export declare function runSetupMain(argv: readonly string[]): Promise; //# sourceMappingURL=setup.d.ts.map