import type { Server as HttpServer } from "node:http"; import type { ServerError } from "@superblocksteam/library-shared/types"; import type { AuthHotReloadServer, MultiPageApplicationWrapper, TokenManager, SuperblocksSdk } from "../index.js"; import { type Logger } from "../telemetry/logging.js"; import type { ApplicationConfig, TokenConfig } from "../types/index.js"; import { type PackageJsonSnapshot } from "./package-json-snapshot.mjs"; import type { StartupPhase } from "./startup-phase.mjs"; export declare enum DevServerAutoUpgradeMode { SKIP = "skip-upgrade", FORCE = "force-upgrade", /** * Skip CLI auto-upgrade but still run the package.json version sync. * Used by warm pool activation — the warm pod ships with the latest CLI * so an auto-upgrade would pointlessly restart the process, but the user's * application package.json still needs to be pinned to the target * @superblocksteam/library version. */ SKIP_CLI_ONLY = "skip-cli-only" } export declare function shouldUseExternalAiService(env?: NodeJS.ProcessEnv | { SUPERBLOCKS_AI_SERVICE_URL?: string; }): boolean; /** * Seed `tokenManager` with the initial token the CLI received from auth.json * (standalone dev) or `/_sb_activate` (SABS live-edit pod activation), so the * `NpmRegistryClient`'s JWT source has a usable bearer credential on cold * boot. * * Without this seeding, `TokenManager.updateToken` is only ever called by * `AuthHotReloadServer` (`auth-hot-reload.mts`) — and that socket is * explicitly disabled on live-edit pods (`sabs/entrypoint-local.sh` sets * `SUPERBLOCKS_AUTH_HOT_RELOAD=false`), so `NpmRegistryClient.getConfig()` * cannot authenticate its server fetch on cold boot. The result is * `source: "unreachable"`, `syncHomeNpmrc` skips with `~/.npmrc` left * untouched, and the CLI auto-upgrade that fires moments later resolves * `npm install -g @superblocksteam/cli@…` through public npm instead of the * customer's configured private registry. * * Call-site ordering is intentionally NOT load-bearing: `TokenManager` * retains the current token as state and `AiService` reads * `tokenManager.getCurrentToken()` synchronously at construction time, so * the prime call works whether it runs before or after consumer * construction. The `tokenUpdated` event stream stays as the refresh * channel for future rotations (hot-reload pushes, JWT renewal), so this * seed is purely additive. */ export declare function primeTokenManagerWithInitialToken(tokenManager: TokenManager, token: string): void; export interface DevServerStatus { serverErrors: ServerError[]; } /** * True when this dev server was launched as a CSB sandbox — `SUPERBLOCKS_IS_CSB * === "true"`, the same variable that already gates the CSB-only behavior * elsewhere in this file. That is two populations, not one: * * - a disposable, orchestrator-managed SABS live-edit pod, and * - the LOCAL `csb-mock-server` emulator, which sets the identical variable * when it spawns `superblocks dev` for code-mode work * (`packages/csb-mock-server/src/app-manager.ts`, `startProcess` and * `restartSandbox`; neither passes anything that skips the auto-upgrade). * * So this is NOT "cloud, not a developer's machine". The emulator answering * `true` is intended — it exists to emulate a pod, and an exit is the behavior a * pod would show — but it emulates only half the story: `attachEventListeners` * in that file merely logs the child's exit, so nothing restarts it. The * practical consequence for a local dev whose auto-upgrade fails this way is a * dead dev server until something calls `startProcess` again (reopening or * restarting the app in code mode). Only a dev server the developer ran * directly, with no `SUPERBLOCKS_IS_CSB`, still degrades. * * Read at call time, not module load, so tests can set the variable per-case. */ export declare function isDisposableSandbox(): boolean; /** * The single implementation of "flush telemetry, but never wait forever". * * Two paths need it, for the same reason: the OTel metric reader exports on a * 10s interval, so a sample recorded moments before the process goes away dies * with it unless something forces the export — and neither path may block * indefinitely on a slow or unreachable collector. The callers are the * fatal-exit sequence (`flushTelemetryBeforeExit`, just below) and the * auto-upgrade CLI restart in `dev()`, which used to carry its own copy of this * race with the timer-clear missing. * * Resolves `true` only when a flush actually ran to completion — `false` on an * internal shutdown error, when telemetry was never initialized, or when the * timeout won the race. * * Reporting that boolean is left to the callers rather than parameterized here, * because the useful part of the message is what an operator should read * INSTEAD of the dropped samples, and that differs per path: the fatal paths * point at their `event=dev_server_fatal_exit` line, which the restart path * never writes. * * Rejects only if `shutdownTelemetry()` breaks its documented never-reject * contract, so callers keep their own defensive catch. */ export declare function flushTelemetryWithTimeout(): Promise; /** * One teardown step. Returning `undefined` is allowed so callers can pass the * optional-chained call directly (`() => lockService?.relinquishLock()`) * without having to re-narrow a mutable binding at the call site. */ type FatalExitStep = () => Promise | undefined; /** * The ordered teardown every fatal-exit path in `dev()` runs immediately before * `process.exit(1)`. Stated once, here, so the ordering guarantee cannot drift * between the two call sites. * * Order is load-bearing: * 1. `removeIntegrationCache` — ai-service state that must not outlive us. * 2. `relinquishLock` — hands the AppBranchLock back. Recovery-critical: * the heartbeat dies with the process, so skipping this makes the * replacement sandbox's `acquireLock()` fail with a ConflictError until * the server-side TTL expires, and the replacement is the entire point of * exiting. * 3. `flushTelemetryBeforeExit` — LAST, because it tears the telemetry * provider down (anything after it is unobservable) and can block for * `TELEMETRY_FLUSH_TIMEOUT_MS`, which would otherwise delay the lock * release above. * * Every step is individually guarded and individually bounded, so neither a * throw nor a hang in an earlier step can cost a later one. Never throws. * * This does NOT exit. The caller owns `process.exit(1)` from a `finally`, so * the exit still happens even if this function is somehow bypassed. That is * also why callers must drive the lock service through `relinquishLock()` and * not `shutdownAndExit()`: the latter ends in its own unconditional * `finally { process.exit(1) }`, and `process.exit` is synchronous, so * everything sequenced after it — including the flush — silently never ran. */ export declare function runFatalExitShutdown(steps: { logger: Logger; removeIntegrationCache?: FatalExitStep; relinquishLock?: FatalExitStep; }): Promise; /** * True when a failed auto-upgrade could have mutated the live dependency tree, * and therefore may have killed the running esbuild service out from under * Vite. Unknown/unclassified categories deliberately answer `true`: ENG-5297 * arrived as `category: "unknown"`, and a category this function has never seen * must fail closed (exit) rather than inherit the degrade path by default. * * This reduces to `category === "unknown"` against today's five-member union — * see `RESOLUTION_PHASE_UPGRADE_CATEGORIES` for why the set is kept rather than * collapsed to that comparison. * * The parameter is `string`, not `DependencyUpgradeError["category"]`, because * that is the actual runtime contract: the payload is deserialized from the * socket, so the five-member union is a promise about our own writers rather * than a guarantee about the value — which is also why the set above is a * `ReadonlySet`. It is also what lets the fail-closed default be * asserted for a category a FUTURE CLI adds; that case is not expressible * through an `UpgradeFailed` marker without an `as` cast, and it is the one this * gate most needs to keep. * * `"unknown"` is itself coarse; the two sub-cases sit on opposite sides of the * question this function asks, and only one of them is a false positive: * * no install command `resolveCommand` returned nothing, so `child_process.exec` * was never reached (automatic-upgrades.ts, the * `if (!installCommand)` early throw). The tree is * provably untouched and exiting is a false positive. * version validation `getCurrentCliVersion()` returned nothing / mismatched. * That check runs AFTER `await promise` resolves — i.e. * after the global install completed — so the tree * genuinely moved and exiting is correct. * * Only the first is a false positive, and it is not distinguishable from data * already on `serverError`: both arrive as `category: "unknown"` with no * `npmErrorCode`, and the only thing separating them is free-text English in * `rawError`. Keying the exit decision on that string would be the same * phase-blind text proxy this file already flags as the gate's weak point. * Splitting them properly means a flag stamped at the throw site on * `DependencyUpgradeError` — a change to a shared contract, deliberately not * made here. Until then both land on exit, the direction this function is meant * to err in. * * Note on cost if the false positive fires: SABS pods run `RestartPolicy: Never`, * so the retry is a pod REPLACEMENT driven by the orchestrator, not an in-pod * crash loop. A deterministic, environment-derived cause would therefore * re-trigger in each replacement, and no per-process bound inside this file * could stop that — a replacement is a fresh process with a fresh counter. Any * real bound belongs in the orchestrator that decides to replace. */ export declare function upgradeCategoryCouldTaintTree(category: string): boolean; /** Decide how the startup catch handles an error: degrade (record, keep Vite up) * for an app-install failure (`InitialInstallFailed`) or a best-effort * CLI/library auto-upgrade failure (`UpgradeFailed`), or exit for anything else * (lock/sync/genuinely-unusable upgrade env). Pure + unit-tested. Does NOT call * process.exit. */ export declare function handleStartupError(error: unknown, status: DevServerStatus, logger: Logger, options?: { /** * True when this process was launched as a CSB sandbox — a SABS live-edit * pod OR the local `csb-mock-server` emulator, which sets the same * `SUPERBLOCKS_IS_CSB` variable. Decides whether a failed best-effort * upgrade may degrade or must be fatal — see the `UpgradeFailed` branch. * `isDisposableSandbox` documents why the local emulator is deliberately * included and what that costs a local dev. */ disposableSandbox?: boolean; }): "degrade" | "exit"; export declare function dev(options: { cwd: string; devServerPort?: number; uploadFirst?: boolean; downloadFirst?: boolean; forceTakeover?: boolean; skipSync?: boolean; applicationConfig: ApplicationConfig; tokenConfig: TokenConfig; superblocksPath?: string; logger?: (...messages: unknown[]) => void; autoUpgradeMode?: DevServerAutoUpgradeMode; signal?: AbortSignal; tokenManager: TokenManager; authHotReloadServer?: AuthHotReloadServer; sdk: SuperblocksSdk; /** Pre-fetched application data from the CLI validation step (avoids a duplicate network call). */ prefetchedApplication?: MultiPageApplicationWrapper; /** Pre-existing HTTP server from warm standby mode (avoids port gap on transition). */ existingServer?: HttpServer; /** * Wall-clock timestamp (Date.now()) when the warm-standby /_sb_activate * POST was accepted. Forwarded to createDevServer so the warm-activation * handoff histogram can be recorded. */ warmActivationStart?: number; /** Force package installation when the caller has detected dependency drift. */ forcePackageInstall?: boolean; /** Package snapshot before S3 workspace restore; used to refresh forced install after DBFS sync. */ packageJsonSnapshotBeforeRestore?: PackageJsonSnapshot | null; /** Restore managed warm-template package pins after DBFS download in S3 warm restore flow. */ normalizeManagedPackageDependencies?: boolean; /** Start only the AI service HTTP/socket server, not Vite. */ aiOnly?: boolean; /** Reuse the AI warm-standby listener during activation. */ aiHttpServer?: HttpServer; /** Receives the startup install phase the warm-standby `/_sb_health` reports. */ startupPhase?: Pick; }): Promise; export {}; //# sourceMappingURL=dev.d.mts.map