@madarco/agentbox 0.1.0 → 0.3.0
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/{chunk-OOOKFFR5.js → chunk-3NCUES35.js} +3 -3
- package/dist/{chunk-RWJE6AER.js → chunk-7NQFIBQG.js} +10 -4
- package/dist/chunk-7NQFIBQG.js.map +1 -0
- package/dist/{chunk-O5HS3QHW.js → chunk-MOC54XL6.js} +67 -16
- package/dist/chunk-MOC54XL6.js.map +1 -0
- package/dist/{create-LSSO7H4I-GWNALUMF.js → create-SE6H4B5U-IWAZHJHV.js} +3 -3
- package/dist/index.js +439 -48
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-P4FSKGR2-3466P54Y.js → lifecycle-YTMZYKOE-R4M3OR27.js} +3 -3
- package/package.json +1 -1
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +8 -4
- package/runtime/docker/packages/ctl/dist/bin.cjs +24 -3
- package/share/agentbox-setup/SKILL.md +8 -4
- package/dist/chunk-O5HS3QHW.js.map +0 -1
- package/dist/chunk-RWJE6AER.js.map +0 -1
- /package/dist/{chunk-OOOKFFR5.js.map → chunk-3NCUES35.js.map} +0 -0
- /package/dist/{create-LSSO7H4I-GWNALUMF.js.map → create-SE6H4B5U-IWAZHJHV.js.map} +0 -0
- /package/dist/{lifecycle-P4FSKGR2-3466P54Y.js.map → lifecycle-YTMZYKOE-R4M3OR27.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../packages/sandbox-docker/src/claude.ts","../../../packages/sandbox-docker/src/claude-hooks-filter.ts","../../../packages/sandbox-docker/src/claude-pull.ts","../../../packages/sandbox-docker/src/dockerd.ts","../../../packages/sandbox-docker/src/vnc.ts","../../../packages/sandbox-docker/src/web.ts","../../../packages/sandbox-docker/src/git-worktree.ts","../../../packages/sandbox-docker/src/overlay.ts","../../../packages/sandbox-docker/src/snapshot.ts","../../../packages/sandbox-docker/src/ctl.ts","../../../packages/sandbox-docker/src/relay.ts","../../../packages/sandbox-docker/src/vscode.ts","../../../packages/relay/src/types.ts","../../../packages/relay/src/registry.ts","../../../packages/relay/src/status-store.ts","../../../packages/relay/src/server.ts","../../../packages/relay/src/autopause.ts","../../../packages/ctl/src/types.ts","../../../packages/ctl/src/client.ts","../../../packages/ctl/src/render.ts","../../../packages/ctl/src/config.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';\nimport { homedir, tmpdir } from 'node:os';\nimport { join, relative } from 'node:path';\nimport { execa } from 'execa';\nimport { addProjectAlias, clearInstallMethod, filterHostHooks } from './claude-hooks-filter.js';\nimport {\n mergeInstalledPlugins,\n mergeKnownMarketplaces,\n pickNewItems,\n SKILL_EXCLUDE_PREFIXES,\n} from './claude-pull.js';\nimport { ensureVolume, volumeExists } from './docker.js';\nimport { detectEngine, orbstackVolumePath } from './host-export.js';\n\nexport const SHARED_CLAUDE_VOLUME = 'agentbox-claude-config';\nexport const DEFAULT_CLAUDE_SESSION = 'claude';\nconst CONTAINER_CLAUDE_DIR = '/home/vscode/.claude';\nexport const CONTAINER_USER = 'vscode';\n/** Workspace is always mounted here inside the box, regardless of host path. */\nconst CONTAINER_WORKSPACE = '/workspace';\n\nexport interface ClaudeConfigSpec {\n /** Resolved Docker volume name mounted at /home/vscode/.claude. */\n volume: string;\n}\n\nexport function resolveClaudeVolume(opts: { isolate: boolean; boxId: string }): ClaudeConfigSpec {\n if (opts.isolate) {\n return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };\n }\n return { volume: SHARED_CLAUDE_VOLUME };\n}\n\nexport interface EnsureClaudeVolumeOptions {\n /**\n * When true and the host's ~/.claude exists, rsync host -> volume on every call.\n * Sync is additive: files present on host overwrite same-named files in the\n * volume; box-only files (e.g. `projects/<hash>/*.jsonl` session history written\n * inside earlier boxes) are preserved.\n */\n syncFromHost: boolean;\n /** Image used by the throwaway sync helper container; we use the box image to avoid extra pulls. */\n image: string;\n /**\n * Host-absolute path of the workspace being bound to /workspace inside the\n * box. When provided, the synced `_claude.json` gets `projects[<hostWorkspace>]`\n * duplicated to `projects['/workspace']` so project-scoped MCP servers,\n * trust state, and history match what the host has for this project.\n */\n hostWorkspace?: string;\n}\n\nexport interface EnsureClaudeVolumeResult {\n /** True only the very first time the volume is created (on this host). */\n created: boolean;\n /** True when the rsync helper actually ran (syncFromHost was true AND host ~/.claude existed). */\n synced: boolean;\n /**\n * Number of hook entries dropped during sync because their `command` pointed\n * at a host path (under `$HOME/`) that wouldn't exist inside the container.\n * 0 when nothing was filtered or no sync ran.\n */\n filteredHookCount?: number;\n /**\n * True when the synced `_claude.json` had its top-level `installMethod`\n * field scrubbed (host had it set; we let in-box claude redetect).\n */\n clearedInstallMethod?: boolean;\n /**\n * True when `projects[<hostWorkspace>]` was duplicated to\n * `projects['/workspace']` in the synced `_claude.json` so the in-box claude\n * sees the host's project-scoped state (mcpServers, history, …).\n */\n aliasedProjectKey?: boolean;\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Walk `root` and return rsync-style relative paths of every symlink whose\n * target doesn't resolve. We pass these to rsync as `--exclude` patterns so\n * the broken-symlink set (e.g. claude's `debug/latest` once an older debug\n * file is reaped) doesn't abort the whole sync under `--copy-unsafe-links`.\n *\n * Crosses into subdirs; doesn't follow symlinks (the whole point is to test\n * them rather than traverse them).\n */\nasync function findBrokenSymlinks(root: string): Promise<string[]> {\n const broken: string[] = [];\n async function walk(dir: string): Promise<void> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const ent of entries) {\n const full = join(dir, ent.name);\n if (ent.isSymbolicLink()) {\n try {\n await stat(full);\n } catch {\n broken.push(relative(root, full));\n }\n } else if (ent.isDirectory()) {\n await walk(full);\n }\n }\n }\n await walk(root);\n return broken;\n}\n\n/**\n * Ensure the named volume exists, then (when {@link EnsureClaudeVolumeOptions.syncFromHost}\n * is true and the host has a `~/.claude` directory) rsync host -> volume via a throwaway\n * helper container. The host is treated as the authoritative source for config:\n * settings, auth token, skills, plugins, and MCP entries on the host overwrite the\n * same-named files in the volume on every call. Files that only exist in the volume\n * (in-box session history under `projects/`, statsig cache, etc.) are preserved —\n * rsync runs without `--delete`.\n *\n * Caveat: if another box is currently running with the same shared volume mounted,\n * the rsync can change config files under it mid-session. We accept this as part of\n * \"host is authoritative\" — per-box state under `projects/` is untouched, so the\n * effect is limited to overlapping config files (rare to be edited live).\n *\n * Returns `created: true` only on the very first run for this volume; `synced: true`\n * whenever the rsync actually executed.\n */\nexport async function ensureClaudeVolume(\n spec: ClaudeConfigSpec,\n opts: EnsureClaudeVolumeOptions,\n): Promise<EnsureClaudeVolumeResult> {\n const existed = await volumeExists(spec.volume);\n await ensureVolume(spec.volume);\n const created = !existed;\n\n if (!opts.syncFromHost) return { created, synced: false };\n\n const hostClaude = join(homedir(), '.claude');\n if (!(await pathExists(hostClaude))) return { created, synced: false };\n\n // rsync (not cp -a) so repeat syncs skip unchanged files. rsync is installed in\n // the box image (Dockerfile.box). Trailing slash on /src-claude/ means\n // \"contents of src\", matching the original cp -a /src/. /dst/ semantics.\n // We run as root (--user 0) because the volume's existing content may be a\n // mix of UIDs (host's macOS UID for files copied from ~/.claude, plus\n // vscode's UID 1000 for anything claude wrote inside a box); only root can\n // rewrite arbitrary ownership. The post-chown brings everything back to\n // UID 1000 so the in-box vscode user can read/write.\n //\n // We also pull in ~/.claude.json (the *file* at home root that Claude Code\n // uses for global state: hasCompletedOnboarding, anonymousId, oauthAccount,\n // plugin caches). It's not inside ~/.claude, so we bind-mount it separately\n // (when present) and copy it into the volume as _claude.json. A symlink\n // baked into the image (/home/vscode/.claude.json -> .../_claude.json)\n // makes it reachable from the path claude expects.\n const hostClaudeJson = join(homedir(), '.claude.json');\n const hasJson = await pathExists(hostClaudeJson);\n const hostHome = homedir();\n // Claude Code's user-skills convention: ~/.claude/skills/<name> is a\n // RELATIVE symlink to ../../.agents/skills/<name>. From /src-claude/skills/\n // inside the helper that resolves to /.agents/skills/<name>. Bind-mount the\n // host's ~/.agents at /.agents so --copy-unsafe-links can dereference each\n // symlink into a real directory in /dst. Without this, rsync errors with\n // \"symlink has no referent\" and the whole sync aborts.\n const hostAgents = join(homedir(), '.agents');\n const hasAgents = await pathExists(hostAgents);\n const args: string[] = [\n 'run',\n '--rm',\n '--user',\n '0',\n // HOST_HOME used inside the shell script to rewrite host-absolute\n // installPath values in plugins/installed_plugins.json.\n '-e',\n `HOST_HOME=${hostHome}`,\n '-v',\n `${spec.volume}:/dst`,\n '-v',\n `${hostClaude}:/src-claude:ro`,\n ];\n if (hasJson) args.push('-v', `${hostClaudeJson}:/src-claude-json:ro`);\n if (hasAgents) args.push('-v', `${hostAgents}:/.agents:ro`);\n\n // Pre-filter host-path hooks. Hook commands whose path is under the user's\n // host home (e.g. `/Users/marco/.config/iterm2/cc-status`) won't exist\n // inside the Linux container, and Claude logs a noisy\n // `SessionStart:startup hook error /bin/sh: …: not found` every time. We\n // build a small tempdir with filtered copies of `settings.json` /\n // `.claude.json`, mount it as `/src-filter`, and let the helper container\n // overlay it on top of what rsync brought in. The host files are never\n // touched.\n const filterDir = await mkdtemp(join(tmpdir(), 'agentbox-claude-filter-'));\n let filteredHookCount = 0;\n let clearedInstallMethod = false;\n let aliasedProjectKey = false;\n try {\n const settingsResult = await maybeFilterTo(\n join(hostClaude, 'settings.json'),\n join(filterDir, 'settings.json'),\n hostHome,\n );\n filteredHookCount += settingsResult.removedHooks;\n if (hasJson) {\n const jsonResult = await maybeFilterTo(\n hostClaudeJson,\n join(filterDir, '_claude.json'),\n hostHome,\n {\n clearInstallMethod: true,\n aliasProject: opts.hostWorkspace\n ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE }\n : undefined,\n },\n );\n filteredHookCount += jsonResult.removedHooks;\n clearedInstallMethod = jsonResult.clearedInstallMethod;\n aliasedProjectKey = jsonResult.aliasedProjectKey;\n }\n if (filteredHookCount > 0 || clearedInstallMethod || aliasedProjectKey) {\n args.push('-v', `${filterDir}:/src-filter:ro`);\n }\n // Pre-scan for broken symlinks. With --copy-unsafe-links rsync errors out\n // and exits 23 when any unsafe symlink's referent is missing — e.g.\n // `~/.claude/debug/latest` regularly points to a debug file that's been\n // reaped. We can't predict every such case, so we walk once and tell\n // rsync to skip exactly those entries.\n const brokenSymlinks = await findBrokenSymlinks(hostClaude);\n const rsyncExcludes = ['--exclude=node_modules'];\n for (const rel of brokenSymlinks) rsyncExcludes.push(`--exclude=/${rel}`);\n const rsyncFlags = `-a --copy-unsafe-links ${rsyncExcludes.join(' ')}`;\n args.push(\n opts.image,\n 'sh',\n '-c',\n // Each step in its own brace group so a missing optional file (no\n // .claude.json on host, no filtered overlays) doesn't short-circuit the\n // final chown.\n //\n // --copy-unsafe-links: dereference symlinks pointing OUTSIDE\n // /src-claude (e.g. ~/.claude/skills/* -> ../../.agents/skills/*),\n // so user skills materialize as real directories inside the volume\n // without needing to also bind-mount ~/.agents.\n // --exclude=node_modules: skip every node_modules directory anywhere\n // in the tree. Plugin caches (plugins/cache/<m>/<p>/<v>/node_modules)\n // ship host-platform-specific binaries (darwin-arm64 fsevents,\n // esbuild, rollup, sharp) that are useless on linux/amd64. The\n // plugin source still lands; node_modules is rebuilt lazily inside\n // the box on first claude session (see rebuildPluginNativeDeps).\n //\n // The top-level plugin registry JSONs (installed_plugins.json,\n // known_marketplaces.json) carry host-absolute `installPath` /\n // `installLocation` values; without rewriting, claude resolves them\n // to `/Users/<you>/...` (or, when claude detects the missing path,\n // falls back to a slug derived from `source.repo` like\n // `microsoft-playwright-cli` — neither exists in the box, and the\n // marketplace fails to load, which masquerades as \"plugin not\n // found in marketplace\"). One sweep over every JSON directly under\n // /dst/plugins/ catches both files (and any future registry).\n // One-shot migration for volumes that were populated before\n // --exclude=node_modules existed. Without it, the volume keeps\n // host-darwin node_modules forever (rsync without --delete won't\n // remove them). The `.agentbox-cleaned-nm-v1` sentinel makes the wipe\n // a no-op after the first run; rebuildPluginNativeDeps repopulates\n // linux/amd64 node_modules on the next `agentbox claude`.\n '{ [ ! -f /dst/.agentbox-cleaned-nm-v1 ] && ' +\n 'find /dst -name node_modules -type d -prune -exec rm -rf {} + && ' +\n 'touch /dst/.agentbox-cleaned-nm-v1; true; }' +\n ` && rsync ${rsyncFlags} /src-claude/ /dst/` +\n ' && { [ -f /src-claude-json ] && cp -a /src-claude-json /dst/_claude.json; true; }' +\n ' && { [ -f /src-filter/settings.json ] && cp -a /src-filter/settings.json /dst/settings.json; true; }' +\n ' && { [ -f /src-filter/_claude.json ] && cp -a /src-filter/_claude.json /dst/_claude.json; true; }' +\n ' && { [ -d /dst/plugins ] && [ -n \"$HOST_HOME\" ] && ' +\n 'find /dst/plugins -maxdepth 1 -type f -name \"*.json\" ' +\n '-exec sed -i \"s|$HOST_HOME/.claude/plugins/|/home/vscode/.claude/plugins/|g\" {} +; true; }' +\n ' && chown -R 1000:1000 /dst',\n );\n await execa('docker', args);\n } finally {\n await rm(filterDir, { recursive: true, force: true });\n }\n\n return { created, synced: true, filteredHookCount, clearedInstallMethod, aliasedProjectKey };\n}\n\n/**\n * Read a JSON file, run it through {@link filterHostHooks}, (when opted in)\n * {@link clearInstallMethod}, and (when opted in) {@link addProjectAlias},\n * and write the result to `dest` ONLY when at least one change was made.\n * Tolerant of missing or garbage JSON — silently returns zero changes in\n * those cases (sync proceeds with the raw rsync'd file).\n */\nasync function maybeFilterTo(\n src: string,\n dest: string,\n hostHome: string,\n opts: {\n clearInstallMethod?: boolean;\n aliasProject?: { from: string; to: string };\n } = {},\n): Promise<{\n removedHooks: number;\n clearedInstallMethod: boolean;\n aliasedProjectKey: boolean;\n}> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(src, 'utf8'));\n } catch {\n return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };\n }\n const filtered = filterHostHooks(parsed, hostHome);\n let working: unknown = filtered.data;\n let cleared = false;\n if (opts.clearInstallMethod) {\n const r = clearInstallMethod(working);\n working = r.data;\n cleared = r.cleared;\n }\n let aliased = false;\n if (opts.aliasProject) {\n const r = addProjectAlias(working, opts.aliasProject.from, opts.aliasProject.to);\n working = r.data;\n aliased = r.aliased;\n }\n if (filtered.removedCommands.length === 0 && !cleared && !aliased) {\n return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };\n }\n await writeFile(dest, JSON.stringify(working, null, 2));\n return {\n removedHooks: filtered.removedCommands.length,\n clearedInstallMethod: cleared,\n aliasedProjectKey: aliased,\n };\n}\n\nexport interface ClaudeMountResult {\n /** Docker -v spec strings to append to runBox(extraVolumes). */\n extraVolumes: string[];\n /** Env vars to forward into the container; only includes keys that were set + non-empty on the host. */\n env: Record<string, string>;\n volumeName: string;\n}\n\n// Forwarded from the host's `process.env` into the box at `docker run -e` time\n// (and re-forwarded by `startClaudeSession` at `docker exec -e` time, so a\n// later `agentbox claude start <existing-box>` picks up the host's current\n// session env even when the container was created from a different shell).\n//\n// CLAUDE_EFFORT / ANTHROPIC_MODEL: Claude Code stores the user's model\n// selection (Opus/Sonnet/Haiku via /model or --effort) only in the parent\n// claude's process env — not in `~/.claude.json` or `~/.claude/settings.json`.\n// When the user invokes `agentbox claude` from inside their host claude\n// session, that env IS present in the calling shell; forwarding it is the\n// only way the in-box claude inherits the same model default.\nconst FORWARDED_ENV_KEYS = [\n 'ANTHROPIC_API_KEY',\n 'CLAUDE_CODE_OAUTH_TOKEN',\n 'CLAUDE_EFFORT',\n 'ANTHROPIC_MODEL',\n] as const;\n\nexport function buildClaudeMounts(\n spec: ClaudeConfigSpec,\n hostEnv: NodeJS.ProcessEnv,\n): ClaudeMountResult {\n const env: Record<string, string> = {};\n for (const k of FORWARDED_ENV_KEYS) {\n const v = hostEnv[k];\n if (typeof v === 'string' && v.length > 0) env[k] = v;\n }\n return {\n extraVolumes: [`${spec.volume}:${CONTAINER_CLAUDE_DIR}`],\n env,\n volumeName: spec.volume,\n };\n}\n\nexport interface RebuildPluginNativeDepsResult {\n /** Plugin cache directories whose node_modules was (re)installed during this call. */\n rebuilt: string[];\n /** Plugin cache directories where install failed; non-fatal, claude often still loads. */\n failed: Array<{ dir: string; stderr: string }>;\n /**\n * True when the in-box exec was skipped entirely because a host-side scan\n * proved every package.json-bearing plugin already carries its install\n * marker. Only possible when the volume is host-visible (OrbStack).\n */\n skipped: boolean;\n}\n\n/** Per-plugin sentinel written inside the cache dir after a successful install. */\nconst PLUGIN_INSTALLED_MARKER = '.agentbox-installed';\n\n/**\n * Per-plugin sentinel written (mtime = failure time) when an install fails. A\n * plugin with a *recent* fail marker is skipped instead of retried on every\n * launch; once the marker ages past {@link PLUGIN_INSTALL_BACKOFF_MS} it's\n * retried. Cleared on a later success.\n */\nconst PLUGIN_FAILED_MARKER = '.agentbox-install-failed';\n\n/** How long a failed plugin install is skipped before it's retried. */\nconst PLUGIN_INSTALL_BACKOFF_MS = 6 * 60 * 60 * 1000;\n\n/** Backoff window in whole minutes, for the in-box `find -mmin` recency test. */\nconst PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 60000);\n\n/**\n * Persistent npm cache, kept inside the claude-config volume so a given\n * package@version is fetched from the registry once *globally* and reused by\n * every later box and plugin version. Shared across boxes with the default\n * shared volume; per-box only under `--isolate-claude-config`. Not named\n * `node_modules`, so the one-time node_modules cleanup migration leaves it\n * alone; the host->volume rsync is additive (won't delete it) and `pull claude`\n * only pulls skills/plugins/agents/commands (won't drag it to the host).\n */\nconst NPM_CACHE_DIR = '/home/vscode/.claude/.agentbox-npm-cache';\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/** True when `p` exists and its mtime is within the install-backoff window. */\nasync function isRecentFailMarker(p: string): Promise<boolean> {\n try {\n const st = await stat(p);\n return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;\n } catch {\n return false;\n }\n}\n\nasync function isDir(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Pure host-side scan of a plugin `cache/<m>/<p>/<v>/` tree. Returns true iff\n * at least one version dir has a `package.json`, no install marker, and no\n * *recent* failure marker — i.e. the in-box rebuild would actually do npm\n * work. A missing/empty cache root means nothing to do (false). Mirrors the\n * in-box script's accept/skip rules (`packages/sandbox-docker/src/claude.ts`\n * rebuild script) so the host pre-check and the container never disagree.\n */\nexport async function scanPluginCacheForRebuild(cacheRoot: string): Promise<boolean> {\n let marketplaces;\n try {\n marketplaces = await readdir(cacheRoot, { withFileTypes: true });\n } catch {\n return false;\n }\n for (const m of marketplaces) {\n if (!m.isDirectory()) continue;\n const mPath = join(cacheRoot, m.name);\n let plugins;\n try {\n plugins = await readdir(mPath, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const p of plugins) {\n if (!p.isDirectory()) continue;\n const pPath = join(mPath, p.name);\n let versions;\n try {\n versions = await readdir(pPath, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const v of versions) {\n if (!v.isDirectory()) continue;\n const vPath = join(pPath, v.name);\n if (!(await isFile(join(vPath, 'package.json')))) continue;\n if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;\n if (await isRecentFailMarker(join(vPath, PLUGIN_FAILED_MARKER))) continue;\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Host-visible `plugins/cache` dir for a claude-config volume, or null when the\n * engine doesn't expose volume contents to the host (Docker Desktop / other).\n * Returns the cache path even if it doesn't exist yet — {@link\n * scanPluginCacheForRebuild} treats a missing cache as \"nothing to do\" — but\n * only once the volume itself is materialized on the host.\n */\nasync function resolveClaudeCacheLiveOnHost(volume: string): Promise<string | null> {\n if ((await detectEngine()) !== 'orbstack') return null;\n if (!(await isDir(orbstackVolumePath(volume)))) return null;\n return orbstackVolumePath(volume, 'plugins', 'cache');\n}\n\n/**\n * Walk `/home/vscode/.claude/plugins/cache/<m>/<p>/<v>/` inside the box and run\n * `npm install` (or `npm ci` when a lockfile is present) for any plugin that\n * ships a `package.json` but hasn't been installed yet. Marker-gated (not\n * node_modules — plugins with empty dep lists install cleanly without ever\n * creating a node_modules dir, so a dir check would loop forever).\n *\n * This exists because the host→volume rsync excludes `node_modules` (host\n * darwin-arm64 native binaries like fsevents.node / @esbuild/darwin-arm64\n * are useless on the linux/amd64 box). The first claude session in a fresh\n * box pays the install cost; subsequent attaches don't.\n *\n * Three things keep this fast: installs run **in parallel** (bounded), npm\n * shares a **persistent cache in the claude-config volume** ({@link\n * NPM_CACHE_DIR}) with `--prefer-offline` so a package@version is fetched once\n * globally, and a failed plugin records {@link PLUGIN_FAILED_MARKER} so it's\n * skipped (not retried) until {@link PLUGIN_INSTALL_BACKOFF_MS} elapses.\n *\n * Failures on individual plugins are reported but don't throw — most\n * plugins still load with a partial dependency graph, and we prefer\n * launching claude over blocking on a third-party plugin's install hiccup.\n */\nexport async function rebuildPluginNativeDeps(\n container: string,\n opts: {\n onProgress?: (line: string) => void;\n /**\n * The claude-config volume backing this box. When given and host-visible\n * (OrbStack), a pure-fs pre-scan skips the `docker exec` entirely if every\n * package.json plugin already has its install marker — the common case for\n * every box after the first global install.\n */\n volume?: string;\n } = {},\n): Promise<RebuildPluginNativeDepsResult> {\n if (opts.volume) {\n const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);\n if (cacheRoot && !(await scanPluginCacheForRebuild(cacheRoot))) {\n return { rebuilt: [], failed: [], skipped: true };\n }\n }\n // The host parser below expects the REBUILD_START / REBUILD_OK /\n // REBUILD_FAIL..REBUILD_FAIL_END protocol; parallel jobs write per-dir\n // result+stderr files and we replay them in that protocol after `wait`.\n const script = `set -u\nPLUGINS_DIR=/home/vscode/.claude/plugins/cache\nMARKER=${PLUGIN_INSTALLED_MARKER}\nFAILMARKER=${PLUGIN_FAILED_MARKER}\nNPM_CACHE=${NPM_CACHE_DIR}\nBACKOFF_MIN=${PLUGIN_INSTALL_BACKOFF_MIN}\nMAX=4\n[ -d \"$PLUGINS_DIR\" ] || exit 0\nmkdir -p \"$NPM_CACHE\"\nWORK=\\$(mktemp -d)\nrelkey() { printf '%s' \"\\${1#$PLUGINS_DIR/}\" | tr '/' '_'; }\n# Run one plugin's install. $1 is frozen by value at call time, so it's safe\n# to read from the backgrounded subshell; the rest are set-once constants.\ndo_one() {\n d=\\$1\n key=\\$(relkey \"$d\")\n if (cd \"$d\" && \\\\\n if [ -f package-lock.json ]; then \\\\\n npm ci --no-audit --no-fund --silent --prefer-offline --cache \"$NPM_CACHE\"; \\\\\n else \\\\\n npm install --no-audit --no-fund --silent --no-package-lock --prefer-offline --cache \"$NPM_CACHE\"; \\\\\n fi) >\"$WORK/$key.out\" 2>\"$WORK/$key.err\"; then\n touch \"$d/$MARKER\"\n rm -f \"$d/$FAILMARKER\"\n printf 'OK\\\\n' > \"$WORK/$key.res\"\n else\n : > \"$d/$FAILMARKER\"\n printf 'FAIL\\\\n' > \"$WORK/$key.res\"\n fi\n}\nn=0\nfor dir in \"$PLUGINS_DIR\"/*/*/*/; do\n [ -d \"$dir\" ] || continue\n [ -f \"$dir/package.json\" ] || continue\n [ -f \"$dir/$MARKER\" ] && continue\n [ -n \"\\$(find \"$dir\" -maxdepth 1 -name \"$FAILMARKER\" -mmin -\\$BACKOFF_MIN 2>/dev/null)\" ] && continue\n echo \"REBUILD_START \\${dir#$PLUGINS_DIR/}\"\n n=\\$((n+1))\n printf '%s\\\\n' \"$dir\" >> \"$WORK/dirs\"\ndone\nif [ \"$n\" -eq 0 ]; then rm -rf \"$WORK\"; exit 0; fi\nrunning=0\nwhile IFS= read -r dir; do\n do_one \"$dir\" &\n running=\\$((running+1))\n if [ \"$running\" -ge \"$MAX\" ]; then wait; running=0; fi\ndone < \"$WORK/dirs\"\nwait\nwhile IFS= read -r dir; do\n key=\\$(relkey \"$dir\")\n rel=\\${dir#$PLUGINS_DIR/}\n [ -f \"$WORK/$key.res\" ] || continue\n read -r st < \"$WORK/$key.res\"\n if [ \"$st\" = OK ]; then\n echo \"REBUILD_OK $rel\"\n else\n echo \"REBUILD_FAIL $rel\"\n sed 's/^/ /' \"$WORK/$key.err\"\n echo \"REBUILD_FAIL_END\"\n fi\ndone < \"$WORK/dirs\"\nrm -rf \"$WORK\"\n`;\n const result = await execa(\n 'docker',\n ['exec', '--user', CONTAINER_USER, container, 'sh', '-c', script],\n { reject: false },\n );\n const rebuilt: string[] = [];\n const failed: Array<{ dir: string; stderr: string }> = [];\n const lines = (result.stdout ?? '').split('\\n');\n let collectingFail: { dir: string; stderr: string[] } | null = null;\n for (const line of lines) {\n if (collectingFail) {\n if (line === 'REBUILD_FAIL_END') {\n failed.push({ dir: collectingFail.dir, stderr: collectingFail.stderr.join('\\n') });\n collectingFail = null;\n } else {\n collectingFail.stderr.push(line);\n }\n continue;\n }\n if (line.startsWith('REBUILD_START ')) {\n opts.onProgress?.(`rebuilding ${line.slice('REBUILD_START '.length)}`);\n } else if (line.startsWith('REBUILD_OK ')) {\n rebuilt.push(line.slice('REBUILD_OK '.length));\n } else if (line.startsWith('REBUILD_FAIL ')) {\n collectingFail = { dir: line.slice('REBUILD_FAIL '.length), stderr: [] };\n }\n }\n return { rebuilt, failed, skipped: false };\n}\n\nexport class ClaudeSessionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ClaudeSessionError';\n }\n}\n\nexport interface StartClaudeSessionOptions {\n container: string;\n claudeArgs: string[];\n sessionName?: string;\n /** Shown in the session's tmux status bar. Defaults to the container name\n * with the `agentbox-` prefix stripped (containers are `agentbox-<name>`). */\n boxName?: string;\n}\n\n/**\n * Single-quote a token for /bin/sh. Conservative: anything outside the safe alphabet\n * gets wrapped. We don't try to detect \"obviously safe\" inputs; quoting is cheap.\n */\nfunction shQuote(arg: string): string {\n if (arg.length === 0) return `''`;\n if (/^[A-Za-z0-9_\\-./=:@%+,]+$/.test(arg)) return arg;\n return `'${arg.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * Start a detached tmux session running Claude Code inside the container. The session\n * survives client disconnects; reattach via {@link attachClaudeSession}.\n *\n * We forward the host's TERM (default xterm-256color) so the in-container tmux\n * picks the right terminal-overrides at session creation time — without this,\n * docker exec defaults TERM to `xterm` and tmux can't declare 24-bit color.\n *\n * We also re-forward {@link FORWARDED_ENV_KEYS} from the host's process env.\n * Values set at container-create time (via runBox -e) are still inherited\n * for free, but the user might be invoking `agentbox claude start <box>`\n * from a different shell session — e.g. inside their host claude (which sets\n * CLAUDE_EFFORT) for a box created earlier from a plain terminal. Re-passing\n * at exec time lets the in-box claude pick up the host's *current* selection.\n */\nexport async function startClaudeSession(opts: StartClaudeSessionOptions): Promise<void> {\n const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;\n const boxName = opts.boxName ?? opts.container.replace(/^agentbox-/, '');\n const cmd = ['claude', ...opts.claudeArgs].map(shQuote).join(' ');\n const term = process.env['TERM'] ?? 'xterm-256color';\n const envFlags: string[] = ['-e', `TERM=${term}`];\n for (const k of FORWARDED_ENV_KEYS) {\n const v = process.env[k];\n if (typeof v === 'string' && v.length > 0) envFlags.push('-e', `${k}=${v}`);\n }\n const result = await execa(\n 'docker',\n [\n 'exec',\n ...envFlags,\n '--user',\n CONTAINER_USER,\n opts.container,\n 'tmux',\n 'new-session',\n '-d',\n '-s',\n sessionName,\n cmd,\n ...buildClaudeStatusBarArgs(sessionName, boxName),\n ],\n { reject: false },\n );\n if (result.exitCode === 0) return;\n const stderr = (result.stderr ?? '').toString();\n if (result.exitCode === 127 || /command not found|tmux: not found/i.test(stderr)) {\n throw new ClaudeSessionError(\n `tmux is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`,\n );\n }\n if (/claude.*not found|exec: \"claude\"/i.test(stderr)) {\n throw new ClaudeSessionError(\n `claude is missing from the box image. Rebuild with: docker rmi agentbox/box:dev && retry.`,\n );\n }\n if (/duplicate session/i.test(stderr)) {\n throw new ClaudeSessionError(\n `a tmux session \"${sessionName}\" already exists in ${opts.container}; use \\`agentbox claude attach\\` to reattach.`,\n );\n }\n throw new ClaudeSessionError(\n `failed to start claude session in ${opts.container}: ${stderr.trim() || `exit ${String(result.exitCode)}`}`,\n );\n}\n\n/**\n * Replace the current process with `docker exec -it tmux attach`. Ctrl-b d returns\n * the user to their host shell with exit 0. We forward TERM so tmux declares\n * the outer terminal's true-color and hyperlink capabilities; without it\n * docker exec sets TERM=xterm and Claude renders without RGB.\n */\n/**\n * The `docker` argv that attaches an interactive terminal to a box's Claude\n * tmux session. Shared by {@link attachClaudeSession} (which `spawnSync`s it\n * directly) and the dashboard command (which hands it to `tmux respawn-pane`).\n */\nexport function buildClaudeAttachArgv(container: string, sessionName?: string): string[] {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const term = process.env['TERM'] ?? 'xterm-256color';\n return [\n 'exec',\n '-it',\n '-e',\n `TERM=${term}`,\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'attach',\n '-t',\n name,\n ];\n}\n\n/**\n * Like {@link buildClaudeAttachArgv}, but for the dashboard's right pane. The\n * dashboard already draws its own bottom status bar, so a second client must\n * not show the inner tmux status bar. We attach via a *grouped* sibling session\n * (`<name>-dash`, `tmux new-session -t <name>`): grouped sessions share the\n * same windows/panes (identical live screen + scrollback) but keep independent\n * session options, so `status off` here does not affect a direct\n * `agentbox claude attach` to `<name>`. The bare `;` elements are tmux's\n * command separator — node-pty spawns docker without a shell, so they reach\n * tmux verbatim. `new-session -A -d` is a no-op if the grouped session already\n * exists; `attach` runs after `status off` so the footer is gone on first paint.\n */\nexport function buildClaudeDashboardAttachArgv(\n container: string,\n sessionName?: string,\n): string[] {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const dash = `${name}-dash`;\n const term = process.env['TERM'] ?? 'xterm-256color';\n return [\n 'exec',\n '-it',\n '-e',\n `TERM=${term}`,\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'new-session',\n '-A',\n '-d',\n '-s',\n dash,\n '-t',\n name,\n ';',\n 'set',\n '-t',\n dash,\n 'status',\n 'off',\n ';',\n 'attach',\n '-t',\n dash,\n ];\n}\n\n/**\n * tmux command-list (separator-prefixed) that styles the claude session's\n * status bar: ` agentbox ▸ <box> ` on the left, ` Ctrl-b d detach ` on the\n * right, dark bar, no window-list clutter — visually consistent with the\n * dashboard's own `statusLine()`.\n *\n * Appended after `tmux new-session …` in {@link startClaudeSession}; the bare\n * `;` elements are tmux's command separator (execa array args, no host shell,\n * so they reach tmux verbatim). `status-*` are session options scoped with\n * `-t <session>` — the dashboard's grouped `<name>-dash` runs its own\n * `status off` and is unaffected.\n *\n * The box name is injected as a literal string (known host-side at session\n * start) rather than read in-box via tmux `#()` — tmux runs `status-left`\n * through strftime, so a shell-substituted value was fragile (a `%`/clock\n * artifact slipped through). Box names are a restricted charset (no `%`, `#`,\n * spaces, or globs), so the literal is strftime- and tmux-format-safe as-is.\n */\nexport function buildClaudeStatusBarArgs(sessionName: string, boxName: string): string[] {\n const s = sessionName;\n const name = boxName;\n return [\n ';', 'set', '-t', s, 'status-interval', '60',\n ';', 'set', '-t', s, 'status-justify', 'left',\n ';', 'set', '-t', s, 'status-style', 'bg=colour236,fg=colour250',\n ';', 'set', '-t', s, 'status-left-length', '60',\n ';', 'set', '-t', s, 'status-left', `#[fg=colour16,bg=colour39,bold] agentbox ▸ ${name} #[default] `,\n ';', 'set', '-t', s, 'status-right-length', '30',\n ';', 'set', '-t', s, 'status-right', '#[fg=colour245]Ctrl-b d detach ',\n ';', 'set', '-t', s, 'window-status-format', '',\n ';', 'set', '-t', s, 'window-status-current-format', '',\n ];\n}\n\n/**\n * The `docker` argv for an interactive login shell in a box — the same shape\n * `agentbox shell` uses (vscode user, image WORKDIR `/workspace`, `bash -l`).\n * Handed to node-pty by the dashboard's \"open a shell\" action.\n */\nexport function buildShellArgv(container: string): string[] {\n const term = process.env['TERM'] ?? 'xterm-256color';\n return ['exec', '-it', '-e', `TERM=${term}`, '--user', CONTAINER_USER, container, 'bash', '-l'];\n}\n\nexport function formatDetachNotice(ref: string): string {\n return `Session detached. Reattach with: agentbox claude attach ${ref}`;\n}\n\nexport function attachClaudeSession(\n container: string,\n sessionName?: string,\n reattachRef?: string,\n): never {\n const child = spawnSync('docker', buildClaudeAttachArgv(container, sessionName), {\n stdio: 'inherit',\n });\n const code = child.status ?? 0;\n if (reattachRef && code === 0) {\n // Overwrite tmux's own `[detached (from session …)]` line (printed just\n // above the cursor on a clean detach). Best-effort cosmetics: if the\n // terminal ignores the cursor moves, our line still prints below it.\n process.stdout.write('\\x1b[1A\\x1b[2K\\r' + formatDetachNotice(reattachRef) + '\\n');\n }\n process.exit(code);\n}\n\nexport interface ClaudeSessionInfo {\n running: boolean;\n sessionName: string;\n /** ISO-8601 timestamp from tmux's `#{session_created}` format string, or null when not running. */\n startedAt: string | null;\n}\n\n/**\n * Best-effort: returns `{ running: false, …, startedAt: null }` for any non-zero exit\n * from `tmux has-session` (which includes \"no server running\" and \"no such session\").\n */\nexport async function claudeSessionInfo(\n container: string,\n sessionName?: string,\n): Promise<ClaudeSessionInfo> {\n const name = sessionName ?? DEFAULT_CLAUDE_SESSION;\n const has = await execa(\n 'docker',\n ['exec', '--user', CONTAINER_USER, container, 'tmux', 'has-session', '-t', name],\n { reject: false },\n );\n if (has.exitCode !== 0) {\n return { running: false, sessionName: name, startedAt: null };\n }\n const ts = await execa(\n 'docker',\n [\n 'exec',\n '--user',\n CONTAINER_USER,\n container,\n 'tmux',\n 'display-message',\n '-p',\n '-t',\n name,\n '#{session_created}',\n ],\n { reject: false },\n );\n let startedAt: string | null = null;\n if (ts.exitCode === 0) {\n const secs = Number.parseInt((ts.stdout ?? '').trim(), 10);\n if (Number.isFinite(secs) && secs > 0) startedAt = new Date(secs * 1000).toISOString();\n }\n return { running: true, sessionName: name, startedAt };\n}\n\nexport interface PullClaudeResult {\n /**\n * Box-installed extensions not present on the host. `category` is one of\n * skills/agents/commands (then `name` is the dir name) or `plugins` (then\n * `name` is the `<marketplace>/<plugin>` cache key).\n */\n newItems: Array<{ category: string; name: string }>;\n /** Registry JSONs that gained box-only entries (e.g. `known_marketplaces.json`). */\n mergedRegistries: string[];\n}\n\nexport interface PullClaudeOptions {\n /** Image for the throwaway helper container; use the box's image to avoid extra pulls. */\n image: string;\n /** When true, compute the delta but write nothing. */\n dryRun?: boolean;\n}\n\nconst PULL_DIR_CATEGORIES = ['skills', 'agents', 'commands'] as const;\n\n/**\n * Immediate child item names of `dir`, or [] if it doesn't exist. Symlinks\n * count: the host's `~/.claude/skills/<name>` is a symlink into `~/.agents`\n * (Claude Code's user-skills convention), so `isDirectory()` alone would miss\n * them and every host skill would look \"new\".\n */\nasync function listChildDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);\n } catch {\n return [];\n }\n}\n\nasync function readJsonFile(path: string): Promise<unknown> {\n try {\n return JSON.parse(await readFile(path, 'utf8'));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reverse of {@link ensureClaudeVolume}: pull box-installed Claude extensions\n * (skills/agents/commands dirs + plugins) from the claude-config volume back to\n * the host's `~/.claude`. Additive only — an item already present on the host\n * is never overwritten. The box need not be running; we read the *volume* via a\n * throwaway helper container (the exact mirror of the forward sync), so this\n * also works while the box is stopped.\n *\n * Plugin registry JSONs (`installed_plugins.json`, `known_marketplaces.json`)\n * are merged host-side: only box-only keys are added, with the forward sync's\n * `/home/vscode/.claude/plugins/` rewrite reversed back to the host path.\n */\nexport async function pullClaudeExtras(\n spec: ClaudeConfigSpec,\n opts: PullClaudeOptions,\n): Promise<PullClaudeResult> {\n const hostHome = homedir();\n const hostClaude = join(hostHome, '.claude');\n\n // Inventory pass: enumerate the volume's contents via a read-only helper\n // container. `--user 0` so root can read files claude wrote as uid 1000.\n // base64 -w0 keeps each registry JSON on one parseable line.\n const inventoryScript = [\n 'for cat in skills agents commands; do',\n ' [ -d \"/src/$cat\" ] || continue;',\n ' for d in \"/src/$cat\"/*/; do',\n ' [ -d \"$d\" ] || continue;',\n ' printf \"DIR %s %s\\\\n\" \"$cat\" \"$(basename \"$d\")\";',\n ' done;',\n 'done;',\n 'if [ -d /src/plugins/cache ]; then',\n ' for m in /src/plugins/cache/*/; do',\n ' [ -d \"$m\" ] || continue;',\n ' for p in \"$m\"*/; do',\n ' [ -d \"$p\" ] || continue;',\n ' printf \"PLUGIN %s/%s\\\\n\" \"$(basename \"$m\")\" \"$(basename \"$p\")\";',\n ' done;',\n ' done;',\n 'fi;',\n 'for f in installed_plugins known_marketplaces; do',\n ' [ -f \"/src/plugins/$f.json\" ] || continue;',\n ' printf \"JSON %s \" \"$f\";',\n ' base64 -w0 \"/src/plugins/$f.json\";',\n ' printf \"\\\\n\";',\n 'done',\n ].join(' ');\n\n const inv = await execa(\n 'docker',\n ['run', '--rm', '--user', '0', '-v', `${spec.volume}:/src:ro`, opts.image, 'sh', '-c', inventoryScript],\n { reject: false },\n );\n if (inv.exitCode !== 0) {\n throw new ClaudeSessionError(\n `failed to read claude-config volume ${spec.volume}: ${(inv.stderr ?? '').toString().trim() || `exit ${String(inv.exitCode)}`}`,\n );\n }\n\n const boxDirs: Record<string, string[]> = { skills: [], agents: [], commands: [] };\n const boxPlugins: string[] = [];\n const boxJson: Record<string, unknown> = {};\n for (const line of (inv.stdout ?? '').split('\\n')) {\n if (line.startsWith('DIR ')) {\n const rest = line.slice(4);\n const sp = rest.indexOf(' ');\n if (sp === -1) continue;\n const cat = rest.slice(0, sp);\n const name = rest.slice(sp + 1);\n if (cat in boxDirs) boxDirs[cat]!.push(name);\n } else if (line.startsWith('PLUGIN ')) {\n boxPlugins.push(line.slice(7));\n } else if (line.startsWith('JSON ')) {\n const rest = line.slice(5);\n const sp = rest.indexOf(' ');\n if (sp === -1) continue;\n const which = rest.slice(0, sp);\n try {\n boxJson[which] = JSON.parse(Buffer.from(rest.slice(sp + 1), 'base64').toString('utf8'));\n } catch {\n // Leave undefined; the merge helpers tolerate it.\n }\n }\n }\n\n // Compute deltas host-side (the host ~/.claude is directly accessible —\n // only the volume needed a container).\n const newItems: PullClaudeResult['newItems'] = [];\n const applyPaths: Array<{ src: string; dest: string }> = [];\n for (const cat of PULL_DIR_CATEGORIES) {\n const hostNames = await listChildDirs(join(hostClaude, cat));\n const excludes = cat === 'skills' ? SKILL_EXCLUDE_PREFIXES : [];\n for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {\n newItems.push({ category: cat, name });\n applyPaths.push({ src: `/src/${cat}/${name}`, dest: `/dst/${cat}/${name}` });\n }\n }\n const hostPluginKeys: string[] = [];\n for (const m of await listChildDirs(join(hostClaude, 'plugins', 'cache'))) {\n for (const p of await listChildDirs(join(hostClaude, 'plugins', 'cache', m))) {\n hostPluginKeys.push(`${m}/${p}`);\n }\n }\n for (const key of pickNewItems(boxPlugins, hostPluginKeys)) {\n newItems.push({ category: 'plugins', name: key });\n applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });\n }\n\n // Additive merge of the two plugin registries (reverses the forward path\n // rewrite). Computed regardless so the preview can report it.\n const hostInstalled = await readJsonFile(join(hostClaude, 'plugins', 'installed_plugins.json'));\n const hostMarkets = await readJsonFile(join(hostClaude, 'plugins', 'known_marketplaces.json'));\n const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson['installed_plugins'], {\n hostHome,\n });\n const mergedMarkets = mergeKnownMarketplaces(hostMarkets, boxJson['known_marketplaces'], {\n hostHome,\n });\n const mergedRegistries: string[] = [];\n if (mergedInstalled.changed) mergedRegistries.push('installed_plugins.json');\n if (mergedMarkets.changed) mergedRegistries.push('known_marketplaces.json');\n\n if (opts.dryRun || (newItems.length === 0 && mergedRegistries.length === 0)) {\n return { newItems, mergedRegistries };\n }\n\n // Apply pass: rsync each new item dir from the volume into the host\n // ~/.claude bind mount. --ignore-existing is belt-and-suspenders (the\n // host-side delta is the real guard); --exclude=node_modules because the\n // box carries linux/amd64 binaries useless on the darwin host (claude/host\n // rebuilds lazily, same rationale as the forward sync's exclude).\n if (applyPaths.length > 0) {\n const cmds = applyPaths.map(({ src, dest }) => {\n const parent = dest.slice(0, dest.lastIndexOf('/'));\n return `mkdir -p '${parent}' && rsync -a --ignore-existing --exclude=node_modules '${src}/' '${dest}/'`;\n });\n const apply = await execa(\n 'docker',\n [\n 'run',\n '--rm',\n '--user',\n '0',\n '-v',\n `${spec.volume}:/src:ro`,\n '-v',\n `${hostClaude}:/dst`,\n opts.image,\n 'sh',\n '-c',\n cmds.join(' && '),\n ],\n { reject: false },\n );\n if (apply.exitCode !== 0) {\n throw new ClaudeSessionError(\n `failed to copy extensions from ${spec.volume}: ${(apply.stderr ?? '').toString().trim() || `exit ${String(apply.exitCode)}`}`,\n );\n }\n }\n\n // Registry JSONs are written host-side (host path is directly writable;\n // no container needed) — only when the merge actually added keys.\n if (mergedMarkets.changed || mergedInstalled.changed) {\n await mkdir(join(hostClaude, 'plugins'), { recursive: true });\n if (mergedMarkets.changed) {\n await writeFile(\n join(hostClaude, 'plugins', 'known_marketplaces.json'),\n `${JSON.stringify(mergedMarkets.data, null, 2)}\\n`,\n );\n }\n if (mergedInstalled.changed) {\n await writeFile(\n join(hostClaude, 'plugins', 'installed_plugins.json'),\n `${JSON.stringify(mergedInstalled.data, null, 2)}\\n`,\n );\n }\n }\n\n return { newItems, mergedRegistries };\n}\n","export interface HookFilterResult<T = unknown> {\n data: T;\n removedCommands: string[];\n}\n\n/**\n * Predicate for the filter: does `command` look like it points at a host path\n * outside the project, i.e. under the user's host home directory?\n *\n * Uses `.includes(hostHome + '/')` so we also catch shell-quoted forms like\n * `bash -c '/Users/marco/.config/iterm2/cc-status'`. The trailing slash gates\n * against false matches on similar-prefix home dirs (e.g. `/Users/marco`\n * shouldn't match `/Users/marco-other/...`).\n *\n * Returns false when `hostHome` is empty so we degrade safely if no home is\n * resolvable for some reason.\n */\nexport function isHostPathHookCommand(command: string, hostHome: string): boolean {\n if (typeof command !== 'string' || command.length === 0) return false;\n if (hostHome.length === 0) return false;\n return command.includes(hostHome + '/');\n}\n\ninterface HookLeaf {\n type?: string;\n command?: string;\n [k: string]: unknown;\n}\n\ninterface HookMatcherEntry {\n hooks?: unknown;\n [k: string]: unknown;\n}\n\n/**\n * Walk Claude Code's documented `hooks.<Trigger>[].hooks[]` structure and drop\n * any leaf `{ type: 'command', command: '<host-path>' }` whose `command` matches\n * {@link isHostPathHookCommand}. Empty `hooks: []` arrays and their matcher\n * wrappers are left intact — they don't break Claude and avoiding recursive\n * cleanup keeps the filter predictable.\n *\n * Returns a deep clone; input is not mutated. Tolerant of unexpected shapes\n * (string, number, null at any level): unrecognized branches pass through\n * unchanged, removed count stays accurate.\n */\nexport function filterHostHooks<T = unknown>(data: T, hostHome: string): HookFilterResult<T> {\n // structuredClone is in node >= 17; the repo targets node20, so this is safe.\n const clone = structuredClone(data) as unknown;\n const removedCommands: string[] = [];\n\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, removedCommands };\n }\n\n const top = clone as { hooks?: unknown };\n const hooksRoot = top.hooks;\n if (hooksRoot === null || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot)) {\n return { data: clone as T, removedCommands };\n }\n\n for (const triggerName of Object.keys(hooksRoot as Record<string, unknown>)) {\n const triggerValue = (hooksRoot as Record<string, unknown>)[triggerName];\n if (!Array.isArray(triggerValue)) continue;\n for (const entry of triggerValue) {\n if (entry === null || typeof entry !== 'object') continue;\n const matcher = entry as HookMatcherEntry;\n const inner = matcher.hooks;\n if (!Array.isArray(inner)) continue;\n // In-place filter on the cloned array.\n for (let i = inner.length - 1; i >= 0; i--) {\n const leaf = inner[i] as HookLeaf | null;\n if (leaf === null || typeof leaf !== 'object') continue;\n if (\n leaf.type === 'command' &&\n typeof leaf.command === 'string' &&\n isHostPathHookCommand(leaf.command, hostHome)\n ) {\n removedCommands.push(leaf.command);\n inner.splice(i, 1);\n }\n }\n }\n }\n\n return { data: clone as T, removedCommands };\n}\n\nexport interface ClearInstallMethodResult<T = unknown> {\n data: T;\n cleared: boolean;\n}\n\nexport interface AddProjectAliasResult<T = unknown> {\n data: T;\n aliased: boolean;\n}\n\n/**\n * Claude Code keys project-scoped state (history, mcpServers, enabledPlugins,\n * trust prompts) under `projects[<absolute-workspace-path>]` in\n * `~/.claude.json`. On the host the key is something like\n * `/Users/marco/Projects/foo`; inside the box the workspace is always\n * `/workspace`. Without rewriting, the box never sees the host's project-\n * scoped settings.\n *\n * Copy (don't move) the host-keyed entry to `toPath` if present. Existing\n * `projects[toPath]` is preserved by merging the host entry on top — host\n * is authoritative for keys it sets; box-only keys (e.g. session ids\n * accumulated inside earlier boxes) stay intact.\n *\n * No-op (returns `aliased: false`) when:\n * - data isn't an object, or `projects` isn't an object\n * - fromPath equals toPath\n * - projects[fromPath] doesn't exist or isn't an object\n *\n * Returns a deep-cloned, modified copy; input is not mutated.\n */\nexport function addProjectAlias<T = unknown>(\n data: T,\n fromPath: string,\n toPath: string,\n): AddProjectAliasResult<T> {\n const clone = structuredClone(data) as unknown;\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, aliased: false };\n }\n if (fromPath === toPath || fromPath.length === 0 || toPath.length === 0) {\n return { data: clone as T, aliased: false };\n }\n const obj = clone as { projects?: unknown };\n const projects = obj.projects;\n if (projects === null || typeof projects !== 'object' || Array.isArray(projects)) {\n return { data: clone as T, aliased: false };\n }\n const projectsMap = projects as Record<string, unknown>;\n const src = projectsMap[fromPath];\n if (src === null || typeof src !== 'object' || Array.isArray(src)) {\n return { data: clone as T, aliased: false };\n }\n const existing = projectsMap[toPath];\n if (existing !== null && typeof existing === 'object' && !Array.isArray(existing)) {\n projectsMap[toPath] = { ...(existing as Record<string, unknown>), ...(src as Record<string, unknown>) };\n } else {\n projectsMap[toPath] = structuredClone(src);\n }\n return { data: clone as T, aliased: true };\n}\n\n/**\n * Drop the top-level `installMethod` field from a parsed `~/.claude.json`.\n *\n * The host records how *the host* was installed (e.g. `\"native\"` on macOS,\n * `\"npm-global\"` for npm users). The box now uses the native installer, so\n * the field usually matches; but if a user's host recorded a different\n * value, it would clash with the box's binary location and trigger an\n * `installMethod is X, but <dir> does not exist` integrity warning.\n * Stripping the field defensively lets claude inside the box redetect.\n *\n * Returns a deep-cloned, scrubbed copy plus a flag indicating whether the\n * field was actually present. Input is not mutated.\n */\nexport function clearInstallMethod<T = unknown>(data: T): ClearInstallMethodResult<T> {\n const clone = structuredClone(data) as unknown;\n if (clone === null || typeof clone !== 'object' || Array.isArray(clone)) {\n return { data: clone as T, cleared: false };\n }\n const obj = clone as Record<string, unknown>;\n if (Object.prototype.hasOwnProperty.call(obj, 'installMethod')) {\n delete obj.installMethod;\n return { data: clone as T, cleared: true };\n }\n return { data: clone as T, cleared: false };\n}\n","/**\n * Pure, docker-free helpers for `agentbox pull claude` (box -> host pull of\n * Claude extensions). Kept separate from `claude.ts` so the delta + JSON-merge\n * logic is unit-testable without spawning containers — mirrors how\n * `claude-hooks-filter.ts` factors the forward-sync transforms.\n *\n * The forward sync (`ensureClaudeVolume` in claude.ts) is host-authoritative\n * and rewrites `$HOST_HOME/.claude/plugins/` -> `/home/vscode/.claude/plugins/`\n * in the plugin registry JSONs. This module is the reverse: additive (host\n * wins, only missing items are added) and rewrites the container path back to\n * the host path.\n */\n\n/** Categories under ~/.claude we pull box-side additions for. */\nexport const PULL_CATEGORIES = ['skills', 'plugins', 'agents', 'commands'] as const;\nexport type PullCategory = (typeof PULL_CATEGORIES)[number];\n\n/**\n * Skills whose directory name starts with one of these prefixes are agentbox's\n * own (currently just `agentbox-setup`, installed onto the host by\n * `apps/cli/src/wizard.ts`). They round-trip into the box via the forward sync;\n * pulling them back would be a no-op at best and noise at worst, so we never\n * treat them as user-authored additions.\n */\nexport const SKILL_EXCLUDE_PREFIXES = ['agentbox-'] as const;\n\n/** Container path prefix the forward sync rewrites host plugin paths to. */\nexport const CONTAINER_PLUGINS_PREFIX = '/home/vscode/.claude/plugins/';\n\n/**\n * Set-difference of `boxNames` against `hostNames`, dropping any name that\n * starts with one of `excludePrefixes`. Result is sorted for stable output.\n * Used for skills/agents/commands (top-level dir names) and plugin cache\n * (`<marketplace>/<plugin>` keys).\n */\nexport function pickNewItems(\n boxNames: string[],\n hostNames: string[],\n excludePrefixes: readonly string[] = [],\n): string[] {\n const host = new Set(hostNames);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const name of boxNames) {\n if (name.length === 0 || host.has(name) || seen.has(name)) continue;\n if (excludePrefixes.some((p) => name.startsWith(p))) continue;\n seen.add(name);\n out.push(name);\n }\n return out.sort();\n}\n\n/**\n * Rewrite the forward sync's container plugin prefix back to the host's, in\n * every string anywhere in `value`. Generic over the JSON shape so it covers\n * both `installLocation` (known_marketplaces.json) and `installPath`\n * (installed_plugins.json) plus any future path-bearing field.\n */\nfunction rewritePluginPaths<T>(value: T, hostPluginsPrefix: string): T {\n if (typeof value === 'string') {\n return value.split(CONTAINER_PLUGINS_PREFIX).join(hostPluginsPrefix) as unknown as T;\n }\n if (Array.isArray(value)) {\n return value.map((v) => rewritePluginPaths(v, hostPluginsPrefix)) as unknown as T;\n }\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n out[k] = rewritePluginPaths(v, hostPluginsPrefix);\n }\n return out as T;\n }\n return value;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nexport interface MergeResult {\n /** Merged JSON to write back to the host (the host object plus added keys). */\n data: unknown;\n /** True when at least one key was added (caller should write on true only). */\n changed: boolean;\n /** Keys added to the host map, for the preview/summary. */\n addedKeys: string[];\n}\n\n/**\n * Additive object-key merge. For every key in `boxMap` absent from `hostMap`,\n * copy the box's value in (with plugin paths rewritten to the host prefix).\n * Existing host keys are never touched. Tolerant of missing / non-object\n * inputs: returns the host value unchanged with `changed: false` (matches\n * `maybeFilterTo`'s defensiveness in claude.ts).\n *\n * `selectMap` projects the registry object to the map being merged\n * (identity for known_marketplaces.json, `.plugins` for installed_plugins.json)\n * and `withMap` writes the merged map back into a clone of the host registry.\n */\nfunction additiveMerge(\n hostRoot: unknown,\n boxRoot: unknown,\n hostPluginsPrefix: string,\n selectMap: (root: unknown) => unknown,\n withMap: (hostRoot: unknown, mergedMap: Record<string, unknown>) => unknown,\n): MergeResult {\n const hostMap = selectMap(hostRoot);\n const boxMap = selectMap(boxRoot);\n if (!isPlainObject(boxMap)) {\n return { data: hostRoot, changed: false, addedKeys: [] };\n }\n const base: Record<string, unknown> = isPlainObject(hostMap) ? { ...hostMap } : {};\n const addedKeys: string[] = [];\n for (const [key, value] of Object.entries(boxMap)) {\n if (Object.prototype.hasOwnProperty.call(base, key)) continue;\n base[key] = rewritePluginPaths(value, hostPluginsPrefix);\n addedKeys.push(key);\n }\n if (addedKeys.length === 0) {\n return { data: hostRoot, changed: false, addedKeys: [] };\n }\n return { data: withMap(hostRoot, base), changed: true, addedKeys: addedKeys.sort() };\n}\n\n/**\n * known_marketplaces.json is a flat object keyed by marketplace name\n * (`{ \"<name>\": { source, installLocation, lastUpdated } }`). Add box-only\n * marketplaces; rewrite their `installLocation` back to the host path.\n */\nexport function mergeKnownMarketplaces(\n hostJson: unknown,\n boxJson: unknown,\n opts: { hostHome: string },\n): MergeResult {\n const prefix = `${opts.hostHome}/.claude/plugins/`;\n return additiveMerge(\n isPlainObject(hostJson) ? hostJson : {},\n boxJson,\n prefix,\n (root) => root,\n (_host, merged) => merged,\n );\n}\n\n/**\n * installed_plugins.json is `{ version, plugins: { \"<name>@<mkt>\": [...] } }`.\n * Add box-only entries under `.plugins`; rewrite each entry's `installPath`\n * back to the host path. The top-level `version` and any other host keys are\n * preserved as-is.\n */\nexport function mergeInstalledPlugins(\n hostJson: unknown,\n boxJson: unknown,\n opts: { hostHome: string },\n): MergeResult {\n const prefix = `${opts.hostHome}/.claude/plugins/`;\n const hostRoot = isPlainObject(hostJson) ? hostJson : { plugins: {} };\n return additiveMerge(\n hostRoot,\n boxJson,\n prefix,\n (root) => (isPlainObject(root) ? (root as Record<string, unknown>)['plugins'] : undefined),\n (host, merged) => ({ ...(host as Record<string, unknown>), plugins: merged }),\n );\n}\n","import { execInBox } from './docker.js';\n\nexport interface DockerdLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * Shared image-cache volume across boxes. When a box is created with\n * `dockerCacheShared=true`, its in-box dockerd's data root is this volume\n * instead of the per-box `agentbox-docker-<id>` volume. Mutually exclusive at\n * runtime: only one box can hold the lock on `/var/lib/docker` at a time —\n * dockerd's own boltdb lock will refuse a second start. This is a power-user\n * feature for users who run boxes serially and want pulled layers to persist\n * across recreations; documented as such on the CLI flag.\n */\nexport const SHARED_DOCKER_CACHE_VOLUME = 'agentbox-docker-cache';\n\nexport function dockerVolumeName(boxId: string, shared: boolean): string {\n return shared ? SHARED_DOCKER_CACHE_VOLUME : `agentbox-docker-${boxId}`;\n}\n\n/**\n * Spawn the in-container dockerd via `/usr/local/bin/agentbox-dockerd-start`\n * detached, then poll for `/var/run/docker.sock` to become accept()-able.\n * Best-effort, mirroring {@link launchVncDaemon} — failure is logged but\n * doesn't fail box creation. Default timeout 30s: first start has to\n * initialize iptables + the fuse-overlayfs graphdriver, which is slower than\n * the VNC stack.\n */\nexport async function launchDockerdDaemon(\n container: string,\n timeoutMs = 30_000,\n): Promise<DockerdLaunchResult> {\n const result = await execInBox(container, ['/usr/local/bin/agentbox-dockerd-start'], {\n user: 'root',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const probe = await execInBox(\n container,\n [\n 'bash',\n '-lc',\n '[ -S /var/run/docker.sock ] && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1',\n ],\n { user: 'root' },\n );\n if (probe.exitCode === 0) return { up: true };\n await new Promise((r) => setTimeout(r, 200));\n }\n return { up: false, reason: `dockerd did not become ready within ${String(timeoutMs)}ms` };\n}\n","import { randomBytes } from 'node:crypto';\nimport { execInBox } from './docker.js';\n\nexport interface VncLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * Spawn the in-container VNC supervisor (`/usr/local/bin/agentbox-vnc-start`)\n * detached, then poll the container's TCP 6080 to confirm websockify is up.\n * Best-effort, mirroring {@link launchCtlDaemon} — failure is logged but\n * doesn't fail box creation. The password reaches the script through the\n * container's AGENTBOX_VNC_PASSWORD env, set at `docker run` time, so we don't\n * need `-e` on the exec (and the re-launch path on `agentbox start` works\n * without it too).\n */\nexport async function launchVncDaemon(\n container: string,\n timeoutMs = 5000,\n): Promise<VncLaunchResult> {\n const result = await execInBox(container, ['/usr/local/bin/agentbox-vnc-start'], {\n user: 'vscode',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const probe = await execInBox(\n container,\n ['bash', '-lc', '(echo > /dev/tcp/127.0.0.1/6080) 2>/dev/null'],\n { user: 'vscode' },\n );\n if (probe.exitCode === 0) return { up: true };\n await new Promise((r) => setTimeout(r, 150));\n }\n return { up: false, reason: `websockify did not bind 6080 within ${String(timeoutMs)}ms` };\n}\n\nconst VNC_PASSWORD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n/**\n * 8-char password from a 62-symbol alphabet. The 8-char cap is a real RFB\n * protocol limit — VncAuth truncates at compare time, so longer passwords give\n * no security gain. 62^8 ≈ 47 bits; adequate for the loopback-bound surface\n * we expose (host port pinned to 127.0.0.1 + OrbStack's name-based routing,\n * neither of which is reachable from off-host without explicit tunnelling).\n */\nexport function generateVncPassword(): string {\n const bytes = randomBytes(8);\n let out = '';\n for (let i = 0; i < 8; i++) {\n out += VNC_PASSWORD_ALPHABET[bytes[i]! % VNC_PASSWORD_ALPHABET.length];\n }\n return out;\n}\n\n/**\n * Container port the VNC web client (noVNC) binds inside the box. Fixed today;\n * stored on BoxRecord for future-proofing if we ever support multiple displays.\n */\nexport const VNC_CONTAINER_PORT = 6080;\n\nexport interface VncUrls {\n /** OrbStack name-based URL, e.g. http://agentbox-foo.orb.local:6080/... Present only on OrbStack hosts. */\n orbUrl?: string;\n /** Loopback URL via the auto-allocated host port, e.g. http://127.0.0.1:54321/... Present whenever vncHostPort is known. */\n loopbackUrl?: string;\n}\n\n/**\n * Build the noVNC URLs for a box, given the box record + (host engine).\n * `engine === 'orbstack'` triggers the `<container>.orb.local:6080` route;\n * either engine produces the loopback URL when the host port is resolved.\n * Returns an empty object when VNC isn't enabled or the password isn't known.\n */\nexport function buildVncUrls(\n record: {\n container: string;\n vncEnabled?: boolean;\n vncHostPort?: number;\n vncContainerPort?: number;\n vncPassword?: string;\n },\n engine: 'orbstack' | 'docker-desktop' | 'other',\n): VncUrls {\n if (!record.vncEnabled || !record.vncPassword) return {};\n const containerPort = record.vncContainerPort ?? VNC_CONTAINER_PORT;\n const qs = `autoconnect=1&password=${encodeURIComponent(record.vncPassword)}`;\n const urls: VncUrls = {};\n if (engine === 'orbstack') {\n urls.orbUrl = `http://${record.container}.orb.local:${String(containerPort)}/vnc.html?${qs}`;\n }\n if (record.vncHostPort) {\n urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;\n }\n return urls;\n}\n","/**\n * The single container port AgentBox reserves + publishes for a box's web\n * service. The `-p 127.0.0.1:0:80` mapping is created unconditionally at\n * `create` (immutable after `docker run`); the in-box supervisor forwards :80\n * to the `expose:`-flagged service's port once `agentbox.yaml` is set. Mirrors\n * `VNC_CONTAINER_PORT`. Must equal `RESERVED_WEB_PORT` in @agentbox/ctl.\n */\nexport const WEB_CONTAINER_PORT = 80;\n","import { execa } from 'execa';\nimport { readdir, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport interface DetectedGitRepo {\n kind: 'root' | 'nested';\n /** Absolute host path of the repo working tree (== `<workspace>` for root). */\n hostMainRepo: string;\n /** Path relative to the workspace where the repo lives. Empty string for root. */\n relPathFromWorkspace: string;\n}\n\n/**\n * Look for `.git` directories at the workspace root and at every 1st-level\n * subdirectory. Worktree-form `.git` files (regular file containing\n * `gitdir: …`) are intentionally skipped — turning an existing worktree into\n * another worktree gets weird, and the user case for it is rare.\n */\nexport async function detectGitRepos(workspace: string): Promise<DetectedGitRepo[]> {\n const out: DetectedGitRepo[] = [];\n if (await isGitDir(join(workspace, '.git'))) {\n out.push({ kind: 'root', hostMainRepo: workspace, relPathFromWorkspace: '' });\n }\n let entries: Array<{ name: string; isDirectory: () => boolean }>;\n try {\n entries = await readdir(workspace, { withFileTypes: true });\n } catch {\n return out;\n }\n for (const e of entries) {\n if (!e.isDirectory() || e.name.startsWith('.')) continue;\n const sub = join(workspace, e.name);\n if (await isGitDir(join(sub, '.git'))) {\n out.push({ kind: 'nested', hostMainRepo: sub, relPathFromWorkspace: e.name });\n }\n }\n return out;\n}\n\nasync function isGitDir(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport interface CreateBoxWorktreeArgs {\n hostMainRepo: string;\n branchName: string;\n worktreeDir: string;\n onLog?: (line: string) => void;\n}\n\nexport interface CreateBoxWorktreeResult {\n branchName: string;\n /** Tracked-changes stash SHA, if any uncommitted state was present. */\n stashSha: string | null;\n untrackedCount: number;\n}\n\n/**\n * Create a per-box worktree on a fresh branch, carrying over the host's\n * uncommitted tracked + untracked state so the agent picks up where the user\n * left off. The host's working directory is left untouched.\n */\nexport async function createBoxWorktree(\n args: CreateBoxWorktreeArgs,\n): Promise<CreateBoxWorktreeResult> {\n const log = args.onLog ?? (() => {});\n\n // `stash create` produces a stash commit without touching the working tree\n // or stash list. Empty output means \"no tracked changes\" (a clean main).\n const stash = await execa('git', ['-C', args.hostMainRepo, 'stash', 'create'], {\n reject: false,\n });\n const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;\n\n const untracked = await execa(\n 'git',\n ['-C', args.hostMainRepo, 'ls-files', '--others', '--exclude-standard', '-z'],\n { reject: false },\n );\n const untrackedList =\n untracked.exitCode === 0 && untracked.stdout.length > 0\n ? untracked.stdout.split('\\0').filter((s) => s.length > 0)\n : [];\n\n // `git worktree add` creates the target dir itself; we only need to ensure\n // the parent exists (the caller does that).\n const branchName = await pickFreshBranch(args.hostMainRepo, args.branchName);\n const wadd = await execa(\n 'git',\n ['-C', args.hostMainRepo, 'worktree', 'add', '-b', branchName, args.worktreeDir, 'HEAD'],\n { reject: false },\n );\n if (wadd.exitCode !== 0) {\n throw new GitWorktreeError(\n `git worktree add failed for ${args.hostMainRepo}: ${wadd.stderr || wadd.stdout}`,\n );\n }\n log(`created worktree ${args.worktreeDir} on branch ${branchName}`);\n\n // Boxes don't carry the user's signing keys, so commit.gpgsign=true (a\n // common host setting) would make every in-box `git commit` fail. Enable\n // per-worktree config and disable signing on this worktree only — the\n // user's main checkout keeps signing on.\n await execa(\n 'git',\n ['-C', args.hostMainRepo, 'config', 'extensions.worktreeConfig', 'true'],\n { reject: false },\n );\n await execa(\n 'git',\n ['-C', args.worktreeDir, 'config', '--worktree', 'commit.gpgsign', 'false'],\n { reject: false },\n );\n\n if (stashSha) {\n // `--index` restores the staged-vs-unstaged distinction. On rare conflict\n // (same HEAD — shouldn't happen), fall back to apply-without-index so we\n // at least recover the file contents.\n const withIndex = await execa(\n 'git',\n ['-C', args.worktreeDir, 'stash', 'apply', '--index', stashSha],\n { reject: false },\n );\n if (withIndex.exitCode !== 0) {\n const noIndex = await execa(\n 'git',\n ['-C', args.worktreeDir, 'stash', 'apply', stashSha],\n { reject: false },\n );\n if (noIndex.exitCode !== 0) {\n log(\n `warning: stash apply failed in worktree (${withIndex.stderr || withIndex.stdout || 'no message'})`,\n );\n } else {\n log(`applied tracked changes (without index — staged state lost)`);\n }\n } else {\n log(`applied tracked changes from host main`);\n }\n }\n\n if (untrackedList.length > 0) {\n // One fork: pack the list, stream tar from main → tar into worktree.\n const tarOut = await execa(\n 'tar',\n ['-C', args.hostMainRepo, '--null', '-T', '-', '-cf', '-'],\n {\n input: untrackedList.join('\\0'),\n encoding: 'buffer',\n reject: false,\n },\n );\n if (tarOut.exitCode === 0) {\n const tarIn = await execa('tar', ['-C', args.worktreeDir, '-xf', '-'], {\n input: tarOut.stdout,\n reject: false,\n });\n if (tarIn.exitCode !== 0) {\n log(`warning: untracked-file copy into worktree failed: ${tarIn.stderr}`);\n } else {\n log(`copied ${String(untrackedList.length)} untracked file(s) into worktree`);\n }\n } else {\n log(`warning: tar of untracked files failed: ${tarOut.stderr}`);\n }\n }\n\n return { branchName, stashSha, untrackedCount: untrackedList.length };\n}\n\n/**\n * Pick `<base>`, `<base>-2`, `<base>-3`, … until git reports no such branch\n * exists. Avoids collision when the user reruns `agentbox create -n same-name`\n * after destroying — the destroyed box's branch still lives in the host repo.\n */\nexport async function pickFreshBranch(hostMainRepo: string, base: string): Promise<string> {\n let candidate = base;\n let suffix = 2;\n while (await branchExists(hostMainRepo, candidate)) {\n candidate = `${base}-${String(suffix++)}`;\n if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);\n }\n return candidate;\n}\n\nasync function branchExists(hostMainRepo: string, name: string): Promise<boolean> {\n const result = await execa(\n 'git',\n ['-C', hostMainRepo, 'show-ref', '--verify', '--quiet', `refs/heads/${name}`],\n { reject: false },\n );\n return result.exitCode === 0;\n}\n\nexport interface RemoveBoxWorktreeArgs {\n hostMainRepo: string;\n worktreeDir: string;\n}\n\n/**\n * Remove a per-box worktree. Worktree-remove leaves any in-flight changes in\n * place by default; `--force` strips it. Falls back to manual rm + prune if\n * git refuses (e.g. the dir was already deleted out from under it).\n */\nexport async function removeBoxWorktree(args: RemoveBoxWorktreeArgs): Promise<void> {\n const remove = await execa(\n 'git',\n ['-C', args.hostMainRepo, 'worktree', 'remove', '--force', args.worktreeDir],\n { reject: false },\n );\n if (remove.exitCode === 0) return;\n await execa('rm', ['-rf', args.worktreeDir], { reject: false });\n await execa('git', ['-C', args.hostMainRepo, 'worktree', 'prune'], { reject: false });\n}\n\nexport class GitWorktreeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitWorktreeError';\n }\n}\n","import { execInBox } from './docker.js';\n\nexport interface MountOverlayResult {\n upperWritePath: string;\n}\n\nexport interface NestedWorktreeBind {\n /** Path inside the container the nested worktree should appear at (e.g. /workspace/app). */\n containerPath: string;\n /** Source path inside the container where the worktree was bind-mounted at run time (e.g. /agentbox-worktrees/app). */\n mountFromPath: string;\n}\n\nexport interface MountOverlayOptions {\n /**\n * Ordered list of in-container lower directories, upper-most (highest\n * precedence) first. Composed into `lowerdir=a:b:c`. Defaults to the single\n * base bind `['/host-src']` so non-checkpoint boxes are byte-identical.\n * Checkpoint boxes prepend their captured layer dirs (e.g.\n * `['/checkpoint-layers/0', '/host-src']`).\n */\n lowerDirs?: string[];\n /**\n * Sub-paths under /workspace that should reflect a separate host worktree\n * directly (writes go through to the host, bypassing the FUSE upper layer).\n * Applied as `mount --bind` after fuse-overlayfs is up so the overlay\n * doesn't hide them.\n */\n nestedWorktrees?: NestedWorktreeBind[];\n}\n\nexport const DEFAULT_LOWER_DIRS: readonly string[] = ['/host-src'];\n\n// The `vscode` user provided by mcr.microsoft.com/devcontainers/base:ubuntu.\n// The overlay is mounted as root (FUSE requires it), so without squashing the\n// kernel sees overlay files with their original host UIDs (e.g. 501 on macOS)\n// and refuses writes from vscode under `default_permissions`. Squashing reports\n// everything as owned by vscode so interactive shells can write naturally.\nconst BOX_USER_UID = 1000;\nconst BOX_USER_GID = 1000;\n\n/**\n * Mount the FUSE overlay inside a running box:\n *\n * /workspace = overlay(lower=<lowerDirs joined>, upper=/upper/upper, work=/upper/work)\n *\n * `lowerDirs` is upper-most first; for a plain box it is just `/host-src`,\n * for a checkpoint box it is the captured layer dir(s) stacked over the base.\n *\n * Runs as root inside the container so it can attach to /dev/fuse. If\n * `nestedWorktrees` is provided, each entry is layered on top of /workspace\n * via `mount --bind` after the FUSE overlay is up — bind-after-overlay is the\n * only ordering that survives, since fuse-overlayfs hides any pre-existing\n * mounts under /workspace.\n */\nexport async function mountOverlay(\n container: string,\n opts: MountOverlayOptions = {},\n): Promise<MountOverlayResult> {\n const lowerDirs = opts.lowerDirs && opts.lowerDirs.length > 0 ? opts.lowerDirs : ['/host-src'];\n const mountOpts = [\n `lowerdir=${lowerDirs.join(':')}`,\n 'upperdir=/upper/upper',\n 'workdir=/upper/work',\n `squash_to_uid=${String(BOX_USER_UID)}`,\n `squash_to_gid=${String(BOX_USER_GID)}`,\n ].join(',');\n\n const lines = [\n 'set -euo pipefail',\n 'mkdir -p /upper/upper /upper/work /workspace',\n // Idempotent — if a previous attempt left a stale overlay, unmount first.\n 'mountpoint -q /workspace && fusermount3 -u /workspace || true',\n `fuse-overlayfs -o ${mountOpts} /workspace`,\n 'mountpoint -q /workspace',\n ];\n\n for (const w of opts.nestedWorktrees ?? []) {\n // The bind target lives inside the just-mounted FUSE overlay; make sure\n // the directory exists (mkdir-on-overlay materializes it in /upper). Then\n // unmount any previous bind (idempotent re-runs from startBox) and rebind.\n lines.push(\n `mkdir -p ${shellQuote(w.containerPath)}`,\n `mountpoint -q ${shellQuote(w.containerPath)} && umount ${shellQuote(w.containerPath)} || true`,\n `mount --bind ${shellQuote(w.mountFromPath)} ${shellQuote(w.containerPath)}`,\n `mountpoint -q ${shellQuote(w.containerPath)}`,\n );\n }\n\n const result = await execInBox(container, ['bash', '-lc', lines.join('\\n')], { user: 'root' });\n if (result.exitCode !== 0) {\n throw new OverlayError(\n `failed to mount FUSE overlay in ${container}`,\n result.stdout,\n result.stderr,\n );\n }\n return { upperWritePath: '/upper/upper' };\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n\nexport interface OverlayCheck {\n name: string;\n ok: boolean;\n detail: string;\n}\n\n/**\n * Four-assertion smoke test that proves the overlay actually behaves like an\n * overlay: writes go to upper, lower stays untouched.\n */\nexport async function verifyOverlay(\n container: string,\n lowerDirs: readonly string[] = DEFAULT_LOWER_DIRS,\n): Promise<OverlayCheck[]> {\n const sentinel = '.agentbox-overlay-check';\n const checks: OverlayCheck[] = [];\n\n // 1. lower is visible through the overlay.\n const ls = await execInBox(container, ['bash', '-lc', `ls -A /workspace | head -1`], {\n user: 'root',\n });\n checks.push({\n name: 'workspace lists lower contents',\n ok: ls.exitCode === 0,\n detail: ls.exitCode === 0 ? `first entry: ${ls.stdout.trim() || '(empty)'}` : ls.stderr.trim(),\n });\n\n // 2. write into the overlay.\n const write = await execInBox(container, ['bash', '-lc', `touch /workspace/${sentinel}`], {\n user: 'root',\n });\n checks.push({\n name: 'write through overlay succeeds',\n ok: write.exitCode === 0,\n detail: write.exitCode === 0 ? `created /workspace/${sentinel}` : write.stderr.trim(),\n });\n\n // 3. write landed in the upper volume.\n const upper = await execInBox(container, ['bash', '-lc', `test -f /upper/upper/${sentinel}`], {\n user: 'root',\n });\n checks.push({\n name: 'write lands in /upper (cow target)',\n ok: upper.exitCode === 0,\n detail:\n upper.exitCode === 0\n ? `/upper/upper/${sentinel} exists`\n : `expected /upper/upper/${sentinel} to exist`,\n });\n\n // 4. no lower layer was touched (write must stay in the upper for every\n // stacked lowerdir, not just the base bind).\n const lowerProbe = lowerDirs.map((d) => `test ! -e ${shellQuote(d)}/${sentinel}`).join(' && ');\n const lower = await execInBox(container, ['bash', '-lc', lowerProbe], { user: 'root' });\n checks.push({\n name: `lower untouched (${lowerDirs.join(', ')})`,\n ok: lower.exitCode === 0,\n detail:\n lower.exitCode === 0\n ? `${sentinel} absent from every lower`\n : `${sentinel} leaked into a lower layer`,\n });\n\n // Tidy up the sentinel so subsequent commands don't see it.\n await execInBox(container, ['bash', '-lc', `rm -f /workspace/${sentinel}`], { user: 'root' });\n\n return checks;\n}\n\nexport class OverlayError extends Error {\n constructor(\n message: string,\n public readonly stdout: string,\n public readonly stderr: string,\n ) {\n super(message);\n this.name = 'OverlayError';\n }\n}\n","import { execa } from 'execa';\nimport { mkdir, readdir, rm, stat } from 'node:fs/promises';\nimport { homedir, platform } from 'node:os';\nimport { join, resolve } from 'node:path';\n\n/**\n * Directories whose contents are either platform-specific (built native modules,\n * compiled outputs) or large enough to be wasteful to include in a frozen\n * workspace snapshot. Pruned from the snapshot tree *after* the APFS clone\n * — removing CoW-cloned entries is essentially free.\n */\nexport const EXCLUDE_DIRS: ReadonlySet<string> = new Set([\n 'node_modules',\n '.next',\n '.nuxt',\n '.turbo',\n '.svelte-kit',\n 'dist',\n 'build',\n 'out',\n 'target',\n '.venv',\n '__pycache__',\n '.cache',\n '.parcel-cache',\n]);\n\nexport const SNAPSHOTS_ROOT = join(homedir(), '.agentbox', 'snapshots');\n\nexport function snapshotPathFor(boxId: string): string {\n return join(SNAPSHOTS_ROOT, boxId);\n}\n\n/**\n * Walk a directory tree and return absolute paths of every directory whose\n * basename matches `EXCLUDE_DIRS`. Does not descend into a matched directory.\n * Pure (modulo `fs.readdir`) — easy to unit-test against a fixture tree.\n */\nexport async function findExcludedDirs(\n root: string,\n excluded: ReadonlySet<string> = EXCLUDE_DIRS,\n): Promise<string[]> {\n const matches: string[] = [];\n const walk = async (dir: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const abs = join(dir, entry.name);\n if (excluded.has(entry.name)) {\n matches.push(abs);\n continue; // do not descend\n }\n await walk(abs);\n }\n };\n await walk(root);\n return matches;\n}\n\nexport interface CreateSnapshotOptions {\n source: string;\n destination: string;\n excluded?: ReadonlySet<string>;\n}\n\nexport interface CreateSnapshotResult {\n destination: string;\n prunedPaths: string[];\n}\n\n/**\n * Create a frozen workspace snapshot. On macOS (APFS) this is an instant CoW\n * clone via `cp -cR`; on other platforms it falls back to plain `cp -R`\n * (slow, but functional — the production fallback will be `rsync --exclude`).\n *\n * After the copy, prune all `EXCLUDE_DIRS` directories so the snapshot is free\n * of platform-specific artifacts before it becomes the overlay's lower layer.\n */\nexport async function createSnapshot(opts: CreateSnapshotOptions): Promise<CreateSnapshotResult> {\n const source = resolve(opts.source);\n const destination = resolve(opts.destination);\n const excluded = opts.excluded ?? EXCLUDE_DIRS;\n\n await mkdir(SNAPSHOTS_ROOT, { recursive: true });\n\n // `cp -c` only exists on macOS and is the APFS clone flag.\n const cpArgs = platform() === 'darwin' ? ['-cR'] : ['-R'];\n await execa('cp', [...cpArgs, `${source}/`, destination]);\n\n const toPrune = await findExcludedDirs(destination, excluded);\n await Promise.all(toPrune.map((p) => rm(p, { recursive: true, force: true })));\n\n return { destination, prunedPaths: toPrune };\n}\n\n/** Guard used by tests + by `create.ts` when we don't want to clobber a path. */\nexport async function snapshotExists(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n","import { stat } from 'node:fs/promises';\nimport { execInBox } from './docker.js';\n\nexport interface CtlLaunchResult {\n up: boolean;\n reason?: string;\n}\n\n/**\n * Spawn `agentbox-ctl daemon` detached inside the container and wait briefly\n * for the unix socket to appear on the host-mounted path. Best-effort —\n * failure is logged but doesn't fail box creation, since a missing or empty\n * agentbox.yaml is a perfectly valid state.\n */\nexport async function launchCtlDaemon(\n container: string,\n hostSocketPath: string,\n timeoutMs = 3000,\n): Promise<CtlLaunchResult> {\n const result = await execInBox(container, ['agentbox-ctl', 'daemon'], {\n user: 'vscode',\n detach: true,\n });\n if (result.exitCode !== 0) {\n return { up: false, reason: `docker exec failed: ${result.stderr || result.stdout}` };\n }\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (await pathExists(hostSocketPath)) return { up: true };\n await new Promise((r) => setTimeout(r, 100));\n }\n return {\n up: false,\n reason: `socket ${hostSocketPath} did not appear within ${String(timeoutMs)}ms`,\n };\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n","import { spawn } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { existsSync, openSync } from 'node:fs';\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { request as httpRequest } from 'node:http';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { fileURLToPath } from 'node:url';\nimport {\n DEFAULT_RELAY_PORT,\n RELAY_CONTAINER_NAME,\n RELAY_IMAGE_REF,\n RELAY_NETWORK_NAME,\n type BoxWorktree,\n} from '@agentbox/relay';\nimport { containerExists, removeContainer } from './docker.js';\nimport type { GitWorktreeRecord } from './state.js';\n\nconst STATE_DIR = join(homedir(), '.agentbox');\nconst PID_FILE = join(STATE_DIR, 'relay.pid');\nconst LOG_FILE = join(STATE_DIR, 'relay.log');\n\nexport interface RelayEndpoint {\n /** URL boxes use to reach the relay from inside the container. */\n url: string;\n /** URL host-side processes (the CLI itself) use. */\n hostUrl: string;\n port: number;\n}\n\nconst PORT = DEFAULT_RELAY_PORT;\nconst ENDPOINT: RelayEndpoint = {\n // host.docker.internal is the Docker Desktop / OrbStack-supplied alias for\n // the host's loopback as seen from inside a container. The corresponding\n // `--add-host=host.docker.internal:host-gateway` flag in runBox makes the\n // resolution work on Linux native Docker too.\n url: `http://host.docker.internal:${String(PORT)}`,\n hostUrl: `http://127.0.0.1:${String(PORT)}`,\n port: PORT,\n};\n\nexport interface EnsureRelayOptions {\n onLog?: (line: string) => void;\n}\n\n/**\n * Idempotently bring up the host relay. Spawns the bundled `agentbox-relay`\n * bin as a detached node process bound to 0.0.0.0:8787 (so boxes can reach\n * it via host.docker.internal, and the CLI via 127.0.0.1). Best-effort:\n * failures throw and the caller treats it as \"relay not reachable\".\n *\n * If a legacy relay container from a previous version of agentbox is still\n * around, it's removed first so its bound DNS name doesn't shadow the new\n * host process for any old boxes that happen to still be running.\n */\nexport async function ensureRelay(opts: EnsureRelayOptions = {}): Promise<RelayEndpoint> {\n const log = opts.onLog ?? (() => {});\n await mkdir(STATE_DIR, { recursive: true });\n\n // Migration: kill the old in-docker relay if it's around. The host process\n // wants the same port; the container did NOT publish to host:8787, so there\n // is no actual port collision. We still remove it to avoid confusion (it'd\n // show up in `docker ps -a` forever otherwise).\n if (await containerExists(RELAY_CONTAINER_NAME)) {\n await removeContainer(RELAY_CONTAINER_NAME);\n log(`removed legacy relay container ${RELAY_CONTAINER_NAME}`);\n }\n\n if (await pingHealthz(500)) {\n return ENDPOINT;\n }\n\n const existingPid = await readPidFile();\n if (existingPid !== null && (await processAlive(existingPid))) {\n // Pid exists but healthz isn't responding yet — give it a beat to finish\n // startup. If it stays unresponsive, leave it alone (someone might be\n // debugging it) and let downstream POSTs fail as best-effort.\n for (let i = 0; i < 10; i++) {\n if (await pingHealthz(300)) return ENDPOINT;\n await delay(200);\n }\n log(`relay pid ${String(existingPid)} alive but /healthz unresponsive — proceeding anyway`);\n return ENDPOINT;\n }\n if (existingPid !== null) {\n await unlink(PID_FILE).catch(() => {});\n }\n\n const relayBin = resolveRelayBin();\n const logFd = openSync(LOG_FILE, 'a');\n // The relay shells out to this CLI entry for the checkpoint.create RPC\n // (it only knows the box id; the CLI resolves the rest). Resolve best-effort\n // — if not found the relay's handler reports a clear error.\n const cliEntry = resolveCliEntry();\n const child = spawn(\n process.execPath,\n [relayBin, 'serve', '--port', String(PORT), '--host', '0.0.0.0'],\n {\n detached: true,\n stdio: ['ignore', logFd, logFd],\n env: {\n ...process.env,\n ...(cliEntry ? { AGENTBOX_CLI_ENTRY: cliEntry } : {}),\n },\n },\n );\n child.unref();\n if (typeof child.pid === 'number') {\n await writeFile(PID_FILE, String(child.pid), 'utf8');\n log(`spawned relay host process (pid ${String(child.pid)}, port ${String(PORT)})`);\n }\n\n for (let i = 0; i < 25; i++) {\n if (await pingHealthz(300)) {\n log(`relay reachable on ${ENDPOINT.hostUrl}`);\n return ENDPOINT;\n }\n await delay(200);\n }\n throw new Error(\n `relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`,\n );\n}\n\n/**\n * Locate the `agentbox-relay` bin spawned as a child process. Layouts:\n * 0. env override: `AGENTBOX_RELAY_BIN`\n * 1. bundled CLI (dev + published `agent-box`): this module is bundled into\n * the CLI at `<root>/dist/index.js`, the stage step puts the bin at\n * `<root>/runtime/relay/bin.cjs` (sibling of dist/ in both layouts)\n * 2. legacy workspace: `<repo>/packages/sandbox-docker/dist` ↔ `<repo>/packages/relay/dist/bin.cjs`\n * 3. legacy externalized install: `<...>/node_modules/@agentbox/relay/dist/bin.cjs`\n */\nfunction resolveRelayBin(): string {\n const override = process.env.AGENTBOX_RELAY_BIN;\n if (override && existsSync(override)) return override;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n resolve(here, '..', 'runtime', 'relay', 'bin.cjs'),\n resolve(here, '..', '..', 'relay', 'dist', 'bin.cjs'),\n resolve(here, '..', '..', '..', '@agentbox', 'relay', 'dist', 'bin.cjs'),\n resolve(here, '..', '..', 'node_modules', '@agentbox', 'relay', 'dist', 'bin.cjs'),\n ];\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n throw new Error(\n `could not locate @agentbox/relay bin; tried:\\n ${candidates.join('\\n ')}`,\n );\n}\n\n/**\n * Locate the agentbox CLI entry the relay spawns for `checkpoint.create`.\n * Mirrors {@link resolveRelayBin}'s two layouts:\n * 1. workspace dev: `<repo>/packages/sandbox-docker/dist` ↔ `<repo>/apps/cli/dist/index.js`\n * 2. installed: `<...>/agentbox/node_modules/@agentbox/sandbox-docker/dist` ↔ `<...>/agentbox/dist/index.js`\n * Best-effort: returns null when not found (relay reports a clear error).\n */\nfunction resolveCliEntry(): string | null {\n const override = process.env.AGENTBOX_CLI_ENTRY;\n if (override && existsSync(override)) return override;\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n // Bundled CLI (dev + published): this module IS bundled into the CLI\n // entry, so the entry is index.js next to this file.\n resolve(here, 'index.js'),\n resolve(here, '..', '..', '..', 'apps', 'cli', 'dist', 'index.js'),\n resolve(here, '..', '..', '..', '..', 'dist', 'index.js'),\n ];\n for (const c of candidates) {\n if (existsSync(c)) return c;\n }\n return null;\n}\n\nexport interface StopRelayResult {\n /** True when a live relay process was signalled (and the pidfile cleared). */\n stopped: boolean;\n /** The pid that was found in the pidfile, if any. */\n pid: number | null;\n}\n\n/**\n * Stop the host relay process and clear its pidfile. SIGTERM first, then\n * SIGKILL if it's still alive after a short grace period. Idempotent: a\n * missing/stale pidfile is not an error (returns `{ stopped: false }`).\n *\n * Used by `agentbox update` to reload the relay; the next box command brings\n * it back via {@link ensureRelay} (running the freshly-installed bin).\n */\nexport async function stopRelay(): Promise<StopRelayResult> {\n const pid = await readPidFile();\n if (pid === null) {\n return { stopped: false, pid: null };\n }\n if (!(await processAlive(pid))) {\n await unlink(PID_FILE).catch(() => {});\n return { stopped: false, pid };\n }\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // already gone between the liveness check and the signal\n }\n for (let i = 0; i < 20; i++) {\n if (!(await processAlive(pid))) break;\n await delay(100);\n }\n if (await processAlive(pid)) {\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // best-effort\n }\n }\n await unlink(PID_FILE).catch(() => {});\n return { stopped: true, pid };\n}\n\nfunction pingHealthz(timeoutMs: number): Promise<boolean> {\n return new Promise<boolean>((resolveP) => {\n const req = httpRequest(\n { host: '127.0.0.1', port: PORT, method: 'GET', path: '/healthz', timeout: timeoutMs },\n (res) => {\n res.resume();\n const status = res.statusCode ?? 0;\n resolveP(status >= 200 && status < 300);\n },\n );\n req.on('error', () => resolveP(false));\n req.on('timeout', () => {\n req.destroy();\n resolveP(false);\n });\n req.end();\n });\n}\n\nasync function readPidFile(): Promise<number | null> {\n try {\n const text = await readFile(PID_FILE, 'utf8');\n const pid = Number.parseInt(text.trim(), 10);\n return Number.isFinite(pid) && pid > 0 ? pid : null;\n } catch {\n return null;\n }\n}\n\nasync function processAlive(pid: number): Promise<boolean> {\n try {\n // Signal 0 is the existence probe: throws if no such process.\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function generateRelayToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface RegisterBoxArgs {\n boxId: string;\n token: string;\n name: string;\n /** Docker container name; lets the relay `docker pause` the box for auto-pause. */\n containerName?: string;\n /** ISO-8601 box-creation time (BoxRecord.createdAt); auto-pause tie-break. */\n createdAt?: string;\n /**\n * Subset of BoxRecord.gitWorktrees the relay needs to dispatch git RPCs.\n * Empty/omitted for boxes without git repos.\n */\n worktrees?: GitWorktreeRecord[];\n}\n\nexport async function registerBoxWithRelay(args: RegisterBoxArgs): Promise<void> {\n const worktrees: BoxWorktree[] = (args.worktrees ?? []).map((w) => ({\n containerPath: w.containerPath,\n hostWorktreeDir: w.hostWorktreeDir,\n branch: w.branch,\n }));\n await adminPost('/admin/register-box', {\n boxId: args.boxId,\n token: args.token,\n name: args.name,\n containerName: args.containerName,\n createdAt: args.createdAt,\n worktrees,\n });\n}\n\nexport async function forgetBoxFromRelay(boxId: string): Promise<void> {\n try {\n await adminPost('/admin/forget-box', { boxId });\n } catch {\n // best-effort\n }\n}\n\nasync function adminPost(path: string, body: unknown): Promise<void> {\n const json = JSON.stringify(body);\n await new Promise<void>((resolveP, rejectP) => {\n const req = httpRequest(\n {\n host: '127.0.0.1',\n port: PORT,\n method: 'POST',\n path,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(json).toString(),\n },\n timeout: 3000,\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (c: Buffer) => chunks.push(c));\n res.on('end', () => {\n const status = res.statusCode ?? 0;\n if (status >= 200 && status < 300) {\n resolveP();\n } else {\n const text = Buffer.concat(chunks).toString('utf8');\n rejectP(new Error(`relay ${path} → ${String(status)}: ${text}`));\n }\n });\n },\n );\n req.on('error', rejectP);\n req.on('timeout', () => {\n req.destroy();\n rejectP(new Error(`relay ${path} timeout`));\n });\n req.write(json);\n req.end();\n });\n}\n\nexport interface BoxWithToken {\n id: string;\n name: string;\n container?: string;\n createdAt?: string;\n relayToken?: string;\n gitWorktrees?: GitWorktreeRecord[];\n}\n\n/**\n * Re-push every known (id, token) to the relay's in-memory registry. Called\n * after `ensureRelay()` so a fresh / restarted relay learns about boxes that\n * were created in a previous CLI invocation.\n */\nexport async function rehydrateRelayRegistry(boxes: BoxWithToken[]): Promise<void> {\n for (const b of boxes) {\n if (!b.relayToken) continue;\n try {\n await registerBoxWithRelay({\n boxId: b.id,\n token: b.relayToken,\n name: b.name,\n containerName: b.container,\n createdAt: b.createdAt,\n worktrees: b.gitWorktrees,\n });\n } catch {\n // best-effort\n }\n }\n}\n\nexport { RELAY_CONTAINER_NAME, RELAY_NETWORK_NAME, RELAY_IMAGE_REF, DEFAULT_RELAY_PORT };\n","import { ensureVolume, execInBox, type DockerExecResult } from './docker.js';\n\nexport type IdeFlavor = 'vscode' | 'cursor';\n\ninterface IdeProfile {\n /** Container path the IDE's server installs into. */\n serverDir: string;\n /** Container path of the extensions subdir under serverDir. */\n extensionsDir: string;\n /** Per-box volume name = perBoxVolumePrefix + boxId. */\n perBoxVolumePrefix: string;\n /** Shared extensions volume name (never auto-removed). */\n sharedExtensionsVolume: string;\n /** Host CLI binary that opens this IDE (`code` / `cursor`). */\n cli: string;\n /** Human-readable label used in CLI output. */\n displayName: string;\n /** macOS protocol scheme for the `open` fallback (no trailing colon). */\n protocolScheme: string;\n}\n\nconst PROFILES: Record<IdeFlavor, IdeProfile> = {\n vscode: {\n serverDir: '/home/vscode/.vscode-server',\n extensionsDir: '/home/vscode/.vscode-server/extensions',\n perBoxVolumePrefix: 'agentbox-vscode-server-',\n sharedExtensionsVolume: 'agentbox-vscode-extensions',\n cli: 'code',\n displayName: 'VS Code',\n protocolScheme: 'vscode',\n },\n cursor: {\n serverDir: '/home/vscode/.cursor-server',\n extensionsDir: '/home/vscode/.cursor-server/extensions',\n perBoxVolumePrefix: 'agentbox-cursor-server-',\n sharedExtensionsVolume: 'agentbox-cursor-extensions',\n cli: 'cursor',\n displayName: 'Cursor',\n protocolScheme: 'cursor',\n },\n};\n\nexport const IDE_FLAVORS: readonly IdeFlavor[] = ['vscode', 'cursor'];\n\nexport function ideProfile(flavor: IdeFlavor): IdeProfile {\n return PROFILES[flavor];\n}\n\n/**\n * Shared across all boxes. Holds downloaded VS Code extensions so the second\n * box onward doesn't re-download them. Never auto-removed by destroy/prune\n * (parallel to SHARED_CLAUDE_VOLUME).\n */\nexport const SHARED_VSCODE_EXTENSIONS_VOLUME = PROFILES.vscode.sharedExtensionsVolume;\n\n/** Same idea for Cursor's downloaded extensions. */\nexport const SHARED_CURSOR_EXTENSIONS_VOLUME = PROFILES.cursor.sharedExtensionsVolume;\n\n/** Per-box VS Code server volume name. Holds server binary + TS cache + workspace state. */\nexport function vscodeServerVolumeName(boxId: string): string {\n return ideServerVolumeName('vscode', boxId);\n}\n\n/** Per-box Cursor server volume name. */\nexport function cursorServerVolumeName(boxId: string): string {\n return ideServerVolumeName('cursor', boxId);\n}\n\nexport function ideServerVolumeName(flavor: IdeFlavor, boxId: string): string {\n return `${PROFILES[flavor].perBoxVolumePrefix}${boxId}`;\n}\n\nexport interface IdeMounts {\n /** Volume names to ensure() before runBox. */\n volumes: string[];\n /** `-v` arg values to pass to runBox. */\n extraVolumes: string[];\n}\n\n/**\n * Build the volume mounts for one IDE flavor: per-box `.vscode-server` (or\n * `.cursor-server`) mounts first, then the shared extensions volume layered\n * over its `extensions` subdir.\n */\nexport function buildFlavorMounts(flavor: IdeFlavor, boxId: string): IdeMounts {\n const profile = PROFILES[flavor];\n const perBox = ideServerVolumeName(flavor, boxId);\n return {\n volumes: [perBox, profile.sharedExtensionsVolume],\n extraVolumes: [\n `${perBox}:${profile.serverDir}`,\n `${profile.sharedExtensionsVolume}:${profile.extensionsDir}`,\n ],\n };\n}\n\n/** VS Code subset — kept for callers that only want the VS Code mounts. */\nexport function buildVscodeMounts(boxId: string): IdeMounts {\n return buildFlavorMounts('vscode', boxId);\n}\n\n/**\n * All IDE flavors' mounts unioned together. This is what createBox uses so\n * any existing box can be opened with either IDE without recreating.\n */\nexport function buildIdeMounts(boxId: string): IdeMounts {\n const merged: IdeMounts = { volumes: [], extraVolumes: [] };\n for (const f of IDE_FLAVORS) {\n const m = buildFlavorMounts(f, boxId);\n merged.volumes.push(...m.volumes);\n merged.extraVolumes.push(...m.extraVolumes);\n }\n return merged;\n}\n\n/** Ensure VS Code's volumes exist. */\nexport async function ensureVscodeVolumes(boxId: string): Promise<void> {\n for (const v of buildFlavorMounts('vscode', boxId).volumes) await ensureVolume(v);\n}\n\n/** Ensure every IDE flavor's volumes exist. */\nexport async function ensureIdeVolumes(boxId: string): Promise<void> {\n for (const v of buildIdeMounts(boxId).volumes) await ensureVolume(v);\n}\n\n/**\n * Belt-and-suspenders chown of the server trees after the named volumes are\n * mounted. The Dockerfile pre-creates these dirs so first-mount inherits\n * vscode:vscode ownership, but a shared extensions volume might already exist\n * from a box created against an older image where the dirs weren't seeded —\n * in that case the volume is root-owned and the Dev Containers extension\n * fails with \"mkdir: cannot create directory '<server>/bin': Permission\n * denied\". One docker exec fixes it idempotently for both flavors.\n */\nexport async function repairVscodeServerOwnership(container: string): Promise<void> {\n await execInBox(container, ['chown', '-R', 'vscode:vscode', PROFILES.vscode.serverDir], {\n user: 'root',\n });\n}\n\nexport async function repairIdeOwnership(container: string): Promise<void> {\n for (const flavor of IDE_FLAVORS) {\n await execInBox(container, ['chown', '-R', 'vscode:vscode', PROFILES[flavor].serverDir], {\n user: 'root',\n });\n }\n}\n\n/**\n * VS Code's `vscode-remote://attached-container+<hex>/...` URI takes the\n * *container name* hex-encoded. Cursor uses the same URI scheme (it's a fork)\n * — pass it to `cursor --folder-uri` the same way.\n */\nexport function containerHex(containerName: string): string {\n return Buffer.from(containerName, 'utf8').toString('hex');\n}\n\n/**\n * Resource URI for an attached container. Consumed by `code --folder-uri` /\n * `cursor --folder-uri`.\n *\n * Note: the `vscode://vscode-remote/...` protocol-handler form looks similar\n * but goes through macOS `open`, which percent-encodes the `+` authority\n * separator into `%2B` and the Dev Containers extension then fails to\n * resolve it. Use `<cli> --folder-uri <this>` to bypass that.\n */\nexport function attachedContainerUri(containerName: string, workspacePath = '/workspace'): string {\n return `vscode-remote://attached-container+${containerHex(containerName)}${workspacePath}`;\n}\n\n/**\n * agentbox-managed `.vscode/tasks.json` lives in the overlay's upper layer so\n * it doesn't pollute the host's working tree. The sentinel comment lets us\n * detect our own file and regenerate it on every `agentbox code` invocation\n * without overwriting a user-authored one. The file lives at `.vscode/` —\n * Cursor reads the same path (VS Code fork), so no per-IDE variant needed.\n */\nconst SENTINEL =\n '// agentbox-managed: regenerated on `agentbox code`; remove this header to take ownership';\n\nexport type ServiceTailHint = { name: string };\n\nexport interface EnsureTasksFileResult {\n status: 'wrote' | 'skipped-user-owned' | 'skipped-no-services';\n}\n\n/**\n * Write (or skip) `/workspace/.vscode/tasks.json` inside the container. Each\n * service in `services` gets a background task that tails its log so the IDE\n * shows a dedicated terminal panel on attach.\n *\n * - File absent → write.\n * - File present with our sentinel → overwrite.\n * - File present without sentinel → skip (user owns it). Caller can force\n * by setting `regen: true`.\n */\nexport async function ensureAgentboxTasksFile(\n container: string,\n services: ServiceTailHint[],\n opts: { regen?: boolean } = {},\n): Promise<EnsureTasksFileResult> {\n if (services.length === 0) return { status: 'skipped-no-services' };\n\n const existing = await execInBox(container, ['cat', '/workspace/.vscode/tasks.json'], {\n user: 'vscode',\n });\n if (existing.exitCode === 0 && !opts.regen && !existing.stdout.includes(SENTINEL)) {\n return { status: 'skipped-user-owned' };\n }\n\n const tasks = services.map((s) => ({\n label: `agentbox: ${s.name}`,\n type: 'shell',\n command: `tail -F /var/log/agentbox/${s.name}.log`,\n isBackground: true,\n presentation: { panel: 'dedicated', reveal: 'always', echo: false },\n runOptions: { runOn: 'folderOpen' },\n problemMatcher: [] as unknown[],\n }));\n const body =\n `${SENTINEL}\\n` +\n JSON.stringify(\n {\n version: '2.0.0',\n tasks,\n },\n null,\n 2,\n ) +\n '\\n';\n\n await execInBox(container, ['mkdir', '-p', '/workspace/.vscode'], { user: 'vscode' });\n const write = await writeFileInBox(container, '/workspace/.vscode/tasks.json', body);\n if (write.exitCode !== 0) {\n throw new Error(`failed to write tasks.json in ${container}: ${write.stderr || write.stdout}`);\n }\n return { status: 'wrote' };\n}\n\nasync function writeFileInBox(\n container: string,\n path: string,\n content: string,\n): Promise<DockerExecResult> {\n const { execa } = await import('execa');\n const result = await execa(\n 'docker',\n ['exec', '-i', '--user', 'vscode', container, 'sh', '-c', `cat > ${shellQuote(path)}`],\n { input: content, reject: false },\n );\n return {\n exitCode: result.exitCode ?? -1,\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n };\n}\n\nfunction shellQuote(s: string): string {\n return `'${s.replace(/'/g, `'\\\\''`)}'`;\n}\n\n// Backward-compat alias for the previous mount type name.\nexport type VscodeMounts = IdeMounts;\n","export const DEFAULT_RELAY_PORT = 8787;\nexport const RELAY_CONTAINER_NAME = 'agentbox-relay';\nexport const RELAY_NETWORK_NAME = 'agentbox-net';\nexport const RELAY_IMAGE_REF = 'agentbox/relay:dev';\nexport const RELAY_EVENT_RING_SIZE = 1000;\n\nexport interface BoxRegistration {\n boxId: string;\n token: string;\n name: string;\n /** ISO-8601 time the relay received this registration. */\n registeredAt: string;\n /** Docker container name; the relay needs it to `docker pause` an idle box. */\n containerName?: string;\n /** ISO-8601 box-creation time (BoxRecord.createdAt); used as a tie-break in auto-pause ordering. */\n createdAt?: string;\n /**\n * Container-path → host-worktree-dir mapping the host uses to resolve\n * git.pull/git.push RPCs. Empty when the box has no git repos.\n */\n worktrees?: BoxWorktree[];\n}\n\nexport interface BoxWorktree {\n /** Path inside the container (e.g. /workspace, /workspace/app). */\n containerPath: string;\n /** Host path to the worktree directory. */\n hostWorktreeDir: string;\n /** Branch the worktree was created on. */\n branch: string;\n}\n\nexport interface RelayEvent {\n /** Monotonic per-relay-process id, useful for `since=` polling. */\n id: number;\n /** Box id that posted the event. */\n boxId: string;\n /** Free-form event type, e.g. 'service-state', 'task-state', 'notify'. */\n type: string;\n /** ISO-8601 timestamp the relay assigned on receipt. */\n receivedAt: string;\n /** ISO-8601 client-supplied timestamp, if any. */\n ts?: string;\n /** Arbitrary JSON payload. */\n payload?: unknown;\n}\n\nexport interface PostEventBody {\n type: string;\n ts?: string;\n payload?: unknown;\n}\n\nexport interface PostRpcBody {\n method: string;\n params?: unknown;\n}\n\nexport interface RegisterBoxBody {\n boxId: string;\n token: string;\n name: string;\n containerName?: string;\n createdAt?: string;\n worktrees?: BoxWorktree[];\n}\n\nexport interface GitRpcParams {\n /** Container path identifying which worktree to run against. Defaults to /workspace. */\n path?: string;\n /** Remote name; defaults to 'origin'. */\n remote?: string;\n /** Extra argv tail appended after the standard args (e.g. ['--set-upstream', 'origin', 'branch']). */\n args?: string[];\n}\n\nexport interface GitRpcResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface CheckpointRpcParams {\n /** Checkpoint name; defaults host-side to `<box-name>-<next>`. */\n name?: string;\n /** Flatten lower+upper into one tree instead of a layered delta. */\n merged?: boolean;\n /** Mark the new checkpoint as the project default. */\n setDefault?: boolean;\n}\n","import type { BoxRegistration, RelayEvent } from './types.js';\nimport { RELAY_EVENT_RING_SIZE } from './types.js';\n\nexport class BoxRegistry {\n private readonly map = new Map<string, BoxRegistration>();\n\n register(reg: BoxRegistration): void {\n this.map.set(reg.boxId, reg);\n }\n\n forget(boxId: string): boolean {\n return this.map.delete(boxId);\n }\n\n /** Returns the registration whose token matches, or null. */\n authenticate(token: string): BoxRegistration | null {\n if (token.length === 0) return null;\n for (const reg of this.map.values()) {\n if (reg.token === token) return reg;\n }\n return null;\n }\n\n get(boxId: string): BoxRegistration | undefined {\n return this.map.get(boxId);\n }\n\n list(): BoxRegistration[] {\n return [...this.map.values()];\n }\n\n size(): number {\n return this.map.size;\n }\n}\n\nexport class EventBuffer {\n private readonly buf: RelayEvent[] = [];\n private nextId = 1;\n constructor(private readonly capacity: number = RELAY_EVENT_RING_SIZE) {}\n\n append(input: Omit<RelayEvent, 'id' | 'receivedAt'>): RelayEvent {\n const ev: RelayEvent = {\n id: this.nextId++,\n receivedAt: new Date().toISOString(),\n ...input,\n };\n this.buf.push(ev);\n if (this.buf.length > this.capacity) this.buf.shift();\n return ev;\n }\n\n /** Returns events with id > since. If `box` is given, filters to that box. */\n since(since: number, box?: string): RelayEvent[] {\n const out: RelayEvent[] = [];\n for (const ev of this.buf) {\n if (ev.id <= since) continue;\n if (box && ev.boxId !== box) continue;\n out.push(ev);\n }\n return out;\n }\n\n all(): RelayEvent[] {\n return this.buf.slice();\n }\n\n size(): number {\n return this.buf.length;\n }\n}\n","import { mkdir, rename, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n/**\n * A box status snapshot as received from the in-box daemon. The relay treats\n * it structurally (it has no dep on `@agentbox/ctl`); the rich type lives in\n * `@agentbox/ctl` (`BoxStatus`) and is what the host CLI parses back.\n */\nexport type BoxStatusSnapshot = Record<string, unknown>;\n\n/** Mirrors `boxRunDirFor` in @agentbox/sandbox-docker — kept in sync by hand. */\nfunction boxStatusPathFor(boxId: string): string {\n return join(homedir(), '.agentbox', 'boxes', boxId, 'status.json');\n}\n\n/**\n * Structural guard: a valid box-status payload is an object with `schema === 1`\n * and a non-empty `boxId` string. The relay persists it verbatim; the host\n * reader does the strict typing.\n */\nexport function isValidBoxStatus(payload: unknown): payload is BoxStatusSnapshot {\n if (typeof payload !== 'object' || payload === null) return false;\n const o = payload as Record<string, unknown>;\n return o.schema === 1 && typeof o.boxId === 'string' && o.boxId.length > 0;\n}\n\n/**\n * In-memory latest-status map plus a durable per-box file at\n * `~/.agentbox/boxes/<id>/status.json`. The relay is a single process so it is\n * the single writer; the atomic tmp+rename means the host CLI never reads a\n * torn file. The on-disk copy is what makes status survive box pause/stop,\n * relay restart, and host reboot.\n */\nexport class BoxStatusStore {\n private readonly map = new Map<string, BoxStatusSnapshot>();\n\n get(boxId: string): BoxStatusSnapshot | undefined {\n return this.map.get(boxId);\n }\n\n /** Update the in-memory entry and best-effort persist it to disk. */\n async set(boxId: string, status: BoxStatusSnapshot): Promise<void> {\n this.map.set(boxId, status);\n const target = boxStatusPathFor(boxId);\n const tmp = `${target}.${String(process.pid)}.tmp`;\n try {\n await mkdir(join(homedir(), '.agentbox', 'boxes', boxId), { recursive: true });\n await writeFile(tmp, JSON.stringify(status), 'utf8');\n await rename(tmp, target);\n } catch {\n await rm(tmp, { force: true }).catch(() => {});\n }\n }\n\n /** Drop the in-memory entry (the on-disk file is wiped with the box dir). */\n delete(boxId: string): void {\n this.map.delete(boxId);\n }\n}\n","import { spawn } from 'node:child_process';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { BoxRegistry, EventBuffer } from './registry.js';\nimport { BoxStatusStore, isValidBoxStatus } from './status-store.js';\nimport type {\n BoxRegistration,\n BoxWorktree,\n CheckpointRpcParams,\n GitRpcParams,\n GitRpcResult,\n PostEventBody,\n PostRpcBody,\n RegisterBoxBody,\n RelayEvent,\n} from './types.js';\n\nexport interface RelayServerOptions {\n port: number;\n /** Bind address; defaults to '0.0.0.0' so the container reachable from other containers on the same docker network. */\n host?: string;\n logger?: (line: string) => void;\n}\n\nexport interface RelayServerHandle {\n server: Server;\n registry: BoxRegistry;\n events: EventBuffer;\n statusStore: BoxStatusStore;\n url: string;\n close: () => Promise<void>;\n}\n\n/** Event type whose payload is a durable BoxStatus snapshot (persisted, not ringed). */\nconst BOX_STATUS_EVENT = 'box-status';\n\nconst MAX_BODY_BYTES = 1024 * 1024; // 1 MiB hard cap; relay is for control-plane traffic, not payloads.\nconst GIT_RPC_TIMEOUT_MS = 120_000; // git push/pull can be slow on big repos.\nconst CHECKPOINT_RPC_TIMEOUT_MS = 600_000; // capturing node_modules/build trees can be slow.\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: unknown,\n contentType: string = 'application/json',\n): void {\n const text = body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);\n res.statusCode = status;\n if (text.length > 0) {\n res.setHeader('Content-Type', contentType);\n res.setHeader('Content-Length', Buffer.byteLength(text).toString());\n res.end(text);\n } else {\n res.end();\n }\n}\n\nasync function readJsonBody<T>(req: IncomingMessage): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n let total = 0;\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => {\n total += chunk.length;\n if (total > MAX_BODY_BYTES) {\n reject(new Error('request body too large'));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n const text = Buffer.concat(chunks).toString('utf8');\n if (text.length === 0) {\n resolve({} as T);\n return;\n }\n try {\n resolve(JSON.parse(text) as T);\n } catch (err) {\n reject(err instanceof Error ? err : new Error(String(err)));\n }\n });\n req.on('error', reject);\n });\n}\n\nfunction bearerToken(req: IncomingMessage): string {\n const raw = req.headers.authorization;\n if (typeof raw !== 'string') return '';\n const m = /^Bearer\\s+(.+)$/i.exec(raw.trim());\n return m ? m[1]!.trim() : '';\n}\n\nfunction isLoopbackAddress(addr: string | undefined): boolean {\n if (!addr) return false;\n return (\n addr === '127.0.0.1' ||\n addr === '::1' ||\n addr === '::ffff:127.0.0.1' ||\n addr.startsWith('127.')\n );\n}\n\n/**\n * Build the relay HTTP server. Routes:\n * POST /events — bearer auth (box token), appends to ring buffer.\n * POST /rpc — bearer auth; dispatches git.pull / git.push on the host.\n * POST /admin/register-box — loopback only.\n * POST /admin/forget-box — loopback only.\n * GET /admin/box-status — loopback only; query `box`; latest snapshot.\n * GET /admin/events — loopback only; query `box`, `since`.\n * GET /admin/registry — loopback only; list registered boxes (token redacted).\n * GET /healthz — liveness probe (no auth).\n */\nexport function createRelayServer(opts: RelayServerOptions): RelayServerHandle {\n const log = opts.logger ?? (() => {});\n const registry = new BoxRegistry();\n const events = new EventBuffer();\n const statusStore = new BoxStatusStore();\n const host = opts.host ?? '0.0.0.0';\n\n const server = createServer((req, res) => {\n handle(req, res).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n log(`relay: handler error: ${msg}`);\n if (!res.headersSent) send(res, 500, { error: 'internal error' });\n else res.end();\n });\n });\n\n async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'relay'}`);\n const route = `${req.method ?? 'GET'} ${url.pathname}`;\n\n if (route === 'GET /healthz') {\n send(res, 200, { ok: true, boxes: registry.size(), events: events.size() });\n return;\n }\n\n // Admin endpoints are reachable from loopback only. The relay binds to\n // 0.0.0.0 so containers can reach /events and /rpc via host.docker.internal,\n // but admin operations (register-box, forget-box, list events, etc.) are\n // for the host CLI and must not be exposed to boxes.\n if (url.pathname.startsWith('/admin/')) {\n if (!isLoopbackAddress(req.socket.remoteAddress)) {\n send(res, 403, { error: 'admin endpoints are loopback-only' });\n return;\n }\n }\n\n if (route === 'POST /events') {\n const reg = authBox(req, res, registry);\n if (!reg) return;\n const body = await readJsonBody<PostEventBody>(req);\n if (!body || typeof body.type !== 'string' || body.type.length === 0) {\n send(res, 400, { error: 'missing \"type\" string' });\n return;\n }\n // box-status is durable state, not an event: persist the latest snapshot\n // per box and skip the ring buffer (a 15s heartbeat per box would\n // otherwise evict the useful git/service events from the 1000-cap ring).\n if (body.type === BOX_STATUS_EVENT) {\n if (!isValidBoxStatus(body.payload)) {\n send(res, 400, { error: 'invalid box-status payload' });\n return;\n }\n await statusStore.set(reg.boxId, body.payload);\n log(`box-status box=${reg.boxId}`);\n send(res, 202, { ok: true });\n return;\n }\n const ev = events.append({\n boxId: reg.boxId,\n type: body.type,\n ts: typeof body.ts === 'string' ? body.ts : undefined,\n payload: body.payload,\n });\n log(`event ${String(ev.id)} box=${reg.boxId} type=${body.type}`);\n send(res, 202, { id: ev.id });\n return;\n }\n\n if (route === 'POST /rpc') {\n const reg = authBox(req, res, registry);\n if (!reg) return;\n const body = await readJsonBody<PostRpcBody>(req);\n if (!body || typeof body.method !== 'string' || body.method.length === 0) {\n send(res, 400, { error: 'missing \"method\" string' });\n return;\n }\n log(`rpc box=${reg.boxId} method=${body.method}`);\n if (body.method === 'git.pull' || body.method === 'git.push') {\n const result = await handleGitRpc(reg, body.method, body.params as GitRpcParams | undefined);\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n if (body.method === 'checkpoint.create') {\n const result = await handleCheckpointRpc(\n reg,\n body.params as CheckpointRpcParams | undefined,\n );\n const status = result.exitCode === 0 ? 200 : 500;\n send(res, status, result);\n return;\n }\n events.append({\n boxId: reg.boxId,\n type: 'rpc-unknown',\n payload: { method: body.method },\n });\n send(res, 501, { error: 'rpc method not implemented', method: body.method });\n return;\n }\n\n if (route === 'POST /admin/register-box') {\n const body = await readJsonBody<RegisterBoxBody>(req);\n if (\n !body ||\n typeof body.boxId !== 'string' ||\n typeof body.token !== 'string' ||\n typeof body.name !== 'string' ||\n body.boxId.length === 0 ||\n body.token.length === 0\n ) {\n send(res, 400, { error: 'expected {boxId, token, name}' });\n return;\n }\n const worktrees = sanitizeWorktrees(body.worktrees);\n const reg: BoxRegistration = {\n boxId: body.boxId,\n token: body.token,\n name: body.name,\n registeredAt: new Date().toISOString(),\n containerName:\n typeof body.containerName === 'string' && body.containerName.length > 0\n ? body.containerName\n : undefined,\n createdAt:\n typeof body.createdAt === 'string' && body.createdAt.length > 0\n ? body.createdAt\n : undefined,\n worktrees,\n };\n registry.register(reg);\n log(\n `registered box ${reg.boxId} (${reg.name})` +\n (worktrees && worktrees.length > 0 ? ` with ${String(worktrees.length)} worktree(s)` : ''),\n );\n send(res, 204, null);\n return;\n }\n\n if (route === 'POST /admin/forget-box') {\n const body = await readJsonBody<{ boxId?: string }>(req);\n if (!body || typeof body.boxId !== 'string' || body.boxId.length === 0) {\n send(res, 400, { error: 'expected {boxId}' });\n return;\n }\n const existed = registry.forget(body.boxId);\n statusStore.delete(body.boxId);\n log(`forgot box ${body.boxId} (existed=${String(existed)})`);\n send(res, 204, null);\n return;\n }\n\n if (route === 'GET /admin/box-status') {\n const box = url.searchParams.get('box') ?? '';\n const status = box ? statusStore.get(box) : undefined;\n if (!status) {\n send(res, 404, { error: 'no status for box', box });\n return;\n }\n send(res, 200, status);\n return;\n }\n\n if (route === 'GET /admin/events') {\n const since = Number.parseInt(url.searchParams.get('since') ?? '0', 10) || 0;\n const box = url.searchParams.get('box') ?? undefined;\n const list = events.since(since, box ?? undefined);\n send(res, 200, { events: list });\n return;\n }\n\n if (route === 'GET /admin/registry') {\n // Redact tokens; callers on the admin path don't need them and we don't\n // want them showing up in logs if someone curls this.\n const redacted = registry.list().map((r) => ({\n boxId: r.boxId,\n name: r.name,\n registeredAt: r.registeredAt,\n containerName: r.containerName,\n createdAt: r.createdAt,\n worktrees: r.worktrees ?? [],\n }));\n send(res, 200, { boxes: redacted });\n return;\n }\n\n send(res, 404, { error: 'not found', route });\n }\n\n function authBox(\n req: IncomingMessage,\n res: ServerResponse,\n reg: BoxRegistry,\n ): BoxRegistration | null {\n const token = bearerToken(req);\n if (token.length === 0) {\n send(res, 401, { error: 'missing bearer token' });\n return null;\n }\n const match = reg.authenticate(token);\n if (!match) {\n send(res, 401, { error: 'unknown box token' });\n return null;\n }\n return match;\n }\n\n return {\n server,\n registry,\n events,\n statusStore,\n url: `http://${host}:${String(opts.port)}`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n }),\n };\n}\n\nfunction sanitizeWorktrees(input: BoxWorktree[] | undefined): BoxWorktree[] | undefined {\n if (!Array.isArray(input)) return undefined;\n const out: BoxWorktree[] = [];\n for (const w of input) {\n if (\n w &&\n typeof w.containerPath === 'string' &&\n typeof w.hostWorktreeDir === 'string' &&\n typeof w.branch === 'string'\n ) {\n out.push({\n containerPath: w.containerPath,\n hostWorktreeDir: w.hostWorktreeDir,\n branch: w.branch,\n });\n }\n }\n return out;\n}\n\n/**\n * Resolve `params.path` (a path inside the container) to the host worktree\n * directory the relay should run git in. `/workspace` always maps to the root\n * worktree; `/workspace/<sub>` maps to a nested worktree when one is\n * registered for that subpath, otherwise falls back to the root.\n */\nfunction resolveWorktree(reg: BoxRegistration, containerPath: string): BoxWorktree | null {\n const trees = reg.worktrees ?? [];\n if (trees.length === 0) return null;\n const exact = trees.find((w) => w.containerPath === containerPath);\n if (exact) return exact;\n // Longest containerPath prefix wins so /workspace/app/sub picks /workspace/app if registered.\n const prefixMatches = trees\n .filter((w) => containerPath === w.containerPath || containerPath.startsWith(w.containerPath + '/'))\n .sort((a, b) => b.containerPath.length - a.containerPath.length);\n return prefixMatches[0] ?? trees.find((w) => w.containerPath === '/workspace') ?? null;\n}\n\nasync function handleGitRpc(\n reg: BoxRegistration,\n method: 'git.pull' | 'git.push',\n params: GitRpcParams | undefined,\n): Promise<GitRpcResult> {\n const containerPath = params?.path ?? '/workspace';\n const worktree = resolveWorktree(reg, containerPath);\n if (!worktree) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: `no worktree registered for box ${reg.boxId} matching ${containerPath}`,\n };\n }\n const op = method === 'git.pull' ? 'pull' : 'push';\n const remote = params?.remote ?? 'origin';\n const argv = ['git', '-C', worktree.hostWorktreeDir, op, remote];\n if (Array.isArray(params?.args)) {\n for (const a of params.args) {\n if (typeof a === 'string') argv.push(a);\n }\n }\n return runHostCommand(argv);\n}\n\n/**\n * Capture a checkpoint host-side by shelling out to the installed agentbox\n * CLI (same decoupling philosophy as `handleGitRpc` spawning `git`). The\n * relay only knows the box id; the CLI resolves the BoxRecord (project root,\n * checkpoint config, snapshot storage) from it. `AGENTBOX_CLI_ENTRY` is set\n * by `ensureRelay` when it spawns this process.\n */\nasync function handleCheckpointRpc(\n reg: BoxRegistration,\n params: CheckpointRpcParams | undefined,\n): Promise<GitRpcResult> {\n const entry = process.env.AGENTBOX_CLI_ENTRY;\n if (!entry) {\n return {\n exitCode: 64,\n stdout: '',\n stderr: 'relay: AGENTBOX_CLI_ENTRY not set; cannot run checkpoint host-side',\n };\n }\n const argv = [process.execPath, entry, 'checkpoint', 'create', reg.boxId];\n if (params?.name) argv.push('--name', params.name);\n if (params?.merged === true) argv.push('--merged');\n if (params?.setDefault === true) argv.push('--set-default');\n return runHostCommand(argv, CHECKPOINT_RPC_TIMEOUT_MS);\n}\n\nfunction runHostCommand(\n argv: string[],\n timeoutMs: number = GIT_RPC_TIMEOUT_MS,\n): Promise<GitRpcResult> {\n return new Promise<GitRpcResult>((resolve) => {\n const [cmd, ...rest] = argv;\n if (!cmd) {\n resolve({ exitCode: 64, stdout: '', stderr: 'empty command' });\n return;\n }\n const child = spawn(cmd, rest, {\n env: process.env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let stdout = '';\n let stderr = '';\n let settled = false;\n const finish = (exitCode: number): void => {\n if (settled) return;\n settled = true;\n resolve({ exitCode, stdout, stderr });\n };\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n stderr += `\\nrelay: command timed out after ${String(timeoutMs)}ms\\n`;\n finish(124);\n }, timeoutMs);\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf8');\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf8');\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n stderr += String(err.message ?? err);\n finish(127);\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n finish(code ?? -1);\n });\n });\n}\n\nexport async function startRelayServer(opts: RelayServerOptions): Promise<RelayServerHandle> {\n const handle = createRelayServer(opts);\n await new Promise<void>((resolve, reject) => {\n handle.server.once('error', reject);\n handle.server.listen(opts.port, opts.host ?? '0.0.0.0', () => {\n handle.server.removeListener('error', reject);\n resolve();\n });\n });\n return handle;\n}\n\nexport type { BoxRegistration, RelayEvent };\n","import { spawn } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport {\n BUILT_IN_DEFAULTS,\n GLOBAL_CONFIG_FILE,\n parseUserConfig,\n type UserConfig,\n} from '@agentbox/config';\nimport type { BoxRegistry, EventBuffer } from './registry.js';\nimport type { BoxStatusStore } from './status-store.js';\n\nexport interface AutopauseConfig {\n enabled: boolean;\n maxRunningBoxes: number;\n idleMinutes: number;\n}\n\nexport type ContainerState = 'running' | 'paused' | 'stopped' | 'missing';\nexport type ClaudeState = 'working' | 'idle' | 'waiting' | 'unknown';\n\n/** One box's runtime facts the pure selector reasons about. No I/O, no clock. */\nexport interface BoxScanEntry {\n boxId: string;\n containerName: string;\n /** docker inspect status === 'running'. */\n running: boolean;\n /** Latest reported claude activity, or null when no snapshot exists. */\n claudeState: ClaudeState | null;\n /** ms the box has been idle (now - claude.updatedAt) when idle; else null. */\n idleMs: number | null;\n /** Box creation time as epoch ms; 0 when unknown/unparseable. */\n createdAt: number;\n}\n\n/**\n * Pure selection: given each running box's idle facts and the config, return\n * the boxIds to pause, in pause order. Pauses only enough to bring the running\n * count back to `maxRunningBoxes`, picking provably-idle boxes longest-idle\n * first (tie-break: oldest box, then boxId for determinism).\n */\nexport function selectBoxesToPause(entries: BoxScanEntry[], cfg: AutopauseConfig): string[] {\n if (!cfg.enabled) return [];\n const runningCount = entries.reduce((n, e) => (e.running ? n + 1 : n), 0);\n const excess = runningCount - cfg.maxRunningBoxes;\n if (excess <= 0) return [];\n\n const idleThresholdMs = cfg.idleMinutes * 60_000;\n const candidates = entries.filter(\n (e) => e.running && e.claudeState === 'idle' && e.idleMs != null && e.idleMs >= idleThresholdMs,\n );\n candidates.sort(\n (a, b) =>\n (b.idleMs as number) - (a.idleMs as number) ||\n a.createdAt - b.createdAt ||\n (a.boxId < b.boxId ? -1 : a.boxId > b.boxId ? 1 : 0),\n );\n return candidates.slice(0, excess).map((e) => e.boxId);\n}\n\n/**\n * Global+built-in autopause config. The relay is host-wide (not project\n * scoped), so it deliberately ignores the project/workspace layers that\n * `loadEffectiveConfig` would apply. Re-read every tick so\n * `agentbox config set --global autopause.*` takes effect without a relay\n * restart. A missing or malformed global file falls back to built-in defaults.\n */\nexport async function loadAutopauseConfig(): Promise<AutopauseConfig> {\n const d = BUILT_IN_DEFAULTS.autopause;\n let global: Partial<UserConfig> = {};\n try {\n global = parseUserConfig(await readFile(GLOBAL_CONFIG_FILE, 'utf8'), GLOBAL_CONFIG_FILE);\n } catch {\n // ENOENT (no global config yet) or a parse error -> built-in defaults.\n }\n const a = global.autopause ?? {};\n return {\n enabled: a.enabled ?? d.enabled,\n maxRunningBoxes: a.maxRunningBoxes ?? d.maxRunningBoxes,\n idleMinutes: a.idleMinutes ?? d.idleMinutes,\n };\n}\n\nexport interface AutopauseLoopDeps {\n registry: BoxRegistry;\n statusStore: BoxStatusStore;\n events: EventBuffer;\n log: (line: string) => void;\n /** Injectable for tests; defaults to the global-config loader. */\n loadConfig?: () => Promise<AutopauseConfig>;\n /** Injectable for tests; defaults to `docker inspect`. */\n inspectStatus?: (containerName: string) => Promise<ContainerState>;\n /** Injectable for tests; defaults to `docker pause`. */\n pause?: (containerName: string) => Promise<void>;\n intervalMs?: number;\n}\n\nexport interface AutopauseLoopHandle {\n /** Stop scheduling and await any in-flight tick. */\n stop: () => Promise<void>;\n}\n\nconst DEFAULT_INTERVAL_MS = 60_000;\n\nexport function startAutopauseLoop(deps: AutopauseLoopDeps): AutopauseLoopHandle {\n const loadConfig = deps.loadConfig ?? loadAutopauseConfig;\n const inspectStatus = deps.inspectStatus ?? inspectContainerState;\n const pause = deps.pause ?? pauseContainer;\n const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS;\n const { registry, statusStore, events, log } = deps;\n\n let ticking = false;\n let stopped = false;\n let inFlight: Promise<void> = Promise.resolve();\n\n async function tick(): Promise<void> {\n // A slow tick (many `docker inspect`s) must not overlap the next interval.\n if (ticking) return;\n ticking = true;\n try {\n const cfg = await loadConfig();\n if (!cfg.enabled) return;\n\n const entries: BoxScanEntry[] = [];\n for (const reg of registry.list()) {\n if (!reg.containerName) continue; // pre-feature box; can't pause it\n const state = await inspectStatus(reg.containerName);\n const claude = readClaude(statusStore.get(reg.boxId));\n const idleMs =\n claude.state === 'idle' && claude.updatedAt ? msSince(claude.updatedAt) : null;\n entries.push({\n boxId: reg.boxId,\n containerName: reg.containerName,\n running: state === 'running',\n claudeState: claude.state,\n idleMs,\n createdAt: reg.createdAt ? toEpoch(reg.createdAt) : 0,\n });\n }\n\n const toPause = selectBoxesToPause(entries, cfg);\n if (toPause.length === 0) return;\n\n const byId = new Map(entries.map((e) => [e.boxId, e]));\n const runningBefore = entries.reduce((n, e) => (e.running ? n + 1 : n), 0);\n for (const boxId of toPause) {\n const e = byId.get(boxId);\n if (!e) continue;\n try {\n await pause(e.containerName);\n const mins = e.idleMs != null ? Math.round(e.idleMs / 60_000) : null;\n events.append({\n boxId,\n type: 'autopause',\n payload: {\n containerName: e.containerName,\n action: 'paused',\n idleMs: e.idleMs,\n runningBefore,\n max: cfg.maxRunningBoxes,\n },\n });\n log(\n `autopause: paused box ${boxId} (${e.containerName})` +\n (mins != null ? ` after ~${String(mins)}m idle` : '') +\n `; running ${String(runningBefore)} -> target ${String(cfg.maxRunningBoxes)}`,\n );\n } catch (err) {\n // docker failure is non-fatal: log, record, keep going.\n const msg = err instanceof Error ? err.message : String(err);\n log(`autopause: docker pause ${e.containerName} failed: ${msg}`);\n events.append({\n boxId,\n type: 'autopause',\n payload: { containerName: e.containerName, action: 'pause-failed', error: msg },\n });\n }\n }\n } catch (err) {\n // The loop must never crash the relay or stop scheduling.\n const msg = err instanceof Error ? err.message : String(err);\n log(`autopause: tick error: ${msg}`);\n } finally {\n ticking = false;\n }\n }\n\n const timer = setInterval(() => {\n if (stopped) return;\n inFlight = tick();\n }, intervalMs);\n // The HTTP server keeps the process alive; the timer shouldn't on its own.\n timer.unref();\n\n return {\n stop: async () => {\n stopped = true;\n clearInterval(timer);\n await inFlight.catch(() => {});\n },\n };\n}\n\ninterface ClaudeSnap {\n state: ClaudeState | null;\n updatedAt: string | null;\n}\n\nfunction readClaude(snap: Record<string, unknown> | undefined): ClaudeSnap {\n const c = snap && typeof snap === 'object' ? (snap as { claude?: unknown }).claude : undefined;\n if (!c || typeof c !== 'object') return { state: null, updatedAt: null };\n const o = c as Record<string, unknown>;\n const state =\n o.state === 'working' || o.state === 'idle' || o.state === 'waiting' || o.state === 'unknown'\n ? o.state\n : null;\n return { state, updatedAt: typeof o.updatedAt === 'string' ? o.updatedAt : null };\n}\n\nfunction msSince(iso: string): number | null {\n const t = Date.parse(iso);\n return Number.isNaN(t) ? null : Date.now() - t;\n}\n\nfunction toEpoch(iso: string): number {\n const t = Date.parse(iso);\n return Number.isNaN(t) ? 0 : t;\n}\n\nconst INSPECT_TIMEOUT_MS = 15_000;\nconst PAUSE_TIMEOUT_MS = 30_000;\n\ninterface DockerResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nfunction runDocker(args: string[], timeoutMs: number): Promise<DockerResult> {\n return new Promise<DockerResult>((resolve) => {\n const child = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });\n let stdout = '';\n let stderr = '';\n let settled = false;\n const finish = (exitCode: number): void => {\n if (settled) return;\n settled = true;\n resolve({ exitCode, stdout, stderr });\n };\n const timer = setTimeout(() => {\n child.kill('SIGTERM');\n stderr += `\\nrelay: docker ${args.join(' ')} timed out after ${String(timeoutMs)}ms\\n`;\n finish(124);\n }, timeoutMs);\n child.stdout?.on('data', (c: Buffer) => {\n stdout += c.toString('utf8');\n });\n child.stderr?.on('data', (c: Buffer) => {\n stderr += c.toString('utf8');\n });\n child.on('error', (err) => {\n clearTimeout(timer);\n stderr += String(err.message ?? err);\n finish(127);\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n finish(code ?? -1);\n });\n });\n}\n\n/** Mirrors `inspectContainerStatus` in @agentbox/sandbox-docker (no dep on it — cycle). */\nasync function inspectContainerState(name: string): Promise<ContainerState> {\n const r = await runDocker(['inspect', '--format', '{{.State.Status}}', name], INSPECT_TIMEOUT_MS);\n if (r.exitCode !== 0) return 'missing';\n switch (r.stdout.trim()) {\n case 'running':\n return 'running';\n case 'paused':\n return 'paused';\n case 'created':\n case 'exited':\n case 'dead':\n case 'restarting':\n case 'removing':\n return 'stopped';\n default:\n return 'missing';\n }\n}\n\nasync function pauseContainer(name: string): Promise<void> {\n const r = await runDocker(['pause', name], PAUSE_TIMEOUT_MS);\n if (r.exitCode !== 0) {\n throw new Error(r.stderr.trim() || `docker pause ${name} exited ${String(r.exitCode)}`);\n }\n}\n","export type ServiceState =\n | 'pending'\n | 'waiting'\n | 'starting'\n | 'running'\n | 'ready'\n | 'unhealthy'\n | 'crashed'\n | 'backoff'\n | 'stopped';\n\nexport type TaskState = 'pending' | 'waiting' | 'running' | 'done' | 'failed' | 'skipped';\n\nexport interface ServiceStatus {\n name: string;\n state: ServiceState;\n pid: number | null;\n restarts: number;\n lastExitCode: number | null;\n startedAt: string | null;\n readyAt: string | null;\n nextRetryAt: string | null;\n blockedOn: string[];\n command: string;\n}\n\nexport interface StatusReply {\n services: ServiceStatus[];\n tasks: TaskStatus[];\n ports: BoxStatusPort[];\n}\n\nexport interface WaitReadyArgs {\n timeoutMs?: number;\n units?: string[];\n}\n\nexport type WaitReadyReply =\n | { ready: true }\n | { ready: false; timedOut: string[]; failed: string[] };\n\nexport interface TaskStatus {\n name: string;\n state: TaskState;\n pid: number | null;\n lastExitCode: number | null;\n startedAt: string | null;\n finishedAt: string | null;\n command: string;\n}\n\nexport interface LogEvent {\n service: string;\n ts: string;\n stream: 'stdout' | 'stderr';\n line: string;\n}\n\nexport interface ReloadResult {\n added: string[];\n removed: string[];\n changed: string[];\n}\n\n/**\n * Coarse activity state of the in-box Claude Code session, fed by Claude Code\n * hooks via `agentbox-ctl claude-state <state>`. `unknown` is the initial value\n * before any hook has fired (or for boxes whose image predates the hooks).\n */\nexport type ClaudeActivityState = 'working' | 'idle' | 'waiting' | 'unknown';\n\nexport const CLAUDE_ACTIVITY_STATES: readonly ClaudeActivityState[] = [\n 'working',\n 'idle',\n 'waiting',\n 'unknown',\n];\n\nexport interface BoxStatusServiceEntry {\n name: string;\n state: ServiceState;\n /** Configured `ready_when` port for this service, else null. */\n port: number | null;\n /**\n * The service's `expose:` mapping (container `as` → in-box `port`) when it is\n * the designated web service, else absent. Additive field — snapshots written\n * before this existed simply lack it (schema stays 1; treat absent as none).\n */\n expose?: { port: number; as: number };\n}\n\nexport interface BoxStatusTaskEntry {\n name: string;\n state: TaskState;\n}\n\nexport interface BoxStatusPort {\n port: number;\n /** Name of the service whose `ready_when` port matches, else null (ad-hoc). */\n service: string | null;\n}\n\nexport interface BoxStatusClaude {\n state: ClaudeActivityState;\n /** ISO-8601 time the last claude-state hook fired, or null if none yet. */\n updatedAt: string | null;\n /** Whether the claude tmux session was present at snapshot time. */\n sessionRunning: boolean;\n /**\n * Human-readable title Claude Code set on its terminal (the in-box tmux\n * pane title), sanitized. Additive field — snapshots written before this\n * existed simply lack it (schema stays 1; treat absent as no title).\n */\n sessionTitle?: string;\n}\n\n/**\n * Durable snapshot of a box's runtime status. The in-box daemon builds it and\n * pushes it to the host relay, which persists it to\n * `~/.agentbox/boxes/<id>/status.json` so `agentbox status` / `list` /\n * `inspect` can show it even when the box is paused or stopped.\n */\nexport interface BoxStatus {\n /** Schema version; bump on incompatible changes so old readers can reject. */\n schema: 1;\n boxId: string;\n /** ISO-8601 time the daemon built this snapshot. */\n timestamp: string;\n services: BoxStatusServiceEntry[];\n tasks: BoxStatusTaskEntry[];\n /** Live-discovered listening TCP ports inside the box. */\n ports: BoxStatusPort[];\n claude: BoxStatusClaude;\n}\n\nexport const BOX_STATUS_SCHEMA = 1 as const;\n\n/** Relay event type carrying a `BoxStatus` payload. */\nexport const BOX_STATUS_EVENT = 'box-status';\n\nexport type CtlRequest =\n | { op: 'status' }\n | { op: 'task-status' }\n | { op: 'wait-ready'; timeoutMs?: number; units?: string[] }\n | { op: 'run-task'; name: string; force?: boolean }\n | { op: 'logs'; service: string; tail?: number; follow?: boolean }\n | { op: 'restart'; service: string }\n | { op: 'stop'; service: string }\n | { op: 'start'; service: string }\n | { op: 'reload' }\n | { op: 'ping' }\n | { op: 'claude-session'; sessionName?: string }\n | { op: 'claude-state'; state: ClaudeActivityState };\n\nexport type CtlResponse = { ok: true; data: unknown } | { ok: false; error: string };\n\n/**\n * Status of the in-container tmux session running Claude Code. The daemon\n * doesn't own this session lifecycle — it probes via `tmux has-session` and\n * `tmux display-message`. Missing tmux server / missing session both surface\n * as `running: false`.\n */\nexport interface ClaudeSessionStatus {\n running: boolean;\n sessionName: string;\n /** ISO-8601 timestamp from tmux's `#{session_created}`, or null when not running. */\n startedAt: string | null;\n /**\n * Sanitized tmux `#{pane_title}` (the title Claude Code set on its\n * terminal), or null when not running / no meaningful title.\n */\n title: string | null;\n}\n\nexport const DEFAULT_SOCKET_PATH = '/run/agentbox/ctl.sock';\nexport const DEFAULT_CONFIG_PATH = '/workspace/agentbox.yaml';\nexport const DEFAULT_LOG_DIR = '/var/log/agentbox';\nexport const DEFAULT_CLAUDE_SESSION_NAME = 'claude';\n","import { createConnection, type Socket } from 'node:net';\nimport type {\n ClaudeActivityState,\n ClaudeSessionStatus,\n CtlRequest,\n CtlResponse,\n LogEvent,\n ReloadResult,\n ServiceStatus,\n StatusReply,\n TaskStatus,\n WaitReadyArgs,\n WaitReadyReply,\n} from './types.js';\n\nexport interface ConnectOptions {\n socketPath: string;\n /** Default 3000 ms. */\n timeoutMs?: number;\n}\n\nasync function connect(opts: ConnectOptions): Promise<Socket> {\n const sock = createConnection(opts.socketPath);\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n sock.destroy();\n reject(new Error(`connect ${opts.socketPath} timed out`));\n }, opts.timeoutMs ?? 3000);\n sock.once('connect', () => {\n clearTimeout(timer);\n resolve();\n });\n sock.once('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n return sock;\n}\n\nasync function sendOneShot<T>(opts: ConnectOptions, req: CtlRequest): Promise<T> {\n const sock = await connect(opts);\n sock.write(`${JSON.stringify(req)}\\n`);\n let buf = '';\n sock.setEncoding('utf8');\n for await (const chunk of sock) {\n buf += chunk as string;\n const idx = buf.indexOf('\\n');\n if (idx !== -1) {\n const line = buf.slice(0, idx);\n sock.end();\n return parseResponse<T>(line);\n }\n }\n // Connection closed before we got a full line.\n if (buf.length > 0) return parseResponse<T>(buf);\n throw new Error('connection closed with no response');\n}\n\nfunction parseResponse<T>(line: string): T {\n const parsed = JSON.parse(line) as CtlResponse;\n if (parsed.ok) return parsed.data as T;\n throw new Error(parsed.error);\n}\n\nexport async function ping(opts: ConnectOptions): Promise<'pong'> {\n return sendOneShot<'pong'>(opts, { op: 'ping' });\n}\n\nexport async function status(opts: ConnectOptions): Promise<StatusReply> {\n return sendOneShot<StatusReply>(opts, { op: 'status' });\n}\n\nexport async function taskStatus(opts: ConnectOptions): Promise<TaskStatus[]> {\n return sendOneShot<TaskStatus[]>(opts, { op: 'task-status' });\n}\n\nexport async function waitReady(\n opts: ConnectOptions,\n args: WaitReadyArgs = {},\n): Promise<WaitReadyReply> {\n return sendOneShot<WaitReadyReply>(opts, {\n op: 'wait-ready',\n timeoutMs: args.timeoutMs,\n units: args.units,\n });\n}\n\nexport async function runTask(\n opts: ConnectOptions,\n name: string,\n force?: boolean,\n): Promise<TaskStatus> {\n return sendOneShot<TaskStatus>(opts, { op: 'run-task', name, force });\n}\n\nexport async function restart(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'restart', service });\n}\n\nexport async function stop(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'stop', service });\n}\n\nexport async function start(opts: ConnectOptions, service: string): Promise<ServiceStatus> {\n return sendOneShot<ServiceStatus>(opts, { op: 'start', service });\n}\n\nexport async function reload(opts: ConnectOptions): Promise<ReloadResult> {\n return sendOneShot<ReloadResult>(opts, { op: 'reload' });\n}\n\nexport async function claudeSession(\n opts: ConnectOptions & { sessionName?: string },\n): Promise<ClaudeSessionStatus> {\n return sendOneShot<ClaudeSessionStatus>(opts, {\n op: 'claude-session',\n sessionName: opts.sessionName,\n });\n}\n\nexport async function claudeState(\n opts: ConnectOptions,\n state: ClaudeActivityState,\n): Promise<'ok'> {\n return sendOneShot<'ok'>(opts, { op: 'claude-state', state });\n}\n\nexport interface LogsResult {\n initial: LogEvent[];\n /**\n * When `follow: true` was passed, this async iterator yields further events\n * until the caller breaks out (which closes the socket).\n */\n follow?: AsyncIterableIterator<LogEvent>;\n}\n\nexport async function logs(\n opts: ConnectOptions,\n args: { service: string; tail?: number; follow?: boolean },\n): Promise<LogsResult> {\n const sock = await connect(opts);\n sock.write(`${JSON.stringify({ op: 'logs', ...args })}\\n`);\n\n const lines = createLineIterator(sock);\n const first = await lines.next();\n if (first.done) {\n sock.end();\n throw new Error('connection closed with no response');\n }\n const parsed = JSON.parse(first.value) as CtlResponse;\n if (!parsed.ok) {\n sock.end();\n throw new Error(parsed.error);\n }\n const data = parsed.data as { events: LogEvent[]; follow: boolean };\n if (!data.follow) {\n sock.end();\n return { initial: data.events };\n }\n\n const followGen = (async function* () {\n try {\n for await (const line of lines) {\n const p = JSON.parse(line) as CtlResponse;\n if (p.ok && p.data && typeof p.data === 'object' && 'event' in p.data) {\n yield (p.data as { event: LogEvent }).event;\n }\n }\n } finally {\n sock.end();\n }\n })();\n\n return { initial: data.events, follow: followGen };\n}\n\nasync function* createLineIterator(sock: Socket): AsyncIterableIterator<string> {\n let buf = '';\n sock.setEncoding('utf8');\n for await (const chunk of sock) {\n buf += chunk as string;\n let idx = buf.indexOf('\\n');\n while (idx !== -1) {\n yield buf.slice(0, idx);\n buf = buf.slice(idx + 1);\n idx = buf.indexOf('\\n');\n }\n }\n if (buf.length > 0) yield buf;\n}\n","import type { BoxStatusPort, ServiceStatus, TaskStatus } from './types.js';\n\nexport function renderStatusTable(rows: ServiceStatus[]): string {\n if (rows.length === 0) return '(no services configured)';\n const headers = ['NAME', 'STATE', 'PID', 'RESTARTS', 'LAST EXIT', 'BLOCKED ON', 'COMMAND'];\n const data: string[][] = rows.map((r) => [\n r.name,\n r.state,\n r.pid === null ? '-' : String(r.pid),\n String(r.restarts),\n r.lastExitCode === null ? '-' : String(r.lastExitCode),\n r.blockedOn.length === 0 ? '-' : r.blockedOn.join(','),\n truncate(r.command, 40),\n ]);\n return renderTable(headers, data);\n}\n\nexport function renderTaskTable(rows: TaskStatus[]): string {\n if (rows.length === 0) return '(no tasks configured)';\n const headers = ['NAME', 'STATE', 'EXIT', 'STARTED', 'FINISHED', 'COMMAND'];\n const data: string[][] = rows.map((r) => [\n r.name,\n r.state,\n r.lastExitCode === null ? '-' : String(r.lastExitCode),\n r.startedAt ?? '-',\n r.finishedAt ?? '-',\n truncate(r.command, 40),\n ]);\n return renderTable(headers, data);\n}\n\nexport function renderPortsTable(rows: BoxStatusPort[]): string {\n if (rows.length === 0) return '(no ports listening)';\n const named = rows.filter((r) => r.service);\n const other = rows\n .filter((r) => !r.service)\n .map((r) => r.port)\n .sort((a, b) => a - b);\n const lines: string[] = [];\n if (named.length > 0) {\n lines.push(\n renderTable(\n ['PORT', 'SERVICE'],\n named.map((r) => [`:${String(r.port)}`, r.service ?? '-']),\n ),\n );\n }\n // Collapse the unattributed ports (VNC, dev-tool worker IPC, multi-port\n // services like inngest) into one line so they don't drown the real services.\n if (other.length > 0) {\n lines.push(`other (${other.length}): ${other.join(', ')}`);\n }\n return lines.join('\\n');\n}\n\nfunction renderTable(headers: string[], data: string[][]): string {\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...data.map((row) => (row[i] ?? '').length)),\n );\n const fmt = (row: string[]): string =>\n row.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(' ');\n return [fmt(headers), ...data.map(fmt)].join('\\n');\n}\n\nfunction truncate(s: string, n: number): string {\n if (s.length <= n) return s;\n return s.slice(0, n - 1) + '…';\n}\n","import { readFile } from 'node:fs/promises';\nimport { parse as parseYaml } from 'yaml';\n\nexport type RestartPolicy = 'always' | 'on-failure' | 'never';\nexport type ProbeOnTimeout = 'kill' | 'mark_unhealthy';\n\nexport interface BackoffSpec {\n initialMs: number;\n maxMs: number;\n factor: number;\n}\n\nexport interface PortProbe {\n kind: 'port';\n port: number;\n host: string;\n intervalMs: number;\n initialDelayMs: number;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport interface LogMatchProbe {\n kind: 'log_match';\n pattern: RegExp;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport interface HttpProbe {\n kind: 'http';\n url: string;\n expectStatus?: number;\n intervalMs: number;\n initialDelayMs: number;\n timeoutMs: number;\n onTimeout: ProbeOnTimeout;\n}\n\nexport type ReadyProbe = PortProbe | LogMatchProbe | HttpProbe;\n\nexport interface ExposeSpec {\n /** The port this service listens on inside the box. */\n port: number;\n /** Container port forwarded to it. Only 80 is reserved/published today. */\n as: number;\n}\n\nexport interface TaskSpec {\n name: string;\n command: string | string[];\n cwd?: string;\n env?: Record<string, string>;\n needs: string[];\n}\n\nexport interface ServiceSpec {\n name: string;\n command: string | string[];\n cwd?: string;\n env?: Record<string, string>;\n autostart: boolean;\n restart: RestartPolicy;\n backoff: BackoffSpec;\n needs: string[];\n readyWhen?: ReadyProbe;\n /** When set, container port `expose.as` forwards to `127.0.0.1:expose.port`. */\n expose?: ExposeSpec;\n}\n\nexport interface CtlConfig {\n services: ServiceSpec[];\n tasks: TaskSpec[];\n}\n\nexport const DEFAULT_BACKOFF: BackoffSpec = {\n initialMs: 500,\n maxMs: 30_000,\n factor: 2,\n};\n\nexport const DEFAULT_PROBE_INTERVAL_MS = 500;\nexport const DEFAULT_PROBE_INITIAL_DELAY_MS = 0;\nexport const DEFAULT_PROBE_TIMEOUT_MS = 60_000;\nexport const DEFAULT_PROBE_HOST = '127.0.0.1';\nexport const DEFAULT_PROBE_ON_TIMEOUT: ProbeOnTimeout = 'kill';\n\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction parseEnv(raw: unknown, where: string): Record<string, string> | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.env must be a mapping of string → string`);\n }\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(raw)) {\n if (typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {\n throw new ConfigError(`${where}.env.${k} must be a scalar`);\n }\n out[k] = String(v);\n }\n return out;\n}\n\nfunction parseCommand(raw: unknown, where: string): string | string[] {\n if (typeof raw === 'string') {\n if (raw.trim().length === 0) {\n throw new ConfigError(`${where}.command must not be empty`);\n }\n return raw;\n }\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new ConfigError(`${where}.command array must not be empty`);\n }\n const argv: string[] = [];\n for (const [i, item] of raw.entries()) {\n if (typeof item !== 'string') {\n throw new ConfigError(`${where}.command[${String(i)}] must be a string`);\n }\n argv.push(item);\n }\n return argv;\n }\n throw new ConfigError(`${where}.command must be a string or array of strings`);\n}\n\nfunction parseRestart(raw: unknown, where: string): RestartPolicy {\n if (raw === undefined) return 'on-failure';\n if (raw === 'always' || raw === 'on-failure' || raw === 'never') return raw;\n throw new ConfigError(`${where}.restart must be one of: always, on-failure, never`);\n}\n\nconst BACKOFF_KEYS = new Set(['initial_ms', 'max_ms', 'factor']);\n\nfunction parseBackoff(raw: unknown, where: string): BackoffSpec {\n if (raw === undefined) return { ...DEFAULT_BACKOFF };\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.backoff must be a mapping`);\n }\n rejectUnknownKeys(raw, BACKOFF_KEYS, `${where}.backoff`);\n const initialMs = parseNonNegativeInt(\n raw.initial_ms,\n `${where}.backoff.initial_ms`,\n DEFAULT_BACKOFF.initialMs,\n );\n const maxMs = parseNonNegativeInt(raw.max_ms, `${where}.backoff.max_ms`, DEFAULT_BACKOFF.maxMs);\n const factor = parseFactor(raw.factor, `${where}.backoff.factor`, DEFAULT_BACKOFF.factor);\n if (maxMs < initialMs) {\n throw new ConfigError(`${where}.backoff.max_ms must be >= initial_ms`);\n }\n return { initialMs, maxMs, factor };\n}\n\nfunction rejectUnknownKeys(\n obj: Record<string, unknown>,\n allowed: Set<string>,\n where: string,\n): void {\n for (const key of Object.keys(obj)) {\n if (!allowed.has(key)) {\n throw new ConfigError(`${where} has unknown key \"${key}\"`);\n }\n }\n}\n\nfunction parseNonNegativeInt(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {\n throw new ConfigError(`${where} must be a non-negative number`);\n }\n return Math.floor(raw);\n}\n\nfunction parsePositiveInt(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 1) {\n throw new ConfigError(`${where} must be a positive integer`);\n }\n return Math.floor(raw);\n}\n\nfunction parseFactor(raw: unknown, where: string, fallback: number): number {\n if (raw === undefined) return fallback;\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 1) {\n throw new ConfigError(`${where} must be a number >= 1`);\n }\n return raw;\n}\n\nfunction parseOnTimeout(raw: unknown, where: string): ProbeOnTimeout {\n if (raw === undefined) return DEFAULT_PROBE_ON_TIMEOUT;\n if (raw === 'kill' || raw === 'mark_unhealthy') return raw;\n throw new ConfigError(`${where} must be one of: kill, mark_unhealthy`);\n}\n\nfunction parseNeeds(raw: unknown, where: string): string[] {\n if (raw === undefined || raw === null) return [];\n if (!Array.isArray(raw)) {\n throw new ConfigError(`${where} must be an array of unit names`);\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const [i, item] of raw.entries()) {\n if (typeof item !== 'string') {\n throw new ConfigError(`${where}[${String(i)}] must be a string`);\n }\n if (!/^[A-Za-z0-9_-]+$/.test(item)) {\n throw new ConfigError(`${where}[${String(i)}] \"${item}\" must match [A-Za-z0-9_-]+`);\n }\n if (seen.has(item)) continue;\n seen.add(item);\n out.push(item);\n }\n return out;\n}\n\nconst PROBE_KEYS = new Set([\n 'port',\n 'host',\n 'log_match',\n 'http',\n 'expect_status',\n 'interval_ms',\n 'initial_delay_ms',\n 'timeout_ms',\n 'on_timeout',\n]);\n\nfunction parseReadyWhen(raw: unknown, where: string): ReadyProbe | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.ready_when must be a mapping`);\n }\n rejectUnknownKeys(raw, PROBE_KEYS, `${where}.ready_when`);\n\n const kinds: Array<'port' | 'log_match' | 'http'> = [];\n if (raw.port !== undefined) kinds.push('port');\n if (raw.log_match !== undefined) kinds.push('log_match');\n if (raw.http !== undefined) kinds.push('http');\n if (kinds.length === 0) {\n throw new ConfigError(\n `${where}.ready_when must declare exactly one of: port, log_match, http`,\n );\n }\n if (kinds.length > 1) {\n throw new ConfigError(\n `${where}.ready_when may declare only one of: port, log_match, http (got ${kinds.join(', ')})`,\n );\n }\n\n const timeoutMs = parsePositiveInt(\n raw.timeout_ms,\n `${where}.ready_when.timeout_ms`,\n DEFAULT_PROBE_TIMEOUT_MS,\n );\n const onTimeout = parseOnTimeout(raw.on_timeout, `${where}.ready_when.on_timeout`);\n\n const kind = kinds[0]!;\n if (kind === 'log_match') {\n if (raw.host !== undefined || raw.expect_status !== undefined || raw.interval_ms !== undefined || raw.initial_delay_ms !== undefined) {\n throw new ConfigError(\n `${where}.ready_when.log_match cannot be combined with host/expect_status/interval_ms/initial_delay_ms`,\n );\n }\n const pat = assertString(raw.log_match, `${where}.ready_when.log_match`);\n let pattern: RegExp;\n try {\n pattern = new RegExp(pat);\n } catch (err) {\n throw new ConfigError(\n `${where}.ready_when.log_match is not a valid regex: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return { kind: 'log_match', pattern, timeoutMs, onTimeout };\n }\n\n const intervalMs = parsePositiveInt(\n raw.interval_ms,\n `${where}.ready_when.interval_ms`,\n DEFAULT_PROBE_INTERVAL_MS,\n );\n const initialDelayMs = parseNonNegativeInt(\n raw.initial_delay_ms,\n `${where}.ready_when.initial_delay_ms`,\n DEFAULT_PROBE_INITIAL_DELAY_MS,\n );\n\n if (kind === 'port') {\n if (raw.expect_status !== undefined) {\n throw new ConfigError(`${where}.ready_when.expect_status only applies to http probes`);\n }\n const port = parsePositiveInt(raw.port, `${where}.ready_when.port`, 0);\n if (port < 1 || port > 65535) {\n throw new ConfigError(`${where}.ready_when.port must be between 1 and 65535`);\n }\n const host =\n raw.host === undefined\n ? DEFAULT_PROBE_HOST\n : assertString(raw.host, `${where}.ready_when.host`);\n return { kind: 'port', port, host, intervalMs, initialDelayMs, timeoutMs, onTimeout };\n }\n\n if (raw.host !== undefined) {\n throw new ConfigError(`${where}.ready_when.host only applies to port probes`);\n }\n const url = assertString(raw.http, `${where}.ready_when.http`);\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new ConfigError(`${where}.ready_when.http must be a valid URL`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new ConfigError(`${where}.ready_when.http must use http(s):// (got ${parsed.protocol})`);\n }\n let expectStatus: number | undefined;\n if (raw.expect_status !== undefined) {\n expectStatus = parsePositiveInt(raw.expect_status, `${where}.ready_when.expect_status`, 0);\n if (expectStatus < 100 || expectStatus > 599) {\n throw new ConfigError(`${where}.ready_when.expect_status must be between 100 and 599`);\n }\n }\n return { kind: 'http', url, expectStatus, intervalMs, initialDelayMs, timeoutMs, onTimeout };\n}\n\n/**\n * The only container port AgentBox reserves + publishes for a web service today\n * (see `WEB_CONTAINER_PORT` host-side in @agentbox/sandbox-docker). A service's\n * `expose.as` must equal this — any other value would parse fine but be\n * unreachable from the host, so we reject it loudly.\n */\nexport const RESERVED_WEB_PORT = 80;\n\nconst SERVICE_KEYS = new Set([\n 'command',\n 'cwd',\n 'env',\n 'autostart',\n 'restart',\n 'backoff',\n 'needs',\n 'ready_when',\n 'expose',\n 'ide',\n]);\n\nconst EXPOSE_KEYS = new Set(['port', 'as']);\n\nfunction parseExpose(raw: unknown, where: string): ExposeSpec | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where}.expose must be a mapping`);\n }\n rejectUnknownKeys(raw, EXPOSE_KEYS, `${where}.expose`);\n if (raw.port === undefined) {\n throw new ConfigError(`${where}.expose.port is required`);\n }\n const port = parsePortNumber(raw.port, `${where}.expose.port`);\n const as = raw.as === undefined ? RESERVED_WEB_PORT : parsePortNumber(raw.as, `${where}.expose.as`);\n if (as !== RESERVED_WEB_PORT) {\n throw new ConfigError(\n `${where}.expose.as must be ${String(RESERVED_WEB_PORT)} (the only container port AgentBox publishes)`,\n );\n }\n return { port, as };\n}\n\nfunction parsePortNumber(raw: unknown, where: string): number {\n if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1 || raw > 65535) {\n throw new ConfigError(`${where} must be an integer between 1 and 65535`);\n }\n return raw;\n}\n\nfunction parseService(name: string, raw: unknown): ServiceSpec {\n const where = `services.${name}`;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where} must be a mapping`);\n }\n rejectUnknownKeys(raw, SERVICE_KEYS, where);\n const command = parseCommand(raw.command, where);\n const cwd = raw.cwd === undefined ? undefined : assertString(raw.cwd, `${where}.cwd`);\n const env = parseEnv(raw.env, where);\n const autostart =\n raw.autostart === undefined ? true : assertBool(raw.autostart, `${where}.autostart`);\n const restart = parseRestart(raw.restart, where);\n const backoff = parseBackoff(raw.backoff, where);\n const needs = parseNeeds(raw.needs, `${where}.needs`);\n const readyWhen = parseReadyWhen(raw.ready_when, where);\n const expose = parseExpose(raw.expose, where);\n return { name, command, cwd, env, autostart, restart, backoff, needs, readyWhen, expose };\n}\n\nconst TASK_KEYS = new Set(['command', 'cwd', 'env', 'needs']);\n\nfunction parseTask(name: string, raw: unknown): TaskSpec {\n const where = `tasks.${name}`;\n if (!isPlainObject(raw)) {\n throw new ConfigError(`${where} must be a mapping`);\n }\n rejectUnknownKeys(raw, TASK_KEYS, where);\n const command = parseCommand(raw.command, where);\n const cwd = raw.cwd === undefined ? undefined : assertString(raw.cwd, `${where}.cwd`);\n const env = parseEnv(raw.env, where);\n const needs = parseNeeds(raw.needs, `${where}.needs`);\n return { name, command, cwd, env, needs };\n}\n\nfunction assertString(raw: unknown, where: string): string {\n if (typeof raw !== 'string') throw new ConfigError(`${where} must be a string`);\n return raw;\n}\n\nfunction assertBool(raw: unknown, where: string): boolean {\n if (typeof raw !== 'boolean') throw new ConfigError(`${where} must be a boolean`);\n return raw;\n}\n\n// `defaults` is the host-side config layer (read by @agentbox/config) — the\n// supervisor doesn't touch it, but we accept it here so `ctl validate` doesn't\n// flag it as unknown. Strict typo-detection still applies (top-level keys\n// outside this set are rejected).\nconst TOP_LEVEL_KEYS = new Set(['services', 'tasks', 'ide', 'defaults']);\n\nfunction validateUnitGraph(tasks: TaskSpec[], services: ServiceSpec[]): void {\n const names = new Set<string>();\n for (const t of tasks) {\n if (names.has(t.name)) {\n throw new ConfigError(`unit name \"${t.name}\" declared more than once (task vs service collision)`);\n }\n names.add(t.name);\n }\n for (const s of services) {\n if (names.has(s.name)) {\n throw new ConfigError(`unit name \"${s.name}\" declared more than once (task vs service collision)`);\n }\n names.add(s.name);\n }\n\n const deps = new Map<string, string[]>();\n for (const t of tasks) deps.set(t.name, t.needs);\n for (const s of services) deps.set(s.name, s.needs);\n\n for (const [unit, list] of deps) {\n for (const dep of list) {\n if (!names.has(dep)) {\n throw new ConfigError(`unit \"${unit}\" needs unknown unit \"${dep}\"`);\n }\n if (dep === unit) {\n throw new ConfigError(`unit \"${unit}\" cannot depend on itself`);\n }\n }\n }\n\n // DFS with three-color marking; record the cycle path in the error.\n const WHITE = 0, GRAY = 1, BLACK = 2;\n const color = new Map<string, number>();\n for (const name of deps.keys()) color.set(name, WHITE);\n const stack: string[] = [];\n\n function visit(name: string): void {\n color.set(name, GRAY);\n stack.push(name);\n for (const dep of deps.get(name)!) {\n const c = color.get(dep) ?? WHITE;\n if (c === GRAY) {\n const startIdx = stack.indexOf(dep);\n const cycle = stack.slice(startIdx).concat(dep).join(' → ');\n throw new ConfigError(`cyclic dependency: ${cycle}`);\n }\n if (c === WHITE) visit(dep);\n }\n stack.pop();\n color.set(name, BLACK);\n }\n\n for (const name of deps.keys()) {\n if (color.get(name) === WHITE) visit(name);\n }\n}\n\nexport function parseConfig(text: string): CtlConfig {\n let doc: unknown;\n try {\n doc = parseYaml(text);\n } catch (err) {\n throw new ConfigError(`yaml parse error: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (doc === null || doc === undefined) return { services: [], tasks: [] };\n if (!isPlainObject(doc)) {\n throw new ConfigError('top-level config must be a mapping');\n }\n rejectUnknownKeys(doc, TOP_LEVEL_KEYS, '(root)');\n\n const services: ServiceSpec[] = [];\n const servicesRaw = doc.services;\n if (servicesRaw !== undefined && servicesRaw !== null) {\n if (!isPlainObject(servicesRaw)) {\n throw new ConfigError('services must be a mapping of name → service');\n }\n for (const [name, raw] of Object.entries(servicesRaw)) {\n if (!/^[A-Za-z0-9_-]+$/.test(name)) {\n throw new ConfigError(`service name \"${name}\" must match [A-Za-z0-9_-]+`);\n }\n services.push(parseService(name, raw));\n }\n }\n\n const tasks: TaskSpec[] = [];\n const tasksRaw = doc.tasks;\n if (tasksRaw !== undefined && tasksRaw !== null) {\n if (!isPlainObject(tasksRaw)) {\n throw new ConfigError('tasks must be a mapping of name → task');\n }\n for (const [name, raw] of Object.entries(tasksRaw)) {\n if (!/^[A-Za-z0-9_-]+$/.test(name)) {\n throw new ConfigError(`task name \"${name}\" must match [A-Za-z0-9_-]+`);\n }\n tasks.push(parseTask(name, raw));\n }\n }\n\n // ide: parsed only enough to confirm it's an object; contents are host-side\n // and the supervisor doesn't touch them. Schema is permissive here too.\n if (doc.ide !== undefined && doc.ide !== null && !isPlainObject(doc.ide)) {\n throw new ConfigError('ide must be a mapping');\n }\n\n // defaults: host-side layered-config block. We only require it to be a\n // mapping here; @agentbox/config validates the leaves strictly when the\n // host loads the file. Letting ctl deep-validate would force a circular\n // dependency on the host-only @agentbox/config package — see CLAUDE.md.\n if (doc.defaults !== undefined && doc.defaults !== null && !isPlainObject(doc.defaults)) {\n throw new ConfigError('defaults must be a mapping');\n }\n\n validateUnitGraph(tasks, services);\n\n const exposed = services.filter((s) => s.expose);\n if (exposed.length > 1) {\n throw new ConfigError(\n `at most one service may set expose: (got: ${exposed.map((s) => s.name).join(', ')})`,\n );\n }\n\n return { services, tasks };\n}\n\nexport async function loadConfig(path: string): Promise<CtlConfig> {\n let text: string;\n try {\n text = await readFile(path, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { services: [], tasks: [] };\n }\n throw err;\n }\n return parseConfig(text);\n}\n\nexport function describeCommand(cmd: string | string[]): string {\n return Array.isArray(cmd) ? cmd.join(' ') : cmd;\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,OAAO,SAAS,SAAS,UAAU,IAAI,MAAM,iBAAiB;AACvE,SAAS,SAAS,cAAc;AAChC,SAAS,MAAM,gBAAgB;AAC/B,SAAS,aAAa;AIJtB,SAAS,mBAAmB;AEA5B,SAAS,SAAAA,cAAa;AACtB,SAAS,WAAAC,UAAS,QAAAC,aAAY;AAC9B,SAAS,QAAAC,aAAY;AEFrB,SAAS,SAAAH,cAAa;AACtB,SAAS,SAAAI,QAAO,WAAAH,UAAS,MAAAI,KAAI,QAAAH,aAAY;AACzC,SAAS,WAAAI,UAAS,gBAAgB;AAClC,SAAS,QAAAH,OAAM,eAAe;ACH9B,SAAS,QAAAD,aAAY;ACArB,SAAS,aAAa;AACtB,SAAS,eAAAK,oBAAmB;AAC5B,SAAS,YAAY,gBAAgB;AACrC,SAAS,SAAAH,QAAO,YAAAI,WAAU,QAAQ,aAAAC,kBAAiB;AACnD,SAAS,WAAW,mBAAmB;AACvC,SAAS,WAAAH,gBAAe;AACxB,SAAS,SAAS,QAAAH,OAAM,WAAAO,gBAAe;AACvC,SAAS,cAAc,aAAa;AACpC,SAAS,qBAAqB;;;AERvB,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAC7B,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AGgC/B,IAAM,iBAAiB,OAAO;;;AdlBvB,SAAS,sBAAsB,SAAiB,UAA2B;AAChF,MAAI,OAAO,YAAY,YAAY,QAAQ,WAAW,EAAG,QAAO;AAChE,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,SAAO,QAAQ,SAAS,WAAW,GAAG;AACxC;AAwBO,SAAS,gBAA6B,MAAS,UAAuC;AAE3F,QAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAM,kBAA4B,CAAC;AAEnC,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO,EAAE,MAAM,OAAY,gBAAgB;EAC7C;AAEA,QAAM,MAAM;AACZ,QAAM,YAAY,IAAI;AACtB,MAAI,cAAc,QAAQ,OAAO,cAAc,YAAY,MAAM,QAAQ,SAAS,GAAG;AACnF,WAAO,EAAE,MAAM,OAAY,gBAAgB;EAC7C;AAEA,aAAW,eAAe,OAAO,KAAK,SAAoC,GAAG;AAC3E,UAAM,eAAgB,UAAsC,WAAW;AACvE,QAAI,CAAC,MAAM,QAAQ,YAAY,EAAG;AAClC,eAAW,SAAS,cAAc;AAChC,UAAI,UAAU,QAAQ,OAAO,UAAU,SAAU;AACjD,YAAM,UAAU;AAChB,YAAM,QAAQ,QAAQ;AACtB,UAAI,CAAC,MAAM,QAAQ,KAAK,EAAG;AAE3B,eAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,cAAM,OAAO,MAAM,CAAC;AACpB,YAAI,SAAS,QAAQ,OAAO,SAAS,SAAU;AAC/C,YACE,KAAK,SAAS,aACd,OAAO,KAAK,YAAY,YACxB,sBAAsB,KAAK,SAAS,QAAQ,GAC5C;AACA,0BAAgB,KAAK,KAAK,OAAO;AACjC,gBAAM,OAAO,GAAG,CAAC;QACnB;MACF;IACF;EACF;AAEA,SAAO,EAAE,MAAM,OAAY,gBAAgB;AAC7C;AAgCO,SAAS,gBACd,MACA,UACA,QAC0B;AAC1B,QAAM,QAAQ,gBAAgB,IAAI;AAClC,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO,EAAE,MAAM,OAAY,SAAS,MAAM;EAC5C;AACA,MAAI,aAAa,UAAU,SAAS,WAAW,KAAK,OAAO,WAAW,GAAG;AACvE,WAAO,EAAE,MAAM,OAAY,SAAS,MAAM;EAC5C;AACA,QAAM,MAAM;AACZ,QAAM,WAAW,IAAI;AACrB,MAAI,aAAa,QAAQ,OAAO,aAAa,YAAY,MAAM,QAAQ,QAAQ,GAAG;AAChF,WAAO,EAAE,MAAM,OAAY,SAAS,MAAM;EAC5C;AACA,QAAM,cAAc;AACpB,QAAM,MAAM,YAAY,QAAQ;AAChC,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjE,WAAO,EAAE,MAAM,OAAY,SAAS,MAAM;EAC5C;AACA,QAAM,WAAW,YAAY,MAAM;AACnC,MAAI,aAAa,QAAQ,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GAAG;AACjF,gBAAY,MAAM,IAAI,EAAE,GAAI,UAAsC,GAAI,IAAgC;EACxG,OAAO;AACL,gBAAY,MAAM,IAAI,gBAAgB,GAAG;EAC3C;AACA,SAAO,EAAE,MAAM,OAAY,SAAS,KAAK;AAC3C;AAeO,SAAS,mBAAgC,MAAsC;AACpF,QAAM,QAAQ,gBAAgB,IAAI;AAClC,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO,EAAE,MAAM,OAAY,SAAS,MAAM;EAC5C;AACA,QAAM,MAAM;AACZ,MAAI,OAAO,UAAU,eAAe,KAAK,KAAK,eAAe,GAAG;AAC9D,WAAO,IAAI;AACX,WAAO,EAAE,MAAM,OAAY,SAAS,KAAK;EAC3C;AACA,SAAO,EAAE,MAAM,OAAY,SAAS,MAAM;AAC5C;ACpJO,IAAM,yBAAyB,CAAC,WAAW;AAG3C,IAAM,2BAA2B;AAQjC,SAAS,aACd,UACA,WACA,kBAAqC,CAAC,GAC5B;AACV,QAAM,OAAO,IAAI,IAAI,SAAS;AAC9B,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,UAAU;AAC3B,QAAI,KAAK,WAAW,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,EAAG;AAC3D,QAAI,gBAAgB,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,EAAG;AACrD,SAAK,IAAI,IAAI;AACb,QAAI,KAAK,IAAI;EACf;AACA,SAAO,IAAI,KAAK;AAClB;AAQA,SAAS,mBAAsB,OAAU,mBAA8B;AACrE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,MAAM,MAAM,wBAAwB,EAAE,KAAK,iBAAiB;EACrE;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;EAClE;AACA,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,UAAI,CAAC,IAAI,mBAAmB,GAAG,iBAAiB;IAClD;AACA,WAAO;EACT;AACA,SAAO;AACT;AAEA,SAAS,cAAc,GAA0C;AAC/D,SAAO,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAChE;AAsBA,SAAS,cACP,UACA,SACA,mBACA,WACA,SACa;AACb,QAAM,UAAU,UAAU,QAAQ;AAClC,QAAM,SAAS,UAAU,OAAO;AAChC,MAAI,CAAC,cAAc,MAAM,GAAG;AAC1B,WAAO,EAAE,MAAM,UAAU,SAAS,OAAO,WAAW,CAAC,EAAE;EACzD;AACA,QAAM,OAAgC,cAAc,OAAO,IAAI,EAAE,GAAG,QAAQ,IAAI,CAAC;AACjF,QAAM,YAAsB,CAAC;AAC7B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,OAAO,UAAU,eAAe,KAAK,MAAM,GAAG,EAAG;AACrD,SAAK,GAAG,IAAI,mBAAmB,OAAO,iBAAiB;AACvD,cAAU,KAAK,GAAG;EACpB;AACA,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO,EAAE,MAAM,UAAU,SAAS,OAAO,WAAW,CAAC,EAAE;EACzD;AACA,SAAO,EAAE,MAAM,QAAQ,UAAU,IAAI,GAAG,SAAS,MAAM,WAAW,UAAU,KAAK,EAAE;AACrF;AAOO,SAAS,uBACd,UACA,SACA,MACa;AACb,QAAM,SAAS,GAAG,KAAK,QAAQ;AAC/B,SAAO;IACL,cAAc,QAAQ,IAAI,WAAW,CAAC;IACtC;IACA;IACA,CAAC,SAAS;IACV,CAAC,OAAO,WAAW;EACrB;AACF;AAQO,SAAS,sBACd,UACA,SACA,MACa;AACb,QAAM,SAAS,GAAG,KAAK,QAAQ;AAC/B,QAAM,WAAW,cAAc,QAAQ,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE;AACpE,SAAO;IACL;IACA;IACA;IACA,CAAC,SAAU,cAAc,IAAI,IAAK,KAAiC,SAAS,IAAI;IAChF,CAAC,MAAM,YAAY,EAAE,GAAI,MAAkC,SAAS,OAAO;EAC7E;AACF;AFrJO,IAAM,uBAAuB;AAC7B,IAAM,yBAAyB;AACtC,IAAM,uBAAuB;AACtB,IAAM,iBAAiB;AAE9B,IAAM,sBAAsB;AAOrB,SAAS,oBAAoB,MAA6D;AAC/F,MAAI,KAAK,SAAS;AAChB,WAAO,EAAE,QAAQ,GAAG,oBAAoB,IAAI,KAAK,KAAK,GAAG;EAC3D;AACA,SAAO,EAAE,QAAQ,qBAAqB;AACxC;AA6CA,eAAe,WAAW,GAA6B;AACrD,MAAI;AACF,UAAM,KAAK,CAAC;AACZ,WAAO;EACT,QAAQ;AACN,WAAO;EACT;AACF;AAWA,eAAe,mBAAmB,MAAiC;AACjE,QAAM,SAAmB,CAAC;AAC1B,iBAAe,KAAK,KAA4B;AAC9C,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;IACtD,QAAQ;AACN;IACF;AACA,eAAW,OAAO,SAAS;AACzB,YAAM,OAAO,KAAK,KAAK,IAAI,IAAI;AAC/B,UAAI,IAAI,eAAe,GAAG;AACxB,YAAI;AACF,gBAAM,KAAK,IAAI;QACjB,QAAQ;AACN,iBAAO,KAAK,SAAS,MAAM,IAAI,CAAC;QAClC;MACF,WAAW,IAAI,YAAY,GAAG;AAC5B,cAAM,KAAK,IAAI;MACjB;IACF;EACF;AACA,QAAM,KAAK,IAAI;AACf,SAAO;AACT;AAmBA,eAAsB,mBACpB,MACA,MACmC;AACnC,QAAM,UAAU,MAAM,aAAa,KAAK,MAAM;AAC9C,QAAM,aAAa,KAAK,MAAM;AAC9B,QAAM,UAAU,CAAC;AAEjB,MAAI,CAAC,KAAK,aAAc,QAAO,EAAE,SAAS,QAAQ,MAAM;AAExD,QAAM,aAAa,KAAK,QAAQ,GAAG,SAAS;AAC5C,MAAI,CAAE,MAAM,WAAW,UAAU,EAAI,QAAO,EAAE,SAAS,QAAQ,MAAM;AAiBrE,QAAM,iBAAiB,KAAK,QAAQ,GAAG,cAAc;AACrD,QAAM,UAAU,MAAM,WAAW,cAAc;AAC/C,QAAM,WAAW,QAAQ;AAOzB,QAAM,aAAa,KAAK,QAAQ,GAAG,SAAS;AAC5C,QAAM,YAAY,MAAM,WAAW,UAAU;AAC7C,QAAM,OAAiB;IACrB;IACA;IACA;IACA;;;IAGA;IACA,aAAa,QAAQ;IACrB;IACA,GAAG,KAAK,MAAM;IACd;IACA,GAAG,UAAU;EACf;AACA,MAAI,QAAS,MAAK,KAAK,MAAM,GAAG,cAAc,sBAAsB;AACpE,MAAI,UAAW,MAAK,KAAK,MAAM,GAAG,UAAU,cAAc;AAU1D,QAAM,YAAY,MAAM,QAAQ,KAAK,OAAO,GAAG,yBAAyB,CAAC;AACzE,MAAI,oBAAoB;AACxB,MAAI,uBAAuB;AAC3B,MAAI,oBAAoB;AACxB,MAAI;AACF,UAAM,iBAAiB,MAAM;MAC3B,KAAK,YAAY,eAAe;MAChC,KAAK,WAAW,eAAe;MAC/B;IACF;AACA,yBAAqB,eAAe;AACpC,QAAI,SAAS;AACX,YAAM,aAAa,MAAM;QACvB;QACA,KAAK,WAAW,cAAc;QAC9B;QACA;UACE,oBAAoB;UACpB,cAAc,KAAK,gBACf,EAAE,MAAM,KAAK,eAAe,IAAI,oBAAoB,IACpD;QACN;MACF;AACA,2BAAqB,WAAW;AAChC,6BAAuB,WAAW;AAClC,0BAAoB,WAAW;IACjC;AACA,QAAI,oBAAoB,KAAK,wBAAwB,mBAAmB;AACtE,WAAK,KAAK,MAAM,GAAG,SAAS,iBAAiB;IAC/C;AAMA,UAAM,iBAAiB,MAAM,mBAAmB,UAAU;AAC1D,UAAM,gBAAgB,CAAC,wBAAwB;AAC/C,eAAW,OAAO,eAAgB,eAAc,KAAK,cAAc,GAAG,EAAE;AACxE,UAAM,aAAa,0BAA0B,cAAc,KAAK,GAAG,CAAC;AACpE,SAAK;MACH,KAAK;MACL;MACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA+BA,oKAGe,UAAU;IAQ3B;AACA,UAAM,MAAM,UAAU,IAAI;EAC5B,UAAA;AACE,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;EACtD;AAEA,SAAO,EAAE,SAAS,QAAQ,MAAM,mBAAmB,sBAAsB,kBAAkB;AAC7F;AASA,eAAe,cACb,KACA,MACA,UACA,OAGI,CAAC,GAKJ;AACD,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,MAAM,SAAS,KAAK,MAAM,CAAC;EACjD,QAAQ;AACN,WAAO,EAAE,cAAc,GAAG,sBAAsB,OAAO,mBAAmB,MAAM;EAClF;AACA,QAAM,WAAW,gBAAgB,QAAQ,QAAQ;AACjD,MAAI,UAAmB,SAAS;AAChC,MAAI,UAAU;AACd,MAAI,KAAK,oBAAoB;AAC3B,UAAM,IAAI,mBAAmB,OAAO;AACpC,cAAU,EAAE;AACZ,cAAU,EAAE;EACd;AACA,MAAI,UAAU;AACd,MAAI,KAAK,cAAc;AACrB,UAAM,IAAI,gBAAgB,SAAS,KAAK,aAAa,MAAM,KAAK,aAAa,EAAE;AAC/E,cAAU,EAAE;AACZ,cAAU,EAAE;EACd;AACA,MAAI,SAAS,gBAAgB,WAAW,KAAK,CAAC,WAAW,CAAC,SAAS;AACjE,WAAO,EAAE,cAAc,GAAG,sBAAsB,OAAO,mBAAmB,MAAM;EAClF;AACA,QAAM,UAAU,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AACtD,SAAO;IACL,cAAc,SAAS,gBAAgB;IACvC,sBAAsB;IACtB,mBAAmB;EACrB;AACF;AAqBA,IAAM,qBAAqB;EACzB;EACA;EACA;EACA;AACF;AAEO,SAAS,kBACd,MACA,SACmB;AACnB,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,oBAAoB;AAClC,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,KAAI,CAAC,IAAI;EACtD;AACA,SAAO;IACL,cAAc,CAAC,GAAG,KAAK,MAAM,IAAI,oBAAoB,EAAE;IACvD;IACA,YAAY,KAAK;EACnB;AACF;AAgBA,IAAM,0BAA0B;AAQhC,IAAM,uBAAuB;AAG7B,IAAM,4BAA4B,IAAI,KAAK,KAAK;AAGhD,IAAM,6BAA6B,KAAK,MAAM,4BAA4B,GAAK;AAW/E,IAAM,gBAAgB;AAEtB,eAAe,OAAO,GAA6B;AACjD,MAAI;AACF,YAAQ,MAAM,KAAK,CAAC,GAAG,OAAO;EAChC,QAAQ;AACN,WAAO;EACT;AACF;AAGA,eAAe,mBAAmB,GAA6B;AAC7D,MAAI;AACF,UAAM,KAAK,MAAM,KAAK,CAAC;AACvB,WAAO,KAAK,IAAI,IAAI,GAAG,UAAU;EACnC,QAAQ;AACN,WAAO;EACT;AACF;AAEA,eAAe,MAAM,GAA6B;AAChD,MAAI;AACF,YAAQ,MAAM,KAAK,CAAC,GAAG,YAAY;EACrC,QAAQ;AACN,WAAO;EACT;AACF;AAUA,eAAsB,0BAA0B,WAAqC;AACnF,MAAI;AACJ,MAAI;AACF,mBAAe,MAAM,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EACjE,QAAQ;AACN,WAAO;EACT;AACA,aAAW,KAAK,cAAc;AAC5B,QAAI,CAAC,EAAE,YAAY,EAAG;AACtB,UAAM,QAAQ,KAAK,WAAW,EAAE,IAAI;AACpC,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,QAAQ,OAAO,EAAE,eAAe,KAAK,CAAC;IACxD,QAAQ;AACN;IACF;AACA,eAAW,KAAK,SAAS;AACvB,UAAI,CAAC,EAAE,YAAY,EAAG;AACtB,YAAM,QAAQ,KAAK,OAAO,EAAE,IAAI;AAChC,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM,QAAQ,OAAO,EAAE,eAAe,KAAK,CAAC;MACzD,QAAQ;AACN;MACF;AACA,iBAAW,KAAK,UAAU;AACxB,YAAI,CAAC,EAAE,YAAY,EAAG;AACtB,cAAM,QAAQ,KAAK,OAAO,EAAE,IAAI;AAChC,YAAI,CAAE,MAAM,OAAO,KAAK,OAAO,cAAc,CAAC,EAAI;AAClD,YAAI,MAAM,OAAO,KAAK,OAAO,uBAAuB,CAAC,EAAG;AACxD,YAAI,MAAM,mBAAmB,KAAK,OAAO,oBAAoB,CAAC,EAAG;AACjE,eAAO;MACT;IACF;EACF;AACA,SAAO;AACT;AASA,eAAe,6BAA6B,QAAwC;AAClF,MAAK,MAAM,aAAa,MAAO,WAAY,QAAO;AAClD,MAAI,CAAE,MAAM,MAAM,mBAAmB,MAAM,CAAC,EAAI,QAAO;AACvD,SAAO,mBAAmB,QAAQ,WAAW,OAAO;AACtD;AAwBA,eAAsB,wBACpB,WACA,OASI,CAAC,GACmC;AACxC,MAAI,KAAK,QAAQ;AACf,UAAM,YAAY,MAAM,6BAA6B,KAAK,MAAM;AAChE,QAAI,aAAa,CAAE,MAAM,0BAA0B,SAAS,GAAI;AAC9D,aAAO,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,GAAG,SAAS,KAAK;IAClD;EACF;AAIA,QAAM,SAAS;;SAER,uBAAuB;aACnB,oBAAoB;YACrB,aAAa;cACX,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DtC,QAAM,SAAS,MAAM;IACnB;IACA,CAAC,QAAQ,UAAU,gBAAgB,WAAW,MAAM,MAAM,MAAM;IAChE,EAAE,QAAQ,MAAM;EAClB;AACA,QAAM,UAAoB,CAAC;AAC3B,QAAM,SAAiD,CAAC;AACxD,QAAM,SAAS,OAAO,UAAU,IAAI,MAAM,IAAI;AAC9C,MAAI,iBAA2D;AAC/D,aAAW,QAAQ,OAAO;AACxB,QAAI,gBAAgB;AAClB,UAAI,SAAS,oBAAoB;AAC/B,eAAO,KAAK,EAAE,KAAK,eAAe,KAAK,QAAQ,eAAe,OAAO,KAAK,IAAI,EAAE,CAAC;AACjF,yBAAiB;MACnB,OAAO;AACL,uBAAe,OAAO,KAAK,IAAI;MACjC;AACA;IACF;AACA,QAAI,KAAK,WAAW,gBAAgB,GAAG;AACrC,WAAK,aAAa,cAAc,KAAK,MAAM,iBAAiB,MAAM,CAAC,EAAE;IACvE,WAAW,KAAK,WAAW,aAAa,GAAG;AACzC,cAAQ,KAAK,KAAK,MAAM,cAAc,MAAM,CAAC;IAC/C,WAAW,KAAK,WAAW,eAAe,GAAG;AAC3C,uBAAiB,EAAE,KAAK,KAAK,MAAM,gBAAgB,MAAM,GAAG,QAAQ,CAAC,EAAE;IACzE;EACF;AACA,SAAO,EAAE,SAAS,QAAQ,SAAS,MAAM;AAC3C;AAEO,IAAM,qBAAN,cAAiC,MAAM;EAC5C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;EACd;AACF;AAeA,SAAS,QAAQ,KAAqB;AACpC,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,MAAI,4BAA4B,KAAK,GAAG,EAAG,QAAO;AAClD,SAAO,IAAI,IAAI,QAAQ,MAAM,OAAO,CAAC;AACvC;AAiBA,eAAsB,mBAAmB,MAAgD;AACvF,QAAM,cAAc,KAAK,eAAe;AACxC,QAAM,UAAU,KAAK,WAAW,KAAK,UAAU,QAAQ,cAAc,EAAE;AACvE,QAAM,MAAM,CAAC,UAAU,GAAG,KAAK,UAAU,EAAE,IAAI,OAAO,EAAE,KAAK,GAAG;AAChE,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AACpC,QAAM,WAAqB,CAAC,MAAM,QAAQ,IAAI,EAAE;AAChD,aAAW,KAAK,oBAAoB;AAClC,UAAM,IAAI,QAAQ,IAAI,CAAC;AACvB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,UAAS,KAAK,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE;EAC5E;AACA,QAAM,SAAS,MAAM;IACnB;IACA;MACE;MACA,GAAG;MACH;MACA;MACA,KAAK;MACL;MACA;MACA;MACA;MACA;MACA;MACA,GAAG,yBAAyB,aAAa,OAAO;IAClD;IACA,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,OAAO,aAAa,EAAG;AAC3B,QAAM,UAAU,OAAO,UAAU,IAAI,SAAS;AAC9C,MAAI,OAAO,aAAa,OAAO,qCAAqC,KAAK,MAAM,GAAG;AAChF,UAAM,IAAI;MACR;IACF;EACF;AACA,MAAI,oCAAoC,KAAK,MAAM,GAAG;AACpD,UAAM,IAAI;MACR;IACF;EACF;AACA,MAAI,qBAAqB,KAAK,MAAM,GAAG;AACrC,UAAM,IAAI;MACR,mBAAmB,WAAW,uBAAuB,KAAK,SAAS;IACrE;EACF;AACA,QAAM,IAAI;IACR,qCAAqC,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,QAAQ,OAAO,OAAO,QAAQ,CAAC,EAAE;EAC5G;AACF;AAaO,SAAS,sBAAsB,WAAmB,aAAgC;AACvF,QAAM,OAAO,eAAe;AAC5B,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AACpC,SAAO;IACL;IACA;IACA;IACA,QAAQ,IAAI;IACZ;IACA;IACA;IACA;IACA;IACA;IACA;EACF;AACF;AAcO,SAAS,+BACd,WACA,aACU;AACV,QAAM,OAAO,eAAe;AAC5B,QAAM,OAAO,GAAG,IAAI;AACpB,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AACpC,SAAO;IACL;IACA;IACA;IACA,QAAQ,IAAI;IACZ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;EACF;AACF;AAoBO,SAAS,yBAAyB,aAAqB,SAA2B;AACvF,QAAM,IAAI;AACV,QAAM,OAAO;AACb,SAAO;IACL;IAAK;IAAO;IAAM;IAAG;IAAmB;IACxC;IAAK;IAAO;IAAM;IAAG;IAAkB;IACvC;IAAK;IAAO;IAAM;IAAG;IAAgB;IACrC;IAAK;IAAO;IAAM;IAAG;IAAsB;IAC3C;IAAK;IAAO;IAAM;IAAG;IAAe,mDAA8C,IAAI;IACtF;IAAK;IAAO;IAAM;IAAG;IAAuB;IAC5C;IAAK;IAAO;IAAM;IAAG;IAAgB;IACrC;IAAK;IAAO;IAAM;IAAG;IAAwB;IAC7C;IAAK;IAAO;IAAM;IAAG;IAAgC;EACvD;AACF;AAOO,SAAS,eAAe,WAA6B;AAC1D,QAAM,OAAO,QAAQ,IAAI,MAAM,KAAK;AACpC,SAAO,CAAC,QAAQ,OAAO,MAAM,QAAQ,IAAI,IAAI,UAAU,gBAAgB,WAAW,QAAQ,IAAI;AAChG;AAEO,SAAS,mBAAmB,KAAqB;AACtD,SAAO,2DAA2D,GAAG;AACvE;AAEO,SAAS,oBACd,WACA,aACA,aACO;AACP,QAAM,QAAQ,UAAU,UAAU,sBAAsB,WAAW,WAAW,GAAG;IAC/E,OAAO;EACT,CAAC;AACD,QAAM,OAAO,MAAM,UAAU;AAC7B,MAAI,eAAe,SAAS,GAAG;AAI7B,YAAQ,OAAO,MAAM,qBAAqB,mBAAmB,WAAW,IAAI,IAAI;EAClF;AACA,UAAQ,KAAK,IAAI;AACnB;AAaA,eAAsB,kBACpB,WACA,aAC4B;AAC5B,QAAM,OAAO,eAAe;AAC5B,QAAM,MAAM,MAAM;IAChB;IACA,CAAC,QAAQ,UAAU,gBAAgB,WAAW,QAAQ,eAAe,MAAM,IAAI;IAC/E,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,IAAI,aAAa,GAAG;AACtB,WAAO,EAAE,SAAS,OAAO,aAAa,MAAM,WAAW,KAAK;EAC9D;AACA,QAAM,KAAK,MAAM;IACf;IACA;MACE;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;IACF;IACA,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,YAA2B;AAC/B,MAAI,GAAG,aAAa,GAAG;AACrB,UAAM,OAAO,OAAO,UAAU,GAAG,UAAU,IAAI,KAAK,GAAG,EAAE;AACzD,QAAI,OAAO,SAAS,IAAI,KAAK,OAAO,EAAG,aAAY,IAAI,KAAK,OAAO,GAAI,EAAE,YAAY;EACvF;AACA,SAAO,EAAE,SAAS,MAAM,aAAa,MAAM,UAAU;AACvD;AAoBA,IAAM,sBAAsB,CAAC,UAAU,UAAU,UAAU;AAQ3D,eAAe,cAAc,KAAgC;AAC3D,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC1D,WAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI;EACvF,QAAQ;AACN,WAAO,CAAC;EACV;AACF;AAEA,eAAe,aAAa,MAAgC;AAC1D,MAAI;AACF,WAAO,KAAK,MAAM,MAAM,SAAS,MAAM,MAAM,CAAC;EAChD,QAAQ;AACN,WAAO;EACT;AACF;AAcA,eAAsB,iBACpB,MACA,MAC2B;AAC3B,QAAM,WAAW,QAAQ;AACzB,QAAM,aAAa,KAAK,UAAU,SAAS;AAK3C,QAAM,kBAAkB;IACtB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;EACF,EAAE,KAAK,GAAG;AAEV,QAAM,MAAM,MAAM;IAChB;IACA,CAAC,OAAO,QAAQ,UAAU,KAAK,MAAM,GAAG,KAAK,MAAM,YAAY,KAAK,OAAO,MAAM,MAAM,eAAe;IACtG,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,IAAI,aAAa,GAAG;AACtB,UAAM,IAAI;MACR,uCAAuC,KAAK,MAAM,MAAM,IAAI,UAAU,IAAI,SAAS,EAAE,KAAK,KAAK,QAAQ,OAAO,IAAI,QAAQ,CAAC,EAAE;IAC/H;EACF;AAEA,QAAM,UAAoC,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,UAAU,CAAC,EAAE;AACjF,QAAM,aAAuB,CAAC;AAC9B,QAAM,UAAmC,CAAC;AAC1C,aAAW,SAAS,IAAI,UAAU,IAAI,MAAM,IAAI,GAAG;AACjD,QAAI,KAAK,WAAW,MAAM,GAAG;AAC3B,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,UAAI,OAAO,GAAI;AACf,YAAM,MAAM,KAAK,MAAM,GAAG,EAAE;AAC5B,YAAM,OAAO,KAAK,MAAM,KAAK,CAAC;AAC9B,UAAI,OAAO,QAAS,SAAQ,GAAG,EAAG,KAAK,IAAI;IAC7C,WAAW,KAAK,WAAW,SAAS,GAAG;AACrC,iBAAW,KAAK,KAAK,MAAM,CAAC,CAAC;IAC/B,WAAW,KAAK,WAAW,OAAO,GAAG;AACnC,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,UAAI,OAAO,GAAI;AACf,YAAM,QAAQ,KAAK,MAAM,GAAG,EAAE;AAC9B,UAAI;AACF,gBAAQ,KAAK,IAAI,KAAK,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK,CAAC,GAAG,QAAQ,EAAE,SAAS,MAAM,CAAC;MACxF,QAAQ;MAER;IACF;EACF;AAIA,QAAM,WAAyC,CAAC;AAChD,QAAM,aAAmD,CAAC;AAC1D,aAAW,OAAO,qBAAqB;AACrC,UAAM,YAAY,MAAM,cAAc,KAAK,YAAY,GAAG,CAAC;AAC3D,UAAM,WAAW,QAAQ,WAAW,yBAAyB,CAAC;AAC9D,eAAW,QAAQ,aAAa,QAAQ,GAAG,KAAK,CAAC,GAAG,WAAW,QAAQ,GAAG;AACxE,eAAS,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC;AACrC,iBAAW,KAAK,EAAE,KAAK,QAAQ,GAAG,IAAI,IAAI,IAAI,MAAM,QAAQ,GAAG,IAAI,IAAI,GAAG,CAAC;IAC7E;EACF;AACA,QAAM,iBAA2B,CAAC;AAClC,aAAW,KAAK,MAAM,cAAc,KAAK,YAAY,WAAW,OAAO,CAAC,GAAG;AACzE,eAAW,KAAK,MAAM,cAAc,KAAK,YAAY,WAAW,SAAS,CAAC,CAAC,GAAG;AAC5E,qBAAe,KAAK,GAAG,CAAC,IAAI,CAAC,EAAE;IACjC;EACF;AACA,aAAW,OAAO,aAAa,YAAY,cAAc,GAAG;AAC1D,aAAS,KAAK,EAAE,UAAU,WAAW,MAAM,IAAI,CAAC;AAChD,eAAW,KAAK,EAAE,KAAK,sBAAsB,GAAG,IAAI,MAAM,sBAAsB,GAAG,GAAG,CAAC;EACzF;AAIA,QAAM,gBAAgB,MAAM,aAAa,KAAK,YAAY,WAAW,wBAAwB,CAAC;AAC9F,QAAM,cAAc,MAAM,aAAa,KAAK,YAAY,WAAW,yBAAyB,CAAC;AAC7F,QAAM,kBAAkB,sBAAsB,eAAe,QAAQ,mBAAmB,GAAG;IACzF;EACF,CAAC;AACD,QAAM,gBAAgB,uBAAuB,aAAa,QAAQ,oBAAoB,GAAG;IACvF;EACF,CAAC;AACD,QAAM,mBAA6B,CAAC;AACpC,MAAI,gBAAgB,QAAS,kBAAiB,KAAK,wBAAwB;AAC3E,MAAI,cAAc,QAAS,kBAAiB,KAAK,yBAAyB;AAE1E,MAAI,KAAK,UAAW,SAAS,WAAW,KAAK,iBAAiB,WAAW,GAAI;AAC3E,WAAO,EAAE,UAAU,iBAAiB;EACtC;AAOA,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,OAAO,WAAW,IAAI,CAAC,EAAE,KAAK,KAAK,MAAM;AAC7C,YAAM,SAAS,KAAK,MAAM,GAAG,KAAK,YAAY,GAAG,CAAC;AAClD,aAAO,aAAa,MAAM,2DAA2D,GAAG,OAAO,IAAI;IACrG,CAAC;AACD,UAAM,QAAQ,MAAM;MAClB;MACA;QACE;QACA;QACA;QACA;QACA;QACA,GAAG,KAAK,MAAM;QACd;QACA,GAAG,UAAU;QACb,KAAK;QACL;QACA;QACA,KAAK,KAAK,MAAM;MAClB;MACA,EAAE,QAAQ,MAAM;IAClB;AACA,QAAI,MAAM,aAAa,GAAG;AACxB,YAAM,IAAI;QACR,kCAAkC,KAAK,MAAM,MAAM,MAAM,UAAU,IAAI,SAAS,EAAE,KAAK,KAAK,QAAQ,OAAO,MAAM,QAAQ,CAAC,EAAE;MAC9H;IACF;EACF;AAIA,MAAI,cAAc,WAAW,gBAAgB,SAAS;AACpD,UAAM,MAAM,KAAK,YAAY,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5D,QAAI,cAAc,SAAS;AACzB,YAAM;QACJ,KAAK,YAAY,WAAW,yBAAyB;QACrD,GAAG,KAAK,UAAU,cAAc,MAAM,MAAM,CAAC,CAAC;;MAChD;IACF;AACA,QAAI,gBAAgB,SAAS;AAC3B,YAAM;QACJ,KAAK,YAAY,WAAW,wBAAwB;QACpD,GAAG,KAAK,UAAU,gBAAgB,MAAM,MAAM,CAAC,CAAC;;MAClD;IACF;EACF;AAEA,SAAO,EAAE,UAAU,iBAAiB;AACtC;AGnnCO,IAAM,6BAA6B;AAEnC,SAAS,iBAAiB,OAAe,QAAyB;AACvE,SAAO,SAAS,6BAA6B,mBAAmB,KAAK;AACvE;AAUA,eAAsB,oBACpB,WACA,YAAY,KACkB;AAC9B,QAAM,SAAS,MAAM,UAAU,WAAW,CAAC,uCAAuC,GAAG;IACnF,MAAM;IACN,QAAQ;EACV,CAAC;AACD,MAAI,OAAO,aAAa,GAAG;AACzB,WAAO,EAAE,IAAI,OAAO,QAAQ,uBAAuB,OAAO,UAAU,OAAO,MAAM,GAAG;EACtF;AAEA,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,QAAQ,MAAM;MAClB;MACA;QACE;QACA;QACA;MACF;MACA,EAAE,MAAM,OAAO;IACjB;AACA,QAAI,MAAM,aAAa,EAAG,QAAO,EAAE,IAAI,KAAK;AAC5C,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;EAC7C;AACA,SAAO,EAAE,IAAI,OAAO,QAAQ,uCAAuC,OAAO,SAAS,CAAC,KAAK;AAC3F;ACxCA,eAAsB,gBACpB,WACA,YAAY,KACc;AAC1B,QAAM,SAAS,MAAM,UAAU,WAAW,CAAC,mCAAmC,GAAG;IAC/E,MAAM;IACN,QAAQ;EACV,CAAC;AACD,MAAI,OAAO,aAAa,GAAG;AACzB,WAAO,EAAE,IAAI,OAAO,QAAQ,uBAAuB,OAAO,UAAU,OAAO,MAAM,GAAG;EACtF;AAEA,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,QAAQ,MAAM;MAClB;MACA,CAAC,QAAQ,OAAO,8CAA8C;MAC9D,EAAE,MAAM,SAAS;IACnB;AACA,QAAI,MAAM,aAAa,EAAG,QAAO,EAAE,IAAI,KAAK;AAC5C,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;EAC7C;AACA,SAAO,EAAE,IAAI,OAAO,QAAQ,uCAAuC,OAAO,SAAS,CAAC,KAAK;AAC3F;AAEA,IAAM,wBAAwB;AASvB,SAAS,sBAA8B;AAC5C,QAAM,QAAQ,YAAY,CAAC;AAC3B,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,WAAO,sBAAsB,MAAM,CAAC,IAAK,sBAAsB,MAAM;EACvE;AACA,SAAO;AACT;AAMO,IAAM,qBAAqB;AAe3B,SAAS,aACd,QAOA,QACS;AACT,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,YAAa,QAAO,CAAC;AACvD,QAAM,gBAAgB,OAAO,oBAAoB;AACjD,QAAM,KAAK,0BAA0B,mBAAmB,OAAO,WAAW,CAAC;AAC3E,QAAM,OAAgB,CAAC;AACvB,MAAI,WAAW,YAAY;AACzB,SAAK,SAAS,UAAU,OAAO,SAAS,cAAc,OAAO,aAAa,CAAC,aAAa,EAAE;EAC5F;AACA,MAAI,OAAO,aAAa;AACtB,SAAK,cAAc,oBAAoB,OAAO,OAAO,WAAW,CAAC,aAAa,EAAE;EAClF;AACA,SAAO;AACT;AC7FO,IAAM,qBAAqB;ACWlC,eAAsB,eAAe,WAA+C;AAClF,QAAM,MAAyB,CAAC;AAChC,MAAI,MAAM,SAASC,MAAK,WAAW,MAAM,CAAC,GAAG;AAC3C,QAAI,KAAK,EAAE,MAAM,QAAQ,cAAc,WAAW,sBAAsB,GAAG,CAAC;EAC9E;AACA,MAAI;AACJ,MAAI;AACF,cAAU,MAAMC,SAAQ,WAAW,EAAE,eAAe,KAAK,CAAC;EAC5D,QAAQ;AACN,WAAO;EACT;AACA,aAAW,KAAK,SAAS;AACvB,QAAI,CAAC,EAAE,YAAY,KAAK,EAAE,KAAK,WAAW,GAAG,EAAG;AAChD,UAAM,MAAMD,MAAK,WAAW,EAAE,IAAI;AAClC,QAAI,MAAM,SAASA,MAAK,KAAK,MAAM,CAAC,GAAG;AACrC,UAAI,KAAK,EAAE,MAAM,UAAU,cAAc,KAAK,sBAAsB,EAAE,KAAK,CAAC;IAC9E;EACF;AACA,SAAO;AACT;AAEA,eAAe,SAAS,MAAgC;AACtD,MAAI;AACF,UAAM,IAAI,MAAME,MAAK,IAAI;AACzB,WAAO,EAAE,YAAY;EACvB,QAAQ;AACN,WAAO;EACT;AACF;AAqBA,eAAsB,kBACpB,MACkC;AAClC,QAAM,MAAM,KAAK,UAAU,MAAM;EAAC;AAIlC,QAAM,QAAQ,MAAMC,OAAM,OAAO,CAAC,MAAM,KAAK,cAAc,SAAS,QAAQ,GAAG;IAC7E,QAAQ;EACV,CAAC;AACD,QAAM,WAAW,MAAM,aAAa,IAAI,MAAM,OAAO,KAAK,KAAK,OAAO;AAEtE,QAAM,YAAY,MAAMA;IACtB;IACA,CAAC,MAAM,KAAK,cAAc,YAAY,YAAY,sBAAsB,IAAI;IAC5E,EAAE,QAAQ,MAAM;EAClB;AACA,QAAM,gBACJ,UAAU,aAAa,KAAK,UAAU,OAAO,SAAS,IAClD,UAAU,OAAO,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,IACvD,CAAC;AAIP,QAAM,aAAa,MAAM,gBAAgB,KAAK,cAAc,KAAK,UAAU;AAC3E,QAAM,OAAO,MAAMA;IACjB;IACA,CAAC,MAAM,KAAK,cAAc,YAAY,OAAO,MAAM,YAAY,KAAK,aAAa,MAAM;IACvF,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,KAAK,aAAa,GAAG;AACvB,UAAM,IAAI;MACR,+BAA+B,KAAK,YAAY,KAAK,KAAK,UAAU,KAAK,MAAM;IACjF;EACF;AACA,MAAI,oBAAoB,KAAK,WAAW,cAAc,UAAU,EAAE;AAMlE,QAAMA;IACJ;IACA,CAAC,MAAM,KAAK,cAAc,UAAU,6BAA6B,MAAM;IACvE,EAAE,QAAQ,MAAM;EAClB;AACA,QAAMA;IACJ;IACA,CAAC,MAAM,KAAK,aAAa,UAAU,cAAc,kBAAkB,OAAO;IAC1E,EAAE,QAAQ,MAAM;EAClB;AAEA,MAAI,UAAU;AAIZ,UAAM,YAAY,MAAMA;MACtB;MACA,CAAC,MAAM,KAAK,aAAa,SAAS,SAAS,WAAW,QAAQ;MAC9D,EAAE,QAAQ,MAAM;IAClB;AACA,QAAI,UAAU,aAAa,GAAG;AAC5B,YAAM,UAAU,MAAMA;QACpB;QACA,CAAC,MAAM,KAAK,aAAa,SAAS,SAAS,QAAQ;QACnD,EAAE,QAAQ,MAAM;MAClB;AACA,UAAI,QAAQ,aAAa,GAAG;AAC1B;UACE,4CAA4C,UAAU,UAAU,UAAU,UAAU,YAAY;QAClG;MACF,OAAO;AACL,YAAI,kEAA6D;MACnE;IACF,OAAO;AACL,UAAI,wCAAwC;IAC9C;EACF;AAEA,MAAI,cAAc,SAAS,GAAG;AAE5B,UAAM,SAAS,MAAMA;MACnB;MACA,CAAC,MAAM,KAAK,cAAc,UAAU,MAAM,KAAK,OAAO,GAAG;MACzD;QACE,OAAO,cAAc,KAAK,IAAI;QAC9B,UAAU;QACV,QAAQ;MACV;IACF;AACA,QAAI,OAAO,aAAa,GAAG;AACzB,YAAM,QAAQ,MAAMA,OAAM,OAAO,CAAC,MAAM,KAAK,aAAa,OAAO,GAAG,GAAG;QACrE,OAAO,OAAO;QACd,QAAQ;MACV,CAAC;AACD,UAAI,MAAM,aAAa,GAAG;AACxB,YAAI,sDAAsD,MAAM,MAAM,EAAE;MAC1E,OAAO;AACL,YAAI,UAAU,OAAO,cAAc,MAAM,CAAC,kCAAkC;MAC9E;IACF,OAAO;AACL,UAAI,2CAA2C,OAAO,MAAM,EAAE;IAChE;EACF;AAEA,SAAO,EAAE,YAAY,UAAU,gBAAgB,cAAc,OAAO;AACtE;AAOA,eAAsB,gBAAgB,cAAsB,MAA+B;AACzF,MAAI,YAAY;AAChB,MAAI,SAAS;AACb,SAAO,MAAM,aAAa,cAAc,SAAS,GAAG;AAClD,gBAAY,GAAG,IAAI,IAAI,OAAO,QAAQ,CAAC;AACvC,QAAI,SAAS,IAAK,OAAM,IAAI,iBAAiB,0CAA0C,IAAI,EAAE;EAC/F;AACA,SAAO;AACT;AAEA,eAAe,aAAa,cAAsB,MAAgC;AAChF,QAAM,SAAS,MAAMA;IACnB;IACA,CAAC,MAAM,cAAc,YAAY,YAAY,WAAW,cAAc,IAAI,EAAE;IAC5E,EAAE,QAAQ,MAAM;EAClB;AACA,SAAO,OAAO,aAAa;AAC7B;AAYA,eAAsB,kBAAkB,MAA4C;AAClF,QAAM,SAAS,MAAMA;IACnB;IACA,CAAC,MAAM,KAAK,cAAc,YAAY,UAAU,WAAW,KAAK,WAAW;IAC3E,EAAE,QAAQ,MAAM;EAClB;AACA,MAAI,OAAO,aAAa,EAAG;AAC3B,QAAMA,OAAM,MAAM,CAAC,OAAO,KAAK,WAAW,GAAG,EAAE,QAAQ,MAAM,CAAC;AAC9D,QAAMA,OAAM,OAAO,CAAC,MAAM,KAAK,cAAc,YAAY,OAAO,GAAG,EAAE,QAAQ,MAAM,CAAC;AACtF;AAEO,IAAM,mBAAN,cAA+B,MAAM;EAC1C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;EACd;AACF;AClMO,IAAM,qBAAwC,CAAC,WAAW;AAOjE,IAAM,eAAe;AACrB,IAAM,eAAe;AAgBrB,eAAsB,aACpB,WACA,OAA4B,CAAC,GACA;AAC7B,QAAM,YAAY,KAAK,aAAa,KAAK,UAAU,SAAS,IAAI,KAAK,YAAY,CAAC,WAAW;AAC7F,QAAM,YAAY;IAChB,YAAY,UAAU,KAAK,GAAG,CAAC;IAC/B;IACA;IACA,iBAAiB,OAAO,YAAY,CAAC;IACrC,iBAAiB,OAAO,YAAY,CAAC;EACvC,EAAE,KAAK,GAAG;AAEV,QAAM,QAAQ;IACZ;IACA;;IAEA;IACA,qBAAqB,SAAS;IAC9B;EACF;AAEA,aAAW,KAAK,KAAK,mBAAmB,CAAC,GAAG;AAI1C,UAAM;MACJ,YAAY,WAAW,EAAE,aAAa,CAAC;MACvC,iBAAiB,WAAW,EAAE,aAAa,CAAC,cAAc,WAAW,EAAE,aAAa,CAAC;MACrF,gBAAgB,WAAW,EAAE,aAAa,CAAC,IAAI,WAAW,EAAE,aAAa,CAAC;MAC1E,iBAAiB,WAAW,EAAE,aAAa,CAAC;IAC9C;EACF;AAEA,QAAM,SAAS,MAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,MAAM,KAAK,IAAI,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC;AAC7F,MAAI,OAAO,aAAa,GAAG;AACzB,UAAM,IAAI;MACR,mCAAmC,SAAS;MAC5C,OAAO;MACP,OAAO;IACT;EACF;AACA,SAAO,EAAE,gBAAgB,eAAe;AAC1C;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,IAAI,EAAE,QAAQ,MAAM,OAAO,CAAC;AACrC;AAYA,eAAsB,cACpB,WACA,YAA+B,oBACN;AACzB,QAAM,WAAW;AACjB,QAAM,SAAyB,CAAC;AAGhC,QAAM,KAAK,MAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,4BAA4B,GAAG;IACnF,MAAM;EACR,CAAC;AACD,SAAO,KAAK;IACV,MAAM;IACN,IAAI,GAAG,aAAa;IACpB,QAAQ,GAAG,aAAa,IAAI,gBAAgB,GAAG,OAAO,KAAK,KAAK,SAAS,KAAK,GAAG,OAAO,KAAK;EAC/F,CAAC;AAGD,QAAM,QAAQ,MAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,oBAAoB,QAAQ,EAAE,GAAG;IACxF,MAAM;EACR,CAAC;AACD,SAAO,KAAK;IACV,MAAM;IACN,IAAI,MAAM,aAAa;IACvB,QAAQ,MAAM,aAAa,IAAI,sBAAsB,QAAQ,KAAK,MAAM,OAAO,KAAK;EACtF,CAAC;AAGD,QAAM,QAAQ,MAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,wBAAwB,QAAQ,EAAE,GAAG;IAC5F,MAAM;EACR,CAAC;AACD,SAAO,KAAK;IACV,MAAM;IACN,IAAI,MAAM,aAAa;IACvB,QACE,MAAM,aAAa,IACf,gBAAgB,QAAQ,YACxB,yBAAyB,QAAQ;EACzC,CAAC;AAID,QAAM,aAAa,UAAU,IAAI,CAAC,MAAM,aAAa,WAAW,CAAC,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,MAAM;AAC7F,QAAM,QAAQ,MAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,UAAU,GAAG,EAAE,MAAM,OAAO,CAAC;AACtF,SAAO,KAAK;IACV,MAAM,oBAAoB,UAAU,KAAK,IAAI,CAAC;IAC9C,IAAI,MAAM,aAAa;IACvB,QACE,MAAM,aAAa,IACf,GAAG,QAAQ,6BACX,GAAG,QAAQ;EACnB,CAAC;AAGD,QAAM,UAAU,WAAW,CAAC,QAAQ,OAAO,oBAAoB,QAAQ,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAE5F,SAAO;AACT;AAEO,IAAM,eAAN,cAA2B,MAAM;EACtC,YACE,SACgB,QACA,QAChB;AACA,UAAM,OAAO;AAHG,SAAA,SAAA;AACA,SAAA,SAAA;AAGhB,SAAK,OAAO;EACd;EALkB;EACA;AAKpB;AC3KO,IAAM,eAAoC,oBAAI,IAAI;EACvD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF,CAAC;AAEM,IAAM,iBAAiBH,MAAKI,SAAQ,GAAG,aAAa,WAAW;AAE/D,SAAS,gBAAgB,OAAuB;AACrD,SAAOJ,MAAK,gBAAgB,KAAK;AACnC;AAOA,eAAsB,iBACpB,MACA,WAAgC,cACb;AACnB,QAAM,UAAoB,CAAC;AAC3B,QAAM,OAAO,OAAO,QAA+B;AACjD,QAAI;AACJ,QAAI;AACF,gBAAU,MAAMC,SAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;IACtD,QAAQ;AACN;IACF;AACA,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,YAAM,MAAMD,MAAK,KAAK,MAAM,IAAI;AAChC,UAAI,SAAS,IAAI,MAAM,IAAI,GAAG;AAC5B,gBAAQ,KAAK,GAAG;AAChB;MACF;AACA,YAAM,KAAK,GAAG;IAChB;EACF;AACA,QAAM,KAAK,IAAI;AACf,SAAO;AACT;AAqBA,eAAsB,eAAe,MAA4D;AAC/F,QAAM,SAAS,QAAQ,KAAK,MAAM;AAClC,QAAM,cAAc,QAAQ,KAAK,WAAW;AAC5C,QAAM,WAAW,KAAK,YAAY;AAElC,QAAMK,OAAM,gBAAgB,EAAE,WAAW,KAAK,CAAC;AAG/C,QAAM,SAAS,SAAS,MAAM,WAAW,CAAC,KAAK,IAAI,CAAC,IAAI;AACxD,QAAMF,OAAM,MAAM,CAAC,GAAG,QAAQ,GAAG,MAAM,KAAK,WAAW,CAAC;AAExD,QAAM,UAAU,MAAM,iBAAiB,aAAa,QAAQ;AAC5D,QAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,MAAMG,IAAG,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,CAAC,CAAC;AAE7E,SAAO,EAAE,aAAa,aAAa,QAAQ;AAC7C;ACpFA,eAAsB,gBACpB,WACA,gBACA,YAAY,KACc;AAC1B,QAAM,SAAS,MAAM,UAAU,WAAW,CAAC,gBAAgB,QAAQ,GAAG;IACpE,MAAM;IACN,QAAQ;EACV,CAAC;AACD,MAAI,OAAO,aAAa,GAAG;AACzB,WAAO,EAAE,IAAI,OAAO,QAAQ,uBAAuB,OAAO,UAAU,OAAO,MAAM,GAAG;EACtF;AAEA,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAI,MAAMC,YAAW,cAAc,EAAG,QAAO,EAAE,IAAI,KAAK;AACxD,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;EAC7C;AACA,SAAO;IACL,IAAI;IACJ,QAAQ,UAAU,cAAc,0BAA0B,OAAO,SAAS,CAAC;EAC7E;AACF;AAEA,eAAeA,YAAW,GAA6B;AACrD,MAAI;AACF,UAAML,MAAK,CAAC;AACZ,WAAO;EACT,QAAQ;AACN,WAAO;EACT;AACF;AC1BA,IAAM,YAAYF,MAAKI,SAAQ,GAAG,WAAW;AAC7C,IAAM,WAAWJ,MAAK,WAAW,WAAW;AAC5C,IAAM,WAAWA,MAAK,WAAW,WAAW;AAU5C,IAAM,OAAO;AACb,IAAM,WAA0B;;;;;EAK9B,KAAK,+BAA+B,OAAO,IAAI,CAAC;EAChD,SAAS,oBAAoB,OAAO,IAAI,CAAC;EACzC,MAAM;AACR;AAgBA,eAAsB,YAAY,OAA2B,CAAC,GAA2B;AACvF,QAAM,MAAM,KAAK,UAAU,MAAM;EAAC;AAClC,QAAMK,OAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAM1C,MAAI,MAAM,gBAAgB,oBAAoB,GAAG;AAC/C,UAAM,gBAAgB,oBAAoB;AAC1C,QAAI,kCAAkC,oBAAoB,EAAE;EAC9D;AAEA,MAAI,MAAM,YAAY,GAAG,GAAG;AAC1B,WAAO;EACT;AAEA,QAAM,cAAc,MAAM,YAAY;AACtC,MAAI,gBAAgB,QAAS,MAAM,aAAa,WAAW,GAAI;AAI7D,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAI,MAAM,YAAY,GAAG,EAAG,QAAO;AACnC,YAAM,MAAM,GAAG;IACjB;AACA,QAAI,aAAa,OAAO,WAAW,CAAC,2DAAsD;AAC1F,WAAO;EACT;AACA,MAAI,gBAAgB,MAAM;AACxB,UAAM,OAAO,QAAQ,EAAE,MAAM,MAAM;IAAC,CAAC;EACvC;AAEA,QAAM,WAAW,gBAAgB;AACjC,QAAM,QAAQ,SAAS,UAAU,GAAG;AAIpC,QAAM,WAAW,gBAAgB;AACjC,QAAM,QAAQ;IACZ,QAAQ;IACR,CAAC,UAAU,SAAS,UAAU,OAAO,IAAI,GAAG,UAAU,SAAS;IAC/D;MACE,UAAU;MACV,OAAO,CAAC,UAAU,OAAO,KAAK;MAC9B,KAAK;QACH,GAAG,QAAQ;QACX,GAAI,WAAW,EAAE,oBAAoB,SAAS,IAAI,CAAC;MACrD;IACF;EACF;AACA,QAAM,MAAM;AACZ,MAAI,OAAO,MAAM,QAAQ,UAAU;AACjC,UAAMG,WAAU,UAAU,OAAO,MAAM,GAAG,GAAG,MAAM;AACnD,QAAI,mCAAmC,OAAO,MAAM,GAAG,CAAC,UAAU,OAAO,IAAI,CAAC,GAAG;EACnF;AAEA,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QAAI,MAAM,YAAY,GAAG,GAAG;AAC1B,UAAI,sBAAsB,SAAS,OAAO,EAAE;AAC5C,aAAO;IACT;AACA,UAAM,MAAM,GAAG;EACjB;AACA,QAAM,IAAI;IACR,qCAAqC,SAAS,OAAO,mBAAmB,QAAQ;EAClF;AACF;AAWA,SAAS,kBAA0B;AACjC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,WAAW,QAAQ,EAAG,QAAO;AAC7C,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,QAAM,aAAa;IACjBC,SAAQ,MAAM,MAAM,WAAW,SAAS,SAAS;IACjDA,SAAQ,MAAM,MAAM,MAAM,SAAS,QAAQ,SAAS;IACpDA,SAAQ,MAAM,MAAM,MAAM,MAAM,aAAa,SAAS,QAAQ,SAAS;IACvEA,SAAQ,MAAM,MAAM,MAAM,gBAAgB,aAAa,SAAS,QAAQ,SAAS;EACnF;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,WAAW,CAAC,EAAG,QAAO;EAC5B;AACA,QAAM,IAAI;IACR;IAAmD,WAAW,KAAK,MAAM,CAAC;EAC5E;AACF;AASA,SAAS,kBAAiC;AACxC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,WAAW,QAAQ,EAAG,QAAO;AAC7C,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,QAAM,aAAa;;;IAGjBA,SAAQ,MAAM,UAAU;IACxBA,SAAQ,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,UAAU;IACjEA,SAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,QAAQ,UAAU;EAC1D;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,WAAW,CAAC,EAAG,QAAO;EAC5B;AACA,SAAO;AACT;AAiBA,eAAsB,YAAsC;AAC1D,QAAM,MAAM,MAAM,YAAY;AAC9B,MAAI,QAAQ,MAAM;AAChB,WAAO,EAAE,SAAS,OAAO,KAAK,KAAK;EACrC;AACA,MAAI,CAAE,MAAM,aAAa,GAAG,GAAI;AAC9B,UAAM,OAAO,QAAQ,EAAE,MAAM,MAAM;IAAC,CAAC;AACrC,WAAO,EAAE,SAAS,OAAO,IAAI;EAC/B;AACA,MAAI;AACF,YAAQ,KAAK,KAAK,SAAS;EAC7B,QAAQ;EAER;AACA,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,QAAI,CAAE,MAAM,aAAa,GAAG,EAAI;AAChC,UAAM,MAAM,GAAG;EACjB;AACA,MAAI,MAAM,aAAa,GAAG,GAAG;AAC3B,QAAI;AACF,cAAQ,KAAK,KAAK,SAAS;IAC7B,QAAQ;IAER;EACF;AACA,QAAM,OAAO,QAAQ,EAAE,MAAM,MAAM;EAAC,CAAC;AACrC,SAAO,EAAE,SAAS,MAAM,IAAI;AAC9B;AAEA,SAAS,YAAY,WAAqC;AACxD,SAAO,IAAI,QAAiB,CAAC,aAAa;AACxC,UAAM,MAAM;MACV,EAAE,MAAM,aAAa,MAAM,MAAM,QAAQ,OAAO,MAAM,YAAY,SAAS,UAAU;MACrF,CAAC,QAAQ;AACP,YAAI,OAAO;AACX,cAAM,SAAS,IAAI,cAAc;AACjC,iBAAS,UAAU,OAAO,SAAS,GAAG;MACxC;IACF;AACA,QAAI,GAAG,SAAS,MAAM,SAAS,KAAK,CAAC;AACrC,QAAI,GAAG,WAAW,MAAM;AACtB,UAAI,QAAQ;AACZ,eAAS,KAAK;IAChB,CAAC;AACD,QAAI,IAAI;EACV,CAAC;AACH;AAEA,eAAe,cAAsC;AACnD,MAAI;AACF,UAAM,OAAO,MAAMC,UAAS,UAAU,MAAM;AAC5C,UAAM,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG,EAAE;AAC3C,WAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;EACjD,QAAQ;AACN,WAAO;EACT;AACF;AAEA,eAAe,aAAa,KAA+B;AACzD,MAAI;AAEF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;EACT,QAAQ;AACN,WAAO;EACT;AACF;AAEO,SAAS,qBAA6B;AAC3C,SAAOC,aAAY,EAAE,EAAE,SAAS,KAAK;AACvC;AAiBA,eAAsB,qBAAqB,MAAsC;AAC/E,QAAM,aAA4B,KAAK,aAAa,CAAC,GAAG,IAAI,CAAC,OAAO;IAClE,eAAe,EAAE;IACjB,iBAAiB,EAAE;IACnB,QAAQ,EAAE;EACZ,EAAE;AACF,QAAM,UAAU,uBAAuB;IACrC,OAAO,KAAK;IACZ,OAAO,KAAK;IACZ,MAAM,KAAK;IACX,eAAe,KAAK;IACpB,WAAW,KAAK;IAChB;EACF,CAAC;AACH;AAEA,eAAsB,mBAAmB,OAA8B;AACrE,MAAI;AACF,UAAM,UAAU,qBAAqB,EAAE,MAAM,CAAC;EAChD,QAAQ;EAER;AACF;AAEA,eAAe,UAAU,MAAc,MAA8B;AACnE,QAAM,OAAO,KAAK,UAAU,IAAI;AAChC,QAAM,IAAI,QAAc,CAAC,UAAU,YAAY;AAC7C,UAAM,MAAM;MACV;QACE,MAAM;QACN,MAAM;QACN,QAAQ;QACR;QACA,SAAS;UACP,gBAAgB;UAChB,kBAAkB,OAAO,WAAW,IAAI,EAAE,SAAS;QACrD;QACA,SAAS;MACX;MACA,CAAC,QAAQ;AACP,cAAM,SAAmB,CAAC;AAC1B,YAAI,GAAG,QAAQ,CAAC,MAAc,OAAO,KAAK,CAAC,CAAC;AAC5C,YAAI,GAAG,OAAO,MAAM;AAClB,gBAAM,SAAS,IAAI,cAAc;AACjC,cAAI,UAAU,OAAO,SAAS,KAAK;AACjC,qBAAS;UACX,OAAO;AACL,kBAAM,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAClD,oBAAQ,IAAI,MAAM,SAAS,IAAI,WAAM,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;UACjE;QACF,CAAC;MACH;IACF;AACA,QAAI,GAAG,SAAS,OAAO;AACvB,QAAI,GAAG,WAAW,MAAM;AACtB,UAAI,QAAQ;AACZ,cAAQ,IAAI,MAAM,SAAS,IAAI,UAAU,CAAC;IAC5C,CAAC;AACD,QAAI,MAAM,IAAI;AACd,QAAI,IAAI;EACV,CAAC;AACH;AAgBA,eAAsB,uBAAuB,OAAsC;AACjF,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,WAAY;AACnB,QAAI;AACF,YAAM,qBAAqB;QACzB,OAAO,EAAE;QACT,OAAO,EAAE;QACT,MAAM,EAAE;QACR,eAAe,EAAE;QACjB,WAAW,EAAE;QACb,WAAW,EAAE;MACf,CAAC;IACH,QAAQ;IAER;EACF;AACF;AC9VA,IAAM,WAA0C;EAC9C,QAAQ;IACN,WAAW;IACX,eAAe;IACf,oBAAoB;IACpB,wBAAwB;IACxB,KAAK;IACL,aAAa;IACb,gBAAgB;EAClB;EACA,QAAQ;IACN,WAAW;IACX,eAAe;IACf,oBAAoB;IACpB,wBAAwB;IACxB,KAAK;IACL,aAAa;IACb,gBAAgB;EAClB;AACF;AAEO,IAAM,cAAoC,CAAC,UAAU,QAAQ;AAE7D,SAAS,WAAW,QAA+B;AACxD,SAAO,SAAS,MAAM;AACxB;AAOO,IAAM,kCAAkC,SAAS,OAAO;AAGxD,IAAM,kCAAkC,SAAS,OAAO;AAGxD,SAAS,uBAAuB,OAAuB;AAC5D,SAAO,oBAAoB,UAAU,KAAK;AAC5C;AAGO,SAAS,uBAAuB,OAAuB;AAC5D,SAAO,oBAAoB,UAAU,KAAK;AAC5C;AAEO,SAAS,oBAAoB,QAAmB,OAAuB;AAC5E,SAAO,GAAG,SAAS,MAAM,EAAE,kBAAkB,GAAG,KAAK;AACvD;AAcO,SAAS,kBAAkB,QAAmB,OAA0B;AAC7E,QAAM,UAAU,SAAS,MAAM;AAC/B,QAAM,SAAS,oBAAoB,QAAQ,KAAK;AAChD,SAAO;IACL,SAAS,CAAC,QAAQ,QAAQ,sBAAsB;IAChD,cAAc;MACZ,GAAG,MAAM,IAAI,QAAQ,SAAS;MAC9B,GAAG,QAAQ,sBAAsB,IAAI,QAAQ,aAAa;IAC5D;EACF;AACF;AAWO,SAAS,eAAe,OAA0B;AACvD,QAAM,SAAoB,EAAE,SAAS,CAAC,GAAG,cAAc,CAAC,EAAE;AAC1D,aAAW,KAAK,aAAa;AAC3B,UAAM,IAAI,kBAAkB,GAAG,KAAK;AACpC,WAAO,QAAQ,KAAK,GAAG,EAAE,OAAO;AAChC,WAAO,aAAa,KAAK,GAAG,EAAE,YAAY;EAC5C;AACA,SAAO;AACT;AAQA,eAAsB,iBAAiB,OAA8B;AACnE,aAAW,KAAK,eAAe,KAAK,EAAE,QAAS,OAAM,aAAa,CAAC;AACrE;AAiBA,eAAsB,mBAAmB,WAAkC;AACzE,aAAW,UAAU,aAAa;AAChC,UAAM,UAAU,WAAW,CAAC,SAAS,MAAM,iBAAiB,SAAS,MAAM,EAAE,SAAS,GAAG;MACvF,MAAM;IACR,CAAC;EACH;AACF;AAOO,SAAS,aAAa,eAA+B;AAC1D,SAAO,OAAO,KAAK,eAAe,MAAM,EAAE,SAAS,KAAK;AAC1D;AAsBA,IAAM,WACJ;AAkBF,eAAsB,wBACpB,WACA,UACA,OAA4B,CAAC,GACG;AAChC,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,QAAQ,sBAAsB;AAElE,QAAM,WAAW,MAAM,UAAU,WAAW,CAAC,OAAO,+BAA+B,GAAG;IACpF,MAAM;EACR,CAAC;AACD,MAAI,SAAS,aAAa,KAAK,CAAC,KAAK,SAAS,CAAC,SAAS,OAAO,SAAS,QAAQ,GAAG;AACjF,WAAO,EAAE,QAAQ,qBAAqB;EACxC;AAEA,QAAM,QAAQ,SAAS,IAAI,CAAC,OAAO;IACjC,OAAO,aAAa,EAAE,IAAI;IAC1B,MAAM;IACN,SAAS,6BAA6B,EAAE,IAAI;IAC5C,cAAc;IACd,cAAc,EAAE,OAAO,aAAa,QAAQ,UAAU,MAAM,MAAM;IAClE,YAAY,EAAE,OAAO,aAAa;IAClC,gBAAgB,CAAC;EACnB,EAAE;AACF,QAAM,OACJ,GAAG,QAAQ;IACX,KAAK;IACH;MACE,SAAS;MACT;IACF;IACA;IACA;EACF,IACA;AAEF,QAAM,UAAU,WAAW,CAAC,SAAS,MAAM,oBAAoB,GAAG,EAAE,MAAM,SAAS,CAAC;AACpF,QAAM,QAAQ,MAAM,eAAe,WAAW,iCAAiC,IAAI;AACnF,MAAI,MAAM,aAAa,GAAG;AACxB,UAAM,IAAI,MAAM,iCAAiC,SAAS,KAAK,MAAM,UAAU,MAAM,MAAM,EAAE;EAC/F;AACA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAEA,eAAe,eACb,WACA,MACA,SAC2B;AAC3B,QAAM,EAAE,OAAAC,OAAM,IAAI,MAAM,OAAO,OAAO;AACtC,QAAM,SAAS,MAAMA;IACnB;IACA,CAAC,QAAQ,MAAM,UAAU,UAAU,WAAW,MAAM,MAAM,SAASC,YAAW,IAAI,CAAC,EAAE;IACrF,EAAE,OAAO,SAAS,QAAQ,MAAM;EAClC;AACA,SAAO;IACL,UAAU,OAAO,YAAY;IAC7B,QAAQ,OAAO,UAAU;IACzB,QAAQ,OAAO,UAAU;EAC3B;AACF;AAEA,SAASA,YAAW,GAAmB;AACrC,SAAO,IAAI,EAAE,QAAQ,MAAM,OAAO,CAAC;AACrC;;;ASnQA,SAAS,YAAAC,iBAAgB;AACzB,SAAS,SAAS,iBAAiB;ADC5B,SAAS,kBAAkB,MAA+B;AAC/D,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,UAAU,CAAC,QAAQ,SAAS,OAAO,YAAY,aAAa,cAAc,SAAS;AACzF,QAAM,OAAmB,KAAK,IAAI,CAAC,MAAM;IACvC,EAAE;IACF,EAAE;IACF,EAAE,QAAQ,OAAO,MAAM,OAAO,EAAE,GAAG;IACnC,OAAO,EAAE,QAAQ;IACjB,EAAE,iBAAiB,OAAO,MAAM,OAAO,EAAE,YAAY;IACrD,EAAE,UAAU,WAAW,IAAI,MAAM,EAAE,UAAU,KAAK,GAAG;IACrD,SAAS,EAAE,SAAS,EAAE;EACxB,CAAC;AACD,SAAO,YAAY,SAAS,IAAI;AAClC;AAEO,SAAS,gBAAgB,MAA4B;AAC1D,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,UAAU,CAAC,QAAQ,SAAS,QAAQ,WAAW,YAAY,SAAS;AAC1E,QAAM,OAAmB,KAAK,IAAI,CAAC,MAAM;IACvC,EAAE;IACF,EAAE;IACF,EAAE,iBAAiB,OAAO,MAAM,OAAO,EAAE,YAAY;IACrD,EAAE,aAAa;IACf,EAAE,cAAc;IAChB,SAAS,EAAE,SAAS,EAAE;EACxB,CAAC;AACD,SAAO,YAAY,SAAS,IAAI;AAClC;AAEO,SAAS,iBAAiB,MAA+B;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO;AAC1C,QAAM,QAAQ,KACX,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EACxB,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACvB,QAAM,QAAkB,CAAC;AACzB,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM;MACJ;QACE,CAAC,QAAQ,SAAS;QAClB,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,CAAC;MAC3D;IACF;EACF;AAGA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,KAAK,UAAU,MAAM,MAAM,MAAM,MAAM,KAAK,IAAI,CAAC,EAAE;EAC3D;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,YAAY,SAAmB,MAA0B;AAChE,QAAM,SAAS,QAAQ;IAAI,CAAC,GAAG,MAC7B,KAAK,IAAI,EAAE,QAAQ,GAAG,KAAK,IAAI,CAAC,SAAS,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;EAChE;AACA,QAAM,MAAM,CAAC,QACX,IAAI,IAAI,CAAC,MAAM,MAAM,KAAK,OAAO,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,KAAK,IAAI;AACvE,SAAO,CAAC,IAAI,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,CAAC,EAAE,KAAK,IAAI;AACnD;AAEA,SAAS,SAAS,GAAW,GAAmB;AAC9C,MAAI,EAAE,UAAU,EAAG,QAAO;AAC1B,SAAO,EAAE,MAAM,GAAG,IAAI,CAAC,IAAI;AAC7B;ACQO,IAAM,kBAA+B;EAC1C,WAAW;EACX,OAAO;EACP,QAAQ;AACV;AAEO,IAAM,4BAA4B;AAClC,IAAM,iCAAiC;AACvC,IAAM,2BAA2B;AACjC,IAAM,qBAAqB;AAC3B,IAAM,2BAA2C;AAEjD,IAAM,cAAN,cAA0B,MAAM;EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;EACd;AACF;AAEA,SAASC,eAAc,GAA0C;AAC/D,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAChE;AAEA,SAAS,SAAS,KAAc,OAAmD;AACjF,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO;AAC9C,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,gDAA2C;EAC3E;AACA,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,OAAO,MAAM,WAAW;AAC5E,YAAM,IAAI,YAAY,GAAG,KAAK,QAAQ,CAAC,mBAAmB;IAC5D;AACA,QAAI,CAAC,IAAI,OAAO,CAAC;EACnB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,KAAc,OAAkC;AACpE,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI,IAAI,KAAK,EAAE,WAAW,GAAG;AAC3B,YAAM,IAAI,YAAY,GAAG,KAAK,4BAA4B;IAC5D;AACA,WAAO;EACT;AACA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,QAAI,IAAI,WAAW,GAAG;AACpB,YAAM,IAAI,YAAY,GAAG,KAAK,kCAAkC;IAClE;AACA,UAAM,OAAiB,CAAC;AACxB,eAAW,CAAC,GAAG,IAAI,KAAK,IAAI,QAAQ,GAAG;AACrC,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,IAAI,YAAY,GAAG,KAAK,YAAY,OAAO,CAAC,CAAC,oBAAoB;MACzE;AACA,WAAK,KAAK,IAAI;IAChB;AACA,WAAO;EACT;AACA,QAAM,IAAI,YAAY,GAAG,KAAK,+CAA+C;AAC/E;AAEA,SAAS,aAAa,KAAc,OAA8B;AAChE,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI,QAAQ,YAAY,QAAQ,gBAAgB,QAAQ,QAAS,QAAO;AACxE,QAAM,IAAI,YAAY,GAAG,KAAK,oDAAoD;AACpF;AAEA,IAAM,eAAe,oBAAI,IAAI,CAAC,cAAc,UAAU,QAAQ,CAAC;AAE/D,SAAS,aAAa,KAAc,OAA4B;AAC9D,MAAI,QAAQ,OAAW,QAAO,EAAE,GAAG,gBAAgB;AACnD,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,4BAA4B;EAC5D;AACA,oBAAkB,KAAK,cAAc,GAAG,KAAK,UAAU;AACvD,QAAM,YAAY;IAChB,IAAI;IACJ,GAAG,KAAK;IACR,gBAAgB;EAClB;AACA,QAAM,QAAQ,oBAAoB,IAAI,QAAQ,GAAG,KAAK,mBAAmB,gBAAgB,KAAK;AAC9F,QAAM,SAAS,YAAY,IAAI,QAAQ,GAAG,KAAK,mBAAmB,gBAAgB,MAAM;AACxF,MAAI,QAAQ,WAAW;AACrB,UAAM,IAAI,YAAY,GAAG,KAAK,uCAAuC;EACvE;AACA,SAAO,EAAE,WAAW,OAAO,OAAO;AACpC;AAEA,SAAS,kBACP,KACA,SACA,OACM;AACN,aAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,YAAM,IAAI,YAAY,GAAG,KAAK,qBAAqB,GAAG,GAAG;IAC3D;EACF;AACF;AAEA,SAAS,oBAAoB,KAAc,OAAe,UAA0B;AAClF,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AAC/D,UAAM,IAAI,YAAY,GAAG,KAAK,gCAAgC;EAChE;AACA,SAAO,KAAK,MAAM,GAAG;AACvB;AAEA,SAAS,iBAAiB,KAAc,OAAe,UAA0B;AAC/E,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AAC/D,UAAM,IAAI,YAAY,GAAG,KAAK,6BAA6B;EAC7D;AACA,SAAO,KAAK,MAAM,GAAG;AACvB;AAEA,SAAS,YAAY,KAAc,OAAe,UAA0B;AAC1E,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,GAAG,KAAK,MAAM,GAAG;AAC/D,UAAM,IAAI,YAAY,GAAG,KAAK,wBAAwB;EACxD;AACA,SAAO;AACT;AAEA,SAAS,eAAe,KAAc,OAA+B;AACnE,MAAI,QAAQ,OAAW,QAAO;AAC9B,MAAI,QAAQ,UAAU,QAAQ,iBAAkB,QAAO;AACvD,QAAM,IAAI,YAAY,GAAG,KAAK,uCAAuC;AACvE;AAEA,SAAS,WAAW,KAAc,OAAyB;AACzD,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,CAAC;AAC/C,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,iCAAiC;EACjE;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,CAAC,GAAG,IAAI,KAAK,IAAI,QAAQ,GAAG;AACrC,QAAI,OAAO,SAAS,UAAU;AAC5B,YAAM,IAAI,YAAY,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,oBAAoB;IACjE;AACA,QAAI,CAAC,mBAAmB,KAAK,IAAI,GAAG;AAClC,YAAM,IAAI,YAAY,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,MAAM,IAAI,6BAA6B;IACpF;AACA,QAAI,KAAK,IAAI,IAAI,EAAG;AACpB,SAAK,IAAI,IAAI;AACb,QAAI,KAAK,IAAI;EACf;AACA,SAAO;AACT;AAEA,IAAM,aAAa,oBAAI,IAAI;EACzB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF,CAAC;AAED,SAAS,eAAe,KAAc,OAAuC;AAC3E,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO;AAC9C,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,+BAA+B;EAC/D;AACA,oBAAkB,KAAK,YAAY,GAAG,KAAK,aAAa;AAExD,QAAM,QAA8C,CAAC;AACrD,MAAI,IAAI,SAAS,OAAW,OAAM,KAAK,MAAM;AAC7C,MAAI,IAAI,cAAc,OAAW,OAAM,KAAK,WAAW;AACvD,MAAI,IAAI,SAAS,OAAW,OAAM,KAAK,MAAM;AAC7C,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI;MACR,GAAG,KAAK;IACV;EACF;AACA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI;MACR,GAAG,KAAK,mEAAmE,MAAM,KAAK,IAAI,CAAC;IAC7F;EACF;AAEA,QAAM,YAAY;IAChB,IAAI;IACJ,GAAG,KAAK;IACR;EACF;AACA,QAAM,YAAY,eAAe,IAAI,YAAY,GAAG,KAAK,wBAAwB;AAEjF,QAAM,OAAO,MAAM,CAAC;AACpB,MAAI,SAAS,aAAa;AACxB,QAAI,IAAI,SAAS,UAAa,IAAI,kBAAkB,UAAa,IAAI,gBAAgB,UAAa,IAAI,qBAAqB,QAAW;AACpI,YAAM,IAAI;QACR,GAAG,KAAK;MACV;IACF;AACA,UAAM,MAAM,aAAa,IAAI,WAAW,GAAG,KAAK,uBAAuB;AACvE,QAAI;AACJ,QAAI;AACF,gBAAU,IAAI,OAAO,GAAG;IAC1B,SAAS,KAAK;AACZ,YAAM,IAAI;QACR,GAAG,KAAK,+CAA+C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;MACzG;IACF;AACA,WAAO,EAAE,MAAM,aAAa,SAAS,WAAW,UAAU;EAC5D;AAEA,QAAM,aAAa;IACjB,IAAI;IACJ,GAAG,KAAK;IACR;EACF;AACA,QAAM,iBAAiB;IACrB,IAAI;IACJ,GAAG,KAAK;IACR;EACF;AAEA,MAAI,SAAS,QAAQ;AACnB,QAAI,IAAI,kBAAkB,QAAW;AACnC,YAAM,IAAI,YAAY,GAAG,KAAK,uDAAuD;IACvF;AACA,UAAM,OAAO,iBAAiB,IAAI,MAAM,GAAG,KAAK,oBAAoB,CAAC;AACrE,QAAI,OAAO,KAAK,OAAO,OAAO;AAC5B,YAAM,IAAI,YAAY,GAAG,KAAK,8CAA8C;IAC9E;AACA,UAAM,OACJ,IAAI,SAAS,SACT,qBACA,aAAa,IAAI,MAAM,GAAG,KAAK,kBAAkB;AACvD,WAAO,EAAE,MAAM,QAAQ,MAAM,MAAM,YAAY,gBAAgB,WAAW,UAAU;EACtF;AAEA,MAAI,IAAI,SAAS,QAAW;AAC1B,UAAM,IAAI,YAAY,GAAG,KAAK,8CAA8C;EAC9E;AACA,QAAM,MAAM,aAAa,IAAI,MAAM,GAAG,KAAK,kBAAkB;AAC7D,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;EACtB,QAAQ;AACN,UAAM,IAAI,YAAY,GAAG,KAAK,sCAAsC;EACtE;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI,YAAY,GAAG,KAAK,6CAA6C,OAAO,QAAQ,GAAG;EAC/F;AACA,MAAI;AACJ,MAAI,IAAI,kBAAkB,QAAW;AACnC,mBAAe,iBAAiB,IAAI,eAAe,GAAG,KAAK,6BAA6B,CAAC;AACzF,QAAI,eAAe,OAAO,eAAe,KAAK;AAC5C,YAAM,IAAI,YAAY,GAAG,KAAK,uDAAuD;IACvF;EACF;AACA,SAAO,EAAE,MAAM,QAAQ,KAAK,cAAc,YAAY,gBAAgB,WAAW,UAAU;AAC7F;AAQO,IAAM,oBAAoB;AAEjC,IAAM,eAAe,oBAAI,IAAI;EAC3B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF,CAAC;AAED,IAAM,cAAc,oBAAI,IAAI,CAAC,QAAQ,IAAI,CAAC;AAE1C,SAAS,YAAY,KAAc,OAAuC;AACxE,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO;AAC9C,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,2BAA2B;EAC3D;AACA,oBAAkB,KAAK,aAAa,GAAG,KAAK,SAAS;AACrD,MAAI,IAAI,SAAS,QAAW;AAC1B,UAAM,IAAI,YAAY,GAAG,KAAK,0BAA0B;EAC1D;AACA,QAAM,OAAO,gBAAgB,IAAI,MAAM,GAAG,KAAK,cAAc;AAC7D,QAAM,KAAK,IAAI,OAAO,SAAY,oBAAoB,gBAAgB,IAAI,IAAI,GAAG,KAAK,YAAY;AAClG,MAAI,OAAO,mBAAmB;AAC5B,UAAM,IAAI;MACR,GAAG,KAAK,sBAAsB,OAAO,iBAAiB,CAAC;IACzD;EACF;AACA,SAAO,EAAE,MAAM,GAAG;AACpB;AAEA,SAAS,gBAAgB,KAAc,OAAuB;AAC5D,MAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,KAAK,MAAM,OAAO;AAC/E,UAAM,IAAI,YAAY,GAAG,KAAK,yCAAyC;EACzE;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAAc,KAA2B;AAC7D,QAAM,QAAQ,YAAY,IAAI;AAC9B,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,oBAAoB;EACpD;AACA,oBAAkB,KAAK,cAAc,KAAK;AAC1C,QAAM,UAAU,aAAa,IAAI,SAAS,KAAK;AAC/C,QAAM,MAAM,IAAI,QAAQ,SAAY,SAAY,aAAa,IAAI,KAAK,GAAG,KAAK,MAAM;AACpF,QAAM,MAAM,SAAS,IAAI,KAAK,KAAK;AACnC,QAAM,YACJ,IAAI,cAAc,SAAY,OAAO,WAAW,IAAI,WAAW,GAAG,KAAK,YAAY;AACrF,QAAMC,WAAU,aAAa,IAAI,SAAS,KAAK;AAC/C,QAAM,UAAU,aAAa,IAAI,SAAS,KAAK;AAC/C,QAAM,QAAQ,WAAW,IAAI,OAAO,GAAG,KAAK,QAAQ;AACpD,QAAM,YAAY,eAAe,IAAI,YAAY,KAAK;AACtD,QAAM,SAAS,YAAY,IAAI,QAAQ,KAAK;AAC5C,SAAO,EAAE,MAAM,SAAS,KAAK,KAAK,WAAW,SAAAA,UAAS,SAAS,OAAO,WAAW,OAAO;AAC1F;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,WAAW,OAAO,OAAO,OAAO,CAAC;AAE5D,SAAS,UAAU,MAAc,KAAwB;AACvD,QAAM,QAAQ,SAAS,IAAI;AAC3B,MAAI,CAACD,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,GAAG,KAAK,oBAAoB;EACpD;AACA,oBAAkB,KAAK,WAAW,KAAK;AACvC,QAAM,UAAU,aAAa,IAAI,SAAS,KAAK;AAC/C,QAAM,MAAM,IAAI,QAAQ,SAAY,SAAY,aAAa,IAAI,KAAK,GAAG,KAAK,MAAM;AACpF,QAAM,MAAM,SAAS,IAAI,KAAK,KAAK;AACnC,QAAM,QAAQ,WAAW,IAAI,OAAO,GAAG,KAAK,QAAQ;AACpD,SAAO,EAAE,MAAM,SAAS,KAAK,KAAK,MAAM;AAC1C;AAEA,SAAS,aAAa,KAAc,OAAuB;AACzD,MAAI,OAAO,QAAQ,SAAU,OAAM,IAAI,YAAY,GAAG,KAAK,mBAAmB;AAC9E,SAAO;AACT;AAEA,SAAS,WAAW,KAAc,OAAwB;AACxD,MAAI,OAAO,QAAQ,UAAW,OAAM,IAAI,YAAY,GAAG,KAAK,oBAAoB;AAChF,SAAO;AACT;AAMA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,YAAY,SAAS,OAAO,UAAU,CAAC;AAEvE,SAAS,kBAAkB,OAAmB,UAA+B;AAC3E,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,KAAK,OAAO;AACrB,QAAI,MAAM,IAAI,EAAE,IAAI,GAAG;AACrB,YAAM,IAAI,YAAY,cAAc,EAAE,IAAI,uDAAuD;IACnG;AACA,UAAM,IAAI,EAAE,IAAI;EAClB;AACA,aAAW,KAAK,UAAU;AACxB,QAAI,MAAM,IAAI,EAAE,IAAI,GAAG;AACrB,YAAM,IAAI,YAAY,cAAc,EAAE,IAAI,uDAAuD;IACnG;AACA,UAAM,IAAI,EAAE,IAAI;EAClB;AAEA,QAAM,OAAO,oBAAI,IAAsB;AACvC,aAAW,KAAK,MAAO,MAAK,IAAI,EAAE,MAAM,EAAE,KAAK;AAC/C,aAAW,KAAK,SAAU,MAAK,IAAI,EAAE,MAAM,EAAE,KAAK;AAElD,aAAW,CAAC,MAAM,IAAI,KAAK,MAAM;AAC/B,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,MAAM,IAAI,GAAG,GAAG;AACnB,cAAM,IAAI,YAAY,SAAS,IAAI,yBAAyB,GAAG,GAAG;MACpE;AACA,UAAI,QAAQ,MAAM;AAChB,cAAM,IAAI,YAAY,SAAS,IAAI,2BAA2B;MAChE;IACF;EACF;AAGA,QAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ;AACnC,QAAM,QAAQ,oBAAI,IAAoB;AACtC,aAAW,QAAQ,KAAK,KAAK,EAAG,OAAM,IAAI,MAAM,KAAK;AACrD,QAAM,QAAkB,CAAC;AAEzB,WAAS,MAAM,MAAoB;AACjC,UAAM,IAAI,MAAM,IAAI;AACpB,UAAM,KAAK,IAAI;AACf,eAAW,OAAO,KAAK,IAAI,IAAI,GAAI;AACjC,YAAM,IAAI,MAAM,IAAI,GAAG,KAAK;AAC5B,UAAI,MAAM,MAAM;AACd,cAAM,WAAW,MAAM,QAAQ,GAAG;AAClC,cAAM,QAAQ,MAAM,MAAM,QAAQ,EAAE,OAAO,GAAG,EAAE,KAAK,UAAK;AAC1D,cAAM,IAAI,YAAY,sBAAsB,KAAK,EAAE;MACrD;AACA,UAAI,MAAM,MAAO,OAAM,GAAG;IAC5B;AACA,UAAM,IAAI;AACV,UAAM,IAAI,MAAM,KAAK;EACvB;AAEA,aAAW,QAAQ,KAAK,KAAK,GAAG;AAC9B,QAAI,MAAM,IAAI,IAAI,MAAM,MAAO,OAAM,IAAI;EAC3C;AACF;AAEO,SAAS,YAAY,MAAyB;AACnD,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,IAAI;EACtB,SAAS,KAAK;AACZ,UAAM,IAAI,YAAY,qBAAqB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;EAC/F;AACA,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO,EAAE,UAAU,CAAC,GAAG,OAAO,CAAC,EAAE;AACxE,MAAI,CAACA,eAAc,GAAG,GAAG;AACvB,UAAM,IAAI,YAAY,oCAAoC;EAC5D;AACA,oBAAkB,KAAK,gBAAgB,QAAQ;AAE/C,QAAM,WAA0B,CAAC;AACjC,QAAM,cAAc,IAAI;AACxB,MAAI,gBAAgB,UAAa,gBAAgB,MAAM;AACrD,QAAI,CAACA,eAAc,WAAW,GAAG;AAC/B,YAAM,IAAI,YAAY,mDAA8C;IACtE;AACA,eAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,WAAW,GAAG;AACrD,UAAI,CAAC,mBAAmB,KAAK,IAAI,GAAG;AAClC,cAAM,IAAI,YAAY,iBAAiB,IAAI,6BAA6B;MAC1E;AACA,eAAS,KAAK,aAAa,MAAM,GAAG,CAAC;IACvC;EACF;AAEA,QAAM,QAAoB,CAAC;AAC3B,QAAM,WAAW,IAAI;AACrB,MAAI,aAAa,UAAa,aAAa,MAAM;AAC/C,QAAI,CAACA,eAAc,QAAQ,GAAG;AAC5B,YAAM,IAAI,YAAY,6CAAwC;IAChE;AACA,eAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAClD,UAAI,CAAC,mBAAmB,KAAK,IAAI,GAAG;AAClC,cAAM,IAAI,YAAY,cAAc,IAAI,6BAA6B;MACvE;AACA,YAAM,KAAK,UAAU,MAAM,GAAG,CAAC;IACjC;EACF;AAIA,MAAI,IAAI,QAAQ,UAAa,IAAI,QAAQ,QAAQ,CAACA,eAAc,IAAI,GAAG,GAAG;AACxE,UAAM,IAAI,YAAY,uBAAuB;EAC/C;AAMA,MAAI,IAAI,aAAa,UAAa,IAAI,aAAa,QAAQ,CAACA,eAAc,IAAI,QAAQ,GAAG;AACvF,UAAM,IAAI,YAAY,4BAA4B;EACpD;AAEA,oBAAkB,OAAO,QAAQ;AAEjC,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,MAAM;AAC/C,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI;MACR,6CAA6C,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACpF;EACF;AAEA,SAAO,EAAE,UAAU,MAAM;AAC3B;AAEA,eAAsB,WAAW,MAAkC;AACjE,MAAI;AACJ,MAAI;AACF,WAAO,MAAME,UAAS,MAAM,MAAM;EACpC,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,aAAO,EAAE,UAAU,CAAC,GAAG,OAAO,CAAC,EAAE;IACnC;AACA,UAAM;EACR;AACA,SAAO,YAAY,IAAI;AACzB;","names":["execa","readdir","stat","join","mkdir","rm","homedir","randomBytes","readFile","writeFile","resolve","join","readdir","stat","execa","homedir","mkdir","rm","pathExists","writeFile","resolve","readFile","randomBytes","execa","shellQuote","readFile","isPlainObject","restart","readFile"]}
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
createBox,
|
|
4
4
|
defaultBoxName,
|
|
5
5
|
sanitizeBasename
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-3NCUES35.js";
|
|
7
|
+
import "./chunk-MOC54XL6.js";
|
|
8
8
|
import "./chunk-IDR4HVIC.js";
|
|
9
9
|
import "./chunk-SOMIKEN2.js";
|
|
10
10
|
export {
|
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
defaultBoxName,
|
|
13
13
|
sanitizeBasename
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=create-
|
|
15
|
+
//# sourceMappingURL=create-SE6H4B5U-IWAZHJHV.js.map
|