Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@codeyam-editor/codeyam-editor

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@codeyam-editor/codeyam-editor - npm Package Compare versions

Comparing version
0.1.0-staging.46bd02f
to
0.1.0-staging.5d9fd64
+914
npm/cloud.js
#!/usr/bin/env node
/**
* cloud.js — one-command orchestrator for hosting a codeyam-editor session
* on a GCE VM. Reuses `scripts/gcp-bootstrap.sh` and
* `scripts/gcp-project-bootstrap.sh` verbatim by streaming them over
* `gcloud compute ssh`. Per-project state lives in `.codeyam/cloud.json`
* (gitignored) so the second invocation is zero-arg. See
* `docs/cloud-gcp.md` for the runbook this wraps.
*/
const { spawn, spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const {
rootDir,
isPortInUse,
waitForPort,
deepMergeJson,
killChildProcess,
} = require("./utils");
const { open } = require("./open");
// =============================================================================
// Pure helpers — every export here is unit-tested in cloud.test.js without
// touching `gcloud` or the network.
// =============================================================================
const AGENT_ENV_VARS = {
claude: "ANTHROPIC_API_KEY",
codex: "OPENAI_API_KEY",
gemini: "GEMINI_API_KEY",
};
const KNOWN_FLAGS = ["repo", "branch", "instance-name", "project", "zone", "agent"];
const FIREWALL_RULE_NAME = "codeyam-editor-iap-ssh";
const INSTANCE_TAG = "codeyam-editor";
const EDITOR_PORT = 14199;
const VITE_PORT = 5173;
/** Map an agent name to its API-key env var. Throws on unknown agents so
* adding an agent is a one-entry change in `AGENT_ENV_VARS`, never a
* silent fallthrough that ships unauthenticated. */
function agentEnvVarName(agent) {
if (!Object.prototype.hasOwnProperty.call(AGENT_ENV_VARS, agent)) {
throw new Error(
`Unknown agent: ${agent}. Supported: ${Object.keys(AGENT_ENV_VARS).join(", ")}`,
);
}
return AGENT_ENV_VARS[agent];
}
/** Parse `--flag value` argv into an object. Unknown flags or missing values
* throw with a usage hint — silently dropping them would let typos like
* `--branchh main` succeed with no branch flag at all. */
function parseFlags(argv) {
const flags = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) {
throw new Error(
`Unexpected positional argument: ${arg}\n` +
`Usage: npm run cloud:<up|tunnel|stop|destroy|status> -- [${KNOWN_FLAGS
.map((f) => `--${f} <value>`)
.join("] [")}]`,
);
}
const key = arg.slice(2);
if (!KNOWN_FLAGS.includes(key)) {
throw new Error(`Unknown flag: ${arg}. Known: --${KNOWN_FLAGS.join(", --")}`);
}
const value = argv[i + 1];
if (value === undefined || value.startsWith("--")) {
throw new Error(`${arg} requires a value`);
}
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
flags[camel] = value;
i++;
}
return flags;
}
/** Strip `.git`, take the basename, lowercase, collapse non-alphanumerics to
* hyphens. Mirrors the existing `scripts/gcp-project-bootstrap.sh` derivation
* so the directory layout on the VM matches the instance name 1:1. */
function repoBasename(repoUrl) {
const stripped = repoUrl.replace(/\.git$/i, "");
const parts = stripped.split(/[/:]+/).filter(Boolean);
return parts[parts.length - 1] || "project";
}
/** Construct a deterministic instance name from a repo URL. GCE caps
* instance names at 63 chars; we cap at 40 to keep room for hyphen
* separators and stay well below the limit. */
function deriveInstanceName(repoUrl) {
const base = repoBasename(repoUrl);
let slug = base
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
if (!slug) slug = "project";
let name = `codeyam-editor-${slug}`;
if (name.length > 40) name = name.slice(0, 40).replace(/-+$/g, "");
return name;
}
function cloudConfigPath(projectDir) {
return path.join(projectDir, ".codeyam", "cloud.json");
}
/** Returns the parsed config, or `null` when the file is absent. Throws
* with the full path on malformed JSON so the user knows which file to
* fix — silently returning `null` would mask corruption as "no config". */
function loadCloudConfig(projectDir) {
const p = cloudConfigPath(projectDir);
if (!fs.existsSync(p)) return null;
let raw;
try {
raw = fs.readFileSync(p, "utf8");
} catch (e) {
throw new Error(`Failed to read ${p}: ${e.message}`);
}
try {
return JSON.parse(raw);
} catch (e) {
throw new Error(`Failed to parse ${p}: ${e.message}`);
}
}
function saveCloudConfig(projectDir, cfg) {
const p = cloudConfigPath(projectDir);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n");
}
/** Build the JSON patch to merge into the VM's `editor.local.json` so the
* configured agent picks up the user's local API key on first launch.
* Returns `null` when the key isn't set locally — the wrapper then prints
* the interactive-auth fallback line instead of writing an empty patch. */
function buildEditorLocalEnvPatch(localEnv, agent) {
const varName = agentEnvVarName(agent);
const value = localEnv[varName];
if (!value) return null;
return { env: { [varName]: value } };
}
/** Parse `docker compose ps --format json` (compose v2 — one JSON object per
* line) and return the editor service's `State` string, or null when the
* service is absent / output is unparseable. Two callers shared an identical
* inline parser; this centralizes the shape so a compose-output change is
* one update, not two. */
function parseEditorComposeState(stdout) {
if (!stdout) return null;
const lines = String(stdout).trim().split("\n").filter(Boolean);
for (const line of lines) {
let svc;
try {
svc = JSON.parse(line);
} catch {
continue;
}
if (svc && svc.Service === "editor") {
return svc.State || null;
}
}
return null;
}
/** Translate one of the wrapper's verbs into the exact `gcloud` argv. The
* switch's job is to keep every per-verb command shape in one reviewable
* place — tests assert the arrays without ever spawning a process. */
function buildGcloudArgs(verb, cfg = {}, extras = {}) {
const { project, zone, instanceName } = cfg;
switch (verb) {
case "config-get-project":
return ["config", "get-value", "project"];
case "config-get-zone":
return ["config", "get-value", "compute/zone"];
case "instances-create":
// The VM keeps the default ephemeral external IP so apt / docker /
// NodeSource / npm can reach the public internet during bootstrap.
// Inbound is still IAP-SSH-only via the firewall rule above; the
// external IP is for egress, not exposure. `--no-address` was the
// earlier default but produced a VM that could not install Docker
// without a Cloud NAT + Cloud Router pair the wrapper does not set up.
return [
"compute", "instances", "create", instanceName,
"--project", project, "--zone", zone,
"--machine-type=e2-standard-4",
"--image-family=debian-12",
"--image-project=debian-cloud",
"--boot-disk-size=100GB",
"--boot-disk-type=pd-balanced",
`--tags=${INSTANCE_TAG}`,
];
case "instances-start":
return ["compute", "instances", "start", instanceName, "--project", project, "--zone", zone];
case "instances-stop":
return ["compute", "instances", "stop", instanceName, "--project", project, "--zone", zone];
case "instances-delete":
return ["compute", "instances", "delete", instanceName, "--project", project, "--zone", zone, "--quiet"];
case "instances-describe":
return ["compute", "instances", "describe", instanceName, "--project", project, "--zone", zone, "--format=json"];
case "instances-list-tagged":
return [
"compute", "instances", "list",
"--project", project,
`--filter=tags.items:${INSTANCE_TAG}`,
"--format=value(name)",
];
case "firewall-describe":
return ["compute", "firewall-rules", "describe", FIREWALL_RULE_NAME, "--project", project, "--format=json"];
case "firewall-create":
return [
"compute", "firewall-rules", "create", FIREWALL_RULE_NAME,
"--project", project,
"--direction=INGRESS",
"--action=ALLOW",
"--rules=tcp:22",
"--source-ranges=35.235.240.0/20",
`--target-tags=${INSTANCE_TAG}`,
];
case "firewall-delete":
return ["compute", "firewall-rules", "delete", FIREWALL_RULE_NAME, "--project", project, "--quiet"];
case "ssh-probe":
return [
"compute", "ssh", instanceName,
"--project", project, "--zone", zone,
"--tunnel-through-iap",
"--command", extras.command,
];
case "ssh-script":
// Stream a script body as base64 over --command so we don't fight
// shell-quoting through gcloud → ssh → remote bash. The script body
// is delivered as one argument; the wrapper feeds the base64 into
// `extras.command` and the caller sets `input` to the script body.
return [
"compute", "ssh", instanceName,
"--project", project, "--zone", zone,
"--tunnel-through-iap",
"--command", extras.command,
];
case "ssh-tunnel":
return [
"compute", "ssh", instanceName,
"--project", project, "--zone", zone,
"--tunnel-through-iap",
"--",
"-L", `${EDITOR_PORT}:127.0.0.1:${EDITOR_PORT}`,
"-L", `${VITE_PORT}:127.0.0.1:${VITE_PORT}`,
"-N",
];
default:
throw new Error(`Unknown gcloud verb: ${verb}`);
}
}
// =============================================================================
// Side-effecting helpers — exercised end-to-end by `npm run cloud:up` in
// real use. Unit tests pin their exports so a refactor cannot drop them.
// =============================================================================
function runGcloud(args, opts = {}) {
return spawnSync("gcloud", args, {
stdio: opts.stdio || "inherit",
input: opts.input,
encoding: opts.encoding || "utf8",
});
}
function ensureGcloud() {
const result = spawnSync("gcloud", ["version"], { stdio: "ignore" });
if (result.error || result.status !== 0) {
console.error(
"gcloud is not on PATH. Install: https://cloud.google.com/sdk/docs/install",
);
process.exit(1);
}
}
function gcloudGetValue(verb) {
const result = runGcloud(buildGcloudArgs(verb), { stdio: ["ignore", "pipe", "ignore"] });
if (result.error || result.status !== 0) return null;
const val = String(result.stdout || "").trim();
if (!val || val === "(unset)") return null;
return val;
}
/** Run `gcloud compute instances describe ... --format=json` and return the
* parsed `.status` field (e.g. "RUNNING", "TERMINATED", "PROVISIONING"), or
* null when the instance is absent or describe returned unparseable output.
* Centralizes the describe-and-parse pattern shared by ensureInstance (used
* twice — initial probe + poll), cmdTunnel, and cmdStatus. */
function fetchInstanceState(cfg) {
const result = runGcloud(buildGcloudArgs("instances-describe", cfg), {
stdio: ["ignore", "pipe", "ignore"],
});
if (result.status !== 0) return null;
try {
const parsed = JSON.parse(result.stdout);
return parsed && typeof parsed.status === "string" ? parsed.status : null;
} catch {
return null;
}
}
function loadOrInitCloudConfig(projectDir, flags) {
const existing = loadCloudConfig(projectDir);
if (existing) {
const merged = { ...existing, ...flags };
if (JSON.stringify(merged) !== JSON.stringify(existing)) {
saveCloudConfig(projectDir, merged);
}
return merged;
}
const cfg = { ...flags };
cfg.project = cfg.project || gcloudGetValue("config-get-project");
cfg.zone = cfg.zone || gcloudGetValue("config-get-zone");
cfg.agent = cfg.agent || "claude";
agentEnvVarName(cfg.agent);
if (!cfg.repo) {
throw new Error(
"--repo <url> is required on first run. Subsequent runs read .codeyam/cloud.json.",
);
}
if (!cfg.project) {
throw new Error(
"--project not provided and `gcloud config get-value project` returned no default.\n" +
"Run `gcloud config set project <id>` or pass --project <id>.",
);
}
if (!cfg.zone) {
throw new Error(
"--zone not provided and `gcloud config get-value compute/zone` returned no default.\n" +
"Run `gcloud config set compute/zone <zone>` or pass --zone <zone>.",
);
}
cfg.instanceName = cfg.instanceName || deriveInstanceName(cfg.repo);
saveCloudConfig(projectDir, cfg);
return cfg;
}
async function ensureFirewallRule(cfg) {
console.error(`Checking firewall rule ${FIREWALL_RULE_NAME}...`);
const describe = runGcloud(buildGcloudArgs("firewall-describe", cfg), {
stdio: ["ignore", "pipe", "pipe"],
});
if (describe.status === 0) {
console.error(` ✓ ${FIREWALL_RULE_NAME} already exists`);
return;
}
console.error(` creating ${FIREWALL_RULE_NAME}...`);
const create = runGcloud(buildGcloudArgs("firewall-create", cfg));
if (create.status !== 0) {
throw new Error(`Failed to create firewall rule ${FIREWALL_RULE_NAME}`);
}
}
async function ensureInstance(cfg) {
console.error(`Checking instance ${cfg.instanceName}...`);
const initialState = fetchInstanceState(cfg);
if (initialState === "RUNNING") {
console.error(` ✓ ${cfg.instanceName} is RUNNING`);
return;
}
if (initialState === "TERMINATED") {
console.error(` ${cfg.instanceName} is TERMINATED — starting...`);
const start = runGcloud(buildGcloudArgs("instances-start", cfg));
if (start.status !== 0) {
throw new Error(`Failed to start instance ${cfg.instanceName}`);
}
} else if (initialState === null) {
console.error(` ${cfg.instanceName} not found — creating...`);
const create = runGcloud(buildGcloudArgs("instances-create", cfg));
if (create.status !== 0) {
throw new Error(`Failed to create instance ${cfg.instanceName}`);
}
} else {
console.error(` ${cfg.instanceName} is ${initialState}; waiting...`);
}
for (let i = 0; i < 60; i++) {
if (fetchInstanceState(cfg) === "RUNNING") {
console.error(` ✓ ${cfg.instanceName} is RUNNING`);
await waitForIapReady(cfg);
return;
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Instance ${cfg.instanceName} did not reach RUNNING within 2 minutes`);
}
/** GCE reports the instance as RUNNING the moment the boot disk is attached,
* but the IAP control plane has its own catalog that updates a few seconds
* later. The first `gcloud compute ssh --tunnel-through-iap` after a fresh
* create races that catalog and fails with `[4047: 'Failed to lookup
* instance']`. Probe an IAP-tunneled SSH that just runs `true` until it
* succeeds — cheap on the warm path (one ~1s round-trip) and bounded so a
* genuinely unreachable VM fails loud rather than silently. */
async function waitForIapReady(cfg) {
console.error(` waiting for IAP to register ${cfg.instanceName}...`);
const deadline = Date.now() + 60_000;
let attempts = 0;
let lastErr = "";
while (Date.now() < deadline) {
attempts++;
const probe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, { command: "true" }),
{ stdio: ["ignore", "pipe", "pipe"] },
);
if (probe.status === 0) {
console.error(` ✓ IAP ready (${attempts} ${attempts === 1 ? "probe" : "probes"})`);
return;
}
lastErr = String(probe.stderr || "").trim().split("\n").slice(-1)[0] || "";
// Exponential-ish backoff: 2s, 3s, 5s, 8s, then cap at 8s.
const delay = Math.min(2000 * Math.pow(1.5, attempts - 1), 8000);
await new Promise((r) => setTimeout(r, delay));
}
throw new Error(
`IAP did not become ready for ${cfg.instanceName} within 60s ` +
`(${attempts} probes; last error: ${lastErr || "unknown"})`,
);
}
function sshScriptCommand(scriptBody, scriptArgs = [], envVars = {}) {
const b64 = Buffer.from(scriptBody, "utf8").toString("base64");
const argsString = scriptArgs.map((a) => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ");
// Prefix each env var as `KEY='value'` so they're set for the bash -s
// process only — no persistence in the VM's shell history, no /etc/environment
// edit. Single-quoted values are safe against shell metacharacters; an
// embedded apostrophe in the value would be exotic for these (token,
// bool flag), but escape it the same way as scriptArgs for symmetry.
const envString = Object.entries(envVars)
.filter(([, v]) => v !== undefined && v !== null && v !== "")
.map(([k, v]) => `${k}='${String(v).replace(/'/g, "'\\''")}'`)
.join(" ");
const envPrefix = envString ? `${envString} ` : "";
return `echo '${b64}' | base64 -d | ${envPrefix}bash -s -- ${argsString}`.trim();
}
async function ensureBootstrapped(cfg) {
console.error("Checking VM bootstrap state...");
const probe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: "command -v docker >/dev/null && docker image inspect codeyam-editor:local >/dev/null 2>&1",
}),
{ stdio: ["ignore", "pipe", "pipe"] },
);
if (probe.status === 0) {
console.error(" ✓ VM already bootstrapped (docker + codeyam-editor:local present)");
return;
}
console.error(" streaming scripts/gcp-bootstrap.sh to VM (this may take ~10 minutes)...");
const scriptPath = path.join(rootDir, "scripts", "gcp-bootstrap.sh");
const scriptBody = fs.readFileSync(scriptPath, "utf8");
// GITHUB_TOKEN is passed through to the bootstrap script so it can clone
// a private editor repo (the bootstrap clones $REPO_URL to build the
// codeyam-editor:local image). Without this the git clone hits the
// "could not read Username for 'https://github.com'" wall whenever the
// repo isn't public — codeyam-editor itself today.
//
// --branch is mandatory when the user is testing wrapper / Dockerfile
// patches from a feature branch: without it, the bootstrap clones the
// repo's default branch and the image build uses a stale Dockerfile,
// even though the user invoked cloud:up with --branch.
const bootstrapArgs = ["--agent", cfg.agent, "--repo", cfg.repo];
if (cfg.branch) {
bootstrapArgs.push("--branch", cfg.branch);
}
const command = sshScriptCommand(scriptBody, bootstrapArgs, {
GITHUB_TOKEN: resolveGithubToken(),
});
const result = runGcloud(
buildGcloudArgs("ssh-script", cfg, { command }),
{ stdio: "inherit" },
);
if (result.status !== 0) {
throw new Error("scripts/gcp-bootstrap.sh failed on the VM");
}
}
async function ensureProjectBootstrapped(cfg) {
const repoName = repoBasename(cfg.repo);
console.error(`Checking project bootstrap state for ${repoName}...`);
// Probe both .codeyam/ (created by `editor init`) AND
// docker-compose.cloud.yml (written near the end of the script). A previous
// run that crashed between init and the compose write would leave the
// first marker but not the second; the old probe wrongly considered the
// project bootstrapped and the next `cloud:up` then hit
// "open docker-compose.cloud.yml: no such file or directory".
// The probe is the seam where idempotency lives — make sure it gates on
// the LAST artifact the script produces, not the first.
const probe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command:
`test -d ~/projects/${repoName}/.codeyam ` +
`&& test -f ~/projects/${repoName}/docker-compose.cloud.yml`,
}),
{ stdio: ["ignore", "pipe", "pipe"] },
);
if (probe.status === 0) {
console.error(` ✓ ${repoName} already bootstrapped on VM`);
return;
}
console.error(` streaming scripts/gcp-project-bootstrap.sh to VM for ${cfg.repo}...`);
const scriptPath = path.join(rootDir, "scripts", "gcp-project-bootstrap.sh");
const scriptBody = fs.readFileSync(scriptPath, "utf8");
const scriptArgs = [cfg.repo];
if (cfg.branch) {
scriptArgs.push("--branch", cfg.branch);
}
const command = sshScriptCommand(scriptBody, scriptArgs, {
GITHUB_TOKEN: resolveGithubToken(),
});
const result = runGcloud(
buildGcloudArgs("ssh-script", cfg, { command }),
{ stdio: "inherit" },
);
if (result.status !== 0) {
throw new Error("scripts/gcp-project-bootstrap.sh failed on the VM");
}
}
async function ensureEditorContainerUp(cfg) {
const repoName = repoBasename(cfg.repo);
// -f docker-compose.cloud.yml everywhere: the editor repo's own
// docker-compose.yml does `build: .` from source and would shadow the
// cloud variant when self-hosting (see gcp-project-bootstrap.sh).
const composeCmd = `docker compose -f docker-compose.cloud.yml`;
console.error("Checking editor container state...");
const probe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: `cd ~/projects/${repoName} && ${composeCmd} ps --format json 2>/dev/null || true`,
}),
{ stdio: ["ignore", "pipe", "ignore"] },
);
if (probe.status === 0 && parseEditorComposeState(probe.stdout) === "running") {
console.error(" ✓ editor container is already running");
return;
}
console.error(" starting editor container (docker compose up -d)...");
const up = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: `cd ~/projects/${repoName} && ${composeCmd} up -d`,
}),
{ stdio: "inherit" },
);
if (up.status !== 0) {
throw new Error("docker compose up -d failed on the VM");
}
}
/** Look up a GitHub token from local environment, falling back to `gh auth
* token`. Returns empty string if neither is available — callers should
* treat empty as "no auth" and either skip or print a hint. Memoizing
* intentionally not done: the cost of one `gh` subprocess per cloud:up is
* trivial and lets a user `gh auth login` between operations. */
function resolveGithubToken() {
const env = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (env) return env;
const ghOut = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
if (ghOut.status === 0) {
return ghOut.stdout.trim();
}
return "";
}
/** Write the laptop's GitHub token into ~/.codeyam-cloud-git-credentials on
* the VM so the cloud compose can mount it into /root/.git-credentials —
* without this, claude / codex / gemini inside the container can clone the
* initial repo (the wrapper does that step out-of-band with a one-shot
* inlined URL) but cannot `git pull` / `git push` afterward. The file is
* chmod 600 and never committed; if the local environment has no token,
* the placeholder file from gcp-project-bootstrap.sh stays in place. */
async function seedGithubCredentials(cfg) {
const token = resolveGithubToken();
if (!token) {
console.error(
"\nNo GITHUB_TOKEN / GH_TOKEN set and `gh auth token` is unavailable —\n" +
"claude inside the cloud container will not be able to git pull/push.\n" +
"Export GITHUB_TOKEN or run `gh auth login` locally, then `cloud:up` again.\n",
);
return;
}
console.error("Writing GitHub token into ~/.codeyam-cloud-git-credentials on the VM...");
const credLine = `https://x-access-token:${token}@github.com`;
// Quote the token through one shell layer (gcloud ssh --command); the
// value is base64'd to keep it out of the shell history.
const b64 = Buffer.from(`${credLine}\n`).toString("base64");
const remoteCmd =
`f=$HOME/.codeyam-cloud-git-credentials && ` +
`echo ${b64} | base64 -d > $f && chmod 600 $f && echo OK`;
const result = runGcloud(
buildGcloudArgs("ssh-probe", cfg, { command: remoteCmd }),
{ stdio: ["ignore", "pipe", "inherit"] },
);
if (result.status !== 0) {
throw new Error("Failed to write ~/.codeyam-cloud-git-credentials on the VM");
}
}
async function seedAgentApiKey(cfg) {
const patch = buildEditorLocalEnvPatch(process.env, cfg.agent);
if (!patch) {
console.error(
`\nNo ${agentEnvVarName(cfg.agent)} set locally — skipping agent auth seed.\n` +
`Authenticate ${cfg.agent} later with:\n` +
` gcloud compute ssh ${cfg.instanceName} --project ${cfg.project} --zone ${cfg.zone} ` +
`--tunnel-through-iap -- ${cfg.agent} auth login\n`,
);
return;
}
const repoName = repoBasename(cfg.repo);
const remotePath = `~/projects/${repoName}/.codeyam/editor.local.json`;
console.error(`Seeding ${agentEnvVarName(cfg.agent)} into ${remotePath}...`);
const read = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: `cat ${remotePath} 2>/dev/null || echo '{}'`,
}),
{ stdio: ["ignore", "pipe", "ignore"] },
);
let current = {};
if (read.status === 0) {
try {
const parsed = JSON.parse(read.stdout);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
current = parsed;
}
} catch {
// Treat unparseable remote content as `{}` — overwriting a corrupt
// local override file with a known-good one is the safer move than
// refusing to seed at all.
}
}
const merged = deepMergeJson(current, patch);
const mergedJson = JSON.stringify(merged, null, 2);
const b64 = Buffer.from(mergedJson, "utf8").toString("base64");
const write = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: `mkdir -p ~/projects/${repoName}/.codeyam && echo '${b64}' | base64 -d > ${remotePath}`,
}),
{ stdio: ["ignore", "inherit", "inherit"] },
);
if (write.status !== 0) {
throw new Error(`Failed to write ${remotePath} on VM`);
}
}
async function openTunnel(cfg) {
console.error(`Opening IAP tunnel to ${cfg.instanceName}...`);
const child = spawn("gcloud", buildGcloudArgs("ssh-tunnel", cfg), { stdio: "inherit" });
const ready14199 = await waitForPort(EDITOR_PORT, 30000, 500);
const ready5173 = await waitForPort(VITE_PORT, 30000, 500);
if (!ready14199 || !ready5173) {
console.error(
`Warning: tunnel ports not reachable on localhost (${EDITOR_PORT}=${ready14199}, ${VITE_PORT}=${ready5173}).`,
);
}
console.error(` Editor UI: http://localhost:${EDITOR_PORT}`);
console.error(` Live Preview: http://localhost:${VITE_PORT}`);
console.error(" (Ctrl+C to tear down the tunnel)");
return child;
}
function attachKeepAliveAndTeardown(child) {
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => {
clearInterval(keepAlive);
killChildProcess(child);
process.exit(0);
});
}
function promptForInstanceName(expected) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stderr,
});
rl.question(
`Type the instance name '${expected}' to confirm destroy: `,
(ans) => {
rl.close();
resolve(ans.trim() === expected);
},
);
});
}
// =============================================================================
// Subcommand handlers
// =============================================================================
async function cmdUp(args) {
ensureGcloud();
const flags = parseFlags(args);
const cfg = loadOrInitCloudConfig(rootDir, flags);
await ensureFirewallRule(cfg);
await ensureInstance(cfg);
await ensureBootstrapped(cfg);
await ensureProjectBootstrapped(cfg);
// Write the GitHub token BEFORE compose up so the credentials file exists
// when the bind-mount happens — bind-mounting a missing file would silently
// create a directory instead and the agent's first `git pull` would fail.
await seedGithubCredentials(cfg);
await ensureEditorContainerUp(cfg);
await seedAgentApiKey(cfg);
const tunnelChild = await openTunnel(cfg);
open(`http://localhost:${EDITOR_PORT}`);
attachKeepAliveAndTeardown(tunnelChild);
}
async function cmdTunnel(args) {
ensureGcloud();
const flags = parseFlags(args);
const existing = loadCloudConfig(rootDir);
if (!existing) {
throw new Error(
"No .codeyam/cloud.json found. Run `npm run cloud:up -- --repo <url>` first.",
);
}
const cfg = { ...existing, ...flags };
const state = fetchInstanceState(cfg);
if (state === null) {
throw new Error(
`Instance ${cfg.instanceName} not found in project ${cfg.project} / zone ${cfg.zone}. ` +
"Has it been deleted? Try `npm run cloud:up`.",
);
}
if (state !== "RUNNING") {
throw new Error(
`Instance ${cfg.instanceName} is ${state}; run \`npm run cloud:up\` to start it.`,
);
}
const tunnelChild = await openTunnel(cfg);
open(`http://localhost:${EDITOR_PORT}`);
attachKeepAliveAndTeardown(tunnelChild);
}
async function cmdStop(args) {
ensureGcloud();
parseFlags(args);
const cfg = loadCloudConfig(rootDir);
if (!cfg) {
throw new Error("No .codeyam/cloud.json found. Nothing to stop.");
}
console.error(`Stopping ${cfg.instanceName}...`);
const result = runGcloud(buildGcloudArgs("instances-stop", cfg));
if (result.status !== 0) {
throw new Error(`Failed to stop instance ${cfg.instanceName}`);
}
console.error(
`\n ✓ ${cfg.instanceName} is stopped.\n` +
" Persistent-disk billing continues (~$10/month for 100 GB pd-balanced).\n" +
" Resume with `npm run cloud:up`.\n",
);
}
async function cmdDestroy(args) {
ensureGcloud();
parseFlags(args);
const cfg = loadCloudConfig(rootDir);
if (!cfg) {
throw new Error("No .codeyam/cloud.json found. Nothing to destroy.");
}
const confirmed = await promptForInstanceName(cfg.instanceName);
if (!confirmed) {
console.error("Aborted.");
return;
}
console.error(`Deleting instance ${cfg.instanceName}...`);
const del = runGcloud(buildGcloudArgs("instances-delete", cfg));
if (del.status !== 0) {
throw new Error(`Failed to delete instance ${cfg.instanceName}`);
}
const list = runGcloud(buildGcloudArgs("instances-list-tagged", cfg), {
stdio: ["ignore", "pipe", "ignore"],
});
const others = String(list.stdout || "").trim().split("\n").filter(Boolean);
if (others.length === 0) {
console.error(`No other ${INSTANCE_TAG}-tagged instances remain — deleting firewall rule...`);
const fwDel = runGcloud(buildGcloudArgs("firewall-delete", cfg));
if (fwDel.status !== 0) {
console.error(` Warning: failed to delete firewall rule ${FIREWALL_RULE_NAME}.`);
}
} else {
console.error(
`Leaving ${FIREWALL_RULE_NAME} in place (${others.length} other ${INSTANCE_TAG}-tagged instance(s) still use it).`,
);
}
try {
fs.unlinkSync(cloudConfigPath(rootDir));
console.error("Deleted .codeyam/cloud.json");
} catch (e) {
if (e.code !== "ENOENT") throw e;
}
}
async function cmdStatus(args) {
ensureGcloud();
parseFlags(args);
const cfg = loadCloudConfig(rootDir);
if (!cfg) {
console.error("No .codeyam/cloud.json found. Run `npm run cloud:up` first.");
return;
}
const vmState = fetchInstanceState(cfg) || "NOT_FOUND";
console.error(`VM: ${cfg.instanceName} (${cfg.zone}) — ${vmState}`);
if (vmState !== "RUNNING") {
console.error(" (image / container / tunnel checks skipped — VM is not RUNNING)");
return;
}
const imgProbe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command:
"docker image inspect codeyam-editor:local >/dev/null 2>&1 && echo built || echo missing",
}),
{ stdio: ["ignore", "pipe", "ignore"] },
);
const imgStatus = String(imgProbe.stdout || "").trim() || "?";
console.error(` image: ${imgStatus}`);
const repoName = repoBasename(cfg.repo);
const containerProbe = runGcloud(
buildGcloudArgs("ssh-probe", cfg, {
command: `cd ~/projects/${repoName} 2>/dev/null && docker compose -f docker-compose.cloud.yml ps --format json 2>/dev/null || echo`,
}),
{ stdio: ["ignore", "pipe", "ignore"] },
);
const containerStatus = parseEditorComposeState(containerProbe.stdout) || "missing";
console.error(` editor container: ${containerStatus}`);
const p14199 = await isPortInUse(EDITOR_PORT);
const p5173 = await isPortInUse(VITE_PORT);
console.error(
` tunnel: ${EDITOR_PORT} ${p14199 ? "✓" : "✗"} ${VITE_PORT} ${p5173 ? "✓" : "✗"}`,
);
}
const HANDLERS = {
up: cmdUp,
tunnel: cmdTunnel,
stop: cmdStop,
destroy: cmdDestroy,
status: cmdStatus,
};
async function cloudMain() {
const subcmd = process.argv[2];
const args = process.argv.slice(3);
if (!subcmd || !Object.prototype.hasOwnProperty.call(HANDLERS, subcmd)) {
console.error(
`Usage: npm run cloud:<${Object.keys(HANDLERS).join("|")}> [-- flags]`,
);
process.exit(2);
}
try {
await HANDLERS[subcmd](args);
} catch (e) {
console.error(`Error: ${e.message}`);
process.exit(1);
}
}
module.exports = {
// pure helpers
agentEnvVarName,
AGENT_ENV_VARS,
KNOWN_FLAGS,
FIREWALL_RULE_NAME,
INSTANCE_TAG,
EDITOR_PORT,
VITE_PORT,
parseFlags,
repoBasename,
deriveInstanceName,
cloudConfigPath,
loadCloudConfig,
saveCloudConfig,
buildEditorLocalEnvPatch,
buildGcloudArgs,
sshScriptCommand,
parseEditorComposeState,
fetchInstanceState,
// side-effecting handlers
cmdUp,
cmdTunnel,
cmdStop,
cmdDestroy,
cmdStatus,
cloudMain,
seedGithubCredentials,
};
if (require.main === module) {
cloudMain();
}
const { createIssue } = require("./scenario-issues");
function getInitScript() {
return `
window.__codeyamUnhandledRejections = [];
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
const message =
reason instanceof Error ? reason.message : String(reason);
window.__codeyamUnhandledRejections.push(message);
});
// Stub WebSocket during capture to prevent terminal reconnection spam.
window.WebSocket = class StubWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
readyState = 3;
onopen = null;
onclose = null;
onerror = null;
onmessage = null;
send() {}
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() { return false; }
constructor() {
setTimeout(() => {
if (this.onerror) this.onerror(new Event("error"));
if (this.onclose) this.onclose(new CloseEvent("close"));
}, 0);
}
};
`;
}
function handleConsoleMessage(message) {
if (message.type() !== "error") return null;
const text = message.text();
// Ignore known dev-server WebSocket/HMR errors from Vite proxy
if (
text.includes("WebSocket connection to") ||
text.includes("Unsupported Media Type")
) {
return null;
}
return createIssue("console", text);
}
function handlePageError(error) {
return createIssue("pageerror", error.message || String(error));
}
function handleRequestFailed(request) {
const errorText = request.failure()?.errorText || "Request failed";
// Filter benign request cancellations. `net::ERR_ABORTED` is what Playwright
// emits when a request is in-flight at the moment the page is closed (or the
// iframe is destroyed). For scenarios whose pages fetch large payloads (the
// editor's own EditorShell mounts a 2.2MB `/api/tests` fetch), the
// browser.close() at the end of capture races the fetch and produces this
// event AFTER the screenshot has already been taken — there is no real
// failure to surface. Genuine network failures arrive under different
// codes (net::ERR_CONNECTION_REFUSED, net::ERR_NAME_NOT_RESOLVED, etc.)
// and continue to be reported.
if (errorText.includes("net::ERR_ABORTED")) {
return null;
}
return createIssue("requestfailed", errorText, { url: request.url() });
}
module.exports = {
getInitScript,
handleConsoleMessage,
handlePageError,
handleRequestFailed,
};
function createIssue(kind, message, extra = {}) {
const issue = {
kind,
message,
url: extra.url ?? null,
status: extra.status ?? null,
};
if (extra.matchedPattern != null) issue.matchedPattern = extra.matchedPattern;
if (extra.contextSnippet != null) issue.contextSnippet = extra.contextSnippet;
return issue;
}
function pushIssue(issues, issue) {
const key = JSON.stringify(issue);
if (!issues.some((existing) => JSON.stringify(existing) === key)) {
issues.push(issue);
}
}
function buildResult({ loaded, hasContent, issues, outputPath, url }) {
return {
ok: loaded && hasContent && issues.length === 0,
loaded,
hasContent,
url,
outputPath: outputPath ?? null,
issues,
};
}
module.exports = {
createIssue,
pushIssue,
buildResult,
};
const LOADING_MARKERS = [
"Loading scenario...",
"Loading tests...",
"Loading scenarios...",
"disconnected",
];
function hasLoadingMarkers(text) {
return LOADING_MARKERS.some((marker) => text.includes(marker));
}
function hasRenderableContent(state) {
if (!state) return false;
if (
state.rootChildCount > 0 ||
state.rootTextLength > 0 ||
state.bodyTextLength > 0
) {
return true;
}
if ((state.loadedImageCount || 0) > 0) return true;
if ((state.mediaBboxCount || 0) > 0) return true;
return false;
}
function describeBlankReason(state) {
if (!state) return "no content state collected";
const parts = [];
if (!(state.bodyTextLength > 0)) {
parts.push("no text");
}
const imageCount = state.imageCount || 0;
const loadedImageCount = state.loadedImageCount || 0;
if (imageCount > 0 && loadedImageCount === 0) {
parts.push(`${imageCount} unloaded image${imageCount === 1 ? "" : "s"}`);
} else if (imageCount === 0) {
parts.push("no images");
}
if (!((state.mediaBboxCount || 0) > 0)) {
parts.push("no svg/canvas/video");
}
return parts.join(", ");
}
function shouldStopWaitingForImages(images, options = {}) {
const { elapsedMs = 0, overallTimeoutMs = 5000 } = options;
if (!Array.isArray(images) || images.length === 0) return true;
if (elapsedMs >= overallTimeoutMs) return true;
return images.every((img) => img && img.complete === true);
}
const ERROR_PATTERNS = [
"not found in registry",
"Component not found",
"Scenario Error",
];
function hasErrorPatterns(text) {
return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
}
function findErrorPattern(text) {
if (!text) return null;
for (const pattern of ERROR_PATTERNS) {
if (text.includes(pattern)) return pattern;
}
return null;
}
const ERROR_CONTEXT_RADIUS = 60;
function buildErrorContextSnippet(text, pattern) {
if (!text || !pattern) return null;
const index = text.indexOf(pattern);
if (index < 0) return null;
const start = Math.max(0, index - ERROR_CONTEXT_RADIUS);
const end = Math.min(text.length, index + pattern.length + ERROR_CONTEXT_RADIUS);
const slice = text.slice(start, end).replace(/\s+/g, " ").trim();
const prefix = start > 0 ? "…" : "";
const suffix = end < text.length ? "…" : "";
return `${prefix}${slice}${suffix}`;
}
module.exports = {
hasLoadingMarkers,
hasRenderableContent,
describeBlankReason,
shouldStopWaitingForImages,
hasErrorPatterns,
findErrorPattern,
buildErrorContextSnippet,
ERROR_PATTERNS,
ERROR_CONTEXT_RADIUS,
};
function normalizeMockCandidates(url) {
try {
const parsed = new URL(url);
return [url, `${parsed.pathname}${parsed.search}`, parsed.pathname];
} catch {
return [url];
}
}
function findHttpMock(httpMocks, request) {
const method = request.method().toUpperCase();
const candidates = normalizeMockCandidates(request.url());
for (const candidate of candidates) {
const key = `${method} ${candidate}`;
if (httpMocks[key]) {
return httpMocks[key];
}
}
return null;
}
async function attachHttpMocks(page, httpMocks) {
if (!httpMocks || Object.keys(httpMocks).length === 0) return;
await page.route("**/*", async (route) => {
const mock = findHttpMock(httpMocks, route.request());
if (!mock) {
await route.continue();
return;
}
const headers = { ...(mock.headers || {}) };
let body;
if (mock.body !== undefined) {
body =
typeof mock.body === "string" ? mock.body : JSON.stringify(mock.body);
const hasContentType = Object.keys(headers).some(
(key) => key.toLowerCase() === "content-type",
);
if (!hasContentType) {
headers["content-type"] = "application/json";
}
}
await route.fulfill({
status: mock.status || 200,
headers,
body,
});
});
// Disable the in-page fetch mock by returning an empty active-mocks.json.
// The HTML injects a script that synchronously loads this file and
// monkey-patches window.fetch, which would bypass Playwright's route
// interception. This route is registered AFTER **/* so it takes priority
// (Playwright uses LIFO ordering for route handlers).
await page.route("**/active-mocks.json", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: "[]",
});
});
}
module.exports = {
normalizeMockCandidates,
findHttpMock,
attachHttpMocks,
};
const {
hasLoadingMarkers,
shouldStopWaitingForImages,
} = require("./scenario-metrics");
function escapeHtmlAttribute(value) {
return String(value).replaceAll("&", "&amp;").replaceAll('"', "&quot;");
}
// The harness background defaults to transparent so the iframe's own
// <body> background paints through — matching what users see in the Live
// Preview. Callers (via scenario-check.js) pass a concrete color when the
// UI has detected a background it wants the capture to paint behind the
// iframe, e.g. `var(--bg-deep)` from the editor shell.
function buildIframeHarness(url, { background = "transparent" } = {}) {
const escapedUrl = escapeHtmlAttribute(url);
const bg = String(background);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: ${bg};
}
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
background: ${bg};
}
</style>
</head>
<body>
<iframe id="scenario-frame" title="Scenario Preview" src="${escapedUrl}"></iframe>
</body>
</html>`;
}
async function collectContentState(target) {
return target.evaluate(() => {
const root = document.getElementById("root");
const imgs = Array.from(document.images || []);
const loadedImageCount = imgs.filter(
(img) => img.complete && img.naturalWidth > 0,
).length;
const mediaSelectors = ["svg", "canvas", "video"];
let mediaBboxCount = 0;
for (const selector of mediaSelectors) {
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
const rect = node.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
mediaBboxCount += 1;
}
}
}
return {
bodyTextLength: document.body.innerText.trim().length,
rootChildCount: root ? root.childElementCount : 0,
rootTextLength: root ? (root.textContent || "").trim().length : 0,
imageCount: imgs.length,
loadedImageCount,
mediaBboxCount,
};
});
}
async function collectImageStates(target) {
return target.evaluate(() =>
Array.from(document.images || []).map((img) => ({
complete: img.complete,
naturalWidth: img.naturalWidth,
src: img.currentSrc || img.src || "",
})),
);
}
async function waitForImagesSettled(
target,
{ overallTimeoutMs = 5000, pollIntervalMs = 100 } = {},
) {
const started = Date.now();
let images = await collectImageStates(target);
while (
!shouldStopWaitingForImages(images, {
elapsedMs: Date.now() - started,
overallTimeoutMs,
})
) {
await new Promise((r) => setTimeout(r, pollIntervalMs));
images = await collectImageStates(target);
}
const elapsedMs = Date.now() - started;
const allComplete = images.every((img) => img && img.complete === true);
return { settled: allComplete, images, elapsedMs };
}
async function waitForAnimationsSettled(
target,
{ timeoutMs = 2000, pollIntervalMs = 100 } = {},
) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const runningCount = await target.evaluate(() =>
document
.getAnimations()
.filter((a) => a.playState === "running").length,
);
if (runningCount === 0) {
return { settled: true, elapsedMs: Date.now() - started };
}
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
return { settled: false, elapsedMs: Date.now() - started };
}
async function waitForStablePage(page, target, timeoutMs = 10000) {
const started = Date.now();
let lastHtml = "";
let stableCount = 0;
while (Date.now() - started < timeoutMs) {
await page.waitForTimeout(500);
const pageState = await target.evaluate(() => ({
bodyText: document.body.innerText,
html: document.body.innerHTML,
}));
if (!hasLoadingMarkers(pageState.bodyText) && pageState.html === lastHtml) {
stableCount += 1;
if (stableCount >= 2) {
const remaining = () => Math.max(0, timeoutMs - (Date.now() - started));
await waitForAnimationsSettled(target, {
timeoutMs: Math.min(2000, remaining()),
});
await waitForImagesSettled(target, { overallTimeoutMs: remaining() });
return;
}
} else {
stableCount = 0;
}
lastHtml = pageState.html;
}
}
async function loadScenarioInIframe(page, url, { background } = {}) {
const responsePromise = page
.waitForResponse(
(response) =>
response.request().resourceType() === "document" &&
response.url() === url,
{ timeout: 30000 },
)
.catch(() => null);
await page.setContent(buildIframeHarness(url, { background }), {
waitUntil: "domcontentloaded",
});
const frameHandle = await page.waitForSelector("#scenario-frame", {
state: "attached",
timeout: 30000,
});
const frame = await frameHandle.contentFrame();
if (!frame) {
throw new Error("Scenario iframe did not attach");
}
await frame.waitForLoadState("load", { timeout: 30000 });
const response = await responsePromise;
return { frame, response };
}
module.exports = {
escapeHtmlAttribute,
buildIframeHarness,
collectContentState,
collectImageStates,
waitForImagesSettled,
waitForAnimationsSettled,
waitForStablePage,
loadScenarioInIframe,
};
import{j as e}from"./markdown-v0F0UEt3.js";import{b as a}from"./react-CSS0HapR.js";import{u as T}from"./useEvents-DggWrtHe.js";function M({connected:t}){return e.jsx("span",{className:`inline-block rounded-full px-2 py-0.5 text-xs font-medium text-[var(--text-primary)] ${t?"bg-[var(--accent-green)]":"bg-[var(--accent-red)]"}`,children:t?"connected":"reconnecting…"})}function P(){const[t,s]=a.useState(null),[n,r]=a.useState(!0),{events:l}=T(),o=a.useCallback(async()=>{try{const c=await(await fetch("/api/inspector/status")).json();s(c)}catch{}finally{r(!1)}},[]);return a.useEffect(()=>{o()},[o]),a.useEffect(()=>{const i=l[l.length-1];i&&i.type==="scenario_switch"&&o()},[l,o]),{scenario:(t==null?void 0:t.scenario)??null,previewScenario:(t==null?void 0:t.previewScenario)??null,routes:(t==null?void 0:t.routes)??[],routeCount:(t==null?void 0:t.routeCount)??0,loading:n,refetch:o}}function k({title:t}){return e.jsx("h2",{style:{fontSize:14,fontWeight:600,color:"#f0f6fc",margin:"0 0 8px 0",borderBottom:"1px solid #30363d",paddingBottom:8},children:t})}function f({color:t,children:s}){return e.jsx("span",{style:{background:`${t}22`,color:t,border:`1px solid ${t}55`,borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,letterSpacing:"0.5px",whiteSpace:"nowrap"},children:s})}const z={GET:"#58a6ff",POST:"#238636",PUT:"#d29922",DELETE:"#da3633",PATCH:"#bc8cff"};function L({method:t}){const s=z[t.toUpperCase()]??"#8b949e";return e.jsx("span",{style:{color:s,fontWeight:600,fontSize:11,minWidth:48,display:"inline-block"},children:t.toUpperCase()})}function E({color:t,children:s}){return e.jsx("div",{style:{padding:"12px 16px",borderRadius:6,border:`1px solid ${t}44`,background:`${t}11`,color:t},children:s})}const h={padding:"8px 12px",textAlign:"center",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},u={padding:"6px 12px"},w={background:"#161b22",border:"1px solid #30363d",borderRadius:6,color:"#c9d1d9",padding:"6px 12px",fontSize:13,fontFamily:"inherit"},B={...w,appearance:"none",paddingRight:24,backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b949e' d='M6 8L1 3h10z'/%3E%3C/svg%3E")`,backgroundRepeat:"no-repeat",backgroundPosition:"right 8px center"};function O({routes:t}){return e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d"},children:[e.jsx("th",{style:h,children:"Method"}),e.jsx("th",{style:{...h,textAlign:"left"},children:"Pattern"}),e.jsx("th",{style:h,children:"Status"})]})}),e.jsx("tbody",{children:t.map((s,n)=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{...u,textAlign:"center"},children:e.jsx(L,{method:s.method})}),e.jsx("td",{style:u,children:s.pattern}),e.jsx("td",{style:{...u,textAlign:"center",color:"#8b949e"},children:s.status})]},n))})]})}function I(){const{scenario:t,previewScenario:s,routes:n,routeCount:r,loading:l}=P();return e.jsxs("section",{children:[e.jsx(k,{title:"Active Scenario"}),l?e.jsx(E,{color:"#8b949e",children:"Loading..."}):t?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#238636",children:"ACTIVE"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:t.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",t.slug,")"]})]}),e.jsxs("div",{style:{color:"#8b949e",fontSize:12},children:[r," mock route",r!==1?"s":""," loaded"]}),n.length>0&&e.jsx(O,{routes:n})]}):s?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#58a6ff",children:"PREVIEW"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:s.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",s.slug,")"]})]}),e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"Component preview — no HTTP mocks active"})]}):e.jsx(E,{color:"#d29922",children:"No scenario active — all requests pass through to upstream"})]})}function A(t){try{return new URL(t).pathname}catch{return t}}function H({textUrlFilter:t,onTextUrlFilterChange:s,methodFilter:n,onMethodFilterChange:r,sourceFilter:l,onSourceFilterChange:o}){return e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12,alignItems:"center"},children:[e.jsx("input",{type:"text",placeholder:"Filter by URL...",value:t,onChange:i=>s(i.target.value),style:{...w,flex:1,minWidth:0}}),e.jsxs("select",{value:n,onChange:i=>r(i.target.value),style:B,children:[e.jsx("option",{value:"ALL",children:"All Methods"}),e.jsx("option",{value:"GET",children:"GET"}),e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"}),e.jsx("option",{value:"DELETE",children:"DELETE"}),e.jsx("option",{value:"PATCH",children:"PATCH"})]}),e.jsxs("select",{value:l,onChange:i=>o(i.target.value),style:B,children:[e.jsx("option",{value:"ALL",children:"All Sources"}),e.jsx("option",{value:"MOCKED",children:"Mocked"}),e.jsx("option",{value:"PASSTHROUGH",children:"Passthrough"})]})]})}const U={color:"#8b949e",fontSize:11,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:4},R={background:"#161b22",border:"1px solid #30363d",borderRadius:4,padding:"8px 12px",margin:0,fontSize:11,color:"#c9d1d9",fontFamily:"monospace",maxHeight:200,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"};function v({label:t,children:s}){return e.jsxs("div",{style:{marginBottom:12},children:[e.jsx("div",{style:U,children:t}),s]})}function W({event:t}){let s=[];try{const o=new URL(t.url,"http://localhost");s=Array.from(o.searchParams.entries())}catch{}const n=t.requestHeaders&&Object.keys(t.requestHeaders).length>0,r=t.requestBody!=null,l=t.responseBody!=null;return e.jsxs("div",{style:{background:"#0d1117",padding:"12px 16px",borderTop:"1px solid #30363d"},children:[s.length>0&&e.jsx(v,{label:"Query Parameters",children:e.jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 12px",fontSize:12},children:s.map(([o,i],c)=>e.jsxs("div",{style:{display:"contents"},children:[e.jsx("span",{style:{color:"#58a6ff",fontFamily:"monospace"},children:o}),e.jsx("span",{style:{color:"#c9d1d9",fontFamily:"monospace"},children:i})]},c))})}),n&&e.jsx(v,{label:"Request Headers",children:e.jsx("pre",{style:R,children:Object.entries(t.requestHeaders).map(([o,i])=>`${o}: ${i}`).join(`
`)})}),r&&e.jsx(v,{label:"Request Body",children:e.jsx("pre",{style:R,children:typeof t.requestBody=="string"?t.requestBody:JSON.stringify(t.requestBody,null,2)})}),l&&e.jsx(v,{label:"Response Body",children:e.jsx("pre",{style:R,children:typeof t.responseBody=="string"?t.responseBody:JSON.stringify(t.responseBody,null,2)})}),s.length===0&&!n&&!r&&!l&&e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"No additional details available for this request"})]})}function F({event:t}){const[s,n]=a.useState(!1),r=new Date(t.timestampMs).toLocaleTimeString(),l=t.mocked;return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{onClick:()=>n(!s),style:{borderBottom:"1px solid #21262d",background:l?"transparent":"rgba(210, 153, 34, 0.05)",cursor:"pointer"},children:[e.jsxs("td",{style:{...u,color:"#8b949e",textAlign:"center"},children:[e.jsx("span",{style:{display:"inline-block",width:12,marginRight:4,fontSize:10,color:"#8b949e",transition:"transform 0.15s",transform:s?"rotate(90deg)":"rotate(0deg)"},children:"▶"}),r]}),e.jsx("td",{style:{...u,textAlign:"center"},children:e.jsx(L,{method:t.method})}),e.jsx("td",{style:{...u,maxWidth:400,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.url}),e.jsx("td",{style:{...u,textAlign:"center",color:"#8b949e"},children:t.status??"---"}),e.jsxs("td",{style:{...u,textAlign:"center",color:"#8b949e"},children:[t.durationMs,"ms"]}),e.jsx("td",{style:{...u,textAlign:"center"},children:l?t.mockSource==="default"?e.jsx(f,{color:"#1f6feb",children:"MOCKED (default)"}):e.jsx(f,{color:"#238636",children:"MOCKED (scenario)"}):e.jsx(f,{color:"#d29922",children:"PASSTHROUGH"})})]}),s&&e.jsx("tr",{children:e.jsx("td",{colSpan:6,style:{padding:0},children:e.jsx(W,{event:t})})})]})}function D({events:t}){return e.jsx("div",{style:{maxHeight:300,overflowY:"auto",border:"1px solid #30363d",borderRadius:6},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",position:"sticky",top:0,background:"#0d1117"},children:[e.jsx("th",{style:h,children:"Time"}),e.jsx("th",{style:h,children:"Method"}),e.jsx("th",{style:{...h,textAlign:"left"},children:"URL"}),e.jsx("th",{style:h,children:"Status"}),e.jsx("th",{style:h,children:"Duration"}),e.jsx("th",{style:h,children:"Source"})]})}),e.jsx("tbody",{children:t.map(s=>e.jsx(F,{event:s},s.id))})]})})}function q(t){if(t===void 0)return"checked";if(t==="checked")return"excluded"}function $({chips:t,chipStates:s,onChipStatesChange:n}){if(t.length===0)return null;const r=o=>{const i=new Map(s),c=q(i.get(o));c===void 0?i.delete(o):i.set(o,c),n(i)},l=()=>{n(new Map)};return e.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:6,marginBottom:12},children:[s.size>0&&e.jsx("button",{onClick:l,style:{background:"#da363322",color:"#da3633",border:"1px solid #da363355",borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,cursor:"pointer",fontFamily:"inherit"},children:"Clear all"}),t.map(({path:o,count:i})=>{const c=s.get(o),d=c==="checked",g=c==="excluded";let j="#30363d",b="#8b949e",S="#30363d",y="";return d?(j="#58a6ff22",b="#58a6ff",S="#58a6ff55",y="✓ "):g&&(j="#da363322",b="#da3633",S="#da363355",y="✗ "),e.jsxs("button",{onClick:()=>r(o),style:{background:j,color:b,border:`1px solid ${S}`,borderRadius:4,padding:"2px 8px",fontSize:11,fontFamily:"monospace",cursor:"pointer",whiteSpace:"nowrap"},children:[y,o,e.jsxs("span",{style:{marginLeft:4,opacity:.6,fontFamily:"inherit"},children:["(",i,")"]})]},o)})]})}function N(){const{events:t}=T(),[s,n]=a.useState(""),[r,l]=a.useState(()=>new Map),[o,i]=a.useState("ALL"),[c,d]=a.useState("ALL"),g=t.filter(p=>p.type==="http_request"),j=a.useMemo(()=>{const p=new Map;for(const x of g){const m=A(x.url);p.set(m,(p.get(m)||0)+1)}return Array.from(p.entries()).sort((x,m)=>m[1]-x[1]).slice(0,10).map(([x,m])=>({path:x,count:m}))},[g]),b=a.useMemo(()=>Array.from(r.values()).some(p=>p==="checked"),[r]),y=g.filter(p=>{if(s&&!p.url.toLowerCase().includes(s.toLowerCase()))return!1;const x=A(p.url);return!(r.get(x)==="excluded"||b&&r.get(x)!=="checked"||o!=="ALL"&&p.method.toUpperCase()!==o||c==="MOCKED"&&!p.mocked||c==="PASSTHROUGH"&&p.mocked)}).slice(-50).reverse();return e.jsxs("section",{children:[e.jsx(k,{title:"Live Traffic"}),e.jsx(H,{textUrlFilter:s,onTextUrlFilterChange:n,methodFilter:o,onMethodFilterChange:i,sourceFilter:c,onSourceFilterChange:p=>d(p)}),e.jsx($,{chips:j,chipStates:r,onChipStatesChange:l}),y.length===0?e.jsx(E,{color:"#8b949e",children:g.length>0?"No matching traffic — try adjusting the filters":"No HTTP traffic yet — requests will appear here in real-time"}):e.jsx(D,{events:y})]})}function G(){const[t,s]=a.useState(null),[n,r]=a.useState(!1);return{explain:a.useCallback(async(o,i)=>{r(!0);try{const d=await(await fetch("/api/inspector/explain",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:o,url:i})})).json();s(d)}catch{}finally{r(!1)}},[]),result:t,loading:n}}function _({result:t}){return e.jsxs("div",{style:{border:"1px solid #30363d",borderRadius:6,padding:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:12},children:[e.jsxs("span",{style:{color:"#f0f6fc",fontWeight:500},children:[t.method," ",t.url]}),t.wouldMatch?e.jsx(f,{color:"#238636",children:"WOULD MATCH"}):e.jsx(f,{color:"#da3633",children:"NO MATCH"})]}),t.attempts.length===0?e.jsx("div",{style:{color:"#8b949e"},children:"No routes loaded to match against"}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6},children:t.attempts.map((s,n)=>e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 8px",borderRadius:4,background:s.failure?"transparent":"rgba(35, 134, 54, 0.1)"},children:[e.jsx("span",{style:{color:s.failure?"#da3633":"#238636",fontWeight:600,width:16},children:s.failure?"x":"v"}),e.jsx(L,{method:s.method}),e.jsx("span",{children:s.pattern}),s.failure&&e.jsx("span",{style:{color:"#8b949e",fontSize:11},children:s.failure.reason==="methodMismatch"?`method: expected ${s.failure.expected}, got ${s.failure.actual}`:`path "${s.failure.path}" does not match "${s.failure.pattern}"`})]},n))})]})}function K(){const{explain:t,result:s,loading:n}=G(),[r,l]=a.useState("GET"),[o,i]=a.useState(""),c=d=>{d.preventDefault(),o.trim()&&t(r,o.trim())};return e.jsxs("section",{children:[e.jsx(k,{title:"Match Explainer"}),e.jsxs("form",{onSubmit:c,style:{display:"flex",gap:8,marginBottom:12},children:[e.jsx("select",{value:r,onChange:d=>l(d.target.value),style:{...w,width:100},children:["GET","POST","PUT","DELETE","PATCH"].map(d=>e.jsx("option",{value:d,children:d},d))}),e.jsx("input",{type:"text",value:o,onChange:d=>i(d.target.value),placeholder:"/api/users",style:{...w,flex:1}}),e.jsx("button",{type:"submit",disabled:n||!o.trim(),style:{background:"#238636",color:"#fff",border:"none",borderRadius:6,padding:"6px 16px",cursor:"pointer",fontSize:12,fontWeight:500,opacity:n||!o.trim()?.5:1},children:n?"Checking...":"Explain"})]}),s&&e.jsx(_,{result:s})]})}const C={"/api/health":"Health check — confirms the proxy and mock engine are running","/api/events":"SSE stream of real-time events (traffic, errors) powering this Inspector","/api/config":"Returns current CodeYam configuration for the project","/api/editor-dev-server":"Polls/controls the dev server status (start, stop, restart)","/api/inspector/status":"Returns active scenario and loaded mock routes","/api/inspector/explain":"Tests which mock route would match a given method + URL","/api/scenarios":"Lists all defined scenarios with metadata","/api/tests":"Returns the AI-maintained test registry joined with per-test-evidence, grouped by file","/api/test-results":"Returns latest test execution results","/api/screenshots/*":"Serves captured scenario screenshots","/api/session-info":"Returns current editor session metadata","/api/data-structure":"Returns the app's data structure definition","/api/dependency-graph":"Returns the project's component dependency graph","/api/app-route-entities":"Lists app routes and their associated entities","/api/glossary":"Returns the project's component glossary","/api/step":"Returns current editor workflow step","/api/editor-commands":"Lists available editor commands"};function J(t){if(C[t])return C[t];for(const[s,n]of Object.entries(C))if(s.endsWith("/*")){const r=s.slice(0,-1);if(t.startsWith(r))return n}}function V(t){const s=new Set([...t,...Object.keys(C)]),n=[];for(const r of Array.from(s).sort()){if(r.endsWith("/*"))continue;const l=J(r);n.push({path:r,description:l??"Application endpoint"})}return n}function Y(){const{events:t}=T(),s=a.useMemo(()=>{const n=[];for(const r of t)r.type==="http_request"&&n.push(A(r.url));return V(n)},[t]);return s.length===0?null:e.jsxs("section",{children:[e.jsx(k,{title:"URL Glossary"}),e.jsx("div",{style:{border:"1px solid #30363d",borderRadius:6,overflow:"hidden"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",background:"#161b22"},children:[e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px",width:"30%"},children:"Path"}),e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},children:"Description"})]})}),e.jsx("tbody",{children:s.map(({path:n,description:r})=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{padding:"4px 12px",fontFamily:"monospace",color:"#58a6ff",fontSize:11},children:n}),e.jsx("td",{style:{padding:"4px 12px",color:"#8b949e"},children:r})]},n))})]})})]})}function Q(){const{connected:t}=T();return e.jsxs("div",{style:{background:"#0d1117",color:"#c9d1d9",minHeight:"100vh",fontFamily:"'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace",fontSize:13,padding:24,display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("header",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("h1",{style:{fontSize:18,fontWeight:600,color:"#f0f6fc",margin:0},children:"Container Inspector"}),e.jsx("span",{style:{color:"#8b949e",fontSize:12},children:"Read-only view of proxy and mock engine state"}),e.jsx(M,{connected:t})]}),e.jsx(I,{}),e.jsx(N,{}),e.jsx(K,{}),e.jsx(Y,{})]})}const te=Object.freeze(Object.defineProperty({__proto__:null,ContainerInspector:Q},Symbol.toStringTag,{value:"Module"}));export{f as B,Q as C,F as I,L as M,k as S,E as a,Y as b,$ as c,D as d,H as e,W as f,_ as g,K as h,N as i,O as j,I as k,M as l,te as m,h as t};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

import{j as r,M as a,r as c}from"./markdown-v0F0UEt3.js";const i={color:"var(--text-secondary)",fontSize:"var(--text-sm, 14px)",lineHeight:1.6},s={...i,fontSize:"var(--text-xs, 12px)",lineHeight:1.5};function x({children:l,variant:d="default"}){const n=d==="compact",t=n?"0.375rem":"0.5rem";return r.jsx("div",{className:"markdown-content",style:n?s:i,children:r.jsx(a,{remarkPlugins:[c],components:{h1:({children:e})=>r.jsx("h1",{style:{fontSize:n?"1rem":"1.125rem",fontWeight:600,color:"var(--text-primary)",margin:`${t} 0`},children:e}),h2:({children:e})=>r.jsx("h2",{style:{fontSize:n?"0.875rem":"1rem",fontWeight:600,color:"var(--text-primary)",margin:`${t} 0`},children:e}),h3:({children:e})=>r.jsx("h3",{style:{fontSize:n?"0.8125rem":"0.875rem",fontWeight:600,color:"var(--text-primary)",margin:`${t} 0`},children:e}),p:({children:e})=>r.jsx("p",{style:{margin:`${t} 0`},children:e}),ul:({children:e})=>r.jsx("ul",{style:{margin:`${t} 0`,paddingLeft:"1.25rem",listStyleType:"disc"},children:e}),ol:({children:e})=>r.jsx("ol",{style:{margin:`${t} 0`,paddingLeft:"1.25rem",listStyleType:"decimal"},children:e}),li:({children:e})=>r.jsx("li",{style:{marginBottom:"0.125rem"},children:e}),code:({className:e,children:o})=>(e==null?void 0:e.includes("language-"))?r.jsx("code",{className:e,style:{fontFamily:"'IBM Plex Mono', monospace",fontSize:"0.8125rem"},children:o}):r.jsx("code",{style:{fontFamily:"'IBM Plex Mono', monospace",fontSize:"0.85em",background:"var(--bg-input)",padding:"0.1em 0.35em",borderRadius:"3px",color:"var(--accent-orange)"},children:o}),pre:({children:e})=>r.jsx("pre",{style:{background:"var(--bg-input)",borderRadius:"6px",padding:n?"0.5rem":"0.75rem",margin:`${t} 0`,overflowX:"auto",fontSize:"0.8125rem",lineHeight:1.5,border:"1px solid var(--border-subtle)"},children:e}),a:({href:e,children:o})=>r.jsx("a",{href:e,style:{color:"var(--accent-blue)",textDecoration:"underline",textUnderlineOffset:"2px"},target:"_blank",rel:"noopener noreferrer",children:o}),strong:({children:e})=>r.jsx("strong",{style:{fontWeight:600,color:"var(--text-primary)"},children:e}),blockquote:({children:e})=>r.jsx("blockquote",{style:{borderLeft:"3px solid var(--accent-blue)",paddingLeft:"0.75rem",margin:`${t} 0`,color:"var(--text-muted)",fontStyle:"italic"},children:e}),table:({children:e})=>r.jsx("div",{style:{overflowX:"auto",margin:`${t} 0`},children:r.jsx("table",{style:{borderCollapse:"collapse",width:"100%",fontSize:"0.8125rem"},children:e})}),th:({children:e})=>r.jsx("th",{style:{borderBottom:"2px solid var(--border-default)",padding:"0.375rem 0.5rem",textAlign:"left",fontWeight:600,color:"var(--text-primary)"},children:e}),td:({children:e})=>r.jsx("td",{style:{borderBottom:"1px solid var(--border-subtle)",padding:"0.375rem 0.5rem"},children:e})},children:l})})}export{x as M};

Sorry, the diff of this file is too big to display

function at(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var de={exports:{}},E={};/**
* @license React
* react.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var xe;function ot(){if(xe)return E;xe=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),n=Symbol.for("react.strict_mode"),a=Symbol.for("react.profiler"),u=Symbol.for("react.consumer"),s=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),l=Symbol.for("react.suspense"),o=Symbol.for("react.memo"),f=Symbol.for("react.lazy"),h=Symbol.for("react.activity"),y=Symbol.iterator;function v(i){return i===null||typeof i!="object"?null:(i=y&&i[y]||i["@@iterator"],typeof i=="function"?i:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,p={};function C(i,m,_){this.props=i,this.context=m,this.refs=p,this.updater=_||w}C.prototype.isReactComponent={},C.prototype.setState=function(i,m){if(typeof i!="object"&&typeof i!="function"&&i!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,i,m,"setState")},C.prototype.forceUpdate=function(i){this.updater.enqueueForceUpdate(this,i,"forceUpdate")};function S(){}S.prototype=C.prototype;function x(i,m,_){this.props=i,this.context=m,this.refs=p,this.updater=_||w}var $=x.prototype=new S;$.constructor=x,g($,C.prototype),$.isPureReactComponent=!0;var O=Array.isArray;function D(){}var P={H:null,A:null,T:null,S:null},B=Object.prototype.hasOwnProperty;function j(i,m,_){var R=_.ref;return{$$typeof:e,type:i,key:m,ref:R!==void 0?R:null,props:_}}function V(i,m){return j(i.type,m,i.props)}function J(i){return typeof i=="object"&&i!==null&&i.$$typeof===e}function fe(i){var m={"=":"=0",":":"=2"};return"$"+i.replace(/[=:]/g,function(_){return m[_]})}var X=/\/+/g;function z(i,m){return typeof i=="object"&&i!==null&&i.key!=null?fe(""+i.key):m.toString(36)}function M(i){switch(i.status){case"fulfilled":return i.value;case"rejected":throw i.reason;default:switch(typeof i.status=="string"?i.then(D,D):(i.status="pending",i.then(function(m){i.status==="pending"&&(i.status="fulfilled",i.value=m)},function(m){i.status==="pending"&&(i.status="rejected",i.reason=m)})),i.status){case"fulfilled":return i.value;case"rejected":throw i.reason}}throw i}function K(i,m,_,R,T){var b=typeof i;(b==="undefined"||b==="boolean")&&(i=null);var L=!1;if(i===null)L=!0;else switch(b){case"bigint":case"string":case"number":L=!0;break;case"object":switch(i.$$typeof){case e:case t:L=!0;break;case f:return L=i._init,K(L(i._payload),m,_,R,T)}}if(L)return T=T(i),L=R===""?"."+z(i,0):R,O(T)?(_="",L!=null&&(_=L.replace(X,"$&/")+"/"),K(T,m,_,"",function(nt){return nt})):T!=null&&(J(T)&&(T=V(T,_+(T.key==null||i&&i.key===T.key?"":(""+T.key).replace(X,"$&/")+"/")+L)),m.push(T)),1;L=0;var H=R===""?".":R+":";if(O(i))for(var N=0;N<i.length;N++)R=i[N],b=H+z(R,N),L+=K(R,m,_,b,T);else if(N=v(i),typeof N=="function")for(i=N.call(i),N=0;!(R=i.next()).done;)R=R.value,b=H+z(R,N++),L+=K(R,m,_,b,T);else if(b==="object"){if(typeof i.then=="function")return K(M(i),m,_,R,T);throw m=String(i),Error("Objects are not valid as a React child (found: "+(m==="[object Object]"?"object with keys {"+Object.keys(i).join(", ")+"}":m)+"). If you meant to render a collection of children, use an array instead.")}return L}function ne(i,m,_){if(i==null)return i;var R=[],T=0;return K(i,R,"","",function(b){return m.call(_,b,T++)}),R}function tt(i){if(i._status===-1){var m=i._result;m=m(),m.then(function(_){(i._status===0||i._status===-1)&&(i._status=1,i._result=_)},function(_){(i._status===0||i._status===-1)&&(i._status=2,i._result=_)}),i._status===-1&&(i._status=0,i._result=m)}if(i._status===1)return i._result.default;throw i._result}var Se=typeof reportError=="function"?reportError:function(i){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var m=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof i=="object"&&i!==null&&typeof i.message=="string"?String(i.message):String(i),error:i});if(!window.dispatchEvent(m))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",i);return}console.error(i)},rt={map:ne,forEach:function(i,m,_){ne(i,function(){m.apply(this,arguments)},_)},count:function(i){var m=0;return ne(i,function(){m++}),m},toArray:function(i){return ne(i,function(m){return m})||[]},only:function(i){if(!J(i))throw Error("React.Children.only expected to receive a single React element child.");return i}};return E.Activity=h,E.Children=rt,E.Component=C,E.Fragment=r,E.Profiler=a,E.PureComponent=x,E.StrictMode=n,E.Suspense=l,E.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=P,E.__COMPILER_RUNTIME={__proto__:null,c:function(i){return P.H.useMemoCache(i)}},E.cache=function(i){return function(){return i.apply(null,arguments)}},E.cacheSignal=function(){return null},E.cloneElement=function(i,m,_){if(i==null)throw Error("The argument must be a React element, but you passed "+i+".");var R=g({},i.props),T=i.key;if(m!=null)for(b in m.key!==void 0&&(T=""+m.key),m)!B.call(m,b)||b==="key"||b==="__self"||b==="__source"||b==="ref"&&m.ref===void 0||(R[b]=m[b]);var b=arguments.length-2;if(b===1)R.children=_;else if(1<b){for(var L=Array(b),H=0;H<b;H++)L[H]=arguments[H+2];R.children=L}return j(i.type,T,R)},E.createContext=function(i){return i={$$typeof:s,_currentValue:i,_currentValue2:i,_threadCount:0,Provider:null,Consumer:null},i.Provider=i,i.Consumer={$$typeof:u,_context:i},i},E.createElement=function(i,m,_){var R,T={},b=null;if(m!=null)for(R in m.key!==void 0&&(b=""+m.key),m)B.call(m,R)&&R!=="key"&&R!=="__self"&&R!=="__source"&&(T[R]=m[R]);var L=arguments.length-2;if(L===1)T.children=_;else if(1<L){for(var H=Array(L),N=0;N<L;N++)H[N]=arguments[N+2];T.children=H}if(i&&i.defaultProps)for(R in L=i.defaultProps,L)T[R]===void 0&&(T[R]=L[R]);return j(i,b,T)},E.createRef=function(){return{current:null}},E.forwardRef=function(i){return{$$typeof:d,render:i}},E.isValidElement=J,E.lazy=function(i){return{$$typeof:f,_payload:{_status:-1,_result:i},_init:tt}},E.memo=function(i,m){return{$$typeof:o,type:i,compare:m===void 0?null:m}},E.startTransition=function(i){var m=P.T,_={};P.T=_;try{var R=i(),T=P.S;T!==null&&T(_,R),typeof R=="object"&&R!==null&&typeof R.then=="function"&&R.then(D,Se)}catch(b){Se(b)}finally{m!==null&&_.types!==null&&(m.types=_.types),P.T=m}},E.unstable_useCacheRefresh=function(){return P.H.useCacheRefresh()},E.use=function(i){return P.H.use(i)},E.useActionState=function(i,m,_){return P.H.useActionState(i,m,_)},E.useCallback=function(i,m){return P.H.useCallback(i,m)},E.useContext=function(i){return P.H.useContext(i)},E.useDebugValue=function(){},E.useDeferredValue=function(i,m){return P.H.useDeferredValue(i,m)},E.useEffect=function(i,m){return P.H.useEffect(i,m)},E.useEffectEvent=function(i){return P.H.useEffectEvent(i)},E.useId=function(){return P.H.useId()},E.useImperativeHandle=function(i,m,_){return P.H.useImperativeHandle(i,m,_)},E.useInsertionEffect=function(i,m){return P.H.useInsertionEffect(i,m)},E.useLayoutEffect=function(i,m){return P.H.useLayoutEffect(i,m)},E.useMemo=function(i,m){return P.H.useMemo(i,m)},E.useOptimistic=function(i,m){return P.H.useOptimistic(i,m)},E.useReducer=function(i,m,_){return P.H.useReducer(i,m,_)},E.useRef=function(i){return P.H.useRef(i)},E.useState=function(i){return P.H.useState(i)},E.useSyncExternalStore=function(i,m,_){return P.H.useSyncExternalStore(i,m,_)},E.useTransition=function(){return P.H.useTransition()},E.version="19.2.4",E}var Pe;function Ie(){return Pe||(Pe=1,de.exports=ot()),de.exports}var he={exports:{}},A={};/**
* @license React
* react-dom.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var Te;function it(){if(Te)return A;Te=1;var e=Ie();function t(l){var o="https://react.dev/errors/"+l;if(1<arguments.length){o+="?args[]="+encodeURIComponent(arguments[1]);for(var f=2;f<arguments.length;f++)o+="&args[]="+encodeURIComponent(arguments[f])}return"Minified React error #"+l+"; visit "+o+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function r(){}var n={d:{f:r,r:function(){throw Error(t(522))},D:r,C:r,L:r,m:r,X:r,S:r,M:r},p:0,findDOMNode:null},a=Symbol.for("react.portal");function u(l,o,f){var h=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:a,key:h==null?null:""+h,children:l,containerInfo:o,implementation:f}}var s=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(l,o){if(l==="font")return"";if(typeof o=="string")return o==="use-credentials"?o:""}return A.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=n,A.createPortal=function(l,o){var f=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!o||o.nodeType!==1&&o.nodeType!==9&&o.nodeType!==11)throw Error(t(299));return u(l,o,null,f)},A.flushSync=function(l){var o=s.T,f=n.p;try{if(s.T=null,n.p=2,l)return l()}finally{s.T=o,n.p=f,n.d.f()}},A.preconnect=function(l,o){typeof l=="string"&&(o?(o=o.crossOrigin,o=typeof o=="string"?o==="use-credentials"?o:"":void 0):o=null,n.d.C(l,o))},A.prefetchDNS=function(l){typeof l=="string"&&n.d.D(l)},A.preinit=function(l,o){if(typeof l=="string"&&o&&typeof o.as=="string"){var f=o.as,h=d(f,o.crossOrigin),y=typeof o.integrity=="string"?o.integrity:void 0,v=typeof o.fetchPriority=="string"?o.fetchPriority:void 0;f==="style"?n.d.S(l,typeof o.precedence=="string"?o.precedence:void 0,{crossOrigin:h,integrity:y,fetchPriority:v}):f==="script"&&n.d.X(l,{crossOrigin:h,integrity:y,fetchPriority:v,nonce:typeof o.nonce=="string"?o.nonce:void 0})}},A.preinitModule=function(l,o){if(typeof l=="string")if(typeof o=="object"&&o!==null){if(o.as==null||o.as==="script"){var f=d(o.as,o.crossOrigin);n.d.M(l,{crossOrigin:f,integrity:typeof o.integrity=="string"?o.integrity:void 0,nonce:typeof o.nonce=="string"?o.nonce:void 0})}}else o==null&&n.d.M(l)},A.preload=function(l,o){if(typeof l=="string"&&typeof o=="object"&&o!==null&&typeof o.as=="string"){var f=o.as,h=d(f,o.crossOrigin);n.d.L(l,f,{crossOrigin:h,integrity:typeof o.integrity=="string"?o.integrity:void 0,nonce:typeof o.nonce=="string"?o.nonce:void 0,type:typeof o.type=="string"?o.type:void 0,fetchPriority:typeof o.fetchPriority=="string"?o.fetchPriority:void 0,referrerPolicy:typeof o.referrerPolicy=="string"?o.referrerPolicy:void 0,imageSrcSet:typeof o.imageSrcSet=="string"?o.imageSrcSet:void 0,imageSizes:typeof o.imageSizes=="string"?o.imageSizes:void 0,media:typeof o.media=="string"?o.media:void 0})}},A.preloadModule=function(l,o){if(typeof l=="string")if(o){var f=d(o.as,o.crossOrigin);n.d.m(l,{as:typeof o.as=="string"&&o.as!=="script"?o.as:void 0,crossOrigin:f,integrity:typeof o.integrity=="string"?o.integrity:void 0})}else n.d.m(l)},A.requestFormReset=function(l){n.d.r(l)},A.unstable_batchedUpdates=function(l,o){return l(o)},A.useFormState=function(l,o,f){return s.H.useFormState(l,o,f)},A.useFormStatus=function(){return s.H.useHostTransitionStatus()},A.version="19.2.4",A}var be;function ut(){if(be)return he.exports;be=1;function e(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),he.exports=it(),he.exports}var c=Ie();const Mr=at(c);/**
* react-router v7.13.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/var Le="popstate";function Oe(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function lt(e={}){function t(n,a){var o;let u=(o=a.state)==null?void 0:o.masked,{pathname:s,search:d,hash:l}=u||n.location;return ge("",{pathname:s,search:d,hash:l},a.state&&a.state.usr||null,a.state&&a.state.key||"default",u?{pathname:n.location.pathname,search:n.location.search,hash:n.location.hash}:void 0)}function r(n,a){return typeof a=="string"?a:Z(a)}return ct(t,r,null,e)}function k(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function F(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function st(){return Math.random().toString(36).substring(2,10)}function $e(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function ge(e,t,r=null,n,a){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?ee(t):t,state:r,key:t&&t.key||n||st(),unstable_mask:a}}function Z({pathname:e="/",search:t="",hash:r=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),r&&r!=="#"&&(e+=r.charAt(0)==="#"?r:"#"+r),e}function ee(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substring(r),e=e.substring(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substring(n),e=e.substring(0,n)),e&&(t.pathname=e)}return t}function ct(e,t,r,n={}){let{window:a=document.defaultView,v5Compat:u=!1}=n,s=a.history,d="POP",l=null,o=f();o==null&&(o=0,s.replaceState({...s.state,idx:o},""));function f(){return(s.state||{idx:null}).idx}function h(){d="POP";let p=f(),C=p==null?null:p-o;o=p,l&&l({action:d,location:g.location,delta:C})}function y(p,C){d="PUSH";let S=Oe(p)?p:ge(g.location,p,C);o=f()+1;let x=$e(S,o),$=g.createHref(S.unstable_mask||S);try{s.pushState(x,"",$)}catch(O){if(O instanceof DOMException&&O.name==="DataCloneError")throw O;a.location.assign($)}u&&l&&l({action:d,location:g.location,delta:1})}function v(p,C){d="REPLACE";let S=Oe(p)?p:ge(g.location,p,C);o=f();let x=$e(S,o),$=g.createHref(S.unstable_mask||S);s.replaceState(x,"",$),u&&l&&l({action:d,location:g.location,delta:0})}function w(p){return ft(p)}let g={get action(){return d},get location(){return e(a,s)},listen(p){if(l)throw new Error("A history only accepts one active listener");return a.addEventListener(Le,h),l=p,()=>{a.removeEventListener(Le,h),l=null}},createHref(p){return t(a,p)},createURL:w,encodeLocation(p){let C=w(p);return{pathname:C.pathname,search:C.search,hash:C.hash}},push:y,replace:v,go(p){return s.go(p)}};return g}function ft(e,t=!1){let r="http://localhost";typeof window<"u"&&(r=window.location.origin!=="null"?window.location.origin:window.location.href),k(r,"No window.location.(origin|href) available to create URL");let n=typeof e=="string"?e:Z(e);return n=n.replace(/ $/,"%20"),!t&&n.startsWith("//")&&(n=r+n),new URL(n,r)}function Me(e,t,r="/"){return dt(e,t,r,!1)}function dt(e,t,r,n){let a=typeof t=="string"?ee(t):t,u=W(a.pathname||"/",r);if(u==null)return null;let s=He(e);ht(s);let d=null;for(let l=0;d==null&&l<s.length;++l){let o=St(u);d=Ct(s[l],o,n)}return d}function He(e,t=[],r=[],n="",a=!1){let u=(s,d,l=a,o)=>{let f={relativePath:o===void 0?s.path||"":o,caseSensitive:s.caseSensitive===!0,childrenIndex:d,route:s};if(f.relativePath.startsWith("/")){if(!f.relativePath.startsWith(n)&&l)return;k(f.relativePath.startsWith(n),`Absolute route path "${f.relativePath}" nested under path "${n}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),f.relativePath=f.relativePath.slice(n.length)}let h=U([n,f.relativePath]),y=r.concat(f);s.children&&s.children.length>0&&(k(s.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${h}".`),He(s.children,t,y,h,l)),!(s.path==null&&!s.index)&&t.push({path:h,score:Rt(h,s.index),routesMeta:y})};return e.forEach((s,d)=>{var l;if(s.path===""||!((l=s.path)!=null&&l.includes("?")))u(s,d);else for(let o of Ue(s.path))u(s,d,!0,o)}),t}function Ue(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,a=r.endsWith("?"),u=r.replace(/\?$/,"");if(n.length===0)return a?[u,""]:[u];let s=Ue(n.join("/")),d=[];return d.push(...s.map(l=>l===""?u:[u,l].join("/"))),a&&d.push(...s),d.map(l=>e.startsWith("/")&&l===""?"/":l)}function ht(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:wt(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}var mt=/^:[\w-]+$/,pt=3,yt=2,gt=1,vt=10,Et=-2,ke=e=>e==="*";function Rt(e,t){let r=e.split("/"),n=r.length;return r.some(ke)&&(n+=Et),t&&(n+=yt),r.filter(a=>!ke(a)).reduce((a,u)=>a+(mt.test(u)?pt:u===""?gt:vt),n)}function wt(e,t){return e.length===t.length&&e.slice(0,-1).every((n,a)=>n===t[a])?e[e.length-1]-t[t.length-1]:0}function Ct(e,t,r=!1){let{routesMeta:n}=e,a={},u="/",s=[];for(let d=0;d<n.length;++d){let l=n[d],o=d===n.length-1,f=u==="/"?t:t.slice(u.length)||"/",h=ue({path:l.relativePath,caseSensitive:l.caseSensitive,end:o},f),y=l.route;if(!h&&o&&r&&!n[n.length-1].route.index&&(h=ue({path:l.relativePath,caseSensitive:l.caseSensitive,end:!1},f)),!h)return null;Object.assign(a,h.params),s.push({params:a,pathname:U([u,h.pathname]),pathnameBase:bt(U([u,h.pathnameBase])),route:y}),h.pathnameBase!=="/"&&(u=U([u,h.pathnameBase]))}return s}function ue(e,t){typeof e=="string"&&(e={path:e,caseSensitive:!1,end:!0});let[r,n]=_t(e.path,e.caseSensitive,e.end),a=t.match(r);if(!a)return null;let u=a[0],s=u.replace(/(.)\/+$/,"$1"),d=a.slice(1);return{params:n.reduce((o,{paramName:f,isOptional:h},y)=>{if(f==="*"){let w=d[y]||"";s=u.slice(0,u.length-w.length).replace(/(.)\/+$/,"$1")}const v=d[y];return h&&!v?o[f]=void 0:o[f]=(v||"").replace(/%2F/g,"/"),o},{}),pathname:u,pathnameBase:s,pattern:e}}function _t(e,t=!1,r=!0){F(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let n=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(s,d,l,o,f)=>{if(n.push({paramName:d,isOptional:l!=null}),l){let h=f.charAt(o+s.length);return h&&h!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(n.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,t?void 0:"i"),n]}function St(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return F(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function W(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}var xt=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Pt(e,t="/"){let{pathname:r,search:n="",hash:a=""}=typeof e=="string"?ee(e):e,u;return r?(r=r.replace(/\/\/+/g,"/"),r.startsWith("/")?u=Ae(r.substring(1),"/"):u=Ae(r,t)):u=t,{pathname:u,search:Lt(n),hash:Ot(a)}}function Ae(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(a=>{a===".."?r.length>1&&r.pop():a!=="."&&r.push(a)}),r.length>1?r.join("/"):"/"}function me(e,t,r,n){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(n)}]. Please separate it out to the \`to.${r}\` field. Alternatively you may provide the full path as a string in <Link to="..."> and the router will parse it for you.`}function Tt(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function Fe(e){let t=Tt(e);return t.map((r,n)=>n===t.length-1?r.pathname:r.pathnameBase)}function ve(e,t,r,n=!1){let a;typeof e=="string"?a=ee(e):(a={...e},k(!a.pathname||!a.pathname.includes("?"),me("?","pathname","search",a)),k(!a.pathname||!a.pathname.includes("#"),me("#","pathname","hash",a)),k(!a.search||!a.search.includes("#"),me("#","search","hash",a)));let u=e===""||a.pathname==="",s=u?"/":a.pathname,d;if(s==null)d=r;else{let h=t.length-1;if(!n&&s.startsWith("..")){let y=s.split("/");for(;y[0]==="..";)y.shift(),h-=1;a.pathname=y.join("/")}d=h>=0?t[h]:"/"}let l=Pt(a,d),o=s&&s!=="/"&&s.endsWith("/"),f=(u||s===".")&&r.endsWith("/");return!l.pathname.endsWith("/")&&(o||f)&&(l.pathname+="/"),l}var U=e=>e.join("/").replace(/\/\/+/g,"/"),bt=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),Lt=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,Ot=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,$t=class{constructor(e,t,r,n=!1){this.status=e,this.statusText=t||"",this.internal=n,r instanceof Error?(this.data=r.toString(),this.error=r):this.data=r}};function kt(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function At(e){return e.map(t=>t.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Be=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function je(e,t){let r=e;if(typeof r!="string"||!xt.test(r))return{absoluteURL:void 0,isExternal:!1,to:r};let n=r,a=!1;if(Be)try{let u=new URL(window.location.href),s=r.startsWith("//")?new URL(u.protocol+r):new URL(r),d=W(s.pathname,t);s.origin===u.origin&&d!=null?r=d+s.search+s.hash:a=!0}catch{F(!1,`<Link to="${r}"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:n,isExternal:a,to:r}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var We=["POST","PUT","PATCH","DELETE"];new Set(We);var Dt=["GET",...We];new Set(Dt);var G=c.createContext(null);G.displayName="DataRouter";var le=c.createContext(null);le.displayName="DataRouterState";var Nt=c.createContext(!1),Ye=c.createContext({isTransitioning:!1});Ye.displayName="ViewTransition";var It=c.createContext(new Map);It.displayName="Fetchers";var Mt=c.createContext(null);Mt.displayName="Await";var I=c.createContext(null);I.displayName="Navigation";var se=c.createContext(null);se.displayName="Location";var Y=c.createContext({outlet:null,matches:[],isDataRoute:!1});Y.displayName="Route";var Ee=c.createContext(null);Ee.displayName="RouteError";var qe="REACT_ROUTER_ERROR",Ht="REDIRECT",Ut="ROUTE_ERROR_RESPONSE";function Ft(e){if(e.startsWith(`${qe}:${Ht}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function Bt(e){if(e.startsWith(`${qe}:${Ut}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new $t(t.status,t.statusText,t.data)}catch{}}function jt(e,{relative:t}={}){k(te(),"useHref() may be used only in the context of a <Router> component.");let{basename:r,navigator:n}=c.useContext(I),{hash:a,pathname:u,search:s}=re(e,{relative:t}),d=u;return r!=="/"&&(d=u==="/"?r:U([r,u])),n.createHref({pathname:d,search:s,hash:a})}function te(){return c.useContext(se)!=null}function q(){return k(te(),"useLocation() may be used only in the context of a <Router> component."),c.useContext(se).location}var ze="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Ke(e){c.useContext(I).static||c.useLayoutEffect(e)}function Wt(){let{isDataRoute:e}=c.useContext(Y);return e?rr():Yt()}function Yt(){k(te(),"useNavigate() may be used only in the context of a <Router> component.");let e=c.useContext(G),{basename:t,navigator:r}=c.useContext(I),{matches:n}=c.useContext(Y),{pathname:a}=q(),u=JSON.stringify(Fe(n)),s=c.useRef(!1);return Ke(()=>{s.current=!0}),c.useCallback((l,o={})=>{if(F(s.current,ze),!s.current)return;if(typeof l=="number"){r.go(l);return}let f=ve(l,JSON.parse(u),a,o.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:U([t,f.pathname])),(o.replace?r.replace:r.push)(f,o.state,o)},[t,r,u,a,e])}c.createContext(null);function re(e,{relative:t}={}){let{matches:r}=c.useContext(Y),{pathname:n}=q(),a=JSON.stringify(Fe(r));return c.useMemo(()=>ve(e,JSON.parse(a),n,t==="path"),[e,a,n,t])}function qt(e,t,r){k(te(),"useRoutes() may be used only in the context of a <Router> component.");let{navigator:n}=c.useContext(I),{matches:a}=c.useContext(Y),u=a[a.length-1],s=u?u.params:{},d=u?u.pathname:"/",l=u?u.pathnameBase:"/",o=u&&u.route;{let p=o&&o.path||"";Ve(d,!o||p.endsWith("*")||p.endsWith("*?"),`You rendered descendant <Routes> (or called \`useRoutes()\`) at "${d}" (under <Route path="${p}">) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.
Please change the parent <Route path="${p}"> to <Route path="${p==="/"?"*":`${p}/*`}">.`)}let f=q(),h;h=f;let y=h.pathname||"/",v=y;if(l!=="/"){let p=l.replace(/^\//,"").split("/");v="/"+y.replace(/^\//,"").split("/").slice(p.length).join("/")}let w=Me(e,{pathname:v});return F(o||w!=null,`No routes matched location "${h.pathname}${h.search}${h.hash}" `),F(w==null||w[w.length-1].route.element!==void 0||w[w.length-1].route.Component!==void 0||w[w.length-1].route.lazy!==void 0,`Matched leaf route at location "${h.pathname}${h.search}${h.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`),Jt(w&&w.map(p=>Object.assign({},p,{params:Object.assign({},s,p.params),pathname:U([l,n.encodeLocation?n.encodeLocation(p.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:p.pathname]),pathnameBase:p.pathnameBase==="/"?l:U([l,n.encodeLocation?n.encodeLocation(p.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:p.pathnameBase])})),a,r)}function zt(){let e=tr(),t=kt(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,n="rgba(200,200,200, 0.5)",a={padding:"0.5rem",backgroundColor:n},u={padding:"2px 4px",backgroundColor:n},s=null;return console.error("Error handled by React Router default ErrorBoundary:",e),s=c.createElement(c.Fragment,null,c.createElement("p",null,"💿 Hey developer 👋"),c.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",c.createElement("code",{style:u},"ErrorBoundary")," or"," ",c.createElement("code",{style:u},"errorElement")," prop on your route.")),c.createElement(c.Fragment,null,c.createElement("h2",null,"Unexpected Application Error!"),c.createElement("h3",{style:{fontStyle:"italic"}},t),r?c.createElement("pre",{style:a},r):null,s)}var Kt=c.createElement(zt,null),Ge=class extends c.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const r=Bt(e.digest);r&&(e=r)}let t=e!==void 0?c.createElement(Y.Provider,{value:this.props.routeContext},c.createElement(Ee.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?c.createElement(Gt,{error:e},t):t}};Ge.contextType=Nt;var pe=new WeakMap;function Gt({children:e,error:t}){let{basename:r}=c.useContext(I);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let n=Ft(t.digest);if(n){let a=pe.get(t);if(a)throw a;let u=je(n.location,r);if(Be&&!pe.get(t))if(u.isExternal||n.reloadDocument)window.location.href=u.absoluteURL||u.to;else{const s=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(u.to,{replace:n.replace}));throw pe.set(t,s),s}return c.createElement("meta",{httpEquiv:"refresh",content:`0;url=${u.absoluteURL||u.to}`})}}return e}function Vt({routeContext:e,match:t,children:r}){let n=c.useContext(G);return n&&n.static&&n.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(n.staticContext._deepestRenderedBoundaryId=t.route.id),c.createElement(Y.Provider,{value:e},r)}function Jt(e,t=[],r){let n=r==null?void 0:r.state;if(e==null){if(!n)return null;if(n.errors)e=n.matches;else if(t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let a=e,u=n==null?void 0:n.errors;if(u!=null){let f=a.findIndex(h=>h.route.id&&(u==null?void 0:u[h.route.id])!==void 0);k(f>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(u).join(",")}`),a=a.slice(0,Math.min(a.length,f+1))}let s=!1,d=-1;if(r&&n){s=n.renderFallback;for(let f=0;f<a.length;f++){let h=a[f];if((h.route.HydrateFallback||h.route.hydrateFallbackElement)&&(d=f),h.route.id){let{loaderData:y,errors:v}=n,w=h.route.loader&&!y.hasOwnProperty(h.route.id)&&(!v||v[h.route.id]===void 0);if(h.route.lazy||w){r.isStatic&&(s=!0),d>=0?a=a.slice(0,d+1):a=[a[0]];break}}}}let l=r==null?void 0:r.onError,o=n&&l?(f,h)=>{var y,v;l(f,{location:n.location,params:((v=(y=n.matches)==null?void 0:y[0])==null?void 0:v.params)??{},unstable_pattern:At(n.matches),errorInfo:h})}:void 0;return a.reduceRight((f,h,y)=>{let v,w=!1,g=null,p=null;n&&(v=u&&h.route.id?u[h.route.id]:void 0,g=h.route.errorElement||Kt,s&&(d<0&&y===0?(Ve("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),w=!0,p=null):d===y&&(w=!0,p=h.route.hydrateFallbackElement||null)));let C=t.concat(a.slice(0,y+1)),S=()=>{let x;return v?x=g:w?x=p:h.route.Component?x=c.createElement(h.route.Component,null):h.route.element?x=h.route.element:x=f,c.createElement(Vt,{match:h,routeContext:{outlet:f,matches:C,isDataRoute:n!=null},children:x})};return n&&(h.route.ErrorBoundary||h.route.errorElement||y===0)?c.createElement(Ge,{location:n.location,revalidation:n.revalidation,component:g,error:v,children:S(),routeContext:{outlet:null,matches:C,isDataRoute:!0},onError:o}):S()},null)}function Re(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Xt(e){let t=c.useContext(G);return k(t,Re(e)),t}function Qt(e){let t=c.useContext(le);return k(t,Re(e)),t}function Zt(e){let t=c.useContext(Y);return k(t,Re(e)),t}function we(e){let t=Zt(e),r=t.matches[t.matches.length-1];return k(r.route.id,`${e} can only be used on routes that contain a unique "id"`),r.route.id}function er(){return we("useRouteId")}function tr(){var n;let e=c.useContext(Ee),t=Qt("useRouteError"),r=we("useRouteError");return e!==void 0?e:(n=t.errors)==null?void 0:n[r]}function rr(){let{router:e}=Xt("useNavigate"),t=we("useNavigate"),r=c.useRef(!1);return Ke(()=>{r.current=!0}),c.useCallback(async(a,u={})=>{F(r.current,ze),r.current&&(typeof a=="number"?await e.navigate(a):await e.navigate(a,{fromRouteId:t,...u}))},[e,t])}var De={};function Ve(e,t,r){!t&&!De[e]&&(De[e]=!0,F(!1,r))}c.memo(nr);function nr({routes:e,future:t,state:r,isStatic:n,onError:a}){return qt(e,void 0,{state:r,isStatic:n,onError:a})}function ar({basename:e="/",children:t=null,location:r,navigationType:n="POP",navigator:a,static:u=!1,unstable_useTransitions:s}){k(!te(),"You cannot render a <Router> inside another <Router>. You should never have more than one in your app.");let d=e.replace(/^\/*/,"/"),l=c.useMemo(()=>({basename:d,navigator:a,static:u,unstable_useTransitions:s,future:{}}),[d,a,u,s]);typeof r=="string"&&(r=ee(r));let{pathname:o="/",search:f="",hash:h="",state:y=null,key:v="default",unstable_mask:w}=r,g=c.useMemo(()=>{let p=W(o,d);return p==null?null:{location:{pathname:p,search:f,hash:h,state:y,key:v,unstable_mask:w},navigationType:n}},[d,o,f,h,y,v,n,w]);return F(g!=null,`<Router basename="${d}"> is not able to match the URL "${o}${f}${h}" because it does not start with the basename, so the <Router> won't render anything.`),g==null?null:c.createElement(I.Provider,{value:l},c.createElement(se.Provider,{children:t,value:g}))}var oe="get",ie="application/x-www-form-urlencoded";function ce(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function or(e){return ce(e)&&e.tagName.toLowerCase()==="button"}function ir(e){return ce(e)&&e.tagName.toLowerCase()==="form"}function ur(e){return ce(e)&&e.tagName.toLowerCase()==="input"}function lr(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function sr(e,t){return e.button===0&&(!t||t==="_self")&&!lr(e)}var ae=null;function cr(){if(ae===null)try{new FormData(document.createElement("form"),0),ae=!1}catch{ae=!0}return ae}var fr=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function ye(e){return e!=null&&!fr.has(e)?(F(!1,`"${e}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` and will default to "${ie}"`),null):e}function dr(e,t){let r,n,a,u,s;if(ir(e)){let d=e.getAttribute("action");n=d?W(d,t):null,r=e.getAttribute("method")||oe,a=ye(e.getAttribute("enctype"))||ie,u=new FormData(e)}else if(or(e)||ur(e)&&(e.type==="submit"||e.type==="image")){let d=e.form;if(d==null)throw new Error('Cannot submit a <button> or <input type="submit"> without a <form>');let l=e.getAttribute("formaction")||d.getAttribute("action");if(n=l?W(l,t):null,r=e.getAttribute("formmethod")||d.getAttribute("method")||oe,a=ye(e.getAttribute("formenctype"))||ye(d.getAttribute("enctype"))||ie,u=new FormData(d,e),!cr()){let{name:o,type:f,value:h}=e;if(f==="image"){let y=o?`${o}.`:"";u.append(`${y}x`,"0"),u.append(`${y}y`,"0")}else o&&u.append(o,h)}}else{if(ce(e))throw new Error('Cannot submit element that is not <form>, <button>, or <input type="submit|image">');r=oe,n=null,a=ie,s=e}return u&&a==="text/plain"&&(s=u,u=void 0),{action:n,method:r.toLowerCase(),encType:a,formData:u,body:s}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");function Ce(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function hr(e,t,r,n){let a=typeof e=="string"?new URL(e,typeof window>"u"?"server://singlefetch/":window.location.origin):e;return r?a.pathname.endsWith("/")?a.pathname=`${a.pathname}_.${n}`:a.pathname=`${a.pathname}.${n}`:a.pathname==="/"?a.pathname=`_root.${n}`:t&&W(a.pathname,t)==="/"?a.pathname=`${t.replace(/\/$/,"")}/_root.${n}`:a.pathname=`${a.pathname.replace(/\/$/,"")}.${n}`,a}async function mr(e,t){if(e.id in t)return t[e.id];try{let r=await import(e.module);return t[e.id]=r,r}catch(r){return console.error(`Error loading route module \`${e.module}\`, reloading page...`),console.error(r),window.__reactRouterContext&&window.__reactRouterContext.isSpaMode,window.location.reload(),new Promise(()=>{})}}function pr(e){return e==null?!1:e.href==null?e.rel==="preload"&&typeof e.imageSrcSet=="string"&&typeof e.imageSizes=="string":typeof e.rel=="string"&&typeof e.href=="string"}async function yr(e,t,r){let n=await Promise.all(e.map(async a=>{let u=t.routes[a.route.id];if(u){let s=await mr(u,r);return s.links?s.links():[]}return[]}));return Rr(n.flat(1).filter(pr).filter(a=>a.rel==="stylesheet"||a.rel==="preload").map(a=>a.rel==="stylesheet"?{...a,rel:"prefetch",as:"style"}:{...a,rel:"prefetch"}))}function Ne(e,t,r,n,a,u){let s=(l,o)=>r[o]?l.route.id!==r[o].route.id:!0,d=(l,o)=>{var f;return r[o].pathname!==l.pathname||((f=r[o].route.path)==null?void 0:f.endsWith("*"))&&r[o].params["*"]!==l.params["*"]};return u==="assets"?t.filter((l,o)=>s(l,o)||d(l,o)):u==="data"?t.filter((l,o)=>{var h;let f=n.routes[l.route.id];if(!f||!f.hasLoader)return!1;if(s(l,o)||d(l,o))return!0;if(l.route.shouldRevalidate){let y=l.route.shouldRevalidate({currentUrl:new URL(a.pathname+a.search+a.hash,window.origin),currentParams:((h=r[0])==null?void 0:h.params)||{},nextUrl:new URL(e,window.origin),nextParams:l.params,defaultShouldRevalidate:!0});if(typeof y=="boolean")return y}return!0}):[]}function gr(e,t,{includeHydrateFallback:r}={}){return vr(e.map(n=>{let a=t.routes[n.route.id];if(!a)return[];let u=[a.module];return a.clientActionModule&&(u=u.concat(a.clientActionModule)),a.clientLoaderModule&&(u=u.concat(a.clientLoaderModule)),r&&a.hydrateFallbackModule&&(u=u.concat(a.hydrateFallbackModule)),a.imports&&(u=u.concat(a.imports)),u}).flat(1))}function vr(e){return[...new Set(e)]}function Er(e){let t={},r=Object.keys(e).sort();for(let n of r)t[n]=e[n];return t}function Rr(e,t){let r=new Set;return new Set(t),e.reduce((n,a)=>{let u=JSON.stringify(Er(a));return r.has(u)||(r.add(u),n.push({key:u,link:a})),n},[])}function Je(){let e=c.useContext(G);return Ce(e,"You must render this element inside a <DataRouterContext.Provider> element"),e}function wr(){let e=c.useContext(le);return Ce(e,"You must render this element inside a <DataRouterStateContext.Provider> element"),e}var _e=c.createContext(void 0);_e.displayName="FrameworkContext";function Xe(){let e=c.useContext(_e);return Ce(e,"You must render this element inside a <HydratedRouter> element"),e}function Cr(e,t){let r=c.useContext(_e),[n,a]=c.useState(!1),[u,s]=c.useState(!1),{onFocus:d,onBlur:l,onMouseEnter:o,onMouseLeave:f,onTouchStart:h}=t,y=c.useRef(null);c.useEffect(()=>{if(e==="render"&&s(!0),e==="viewport"){let g=C=>{C.forEach(S=>{s(S.isIntersecting)})},p=new IntersectionObserver(g,{threshold:.5});return y.current&&p.observe(y.current),()=>{p.disconnect()}}},[e]),c.useEffect(()=>{if(n){let g=setTimeout(()=>{s(!0)},100);return()=>{clearTimeout(g)}}},[n]);let v=()=>{a(!0)},w=()=>{a(!1),s(!1)};return r?e!=="intent"?[u,y,{}]:[u,y,{onFocus:Q(d,v),onBlur:Q(l,w),onMouseEnter:Q(o,v),onMouseLeave:Q(f,w),onTouchStart:Q(h,v)}]:[!1,y,{}]}function Q(e,t){return r=>{e&&e(r),r.defaultPrevented||t(r)}}function _r({page:e,...t}){let{router:r}=Je(),n=c.useMemo(()=>Me(r.routes,e,r.basename),[r.routes,e,r.basename]);return n?c.createElement(xr,{page:e,matches:n,...t}):null}function Sr(e){let{manifest:t,routeModules:r}=Xe(),[n,a]=c.useState([]);return c.useEffect(()=>{let u=!1;return yr(e,t,r).then(s=>{u||a(s)}),()=>{u=!0}},[e,t,r]),n}function xr({page:e,matches:t,...r}){let n=q(),{future:a,manifest:u,routeModules:s}=Xe(),{basename:d}=Je(),{loaderData:l,matches:o}=wr(),f=c.useMemo(()=>Ne(e,t,o,u,n,"data"),[e,t,o,u,n]),h=c.useMemo(()=>Ne(e,t,o,u,n,"assets"),[e,t,o,u,n]),y=c.useMemo(()=>{if(e===n.pathname+n.search+n.hash)return[];let g=new Set,p=!1;if(t.forEach(S=>{var $;let x=u.routes[S.route.id];!x||!x.hasLoader||(!f.some(O=>O.route.id===S.route.id)&&S.route.id in l&&(($=s[S.route.id])!=null&&$.shouldRevalidate)||x.hasClientLoader?p=!0:g.add(S.route.id))}),g.size===0)return[];let C=hr(e,d,a.unstable_trailingSlashAwareDataRequests,"data");return p&&g.size>0&&C.searchParams.set("_routes",t.filter(S=>g.has(S.route.id)).map(S=>S.route.id).join(",")),[C.pathname+C.search]},[d,a.unstable_trailingSlashAwareDataRequests,l,n,u,f,t,e,s]),v=c.useMemo(()=>gr(h,u),[h,u]),w=Sr(h);return c.createElement(c.Fragment,null,y.map(g=>c.createElement("link",{key:g,rel:"prefetch",as:"fetch",href:g,...r})),v.map(g=>c.createElement("link",{key:g,rel:"modulepreload",href:g,...r})),w.map(({key:g,link:p})=>c.createElement("link",{key:g,nonce:r.nonce,...p,crossOrigin:p.crossOrigin??r.crossOrigin})))}function Pr(...e){return t=>{e.forEach(r=>{typeof r=="function"?r(t):r!=null&&(r.current=t)})}}var Tr=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";try{Tr&&(window.__reactRouterVersion="7.13.2")}catch{}function Hr({basename:e,children:t,unstable_useTransitions:r,window:n}){let a=c.useRef();a.current==null&&(a.current=lt({window:n,v5Compat:!0}));let u=a.current,[s,d]=c.useState({action:u.action,location:u.location}),l=c.useCallback(o=>{r===!1?d(o):c.startTransition(()=>d(o))},[r]);return c.useLayoutEffect(()=>u.listen(l),[u,l]),c.createElement(ar,{basename:e,children:t,location:s.location,navigationType:s.action,navigator:u,unstable_useTransitions:r})}var Qe=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Ze=c.forwardRef(function({onClick:t,discover:r="render",prefetch:n="none",relative:a,reloadDocument:u,replace:s,unstable_mask:d,state:l,target:o,to:f,preventScrollReset:h,viewTransition:y,unstable_defaultShouldRevalidate:v,...w},g){let{basename:p,navigator:C,unstable_useTransitions:S}=c.useContext(I),x=typeof f=="string"&&Qe.test(f),$=je(f,p);f=$.to;let O=jt(f,{relative:a}),D=q(),P=null;if(d){let M=ve(d,[],D.unstable_mask?D.unstable_mask.pathname:"/",!0);p!=="/"&&(M.pathname=M.pathname==="/"?p:U([p,M.pathname])),P=C.createHref(M)}let[B,j,V]=Cr(n,w),J=$r(f,{replace:s,unstable_mask:d,state:l,target:o,preventScrollReset:h,relative:a,viewTransition:y,unstable_defaultShouldRevalidate:v,unstable_useTransitions:S});function fe(M){t&&t(M),M.defaultPrevented||J(M)}let X=!($.isExternal||u),z=c.createElement("a",{...w,...V,href:(X?P:void 0)||$.absoluteURL||O,onClick:X?fe:t,ref:Pr(g,j),target:o,"data-discover":!x&&r==="render"?"true":void 0});return B&&!x?c.createElement(c.Fragment,null,z,c.createElement(_r,{page:O})):z});Ze.displayName="Link";var br=c.forwardRef(function({"aria-current":t="page",caseSensitive:r=!1,className:n="",end:a=!1,style:u,to:s,viewTransition:d,children:l,...o},f){let h=re(s,{relative:o.relative}),y=q(),v=c.useContext(le),{navigator:w,basename:g}=c.useContext(I),p=v!=null&&Ir(h)&&d===!0,C=w.encodeLocation?w.encodeLocation(h).pathname:h.pathname,S=y.pathname,x=v&&v.navigation&&v.navigation.location?v.navigation.location.pathname:null;r||(S=S.toLowerCase(),x=x?x.toLowerCase():null,C=C.toLowerCase()),x&&g&&(x=W(x,g)||x);const $=C!=="/"&&C.endsWith("/")?C.length-1:C.length;let O=S===C||!a&&S.startsWith(C)&&S.charAt($)==="/",D=x!=null&&(x===C||!a&&x.startsWith(C)&&x.charAt(C.length)==="/"),P={isActive:O,isPending:D,isTransitioning:p},B=O?t:void 0,j;typeof n=="function"?j=n(P):j=[n,O?"active":null,D?"pending":null,p?"transitioning":null].filter(Boolean).join(" ");let V=typeof u=="function"?u(P):u;return c.createElement(Ze,{...o,"aria-current":B,className:j,ref:f,style:V,to:s,viewTransition:d},typeof l=="function"?l(P):l)});br.displayName="NavLink";var Lr=c.forwardRef(({discover:e="render",fetcherKey:t,navigate:r,reloadDocument:n,replace:a,state:u,method:s=oe,action:d,onSubmit:l,relative:o,preventScrollReset:f,viewTransition:h,unstable_defaultShouldRevalidate:y,...v},w)=>{let{unstable_useTransitions:g}=c.useContext(I),p=Dr(),C=Nr(d,{relative:o}),S=s.toLowerCase()==="get"?"get":"post",x=typeof d=="string"&&Qe.test(d),$=O=>{if(l&&l(O),O.defaultPrevented)return;O.preventDefault();let D=O.nativeEvent.submitter,P=(D==null?void 0:D.getAttribute("formmethod"))||s,B=()=>p(D||O.currentTarget,{fetcherKey:t,method:P,navigate:r,replace:a,state:u,relative:o,preventScrollReset:f,viewTransition:h,unstable_defaultShouldRevalidate:y});g&&r!==!1?c.startTransition(()=>B()):B()};return c.createElement("form",{ref:w,method:S,action:C,onSubmit:n?l:$,...v,"data-discover":!x&&e==="render"?"true":void 0})});Lr.displayName="Form";function Or(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function et(e){let t=c.useContext(G);return k(t,Or(e)),t}function $r(e,{target:t,replace:r,unstable_mask:n,state:a,preventScrollReset:u,relative:s,viewTransition:d,unstable_defaultShouldRevalidate:l,unstable_useTransitions:o}={}){let f=Wt(),h=q(),y=re(e,{relative:s});return c.useCallback(v=>{if(sr(v,t)){v.preventDefault();let w=r!==void 0?r:Z(h)===Z(y),g=()=>f(e,{replace:w,unstable_mask:n,state:a,preventScrollReset:u,relative:s,viewTransition:d,unstable_defaultShouldRevalidate:l});o?c.startTransition(()=>g()):g()}},[h,f,y,r,n,a,t,e,u,s,d,l,o])}var kr=0,Ar=()=>`__${String(++kr)}__`;function Dr(){let{router:e}=et("useSubmit"),{basename:t}=c.useContext(I),r=er(),n=e.fetch,a=e.navigate;return c.useCallback(async(u,s={})=>{let{action:d,method:l,encType:o,formData:f,body:h}=dr(u,t);if(s.navigate===!1){let y=s.fetcherKey||Ar();await n(y,r,s.action||d,{unstable_defaultShouldRevalidate:s.unstable_defaultShouldRevalidate,preventScrollReset:s.preventScrollReset,formData:f,body:h,formMethod:s.method||l,formEncType:s.encType||o,flushSync:s.flushSync})}else await a(s.action||d,{unstable_defaultShouldRevalidate:s.unstable_defaultShouldRevalidate,preventScrollReset:s.preventScrollReset,formData:f,body:h,formMethod:s.method||l,formEncType:s.encType||o,replace:s.replace,state:s.state,fromRouteId:r,flushSync:s.flushSync,viewTransition:s.viewTransition})},[n,a,t,r])}function Nr(e,{relative:t}={}){let{basename:r}=c.useContext(I),n=c.useContext(Y);k(n,"useFormAction must be used inside a RouteContext");let[a]=n.matches.slice(-1),u={...re(e||".",{relative:t})},s=q();if(e==null){u.search=s.search;let d=new URLSearchParams(u.search),l=d.getAll("index");if(l.some(f=>f==="")){d.delete("index"),l.filter(h=>h).forEach(h=>d.append("index",h));let f=d.toString();u.search=f?`?${f}`:""}}return(!e||e===".")&&a.route.index&&(u.search=u.search?u.search.replace(/^\?/,"?index&"):"?index"),r!=="/"&&(u.pathname=u.pathname==="/"?r:U([r,u.pathname])),Z(u)}function Ir(e,{relative:t}={}){let r=c.useContext(Ye);k(r!=null,"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. Did you accidentally import `RouterProvider` from `react-router`?");let{basename:n}=et("useViewTransitionState"),a=re(e,{relative:t});if(!r.isTransitioning)return!1;let u=W(r.currentLocation.pathname,n)||r.currentLocation.pathname,s=W(r.nextLocation.pathname,n)||r.nextLocation.pathname;return ue(a.pathname,s)!=null||ue(a.pathname,u)!=null}var Ur=ut();export{Hr as B,Mr as R,ut as a,c as b,Ur as c,at as g,Ie as r};

Sorry, the diff of this file is too big to display

/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
import{j as c}from"./markdown-v0F0UEt3.js";import{b as o}from"./react-CSS0HapR.js";import{S as m}from"./ScenarioDataPanel-CxzyVTy4.js";import"./useEvents-DggWrtHe.js";import"./xterm--24IGk-x.js";import"./index-DjKWfP5a.js";function S({slug:t}){const[r,i]=o.useState(void 0);return o.useEffect(()=>{let n=!1;return fetch(`/api/scenarios/${encodeURIComponent(t)}`).then(e=>e.ok?e.json():null).then(e=>{if(n||!e)return;const a=typeof e=="object"&&e!==null&&"name"in e?String(e.name):void 0;i(a),a&&(document.title=`${a} · data`)}).catch(()=>{}),()=>{n=!0}},[t]),c.jsx(m,{slug:t,scenarioName:r,variant:"fullPage"})}export{S as ScenarioDataPanelFullPage};

Sorry, the diff of this file is too big to display

import{j as e}from"./markdown-v0F0UEt3.js";import{b as d}from"./react-CSS0HapR.js";import{M as I}from"./Markdown-BHBqz-Mj.js";function E({workflow:n,selectedSlug:r,onSelectSlug:t}){return e.jsx("div",{style:{padding:"4px 8px 16px",flex:1},children:n.steps.map(o=>o.kind==="slug"?e.jsx(k,{position:o.position,slug:o.slug,label:o.label,description:o.description,isSelected:r===o.slug,onSelect:()=>t(o.slug)},`slug-${o.position}-${o.slug}`):e.jsx(D,{position:o.position,ifKey:o.ifKey,arms:o.arms,selectedSlug:r,onSelectSlug:t},`branch-${o.position}`))})}function k({position:n,slug:r,label:t,description:o,isSelected:s,onSelect:i,indent:l=0,branchHint:a}){return e.jsxs("button",{onClick:i,style:{display:"flex",alignItems:"flex-start",gap:8,padding:"7px 8px",paddingLeft:8+l,borderRadius:4,border:"none",cursor:"pointer",textAlign:"left",fontSize:13,width:"100%",background:s?"var(--bg-input)":"transparent",color:s?"var(--accent-active)":"var(--text-secondary)",fontWeight:s?600:400},children:[e.jsx("span",{style:{width:22,textAlign:"right",color:"var(--text-muted)",fontSize:11,fontFamily:"'IBM Plex Mono', monospace",flexShrink:0,paddingTop:1},children:n}),e.jsxs("span",{style:{flex:1,display:"flex",flexDirection:"column"},children:[e.jsxs("span",{children:[t,a&&e.jsx("span",{style:{marginLeft:6,fontSize:10,color:"var(--text-dim)",fontFamily:"'IBM Plex Mono', monospace"},children:a})]}),e.jsx("span",{style:{fontSize:11,color:o?"var(--text-muted)":"var(--accent-error, #d44)",marginTop:2,fontWeight:400},children:o??"(missing description)"}),e.jsx("span",{style:{fontSize:10,color:"var(--text-dim)",marginTop:1,fontFamily:"'IBM Plex Mono', monospace"},children:r})]})]})}function D({position:n,ifKey:r,arms:t,selectedSlug:o,onSelectSlug:s}){return e.jsxs("div",{children:[e.jsxs("div",{style:{padding:"4px 8px",fontSize:11,color:"var(--text-dim)",fontStyle:"italic"},children:["Branch on ",e.jsx("code",{children:r})," (position ",n,")"]}),t.map(i=>e.jsx(k,{position:n,slug:i.slug,label:i.label,description:i.description,isSelected:o===i.slug,onSelect:()=>s(i.slug),indent:20,branchHint:`(${i.outcome})`},`${n}-${i.slug}`))]})}const F=["provider","feature","dimension","screenSizes","hasProject","isResuming","designSystem","stack","needsScaffold","projectDescription","startCommand","hasGlossary","hasUserPrompt","selectedPlan","needsInitialStartingSurface","port","controlPort"],L={provider:"Provider",feature:"Feature Name",dimension:"Dimension",screenSizes:"Screen Sizes",hasProject:"Has Project",isResuming:"Is Resuming",designSystem:"Design System",stack:"Stack Config",needsScaffold:"Needs Scaffold",projectDescription:"Project Description",startCommand:"Start Command",hasGlossary:"Has Glossary",hasUserPrompt:"Has User Prompt",selectedPlan:"Selected Plan",needsInitialStartingSurface:"Needs Initial Starting Surface",port:"Port",controlPort:"Control Port"};function V({visibleVariables:n,vars:r,onVarChange:t}){return n.length===0?e.jsx("div",{style:{padding:"12px 20px",borderBottom:"1px solid var(--border-default)",fontSize:12,color:"var(--text-muted)",fontStyle:"italic"},children:"This step has no input variables — its rendered output is the same in every context."}):e.jsx("div",{style:{padding:"12px 20px",borderBottom:"1px solid var(--border-default)",display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:12},children:n.filter(o=>F.includes(o)).map(o=>e.jsx(A,{fieldKey:o,value:r[o],onChange:s=>t(o,s)},o))})}function A({fieldKey:n,value:r,onChange:t}){const o=L[n],s=typeof r=="boolean",i=typeof r=="number",l=Array.isArray(r);return e.jsxs("label",{style:{display:"flex",flexDirection:"column",gap:4,fontSize:11,color:"var(--text-muted)"},children:[e.jsx("span",{children:o}),s?e.jsx("input",{type:"checkbox",checked:r,onChange:a=>t(a.target.checked),style:{alignSelf:"flex-start"}}):i?e.jsx("input",{type:"number",value:r,onChange:a=>t(Number(a.target.value)),style:y()}):l?e.jsx("input",{type:"text",value:r.join(", "),onChange:a=>t(a.target.value.split(",").map(f=>f.trim()).filter(Boolean)),placeholder:"comma-separated",style:y()}):e.jsx("input",{type:"text",value:typeof r=="string"?r:"",onChange:a=>t(a.target.value||null),placeholder:"(unset)",style:y()})]})}function y(){return{background:"var(--bg-input)",border:"1px solid var(--border-default)",borderRadius:3,padding:"4px 6px",fontSize:12,color:"var(--text-primary)",fontFamily:"'IBM Plex Mono', monospace"}}function W({rendered:n,renderError:r,renderLoading:t,selectedSlug:o,selectedDescription:s,visibleVariables:i,vars:l,onVarChange:a}){return e.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",overflow:"hidden"},children:[e.jsx(T,{rendered:n,selectedSlug:o,selectedDescription:s}),e.jsx(V,{visibleVariables:i,vars:l,onVarChange:a}),e.jsx($,{rendered:n,renderError:r,renderLoading:t})]})}function T({rendered:n,selectedSlug:r,selectedDescription:t}){return e.jsxs("div",{style:{padding:"16px 20px 12px",borderBottom:"1px solid var(--border-default)"},children:[e.jsx("div",{style:{fontSize:11,color:"var(--text-dim)",fontFamily:"'IBM Plex Mono', monospace",marginBottom:4},children:r??"no step selected"}),e.jsx("h2",{style:{fontSize:18,fontWeight:600,color:"var(--text-primary)",margin:0},children:(n==null?void 0:n.label)??"—"}),e.jsx("p",{style:{fontSize:13,color:t?"var(--text-muted)":"var(--accent-error, #d44)",margin:"6px 0 0"},children:t??"(missing description)"})]})}function $({rendered:n,renderError:r,renderLoading:t}){return e.jsx("div",{style:{flex:1,overflow:"auto",padding:"16px 20px"},children:r?e.jsx(S,{message:r,kind:"render"}):n?e.jsx("div",{style:{opacity:t?.6:1},children:e.jsx(I,{children:n.instructions})}):e.jsx("div",{style:{color:"var(--text-muted)",fontSize:13},children:t?"Rendering…":"Select a step."})})}function N(){return{provider:"claude",port:3e3,controlPort:14199,dimension:"Desktop",screenSizes:["Desktop"],hasProject:!1,isResuming:!1,needsScaffold:!1,startCommand:"npm run dev",hasGlossary:!1,hasUserPrompt:!1,selectedPlan:null,needsInitialStartingSurface:!1}}function _(){const[n,r]=d.useState("ui"),[t,o]=d.useState(null),[s,i]=d.useState(null),[l,a]=d.useState(null),[f,P]=d.useState(N),[w,g]=d.useState(null),[B,m]=d.useState(null),[z,b]=d.useState(!1),v=d.useCallback(async x=>{i(null),o(null),g(null),m(null),a(null);try{const u=await(await fetch(`/api/workflow?mode=${x}`)).json();if("error"in u){i(u.error);return}o(u);const p=H(u);a(p)}catch(c){i(c instanceof Error?c.message:String(c))}},[]);d.useEffect(()=>{v(n)},[n,v]);const j=d.useCallback(async(x,c,u)=>{b(!0),m(null);try{const h=await(await fetch("/api/workflow/render",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({slug:x,mode:c,vars:u})})).json();if("error"in h){g(null),m(h.error);return}g(h)}catch(p){g(null),m(p instanceof Error?p.message:String(p))}finally{b(!1)}},[]);d.useEffect(()=>{l&&j(l,n,f)},[l,n,f,j]);const C=l?O(t,l):null,M=t&&l?U(t,l):[],R=t?[...M,...t.globalVariables??[]]:[];return e.jsxs("div",{style:{display:"flex",height:"100vh",background:"var(--bg-deep)",color:"var(--text-secondary)",fontFamily:"'IBM Plex Sans', system-ui, sans-serif"},children:[e.jsx(G,{mode:n,workflow:t,loadError:s,selectedSlug:l,onModeChange:r,onSelectSlug:a}),e.jsx(W,{rendered:w,renderError:B,renderLoading:z,selectedSlug:l,selectedDescription:C,visibleVariables:R,vars:f,onVarChange:(x,c)=>P(u=>({...u,[x]:c}))})]})}function H(n){for(const r of n.steps){if(r.kind==="slug")return r.slug;if(r.kind==="branch"&&r.arms.length>0)return r.arms[0].slug}return null}function O(n,r){if(!n)return null;for(const t of n.steps){if(t.kind==="slug"&&t.slug===r)return t.description;if(t.kind==="branch"){const o=t.arms.find(s=>s.slug===r);if(o)return o.description}}return null}function U(n,r){for(const t of n.steps){if(t.kind==="slug"&&t.slug===r)return t.variables??[];if(t.kind==="branch"){const o=t.arms.find(s=>s.slug===r);if(o)return o.variables??[]}}return[]}function G({mode:n,workflow:r,loadError:t,selectedSlug:o,onModeChange:s,onSelectSlug:i}){return e.jsxs("div",{style:{width:360,minWidth:360,borderRight:"1px solid var(--border-default)",display:"flex",flexDirection:"column",overflow:"auto"},children:[e.jsx(K,{}),e.jsx(J,{mode:n,onChange:s}),t?e.jsx(S,{message:t,kind:"load"}):r?e.jsx(E,{workflow:r,selectedSlug:o,onSelectSlug:i}):e.jsx("div",{style:{padding:16,color:"var(--text-muted)"},children:"Loading…"})]})}function K(){return e.jsxs("div",{style:{padding:"16px 16px 12px",borderBottom:"1px solid var(--border-default)"},children:[e.jsx("h1",{style:{fontSize:16,fontWeight:600,color:"var(--text-primary)",margin:0},children:"Workflow Viewer"}),e.jsx("p",{style:{fontSize:12,color:"var(--text-muted)",margin:"4px 0 0"},children:"Live data from the server. Click a step to see its instructions and edit variables to see how the rendered text changes."})]})}function J({mode:n,onChange:r}){return e.jsx("div",{style:{padding:"12px 16px 8px"},children:e.jsx("div",{style:{display:"flex",gap:4},children:["ui","backend"].map(t=>e.jsx("button",{onClick:()=>r(t),style:{padding:"5px 12px",borderRadius:4,border:"none",cursor:"pointer",fontSize:12,fontWeight:500,flex:1,background:n===t?"var(--accent-active)":"var(--bg-input)",color:n===t?"#000":"var(--text-secondary)"},children:t==="ui"?"UI":"Backend"},t))})})}function S({message:n,kind:r}){return e.jsxs("div",{style:{margin:16,padding:12,borderRadius:4,background:"rgba(220, 60, 60, 0.1)",border:"1px solid rgba(220, 60, 60, 0.3)",color:"#f99",fontSize:12,fontFamily:"'IBM Plex Mono', monospace",whiteSpace:"pre-wrap"},children:[e.jsx("div",{style:{fontWeight:600,marginBottom:4},children:r==="load"?"Failed to load workflow":"Failed to render step"}),n]})}const X=Object.freeze(Object.defineProperty({__proto__:null,ErrorBanner:S,StepsSimulator:_},Symbol.toStringTag,{value:"Module"}));export{W as S,V as a,E as b,_ as c,X as d};
import{b as t}from"./react-CSS0HapR.js";const m=["http_request","db_query","process_output","scenario_switch","health_check","hook_execution","error"],h=new Set(m);function T(c){if(!c)return null;try{const n=JSON.parse(c);return typeof n!="object"||n===null||typeof n.type!="string"||!h.has(n.type)?null:n}catch{return null}}const v=1e3,d=2e3,y=3e4;function C(c="/api/events"){const[n,f]=t.useState([]),[p,E]=t.useState(!1),e=t.useRef(null),o=t.useRef(null),l=t.useRef(0),a=t.useCallback(()=>{e.current&&(e.current.close(),e.current=null);const s=new EventSource(c);e.current=s,s.onopen=()=>{E(!0),l.current=0},s.onmessage=r=>{const u=T(r.data);u&&f(S=>{const i=[...S,u];return i.length>v?i.slice(-500):i})},s.onerror=()=>{E(!1),s.close(),e.current=null,l.current+=1;const r=l.current,u=Math.min(d*2**(r-1),y);(r===1||r%5===0)&&console.warn(`[events] SSE disconnected (attempt #${r}); reconnecting in ${u}ms`),o.current=setTimeout(a,u)}},[c]);t.useEffect(()=>(a(),()=>{e.current&&(e.current.close(),e.current=null),o.current&&(clearTimeout(o.current),o.current=null)}),[a]);const _=t.useCallback(()=>f([]),[]);return{events:n,connected:p,clear:_}}export{C as u};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeYam</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script type="module" crossorigin src="/assets/index-DjKWfP5a.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-CSS0HapR.js">
<link rel="modulepreload" crossorigin href="/assets/markdown-v0F0UEt3.js">
<link rel="stylesheet" crossorigin href="/assets/index-Dnu7mncO.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+5
-2
#!/usr/bin/env node
const { spawn } = require("child_process");
const { ensureBinary, uiDistDir } = require("./utils");
const { ensureBinary } = require("./utils");
const binary = ensureBinary();
// The Rust binary embeds `ui/dist` at build time, so no `CODEYAM_EDITOR_UI_DIR`
// is required. Devs who want to override the bundle can still export the env
// var manually — the binary's resolver checks it first.
const child = spawn(binary, process.argv.slice(2), {
stdio: "inherit",
cwd: process.cwd(),
env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
env: process.env,
});

@@ -13,0 +16,0 @@

@@ -11,8 +11,22 @@ #!/usr/bin/env node

const { ensureServer, rootDir } = require("./utils");
const {
ensureServer,
rootDir,
killChildProcess,
readMergedEditorConfig,
resolveLauncherPort,
} = require("./utils");
const { open } = require("./open");
const PORT = 14199;
async function main() {
// Same resolution order as the editor launcher: env var
// (CODEYAM_EDITOR_PORT) → merged editor.json + editor.local.json
// (proxy.controlPort) → hard fallback (14199).
const merged = readMergedEditorConfig(rootDir);
const PORT = resolveLauncherPort(
process.env.CODEYAM_EDITOR_PORT,
merged.proxy && merged.proxy.controlPort,
14199,
);
async function main() {
const serverChild = await ensureServer(PORT);

@@ -27,3 +41,3 @@ const url = `http://localhost:${PORT}`;

clearInterval(keepAlive);
if (serverChild) serverChild.kill("SIGINT");
killChildProcess(serverChild);
process.exit(0);

@@ -33,2 +47,6 @@ });

main();
module.exports = { main };
if (require.main === module) {
main();
}

@@ -21,4 +21,12 @@ #!/usr/bin/env node

const path = require("path");
const fs = require("fs");
const { ensureServer, restartServer, waitForPort, ensureBinary, uiDistDir, rootDir } = require("./utils");
const {
ensureServer,
restartServer,
waitForPort,
ensureBinary,
rootDir,
killChildProcess,
readMergedEditorConfig,
resolveLauncherPort,
} = require("./utils");
const { open } = require("./open");

@@ -33,5 +41,9 @@

/** Check if the first non-flag argument is a known subcommand. */
function hasSubcommand() {
for (const arg of args) {
/**
* Check if the first non-flag argument is a known subcommand.
* Accepts an explicit argv array so the helper is testable without mutating
* process.argv (the module-level `args` const is the production default).
*/
function hasSubcommand(argv = args) {
for (const arg of argv) {
if (arg.startsWith("-")) continue;

@@ -53,71 +65,81 @@ return KNOWN_SUBCOMMANDS.includes(arg);

// --- Passthrough mode: build binary, then forward to it ---
if (hasSubcommand()) {
buildBinary();
const binary = ensureBinary();
const child = spawn(binary, args, {
stdio: "inherit",
cwd: process.cwd(),
env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
});
child.on("exit", (code) => {
process.exit(code ?? 1);
});
} else {
// --- Launcher mode: build everything, start server, open browser ---
const SERVER_PORT = 14199;
const shouldRestart = args.includes("--restart");
module.exports = { hasSubcommand, buildBinary };
function readProjectPort() {
try {
const configPath = path.join(process.cwd(), ".codeyam", "editor.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
return config.port || 3000;
} catch {
return 3000;
}
}
if (require.main === module) {
// --- Passthrough mode: build binary, then forward to it ---
if (hasSubcommand()) {
buildBinary();
const binary = ensureBinary();
const child = spawn(binary, args, {
stdio: "inherit",
cwd: process.cwd(),
env: process.env,
});
child.on("exit", (code) => {
process.exit(code ?? 1);
});
} else {
// --- Launcher mode: build everything, start server, open browser ---
const shouldRestart = args.includes("--restart");
async function main() {
const projectDir = process.cwd();
console.error(`Editor repo: ${rootDir}`);
console.error(`Target project: ${projectDir}`);
const launcherMain = async () => {
const projectDir = process.cwd();
console.error(`Editor repo: ${rootDir}`);
console.error(`Target project: ${projectDir}`);
buildBinary();
// Editor server's control port is owned by the editor repo's
// merged config (rootDir), not the target project's. The target
// project's own `editor.local.json` only contributes to the
// app/dev-server port below.
const editorMerged = readMergedEditorConfig(rootDir);
const SERVER_PORT = resolveLauncherPort(
process.env.CODEYAM_EDITOR_PORT,
editorMerged.proxy && editorMerged.proxy.controlPort,
14199,
);
// Build the UI from the editor repo
console.error("Building UI...");
try {
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
} catch {
console.error("Failed to build UI. Continuing with existing build (if any)...");
}
// Target project's dev-server port comes from its own
// editor.json + editor.local.json (developers iterating on the
// editor against a real client project rely on the override
// file here too).
const projectMerged = readMergedEditorConfig(projectDir);
const appPort = resolveLauncherPort(undefined, projectMerged.port, 3000);
const serverChild = shouldRestart
? await restartServer(SERVER_PORT)
: await ensureServer(SERVER_PORT);
buildBinary();
const appPort = readProjectPort();
const viteInternalPort = appPort + 1;
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
if (!viteReady) {
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
}
// Build the UI from the editor repo
console.error("Building UI...");
try {
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
} catch {
console.error("Failed to build UI. Continuing with existing build (if any)...");
}
const url = `http://localhost:${SERVER_PORT}`;
console.error(`Editor UI: ${url}`);
console.error(`Live Preview: http://localhost:${appPort}`);
console.error(`Project: ${projectDir}`);
open(url);
const keepAlive = setInterval(() => {}, 60_000);
const serverChild = shouldRestart
? await restartServer(SERVER_PORT)
: await ensureServer(SERVER_PORT);
process.on("SIGINT", () => {
clearInterval(keepAlive);
if (serverChild) serverChild.kill("SIGINT");
process.exit(0);
});
const viteInternalPort = appPort + 1;
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
if (!viteReady) {
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
}
const url = `http://localhost:${SERVER_PORT}`;
console.error(`Editor UI: ${url}`);
console.error(`Live Preview: http://localhost:${appPort}`);
console.error(`Project: ${projectDir}`);
open(url);
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => {
clearInterval(keepAlive);
killChildProcess(serverChild);
process.exit(0);
});
};
launcherMain();
}
main();
}

@@ -16,14 +16,124 @@ #!/usr/bin/env node

const { execSync } = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { ensureServer, restartServer, waitForPort, rootDir } = require("./utils");
const {
ensureServer,
stopServer,
waitForPort,
rootDir,
killChildProcess,
readMergedEditorConfig,
resolveLauncherPort,
classifyPortHolder,
formatForeignListenerError,
formatCrossProjectError,
} = require("./utils");
const { open } = require("./open");
const SERVER_PORT = 14199;
const VITE_PORT = 5173;
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
const VITE_INTERNAL_PORT = VITE_PORT + 1;
const shouldRestart = process.argv.includes("--restart");
function buildBinary() {
console.error("Building Rust binary...");
try {
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
return true;
} catch {
console.error("Failed to build Rust binary. Continuing with existing build (if any)...");
return false;
}
}
// Resolve <CARGO_HOME>/bin (falling back to $HOME/.cargo/bin). Mirrors
// `installed_binary_path()` in crates/control-api/src/pty_broker_startup.rs
// — the Rust broker auto-spawn execs the binary at this path, so we MUST
// keep it in sync with the freshly-built debug binary or `npm run editor`
// will start a server whose PTY-broker auto-spawn execs a stale install.
function cargoBinDir() {
if (process.env.CARGO_HOME) return path.join(process.env.CARGO_HOME, "bin");
return path.join(os.homedir(), ".cargo", "bin");
}
// Install target/debug/codeyam-editor to <cargo_bin>/codeyam-editor
// atomically (write to tmp in same dir, then rename), and refresh the
// codeyam-editor-pty-broker hardlink so it points at the new inode.
//
// The PTY broker auto-spawn (control-api/src/pty_broker_startup.rs) always
// resolves the *installed* path, NOT current_exe() — that was a deliberate
// fix to prevent a fork-bomb where test binaries spawned themselves. So a
// fresh `target/debug/codeyam-editor` is not enough; the install path must
// be refreshed too. Without this, build tabs hang on "Reconnecting..."
// because the broker daemon errors out (`unrecognized subcommand`) before
// the UDS socket opens.
function installBinary() {
const srcPath = path.join(rootDir, "target", "debug", "codeyam-editor");
if (!fs.existsSync(srcPath)) {
console.error(`Skipping install: ${srcPath} not found.`);
return;
}
const binDir = cargoBinDir();
const dstPath = path.join(binDir, "codeyam-editor");
const linkPath = path.join(binDir, "codeyam-editor-pty-broker");
const tmpPath = path.join(binDir, `.codeyam-editor.tmp.${process.pid}`);
console.error(`Installing fresh binary to ${dstPath}...`);
try {
fs.mkdirSync(binDir, { recursive: true });
fs.copyFileSync(srcPath, tmpPath);
fs.chmodSync(tmpPath, 0o755);
fs.renameSync(tmpPath, dstPath);
try {
fs.unlinkSync(linkPath);
} catch (e) {
if (e.code !== "ENOENT") throw e;
}
fs.linkSync(dstPath, linkPath);
} catch (err) {
console.error(`Failed to install binary: ${err.message}`);
try { fs.unlinkSync(tmpPath); } catch {}
}
}
// Evict Rust build artifacts not touched in the last day so target/ doesn't
// grow unboundedly. Must run after the old server is stopped (it holds files
// in target/debug/ open) and before the new server starts. Non-fatal.
function sweepStaleArtifacts() {
console.error("Sweeping stale Rust build artifacts (idle >1 day)...");
try {
execSync("cargo sweep --time 1", { stdio: "inherit", cwd: rootDir });
} catch {
console.error("cargo-sweep failed (install with `cargo install cargo-sweep`). Continuing.");
}
}
async function main() {
// Read ports from the merged editor config (editor.json + per-developer
// editor.local.json). The Rust backend reads the same merged config on
// its own — we resolve here so the launcher's log lines, `waitForPort`
// calls, and `--port` arg agree with what the binary will actually
// bind. Env vars still win for scripted/CI invocations
// (CODEYAM_EDITOR_PORT, PORT), then the merged config, then a hard
// fallback for fresh checkouts with no override file.
const merged = readMergedEditorConfig(rootDir);
const SERVER_PORT = resolveLauncherPort(
process.env.CODEYAM_EDITOR_PORT,
merged.proxy && merged.proxy.controlPort,
14199,
);
const VITE_PORT = resolveLauncherPort(process.env.PORT, merged.port, 5173);
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
const VITE_INTERNAL_PORT = VITE_PORT + 1;
// Rebuild the Rust binary so code changes are picked up on restart.
// This script is only invoked from a source checkout (via `npm run editor`),
// so Cargo.toml and the crates tree are always present.
const buildOk = buildBinary();
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
// the broker auto-spawn execs a binary that knows the `pty-broker
// daemon` subcommand. Skip if the build failed — installing a stale
// debug binary over a working install is worse than leaving it.
if (buildOk) {
installBinary();
}
// Build the UI so the backend can serve it as static files

@@ -41,6 +151,29 @@ console.error("Building UI...");

// Start the Rust backend — it serves ui/dist/ and launches the Vite dev
// server (configured via startCommand in editor.json) for Live Preview HMR
const serverChild = shouldRestart
? await restartServer(SERVER_PORT)
: await ensureServer(SERVER_PORT);
// server (configured via startCommand in editor.json) for Live Preview HMR.
// On --restart: identity-check first, then stop the existing server,
// sweep stale artifacts while target/ is idle, then start fresh.
//
// Stopping someone else's editor server on `--restart` is the worst
// possible outcome of a port collision — only stop the listener if
// the identity probe confirms it's this project's server.
if (shouldRestart) {
const holder = await classifyPortHolder(SERVER_PORT);
if (holder.kind === "foreign") {
console.error(formatForeignListenerError(SERVER_PORT));
process.exit(1);
}
if (holder.kind === "cross-project") {
console.error(formatCrossProjectError(SERVER_PORT, holder, "restart"));
process.exit(1);
}
if (holder.kind === "same-project") {
console.error(`Stopping existing server on port ${SERVER_PORT}...`);
await stopServer(SERVER_PORT);
}
// holder.kind === "free": nothing to stop, fall through to ensureServer.
}
if (shouldRestart) {
sweepStaleArtifacts();
}
const serverChild = await ensureServer(SERVER_PORT);

@@ -64,3 +197,3 @@ // Wait for the actual Vite dev server on the internal port (not the reverse

clearInterval(keepAlive);
if (serverChild) serverChild.kill("SIGINT");
killChildProcess(serverChild);
process.exit(0);

@@ -70,2 +203,12 @@ });

main();
module.exports = {
buildBinary,
cargoBinDir,
installBinary,
main,
sweepStaleArtifacts,
};
if (require.main === module) {
main();
}
#!/usr/bin/env node
// Render environment (colorScheme, deviceScaleFactor, userAgent, locale,
// timezoneId, reduceMotion, forcedColors) is read from config when present
// and passed to browser.newContext(). This is what makes screenshots match
// the Live Preview iframe's host browser — see docs/rendering.md.
//
// iframeBackground is forwarded to buildIframeHarness so the capture paints
// the user's editor-shell background (or whatever the UI detected) behind
// the iframe instead of a hardcoded white.
const fs = require("fs");

@@ -7,224 +16,34 @@ const path = require("path");

const LOADING_MARKERS = [
"Loading scenario...",
"Loading tests...",
"Loading scenarios...",
"disconnected",
];
const {
findErrorPattern,
buildErrorContextSnippet,
hasRenderableContent,
describeBlankReason,
} = require("./scenario-metrics");
function hasLoadingMarkers(text) {
return LOADING_MARKERS.some((marker) => text.includes(marker));
}
const {
createIssue,
pushIssue,
buildResult,
} = require("./scenario-issues");
function hasRenderableContent(state) {
return Boolean(
state &&
(state.rootChildCount > 0 ||
state.rootTextLength > 0 ||
state.bodyTextLength > 0),
);
}
const {
attachHttpMocks,
} = require("./scenario-mocks");
const ERROR_PATTERNS = [
"not found in registry",
"Component not found",
"Scenario Error",
];
const {
loadScenarioInIframe,
waitForStablePage,
collectContentState,
} = require("./scenario-playwright");
function hasErrorPatterns(text) {
return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
}
const {
getInitScript,
handleConsoleMessage,
handlePageError,
handleRequestFailed,
} = require("./scenario-handlers");
function createIssue(kind, message, extra = {}) {
return {
kind,
message,
url: extra.url ?? null,
status: extra.status ?? null,
};
}
const BLANK_RETRY_DELAY_MS = 500;
function pushIssue(issues, issue) {
const key = JSON.stringify(issue);
if (!issues.some((existing) => JSON.stringify(existing) === key)) {
issues.push(issue);
}
}
function escapeHtmlAttribute(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;");
}
function buildIframeHarness(url) {
const escapedUrl = escapeHtmlAttribute(url);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #fff;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
</style>
</head>
<body>
<iframe id="scenario-frame" title="Scenario Preview" src="${escapedUrl}"></iframe>
</body>
</html>`;
}
function buildResult({ loaded, hasContent, issues, outputPath, url }) {
return {
ok: loaded && hasContent && issues.length === 0,
loaded,
hasContent,
url,
outputPath: outputPath ?? null,
issues,
};
}
function normalizeMockCandidates(url) {
try {
const parsed = new URL(url);
return [url, `${parsed.pathname}${parsed.search}`, parsed.pathname];
} catch {
return [url];
}
}
function findHttpMock(httpMocks, request) {
const method = request.method().toUpperCase();
const candidates = normalizeMockCandidates(request.url());
for (const candidate of candidates) {
const key = `${method} ${candidate}`;
if (httpMocks[key]) {
return httpMocks[key];
}
}
return null;
}
async function attachHttpMocks(page, httpMocks) {
if (!httpMocks || Object.keys(httpMocks).length === 0) return;
await page.route("**/*", async (route) => {
const mock = findHttpMock(httpMocks, route.request());
if (!mock) {
await route.continue();
return;
}
const headers = { ...(mock.headers || {}) };
let body;
if (mock.body !== undefined) {
body =
typeof mock.body === "string" ? mock.body : JSON.stringify(mock.body);
const hasContentType = Object.keys(headers).some(
(key) => key.toLowerCase() === "content-type",
);
if (!hasContentType) {
headers["content-type"] = "application/json";
}
}
await route.fulfill({
status: mock.status || 200,
headers,
body,
});
});
// Disable the in-page fetch mock by returning an empty active-mocks.json.
// The HTML injects a script that synchronously loads this file and
// monkey-patches window.fetch, which would bypass Playwright's route
// interception. This route is registered AFTER **/* so it takes priority
// (Playwright uses LIFO ordering for route handlers).
await page.route("**/active-mocks.json", async (route) => {
await route.fulfill({
status: 200,
headers: { "content-type": "application/json" },
body: "[]",
});
});
}
async function collectContentState(target) {
return target.evaluate(() => {
const root = document.getElementById("root");
return {
bodyTextLength: document.body.innerText.trim().length,
rootChildCount: root ? root.childElementCount : 0,
rootTextLength: root ? (root.textContent || "").trim().length : 0,
};
});
}
async function waitForStablePage(page, target, timeoutMs = 10000) {
const started = Date.now();
let lastHtml = "";
let stableCount = 0;
while (Date.now() - started < timeoutMs) {
await page.waitForTimeout(500);
const pageState = await target.evaluate(() => ({
bodyText: document.body.innerText,
html: document.body.innerHTML,
}));
if (
!hasLoadingMarkers(pageState.bodyText) &&
pageState.html === lastHtml
) {
stableCount += 1;
if (stableCount >= 2) return;
} else {
stableCount = 0;
}
lastHtml = pageState.html;
}
}
async function loadScenarioInIframe(page, url) {
const responsePromise = page
.waitForResponse(
(response) =>
response.request().resourceType() === "document" &&
response.url() === url,
{ timeout: 30000 },
)
.catch(() => null);
await page.setContent(buildIframeHarness(url), { waitUntil: "domcontentloaded" });
const frameHandle = await page.waitForSelector("#scenario-frame", {
state: "attached",
timeout: 30000,
});
const frame = await frameHandle.contentFrame();
if (!frame) {
throw new Error("Scenario iframe did not attach");
}
await frame.waitForLoadState("load", { timeout: 30000 });
const response = await responsePromise;
return { frame, response };
}
async function runScenarioCheck(config) {

@@ -234,44 +53,18 @@ const { url, outputPath, width, height, httpMocks = {} } = config;

const browser = await chromium.launch();
const context = await browser.newContext({
const contextOptions = {
viewport: { width: width || 1440, height: height || 900 },
});
};
if (config.colorScheme) contextOptions.colorScheme = config.colorScheme;
if (config.deviceScaleFactor)
contextOptions.deviceScaleFactor = config.deviceScaleFactor;
if (config.userAgent) contextOptions.userAgent = config.userAgent;
if (config.locale) contextOptions.locale = config.locale;
if (config.timezoneId) contextOptions.timezoneId = config.timezoneId;
if (config.reduceMotion) contextOptions.reducedMotion = config.reduceMotion;
if (config.forcedColors) contextOptions.forcedColors = config.forcedColors;
const context = await browser.newContext(contextOptions);
// Context-level init script runs in ALL frames (including cross-origin iframes)
await context.addInitScript(() => {
window.__codeyamUnhandledRejections = [];
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
const message =
reason instanceof Error ? reason.message : String(reason);
window.__codeyamUnhandledRejections.push(message);
});
await context.addInitScript(getInitScript());
// Stub WebSocket during capture to prevent terminal reconnection spam.
// Returns an object that behaves like a closed WebSocket — the terminal
// will attempt to reconnect a few times then give up silently.
window.WebSocket = class StubWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
readyState = 3;
onopen = null;
onclose = null;
onerror = null;
onmessage = null;
send() {}
close() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent() { return false; }
constructor() {
// Fire onerror then onclose on next tick so the terminal sees a failed connection
setTimeout(() => {
if (this.onerror) this.onerror(new Event("error"));
if (this.onclose) this.onclose(new CloseEvent("close"));
}, 0);
}
};
});
const page = await context.newPage();

@@ -281,37 +74,17 @@ await attachHttpMocks(page, httpMocks);

page.on("pageerror", (error) => {
pushIssue(
issues,
createIssue("pageerror", error.message || String(error), {
url: page.url() || url,
}),
);
pushIssue(issues, handlePageError(error));
});
page.on("console", (message) => {
if (message.type() !== "error") return;
const text = message.text();
// Ignore known dev-server WebSocket/HMR errors from Vite proxy
if (
text.includes("WebSocket connection to") ||
text.includes("Unsupported Media Type")
) {
return;
const issue = handleConsoleMessage(message);
if (issue) {
pushIssue(issues, issue);
}
pushIssue(
issues,
createIssue("console", text, {
url: page.url() || url,
}),
);
});
page.on("requestfailed", (request) => {
pushIssue(
issues,
createIssue(
"requestfailed",
request.failure()?.errorText || "Request failed",
{ url: request.url() },
),
);
const issue = handleRequestFailed(request);
if (issue) {
pushIssue(issues, issue);
}
});

@@ -322,3 +95,5 @@

try {
const { frame, response } = await loadScenarioInIframe(page, url);
const { frame, response } = await loadScenarioInIframe(page, url, {
background: config.iframeBackground,
});
loaded = true;

@@ -350,4 +125,14 @@

const contentState = await collectContentState(frame);
const hasContent = hasRenderableContent(contentState);
// Cold-start retry: a single re-collect after a short pause covers
// the React.lazy / Suspense-fallback race that flagged 4-of-59
// scenarios as blank against a cold Vite dev server on 2026-04-30.
// One retry is enough — the Suspense window is sub-second in
// practice, longer waits just slow down genuine blank-render bugs.
let contentState = await collectContentState(frame);
let hasContent = hasRenderableContent(contentState);
if (!hasContent) {
await new Promise((r) => setTimeout(r, BLANK_RETRY_DELAY_MS));
contentState = await collectContentState(frame);
hasContent = hasRenderableContent(contentState);
}

@@ -357,5 +142,7 @@ if (!hasContent) {

issues,
createIssue("blank", "Page rendered no visible content", {
url: page.url() || url,
}),
createIssue(
"blank",
`Page rendered no visible content (${describeBlankReason(contentState)})`,
{ url: page.url() || url },
),
);

@@ -366,8 +153,16 @@ }

const bodyText = await frame.evaluate(() => document.body.innerText || "");
if (hasErrorPatterns(bodyText)) {
const matchedPattern = findErrorPattern(bodyText);
if (matchedPattern) {
const contextSnippet = buildErrorContextSnippet(bodyText, matchedPattern);
pushIssue(
issues,
createIssue("error-state", `Page contains error content: ${bodyText.slice(0, 200)}`, {
url: page.url() || url,
}),
createIssue(
"error-state",
`Page contains error content (matched "${matchedPattern}"): ${contextSnippet ?? bodyText.slice(0, 200)}`,
{
url: page.url() || url,
matchedPattern,
contextSnippet,
},
),
);

@@ -421,15 +216,4 @@ }

module.exports = {
attachHttpMocks,
buildResult,
buildIframeHarness,
createIssue,
findHttpMock,
hasErrorPatterns,
hasLoadingMarkers,
hasRenderableContent,
loadScenarioInIframe,
normalizeMockCandidates,
pushIssue,
runScenarioCheck,
waitForStablePage,
main,
};

@@ -436,0 +220,0 @@

@@ -9,20 +9,82 @@ const { execSync, spawn, spawnSync } = require("child_process");

/** Find the compiled binary — check npm-installed location first, then dev builds. */
/**
* Map (process.platform, process.arch) → the platform sub-package that
* carries the matching native binary. Kept as a small data table so adding
* a new platform (e.g. linux/arm64) is one entry, not a new branch.
*/
const PLATFORM_PACKAGES = {
"darwin-arm64": { pkg: "@codeyam-editor/codeyam-editor-darwin-arm64", binary: "codeyam-editor" },
"darwin-x64": { pkg: "@codeyam-editor/codeyam-editor-darwin-x64", binary: "codeyam-editor" },
"linux-x64": { pkg: "@codeyam-editor/codeyam-editor-linux-x64", binary: "codeyam-editor" },
"win32-x64": { pkg: "@codeyam-editor/codeyam-editor-win32-x64", binary: "codeyam-editor.exe" },
};
/**
* Platform-correct executable filename for the codeyam-editor binary.
* Windows requires the `.exe` extension; on Unix the binary has no extension.
* Accepts an explicit platform so the helper is testable across platforms.
*/
function binaryName(platform = process.platform) {
return platform === "win32" ? "codeyam-editor.exe" : "codeyam-editor";
}
/** Look up the platform sub-package metadata for a given platform/arch pair. */
function platformPackageInfo(platform = process.platform, arch = process.arch) {
return PLATFORM_PACKAGES[`${platform}-${arch}`] || null;
}
/**
* Resolve the native binary path.
*
* Production install (npm install @codeyam-editor/codeyam-editor):
* npm installed the matching @codeyam-editor/codeyam-editor-<plat>-<arch>
* sub-package via optionalDependencies. require.resolve finds the binary
* inside that sibling package.
*
* Dev checkout (`cargo build` in the repo):
* No platform sub-package installed; fall through to target/debug or
* target/release.
*
* Returns null if no binary can be found at any of these locations — the
* caller (ensureBinary / cli.js) decides whether to error or to invoke
* `cargo build` based on whether a Cargo.toml is present.
*/
function findBinary() {
const candidates = [
path.join(rootDir, "bin", "codeyam-editor"), // npm install (postinstall)
path.join(rootDir, "target", "debug", "codeyam-editor"), // local dev (debug)
path.join(rootDir, "target", "release", "codeyam-editor"), // local dev (release)
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
const info = platformPackageInfo();
if (info) {
try {
const resolved = require.resolve(`${info.pkg}/bin/${info.binary}`, {
paths: [rootDir],
});
if (fs.existsSync(resolved)) return resolved;
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") throw e;
}
}
const name = binaryName();
const devCandidates = [
path.join(rootDir, "target", "debug", name),
path.join(rootDir, "target", "release", name),
];
for (const candidate of devCandidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
/** Return the binary path — build from source only if no pre-built binary exists. */
/**
* Return the binary path, building from source if we're in a dev checkout.
*
* Production install: if findBinary returns null, the platform sub-package
* is missing — npm install failed silently (likely because the user's
* platform isn't in the build matrix). Emit a clear, actionable error
* naming the expected sub-package, and exit 1. **Do not** fall through to
* `cargo build` — there's no Cargo.toml in the npm install dir, and the
* resulting "could not find Cargo.toml" error is misleading to users.
*
* Dev checkout: `Cargo.toml` is present at rootDir → run `cargo build`,
* same as the legacy behavior.
*/
function ensureBinary() {

@@ -32,3 +94,22 @@ const existing = findBinary();

// No pre-built binary; try building from source (local dev workflow).
const isDevCheckout = fs.existsSync(path.join(rootDir, "Cargo.toml"));
if (!isDevCheckout) {
const info = platformPackageInfo();
if (!info) {
console.error(
`codeyam-editor: unsupported platform ${process.platform}/${process.arch}.\n` +
`Supported: darwin/arm64, darwin/x64, linux/x64, win32/x64.`
);
} else {
console.error(
`codeyam-editor: native binary missing.\n` +
`Expected at ${info.pkg}/bin/${info.binary} — npm should have installed ` +
`that sub-package as an optionalDependency.\n` +
`Try: npm install --force ${info.pkg}@$(npm view @codeyam-editor/codeyam-editor version)`
);
}
process.exit(1);
}
try {

@@ -38,4 +119,4 @@ execSync("cargo --version", { stdio: "ignore" });

console.error(
"codeyam-editor binary not found. Try reinstalling:\n" +
" npm install @codeyam-editor/codeyam-editor\n"
"codeyam-editor binary not found and `cargo` is not on PATH. " +
"Install Rust from https://rustup.rs/ or reinstall @codeyam-editor/codeyam-editor."
);

@@ -66,2 +147,118 @@ process.exit(1);

function isPlainJsonObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
// Mirrors `deep_merge_json` in crates/types/src/config.rs:1284.
// Objects merge by key (`null` in overlay removes the key); arrays merge
// by index (extra overlay items appended, base items past overlay length
// retained); scalars replace. The JS and Rust sides MUST match — any
// divergence shows up as "the CLI sees port X but `npm run editor`
// opens port Y", which is exactly the bug this whole helper exists to
// prevent.
function deepMergeJson(base, overlay) {
if (isPlainJsonObject(base) && isPlainJsonObject(overlay)) {
for (const k of Object.keys(overlay)) {
const v = overlay[k];
if (v === null) {
delete base[k];
} else if (Object.prototype.hasOwnProperty.call(base, k)) {
base[k] = deepMergeJson(base[k], v);
} else {
base[k] = v;
}
}
return base;
}
if (Array.isArray(base) && Array.isArray(overlay)) {
for (let i = 0; i < overlay.length; i++) {
if (i < base.length) {
base[i] = deepMergeJson(base[i], overlay[i]);
} else {
base.push(overlay[i]);
}
}
return base;
}
return overlay;
}
// Mirrors the post-merge strip in `merged_config_value`
// (crates/types/src/config.rs:1272). Any top-level key starting with
// `_` is documentation-only (`_README`, `_examples`, …) and is removed
// before the merged config is used by the launcher or handed to the
// Rust binary. Mutates in place; returns the same object for chaining.
function stripUnderscoreTopLevelKeys(value) {
if (isPlainJsonObject(value)) {
for (const k of Object.keys(value)) {
if (k.startsWith("_")) delete value[k];
}
}
return value;
}
/**
* Read `<projectDir>/.codeyam/editor.json` and deep-merge an optional
* sibling `editor.local.json` on top, returning the merged plain object.
*
* This is the JS-side equivalent of `merged_config_value` in
* crates/types/src/config.rs — same merge rules, same underscore-prefix
* strip. Launcher scripts call this *before* the Rust binary is built
* (they invoke `cargo build` themselves), so shelling out to
* `codeyam-editor editor config-show` is not an option.
*
* Returns `{}` if the base file is missing (a freshly cloned repo with
* no `.codeyam/` directory should not throw at require-time). Throws
* on malformed JSON, naming the offending file path so the user knows
* which file to fix.
*/
function readMergedEditorConfig(projectDir = rootDir) {
const basePath = path.join(projectDir, ".codeyam", "editor.json");
if (!fs.existsSync(basePath)) return {};
let merged;
try {
merged = JSON.parse(fs.readFileSync(basePath, "utf8"));
} catch (e) {
throw new Error(`Failed to parse ${basePath}: ${e.message}`);
}
if (!isPlainJsonObject(merged)) merged = {};
const overlayPath = path.join(projectDir, ".codeyam", "editor.local.json");
if (fs.existsSync(overlayPath)) {
let overlay;
try {
overlay = JSON.parse(fs.readFileSync(overlayPath, "utf8"));
} catch (e) {
throw new Error(`Failed to parse ${overlayPath}: ${e.message}`);
}
if (isPlainJsonObject(overlay)) {
deepMergeJson(merged, overlay);
}
}
return stripUnderscoreTopLevelKeys(merged);
}
/**
* Resolve a port for a launcher in priority order:
* explicit env var → merged-config value → hard fallback.
*
* Exported so each launcher's port resolution can be unit-tested
* without spawning a process. The fallback is the historical hard-coded
* value (14199 / 5173 / 3000 depending on which slot the caller is
* resolving) and exists so a fresh checkout with no override file and
* no env var still works.
*/
function resolveLauncherPort(envValue, mergedValue, fallback) {
if (envValue !== undefined && envValue !== null && envValue !== "") {
const parsed = parseInt(envValue, 10);
if (Number.isFinite(parsed)) return parsed;
}
if (typeof mergedValue === "number" && Number.isFinite(mergedValue)) {
return mergedValue;
}
return fallback;
}
/** Try to connect to a specific host:port. Returns true if something is listening. */

@@ -124,2 +321,6 @@ function tryConnect(port, host) {

// Default ports across stopServer / restartServer / ensureServer /
// requestServerShutdown are a backstop for any future caller that
// forgets to pass an explicit port. Every production call site now
// resolves the port from `readMergedEditorConfig` + `resolveLauncherPort`.
async function requestServerShutdown(port = 14199, timeoutMs = 1500) {

@@ -142,5 +343,127 @@ const controller = new AbortController();

/**
* GET /api/codeyam-server-identity from the listener on `port` with a
* sub-second timeout. Returns the parsed `{projectDir, pid, version,
* startedAt}` body on a 2xx response, or `null` on any failure
* (timeout, non-200, parse failure, connection refused).
*
* `null` carries two distinct meanings that the caller must
* disambiguate by also checking `isPortInUse`:
* - port is free → no listener at all
* - port is in use → something is listening, but it's not a
* codeyam-editor server (or it's an old build that predates this
* endpoint).
*/
async function fetchServerIdentity(port, timeoutMs = 500) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const resp = await fetch(`http://127.0.0.1:${port}/api/codeyam-server-identity`, {
method: "GET",
signal: controller.signal,
});
if (!resp.ok) return null;
const body = await resp.json();
if (!body || typeof body !== "object" || typeof body.projectDir !== "string") {
return null;
}
return body;
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
/**
* Resolve the canonical (symlink-followed) path for `dir`, falling back to
* `dir` if `fs.realpath` fails (e.g. the path no longer exists). Both
* sides of the identity check route through this so a `~/work/foo`
* symlink and a `/Volumes/dev/foo` direct path compare equal.
*/
async function canonicalisePath(dir) {
try {
return await fs.promises.realpath(dir);
} catch {
return dir;
}
}
/**
* Build the "port held by a non-codeyam-editor process" error string.
* Lives next to `classifyPortHolder` so the wording is tested without
* a running server — both launchers (`ensureServer` and `editor.js`'s
* `--restart`) print this exact message on a "foreign" classification.
*/
function formatForeignListenerError(port) {
return (
`Port ${port} is in use, but no codeyam-editor server responded ` +
`at /api/codeyam-server-identity. Free the port or change ` +
`controlPort in .codeyam/editor.local.json.`
);
}
/**
* Build the "another project's editor is on this port" error string.
* `verb` is the action the launcher would have taken ("attach" /
* "restart"); the wording is otherwise identical so both launcher
* paths share the same advice.
*/
function formatCrossProjectError(port, holder, verb) {
const pidNote = Number.isInteger(holder.identity && holder.identity.pid)
? ` (pid ${holder.identity.pid})`
: "";
return (
`Refusing to ${verb} a codeyam-editor server for a different project.\n` +
` This project: ${holder.wantDir}\n` +
` Server on port ${port}${pidNote}: ${holder.haveDir}\n` +
`\n` +
`Change controlPort in .codeyam/editor.local.json, e.g.:\n` +
` codeyam-editor editor config-override proxy.controlPort 14399`
);
}
/**
* Classify whatever is listening on `port` relative to `projectDir`.
* Returns one of:
* - `{ kind: "free" }` — nothing on the port
* - `{ kind: "foreign" }` — port held by a non-editor process
* - `{ kind: "cross-project", wantDir, haveDir, identity }`
* — held by another project's editor
* - `{ kind: "same-project", identity }` — held by THIS project's editor
*
* Pure: every effect (network, fs) is bounded; no console output, no
* process.exit. The launcher decides what to do with each kind. Splitting
* the policy (what to print, when to exit) from the classification keeps
* the helper testable without mocking process.exit, and lets `ensureServer`
* and `editor.js`'s `--restart` block share the same logic with different
* wording.
*/
async function classifyPortHolder(port, projectDir = process.cwd()) {
if (!(await isPortInUse(port))) {
return { kind: "free" };
}
const identity = await fetchServerIdentity(port);
if (identity === null) {
return { kind: "foreign" };
}
const wantDir = await canonicalisePath(projectDir);
const haveDir = await canonicalisePath(identity.projectDir);
if (wantDir !== haveDir) {
return { kind: "cross-project", wantDir, haveDir, identity };
}
return { kind: "same-project", identity };
}
function tryKillPid(pid) {
try {
process.kill(pid, "SIGINT");
// On Windows, `process.kill(pid, "SIGINT")` either errors or no-ops
// depending on Node version. Drop the signal so Node maps to a
// platform TerminateProcess. Less graceful, but the child does exit.
if (process.platform === "win32") {
process.kill(pid);
} else {
process.kill(pid, "SIGINT");
}
return true;

@@ -152,3 +475,20 @@ } catch {

/**
* Send the platform-correct termination signal to a spawned child process.
* Used by editor.js / editor-dev.js / container.js when the user hits Ctrl+C —
* `child.kill("SIGINT")` is unreliable on Windows.
*/
function killChildProcess(child) {
if (!child) return;
if (process.platform === "win32") {
child.kill();
} else {
child.kill("SIGINT");
}
}
function findListeningPids(port) {
// Process discovery on Windows is best-effort — `lsof` is not installed and
// a `netstat -ano` parser is a follow-up. The API-shutdown path and the
// PID-file path together stop the server cleanly in normal operation.
if (process.platform === "win32") return [];

@@ -211,10 +551,33 @@

/** Start the Rust backend server if not already running. Returns the child process (or null if already running). */
/**
* Start the Rust backend server if not already running, refusing to attach
* if the existing listener belongs to a different project (or to a foreign
* process that doesn't speak the identity endpoint at all). Returns the
* spawned child on a fresh start, or `null` when we attached to an
* already-running same-project server. Exits the process non-zero on a
* mismatch — silently choosing a different port would just relocate the
* surprise.
*/
async function ensureServer(port = 14199) {
if (await isPortInUse(port)) {
console.error(`Server already running on port ${port}`);
return null;
const holder = await classifyPortHolder(port);
switch (holder.kind) {
case "foreign":
console.error(formatForeignListenerError(port));
process.exit(1);
break;
case "cross-project":
console.error(formatCrossProjectError(port, holder, "attach"));
process.exit(1);
break;
case "same-project":
console.error(`Server already running on port ${port}`);
return null;
case "free":
default:
break;
}
const binary = ensureBinary();
// UI bundle is embedded in the Rust binary; CODEYAM_EDITOR_UI_DIR is
// now an explicit dev-override only.
const child = spawn(binary, ["start", "--no-open", "--port", String(port)], {

@@ -224,3 +587,3 @@ stdio: "inherit",

detached: false,
env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
env: process.env,
});

@@ -241,2 +604,5 @@

serverStatePath,
binaryName,
platformPackageInfo,
PLATFORM_PACKAGES,
findBinary,

@@ -250,5 +616,19 @@ ensureBinary,

readServerState,
clearServerState,
requestServerShutdown,
fetchServerIdentity,
canonicalisePath,
classifyPortHolder,
formatForeignListenerError,
formatCrossProjectError,
tryKillPid,
findListeningPids,
stopServer,
restartServer,
ensureServer,
killChildProcess,
deepMergeJson,
stripUnderscoreTopLevelKeys,
readMergedEditorConfig,
resolveLauncherPort,
};
{
"name": "@codeyam-editor/codeyam-editor",
"version": "0.1.0-staging.46bd02f",
"version": "0.1.0-staging.5d9fd64",
"description": "Language-agnostic managed execution sandbox for scenario-driven development",

@@ -8,8 +8,11 @@ "bin": {

},
"scripts": {
"postinstall": "node ./npm/postinstall.js"
},
"dependencies": {
"playwright": "^1.58.2"
},
"optionalDependencies": {
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.0-staging.5d9fd64",
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.0-staging.5d9fd64",
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.0-staging.5d9fd64",
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.0-staging.5d9fd64"
},
"keywords": [

@@ -28,5 +31,4 @@ "codeyam",

"npm/",
"bin/",
"ui/"
]
}
#!/usr/bin/env node
/**
* Post-install script for @codeyam-editor/codeyam-editor.
*
* Downloads the pre-built platform binary and UI assets from GitHub Releases.
* Skipped when running in local development (cargo builds from source instead).
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const rootDir = path.resolve(__dirname, "..");
const PLATFORM_MAP = {
"darwin-arm64": "codeyam-editor-darwin-arm64",
"darwin-x64": "codeyam-editor-darwin-x64",
"linux-x64": "codeyam-editor-linux-x64",
};
function getArtifactName() {
const key = `${process.platform}-${process.arch}`;
const name = PLATFORM_MAP[key];
if (!name) {
console.error(
`codeyam-editor: unsupported platform ${process.platform} ${process.arch}`
);
console.error("Supported: macOS (arm64, x64), Linux (x64)");
process.exit(0); // Don't fail the install — user may be building from source
}
return name;
}
/**
* Determine the GitHub release tag from the installed package version.
*
* - "0.1.0-staging.abc1234" -> "staging"
* - "0.1.2" -> "latest"
*/
function getReleaseTag() {
const pkg = JSON.parse(
fs.readFileSync(path.join(rootDir, "package.json"), "utf8")
);
const version = pkg.version || "0.1.0";
if (version.includes("-staging")) return "staging";
return "latest";
}
function download() {
// Skip if a binary already exists (e.g. local dev with cargo build)
const binPath = path.join(rootDir, "bin", "codeyam-editor");
if (fs.existsSync(binPath)) {
return;
}
// Skip if we're in a dev checkout (has Cargo.toml at root)
if (fs.existsSync(path.join(rootDir, "Cargo.toml"))) {
return;
}
const artifactName = getArtifactName();
const tag = getReleaseTag();
const url = `https://github.com/codeyam-ai/codeyam-editor/releases/download/${tag}/${artifactName}.tar.gz`;
console.error(`Downloading codeyam-editor binary (${artifactName})...`);
const binDir = path.join(rootDir, "bin");
const uiDir = path.join(rootDir, "ui");
fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(uiDir, { recursive: true });
try {
// Download and extract in one step using curl + tar
execSync(
`curl -fsSL "${url}" | tar xz -C "${rootDir}"`,
{ stdio: ["ignore", "inherit", "inherit"], timeout: 120000 }
);
// Ensure binary is executable
if (fs.existsSync(binPath)) {
fs.chmodSync(binPath, 0o755);
}
console.error("codeyam-editor installed successfully!");
} catch (err) {
console.error(`Failed to download codeyam-editor binary from ${url}`);
console.error(
"You can build from source instead: cargo build --release"
);
// Don't fail the install — user may want to build from source
}
}
download();