/** * @fileoverview Shared runtime context — config loading, merging, and writing. * * This module is the central authority on everything configuration: * - Reading raw config files (global + project) without merging * - Merging global + project configs with documented precedence rules * - Resolving effective values (appCode, env, format, riskLevel, etc.) * - Writing scoped configs (global vs. project) atomically * * Why a dedicated context module: configuration is read at startup, written by * `workspace init/use`, `app import`, and `config set`, and consulted by every command via * `buildConfig()`. Centralizing all config operations here means: * 1. The merge algorithm is defined once and applied consistently everywhere. * 2. Adding a new config field only requires updating `buildConfig()`. * 3. Tests can mock `readRawConfig` to simulate different config states. * * Key design decisions: * - Project config always overlays global config with standard key precedence. * - `appCode` is resolved from explicit flags, top-level config, or the named * local app profile selected by `defaultApp`. * - Atomic writes (temp file + rename) prevent config corruption on crashes. * - `migrateOnlineEnv()` silently upgrades old `"online"` env values to * `"production"` so old config files continue to work. */ import { logger } from "./utils/logger.js"; import type { OutputFormat, Risk } from "./framework/types.js"; export type { AuthMode } from "./constant/auth-mode.js"; export { AUTH_MODE, isAuthMode } from "./constant/auth-mode.js"; /** * Legacy app profile shape from older `.lovrabet.json` files. * Kept only for compatibility reads; new writes do not create these blocks. */ export interface AppProfile { appcode: string; env?: "production" | "development" | "daily"; cookie?: string; accessKey?: string; format?: OutputFormat; pageSize?: number; riskLevel?: Risk; locale?: string; description?: string; remoteDescription?: string; } /** * Fully resolved CLI configuration used by the runner and all commands. * * Why `appCode` can be undefined: `buildConfig()` is called even for commands * that don't need an appCode (e.g. `init`, `help`). Commands that require it * must check `config.appCode` and throw via `CliErrors.configMissing()`. * * Why `riskLevel` defaults to `"write"` (not `"read"`): Lovrabet Runtime CLI's * primary purpose is data manipulation. Defaulting to `"write"` avoids forcing * users to configure a risk ceiling before performing routine operations. */ export interface CLIConfig { appCode: string | undefined; /** Source used to resolve `appCode`, useful for async revalidation of `--app`. */ appCodeSource: "explicit" | "env" | "local-alias" | "cache" | "top-level" | undefined; env: "production" | "development" | "daily"; locale: string; cookie: string | undefined; accessKey: string | undefined; format: OutputFormat | undefined; pageSize: number | undefined; riskLevel: Risk; /** Machine-managed host capability used only to redirect blocked writes. */ nativeToolAvailable: boolean; verbose: boolean; /** Default app name used to resolve appCode from the remote app cache. */ defaultApp: string | undefined; /** Active app name: explicit `--app` flag > `defaultApp` > undefined. */ currentApp: string | undefined; /** The raw merged JSON (used by doctor and config commands). */ raw: Record; } /** Stub auth object used by the runtime CLI (AK auth, no session). */ export interface CLIAuth { cookie: string; isLoggedIn: boolean; getOrLogin(): Promise; } /** Domain URLs resolved from env and config. */ export interface CLIEnv { apiDomain: string; userDomain: string; } /** * The top-level context object assembled during CLI startup. * Includes config, auth, logger, environment, and interactivity state. * * Why `auth.isLoggedIn` is always `true` in runtime CLI: we use AK auth which * is validated at config load time. Unlike session-based auth, there is no * "logged out" state to model — if AK is absent, commands throw `authRequired`. */ export interface CLIContext { config: CLIConfig; auth: CLIAuth; logger: typeof logger; env: CLIEnv; nonInteractive: boolean; } /** File name used when creating a new config file. */ export declare const NEW_CONFIG_NAME: ".lovrabet.json"; /** Local app alias profile stored under `apps.` in `.lovrabet.json`. */ export interface LocalAppAliasProfile { /** Runtime app code resolved from this local alias. */ appcode?: unknown; } /** * Determines the write target for a scoped config operation. * * The target is always `.lovrabet.json`; foreign or legacy files are ignored. */ export declare function resolveWriteConfigPath(scope: "project" | "global"): string; /** * Reads and parses a single config file without any merging. * * Why no merging here: merging is done by `readRawConfig()` using `mergeRawConfigLayers()`. * Keeping reads simple means tests can mock `readConfigFile` without worrying about * the merge logic. * * Why it migrates `env: "online"` silently: older `.rabetbase.json` files used * `"online"` before the team standardized on `"production"`. Detecting and * migrating this at read time avoids patching every call site. */ export declare function readConfigFile(filePath: string): Record; /** * Atomically writes a config file to avoid corruption on crash. * * Why atomic: if the process crashes mid-write, a non-atomic write could leave * a truncated or partially written file. Using a temp file + rename ensures the * rename is atomic on POSIX systems (and on Windows, which doesn't guarantee * atomic rename, we fall back to direct write and handle the error gracefully). */ export declare function writeConfigFile(filePath: string, data: Record): void; /** * Detects whether the current session is non-interactive. * * Why multiple signals: no single check is sufficient on its own. * - `CI` plus product-scoped `CI` env vars cover CI systems. * - `--non-interactive` / `--ci` flags are explicit user choices. * - `!process.stdout.isTTY` catches non-TTY environments (pipes, redirects). * - `isStdinRawModeSupported()` is a heuristic: if stdin cannot enter raw mode, * the session is unlikely to be interactive. * * This is a convenience wrapper; commands that need a definitive answer should * also respect the explicit `--non-interactive` flag. */ export declare function isNonInteractiveMode(flags: Record): boolean; /** * Assembles the full `CLIContext` from raw CLI flags. * * This is the primary entry point called from `cli.tsx`. It: * 1. Reads and merges config files. * 2. Builds the resolved `CLIConfig`. * 3. Initializes the global environment (env, domains). * 4. Sets up auth. * 5. Configures the logger with the current directory. * * Why async: `initGlobalEnvironment` currently does only synchronous work, * but making it async leaves room for future async initialization (e.g. keychain * lookups) without changing the call site. */ export declare function createContext(flags: Record): Promise; /** Convenience wrapper: builds CLIConfig from flags without creating the full context. */ export declare function buildConfigFromFlags(flags: Record): CLIConfig; /** * Returns local app alias profiles from a raw config object. * * @param raw - Raw merged config object. * @returns Local alias profile map, or an empty object when absent. */ export declare function getLocalAppAliases(raw: Record): Record; /** * Resolves an app code from a local app alias. * * @param raw - Raw merged config object. * @param name - Alias name selected by `defaultApp` or `--app`. * @returns App code from `apps..appcode`, or undefined when not configured. */ export declare function resolveLocalAppAliasCode(raw: Record, name: string | undefined): string | undefined; /** * Reads the appCode from the config file (fallback path, used when no flag is provided). * * Resolution order: * 1. Top-level `appcode` or `app` field * 2. `defaultApp` local alias profile's `appcode` * 3. Cached remote app list (resolved by app name) * 4. First local alias profile's `appcode` * * Why this fallback exists: commands that don't receive `--appcode` can still * resolve it from the config. This enables the "just run `lovrabet dataset list`" * UX where the active app is implied by the config. */ export declare function readAppCodeFromConfig(): string | undefined; /** * Reads the config file for a specific scope (global or project). * Used by `app list` and `config get/set` commands. * * Why `flags.global` controls scope: this is consistent with how `--global` * is used throughout the CLI. Commands that accept `--global` can use this * function directly. */ export declare function readScopedConfig(flags: Record): Record; /** Writes the config to the appropriate scope file. */ export declare function writeScopedConfig(config: Record, flags: Record): void; /** * Initializes the global runtime environment from the resolved config. * * Why this exists: `initEnv()` sets the module-level env variable in `constant/env.ts`, * and `initDomains()` sets the domain overrides in `constant/domain.ts`. These are * separate modules that both need to be initialized from the same config. Previously, * `cli.tsx` called both functions separately, creating a risk that one would be * forgotten when the config resolution changed. Centralizing the call here ensures * both are always updated together. */ export declare function initGlobalEnvironment(config: CLIConfig): Promise; /** Returns the project config path if inside a project, otherwise undefined. */ export declare function getProjectConfigPath(): string | undefined; /** Returns the global config path in the home directory, or undefined if none exists. */ export declare function getGlobalConfigPath(): string | undefined; /** * Result of inspecting a single config file path. * * Why four states: "missing", "empty", "ok", and "error" are all meaningful * to the `doctor` command, which shows users exactly why their config isn't working. */ export type CliConfigJsonInspect = { status: "missing"; } | { status: "empty"; } | { status: "ok"; data: Record; } | { status: "error"; message: string; }; /** * Inspects a single config file for `doctor` output. * Silently catches errors (doesn't print to stderr) so the doctor can * report them nicely. */ export declare function inspectCliConfigJsonFile(filePath: string): CliConfigJsonInspect; /** * The two-layer config structure: global and project. */ export interface RawConfigLayers { globalPath: string | undefined; projectPath: string | undefined; globalConfig: Record; projectConfig: Record; } /** * Reads both config files as-is (no merging). * * Why not return the merged config: `app list` needs to know per-layer membership * (which apps are in global vs. project). If we only returned the merged result, * we'd lose that information. The caller can call `mergeRawConfigLayers()` if needed. */ export declare function loadRawConfigLayers(projectRoot?: string): RawConfigLayers; /** @returns true if the current directory is inside a project with a config file. */ export declare function isInProject(): boolean; /** * Reads and merges both config layers. * This is the canonical "read config" function used by `buildConfig()`. */ export declare function readRawConfig(projectRoot?: string): Record; /** * Returns the merged config AND the raw layers. * Used by `app list`, which needs both the merged view and * the per-layer information for display. */ export declare function readRawConfigWithLayers(projectRoot?: string): { config: Record; layers: RawConfigLayers; }; /** * Reports where the effective `defaultApp` comes from. * * Why needed: `app list` needs to annotate the fallback app candidate with * "(project)" or "(global)" so users understand why it is available. */ export declare function resolveDefaultAppSource(layers: RawConfigLayers): "project" | "global" | null;