@mycadet/app 0.13.0-dev.202611142107.f5e00a2 → 0.13.0-dev.202611142321.f0c8539
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.
- package/dist/package/package.json +1 -1
- package/dist/package/resolution-contract-lMCy1TXs.js.map +1 -1
- package/dist/standalone/assets/{assistant-selection-BHSWVVGf.js → assistant-selection-orv5LlBm.js} +2 -2
- package/dist/standalone/assets/{assistant-selection-BHSWVVGf.js.map → assistant-selection-orv5LlBm.js.map} +1 -1
- package/dist/standalone/assets/{hatching-screen-CszlzCVf.js → hatching-screen--FhKffez.js} +2 -2
- package/dist/standalone/assets/{hatching-screen-CszlzCVf.js.map → hatching-screen--FhKffez.js.map} +1 -1
- package/dist/standalone/assets/{index-Bo3PoCr5.js → index-DsfaZMDu.js} +4 -4
- package/dist/standalone/assets/{index-Bo3PoCr5.js.map → index-DsfaZMDu.js.map} +1 -1
- package/dist/standalone/assets/{pairing-address-Cwo-xElq.js → pairing-address-CmS8dvFv.js} +2 -2
- package/dist/standalone/assets/{pairing-address-Cwo-xElq.js.map → pairing-address-CmS8dvFv.js.map} +1 -1
- package/dist/standalone/assets/{pairing-page-BjCgj7gG.js → pairing-page-BRVotoUV.js} +2 -2
- package/dist/standalone/assets/{pairing-page-BjCgj7gG.js.map → pairing-page-BRVotoUV.js.map} +1 -1
- package/dist/standalone/assets/remote-web-pairing-C9I6GwY3.js +2 -0
- package/dist/standalone/assets/remote-web-pairing-C9I6GwY3.js.map +1 -0
- package/dist/standalone/assets/resolve-assistant-9Jmr90l9.js.map +1 -1
- package/dist/standalone/assets/{select-assistant-screen-BMLirndH.js → select-assistant-screen-DsewY4va.js} +2 -2
- package/dist/standalone/assets/{select-assistant-screen-BMLirndH.js.map → select-assistant-screen-DsewY4va.js.map} +1 -1
- package/dist/standalone/index.html +4 -4
- package/package.json +1 -1
- package/dist/standalone/assets/remote-web-pairing-BkaYzoWN.js +0 -2
- package/dist/standalone/assets/remote-web-pairing-BkaYzoWN.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolution-contract-lMCy1TXs.js","names":[],"sources":["../../../../packages/shared/github-repository.ts","../../../../packages/shared/seeds.ts","../../../../packages/service-contracts/src/ingress.ts","../../__vite-browser-external","../../../../packages/local-mode/src/paths.ts","../../../../packages/local-mode/src/lockfile-contract.ts","../../src/assistant/resolution-contract.ts"],"sourcesContent":["/** The GitHub repository Cadet's source and releases live in. */\nexport const CADET_GITHUB_REPOSITORY = \"vargasjr-dev/mycadet-temp\";\n\n/** URL of the public GitHub repository. */\nexport const CADET_GITHUB_REPOSITORY_URL = `https://github.com/${CADET_GITHUB_REPOSITORY}`;\n","// Loaded standalone by Capacitor's single-file TS config loader (via the\n// `./seeds` subpath) — keep imports type-only (this file has none) and keep\n// this file free of the import-meta meta-property: the loader transpiles this\n// file to CJS, where that syntax is illegal and makes Node misroute the\n// result as ESM. Browser bundles (chrome extension, web renderer) import it\n// for the same reason.\n\n/**\n * Per-service default port set. Phase 5 (per-environment port offsets) is\n * deferred from MVP, so today every environment uses the same port set. The\n * shape exists so the rest of the stack can call `getDefaultPorts(env)` and\n * gain per-env offsets later without changing any call sites.\n */\nexport interface PortMap {\n daemon: number;\n gateway: number;\n qdrant: number;\n ces: number;\n outboundProxy: number;\n tcp: number;\n}\n\n/**\n * The port the local app server binds by default — the single origin every\n * local surface (webview, assistant daemon, CLI) dials. The local seed's\n * base URL reuses it so no surface carries its own port constant.\n */\nexport const DEFAULT_APP_SERVER_PORT = 7840;\n\n/**\n * A resolved environment definition. Required fields are `name` and\n * `baseUrl`. All other fields are optional and declared upfront — new\n * fields are additive, never breaking. `name` is a `CadetEnvironment`: a\n * definition outside the known names has nowhere to key into.\n */\nexport interface EnvironmentDefinition {\n name: CadetEnvironment;\n\n /**\n * The single origin for API, auth, web, and the assistant mount — one\n * deployment serves all of it, so one URL describes it. The local seed\n * points at the app server's default port; the cloud seeds point at the\n * environment's host.\n */\n baseUrl: string;\n\n /** Human-readable label for UI surfaces. */\n displayName?: string;\n\n /** Hint for UI surfaces that want to tint or badge their display. */\n tintColor?: string;\n\n /** Per-service port overrides merged on top of defaults. */\n portsOverride?: Partial<PortMap>;\n\n /** Override for the XDG config directory. */\n configDirOverride?: string;\n\n /**\n * Override for the directory containing the lockfile. In-memory only —\n * tests inject curated env lists with it; path helpers never read env\n * vars to populate it.\n */\n lockfileDirOverride?: string;\n}\n\n/**\n * Non-prod port blocks. Each environment gets a 1000-port window in the\n * 17000–21000 band. Within a block, services are spaced 100 apart so up to\n * 100 assistants can coexist without the scan (`findAvailablePort`) running\n * one service's range into the next. Band chosen to sit below Linux's\n * default ephemeral start (32768) and macOS's (49152), and away from the\n * 3000/5000/8000/9000 dev-tool swamp. Production keeps its legacy,\n * non-contiguous port set (7821/7830/6333/8090/8080/8765): cross-env\n * collision is the only problem this change targets, prod is unaffected\n * because only one env's assistants compete on a given machine, and\n * churning it would leave existing hatches on 7821 while new ones\n * allocated elsewhere.\n */\nfunction portBlock(base: number): PortMap {\n return {\n daemon: base,\n gateway: base + 100,\n qdrant: base + 200,\n ces: base + 300,\n outboundProxy: base + 400,\n tcp: base + 500,\n };\n}\n\n/**\n * The set of environment names every surface shares — the seeds below are\n * exactly these five. Lockfiles, config directories, release channels, and\n * the mobile shells' scheme keys all key off this union, so a name outside\n * it is a bug the compiler can catch.\n */\nexport const CADET_ENVIRONMENT_NAMES = [\n \"local\",\n \"dev\",\n \"staging\",\n \"production\",\n \"test\",\n] as const;\n\nexport type CadetEnvironment = (typeof CADET_ENVIRONMENT_NAMES)[number];\n\nfunction isCadetEnvironment(\n name: string | undefined,\n): name is CadetEnvironment {\n return (\n typeof name === \"string\" &&\n (CADET_ENVIRONMENT_NAMES as readonly string[]).includes(name)\n );\n}\n\n/**\n * globalThis.process reads the same env under bun/node while staying\n * crash-free in browser bundles (no bare `process` reference) — and free\n * of import-meta syntax, which the Capacitor config loader can't transpile.\n */\nfunction processEnv(): Record<string, string | undefined> | undefined {\n return (\n globalThis as unknown as {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process?.env;\n}\n\n/**\n * Whether a version string denotes a dev build from the dev-release\n * pipeline, e.g. `0.12.6-dev.202610081023.3a27b77`.\n */\nexport function isDevVersion(version: string): boolean {\n return /-dev\\./i.test(version);\n}\n\n/**\n * Whether a version string denotes a staging build, e.g. `0.6.0-staging.3`.\n */\nexport function isStagingVersion(version: string): boolean {\n return /-staging\\./i.test(version);\n}\n\n/**\n * Whether a version string denotes a local build, e.g. `0.12.6-local.42`.\n * Local builds are never published.\n */\nexport function isLocalVersion(version: string): boolean {\n return /-local\\./i.test(version);\n}\n\n/**\n * The environment a released channel version pins, or `undefined` when the\n * version carries no channel suffix. A plain version — a workspace\n * checkout's unstamped package version — reads as \"no version signal\",\n * leaving the remaining steps to decide. Mirrors the CLI resolver's mapping\n * (local, dev, staging).\n */\nexport function environmentFromVersion(\n version: string | undefined,\n): CadetEnvironment | undefined {\n if (isLocalVersion(version ?? \"\")) return \"local\";\n if (isDevVersion(version ?? \"\")) return \"dev\";\n if (isStagingVersion(version ?? \"\")) return \"staging\";\n return undefined;\n}\n\n/**\n * Read the persisted default environment — the file pre-version-tied CLIs\n * wrote so their successors honor the machine's chosen default. Node-only\n * and resolved at call time (`process.getBuiltinModule`), so this file\n * keeps zero static node imports: the Capacitor config loader transpiles it\n * standalone and a static node import would die there, while browser\n * bundles (chrome extension, web renderer) never reach the call.\n */\nfunction readPersistedDefault(): CadetEnvironment | undefined {\n const node = process as unknown as {\n getBuiltinModule?: (id: string) => unknown;\n platform: string;\n };\n const fs = node.getBuiltinModule?.(\"node:fs\") as\n | typeof import(\"node:fs\")\n | undefined;\n const os = node.getBuiltinModule?.(\"node:os\") as\n | typeof import(\"node:os\")\n | undefined;\n const path = node.getBuiltinModule?.(\"node:path\") as\n | typeof import(\"node:path\")\n | undefined;\n if (!fs || !os || !path) return undefined;\n\n // Mirrors local-mode's resolveConfigHome (the same XDG/AppData derivation).\n const isWindows = node.platform === \"win32\";\n const join = isWindows ? path.win32.join : path.posix.join;\n const configHome = isWindows\n ? process.env.APPDATA?.trim() || join(os.homedir(), \"AppData\", \"Roaming\")\n : process.env.XDG_CONFIG_HOME?.trim() || join(os.homedir(), \".config\");\n const filePath = join(configHome, \"cadet\", \"environment\");\n try {\n if (!fs.existsSync(filePath)) return undefined;\n const content = fs.readFileSync(filePath, \"utf-8\").trim();\n return isCadetEnvironment(content) ? content : undefined;\n } catch {\n // Unreadable file — no default.\n return undefined;\n }\n}\n\n/**\n * The environment name every surface shares — the single resolution chain\n * local-mode hosts and the mobile config loader both ride:\n *\n * 1. `CADET_ENVIRONMENT` (explicit override, also inherited by the\n * hatch/retire child processes the CLI spawns), then\n * `VITE_ENVIRONMENT` (the build-time inlined var bundled surfaces bake)\n * 2. the release channel the running copy's version carries —\n * `__CADET_OWN_VERSION__`, which the entrypoints that know their\n * package root inject (the CLI's bin computes its git/local stamp,\n * the Electron builds define it at build time). A released `-dev.`\n * install resolves `dev` even on a machine that never wrote a\n * persisted default\n * 3. the persisted default written by pre-version-tied CLIs (old files\n * are still honored)\n * 4. `production`\n *\n * The version step is what keeps every released install from silently\n * reading production's state: the channel is decided by the release\n * pipeline, not by whatever default an older CLI happened to leave behind.\n * Names outside `CadetEnvironment` are ignored rather than returned: an\n * unrecognized value falls through to the next arm instead of leaking an\n * unkeyable string into `SEEDS` lookups.\n */\nexport function resolveEnvironmentName(): CadetEnvironment {\n const env = processEnv();\n const named = [\n env?.CADET_ENVIRONMENT?.trim(),\n env?.VITE_ENVIRONMENT?.trim(),\n ].find(isCadetEnvironment);\n if (named) return named;\n return (\n environmentFromVersion(\n (globalThis as { __CADET_OWN_VERSION__?: string }).__CADET_OWN_VERSION__,\n ) ??\n readPersistedDefault() ??\n \"production\"\n );\n}\n\n/**\n * The base URL of Cadet Cloud — the hosted deployment the local surfaces\n * pair with (API, auth, web, and the assistant mount all live on it). The\n * explicit `CADET_CLOUD_URL` override wins, else the seed of\n * `resolveEnvironmentName`. This says \"cloud\" because the URL always points\n * at a Cadet Cloud deployment, never at something that represents this\n * surface itself.\n */\nexport function getCloudBaseUrl(): string {\n const explicit = processEnv()?.CADET_CLOUD_URL?.trim();\n if (explicit) return explicit;\n return SEEDS[resolveEnvironmentName()].baseUrl;\n}\n\n/**\n * Built-in environment definitions and the source of truth for the\n * set of known environment names.\n *\n * Custom environments via a user config file are a future phase — see the\n * \"Coexisting environments\" design doc. Until then, a call site that needs a\n * new environment must add it here and rebuild.\n */\nexport const SEEDS: Record<CadetEnvironment, EnvironmentDefinition> = {\n production: {\n name: \"production\",\n baseUrl: \"https://mycadet.ai\",\n },\n staging: {\n name: \"staging\",\n baseUrl: \"https://staging.mycadet.ai\",\n portsOverride: portBlock(17000),\n },\n test: {\n name: \"test\",\n baseUrl: \"https://test.mycadet.ai\",\n portsOverride: portBlock(19000),\n },\n dev: {\n name: \"dev\",\n baseUrl: \"https://dev.mycadet.ai\",\n portsOverride: portBlock(18000),\n },\n local: {\n name: \"local\",\n baseUrl: `http://localhost:${DEFAULT_APP_SERVER_PORT}`,\n // assistantBaseUrl: \"http://host.docker.internal:7840\",\n // ^ uncomment once dockerized hatch path is live — the assistant runs\n // in a different network namespace than the host.\n portsOverride: portBlock(20000),\n },\n};\n","/**\n * Map a Cadet Cloud host onto its environment's tunnel host, following the\n * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,\n * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for\n * hosts outside that convention (localhost, custom domains). Shared by the\n * gateway speech relay (which passes the request's own host) and the web\n * live-voice client (which defaults to the page's origin) so the two ends\n * of the cloud-speech transport can't drift.\n */\nexport function tunnelHostForCloudHost(\n host: string = (globalThis as { location?: { hostname?: string } }).location\n ?.hostname ?? \"\",\n): string | null {\n if (host === \"cloud.mycadet.ai\") {\n return \"tunnel.mycadet.ai\";\n }\n const match = /^([a-z0-9-]+)-cloud\\.mycadet\\.ai$/.exec(host);\n return match ? `${match[1]}-tunnel.mycadet.ai` : null;\n}\n\nexport function normalizePublicBaseUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const normalized = value.trim().replace(/\\/+$/, \"\");\n return normalized.length > 0 ? normalized : undefined;\n}\n\n/**\n * Application close code the gateway's tunnel bridge sends to proxied\n * WebSockets when the tunnel itself is lost (tunnel disconnect, gateway\n * shutdown). A dedicated code because the natural 1001 (going away) cannot\n * be sent through the JS `close()` API (the bridge would remap it to a\n * misleading 4001), and Bun's WebSocket client drops close reasons, so the\n * code is the only signal that survives the relay to the daemon.\n */\nexport const GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE = 4801;\n\nexport function normalizeHttpPublicBaseUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (trimmed.length === 0) return undefined;\n if (/[?#]/.test(trimmed)) return undefined;\n\n try {\n const url = new URL(trimmed);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return undefined;\n }\n if (!url.hostname) return undefined;\n url.pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n return url.toString();\n } catch {\n return undefined;\n }\n}\n\n/**\n * The same validation as `normalizeHttpPublicBaseUrl`, minus the root path it\n * always emits (`https://x` becomes `https://x/`). Callers that append a path\n * or persist the result want this shape, so they neither join through a double\n * slash nor re-strip what the normalizer just added.\n */\nexport function normalizeHttpPublicBaseUrlWithoutTrailingSlash(\n value: unknown,\n): string | undefined {\n return normalizePublicBaseUrl(normalizeHttpPublicBaseUrl(value));\n}\n\n/**\n * A trimmed string, or undefined when the value is not a non-blank string.\n *\n * Exported because the identity check in the daemon's tunnel probe compares an\n * id recorded here against one an edge serves, and the two sides have to trim\n * alike: a padded served id must read as a match, not as a different\n * assistant.\n */\nexport function trimmedNonEmptyString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n// Records `cadet tunnel` leaves in the workspace config's `ingress` section.\n// The CLI writes them and the daemon reads them, and the two are separate\n// build units that cannot import each other, so the key names, the provider\n// allowlist, and the validation all live here rather than in a copy on each\n// side.\n\n/** Key under `ingress` holding the tunnel that most recently ran. */\nexport const INGRESS_LAST_TUNNEL_KEY = \"lastTunnel\";\n\n/**\n * Key under `ingress` holding a tunnel published for device pairing alone.\n *\n * A tailnet-only tunnel started while webhook integrations are configured\n * cannot carry their callbacks, so it leaves `publicBaseUrl` (the callback\n * base) as it is and records itself here. Readers that answer the pairing card\n * prefer this address over `publicBaseUrl`; teardown drops the key.\n */\nexport const INGRESS_PAIRING_TUNNEL_KEY = \"pairingTunnel\";\n\n/** Key under `ingress` holding the assistant id that tunnel fronted. */\nexport const INGRESS_ASSISTANT_ID_KEY = \"assistantId\";\n\n/**\n * The tunnel providers the CLI offers, in the order they are presented —\n * the managed `cadet` tunnel first (listed, not yet implemented), then the\n * local ones. `cadet tunnel`'s accepted `--provider` values and the\n * persisted-record validation below both derive from it.\n */\nexport const TUNNEL_PROVIDERS = [\n \"cadet\",\n \"ngrok\",\n \"cloudflare\",\n \"tailscale\",\n] as const;\n\nexport type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];\n\n/** A recorded tunnel, kept after teardown so UIs can name the command to restart. */\nexport interface TunnelRecord {\n provider: TunnelProviderName;\n publicBaseUrl: string;\n}\n\nfunction isTunnelProviderName(value: unknown): value is TunnelProviderName {\n return (\n typeof value === \"string\" &&\n (TUNNEL_PROVIDERS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Parse an `ingress.lastTunnel` or `ingress.pairingTunnel` value, or null when\n * it is absent or malformed.\n *\n * A hand-edited config must not hand callers an unusable address or a provider\n * name no command accepts: the provider is checked against the allowlist\n * because readers render it into a restart command, and the URL faces the same\n * absolute HTTP(S) constraint as every other public base URL. The URL comes\n * back in the shape the validator produced, so readers never re-normalize it.\n */\nexport function parseTunnelRecord(value: unknown): TunnelRecord | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return null;\n }\n const { provider, publicBaseUrl } = value as Record<string, unknown>;\n if (!isTunnelProviderName(provider)) {\n return null;\n }\n const normalized =\n normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);\n if (normalized === undefined) {\n return null;\n }\n return { provider, publicBaseUrl: normalized };\n}\n\n/**\n * The tunnel a reader should offer to restart, given an `ingress` section, or\n * null when it records none.\n *\n * A pairing-only record wins: it is written by a tunnel that deliberately left\n * `publicBaseUrl` (and the `lastTunnel` beside it) pointing at an older public\n * provider, so naming that older one would send the user to rebuild an ingress\n * they are not using instead of the tunnel actually in front of them.\n */\nexport function selectRestartTunnelRecord(\n ingress: unknown,\n): TunnelRecord | null {\n if (typeof ingress !== \"object\" || ingress === null) {\n return null;\n }\n const section = ingress as Record<string, unknown>;\n return (\n parseTunnelRecord(section[INGRESS_PAIRING_TUNNEL_KEY]) ??\n parseTunnelRecord(section[INGRESS_LAST_TUNNEL_KEY])\n );\n}\n\n/** Parse an `ingress.assistantId` value, or null when it is absent or blank. */\nexport function parseRecordedAssistantId(value: unknown): string | null {\n return trimmedNonEmptyString(value) ?? null;\n}\n","module.exports = {}","import { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nconst PRODUCTION_ENVIRONMENT_NAME = \"production\";\n\nconst isWindows = () => process.platform === \"win32\";\nconst paths = () => (isWindows() ? path.win32 : path.posix);\n\nexport function resolveConfigHome(): string {\n if (isWindows()) {\n return (\n process.env.APPDATA?.trim() ||\n paths().join(homedir(), \"AppData\", \"Roaming\")\n );\n }\n return process.env.XDG_CONFIG_HOME?.trim() || paths().join(homedir(), \".config\");\n}\n\nexport function resolveDataHome(): string {\n if (isWindows()) {\n return (\n process.env.LOCALAPPDATA?.trim() ||\n paths().join(homedir(), \"AppData\", \"Local\")\n );\n }\n return process.env.XDG_DATA_HOME?.trim() || paths().join(homedir(), \".local\", \"share\");\n}\n\nexport const joinLocalPath = (...segments: string[]) => paths().join(...segments);\n\nexport function assertSafePathSegment(value: string, label: string): void {\n const unsafe =\n !value ||\n /[/\\\\\\0]/.test(value) ||\n /^\\.{1,2}$/.test(value) ||\n (isWindows() &&\n (/[<>:\"|?*\\u0000-\\u001f]/.test(value) ||\n /[. ]$/.test(value) ||\n /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$)/i.test(value)));\n if (unsafe) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n}\n\n/**\n * The directory name an environment's state lives under: `cadet` for\n * production (the legacy location other environments must not collide with),\n * `cadet-<env>` otherwise. Exported so hosts that enumerate a SPECIFIC\n * environment's directories — the CLI's cross-environment detection — can\n * compose them from {@link resolveConfigHomes} and {@link resolveDataHome}\n * without an options bag threading through the ambient resolvers.\n */\nexport function environmentDirectoryName(cadetEnv: string): string {\n assertSafePathSegment(cadetEnv, \"environment name\");\n return cadetEnv === PRODUCTION_ENVIRONMENT_NAME\n ? \"cadet\"\n : `cadet-${cadetEnv}`;\n}\n","/**\n * The lockfile wire contract — one Zod schema that is the single source of truth\n * for an assistant entry's shape, its validation, the `cloud` taxonomy, the\n * normalization, and the redaction policy.\n *\n * Kept in its own module, exported from the package as `./contract`, so\n * consumers import the contract without pulling in the host I/O graph\n * (`node:fs`, the CLI resolver). The CLI, the host I/O layer, the Electron and\n * dev-server bridges, and the renderer all depend on this one definition. It\n * references no node built-ins — the renderer bundles it directly — and `zod`\n * is its only third-party import.\n *\n * The lockfile is written by the Cadet CLI and read across the macOS↔CLI\n * boundary, which do not release in lockstep, so the parser is permissive at\n * the file level: it validates each entry whole through the schema (a\n * malformed field drops that entry, never the file), accepts — but drops from\n * the validated value — entry fields this build has never heard of, and\n * defaults `cloud` when it is absent or unknown. The schema is strict over\n * the fields it models: every field a reader needs is IN the schema, so a\n * CLI read-modify-write round-trip preserves it by construction. The\n * local-mode write path still merges raw JSON (unknown root fields survive\n * on disk; see `lockfile.ts`); the validated value carries only the modeled\n * shape.\n */\nimport { getCloudBaseUrl } from \"@mycadet/shared\";\nimport { TUNNEL_PROVIDERS } from \"@mycadet/service-contracts/ingress\";\nimport { z } from \"zod\";\n\nimport { readLockfile } from \"./lockfile\";\n\n/**\n * Known deployment topologies of a Cadet:\n * - `local` — on-machine bare-metal processes\n * - `docker` — docker-managed container\n * - `apple-container` — macOS-app-managed container\n * - `cloud` — managed by Cadet Cloud\n * - `custom` — remote VM accessible via SSH\n */\nexport const KNOWN_REMOTES = [\n \"local\",\n \"docker\",\n \"apple-container\",\n \"cloud\",\n \"custom\",\n \"baseten\",\n] as const;\n\n/**\n * The topology values as one zod enum, shared by the lockfile schema (which\n * wraps it in a `catch`) and the clients' injected-config schema, so the two\n * can never drift.\n */\nexport const RemoteSchema = z.enum(KNOWN_REMOTES);\nexport type Remote = z.infer<typeof RemoteSchema>;\n\n/**\n * Keys that must never cross the host→renderer boundary. Stripped from raw\n * lockfile JSON before it leaves a host (see `stripSensitiveFields`) and absent\n * from the renderer-facing schema below; the CLI's extended type is the only\n * place they are modeled.\n */\nexport const SENSITIVE_KEYS = [\n \"bearerToken\",\n \"guardianBootstrapSecret\",\n] as const;\n\n/**\n * Strip every `SENSITIVE_KEYS` field from a parsed lockfile, in place: from\n * each assistant entry and from that entry's `resources`. The one redaction\n * every host runs before lockfile data crosses to the renderer — pure, no\n * node built-ins, so it lives beside the keys it enforces and bundles into\n * the renderer through the `./contract` subpath.\n */\nexport function stripSensitiveFields(data: Record<string, unknown>): void {\n const assistants = data.assistants;\n if (!Array.isArray(assistants)) return;\n for (const assistant of assistants) {\n if (assistant && typeof assistant === \"object\") {\n const entry = assistant as Record<string, unknown>;\n for (const field of SENSITIVE_KEYS) {\n delete entry[field];\n }\n const resources = entry.resources;\n if (resources && typeof resources === \"object\") {\n for (const field of SENSITIVE_KEYS) {\n delete (resources as Record<string, unknown>)[field];\n }\n }\n }\n }\n}\n\n/**\n * Whether an entry's `runtimeUrl` is one a serving host can forward to: an\n * absolute http(s) URL. Shared by the renderer's paired-gateway URL derivation\n * and the host-side paired allowlist reader so the two can't drift.\n */\nexport function isUsableBaseUrl(url: unknown): url is string {\n if (typeof url !== \"string\" || url === \"\") {\n return false;\n }\n try {\n const parsed = new URL(url);\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\n/**\n * The local build's control-plane base, read from the lockfile alone: the\n * auto-tunnel's recorded `publicUrl` when one is up, else the app server\n * edge's loopback port. A loopback base is reachable from the iOS simulator\n * only — a device's `127.0.0.1` is the phone itself — so a device build\n * wants a tunnel up before it stamps. `undefined` when neither record\n * carries a usable address.\n *\n * The assistants play no role in this resolution — the capacitor app shell\n * only deals with the app server — and the CLI's workspace config is never\n * touched: the lockfile is the single home for edge state.\n */\nexport function resolveLocalServerUrl(): string | undefined {\n const lockfile = readLockfile();\n const tunnelUrl = lockfile.tunnel?.publicUrl;\n if (isUsableBaseUrl(tunnelUrl)) {\n return tunnelUrl;\n }\n const port = lockfile.app?.port;\n if (typeof port === \"number\") {\n return `http://127.0.0.1:${port}`;\n }\n return undefined;\n}\n\n/**\n * Clouds whose gateway listens on a loopback port of this machine: the\n * on-machine daemon (\"local\") and local Docker instances (\"docker\").\n */\nexport const LOOPBACK_GATEWAY_CLOUDS: readonly Remote[] = [\"local\", \"docker\"];\n\nexport function isLoopbackGatewayCloud(cloud: Remote): boolean {\n return LOOPBACK_GATEWAY_CLOUDS.includes(cloud);\n}\n\n/**\n * The gateway port a freshly hatched assistant's gateway listens on, with\n * the `GATEWAY_PORT` env override the CLI tooling honors.\n */\nexport const GATEWAY_PORT = process.env.GATEWAY_PORT\n ? Number(process.env.GATEWAY_PORT)\n : 7830;\n\n/**\n * The URL to dial an entry's gateway on, derived from the entry's canonical\n * fields — the one seam every consumer addresses a gateway through:\n *\n * - `custom` remotes: their recorded host and port.\n * - cloud-managed: the environment's base url (`getCloudBaseUrl()`).\n * - loopback topologies (`local`, `docker`, `apple-container`): the\n * recorded loopback gateway port, else the default gateway port.\n */\nexport function getGatewayUrl(entry: LockfileAssistant): string {\n // Baseten: the private preview is the door when one was minted — the\n // preview edge opens the port dynamically and guards it with a preview\n // token, so `Authorization` stays free for the gateway's own bearer.\n // Without one, the sandbox API's `/port/<PORT>/` route (the sandbox\n // access token authenticates it).\n if (entry.cloud === \"baseten\") {\n if (entry.previewUrl) {\n return entry.previewUrl.replace(/\\/+$/, \"\");\n }\n return `${entry.sandboxUrl.replace(/\\/+$/, \"\")}/port/${entry.port}`;\n }\n if (entry.cloud === \"custom\") {\n // Custom remotes speak TLS. Default ports elided the way a URL prints\n // them. The schema requires the address and port.\n const elided = entry.port === 443;\n return `https://${entry.ipAddress}${elided ? \"\" : `:${entry.port}`}`;\n }\n if (isCloudEntry(entry)) {\n return getCloudBaseUrl().replace(/\\/+$/, \"\");\n }\n return `http://127.0.0.1:${entry.port}`;\n}\n\n/** Docker image metadata for the service group. Enables rollback to known-good refs. */\nexport const ContainerInfoSchema = z.object({\n assistantImage: z.string(),\n gatewayImage: z.string(),\n cesImage: z.string(),\n});\nexport type ContainerInfo = z.infer<typeof ContainerInfoSchema>;\n\n/**\n * Host-independent resource policy for one assistant's machine allocation —\n * what the allocation *should be*, as opposed to the record fields (ports,\n * instanceDir, containerInfo) that describe what *is*. The section rides\n * every topology's entry: the stored shape is the same everywhere, while how\n * a policy materializes differs per remote (see {@link MACHINE_APPLICABILITY}).\n *\n * The schema is strict — unknown keys are rejected — because the write path\n * (future `cadet machine set`, daemon PATCH, cloud API) is the loud validator:\n * a typo'd knob must fail at the boundary, never silently degrade to the\n * runtime default. Parse time is deliberately softer: the field is embedded\n * with `.catch(undefined)`, so a malformed or unrecognized section from a\n * hand-edit or a different build quarantines to the defaults while the\n * entry itself survives, and the drift is visible to readers instead of\n * costing the assistant its identity fields.\n */\nexport const MachineMemoryPolicySchema = z\n .strictObject({\n /**\n * Soft limit in bytes: where the runtime should start reclaiming early\n * and smoothly (cgroup v2 `memory.high` on docker remotes). Absent =\n * unconstrained.\n */\n high: z.number().int().positive().optional(),\n /** Hard limit in bytes: the backstop the runtime may not exceed\n * (cgroup v2 `memory.max` on docker remotes). Absent = unconstrained. */\n max: z.number().int().positive().optional(),\n })\n .superRefine((memory, context) => {\n if (\n memory.high !== undefined &&\n memory.max !== undefined &&\n memory.high > memory.max\n ) {\n context.addIssue({\n code: \"custom\",\n path: [\"high\"],\n message: `memory.high (${memory.high}) must not exceed memory.max (${memory.max})`,\n });\n }\n });\nexport type MachineMemoryPolicy = z.infer<typeof MachineMemoryPolicySchema>;\n\n/** The `machine` section: one key per resource knob, added as they migrate here. */\nexport const MachinePolicySchema = z.strictObject({\n memory: MachineMemoryPolicySchema.optional(),\n});\nexport type MachinePolicy = z.infer<typeof MachinePolicySchema>;\n\n/**\n * How a stored `machine` policy materializes per remote topology. The store\n * is host-independent — this table is the one place that says what a policy\n * *means* on each remote, so the apply layer and readers derive semantics\n * from it instead of re-deciding per call site. Remotes gain real\n * applicability as their apply paths land (cloud/gVisor, apple-container).\n */\nexport type MachineApplicability = \"cgroup\" | \"process-hint\" | \"unsupported\";\n\nexport const MACHINE_APPLICABILITY = {\n local: \"process-hint\",\n docker: \"cgroup\",\n \"apple-container\": \"unsupported\",\n cloud: \"unsupported\",\n custom: \"unsupported\",\n baseten: \"unsupported\",\n} as const satisfies Record<Remote, MachineApplicability>;\n\n/**\n * The fields every topology actually uses: the identity every reader looks\n * entries up by, the display name, and the hatch/onboarding/conversation\n * bookkeeping that is topology-agnostic.\n */\nconst BaseAssistantSchema = z.object({\n assistantId: z.string(),\n /** Platform assistant UUID for this assistant's registration. */\n cloudAssistantId: z.string().optional(),\n /** Platform organization the entry belongs to. */\n cloudOrganizationId: z.string().optional(),\n name: z.string().optional(),\n hatchedAt: z.string().optional(),\n /**\n * When this assistant finished first-run onboarding. Absent for entries that\n * predate the record, which fall back to hatch age (see the web client's\n * `onboarded-assistant.ts`).\n */\n onboardedAt: z.string().optional(),\n /** PID of the file watcher process for instances hatched with --watch. */\n watcherPid: z.number().optional(),\n /**\n * Host-independent resource policy for this assistant's machine allocation\n * (see {@link MachinePolicySchema} for the strict-at-write, fallback-at-parse\n * contract, and {@link MACHINE_APPLICABILITY} for how it materializes per\n * remote). Absent = the runtime's own defaults apply.\n */\n machine: MachinePolicySchema.optional().catch(undefined),\n});\n\n/**\n * The fields the self-hosted, on-machine topologies (local, docker,\n * apple-container) share: host-recorded ports.\n */\nconst LocalRuntimeAssistantFields = {\n /** The gateway port a freshly hatched assistant's gateway listens on. */\n port: z.number(),\n /** The runtime daemon's port, recorded at hatch. */\n daemonPort: z.number().optional(),\n /** The content-editor server's port, recorded at hatch. */\n cesPort: z.number().optional(),\n};\n\n/** A plain on-machine assistant: host-managed ports and instance dir. */\nconst LocalAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n /** The instance's state root on this machine. Local instances keep their\n * data here. */\n instanceDir: z.string().optional(),\n cloud: z.literal(\"local\"),\n});\n\n/** A docker-hosted assistant: container metadata for rollback/upgrade. */\nconst DockerAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n cloud: z.literal(\"docker\"),\n /** Docker image metadata for rollback. Only present for docker topology entries. */\n containerInfo: ContainerInfoSchema.optional(),\n});\n\n/** An apple-container assistant, driven through its management socket. */\nconst AppleContainerAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n cloud: z.literal(\"apple-container\"),\n /** Management socket path for apple-container assistants; written by the macOS host. */\n mgmtSocket: z.string(),\n});\n\n/** A platform-hosted assistant, registered against the cloud API. */\nconst CloudAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"cloud\"),\n});\n\n/** A self-hosted remote assistant, reached at its own address. */\nconst CustomAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"custom\"),\n /** Host address of the custom remote's gateway. */\n ipAddress: z.string(),\n /** Gateway port on the custom remote. */\n port: z.number(),\n});\n\n/**\n * A Baseten sandbox entry: the sandbox server URL and the sandbox-internal\n * gateway port are pinned at creation. When the hatch also minted the\n * gateway's private preview, `previewUrl` is the gateway's canonical\n * address — the preview edge opens the port dynamically and guards it with\n * a preview token, leaving `Authorization` free for the gateway's own\n * bearer end to end. Entries without one (tokenless hatches, legacy) ride\n * the `{sandboxUrl}/port/{port}` route with the sandbox access token.\n */\nconst BasetenAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"baseten\"),\n /** The Baseten sandbox server URL; the port route rides on it. */\n sandboxUrl: z.string(),\n /** The sandbox-internal gateway port, piped in as `GATEWAY_PORT` at creation. */\n port: z.number(),\n /** The gateway's private preview URL, minted at hatch when a token resolves. */\n previewUrl: z.string().optional(),\n});\n\n/**\n * Route absent and unknown topology values to the local default before the\n * union discriminates, keeping the on-disk contract permissive about clouds\n * this build has never heard of.\n */\n/**\n * The one preprocess the entry union parses through. Routes absent and\n * unknown topology values to the local default before the union\n * discriminates, and backfills the legacy `resources.gatewayPort` onto\n * `port` — but only when the flat `port` field would fail the number\n * schema (absent or not a number): a valid port always wins. Zod has no\n * field-level sibling fallback, so the object-level preprocess is the\n * supported surface for this.\n */\nfunction normalizeEntry(entry: unknown): unknown {\n if (typeof entry !== \"object\" || entry === null) return entry;\n let rec = entry as Record<string, unknown>;\n const cloud = rec.cloud;\n if (!(typeof cloud === \"string\" && RemoteSchema.safeParse(cloud).success)) {\n rec = { ...rec, cloud: \"local\" };\n }\n if (rec.port === undefined) {\n // The flat port failed the required schema (absent on a legacy or bare\n // entry): fall back to the legacy resources.gatewayPort, else the\n // gateway default. A mistyped port never falls back — a bad modeled\n // field still drops the entry.\n const resources = rec.resources;\n const gatewayPort =\n resources && typeof resources === \"object\"\n ? (resources as Record<string, unknown>).gatewayPort\n : undefined;\n rec = {\n ...rec,\n port: typeof gatewayPort === \"number\" ? gatewayPort : GATEWAY_PORT,\n };\n }\n return rec;\n}\n\nexport const LockfileAssistantSchema = z.preprocess(\n normalizeEntry,\n z.discriminatedUnion(\"cloud\", [\n LocalAssistantSchema,\n DockerAssistantSchema,\n AppleContainerAssistantSchema,\n CloudAssistantSchema,\n CustomAssistantSchema,\n BasetenAssistantSchema,\n ]),\n);\n\n/**\n * A validated, normalized assistant entry — a discriminated union on\n * `cloud`, so each topology carries exactly the fields it uses: a baseten\n * entry's `sandboxUrl`/`port` are always defined, a docker entry's\n * `containerInfo` reads without touching unrelated topologies, and absent or\n * unknown on-disk clouds route to the local branch.\n */\nexport type LockfileAssistant = z.infer<typeof LockfileAssistantSchema>;\n\nexport type LocalAssistant = z.infer<typeof LocalAssistantSchema>;\nexport type DockerAssistant = z.infer<typeof DockerAssistantSchema>;\nexport type AppleContainerAssistant = z.infer<\n typeof AppleContainerAssistantSchema\n>;\nexport type CloudAssistant = z.infer<typeof CloudAssistantSchema>;\nexport type CustomAssistant = z.infer<typeof CustomAssistantSchema>;\nexport type BasetenAssistant = z.infer<typeof BasetenAssistantSchema>;\n\n/** Whether the entry is a platform-hosted assistant. */\nexport function isCloudEntry(\n entry: LockfileAssistant,\n): entry is CloudAssistant {\n return entry.cloud === \"cloud\";\n}\n\n/** Whether the entry is a docker-hosted assistant. */\nexport function isDockerEntry(\n entry: LockfileAssistant,\n): entry is DockerAssistant {\n return entry.cloud === \"docker\";\n}\n\n/** Whether the entry is an apple-container assistant. */\nexport function isAppleContainerEntry(\n entry: LockfileAssistant,\n): entry is AppleContainerAssistant {\n return entry.cloud === \"apple-container\";\n}\n\n/** Whether the entry is a plain on-machine assistant. */\nexport function isLocalEntry(\n entry: LockfileAssistant,\n): entry is LocalAssistant {\n return entry.cloud === \"local\";\n}\n\n/**\n * A CLI-managed OS process recorded at the lockfile root under `tunnel`:\n * the auto-tunnel agent fronting the edge. `cadet ps` renders it alongside\n * the per-assistant process trees, and the stop paths verify the pid before\n * signaling it.\n */\nexport const TunnelProcessRecordSchema = z.object({\n pid: z.number(),\n /** Which tunnel provider owns the process. */\n provider: z.enum(TUNNEL_PROVIDERS),\n startedAt: z.string(),\n /** The public URL the tunnel agent serves. */\n publicUrl: z.string().optional(),\n});\nexport type TunnelProcessRecord = z.infer<typeof TunnelProcessRecordSchema>;\n\n/**\n * A CLI-managed OS process recorded at the lockfile root under `app`: the\n * app server (web artifacts, `/v1` proxying, the outer shell). Shares\n * nothing with the tunnel record beyond the pid bookkeeping — a tunnel\n * serves a URL, the app server serves a port.\n */\nexport const AppProcessRecordSchema = z.object({\n /** The app server's bookkeeping, present while it runs. */\n pid: z.number().optional(),\n startedAt: z.string().optional(),\n /** Listening port for the app server. */\n port: z.number(),\n /**\n * The commit the edge's source-built client bundles were last built from\n * — the auto-build staleness stamp. Rides the running edge's record: a\n * build only stamps when an edge record exists to carry it.\n */\n builtSha: z.string().optional(),\n});\nexport type AppProcessRecord = z.infer<typeof AppProcessRecordSchema>;\n\n/**\n * Renderer-facing result of a lockfile write, returned across the host bridge.\n * Carries no HTTP-style status — hosts map that away because the renderer\n * surfaces failures by message alone.\n */\nexport type LockfileWriteResult =\n | { ok: true; lockfile: Lockfile }\n | { ok: false; error: string };\n\nexport const LockfileSchema = z.looseObject({\n assistants: z\n .array(LockfileAssistantSchema.nullable().catch(null))\n .catch([])\n .transform((entries) =>\n entries.filter((entry): entry is LockfileAssistant => entry !== null),\n ),\n activeAssistant: z.union([z.string(), z.null()]).catch(null),\n tunnel: TunnelProcessRecordSchema.optional().catch(undefined),\n app: AppProcessRecordSchema.optional().catch(undefined),\n});\n\n/**\n * A validated, normalized lockfile — this schema's own output. Loose at the\n * root so the CLI's read-modify-write round-trip preserves the root keys it\n * owns and anything a newer writer added; readers key on the\n * modeled fields.\n */\nexport type Lockfile = z.infer<typeof LockfileSchema>;\n\n/**\n * Coerce parsed JSON into a validated {@link Lockfile}. Total and non-throwing:\n * the whole file parses through {@link LockfileSchema} — an entry that fails\n * validation is dropped whole (one malformed entry can't discard the file), a\n * missing or non-array `assistants` becomes `[]`, a non-string\n * `activeAssistant` becomes null, and a non-object root becomes an empty\n * lockfile.\n */\nexport function parseLockfile(raw: unknown): Lockfile {\n const parsed = LockfileSchema.safeParse(raw);\n return parsed.success ? parsed.data : LockfileSchema.parse({});\n}\n","import { z } from \"zod\";\n\nimport { RemoteSchema } from \"@mycadet/local-mode/contract\";\n\nfunction publicUrl(value: string): boolean {\n if (/[\\\\\\u0000-\\u0020]/.test(value) || value.startsWith(\"//\")) {\n return false;\n }\n if (\n (!value.startsWith(\"/\") &&\n !/^https?:\\/\\//i.test(value) &&\n value.includes(\":\")) ||\n /(?:^|\\/)(?:\\.|%2e){1,2}(?:\\/|$)/i.test(value)\n ) {\n return false;\n }\n try {\n const url = new URL(value, \"https://app.example\");\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n return (\n value.startsWith(\"/\") ||\n url.protocol === \"https:\" ||\n (url.protocol === \"http:\" &&\n [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname))\n );\n } catch {\n return false;\n }\n}\n\nexport const PublicUrlSchema = z\n .string()\n .min(1)\n .refine(\n publicUrl,\n \"Expected a public host URL without credentials or query parameters.\",\n );\nexport const ResolvedAssistantSchema = z.object({\n id: z.string().min(1),\n version: z.string().min(1),\n});\n\nexport const AssistantChoiceSchema = z.object({\n id: z.string().min(1),\n name: z\n .string()\n .nullish()\n .transform((name) => name?.trim() || \"New Assistant\"),\n /** The cloud's synced avatar thumbnail; null means the cloud holds none. */\n avatar_url: z.string().nullish(),\n /** When the cloud hatched the assistant (ISO 8601); null when unknown. */\n created: z.string().nullish(),\n /**\n * The assistant's own hosting kind, stamped per row by the serving control\n * plane — the cloud lists its assistants \"cloud\", a self-hosted edge\n * stamps its lockfile cloud. Unknown or absent values (servers predating\n * the stamp, a hostile row) fall back to undefined; the chooser then uses\n * the build's own remote.\n */\n remote: RemoteSchema.nullish().catch(() => undefined),\n});\n\nexport const AssistantResolutionSchema = z.discriminatedUnion(\"status\", [\n z.object({ status: z.literal(\"ready\"), assistant: ResolvedAssistantSchema }),\n z.object({ status: z.literal(\"notReady\") }),\n z.object({ status: z.literal(\"unavailable\") }),\n // Selection carries no rows by design — the rows are the list contract's;\n // this arm just sends the shell to the selection page.\n z.object({ status: z.literal(\"selection\") }),\n z.object({ status: z.literal(\"empty\") }),\n z.object({ status: z.literal(\"unauthenticated\") }),\n]);\n/**\n * What the list endpoint alone can answer: the choices on the machine\n * (nothing hatched is the empty arm), or no session to read them with. The chooser renders off this contract only —\n * the single-assistant resolution arms (ready/notReady/unavailable/empty)\n * belong to the shell's resolution flow, whose own selection arm carries no\n * rows — it routes here.\n */\nexport const AssistantListSchema = z.discriminatedUnion(\"status\", [\n z.object({\n status: z.literal(\"selection\"),\n assistants: z.array(AssistantChoiceSchema),\n }),\n z.object({ status: z.literal(\"empty\") }),\n z.object({ status: z.literal(\"unauthenticated\") }),\n]);\nexport type ResolvedAssistant = z.infer<typeof ResolvedAssistantSchema>;\nexport type AssistantResolution = z.infer<typeof AssistantResolutionSchema>;\nexport type AssistantList = z.infer<typeof AssistantListSchema>;\nexport const AssistantNavigationSchema = z.object({\n source: z.literal(\"mycadet-web-nav\"),\n nav: z.enum([\"push\", \"replace\", \"pop\"]),\n path: z.string(),\n});\n\n/**\n * The frame's HOST-navigation request: the assistant bundle opening a\n * surface outside its own url space (the account area — the Doctor page).\n * The frame listener allowlists `/account/*`; the schema only fixes the\n * message's shape.\n */\nexport const HostNavigationSchema = z.object({\n source: z.literal(\"mycadet-web-host-nav\"),\n path: z.string(),\n});\n"],"mappings":";;2FC2Ba,IAA0B;AAoDvC,SAAS,EAAU,GAAuB;CACxC,OAAO;EACL,QAAQ;EACR,SAAS,IAAO;EAChB,QAAQ,IAAO;EACf,KAAK,IAAO;EACZ,eAAe,IAAO;EACtB,KAAK,IAAO;EACb;;AA+LgB,EAAU,KAAM,EAKhB,EAAU,KAAM,EAKhB,EAAU,KAAM,EAItB,GAAoB,EAApB,GAIM,EAAU,IAAM;;;ACzLnC,IAAa,IAAmB;CAC9B;CACA;CACA;CACA;CACD;;CCpHD,EAAO,UAAU,EAAA;;AEoDjB,IAAa,IAAe,EAAE,KAAK;CAbjC;CACA;CACA;CACA;CACA;CACA;CAQiC,CAAc,EAgGpC,IAAe,QAAQ,IAAI,eACpC,OAAO,QAAQ,IAAI,aAAa,GAChC,MAoCS,IAAsB,EAAE,OAAO;CAC1C,gBAAgB,EAAE,QAAQ;CAC1B,cAAc,EAAE,QAAQ;CACxB,UAAU,EAAE,QAAQ;CACrB,CAAC,EAmBW,IAA4B,EACtC,aAAa;CAMZ,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU;CAG5C,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU;CAC5C,CAAC,CACD,aAAa,GAAQ,MAAY;CAChC,AACE,EAAO,SAAS,KAAA,KAChB,EAAO,QAAQ,KAAA,KACf,EAAO,OAAO,EAAO,OAErB,EAAQ,SAAS;EACf,MAAM;EACN,MAAM,CAAC,OAAO;EACd,SAAS,gBAAgB,EAAO,KAAK,gCAAgC,EAAO,IAAI;EACjF,CAAC;EAEJ,EAIS,IAAsB,EAAE,aAAa,EAChD,QAAQ,EAA0B,UAAU,EAC7C,CAAC,EA0BI,IAAsB,EAAE,OAAO;CACnC,aAAa,EAAE,QAAQ;CAEvB,kBAAkB,EAAE,QAAQ,CAAC,UAAU;CAEvC,qBAAqB,EAAE,QAAQ,CAAC,UAAU;CAC1C,MAAM,EAAE,QAAQ,CAAC,UAAU;CAC3B,WAAW,EAAE,QAAQ,CAAC,UAAU;CAMhC,aAAa,EAAE,QAAQ,CAAC,UAAU;CAElC,YAAY,EAAE,QAAQ,CAAC,UAAU;CAOjC,SAAS,EAAoB,UAAU,CAAC,MAAM,KAAA,EAAU;CACzD,CAAC,EAMI,IAA8B;CAElC,MAAM,EAAE,QAAQ;CAEhB,YAAY,EAAE,QAAQ,CAAC,UAAU;CAEjC,SAAS,EAAE,QAAQ,CAAC,UAAU;CAC/B,EAGK,IAAuB,EAAoB,OAAO;CACtD,GAAG;CAGH,aAAa,EAAE,QAAQ,CAAC,UAAU;CAClC,OAAO,EAAE,QAAQ,QAAQ;CAC1B,CAAC,EAGI,IAAwB,EAAoB,OAAO;CACvD,GAAG;CACH,OAAO,EAAE,QAAQ,SAAS;CAE1B,eAAe,EAAoB,UAAU;CAC9C,CAAC,EAGI,IAAgC,EAAoB,OAAO;CAC/D,GAAG;CACH,OAAO,EAAE,QAAQ,kBAAkB;CAEnC,YAAY,EAAE,QAAQ;CACvB,CAAC,EAGI,IAAuB,EAAoB,OAAO,EACtD,OAAO,EAAE,QAAQ,QAAQ,EAC1B,CAAC,EAGI,IAAwB,EAAoB,OAAO;CACvD,OAAO,EAAE,QAAQ,SAAS;CAE1B,WAAW,EAAE,QAAQ;CAErB,MAAM,EAAE,QAAQ;CACjB,CAAC,EAWI,IAAyB,EAAoB,OAAO;CACxD,OAAO,EAAE,QAAQ,UAAU;CAE3B,YAAY,EAAE,QAAQ;CAEtB,MAAM,EAAE,QAAQ;CAEhB,YAAY,EAAE,QAAQ,CAAC,UAAU;CAClC,CAAC;AAgBF,SAAS,EAAe,GAAyB;CAC/C,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAI,IAAM,GACJ,IAAQ,EAAI;CAIlB,IAHM,OAAO,KAAU,YAAY,EAAa,UAAU,EAAM,CAAC,YAC/D,IAAM;EAAE,GAAG;EAAK,OAAO;EAAS,GAE9B,EAAI,SAAS,KAAA,GAAW;EAK1B,IAAM,IAAY,EAAI,WAChB,IACJ,KAAa,OAAO,KAAc,WAC7B,EAAsC,cACvC,KAAA;EACN,IAAM;GACJ,GAAG;GACH,MAAM,OAAO,KAAgB,WAAW,IAAc;GACvD;;CAEH,OAAO;;AAGT,IAAa,IAA0B,EAAE,WACvC,GACA,EAAE,mBAAmB,SAAS;CAC5B;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,CACH,EAsDY,IAA4B,EAAE,OAAO;CAChD,KAAK,EAAE,QAAQ;CAEf,UAAU,EAAE,KAAK,EAAiB;CAClC,WAAW,EAAE,QAAQ;CAErB,WAAW,EAAE,QAAQ,CAAC,UAAU;CACjC,CAAC,EASW,IAAyB,EAAE,OAAO;CAE7C,KAAK,EAAE,QAAQ,CAAC,UAAU;CAC1B,WAAW,EAAE,QAAQ,CAAC,UAAU;CAEhC,MAAM,EAAE,QAAQ;CAMhB,UAAU,EAAE,QAAQ,CAAC,UAAU;CAChC,CAAC;AAY4B,EAAE,YAAY;CAC1C,YAAY,EACT,MAAM,EAAwB,UAAU,CAAC,MAAM,KAAK,CAAC,CACrD,MAAM,EAAE,CAAC,CACT,WAAW,MACV,EAAQ,QAAQ,MAAsC,MAAU,KAAK,CACtE;CACH,iBAAiB,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK;CAC5D,QAAQ,EAA0B,UAAU,CAAC,MAAM,KAAA,EAAU;CAC7D,KAAK,EAAuB,UAAU,CAAC,MAAM,KAAA,EAAU;CACxD,CAAC;;;AC9fF,SAAS,EAAU,GAAwB;CAIzC,IAHI,oBAAoB,KAAK,EAAM,IAAI,EAAM,WAAW,KAAK,IAI1D,CAAC,EAAM,WAAW,IAAI,IACrB,CAAC,gBAAgB,KAAK,EAAM,IAC5B,EAAM,SAAS,IAAI,IACrB,mCAAmC,KAAK,EAAM,EAE9C,OAAO;CAET,IAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAO,sBAAsB;EAIjD,OAHI,EAAI,YAAY,EAAI,YAAY,EAAI,UAAU,EAAI,OAC7C,KAGP,EAAM,WAAW,IAAI,IACrB,EAAI,aAAa,YAChB,EAAI,aAAa,WAChB;GAAC;GAAa;GAAa;GAAQ,CAAC,SAAS,EAAI,SAAS;SAExD;EACN,OAAO;;;AAIX,IAAa,IAAkB,EAC5B,QAAQ,CACR,IAAI,EAAE,CACN,OACC,GACA,sEACD,EACU,IAA0B,EAAE,OAAO;CAC9C,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;CACrB,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC3B,CAAC,EAEW,IAAwB,EAAE,OAAO;CAC5C,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;CACrB,MAAM,EACH,QAAQ,CACR,SAAS,CACT,WAAW,MAAS,GAAM,MAAM,IAAI,gBAAgB;CAEvD,YAAY,EAAE,QAAQ,CAAC,SAAS;CAEhC,SAAS,EAAE,QAAQ,CAAC,SAAS;CAQ7B,QAAQ,EAAa,SAAS,CAAC,YAAY,KAAA,EAAU;CACtD,CAAC,EAEW,IAA4B,EAAE,mBAAmB,UAAU;CACtE,EAAE,OAAO;EAAE,QAAQ,EAAE,QAAQ,QAAQ;EAAE,WAAW;EAAyB,CAAC;CAC5E,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,CAAC;CAC3C,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,cAAc,EAAE,CAAC;CAG9C,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,YAAY,EAAE,CAAC;CAC5C,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,CAAC;CACxC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,kBAAkB,EAAE,CAAC;CACnD,CAAC,EAQW,IAAsB,EAAE,mBAAmB,UAAU;CAChE,EAAE,OAAO;EACP,QAAQ,EAAE,QAAQ,YAAY;EAC9B,YAAY,EAAE,MAAM,EAAsB;EAC3C,CAAC;CACF,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,CAAC;CACxC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,kBAAkB,EAAE,CAAC;CACnD,CAAC,EAIW,IAA4B,EAAE,OAAO;CAChD,QAAQ,EAAE,QAAQ,kBAAkB;CACpC,KAAK,EAAE,KAAK;EAAC;EAAQ;EAAW;EAAM,CAAC;CACvC,MAAM,EAAE,QAAQ;CACjB,CAAC,EAQW,IAAuB,EAAE,OAAO;CAC3C,QAAQ,EAAE,QAAQ,uBAAuB;CACzC,MAAM,EAAE,QAAQ;CACjB,CAAC"}
|
|
1
|
+
{"version":3,"file":"resolution-contract-lMCy1TXs.js","names":[],"sources":["../../../../packages/shared/github-repository.ts","../../../../packages/shared/seeds.ts","../../../../packages/service-contracts/src/ingress.ts","../../__vite-browser-external","../../../../packages/local-mode/src/paths.ts","../../../../packages/local-mode/src/lockfile-contract.ts","../../src/assistant/resolution-contract.ts"],"sourcesContent":["/** The GitHub repository Cadet's source and releases live in. */\nexport const CADET_GITHUB_REPOSITORY = \"vargasjr-dev/mycadet-temp\";\n\n/** URL of the public GitHub repository. */\nexport const CADET_GITHUB_REPOSITORY_URL = `https://github.com/${CADET_GITHUB_REPOSITORY}`;\n","// Loaded standalone by Capacitor's single-file TS config loader (via the\n// `./seeds` subpath) — keep imports type-only (this file has none) and keep\n// this file free of the import-meta meta-property: the loader transpiles this\n// file to CJS, where that syntax is illegal and makes Node misroute the\n// result as ESM. Browser bundles (chrome extension, web renderer) import it\n// for the same reason.\n\n/**\n * Per-service default port set. Phase 5 (per-environment port offsets) is\n * deferred from MVP, so today every environment uses the same port set. The\n * shape exists so the rest of the stack can call `getDefaultPorts(env)` and\n * gain per-env offsets later without changing any call sites.\n */\nexport interface PortMap {\n daemon: number;\n gateway: number;\n qdrant: number;\n ces: number;\n outboundProxy: number;\n tcp: number;\n}\n\n/**\n * The port the local app server binds by default — the single origin every\n * local surface (webview, assistant daemon, CLI) dials. The local seed's\n * base URL reuses it so no surface carries its own port constant.\n */\nexport const DEFAULT_APP_SERVER_PORT = 7840;\n\n/**\n * A resolved environment definition. Required fields are `name` and\n * `baseUrl`. All other fields are optional and declared upfront — new\n * fields are additive, never breaking. `name` is a `CadetEnvironment`: a\n * definition outside the known names has nowhere to key into.\n */\nexport interface EnvironmentDefinition {\n name: CadetEnvironment;\n\n /**\n * The single origin for API, auth, web, and the assistant mount — one\n * deployment serves all of it, so one URL describes it. The local seed\n * points at the app server's default port; the cloud seeds point at the\n * environment's host.\n */\n baseUrl: string;\n\n /** Human-readable label for UI surfaces. */\n displayName?: string;\n\n /** Hint for UI surfaces that want to tint or badge their display. */\n tintColor?: string;\n\n /** Per-service port overrides merged on top of defaults. */\n portsOverride?: Partial<PortMap>;\n\n /** Override for the XDG config directory. */\n configDirOverride?: string;\n\n /**\n * Override for the directory containing the lockfile. In-memory only —\n * tests inject curated env lists with it; path helpers never read env\n * vars to populate it.\n */\n lockfileDirOverride?: string;\n}\n\n/**\n * Non-prod port blocks. Each environment gets a 1000-port window in the\n * 17000–21000 band. Within a block, services are spaced 100 apart so up to\n * 100 assistants can coexist without the scan (`findAvailablePort`) running\n * one service's range into the next. Band chosen to sit below Linux's\n * default ephemeral start (32768) and macOS's (49152), and away from the\n * 3000/5000/8000/9000 dev-tool swamp. Production keeps its legacy,\n * non-contiguous port set (7821/7830/6333/8090/8080/8765): cross-env\n * collision is the only problem this change targets, prod is unaffected\n * because only one env's assistants compete on a given machine, and\n * churning it would leave existing hatches on 7821 while new ones\n * allocated elsewhere.\n */\nfunction portBlock(base: number): PortMap {\n return {\n daemon: base,\n gateway: base + 100,\n qdrant: base + 200,\n ces: base + 300,\n outboundProxy: base + 400,\n tcp: base + 500,\n };\n}\n\n/**\n * The set of environment names every surface shares — the seeds below are\n * exactly these five. Lockfiles, config directories, release channels, and\n * the mobile shells' scheme keys all key off this union, so a name outside\n * it is a bug the compiler can catch.\n */\nexport const CADET_ENVIRONMENT_NAMES = [\n \"local\",\n \"dev\",\n \"staging\",\n \"production\",\n \"test\",\n] as const;\n\nexport type CadetEnvironment = (typeof CADET_ENVIRONMENT_NAMES)[number];\n\nfunction isCadetEnvironment(\n name: string | undefined,\n): name is CadetEnvironment {\n return (\n typeof name === \"string\" &&\n (CADET_ENVIRONMENT_NAMES as readonly string[]).includes(name)\n );\n}\n\n/**\n * globalThis.process reads the same env under bun/node while staying\n * crash-free in browser bundles (no bare `process` reference) — and free\n * of import-meta syntax, which the Capacitor config loader can't transpile.\n */\nfunction processEnv(): Record<string, string | undefined> | undefined {\n return (\n globalThis as unknown as {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process?.env;\n}\n\n/**\n * Whether a version string denotes a dev build from the dev-release\n * pipeline, e.g. `0.12.6-dev.202610081023.3a27b77`.\n */\nexport function isDevVersion(version: string): boolean {\n return /-dev\\./i.test(version);\n}\n\n/**\n * Whether a version string denotes a staging build, e.g. `0.6.0-staging.3`.\n */\nexport function isStagingVersion(version: string): boolean {\n return /-staging\\./i.test(version);\n}\n\n/**\n * Whether a version string denotes a local build, e.g. `0.12.6-local.42`.\n * Local builds are never published.\n */\nexport function isLocalVersion(version: string): boolean {\n return /-local\\./i.test(version);\n}\n\n/**\n * The environment a released channel version pins, or `undefined` when the\n * version carries no channel suffix. A plain version — a workspace\n * checkout's unstamped package version — reads as \"no version signal\",\n * leaving the remaining steps to decide. Mirrors the CLI resolver's mapping\n * (local, dev, staging).\n */\nexport function environmentFromVersion(\n version: string | undefined,\n): CadetEnvironment | undefined {\n if (isLocalVersion(version ?? \"\")) return \"local\";\n if (isDevVersion(version ?? \"\")) return \"dev\";\n if (isStagingVersion(version ?? \"\")) return \"staging\";\n return undefined;\n}\n\n/**\n * Read the persisted default environment — the file pre-version-tied CLIs\n * wrote so their successors honor the machine's chosen default. Node-only\n * and resolved at call time (`process.getBuiltinModule`), so this file\n * keeps zero static node imports: the Capacitor config loader transpiles it\n * standalone and a static node import would die there, while browser\n * bundles (chrome extension, web renderer) never reach the call.\n */\nfunction readPersistedDefault(): CadetEnvironment | undefined {\n const node = process as unknown as {\n getBuiltinModule?: (id: string) => unknown;\n platform: string;\n };\n const fs = node.getBuiltinModule?.(\"node:fs\") as\n | typeof import(\"node:fs\")\n | undefined;\n const os = node.getBuiltinModule?.(\"node:os\") as\n | typeof import(\"node:os\")\n | undefined;\n const path = node.getBuiltinModule?.(\"node:path\") as\n | typeof import(\"node:path\")\n | undefined;\n if (!fs || !os || !path) return undefined;\n\n // Mirrors local-mode's resolveConfigHome (the same XDG/AppData derivation).\n const isWindows = node.platform === \"win32\";\n const join = isWindows ? path.win32.join : path.posix.join;\n const configHome = isWindows\n ? process.env.APPDATA?.trim() || join(os.homedir(), \"AppData\", \"Roaming\")\n : process.env.XDG_CONFIG_HOME?.trim() || join(os.homedir(), \".config\");\n const filePath = join(configHome, \"cadet\", \"environment\");\n try {\n if (!fs.existsSync(filePath)) return undefined;\n const content = fs.readFileSync(filePath, \"utf-8\").trim();\n return isCadetEnvironment(content) ? content : undefined;\n } catch {\n // Unreadable file — no default.\n return undefined;\n }\n}\n\n/**\n * The environment name every surface shares: `CADET_ENVIRONMENT`, then\n * `VITE_ENVIRONMENT`, then the release channel the copy's version carries\n * (`__CADET_OWN_VERSION__`, defined at build time by hosts that bundle\n * local-mode), then the persisted default written by pre-version-tied CLIs,\n * then production.\n */\nexport function resolveEnvironmentName(): CadetEnvironment {\n const env = processEnv();\n const named = [\n env?.CADET_ENVIRONMENT?.trim(),\n env?.VITE_ENVIRONMENT?.trim(),\n ].find(isCadetEnvironment);\n if (named) return named;\n return (\n environmentFromVersion(\n (globalThis as { __CADET_OWN_VERSION__?: string }).__CADET_OWN_VERSION__,\n ) ??\n readPersistedDefault() ??\n \"production\"\n );\n}\n\n/**\n * The base URL of Cadet Cloud — the hosted deployment the local surfaces\n * pair with (API, auth, web, and the assistant mount all live on it). The\n * explicit `CADET_CLOUD_URL` override wins, else the seed of\n * `resolveEnvironmentName`. This says \"cloud\" because the URL always points\n * at a Cadet Cloud deployment, never at something that represents this\n * surface itself.\n */\nexport function getCloudBaseUrl(): string {\n const explicit = processEnv()?.CADET_CLOUD_URL?.trim();\n if (explicit) return explicit;\n return SEEDS[resolveEnvironmentName()].baseUrl;\n}\n\n/**\n * Built-in environment definitions and the source of truth for the\n * set of known environment names.\n *\n * Custom environments via a user config file are a future phase — see the\n * \"Coexisting environments\" design doc. Until then, a call site that needs a\n * new environment must add it here and rebuild.\n */\nexport const SEEDS: Record<CadetEnvironment, EnvironmentDefinition> = {\n production: {\n name: \"production\",\n baseUrl: \"https://mycadet.ai\",\n },\n staging: {\n name: \"staging\",\n baseUrl: \"https://staging.mycadet.ai\",\n portsOverride: portBlock(17000),\n },\n test: {\n name: \"test\",\n baseUrl: \"https://test.mycadet.ai\",\n portsOverride: portBlock(19000),\n },\n dev: {\n name: \"dev\",\n baseUrl: \"https://dev.mycadet.ai\",\n portsOverride: portBlock(18000),\n },\n local: {\n name: \"local\",\n baseUrl: `http://localhost:${DEFAULT_APP_SERVER_PORT}`,\n // assistantBaseUrl: \"http://host.docker.internal:7840\",\n // ^ uncomment once dockerized hatch path is live — the assistant runs\n // in a different network namespace than the host.\n portsOverride: portBlock(20000),\n },\n};\n","/**\n * Map a Cadet Cloud host onto its environment's tunnel host, following the\n * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,\n * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for\n * hosts outside that convention (localhost, custom domains). Shared by the\n * gateway speech relay (which passes the request's own host) and the web\n * live-voice client (which defaults to the page's origin) so the two ends\n * of the cloud-speech transport can't drift.\n */\nexport function tunnelHostForCloudHost(\n host: string = (globalThis as { location?: { hostname?: string } }).location\n ?.hostname ?? \"\",\n): string | null {\n if (host === \"cloud.mycadet.ai\") {\n return \"tunnel.mycadet.ai\";\n }\n const match = /^([a-z0-9-]+)-cloud\\.mycadet\\.ai$/.exec(host);\n return match ? `${match[1]}-tunnel.mycadet.ai` : null;\n}\n\nexport function normalizePublicBaseUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const normalized = value.trim().replace(/\\/+$/, \"\");\n return normalized.length > 0 ? normalized : undefined;\n}\n\n/**\n * Application close code the gateway's tunnel bridge sends to proxied\n * WebSockets when the tunnel itself is lost (tunnel disconnect, gateway\n * shutdown). A dedicated code because the natural 1001 (going away) cannot\n * be sent through the JS `close()` API (the bridge would remap it to a\n * misleading 4001), and Bun's WebSocket client drops close reasons, so the\n * code is the only signal that survives the relay to the daemon.\n */\nexport const GATEWAY_TUNNEL_LOST_WS_CLOSE_CODE = 4801;\n\nexport function normalizeHttpPublicBaseUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (trimmed.length === 0) return undefined;\n if (/[?#]/.test(trimmed)) return undefined;\n\n try {\n const url = new URL(trimmed);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n return undefined;\n }\n if (!url.hostname) return undefined;\n url.pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n return url.toString();\n } catch {\n return undefined;\n }\n}\n\n/**\n * The same validation as `normalizeHttpPublicBaseUrl`, minus the root path it\n * always emits (`https://x` becomes `https://x/`). Callers that append a path\n * or persist the result want this shape, so they neither join through a double\n * slash nor re-strip what the normalizer just added.\n */\nexport function normalizeHttpPublicBaseUrlWithoutTrailingSlash(\n value: unknown,\n): string | undefined {\n return normalizePublicBaseUrl(normalizeHttpPublicBaseUrl(value));\n}\n\n/**\n * A trimmed string, or undefined when the value is not a non-blank string.\n *\n * Exported because the identity check in the daemon's tunnel probe compares an\n * id recorded here against one an edge serves, and the two sides have to trim\n * alike: a padded served id must read as a match, not as a different\n * assistant.\n */\nexport function trimmedNonEmptyString(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n// Records `cadet tunnel` leaves in the workspace config's `ingress` section.\n// The CLI writes them and the daemon reads them, and the two are separate\n// build units that cannot import each other, so the key names, the provider\n// allowlist, and the validation all live here rather than in a copy on each\n// side.\n\n/** Key under `ingress` holding the tunnel that most recently ran. */\nexport const INGRESS_LAST_TUNNEL_KEY = \"lastTunnel\";\n\n/**\n * Key under `ingress` holding a tunnel published for device pairing alone.\n *\n * A tailnet-only tunnel started while webhook integrations are configured\n * cannot carry their callbacks, so it leaves `publicBaseUrl` (the callback\n * base) as it is and records itself here. Readers that answer the pairing card\n * prefer this address over `publicBaseUrl`; teardown drops the key.\n */\nexport const INGRESS_PAIRING_TUNNEL_KEY = \"pairingTunnel\";\n\n/** Key under `ingress` holding the assistant id that tunnel fronted. */\nexport const INGRESS_ASSISTANT_ID_KEY = \"assistantId\";\n\n/**\n * The tunnel providers the CLI offers, in the order they are presented —\n * the managed `cadet` tunnel first (listed, not yet implemented), then the\n * local ones. `cadet tunnel`'s accepted `--provider` values and the\n * persisted-record validation below both derive from it.\n */\nexport const TUNNEL_PROVIDERS = [\n \"cadet\",\n \"ngrok\",\n \"cloudflare\",\n \"tailscale\",\n] as const;\n\nexport type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];\n\n/** A recorded tunnel, kept after teardown so UIs can name the command to restart. */\nexport interface TunnelRecord {\n provider: TunnelProviderName;\n publicBaseUrl: string;\n}\n\nfunction isTunnelProviderName(value: unknown): value is TunnelProviderName {\n return (\n typeof value === \"string\" &&\n (TUNNEL_PROVIDERS as readonly string[]).includes(value)\n );\n}\n\n/**\n * Parse an `ingress.lastTunnel` or `ingress.pairingTunnel` value, or null when\n * it is absent or malformed.\n *\n * A hand-edited config must not hand callers an unusable address or a provider\n * name no command accepts: the provider is checked against the allowlist\n * because readers render it into a restart command, and the URL faces the same\n * absolute HTTP(S) constraint as every other public base URL. The URL comes\n * back in the shape the validator produced, so readers never re-normalize it.\n */\nexport function parseTunnelRecord(value: unknown): TunnelRecord | null {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return null;\n }\n const { provider, publicBaseUrl } = value as Record<string, unknown>;\n if (!isTunnelProviderName(provider)) {\n return null;\n }\n const normalized =\n normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);\n if (normalized === undefined) {\n return null;\n }\n return { provider, publicBaseUrl: normalized };\n}\n\n/**\n * The tunnel a reader should offer to restart, given an `ingress` section, or\n * null when it records none.\n *\n * A pairing-only record wins: it is written by a tunnel that deliberately left\n * `publicBaseUrl` (and the `lastTunnel` beside it) pointing at an older public\n * provider, so naming that older one would send the user to rebuild an ingress\n * they are not using instead of the tunnel actually in front of them.\n */\nexport function selectRestartTunnelRecord(\n ingress: unknown,\n): TunnelRecord | null {\n if (typeof ingress !== \"object\" || ingress === null) {\n return null;\n }\n const section = ingress as Record<string, unknown>;\n return (\n parseTunnelRecord(section[INGRESS_PAIRING_TUNNEL_KEY]) ??\n parseTunnelRecord(section[INGRESS_LAST_TUNNEL_KEY])\n );\n}\n\n/** Parse an `ingress.assistantId` value, or null when it is absent or blank. */\nexport function parseRecordedAssistantId(value: unknown): string | null {\n return trimmedNonEmptyString(value) ?? null;\n}\n","module.exports = {}","import { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nconst PRODUCTION_ENVIRONMENT_NAME = \"production\";\n\nconst isWindows = () => process.platform === \"win32\";\nconst paths = () => (isWindows() ? path.win32 : path.posix);\n\nexport function resolveConfigHome(): string {\n if (isWindows()) {\n return (\n process.env.APPDATA?.trim() ||\n paths().join(homedir(), \"AppData\", \"Roaming\")\n );\n }\n return process.env.XDG_CONFIG_HOME?.trim() || paths().join(homedir(), \".config\");\n}\n\nexport function resolveDataHome(): string {\n if (isWindows()) {\n return (\n process.env.LOCALAPPDATA?.trim() ||\n paths().join(homedir(), \"AppData\", \"Local\")\n );\n }\n return process.env.XDG_DATA_HOME?.trim() || paths().join(homedir(), \".local\", \"share\");\n}\n\nexport const joinLocalPath = (...segments: string[]) => paths().join(...segments);\n\nexport function assertSafePathSegment(value: string, label: string): void {\n const unsafe =\n !value ||\n /[/\\\\\\0]/.test(value) ||\n /^\\.{1,2}$/.test(value) ||\n (isWindows() &&\n (/[<>:\"|?*\\u0000-\\u001f]/.test(value) ||\n /[. ]$/.test(value) ||\n /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$)/i.test(value)));\n if (unsafe) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n}\n\n/**\n * The directory name an environment's state lives under: `cadet` for\n * production (the legacy location other environments must not collide with),\n * `cadet-<env>` otherwise. Exported so hosts that enumerate a SPECIFIC\n * environment's directories — the CLI's cross-environment detection — can\n * compose them from {@link resolveConfigHomes} and {@link resolveDataHome}\n * without an options bag threading through the ambient resolvers.\n */\nexport function environmentDirectoryName(cadetEnv: string): string {\n assertSafePathSegment(cadetEnv, \"environment name\");\n return cadetEnv === PRODUCTION_ENVIRONMENT_NAME\n ? \"cadet\"\n : `cadet-${cadetEnv}`;\n}\n","/**\n * The lockfile wire contract — one Zod schema that is the single source of truth\n * for an assistant entry's shape, its validation, the `cloud` taxonomy, the\n * normalization, and the redaction policy.\n *\n * Kept in its own module, exported from the package as `./contract`, so\n * consumers import the contract without pulling in the host I/O graph\n * (`node:fs`, the CLI resolver). The CLI, the host I/O layer, the Electron and\n * dev-server bridges, and the renderer all depend on this one definition. It\n * references no node built-ins — the renderer bundles it directly — and `zod`\n * is its only third-party import.\n *\n * The lockfile is written by the Cadet CLI and read across the macOS↔CLI\n * boundary, which do not release in lockstep, so the parser is permissive at\n * the file level: it validates each entry whole through the schema (a\n * malformed field drops that entry, never the file), accepts — but drops from\n * the validated value — entry fields this build has never heard of, and\n * defaults `cloud` when it is absent or unknown. The schema is strict over\n * the fields it models: every field a reader needs is IN the schema, so a\n * CLI read-modify-write round-trip preserves it by construction. The\n * local-mode write path still merges raw JSON (unknown root fields survive\n * on disk; see `lockfile.ts`); the validated value carries only the modeled\n * shape.\n */\nimport { getCloudBaseUrl } from \"@mycadet/shared\";\nimport { TUNNEL_PROVIDERS } from \"@mycadet/service-contracts/ingress\";\nimport { z } from \"zod\";\n\nimport { readLockfile } from \"./lockfile\";\n\n/**\n * Known deployment topologies of a Cadet:\n * - `local` — on-machine bare-metal processes\n * - `docker` — docker-managed container\n * - `apple-container` — macOS-app-managed container\n * - `cloud` — managed by Cadet Cloud\n * - `custom` — remote VM accessible via SSH\n */\nexport const KNOWN_REMOTES = [\n \"local\",\n \"docker\",\n \"apple-container\",\n \"cloud\",\n \"custom\",\n \"baseten\",\n] as const;\n\n/**\n * The topology values as one zod enum, shared by the lockfile schema (which\n * wraps it in a `catch`) and the clients' injected-config schema, so the two\n * can never drift.\n */\nexport const RemoteSchema = z.enum(KNOWN_REMOTES);\nexport type Remote = z.infer<typeof RemoteSchema>;\n\n/**\n * Keys that must never cross the host→renderer boundary. Stripped from raw\n * lockfile JSON before it leaves a host (see `stripSensitiveFields`) and absent\n * from the renderer-facing schema below; the CLI's extended type is the only\n * place they are modeled.\n */\nexport const SENSITIVE_KEYS = [\n \"bearerToken\",\n \"guardianBootstrapSecret\",\n] as const;\n\n/**\n * Strip every `SENSITIVE_KEYS` field from a parsed lockfile, in place: from\n * each assistant entry and from that entry's `resources`. The one redaction\n * every host runs before lockfile data crosses to the renderer — pure, no\n * node built-ins, so it lives beside the keys it enforces and bundles into\n * the renderer through the `./contract` subpath.\n */\nexport function stripSensitiveFields(data: Record<string, unknown>): void {\n const assistants = data.assistants;\n if (!Array.isArray(assistants)) return;\n for (const assistant of assistants) {\n if (assistant && typeof assistant === \"object\") {\n const entry = assistant as Record<string, unknown>;\n for (const field of SENSITIVE_KEYS) {\n delete entry[field];\n }\n const resources = entry.resources;\n if (resources && typeof resources === \"object\") {\n for (const field of SENSITIVE_KEYS) {\n delete (resources as Record<string, unknown>)[field];\n }\n }\n }\n }\n}\n\n/**\n * Whether an entry's `runtimeUrl` is one a serving host can forward to: an\n * absolute http(s) URL. Shared by the renderer's paired-gateway URL derivation\n * and the host-side paired allowlist reader so the two can't drift.\n */\nexport function isUsableBaseUrl(url: unknown): url is string {\n if (typeof url !== \"string\" || url === \"\") {\n return false;\n }\n try {\n const parsed = new URL(url);\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\n/**\n * The local build's control-plane base, read from the lockfile alone: the\n * auto-tunnel's recorded `publicUrl` when one is up, else the app server\n * edge's loopback port. A loopback base is reachable from the iOS simulator\n * only — a device's `127.0.0.1` is the phone itself — so a device build\n * wants a tunnel up before it stamps. `undefined` when neither record\n * carries a usable address.\n *\n * The assistants play no role in this resolution — the capacitor app shell\n * only deals with the app server — and the CLI's workspace config is never\n * touched: the lockfile is the single home for edge state.\n */\nexport function resolveLocalServerUrl(): string | undefined {\n const lockfile = readLockfile();\n const tunnelUrl = lockfile.tunnel?.publicUrl;\n if (isUsableBaseUrl(tunnelUrl)) {\n return tunnelUrl;\n }\n const port = lockfile.app?.port;\n if (typeof port === \"number\") {\n return `http://127.0.0.1:${port}`;\n }\n return undefined;\n}\n\n/**\n * Clouds whose gateway listens on a loopback port of this machine: the\n * on-machine daemon (\"local\") and local Docker instances (\"docker\").\n */\nexport const LOOPBACK_GATEWAY_CLOUDS: readonly Remote[] = [\"local\", \"docker\"];\n\nexport function isLoopbackGatewayCloud(cloud: Remote): boolean {\n return LOOPBACK_GATEWAY_CLOUDS.includes(cloud);\n}\n\n/**\n * The gateway port a freshly hatched assistant's gateway listens on, with\n * the `GATEWAY_PORT` env override the CLI tooling honors.\n */\nexport const GATEWAY_PORT = process.env.GATEWAY_PORT\n ? Number(process.env.GATEWAY_PORT)\n : 7830;\n\n/**\n * The URL to dial an entry's gateway on, derived from the entry's canonical\n * fields — the one seam every consumer addresses a gateway through:\n *\n * - `custom` remotes: their recorded host and port.\n * - cloud-managed: the environment's base url (`getCloudBaseUrl()`).\n * - loopback topologies (`local`, `docker`, `apple-container`): the\n * recorded loopback gateway port, else the default gateway port.\n */\nexport function getGatewayUrl(entry: LockfileAssistant): string {\n // Baseten: the private preview is the door when one was minted — the\n // preview edge opens the port dynamically and guards it with a preview\n // token, so `Authorization` stays free for the gateway's own bearer.\n // Without one, the sandbox API's `/port/<PORT>/` route (the sandbox\n // access token authenticates it).\n if (entry.cloud === \"baseten\") {\n if (entry.previewUrl) {\n return entry.previewUrl.replace(/\\/+$/, \"\");\n }\n return `${entry.sandboxUrl.replace(/\\/+$/, \"\")}/port/${entry.port}`;\n }\n if (entry.cloud === \"custom\") {\n // Custom remotes speak TLS. Default ports elided the way a URL prints\n // them. The schema requires the address and port.\n const elided = entry.port === 443;\n return `https://${entry.ipAddress}${elided ? \"\" : `:${entry.port}`}`;\n }\n if (isCloudEntry(entry)) {\n return getCloudBaseUrl().replace(/\\/+$/, \"\");\n }\n return `http://127.0.0.1:${entry.port}`;\n}\n\n/** Docker image metadata for the service group. Enables rollback to known-good refs. */\nexport const ContainerInfoSchema = z.object({\n assistantImage: z.string(),\n gatewayImage: z.string(),\n cesImage: z.string(),\n});\nexport type ContainerInfo = z.infer<typeof ContainerInfoSchema>;\n\n/**\n * Host-independent resource policy for one assistant's machine allocation —\n * what the allocation *should be*, as opposed to the record fields (ports,\n * instanceDir, containerInfo) that describe what *is*. The section rides\n * every topology's entry: the stored shape is the same everywhere, while how\n * a policy materializes differs per remote (see {@link MACHINE_APPLICABILITY}).\n *\n * The schema is strict — unknown keys are rejected — because the write path\n * (future `cadet machine set`, daemon PATCH, cloud API) is the loud validator:\n * a typo'd knob must fail at the boundary, never silently degrade to the\n * runtime default. Parse time is deliberately softer: the field is embedded\n * with `.catch(undefined)`, so a malformed or unrecognized section from a\n * hand-edit or a different build quarantines to the defaults while the\n * entry itself survives, and the drift is visible to readers instead of\n * costing the assistant its identity fields.\n */\nexport const MachineMemoryPolicySchema = z\n .strictObject({\n /**\n * Soft limit in bytes: where the runtime should start reclaiming early\n * and smoothly (cgroup v2 `memory.high` on docker remotes). Absent =\n * unconstrained.\n */\n high: z.number().int().positive().optional(),\n /** Hard limit in bytes: the backstop the runtime may not exceed\n * (cgroup v2 `memory.max` on docker remotes). Absent = unconstrained. */\n max: z.number().int().positive().optional(),\n })\n .superRefine((memory, context) => {\n if (\n memory.high !== undefined &&\n memory.max !== undefined &&\n memory.high > memory.max\n ) {\n context.addIssue({\n code: \"custom\",\n path: [\"high\"],\n message: `memory.high (${memory.high}) must not exceed memory.max (${memory.max})`,\n });\n }\n });\nexport type MachineMemoryPolicy = z.infer<typeof MachineMemoryPolicySchema>;\n\n/** The `machine` section: one key per resource knob, added as they migrate here. */\nexport const MachinePolicySchema = z.strictObject({\n memory: MachineMemoryPolicySchema.optional(),\n});\nexport type MachinePolicy = z.infer<typeof MachinePolicySchema>;\n\n/**\n * How a stored `machine` policy materializes per remote topology. The store\n * is host-independent — this table is the one place that says what a policy\n * *means* on each remote, so the apply layer and readers derive semantics\n * from it instead of re-deciding per call site. Remotes gain real\n * applicability as their apply paths land (cloud/gVisor, apple-container).\n */\nexport type MachineApplicability = \"cgroup\" | \"process-hint\" | \"unsupported\";\n\nexport const MACHINE_APPLICABILITY = {\n local: \"process-hint\",\n docker: \"cgroup\",\n \"apple-container\": \"unsupported\",\n cloud: \"unsupported\",\n custom: \"unsupported\",\n baseten: \"unsupported\",\n} as const satisfies Record<Remote, MachineApplicability>;\n\n/**\n * The fields every topology actually uses: the identity every reader looks\n * entries up by, the display name, and the hatch/onboarding/conversation\n * bookkeeping that is topology-agnostic.\n */\nconst BaseAssistantSchema = z.object({\n assistantId: z.string(),\n /** Platform assistant UUID for this assistant's registration. */\n cloudAssistantId: z.string().optional(),\n /** Platform organization the entry belongs to. */\n cloudOrganizationId: z.string().optional(),\n name: z.string().optional(),\n hatchedAt: z.string().optional(),\n /**\n * When this assistant finished first-run onboarding. Absent for entries that\n * predate the record, which fall back to hatch age (see the web client's\n * `onboarded-assistant.ts`).\n */\n onboardedAt: z.string().optional(),\n /** PID of the file watcher process for instances hatched with --watch. */\n watcherPid: z.number().optional(),\n /**\n * Host-independent resource policy for this assistant's machine allocation\n * (see {@link MachinePolicySchema} for the strict-at-write, fallback-at-parse\n * contract, and {@link MACHINE_APPLICABILITY} for how it materializes per\n * remote). Absent = the runtime's own defaults apply.\n */\n machine: MachinePolicySchema.optional().catch(undefined),\n});\n\n/**\n * The fields the self-hosted, on-machine topologies (local, docker,\n * apple-container) share: host-recorded ports.\n */\nconst LocalRuntimeAssistantFields = {\n /** The gateway port a freshly hatched assistant's gateway listens on. */\n port: z.number(),\n /** The runtime daemon's port, recorded at hatch. */\n daemonPort: z.number().optional(),\n /** The content-editor server's port, recorded at hatch. */\n cesPort: z.number().optional(),\n};\n\n/** A plain on-machine assistant: host-managed ports and instance dir. */\nconst LocalAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n /** The instance's state root on this machine. Local instances keep their\n * data here. */\n instanceDir: z.string().optional(),\n cloud: z.literal(\"local\"),\n});\n\n/** A docker-hosted assistant: container metadata for rollback/upgrade. */\nconst DockerAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n cloud: z.literal(\"docker\"),\n /** Docker image metadata for rollback. Only present for docker topology entries. */\n containerInfo: ContainerInfoSchema.optional(),\n});\n\n/** An apple-container assistant, driven through its management socket. */\nconst AppleContainerAssistantSchema = BaseAssistantSchema.extend({\n ...LocalRuntimeAssistantFields,\n cloud: z.literal(\"apple-container\"),\n /** Management socket path for apple-container assistants; written by the macOS host. */\n mgmtSocket: z.string(),\n});\n\n/** A platform-hosted assistant, registered against the cloud API. */\nconst CloudAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"cloud\"),\n});\n\n/** A self-hosted remote assistant, reached at its own address. */\nconst CustomAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"custom\"),\n /** Host address of the custom remote's gateway. */\n ipAddress: z.string(),\n /** Gateway port on the custom remote. */\n port: z.number(),\n});\n\n/**\n * A Baseten sandbox entry: the sandbox server URL and the sandbox-internal\n * gateway port are pinned at creation. When the hatch also minted the\n * gateway's private preview, `previewUrl` is the gateway's canonical\n * address — the preview edge opens the port dynamically and guards it with\n * a preview token, leaving `Authorization` free for the gateway's own\n * bearer end to end. Entries without one (tokenless hatches, legacy) ride\n * the `{sandboxUrl}/port/{port}` route with the sandbox access token.\n */\nconst BasetenAssistantSchema = BaseAssistantSchema.extend({\n cloud: z.literal(\"baseten\"),\n /** The Baseten sandbox server URL; the port route rides on it. */\n sandboxUrl: z.string(),\n /** The sandbox-internal gateway port, piped in as `GATEWAY_PORT` at creation. */\n port: z.number(),\n /** The gateway's private preview URL, minted at hatch when a token resolves. */\n previewUrl: z.string().optional(),\n});\n\n/**\n * Route absent and unknown topology values to the local default before the\n * union discriminates, keeping the on-disk contract permissive about clouds\n * this build has never heard of.\n */\n/**\n * The one preprocess the entry union parses through. Routes absent and\n * unknown topology values to the local default before the union\n * discriminates, and backfills the legacy `resources.gatewayPort` onto\n * `port` — but only when the flat `port` field would fail the number\n * schema (absent or not a number): a valid port always wins. Zod has no\n * field-level sibling fallback, so the object-level preprocess is the\n * supported surface for this.\n */\nfunction normalizeEntry(entry: unknown): unknown {\n if (typeof entry !== \"object\" || entry === null) return entry;\n let rec = entry as Record<string, unknown>;\n const cloud = rec.cloud;\n if (!(typeof cloud === \"string\" && RemoteSchema.safeParse(cloud).success)) {\n rec = { ...rec, cloud: \"local\" };\n }\n if (rec.port === undefined) {\n // The flat port failed the required schema (absent on a legacy or bare\n // entry): fall back to the legacy resources.gatewayPort, else the\n // gateway default. A mistyped port never falls back — a bad modeled\n // field still drops the entry.\n const resources = rec.resources;\n const gatewayPort =\n resources && typeof resources === \"object\"\n ? (resources as Record<string, unknown>).gatewayPort\n : undefined;\n rec = {\n ...rec,\n port: typeof gatewayPort === \"number\" ? gatewayPort : GATEWAY_PORT,\n };\n }\n return rec;\n}\n\nexport const LockfileAssistantSchema = z.preprocess(\n normalizeEntry,\n z.discriminatedUnion(\"cloud\", [\n LocalAssistantSchema,\n DockerAssistantSchema,\n AppleContainerAssistantSchema,\n CloudAssistantSchema,\n CustomAssistantSchema,\n BasetenAssistantSchema,\n ]),\n);\n\n/**\n * A validated, normalized assistant entry — a discriminated union on\n * `cloud`, so each topology carries exactly the fields it uses: a baseten\n * entry's `sandboxUrl`/`port` are always defined, a docker entry's\n * `containerInfo` reads without touching unrelated topologies, and absent or\n * unknown on-disk clouds route to the local branch.\n */\nexport type LockfileAssistant = z.infer<typeof LockfileAssistantSchema>;\n\nexport type LocalAssistant = z.infer<typeof LocalAssistantSchema>;\nexport type DockerAssistant = z.infer<typeof DockerAssistantSchema>;\nexport type AppleContainerAssistant = z.infer<\n typeof AppleContainerAssistantSchema\n>;\nexport type CloudAssistant = z.infer<typeof CloudAssistantSchema>;\nexport type CustomAssistant = z.infer<typeof CustomAssistantSchema>;\nexport type BasetenAssistant = z.infer<typeof BasetenAssistantSchema>;\n\n/** Whether the entry is a platform-hosted assistant. */\nexport function isCloudEntry(\n entry: LockfileAssistant,\n): entry is CloudAssistant {\n return entry.cloud === \"cloud\";\n}\n\n/** Whether the entry is a docker-hosted assistant. */\nexport function isDockerEntry(\n entry: LockfileAssistant,\n): entry is DockerAssistant {\n return entry.cloud === \"docker\";\n}\n\n/** Whether the entry is an apple-container assistant. */\nexport function isAppleContainerEntry(\n entry: LockfileAssistant,\n): entry is AppleContainerAssistant {\n return entry.cloud === \"apple-container\";\n}\n\n/** Whether the entry is a plain on-machine assistant. */\nexport function isLocalEntry(\n entry: LockfileAssistant,\n): entry is LocalAssistant {\n return entry.cloud === \"local\";\n}\n\n/**\n * A CLI-managed OS process recorded at the lockfile root under `tunnel`:\n * the auto-tunnel agent fronting the edge. `cadet ps` renders it alongside\n * the per-assistant process trees, and the stop paths verify the pid before\n * signaling it.\n */\nexport const TunnelProcessRecordSchema = z.object({\n pid: z.number(),\n /** Which tunnel provider owns the process. */\n provider: z.enum(TUNNEL_PROVIDERS),\n startedAt: z.string(),\n /** The public URL the tunnel agent serves. */\n publicUrl: z.string().optional(),\n});\nexport type TunnelProcessRecord = z.infer<typeof TunnelProcessRecordSchema>;\n\n/**\n * A CLI-managed OS process recorded at the lockfile root under `app`: the\n * app server (web artifacts, `/v1` proxying, the outer shell). Shares\n * nothing with the tunnel record beyond the pid bookkeeping — a tunnel\n * serves a URL, the app server serves a port.\n */\nexport const AppProcessRecordSchema = z.object({\n /** The app server's bookkeeping, present while it runs. */\n pid: z.number().optional(),\n startedAt: z.string().optional(),\n /** Listening port for the app server. */\n port: z.number(),\n /**\n * The commit the edge's source-built client bundles were last built from\n * — the auto-build staleness stamp. Rides the running edge's record: a\n * build only stamps when an edge record exists to carry it.\n */\n builtSha: z.string().optional(),\n});\nexport type AppProcessRecord = z.infer<typeof AppProcessRecordSchema>;\n\n/**\n * Renderer-facing result of a lockfile write, returned across the host bridge.\n * Carries no HTTP-style status — hosts map that away because the renderer\n * surfaces failures by message alone.\n */\nexport type LockfileWriteResult =\n | { ok: true; lockfile: Lockfile }\n | { ok: false; error: string };\n\nexport const LockfileSchema = z.looseObject({\n assistants: z\n .array(LockfileAssistantSchema.nullable().catch(null))\n .catch([])\n .transform((entries) =>\n entries.filter((entry): entry is LockfileAssistant => entry !== null),\n ),\n activeAssistant: z.union([z.string(), z.null()]).catch(null),\n tunnel: TunnelProcessRecordSchema.optional().catch(undefined),\n app: AppProcessRecordSchema.optional().catch(undefined),\n});\n\n/**\n * A validated, normalized lockfile — this schema's own output. Loose at the\n * root so the CLI's read-modify-write round-trip preserves the root keys it\n * owns and anything a newer writer added; readers key on the\n * modeled fields.\n */\nexport type Lockfile = z.infer<typeof LockfileSchema>;\n\n/**\n * Coerce parsed JSON into a validated {@link Lockfile}. Total and non-throwing:\n * the whole file parses through {@link LockfileSchema} — an entry that fails\n * validation is dropped whole (one malformed entry can't discard the file), a\n * missing or non-array `assistants` becomes `[]`, a non-string\n * `activeAssistant` becomes null, and a non-object root becomes an empty\n * lockfile.\n */\nexport function parseLockfile(raw: unknown): Lockfile {\n const parsed = LockfileSchema.safeParse(raw);\n return parsed.success ? parsed.data : LockfileSchema.parse({});\n}\n","import { z } from \"zod\";\n\nimport { RemoteSchema } from \"@mycadet/local-mode/contract\";\n\nfunction publicUrl(value: string): boolean {\n if (/[\\\\\\u0000-\\u0020]/.test(value) || value.startsWith(\"//\")) {\n return false;\n }\n if (\n (!value.startsWith(\"/\") &&\n !/^https?:\\/\\//i.test(value) &&\n value.includes(\":\")) ||\n /(?:^|\\/)(?:\\.|%2e){1,2}(?:\\/|$)/i.test(value)\n ) {\n return false;\n }\n try {\n const url = new URL(value, \"https://app.example\");\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n return (\n value.startsWith(\"/\") ||\n url.protocol === \"https:\" ||\n (url.protocol === \"http:\" &&\n [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname))\n );\n } catch {\n return false;\n }\n}\n\nexport const PublicUrlSchema = z\n .string()\n .min(1)\n .refine(\n publicUrl,\n \"Expected a public host URL without credentials or query parameters.\",\n );\nexport const ResolvedAssistantSchema = z.object({\n id: z.string().min(1),\n version: z.string().min(1),\n});\n\nexport const AssistantChoiceSchema = z.object({\n id: z.string().min(1),\n name: z\n .string()\n .nullish()\n .transform((name) => name?.trim() || \"New Assistant\"),\n /** The cloud's synced avatar thumbnail; null means the cloud holds none. */\n avatar_url: z.string().nullish(),\n /** When the cloud hatched the assistant (ISO 8601); null when unknown. */\n created: z.string().nullish(),\n /**\n * The assistant's own hosting kind, stamped per row by the serving control\n * plane — the cloud lists its assistants \"cloud\", a self-hosted edge\n * stamps its lockfile cloud. Unknown or absent values (servers predating\n * the stamp, a hostile row) fall back to undefined; the chooser then uses\n * the build's own remote.\n */\n remote: RemoteSchema.nullish().catch(() => undefined),\n});\n\nexport const AssistantResolutionSchema = z.discriminatedUnion(\"status\", [\n z.object({ status: z.literal(\"ready\"), assistant: ResolvedAssistantSchema }),\n z.object({ status: z.literal(\"notReady\") }),\n z.object({ status: z.literal(\"unavailable\") }),\n // Selection carries no rows by design — the rows are the list contract's;\n // this arm just sends the shell to the selection page.\n z.object({ status: z.literal(\"selection\") }),\n z.object({ status: z.literal(\"empty\") }),\n z.object({ status: z.literal(\"unauthenticated\") }),\n]);\n/**\n * What the list endpoint alone can answer: the choices on the machine\n * (nothing hatched is the empty arm), or no session to read them with. The chooser renders off this contract only —\n * the single-assistant resolution arms (ready/notReady/unavailable/empty)\n * belong to the shell's resolution flow, whose own selection arm carries no\n * rows — it routes here.\n */\nexport const AssistantListSchema = z.discriminatedUnion(\"status\", [\n z.object({\n status: z.literal(\"selection\"),\n assistants: z.array(AssistantChoiceSchema),\n }),\n z.object({ status: z.literal(\"empty\") }),\n z.object({ status: z.literal(\"unauthenticated\") }),\n]);\nexport type ResolvedAssistant = z.infer<typeof ResolvedAssistantSchema>;\nexport type AssistantResolution = z.infer<typeof AssistantResolutionSchema>;\nexport type AssistantList = z.infer<typeof AssistantListSchema>;\nexport const AssistantNavigationSchema = z.object({\n source: z.literal(\"mycadet-web-nav\"),\n nav: z.enum([\"push\", \"replace\", \"pop\"]),\n path: z.string(),\n});\n\n/**\n * The frame's HOST-navigation request: the assistant bundle opening a\n * surface outside its own url space (the account area — the Doctor page).\n * The frame listener allowlists `/account/*`; the schema only fixes the\n * message's shape.\n */\nexport const HostNavigationSchema = z.object({\n source: z.literal(\"mycadet-web-host-nav\"),\n path: z.string(),\n});\n"],"mappings":"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"}
|
package/dist/standalone/assets/{assistant-selection-BHSWVVGf.js → assistant-selection-orv5LlBm.js}
RENAMED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./jsx-runtime-pmtNMKr4.js";import{r as t}from"./i18n-ZwJySRVD.js";import{c as n}from"./dist-CbweIl-Y.js";import{t as r}from"./confirm-dialog-DREbxzQ6.js";import{l as i,p as a}from"./schemas-D0jfLcVm.js";import{r as o}from"./api-fQ3mgIhr.js";import{i as s,r as c}from"./pairing-address-
|
|
2
|
-
//# sourceMappingURL=assistant-selection-
|
|
1
|
+
import{t as e}from"./jsx-runtime-pmtNMKr4.js";import{r as t}from"./i18n-ZwJySRVD.js";import{c as n}from"./dist-CbweIl-Y.js";import{t as r}from"./confirm-dialog-DREbxzQ6.js";import{l as i,p as a}from"./schemas-D0jfLcVm.js";import{r as o}from"./api-fQ3mgIhr.js";import{i as s,r as c}from"./pairing-address-CmS8dvFv.js";var l=n(`cloud-off`,[[`path`,{d:`M10.94 5.274A7 7 0 0 1 15.71 10h1.79a4.5 4.5 0 0 1 4.222 6.057`,key:`1uxyv8`}],[`path`,{d:`M18.796 18.81A4.5 4.5 0 0 1 17.5 19H9A7 7 0 0 1 5.79 5.78`,key:`99tcn7`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]),u=e();function d({open:e,error:n,isPending:i,onConfirm:a,onCancel:o}){let{t:s}=t();return(0,u.jsx)(r,{open:e,title:s(`retireConfirmDialog.title`),message:s(`retireConfirmDialog.message`),error:n,confirmLabel:s(`retireConfirmDialog.confirmLabel`),destructive:!0,isPending:i,onConfirm:a,onCancel:o})}async function f(e){let t=await o(e);return!t.ok&&t.status!==404?{ok:!1,error:t.error.detail}:(s()===e&&c(),{ok:!0})}var p=i({assistantSelection:i({id:a().min(1),apiBase:a().min(1).optional()})});function m(e,t){return{assistantSelection:t?{id:e,apiBase:t}:{id:e}}}function h(e){let t=p.safeParse(e);return t.success?t.data.assistantSelection:void 0}export{l as a,d as i,h as n,f as r,m as t};
|
|
2
|
+
//# sourceMappingURL=assistant-selection-orv5LlBm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-selection-
|
|
1
|
+
{"version":3,"file":"assistant-selection-orv5LlBm.js","names":[],"sources":["../../../../../node_modules/.bun/lucide-react@1.16.0+d86b59289c1a13ae/node_modules/lucide-react/dist/esm/icons/cloud-off.mjs","../../../src/components/retire-confirm-dialog.tsx","../../../src/assistant/retire-assistant.ts","../../../src/assistant/assistant-selection.ts"],"sourcesContent":["/**\n * @license lucide-react v1.16.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst __iconNode = [\n [\"path\", { d: \"M10.94 5.274A7 7 0 0 1 15.71 10h1.79a4.5 4.5 0 0 1 4.222 6.057\", key: \"1uxyv8\" }],\n [\"path\", { d: \"M18.796 18.81A4.5 4.5 0 0 1 17.5 19H9A7 7 0 0 1 5.79 5.78\", key: \"99tcn7\" }],\n [\"path\", { d: \"m2 2 20 20\", key: \"1ooewy\" }]\n];\nconst CloudOff = createLucideIcon(\"cloud-off\", __iconNode);\n\nexport { __iconNode, CloudOff as default };\n//# sourceMappingURL=cloud-off.mjs.map\n","import { ConfirmDialog } from \"@mycadet/design-library/components/confirm-dialog\";\nimport { useTranslation } from \"@app/i18n\";\n\n/**\n * Canonical destructive confirmation for retiring an assistant — port of\n * web's `RetireConfirmDialog`, so the irreversible-action warning reads the\n * same on every surface that retires through it.\n */\nexport function RetireConfirmDialog({\n open,\n error,\n isPending,\n onConfirm,\n onCancel,\n}: {\n open: boolean;\n /** Inline failure line; the dialog stays open so the user can retry. */\n error?: string | null;\n isPending: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n const { t } = useTranslation();\n return (\n <ConfirmDialog\n open={open}\n title={t(\"retireConfirmDialog.title\")}\n message={t(\"retireConfirmDialog.message\")}\n error={error}\n confirmLabel={t(\"retireConfirmDialog.confirmLabel\")}\n destructive\n isPending={isPending}\n onConfirm={onConfirm}\n onCancel={onCancel}\n />\n );\n}\n","import { retireAssistantById } from \"@app/assistant/api\";\nimport {\n clearSelectedAssistantId,\n readSelectedAssistantId,\n} from \"@app/assistant/selected-assistant-storage\";\n\nexport type RetireAssistantResult = { ok: true } | { ok: false; error: string };\n\n/**\n * Permanently retire an assistant through the scoped surface's one retire\n * endpoint, for every entry type: the request is same-origin, and the serving\n * host decides how to honor it (the desktop proxy and the dev server run the\n * host CLI for on-machine entries; the cloud retires platform rows). The\n * bundle never branches on entry type.\n *\n * A 404 means the assistant is already gone — the same success web's\n * retire-service granted, so a stale chooser row still clears instead of\n * wedging the dialog on an error nothing can fix. Retiring the stored\n * selection drops it, mirroring web's active-id cleanup: without that, the\n * shell could admit a retired assistant with nothing behind it.\n */\nexport async function retireAssistant(\n assistantId: string,\n): Promise<RetireAssistantResult> {\n const result = await retireAssistantById(assistantId);\n if (!result.ok && result.status !== 404) {\n return { ok: false, error: result.error.detail };\n }\n if (readSelectedAssistantId() === assistantId) {\n clearSelectedAssistantId();\n }\n return { ok: true };\n}\n","import { z } from \"zod\";\n\nconst SelectionStateSchema = z.object({\n assistantSelection: z.object({\n id: z.string().min(1),\n /**\n * Same-origin API base for entries the default same-origin `/v1`\n * resolution cannot serve: a paired assistant's gateway rides the\n * shell's paired-gateway path (the lockfile entry allowlists it and\n * the shell injects its guardian credential server-side). Absent for\n * every ordinary selection, which resolves same-origin as before.\n */\n apiBase: z.string().min(1).optional(),\n }),\n});\n\n/**\n * A chosen assistant rides react-router location state between the account\n * chooser and the shell. History state survives reloads and restarts, so\n * malformed state must never resolve to a selection. Selections carry no\n * scope tag: the shell resolves assistants same-origin, and history state\n * never crosses origins to begin with.\n */\nexport function assistantSelectionState(id: string, apiBase?: string) {\n return {\n assistantSelection: apiBase ? { id, apiBase } : { id },\n };\n}\n\nexport function selectedAssistantState(\n state: unknown,\n): { id: string; apiBase?: string } | undefined {\n const parsed = SelectionStateSchema.safeParse(state);\n return parsed.success ? parsed.data.assistantSelection : undefined;\n}\n\nexport function selectedAssistantId(state: unknown): string | undefined {\n return selectedAssistantState(state)?.id;\n}\n"],"x_google_ignoreList":[0],"mappings":"6TAcA,IAAM,EAAW,EAAiB,YAAa,CAJ7C,CAAC,OAAQ,CAAE,EAAG,iEAAkE,IAAK,SAAU,CAAC,CAChG,CAAC,OAAQ,CAAE,EAAG,4DAA6D,IAAK,SAAU,CAAC,CAC3F,CAAC,OAAQ,CAAE,EAAG,aAAc,IAAK,SAAU,CAAC,CAEC,CAAW,OCN1D,SAAgB,EAAoB,CAClC,OACA,QACA,YACA,YACA,YAQC,CACD,GAAM,CAAE,KAAM,GAAgB,CAC9B,OACE,EAAA,EAAA,KAAC,EAAD,CACQ,OACN,MAAO,EAAE,4BAA4B,CACrC,QAAS,EAAE,8BAA8B,CAClC,QACP,aAAc,EAAE,mCAAmC,CACnD,YAAA,GACW,YACA,YACD,WACV,CAAA,CCbN,eAAsB,EACpB,EACgC,CAChC,IAAM,EAAS,MAAM,EAAoB,EAAY,CAOrD,MANI,CAAC,EAAO,IAAM,EAAO,SAAW,IAC3B,CAAE,GAAI,GAAO,MAAO,EAAO,MAAM,OAAQ,EAE9C,GAAyB,GAAK,GAChC,GAA0B,CAErB,CAAE,GAAI,GAAM,EC7BrB,IAAM,EAAuB,EAAS,CACpC,mBAAoB,EAAS,CAC3B,GAAI,GAAU,CAAC,IAAI,EAAE,CAQrB,QAAS,GAAU,CAAC,IAAI,EAAE,CAAC,UAAU,CACtC,CAAC,CACH,CAAC,CASF,SAAgB,EAAwB,EAAY,EAAkB,CACpE,MAAO,CACL,mBAAoB,EAAU,CAAE,KAAI,UAAS,CAAG,CAAE,KAAI,CACvD,CAGH,SAAgB,EACd,EAC8C,CAC9C,IAAM,EAAS,EAAqB,UAAU,EAAM,CACpD,OAAO,EAAO,QAAU,EAAO,KAAK,mBAAqB,IAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e}from"./chunk-BNv3lrIs.js";import{n as t,t as n}from"./jsx-runtime-pmtNMKr4.js";import{t as r}from"./button-CQ38Vc-C.js";import{c as i,u as a}from"./chunk-5KNZJZUH-D6xeNlfl.js";import{r as o}from"./i18n-ZwJySRVD.js";import{a as s}from"./pairing-address-
|
|
2
|
-
//# sourceMappingURL=hatching-screen
|
|
1
|
+
import{a as e}from"./chunk-BNv3lrIs.js";import{n as t,t as n}from"./jsx-runtime-pmtNMKr4.js";import{t as r}from"./button-CQ38Vc-C.js";import{c as i,u as a}from"./chunk-5KNZJZUH-D6xeNlfl.js";import{r as o}from"./i18n-ZwJySRVD.js";import{a as s}from"./pairing-address-CmS8dvFv.js";import{n as c}from"./routes-D7UumuSh.js";import{n as l,o as u,t as d}from"./avatar-random-Buv9zswo.js";import{n as f,o as p}from"./resolve-assistant-9Jmr90l9.js";import{t as m}from"./progress-bar-a0v_IKm1.js";import{t as h}from"./account-shell-CIG3rMlQ.js";import{t as g}from"./onboarding-destination-CcmQ9Jul.js";import{t as _}from"./onboarding-navigation-I4NW12wA.js";import{t as v}from"./avatar-bundled-components-DiV0VKml.js";var y=e(t(),1),b=`Pairing failed. Please try again.`,x=null,S;function C(){x=null,S=void 0}async function w(e){return x&&S!==e&&C(),x||=(S=e,T(e).then(e=>(e.ok||C(),e))),x}async function T(e){try{let t=await(await fetch(`/v1/assistants/hatch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({remote:e})})).json().catch(()=>null);return t?.ok?{ok:!0,assistantId:t.assistantId}:{ok:!1,error:t?.error??b}}catch{return{ok:!1,error:b}}}var E=n(),D=800,O=3e3,k=3e5,A=null;function j(){A=null}var M={initializing:0,provisioning:.33,connecting:.66,ready:1},N=1500,P={initializing:`hatchingScreen.phase.initializing`,provisioning:`hatchingScreen.phase.provisioning`,connecting:`hatchingScreen.phase.connecting`,ready:`hatchingScreen.phase.ready`};function F(e,t,n){if(e>=t)return t;let r=1-(1-Math.min(1,n/N))**3;return e+(t-e)*r}function I(){let{t:e}=o(`account`),t=i(),[n]=a(),b=p.safeParse(n.get(`hosting`)).data,x=n.get(`fail`),[S]=(0,y.useState)(()=>A??=d(v)),T=(0,y.useMemo)(()=>{let e=l(v,S.bodyShape,S.eyeStyle,S.color,320);return e?`data:image/svg+xml;charset=utf-8,${encodeURIComponent(e)}`:``},[S]),[N,I]=(0,y.useState)(`initializing`),[L,R]=(0,y.useState)(null),[z,B]=(0,y.useState)(0),[V,H]=(0,y.useState)(0),[U,W]=(0,y.useState)(0),G=(0,y.useRef)(N),K=(0,y.useRef)(0),q=(0,y.useRef)(0),J=(0,y.useRef)(0),Y=(0,y.useCallback)(e=>{K.current=J.current,q.current=Date.now(),G.current=e,I(e),W(e=>e+1)},[]);return(0,y.useEffect)(()=>{if(x!==null){R(`Simulated hatch failure (developer menu → Replay Hatch Failure).`);return}let e=!1,n=null,r=null,i=Date.now(),a=r=>{r&&u(r,S),H(1),J.current=1,K.current=1,I(`ready`),G.current=`ready`,n=setTimeout(()=>{if(!e){if(C(),j(),b&&b!==`cloud`){let e=new URLSearchParams({hosting:b});r&&e.set(`assistant`,r),t(`${c.onboarding.research}?${e.toString()}`,_);return}t(c.onboarding.research,_)}},D)};return(async()=>{Y(`provisioning`);try{let t=await w(b??`local`);if(e)return;if(!t.ok){j(),R(t.error??`Failed to hatch your assistant.`);return}if(t.assistantId&&s(t.assistantId),Y(`connecting`),t.assistantId){let t=!1;for(;!e&&!t;){if((await f({signal:AbortSignal.timeout(1e4)}).catch(()=>null))?.status===`ready`){t=!0;break}if(Date.now()-i>=k){C(),j(),R(`Your assistant is taking longer than expected. Please try again.`);return}await new Promise(e=>{r=setTimeout(e,O)}),r=null}if(e)return}a(t.assistantId)}catch{if(j(),e)return;R(`Failed to hatch your assistant. Please try again.`)}})(),()=>{e=!0,n&&clearTimeout(n),r&&clearTimeout(r)}},[z,x,S,b,t,Y]),(0,y.useEffect)(()=>{q.current===0&&(q.current=Date.now());let e,t=()=>{let n=Date.now()-q.current,r=M[G.current],i=F(K.current,r,n);J.current=i,H(i),r-i>1e-6&&(e=requestAnimationFrame(t))};return e=requestAnimationFrame(t),()=>cancelAnimationFrame(e)},[U]),L?(0,E.jsx)(h,{children:(0,E.jsxs)(`div`,{role:`alert`,className:`flex w-full flex-col items-center text-center text-[var(--content-default)]`,children:[(0,E.jsx)(`h1`,{className:`text-3xl font-semibold tracking-tight`,children:e(`hatchingScreen.genericFailure`)}),(0,E.jsx)(`p`,{className:`mt-3 text-body-medium-lighter text-[var(--content-tertiary)]`,children:L}),(0,E.jsx)(`img`,{src:T,alt:``,width:160,height:160,className:`my-10`}),(0,E.jsxs)(`div`,{className:`flex w-full flex-col gap-2`,children:[(0,E.jsx)(r,{variant:`primary`,size:`large`,fullWidth:!0,onClick:()=>{K.current=0,q.current=Date.now(),G.current=`initializing`,J.current=0,I(`initializing`),H(0),W(e=>e+1),R(null),B(e=>e+1)},children:e(`actions.tryAgain`)}),(0,E.jsx)(r,{variant:`outlined`,size:`large`,fullWidth:!0,onClick:()=>{t(b&&b!==`cloud`?c.account.onboarding.hosting:`${c.onboarding.research}?${g}=1`,_)},children:e(`actions.back`)})]})]})}):(0,E.jsx)(h,{children:(0,E.jsxs)(`div`,{className:`flex w-full flex-col items-center text-center text-[var(--content-default)]`,children:[(0,E.jsx)(`h1`,{className:`text-3xl font-semibold tracking-tight`,children:e(N===`ready`?`hatchingScreen.ready`:`hatchingScreen.waking`)}),N!==`ready`&&(0,E.jsx)(`p`,{className:`mt-3 text-body-medium-lighter text-[var(--content-tertiary)]`,children:e(`hatchingScreen.wakingBody`)}),(0,E.jsx)(`img`,{src:T,alt:``,width:160,height:160,className:`my-10 ${N===`ready`?``:`animate-pulse`}`}),(0,E.jsx)(m,{value:V,height:6,className:`w-full`,"aria-label":e(`hatchingScreen.progressAriaLabel`)}),(0,E.jsx)(`p`,{className:`mt-3 text-body-small-default text-[var(--content-tertiary)]`,children:e(P[N])})]})})}export{I as HatchingScreen};
|
|
2
|
+
//# sourceMappingURL=hatching-screen--FhKffez.js.map
|
package/dist/standalone/assets/{hatching-screen-CszlzCVf.js.map → hatching-screen--FhKffez.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hatching-screen-CszlzCVf.js","names":[],"sources":["../../../src/assistant/operations.ts","../../../src/domains/onboarding/pages/hatching-screen.tsx"],"sourcesContent":["/**\n * The app bundle's assistant operations: typed wrappers over the same-origin\n * `/v1` routes the serving host answers through its operator route table —\n * the Electron shell's `app://` handler (mounted ahead of the gateway\n * data-plane forward) and, on dev, the Vite dev middleware. The operator\n * backend proxies to the cloud or the local machine as each route decides,\n * so nothing here knows or cares which one is behind the answer.\n *\n * The pairing exchange itself runs in the serving host's trusted process (the\n * shared host library the web dev server and the CLI web interface also use):\n * the device code and the credentials it buys never cross this boundary — the\n * renderer holds only the opaque session handle.\n *\n * Leaf module: must not import from chooser/screen modules.\n */\n\nimport type {\n LocalPairingPollResult,\n LocalPairingStartResult,\n} from \"@mycadet/ipc-contract\";\nimport type { LockfileWriteResult } from \"@mycadet/local-mode/contract\";\nimport { isPublicBaseUrlRejection } from \"@app/utils/pairing-address\";\n\n/** Fallback when a host returns no usable message of its own. */\nconst PAIRING_FALLBACK_ERROR = \"Pairing failed. Please try again.\";\n\nfunction hostError(result: { error?: string }): string {\n return result.error || PAIRING_FALLBACK_ERROR;\n}\n\n/**\n * Begin pairing with the assistant at `address`, a pairing link copied\n * from its \"Pair a device\" card or a bare `https://host` URL. The device\n * code stays in the main process; the reply is the opaque `handle` plus,\n * when the address carried no approved code of its own, the code to\n * approve on the assistant's machine.\n */\nexport async function startAssistantPairing(\n address: string,\n): Promise<LocalPairingStartResult> {\n let result: LocalPairingStartResult | { ok: false; error?: string };\n try {\n const res = await fetch(\"/v1/remote-web/pairing-start\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ address }),\n });\n result = (await res.json().catch(() => null)) ?? { ok: false };\n } catch {\n result = { ok: false };\n }\n if (!result.ok) {\n // Runtime guard: the rejection rides an untyped host response, so a\n // reason no caller has copy for degrades to the host's own message.\n const rejection =\n \"rejection\" in result && isPublicBaseUrlRejection(result.rejection)\n ? result.rejection\n : undefined;\n return { ...result, error: hostError(result), rejection };\n }\n return result;\n}\n\n/**\n * One exchange attempt for a live pairing session. Transport only — an\n * `imported` answer has already persisted the pairing host-side; the caller\n * reloads its own view of the roster before resolving.\n */\nexport async function pollAssistantPairing(\n handle: string,\n name?: string,\n): Promise<LocalPairingPollResult> {\n let result: LocalPairingPollResult | { ok: false; error?: string };\n try {\n const res = await fetch(\"/v1/remote-web/pairing-poll\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handle, name }),\n });\n result = (await res.json().catch(() => null)) ?? { ok: false };\n } catch {\n result = { ok: false };\n }\n if (!result.ok) {\n return { ...result, error: hostError(result) };\n }\n if (result.status === \"pending\") {\n return result;\n }\n if (!result.assistantId) {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n return { ...result, accessOnly: result.accessOnly === true };\n}\n\n/**\n * Forget a pending pairing session, so its code cannot be exchanged later.\n * A handle the host no longer knows is already the outcome wanted, so a\n * rejection resolves quietly.\n */\nexport async function cancelAssistantPairing(handle: string): Promise<void> {\n try {\n await fetch(\"/v1/remote-web/pairing-cancel\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handle }),\n });\n } catch {\n // Unreachable session: cancelled, spent, or the host itself is gone.\n }\n}\n\n/** Remove a paired entry and its stored guardian token from this device. */\nexport async function unpairAssistant(\n assistantId: string,\n): Promise<LockfileWriteResult> {\n try {\n const res = await fetch(\n `/v1/assistants/${encodeURIComponent(assistantId)}/unpair`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({}),\n },\n );\n return (\n (await res.json().catch(() => null)) ??\n ({ ok: false, error: PAIRING_FALLBACK_ERROR } as LockfileWriteResult)\n );\n } catch {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n}\n\n/**\n * Provision an assistant through the serving host's hatch operation: the\n * host runs the Cadet CLI in its trusted process and returns the same\n * `{ ok, assistantId }` contract the CLI's own hatch reports; callers read\n * the resolution endpoint to discover the new assistant regardless. (The\n * platform's own hatch rides the app's cloud API.)\n *\n * The hatch coalesces: concurrent and remount-driven callers await the SAME\n * in-flight (or already-resolved) hatch instead of starting over, so HMR\n * remounts, StrictMode double-mounts, and the shell's session-driven\n * remounts never spawn duplicate hatches. A held promise answers only for\n * the SAME remote — the guard can outlive a trip through the hosting screen,\n * where the user may switch modes, and the abandoned mode's assistant must\n * not be adopted for the new choice. The guard releases on failure (a retry\n * re-hatches) and via `releaseHatchGuard` — the caller knows when the\n * handoff is genuinely done, and a later onboarding session must hatch a\n * brand-new assistant instead of reusing a resolved one.\n */\nlet hatchPromise: Promise<HatchAssistantResult> | null = null;\nlet hatchRemote: string | undefined;\n\nexport function releaseHatchGuard(): void {\n hatchPromise = null;\n hatchRemote = undefined;\n}\n\ntype HatchAssistantResult = {\n ok: boolean;\n assistantId?: string;\n error?: string;\n};\n\nexport async function hatchAssistant(\n remote?: string,\n): Promise<HatchAssistantResult> {\n if (hatchPromise && hatchRemote !== remote) {\n releaseHatchGuard();\n }\n if (!hatchPromise) {\n hatchRemote = remote;\n hatchPromise = requestHatch(remote).then((result) => {\n if (!result.ok) {\n releaseHatchGuard();\n }\n return result;\n });\n }\n return hatchPromise;\n}\n\nasync function requestHatch(remote?: string): Promise<HatchAssistantResult> {\n try {\n const res = await fetch(\"/v1/assistants/hatch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ remote }),\n });\n const parsed = (await res.json().catch(() => null)) as {\n ok?: boolean;\n assistantId?: string;\n error?: string;\n } | null;\n return parsed?.ok\n ? { ok: true, assistantId: parsed.assistantId }\n : { ok: false, error: parsed?.error ?? PAIRING_FALLBACK_ERROR };\n } catch {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useNavigate, useSearchParams } from \"react-router\";\n\nimport { seedHatchAvatar } from \"@app/assistant/avatar-traits\";\nimport { hatchAssistant, releaseHatchGuard } from \"@app/assistant/operations\";\nimport { resolveAssistant } from \"@app/assistant/resolve-assistant\";\nimport { writeSelectedAssistantId } from \"@app/assistant/selected-assistant-storage\";\nimport { AccountShell } from \"@app/components/account/account-shell\";\nimport { NEW_ASSISTANT_PARAM } from \"@app/domains/onboarding/onboarding-destination\";\nimport { SETUP_NAVIGATE } from \"@app/domains/onboarding/onboarding-navigation\";\nimport { useTranslation } from \"@app/i18n\";\nimport { routes } from \"@app/utils/routes\";\nimport { Button } from \"@mycadet/design-library/components/button\";\nimport { ProgressBar } from \"@mycadet/design-library/components/progress-bar\";\nimport { RemoteSchema } from \"@mycadet/local-mode/contract\";\nimport type { CharacterTraits } from \"@app/types/avatar\";\nimport { BUNDLED_COMPONENTS } from \"@app/utils/avatar-bundled-components\";\nimport { randomCharacterTraits } from \"@app/utils/avatar-random\";\nimport { composeSvg } from \"@app/utils/avatar-svg-compositor\";\n\nconst COMPLETION_NAVIGATE_DELAY_MS = 800;\n\n// Web's hatch-wait constants, inlined until the state core re-home gives them\n// a shared package home. A local hatch waits out the CLI's background\n// daemon+gateway startup before handing off.\nconst POLL_INTERVAL_MS = 3000;\nconst MAX_HATCH_WAIT_MS = 300_000;\n\ntype HatchResult = Awaited<ReturnType<typeof hatchAssistant>>;\n\n// The hatch promise's coalescing lives in the operation (module-level there);\n// this screen holds only the avatar traits so every remount riding the same\n// hatch renders the SAME assistant. Released on failure (a retry regenerates)\n// and on genuine completion — never in the window between the hatch resolving\n// and the screen navigating away.\nlet hatchTraitsCache: CharacterTraits | null = null;\n\nfunction releaseHatchTraits(): void {\n hatchTraitsCache = null;\n}\n\ntype HatchPhase = \"initializing\" | \"provisioning\" | \"connecting\" | \"ready\";\n\nconst PHASE_TARGET: Record<HatchPhase, number> = {\n initializing: 0,\n provisioning: 0.33,\n connecting: 0.66,\n ready: 1.0,\n};\n\nconst SEGMENT_DURATION_MS = 1500;\n\n// Written out per phase rather than composed from the phase name, so a phase\n// added without its copy fails to compile and the key stays greppable for the\n// orphan check in the catalog tests.\nconst PHASE_KEY: Record<HatchPhase, `hatchingScreen.phase.${HatchPhase}`> = {\n initializing: \"hatchingScreen.phase.initializing\",\n provisioning: \"hatchingScreen.phase.provisioning\",\n connecting: \"hatchingScreen.phase.connecting\",\n ready: \"hatchingScreen.phase.ready\",\n};\n\nexport function interpolateSegmentProgress(\n segmentStart: number,\n target: number,\n elapsedMs: number,\n): number {\n if (segmentStart >= target) {\n return target;\n }\n const t = Math.min(1.0, elapsedMs / SEGMENT_DURATION_MS);\n const eased = 1.0 - Math.pow(1.0 - t, 3.0);\n return segmentStart + (target - segmentStart) * eased;\n}\n\n/**\n * The funnel's provisioning step, ported from web's\n * `/assistant/onboarding/hatching` — now `/account/onboarding/hatching`,\n * rendered with the account screens' compact chrome.\n *\n * The hatch rides the app bundle's one hatch api — POST /v1/assistants/hatch\n * with the funnel's hosting choice as the remote param. The hatch operation\n * discriminates (local, docker, cloud), so this screen never branches on the\n * mode: it waits the answer out, polls the resolution endpoint until the\n * hatched assistant answers, and hands off to research. The pending provider\n * key stays staged for the downstream apply (same sessionStorage, same\n * origin), and the shell's resolution owns the not-ready states afterwards.\n */\nexport function HatchingScreen() {\n const { t } = useTranslation(\"account\");\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n // The funnel's hosting choice IS a Remote (the hosting screen emits the\n // contract's spelling) — validate the param and ride it as the hatch's\n // remote; anything absent or foreign reads as the local walk.\n const hosting = RemoteSchema.safeParse(searchParams.get(\"hosting\")).data;\n const failParam = searchParams.get(\"fail\");\n const [hatchTraits] = useState<CharacterTraits>(\n () => (hatchTraitsCache ??= randomCharacterTraits(BUNDLED_COMPONENTS)),\n );\n const avatarSvgDataUrl = useMemo(() => {\n const svg = composeSvg(\n BUNDLED_COMPONENTS,\n hatchTraits.bodyShape,\n hatchTraits.eyeStyle,\n hatchTraits.color,\n 320,\n );\n if (!svg) {\n return \"\";\n }\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n }, [hatchTraits]);\n const [phase, setPhase] = useState<HatchPhase>(\"initializing\");\n const [error, setError] = useState<string | null>(null);\n const [attempt, setAttempt] = useState(0);\n const [displayProgress, setDisplayProgress] = useState<number>(0);\n const [animationEpoch, setAnimationEpoch] = useState(0);\n\n const phaseRef = useRef<HatchPhase>(phase);\n const segmentStartRef = useRef(0);\n const segmentStartTimeRef = useRef(0);\n const displayProgressRef = useRef(0);\n\n const transitionPhase = useCallback((next: HatchPhase) => {\n segmentStartRef.current = displayProgressRef.current;\n segmentStartTimeRef.current = Date.now();\n phaseRef.current = next;\n setPhase(next);\n setAnimationEpoch((n) => n + 1);\n }, []);\n\n useEffect(() => {\n // Developer \"Replay Hatch Failure\" tool: when opened with `?fail`, skip\n // the real hatch flow and render the error UI directly so the failure\n // screen can be exercised on demand.\n if (failParam !== null) {\n setError(\n \"Simulated hatch failure (developer menu → Replay Hatch Failure).\",\n );\n return;\n }\n\n let cancelled = false;\n let navigateTimer: ReturnType<typeof setTimeout> | null = null;\n let readyPollTimer: ReturnType<typeof setTimeout> | null = null;\n const pollStartMs = Date.now();\n\n const handleHatchReady = (readyAssistantId?: string) => {\n // Seed the random hatch avatar for the freshly hatched assistant — the\n // D3b flagged gap, re-homed from web's seeding onto the app's scoped\n // surface. Read-then-write (never over an existing sidecar), never\n // awaited: the handoff below must not block on the server-side render,\n // and the research face step overwrites the avatar anyway when the user\n // picks one — this is the dock/tray stopgap during the research window.\n if (readyAssistantId) {\n void seedHatchAvatar(readyAssistantId, hatchTraits);\n }\n setDisplayProgress(1);\n displayProgressRef.current = 1;\n segmentStartRef.current = 1;\n setPhase(\"ready\");\n phaseRef.current = \"ready\";\n navigateTimer = setTimeout(() => {\n if (cancelled) {\n return;\n }\n // The hatch succeeded and we're leaving this screen for good. Release\n // the module-level guards so a later onboarding session hatches a\n // brand-new one instead of reusing this resolved promise and avatar.\n releaseHatchGuard();\n releaseHatchTraits();\n // A local walk carries the hosting choice + the hatched assistant so\n // research adopts exactly this assistant, not whatever a stale\n // selection resolves to (its background hatch resolves the existing\n // local assistant instead of provisioning a cloud one). A cloud walk\n // hands off bare — research owns the managed background hatch.\n if (hosting && hosting !== \"cloud\") {\n const researchParams = new URLSearchParams({ hosting });\n if (readyAssistantId) {\n researchParams.set(\"assistant\", readyAssistantId);\n }\n void navigate(\n `${routes.onboarding.research}?${researchParams.toString()}`,\n SETUP_NAVIGATE,\n );\n return;\n }\n void navigate(routes.onboarding.research, SETUP_NAVIGATE);\n }, COMPLETION_NAVIGATE_DELAY_MS);\n };\n\n const startHatch = async () => {\n transitionPhase(\"provisioning\");\n\n try {\n // The validated hosting choice IS the hatch api's remote param — the\n // hatch operation discriminates (local, docker, cloud), so this\n // screen never branches on the mode.\n const remote = hosting ?? \"local\";\n const result = await hatchAssistant(remote);\n if (cancelled) {\n return;\n }\n if (!result.ok) {\n releaseHatchTraits();\n setError(result.error ?? \"Failed to hatch your assistant.\");\n return;\n }\n if (result.assistantId) {\n writeSelectedAssistantId(result.assistantId);\n }\n // The hatch's answer carries the new assistant; the resolution\n // poll below waits for it to answer before the handoff.\n transitionPhase(\"connecting\");\n if (result.assistantId) {\n let gatewayReady = false;\n while (!cancelled && !gatewayReady) {\n // The resolution endpoint is the lifecycle read: the serving\n // host composes the entry's health and auth into the union, so\n // \"ready\" means the gateway is answering. A probe that cannot\n // settle (transport error, timeout) keeps polling within the\n // deadline below.\n const resolution = await resolveAssistant({\n signal: AbortSignal.timeout(10_000),\n }).catch(() => null);\n if (resolution?.status === \"ready\") {\n gatewayReady = true;\n break;\n }\n if (Date.now() - pollStartMs >= MAX_HATCH_WAIT_MS) {\n // The hatch succeeded but the assistant never went healthy —\n // release the guards so \"Try again\" runs a genuinely fresh\n // hatch.\n releaseHatchGuard();\n releaseHatchTraits();\n setError(\n \"Your assistant is taking longer than expected. Please try again.\",\n );\n return;\n }\n await new Promise<void>((resolve) => {\n readyPollTimer = setTimeout(resolve, POLL_INTERVAL_MS);\n });\n readyPollTimer = null;\n }\n if (cancelled) {\n return;\n }\n }\n handleHatchReady(result.assistantId);\n } catch {\n releaseHatchTraits();\n if (cancelled) {\n return;\n }\n setError(\"Failed to hatch your assistant. Please try again.\");\n }\n };\n\n void startHatch();\n\n return () => {\n cancelled = true;\n if (navigateTimer) {\n clearTimeout(navigateTimer);\n }\n if (readyPollTimer) {\n clearTimeout(readyPollTimer);\n }\n };\n }, [attempt, failParam, hatchTraits, hosting, navigate, transitionPhase]);\n\n useEffect(() => {\n if (segmentStartTimeRef.current === 0) {\n segmentStartTimeRef.current = Date.now();\n }\n let rafId: number;\n const tick = () => {\n const elapsed = Date.now() - segmentStartTimeRef.current;\n const target = PHASE_TARGET[phaseRef.current];\n const value = interpolateSegmentProgress(\n segmentStartRef.current,\n target,\n elapsed,\n );\n displayProgressRef.current = value;\n setDisplayProgress(value);\n if (target - value > 1e-6) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(rafId);\n }, [animationEpoch]);\n\n if (error) {\n return (\n <AccountShell>\n <div\n role=\"alert\"\n className=\"flex w-full flex-col items-center text-center text-[var(--content-default)]\"\n >\n <h1 className=\"text-3xl font-semibold tracking-tight\">\n {t(\"hatchingScreen.genericFailure\")}\n </h1>\n <p className=\"mt-3 text-body-medium-lighter text-[var(--content-tertiary)]\">\n {error}\n </p>\n <img\n src={avatarSvgDataUrl}\n alt=\"\"\n width={160}\n height={160}\n className=\"my-10\"\n />\n <div className=\"flex w-full flex-col gap-2\">\n <Button\n variant=\"primary\"\n size=\"large\"\n fullWidth\n onClick={() => {\n segmentStartRef.current = 0;\n segmentStartTimeRef.current = Date.now();\n phaseRef.current = \"initializing\";\n displayProgressRef.current = 0;\n setPhase(\"initializing\");\n setDisplayProgress(0);\n setAnimationEpoch((n) => n + 1);\n setError(null);\n setAttempt((n) => n + 1);\n }}\n >\n {t(\"actions.tryAgain\")}\n </Button>\n <Button\n variant=\"outlined\"\n size=\"large\"\n fullWidth\n onClick={() => {\n // The hosting choice is the walk's data: a local/docker walk\n // backs up to its hosting step; a cloud walk came through\n // research and returns there.\n const to =\n hosting && hosting !== \"cloud\"\n ? routes.account.onboarding.hosting\n : `${routes.onboarding.research}?${NEW_ASSISTANT_PARAM}=1`;\n void navigate(to, SETUP_NAVIGATE);\n }}\n >\n {t(\"actions.back\")}\n </Button>\n </div>\n </div>\n </AccountShell>\n );\n }\n\n return (\n <AccountShell>\n <div className=\"flex w-full flex-col items-center text-center text-[var(--content-default)]\">\n <h1 className=\"text-3xl font-semibold tracking-tight\">\n {phase === \"ready\"\n ? t(\"hatchingScreen.ready\")\n : t(\"hatchingScreen.waking\")}\n </h1>\n {phase !== \"ready\" && (\n <p className=\"mt-3 text-body-medium-lighter text-[var(--content-tertiary)]\">\n {t(\"hatchingScreen.wakingBody\")}\n </p>\n )}\n <img\n src={avatarSvgDataUrl}\n alt=\"\"\n width={160}\n height={160}\n className={`my-10 ${phase === \"ready\" ? \"\" : \"animate-pulse\"}`}\n />\n <ProgressBar\n value={displayProgress}\n height={6}\n className=\"w-full\"\n aria-label={t(\"hatchingScreen.progressAriaLabel\")}\n />\n <p className=\"mt-3 text-body-small-default text-[var(--content-tertiary)]\">\n {t(PHASE_KEY[phase])}\n </p>\n </div>\n </AccountShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hatching-screen--FhKffez.js","names":[],"sources":["../../../src/assistant/operations.ts","../../../src/domains/onboarding/pages/hatching-screen.tsx"],"sourcesContent":["/**\n * The app bundle's assistant operations: typed wrappers over the same-origin\n * `/v1` routes the serving host answers through its operator route table —\n * the Electron shell's `app://` handler (mounted ahead of the gateway\n * data-plane forward) and, on dev, the Vite dev middleware. The operator\n * backend proxies to the cloud or the local machine as each route decides,\n * so nothing here knows or cares which one is behind the answer.\n *\n * The pairing exchange itself runs in the serving host's trusted process (the\n * shared host library the web dev server and the CLI web interface also use):\n * the device code and the credentials it buys never cross this boundary — the\n * renderer holds only the opaque session handle.\n *\n * Leaf module: must not import from chooser/screen modules.\n */\n\nimport type {\n LocalPairingPollResult,\n LocalPairingStartResult,\n} from \"@mycadet/ipc-contract\";\nimport type { LockfileWriteResult } from \"@mycadet/local-mode/contract\";\nimport { isPublicBaseUrlRejection } from \"@app/utils/pairing-address\";\n\n/** Fallback when a host returns no usable message of its own. */\nconst PAIRING_FALLBACK_ERROR = \"Pairing failed. Please try again.\";\n\nfunction hostError(result: { error?: string }): string {\n return result.error || PAIRING_FALLBACK_ERROR;\n}\n\n/**\n * Begin pairing with the assistant at `address`, a pairing link copied\n * from its \"Pair a device\" card or a bare `https://host` URL. The device\n * code stays in the main process; the reply is the opaque `handle` plus,\n * when the address carried no approved code of its own, the code to\n * approve on the assistant's machine.\n */\nexport async function startAssistantPairing(\n address: string,\n): Promise<LocalPairingStartResult> {\n let result: LocalPairingStartResult | { ok: false; error?: string };\n try {\n const res = await fetch(\"/v1/remote-web/pairing-start\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ address }),\n });\n result = (await res.json().catch(() => null)) ?? { ok: false };\n } catch {\n result = { ok: false };\n }\n if (!result.ok) {\n // Runtime guard: the rejection rides an untyped host response, so a\n // reason no caller has copy for degrades to the host's own message.\n const rejection =\n \"rejection\" in result && isPublicBaseUrlRejection(result.rejection)\n ? result.rejection\n : undefined;\n return { ...result, error: hostError(result), rejection };\n }\n return result;\n}\n\n/**\n * One exchange attempt for a live pairing session. Transport only — an\n * `imported` answer has already persisted the pairing host-side; the caller\n * reloads its own view of the roster before resolving.\n */\nexport async function pollAssistantPairing(\n handle: string,\n name?: string,\n): Promise<LocalPairingPollResult> {\n let result: LocalPairingPollResult | { ok: false; error?: string };\n try {\n const res = await fetch(\"/v1/remote-web/pairing-poll\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handle, name }),\n });\n result = (await res.json().catch(() => null)) ?? { ok: false };\n } catch {\n result = { ok: false };\n }\n if (!result.ok) {\n return { ...result, error: hostError(result) };\n }\n if (result.status === \"pending\") {\n return result;\n }\n if (!result.assistantId) {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n return { ...result, accessOnly: result.accessOnly === true };\n}\n\n/**\n * Forget a pending pairing session, so its code cannot be exchanged later.\n * A handle the host no longer knows is already the outcome wanted, so a\n * rejection resolves quietly.\n */\nexport async function cancelAssistantPairing(handle: string): Promise<void> {\n try {\n await fetch(\"/v1/remote-web/pairing-cancel\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handle }),\n });\n } catch {\n // Unreachable session: cancelled, spent, or the host itself is gone.\n }\n}\n\n/** Remove a paired entry and its stored guardian token from this device. */\nexport async function unpairAssistant(\n assistantId: string,\n): Promise<LockfileWriteResult> {\n try {\n const res = await fetch(\n `/v1/assistants/${encodeURIComponent(assistantId)}/unpair`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({}),\n },\n );\n return (\n (await res.json().catch(() => null)) ??\n ({ ok: false, error: PAIRING_FALLBACK_ERROR } as LockfileWriteResult)\n );\n } catch {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n}\n\n/**\n * Provision an assistant through the serving host's hatch operation: the\n * host runs the Cadet CLI in its trusted process and returns the same\n * `{ ok, assistantId }` contract the CLI's own hatch reports; callers read\n * the resolution endpoint to discover the new assistant regardless. (The\n * platform's own hatch rides the app's cloud API.)\n *\n * The hatch coalesces: concurrent and remount-driven callers await the SAME\n * in-flight (or already-resolved) hatch instead of starting over, so HMR\n * remounts, StrictMode double-mounts, and the shell's session-driven\n * remounts never spawn duplicate hatches. A held promise answers only for\n * the SAME remote — the guard can outlive a trip through the hosting screen,\n * where the user may switch modes, and the abandoned mode's assistant must\n * not be adopted for the new choice. The guard releases on failure (a retry\n * re-hatches) and via `releaseHatchGuard` — the caller knows when the\n * handoff is genuinely done, and a later onboarding session must hatch a\n * brand-new assistant instead of reusing a resolved one.\n */\nlet hatchPromise: Promise<HatchAssistantResult> | null = null;\nlet hatchRemote: string | undefined;\n\nexport function releaseHatchGuard(): void {\n hatchPromise = null;\n hatchRemote = undefined;\n}\n\ntype HatchAssistantResult = {\n ok: boolean;\n assistantId?: string;\n error?: string;\n};\n\nexport async function hatchAssistant(\n remote?: string,\n): Promise<HatchAssistantResult> {\n if (hatchPromise && hatchRemote !== remote) {\n releaseHatchGuard();\n }\n if (!hatchPromise) {\n hatchRemote = remote;\n hatchPromise = requestHatch(remote).then((result) => {\n if (!result.ok) {\n releaseHatchGuard();\n }\n return result;\n });\n }\n return hatchPromise;\n}\n\nasync function requestHatch(remote?: string): Promise<HatchAssistantResult> {\n try {\n const res = await fetch(\"/v1/assistants/hatch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ remote }),\n });\n const parsed = (await res.json().catch(() => null)) as {\n ok?: boolean;\n assistantId?: string;\n error?: string;\n } | null;\n return parsed?.ok\n ? { ok: true, assistantId: parsed.assistantId }\n : { ok: false, error: parsed?.error ?? PAIRING_FALLBACK_ERROR };\n } catch {\n return { ok: false, error: PAIRING_FALLBACK_ERROR };\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useNavigate, useSearchParams } from \"react-router\";\n\nimport { seedHatchAvatar } from \"@app/assistant/avatar-traits\";\nimport { hatchAssistant, releaseHatchGuard } from \"@app/assistant/operations\";\nimport { resolveAssistant } from \"@app/assistant/resolve-assistant\";\nimport { writeSelectedAssistantId } from \"@app/assistant/selected-assistant-storage\";\nimport { AccountShell } from \"@app/components/account/account-shell\";\nimport { NEW_ASSISTANT_PARAM } from \"@app/domains/onboarding/onboarding-destination\";\nimport { SETUP_NAVIGATE } from \"@app/domains/onboarding/onboarding-navigation\";\nimport { useTranslation } from \"@app/i18n\";\nimport { routes } from \"@app/utils/routes\";\nimport { Button } from \"@mycadet/design-library/components/button\";\nimport { ProgressBar } from \"@mycadet/design-library/components/progress-bar\";\nimport { RemoteSchema } from \"@mycadet/local-mode/contract\";\nimport type { CharacterTraits } from \"@app/types/avatar\";\nimport { BUNDLED_COMPONENTS } from \"@app/utils/avatar-bundled-components\";\nimport { randomCharacterTraits } from \"@app/utils/avatar-random\";\nimport { composeSvg } from \"@app/utils/avatar-svg-compositor\";\n\nconst COMPLETION_NAVIGATE_DELAY_MS = 800;\n\n// Web's hatch-wait constants, inlined until the state core re-home gives them\n// a shared package home. A local hatch waits out the CLI's background\n// daemon+gateway startup before handing off.\nconst POLL_INTERVAL_MS = 3000;\nconst MAX_HATCH_WAIT_MS = 300_000;\n\ntype HatchResult = Awaited<ReturnType<typeof hatchAssistant>>;\n\n// The hatch promise's coalescing lives in the operation (module-level there);\n// this screen holds only the avatar traits so every remount riding the same\n// hatch renders the SAME assistant. Released on failure (a retry regenerates)\n// and on genuine completion — never in the window between the hatch resolving\n// and the screen navigating away.\nlet hatchTraitsCache: CharacterTraits | null = null;\n\nfunction releaseHatchTraits(): void {\n hatchTraitsCache = null;\n}\n\ntype HatchPhase = \"initializing\" | \"provisioning\" | \"connecting\" | \"ready\";\n\nconst PHASE_TARGET: Record<HatchPhase, number> = {\n initializing: 0,\n provisioning: 0.33,\n connecting: 0.66,\n ready: 1.0,\n};\n\nconst SEGMENT_DURATION_MS = 1500;\n\n// Written out per phase rather than composed from the phase name, so a phase\n// added without its copy fails to compile and the key stays greppable for the\n// orphan check in the catalog tests.\nconst PHASE_KEY: Record<HatchPhase, `hatchingScreen.phase.${HatchPhase}`> = {\n initializing: \"hatchingScreen.phase.initializing\",\n provisioning: \"hatchingScreen.phase.provisioning\",\n connecting: \"hatchingScreen.phase.connecting\",\n ready: \"hatchingScreen.phase.ready\",\n};\n\nexport function interpolateSegmentProgress(\n segmentStart: number,\n target: number,\n elapsedMs: number,\n): number {\n if (segmentStart >= target) {\n return target;\n }\n const t = Math.min(1.0, elapsedMs / SEGMENT_DURATION_MS);\n const eased = 1.0 - Math.pow(1.0 - t, 3.0);\n return segmentStart + (target - segmentStart) * eased;\n}\n\n/**\n * The funnel's provisioning step, ported from web's\n * `/assistant/onboarding/hatching` — now `/account/onboarding/hatching`,\n * rendered with the account screens' compact chrome.\n *\n * The hatch rides the app bundle's one hatch api — POST /v1/assistants/hatch\n * with the funnel's hosting choice as the remote param. The hatch operation\n * discriminates (local, docker, cloud), so this screen never branches on the\n * mode: it waits the answer out, polls the resolution endpoint until the\n * hatched assistant answers, and hands off to research. The pending provider\n * key stays staged for the downstream apply (same sessionStorage, same\n * origin), and the shell's resolution owns the not-ready states afterwards.\n */\nexport function HatchingScreen() {\n const { t } = useTranslation(\"account\");\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n // The funnel's hosting choice IS a Remote (the hosting screen emits the\n // contract's spelling) — validate the param and ride it as the hatch's\n // remote; anything absent or foreign reads as the local walk.\n const hosting = RemoteSchema.safeParse(searchParams.get(\"hosting\")).data;\n const failParam = searchParams.get(\"fail\");\n const [hatchTraits] = useState<CharacterTraits>(\n () => (hatchTraitsCache ??= randomCharacterTraits(BUNDLED_COMPONENTS)),\n );\n const avatarSvgDataUrl = useMemo(() => {\n const svg = composeSvg(\n BUNDLED_COMPONENTS,\n hatchTraits.bodyShape,\n hatchTraits.eyeStyle,\n hatchTraits.color,\n 320,\n );\n if (!svg) {\n return \"\";\n }\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n }, [hatchTraits]);\n const [phase, setPhase] = useState<HatchPhase>(\"initializing\");\n const [error, setError] = useState<string | null>(null);\n const [attempt, setAttempt] = useState(0);\n const [displayProgress, setDisplayProgress] = useState<number>(0);\n const [animationEpoch, setAnimationEpoch] = useState(0);\n\n const phaseRef = useRef<HatchPhase>(phase);\n const segmentStartRef = useRef(0);\n const segmentStartTimeRef = useRef(0);\n const displayProgressRef = useRef(0);\n\n const transitionPhase = useCallback((next: HatchPhase) => {\n segmentStartRef.current = displayProgressRef.current;\n segmentStartTimeRef.current = Date.now();\n phaseRef.current = next;\n setPhase(next);\n setAnimationEpoch((n) => n + 1);\n }, []);\n\n useEffect(() => {\n // Developer \"Replay Hatch Failure\" tool: when opened with `?fail`, skip\n // the real hatch flow and render the error UI directly so the failure\n // screen can be exercised on demand.\n if (failParam !== null) {\n setError(\n \"Simulated hatch failure (developer menu → Replay Hatch Failure).\",\n );\n return;\n }\n\n let cancelled = false;\n let navigateTimer: ReturnType<typeof setTimeout> | null = null;\n let readyPollTimer: ReturnType<typeof setTimeout> | null = null;\n const pollStartMs = Date.now();\n\n const handleHatchReady = (readyAssistantId?: string) => {\n // Seed the random hatch avatar for the freshly hatched assistant — the\n // D3b flagged gap, re-homed from web's seeding onto the app's scoped\n // surface. Read-then-write (never over an existing sidecar), never\n // awaited: the handoff below must not block on the server-side render,\n // and the research face step overwrites the avatar anyway when the user\n // picks one — this is the dock/tray stopgap during the research window.\n if (readyAssistantId) {\n void seedHatchAvatar(readyAssistantId, hatchTraits);\n }\n setDisplayProgress(1);\n displayProgressRef.current = 1;\n segmentStartRef.current = 1;\n setPhase(\"ready\");\n phaseRef.current = \"ready\";\n navigateTimer = setTimeout(() => {\n if (cancelled) {\n return;\n }\n // The hatch succeeded and we're leaving this screen for good. Release\n // the module-level guards so a later onboarding session hatches a\n // brand-new one instead of reusing this resolved promise and avatar.\n releaseHatchGuard();\n releaseHatchTraits();\n // A local walk carries the hosting choice + the hatched assistant so\n // research adopts exactly this assistant, not whatever a stale\n // selection resolves to (its background hatch resolves the existing\n // local assistant instead of provisioning a cloud one). A cloud walk\n // hands off bare — research owns the managed background hatch.\n if (hosting && hosting !== \"cloud\") {\n const researchParams = new URLSearchParams({ hosting });\n if (readyAssistantId) {\n researchParams.set(\"assistant\", readyAssistantId);\n }\n void navigate(\n `${routes.onboarding.research}?${researchParams.toString()}`,\n SETUP_NAVIGATE,\n );\n return;\n }\n void navigate(routes.onboarding.research, SETUP_NAVIGATE);\n }, COMPLETION_NAVIGATE_DELAY_MS);\n };\n\n const startHatch = async () => {\n transitionPhase(\"provisioning\");\n\n try {\n // The validated hosting choice IS the hatch api's remote param — the\n // hatch operation discriminates (local, docker, cloud), so this\n // screen never branches on the mode.\n const remote = hosting ?? \"local\";\n const result = await hatchAssistant(remote);\n if (cancelled) {\n return;\n }\n if (!result.ok) {\n releaseHatchTraits();\n setError(result.error ?? \"Failed to hatch your assistant.\");\n return;\n }\n if (result.assistantId) {\n writeSelectedAssistantId(result.assistantId);\n }\n // The hatch's answer carries the new assistant; the resolution\n // poll below waits for it to answer before the handoff.\n transitionPhase(\"connecting\");\n if (result.assistantId) {\n let gatewayReady = false;\n while (!cancelled && !gatewayReady) {\n // The resolution endpoint is the lifecycle read: the serving\n // host composes the entry's health and auth into the union, so\n // \"ready\" means the gateway is answering. A probe that cannot\n // settle (transport error, timeout) keeps polling within the\n // deadline below.\n const resolution = await resolveAssistant({\n signal: AbortSignal.timeout(10_000),\n }).catch(() => null);\n if (resolution?.status === \"ready\") {\n gatewayReady = true;\n break;\n }\n if (Date.now() - pollStartMs >= MAX_HATCH_WAIT_MS) {\n // The hatch succeeded but the assistant never went healthy —\n // release the guards so \"Try again\" runs a genuinely fresh\n // hatch.\n releaseHatchGuard();\n releaseHatchTraits();\n setError(\n \"Your assistant is taking longer than expected. Please try again.\",\n );\n return;\n }\n await new Promise<void>((resolve) => {\n readyPollTimer = setTimeout(resolve, POLL_INTERVAL_MS);\n });\n readyPollTimer = null;\n }\n if (cancelled) {\n return;\n }\n }\n handleHatchReady(result.assistantId);\n } catch {\n releaseHatchTraits();\n if (cancelled) {\n return;\n }\n setError(\"Failed to hatch your assistant. Please try again.\");\n }\n };\n\n void startHatch();\n\n return () => {\n cancelled = true;\n if (navigateTimer) {\n clearTimeout(navigateTimer);\n }\n if (readyPollTimer) {\n clearTimeout(readyPollTimer);\n }\n };\n }, [attempt, failParam, hatchTraits, hosting, navigate, transitionPhase]);\n\n useEffect(() => {\n if (segmentStartTimeRef.current === 0) {\n segmentStartTimeRef.current = Date.now();\n }\n let rafId: number;\n const tick = () => {\n const elapsed = Date.now() - segmentStartTimeRef.current;\n const target = PHASE_TARGET[phaseRef.current];\n const value = interpolateSegmentProgress(\n segmentStartRef.current,\n target,\n elapsed,\n );\n displayProgressRef.current = value;\n setDisplayProgress(value);\n if (target - value > 1e-6) {\n rafId = requestAnimationFrame(tick);\n }\n };\n rafId = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(rafId);\n }, [animationEpoch]);\n\n if (error) {\n return (\n <AccountShell>\n <div\n role=\"alert\"\n className=\"flex w-full flex-col items-center text-center text-[var(--content-default)]\"\n >\n <h1 className=\"text-3xl font-semibold tracking-tight\">\n {t(\"hatchingScreen.genericFailure\")}\n </h1>\n <p className=\"mt-3 text-body-medium-lighter text-[var(--content-tertiary)]\">\n {error}\n </p>\n <img\n src={avatarSvgDataUrl}\n alt=\"\"\n width={160}\n height={160}\n className=\"my-10\"\n />\n <div className=\"flex w-full flex-col gap-2\">\n <Button\n variant=\"primary\"\n size=\"large\"\n fullWidth\n onClick={() => {\n segmentStartRef.current = 0;\n segmentStartTimeRef.current = Date.now();\n phaseRef.current = \"initializing\";\n displayProgressRef.current = 0;\n setPhase(\"initializing\");\n setDisplayProgress(0);\n setAnimationEpoch((n) => n + 1);\n setError(null);\n setAttempt((n) => n + 1);\n }}\n >\n {t(\"actions.tryAgain\")}\n </Button>\n <Button\n variant=\"outlined\"\n size=\"large\"\n fullWidth\n onClick={() => {\n // The hosting choice is the walk's data: a local/docker walk\n // backs up to its hosting step; a cloud walk came through\n // research and returns there.\n const to =\n hosting && hosting !== \"cloud\"\n ? routes.account.onboarding.hosting\n : `${routes.onboarding.research}?${NEW_ASSISTANT_PARAM}=1`;\n void navigate(to, SETUP_NAVIGATE);\n }}\n >\n {t(\"actions.back\")}\n </Button>\n </div>\n </div>\n </AccountShell>\n );\n }\n\n return (\n <AccountShell>\n <div className=\"flex w-full flex-col items-center text-center text-[var(--content-default)]\">\n <h1 className=\"text-3xl font-semibold tracking-tight\">\n {phase === \"ready\"\n ? t(\"hatchingScreen.ready\")\n : t(\"hatchingScreen.waking\")}\n </h1>\n {phase !== \"ready\" && (\n <p className=\"mt-3 text-body-medium-lighter text-[var(--content-tertiary)]\">\n {t(\"hatchingScreen.wakingBody\")}\n </p>\n )}\n <img\n src={avatarSvgDataUrl}\n alt=\"\"\n width={160}\n height={160}\n className={`my-10 ${phase === \"ready\" ? \"\" : \"animate-pulse\"}`}\n />\n <ProgressBar\n value={displayProgress}\n height={6}\n className=\"w-full\"\n aria-label={t(\"hatchingScreen.progressAriaLabel\")}\n />\n <p className=\"mt-3 text-body-small-default text-[var(--content-tertiary)]\">\n {t(PHASE_KEY[phase])}\n </p>\n </div>\n </AccountShell>\n );\n}\n"],"mappings":"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"}
|