@zibby/cli 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/zibby.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- process.stdout.on("error",o=>{o.code}),process.stderr.on("error",o=>{o.code}),process.env.DOTENV_CONFIG_QUIET="true";import"@zibby/skills";import{Command as d}from"commander";import{initCommand as p}from"../commands/init.js";import{runCommand as l}from"../commands/run.js";import{videoCommand as m}from"../commands/video.js";import{uploadCommand as u}from"../commands/upload.js";import{ciSetupCommand as f}from"../commands/ci-setup.js";import{setupPlaywrightMcpCommand as w,setupCiCommand as y,testWithVideoCommand as g}from"../commands/setup-scripts.js";import{readFileSync as h}from"fs";import{fileURLToPath as b}from"url";import{dirname as k,join as C}from"path";const v=b(import.meta.url),I=k(v),r=JSON.parse(h(C(I,"../package.json"),"utf-8")),s=`zibby v${r.version}`,i=process.argv.slice(2),P=i.includes("-h")||i.includes("--help"),S=i.includes("-v")||i.includes("-V")||i.includes("--version");S&&(console.log(s),process.exit(0)),console.log(`${s}
3
- `);const _=i[0]==="chat",j=["--verbose","--agent","--stream","-s"],x=i.length>0&&i.every(o=>j.includes(o)||i[i.indexOf(o)-1]==="--agent"),D=i.length===0||_||x;if(D&&!P){const o={},t=i.indexOf("--agent");t!==-1&&i[t+1]&&(o.agent=i[t+1]),i.includes("--verbose")&&(o.verbose=!0),(i.includes("--stream")||i.includes("-s"))&&(o.stream=!0);const{chatCommand:n}=await import("../commands/chat.js");await n(o),process.exit(0)}const e=new d;e.name("zibby").description("Zibby Test Automation - AI-powered test generation").version(r.version,"-V, --version"),e.command("init").description("Initialize a new Zibby test project (like rails new)").argument("[project-name]","Project name (optional, uses current directory if not provided)").option("--agent <type>","Agent to use (claude, cursor, codex, gemini)").option("--memory-backend <backend>","Memory backend to configure (dolt, mem0)","dolt").option("--skip-install","Skip npm install").option("--skip-memory","Skip test memory setup during initialization").option("-f, --force","Force reinitialize (overwrite existing config)").option("--headed","Run MCP browser in headed mode (visible browser)").option("--headless","Run MCP browser in headless mode (hidden browser)").option("--api-key <key>","Zibby API key for cloud sync").option("--cloud-sync","Enable cloud sync and install Zibby MCP").action(p),e.command("test").description("Run a test specification").argument("[spec-path]","Path to test spec file or inline test description in quotes").option("--sources <ids>","Comma-separated test case IDs to fetch from cloud").option("--execution <id>","Execution ID containing the test cases (required with --sources)").option("--agent <type>","Agent to use (claude, cursor, codex, gemini) - overrides config").option("--workflow <name>","Workflow to use (e.g., QuickSmokeWorkflow, quick-smoke)").option("--headless","Run browser in headless mode").option("--node <name>","Run only a specific node (e.g., execute_live, generate_script)").option("--session <id>",'Use existing session (e.g., 1768974629717 or "last") - requires --node').option("--session-path <dir>","Use this session folder (absolute or relative to cwd); Studio pins artifacts here").option("--project <id>","Project ID (optional, auto-detected from ZIBBY_API_KEY)").option("--collection <id-or-name>","Collection ID or name (creates new if name doesn't exist)").option("--folder <path>","Folder path within collection (optional, requires --collection)").option("--sync","Force upload to cloud (overrides cloudSync: false)").option("--no-sync","Skip upload to cloud (overrides cloudSync: true)").option("--config <path>","Path to config file",".zibby.config.mjs").option("--auto-approve","Auto-approve MCP tools (for CI/CD)").option("-o, --open","Open test results in browser after completion").option("--verbose","Show info level logs").option("--debug","Show debug level logs (most verbose)").option("-m, --mem","Enable test memory (Dolt-backed knowledge from previous runs)").action((o,t)=>(t.debug?process.env.ZIBBY_DEBUG="true":t.verbose&&(process.env.ZIBBY_VERBOSE="true"),l(o,t))),e.command("implement").description("Implement a Jira ticket using AI agent (runs in ECS container)").action(async(...o)=>{const{implementCommand:t}=await import("../commands/implement.js");return t(...o)}),e.command("analyze").description("Analyze a Jira ticket against the codebase (runs in ECS container)").option("--workflow <path>","Path to a local workflow JSON file (e.g., .zibby/workflow-analysis.json)").action(async(...o)=>{const{analyzeCommand:t}=await import("../commands/analyze-graph.js");return t(...o)}),e.command("video").description("Organize test videos next to test files").action(m),e.command("upload <spec-path>").description("Upload existing test artifacts to Zibby Cloud").option("--project <id>","Project ID (REQUIRED - use flag or ZIBBY_PROJECT_ID env)").option("--collection <id-or-name>","Collection ID or name (creates new if name doesn't exist)").option("--folder <path>","Folder path within collection (optional)").option("--agent <type>","Agent used (for metadata)").action(u),e.command("login").description("Log in to Zibby (opens browser for authentication)").action(async()=>{const{loginCli:o}=await import("../auth/cli-login.js");await o(),process.exit(0)}),e.command("logout").description("Log out from Zibby (clears saved session)").action(async()=>{const{logoutCli:o}=await import("../auth/cli-login.js");o(),process.exit(0)}),e.command("status").description("Show current authentication status and token details").option("--json","Output in JSON format (for scripts)").action(async o=>{const{showLoginStatus:t}=await import("../auth/cli-login.js");await t(o),process.exit(0)}),e.command("list").description("List your projects and API tokens").action(async()=>{const{listProjectsCommand:o}=await import("../commands/list-projects.js");await o()}),e.command("ci-setup").description("Setup Cursor Agent for CI/CD (patch and configure)").option("--get-keys","Get MCP approval keys").option("--save","Save approval keys to project").action(f),e.command("setup-playwright").description("Setup official Playwright MCP (from cursor-agent-package)").option("--headed","Configure MCP in headed mode (visible browser)").option("--viewport-width <width>","Viewport width (default: 1280)","1280").option("--viewport-height <height>","Viewport height (default: 720)","720").action(o=>{const t={width:parseInt(o.viewportWidth,10)||1280,height:parseInt(o.viewportHeight,10)||720};return w({...o,viewport:t})}),e.command("setup-ci-full").description("Complete CI/CD setup from scratch").action(y),e.command("test-video").description("Run Playwright tests with video recording").argument("[test-file]","Test file to run (default: tests/)").option("--headed","Run in headed mode (visible browser)").action(g),e.command("generate").description("Generate test specs from a ticket + codebase (local analysis using real AI agent)").option("-t, --ticket <key>","Jira ticket key (fetches automatically)").option("-i, --input <file>","Input file with ticket description/requirements").option("-d, --description <text>","Inline ticket description").option("--repo <path>","Path to the codebase (default: current directory)").option("--agent <type>","Agent to use (codex, claude, cursor, gemini)").option("--model <model>","Model override").option("-o, --output <dir>","Output directory for spec files (default: test-specs)").action(async o=>{const{generateCommand:t}=await import("../commands/generate.js");return t(o)}),e.command("studio").description("Launch Zibby Studio desktop (installs from CDN if needed). Uses this folder as the Zibby project.").option("-p, --port <port>","Port for the Studio API bridge (default: 3847)").option("--no-open","Start the API only; do not launch desktop app").option("--update","Force re-download of the latest Studio binary").action(async o=>{const{studioCommand:t}=await import("../commands/studio.js");return t(o)});const E=e.command("g").description("Generate scaffolds");E.command("workflow [name]").description("Scaffold a new custom workflow in .zibby/workflows/<name>/ (auto-generates name if omitted)").action(async o=>{const{generateWorkflowCommand:t}=await import("../commands/workflows/generate.js");return t(o)}),e.command("start <workflow-name>").description("Start a local dev server for a custom workflow").option("-p, --port <port>","Port for the workflow server (default: 3848)").action(async(o,t)=>{const{startWorkflowCommand:n}=await import("../commands/workflows/start.js");return n(o,t)}),e.command("deploy <workflow-name>").description("Deploy a custom workflow to Zibby Cloud").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").action(async(o,t)=>{const{deployWorkflowCommand:n}=await import("../commands/workflows/deploy.js");return n(o,t)}),e.command("logs [jobId]").description("Tail logs from a workflow job (use --workflow to auto-pick latest)").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--workflow <name>","Workflow name (tails the latest run)").option("--all","Show interleaved logs from all runs (requires --workflow)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").option("--no-follow","Fetch logs once and exit (default: tail)").option("--lines <n>","Max log lines per fetch (default: 200)").action(async(o,t)=>{const{logsCommand:n}=await import("../commands/workflows/logs.js");return n(o,t)}),e.command("run-workflow").description("Run a deployed workflow from S3 sources (used by ECS containers)").action(async()=>{const{runWorkflowCommand:o}=await import("../commands/workflows/run.js");return o()});const a=e.command("memory").description("Test memory database \u2014 version-controlled knowledge from runs");a.command("stats").description("Show memory database statistics").action(async()=>{const{memoryStatsCommand:o}=await import("../commands/memory.js");return o()}),a.command("init").description("Initialize the memory database (Dolt)").action(async()=>{const{memoryInitCommand:o}=await import("../commands/memory.js");return o()}),a.command("compact").description("Prune old data and run Dolt GC to reclaim storage").option("--max-runs <n>","Keep last N runs per spec (default: 50)",parseInt).option("--max-age <days>","Remove data older than N days (default: 90)",parseInt).action(async o=>{const{memoryCompactCommand:t}=await import("../commands/memory.js");return t(o)}),a.command("reset").description("Wipe the memory database").option("-f, --force","Confirm reset").action(async o=>{const{memoryResetCommand:t}=await import("../commands/memory.js");return t(o)});const c=e.command("workflow").description("Manage workflow graphs (download, upload, list)");c.command("download").description("Download a workflow graph from Zibby Cloud to .zibby/").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--type <type>","Workflow type: analysis, implementation, run_test").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").option("--output <dir>","Output directory (default: .zibby/)").option("--include-default","Download the built-in default graph if no custom one exists").action(async o=>{const{workflowDownloadCommand:t}=await import("../commands/workflow.js");return t(o)}),c.command("list").description("List all workflows for a project").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").action(async o=>{const{workflowListCommand:t}=await import("../commands/workflow.js");return t(o)});const A=e.command("run-queue").description("Parallel capacity \u2014 on-disk wait queue (see parallel.waitWhenAtCapacity in .zibby.config.mjs)");A.command("list").description("List CLI processes waiting for a run slot").option("--config <path>","Path to config file",".zibby.config.mjs").action(async o=>{const{runCapacityQueueListCommand:t}=await import("../commands/run-capacity-queue-cli.js");await t(o),process.exit(0)}),e.command("uninstall").description("Remove all Zibby data: global CLI, ~/.zibby, Cursor MCP config, Studio, and current project").option("--dry-run","Show what would be deleted without deleting").option("--deep","Also remove npx cache dirs containing zibby").action(async o=>{const{uninstallCommand:t}=await import("../commands/uninstall.js");await t(o),process.exit(0)}),e.parse();
2
+ process.stdout.on("error",o=>{o.code}),process.stderr.on("error",o=>{o.code}),process.env.DOTENV_CONFIG_QUIET="true";import"@zibby/skills";import{Command as d}from"commander";import{initCommand as p}from"../commands/init.js";import{runCommand as m}from"../commands/run.js";import{videoCommand as l}from"../commands/video.js";import{uploadCommand as u}from"../commands/upload.js";import{ciSetupCommand as f}from"../commands/ci-setup.js";import{setupPlaywrightMcpCommand as w,setupCiCommand as y,testWithVideoCommand as g}from"../commands/setup-scripts.js";import{readFileSync as h}from"fs";import{fileURLToPath as b}from"url";import{dirname as k,join as C}from"path";import{bootstrapAgentEnv as v}from"../utils/agent-credentials.js";const I=b(import.meta.url),P=k(I),r=JSON.parse(h(C(P,"../package.json"),"utf-8")),s=`zibby v${r.version}`,i=process.argv.slice(2),S=i.includes("-h")||i.includes("--help"),_=i.includes("-v")||i.includes("-V")||i.includes("--version");_&&(console.log(s),process.exit(0)),console.log(`${s}
3
+ `),v(process.cwd());const j=i[0]==="chat",x=["--verbose","--agent","--stream","-s"],D=i.length>0&&i.every(o=>x.includes(o)||i[i.indexOf(o)-1]==="--agent"),E=i.length===0||j||D;if(E&&!S){const o={},t=i.indexOf("--agent");t!==-1&&i[t+1]&&(o.agent=i[t+1]),i.includes("--verbose")&&(o.verbose=!0),(i.includes("--stream")||i.includes("-s"))&&(o.stream=!0);const{chatCommand:n}=await import("../commands/chat.js");await n(o),process.exit(0)}const e=new d;e.name("zibby").description("Zibby Test Automation - AI-powered test generation").version(r.version,"-V, --version"),e.command("init").description("Initialize a new Zibby test project (like rails new)").argument("[project-name]","Project name (optional, uses current directory if not provided)").option("--agent <type>","Agent to use (claude, cursor, codex, gemini)").option("--memory-backend <backend>","Memory backend to configure (dolt, mem0)","dolt").option("--skip-install","Skip npm install").option("--skip-memory","Skip test memory setup during initialization").option("-f, --force","Force reinitialize (overwrite existing config)").option("--headed","Run MCP browser in headed mode (visible browser)").option("--headless","Run MCP browser in headless mode (hidden browser)").option("--api-key <key>","Zibby API key for cloud sync").option("--cloud-sync","Enable cloud sync and install Zibby MCP").action(p),e.command("test").description("Run a test specification").argument("[spec-path]","Path to test spec file or inline test description in quotes").option("--sources <ids>","Comma-separated test case IDs to fetch from cloud").option("--execution <id>","Execution ID containing the test cases (required with --sources)").option("--agent <type>","Agent to use (claude, cursor, codex, gemini) - overrides config").option("--workflow <name>","Workflow to use (e.g., QuickSmokeWorkflow, quick-smoke)").option("--headless","Run browser in headless mode").option("--node <name>","Run only a specific node (e.g., execute_live, generate_script)").option("--session <id>",'Use existing session (e.g., 1768974629717 or "last") - requires --node').option("--session-path <dir>","Use this session folder (absolute or relative to cwd); Studio pins artifacts here").option("--project <id>","Project ID (optional, auto-detected from ZIBBY_API_KEY)").option("--collection <id-or-name>","Collection ID or name (creates new if name doesn't exist)").option("--folder <path>","Folder path within collection (optional, requires --collection)").option("--sync","Force upload to cloud (overrides cloudSync: false)").option("--no-sync","Skip upload to cloud (overrides cloudSync: true)").option("--config <path>","Path to config file",".zibby.config.mjs").option("--auto-approve","Auto-approve MCP tools (for CI/CD)").option("-o, --open","Open test results in browser after completion").option("--verbose","Show info level logs").option("--debug","Show debug level logs (most verbose)").option("-m, --mem","Enable test memory (Dolt-backed knowledge from previous runs)").action((o,t)=>(t.debug?process.env.ZIBBY_DEBUG="true":t.verbose&&(process.env.ZIBBY_VERBOSE="true"),m(o,t))),e.command("implement").description("Implement a Jira ticket using AI agent (runs in ECS container)").action(async(...o)=>{const{implementCommand:t}=await import("../commands/implement.js");return t(...o)}),e.command("analyze").description("Analyze a Jira ticket against the codebase (runs in ECS container)").option("--workflow <path>","Path to a local workflow JSON file (e.g., .zibby/workflow-analysis.json)").action(async(...o)=>{const{analyzeCommand:t}=await import("../commands/analyze-graph.js");return t(...o)}),e.command("video").description("Organize test videos next to test files").action(l),e.command("upload <spec-path>").description("Upload existing test artifacts to Zibby Cloud").option("--project <id>","Project ID (REQUIRED - use flag or ZIBBY_PROJECT_ID env)").option("--collection <id-or-name>","Collection ID or name (creates new if name doesn't exist)").option("--folder <path>","Folder path within collection (optional)").option("--agent <type>","Agent used (for metadata)").action(u),e.command("login").description("Log in to Zibby (opens browser for authentication)").action(async()=>{const{loginCli:o}=await import("../auth/cli-login.js");await o(),process.exit(0)}),e.command("logout").description("Log out from Zibby (clears saved session)").action(async()=>{const{logoutCli:o}=await import("../auth/cli-login.js");o(),process.exit(0)}),e.command("status").description("Show current authentication status and token details").option("--json","Output in JSON format (for scripts)").action(async o=>{const{showLoginStatus:t}=await import("../auth/cli-login.js");await t(o),process.exit(0)}),e.command("list").description("List your projects and API tokens").action(async()=>{const{listProjectsCommand:o}=await import("../commands/list-projects.js");await o()}),e.command("ci-setup").description("Setup Cursor Agent for CI/CD (patch and configure)").option("--get-keys","Get MCP approval keys").option("--save","Save approval keys to project").action(f),e.command("setup-playwright").description("Setup official Playwright MCP (from cursor-agent-package)").option("--headed","Configure MCP in headed mode (visible browser)").option("--viewport-width <width>","Viewport width (default: 1280)","1280").option("--viewport-height <height>","Viewport height (default: 720)","720").action(o=>{const t={width:parseInt(o.viewportWidth,10)||1280,height:parseInt(o.viewportHeight,10)||720};return w({...o,viewport:t})}),e.command("setup-ci-full").description("Complete CI/CD setup from scratch").action(y),e.command("test-video").description("Run Playwright tests with video recording").argument("[test-file]","Test file to run (default: tests/)").option("--headed","Run in headed mode (visible browser)").action(g),e.command("generate").description("Generate test specs from a ticket + codebase (local analysis using real AI agent)").option("-t, --ticket <key>","Jira ticket key (fetches automatically)").option("-i, --input <file>","Input file with ticket description/requirements").option("-d, --description <text>","Inline ticket description").option("--repo <path>","Path to the codebase (default: current directory)").option("--agent <type>","Agent to use (codex, claude, cursor, gemini)").option("--model <model>","Model override").option("-o, --output <dir>","Output directory for spec files (default: test-specs)").action(async o=>{const{generateCommand:t}=await import("../commands/generate.js");return t(o)}),e.command("studio").description("Launch Zibby Studio desktop (installs from CDN if needed). Uses this folder as the Zibby project.").option("-p, --port <port>","Port for the Studio API bridge (default: 3847)").option("--no-open","Start the API only; do not launch desktop app").option("--update","Force re-download of the latest Studio binary").action(async o=>{const{studioCommand:t}=await import("../commands/studio.js");return t(o)});const A=e.command("g").description("Generate scaffolds");A.command("workflow [name]").description("Scaffold a new custom workflow in .zibby/workflows/<name>/ (auto-generates name if omitted)").action(async o=>{const{generateWorkflowCommand:t}=await import("../commands/workflows/generate.js");return t(o)}),e.command("start <workflow-name>").description("Start a local dev server for a custom workflow").option("-p, --port <port>","Port for the workflow server (default: 3848)").action(async(o,t)=>{const{startWorkflowCommand:n}=await import("../commands/workflows/start.js");return n(o,t)}),e.command("deploy <workflow-name>").description("Deploy a custom workflow to Zibby Cloud").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").action(async(o,t)=>{const{deployWorkflowCommand:n}=await import("../commands/workflows/deploy.js");return n(o,t)}),e.command("logs [jobId]").description("Tail logs from a workflow job (use --workflow to auto-pick latest)").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--workflow <name>","Workflow name (tails the latest run)").option("--all","Show interleaved logs from all runs (requires --workflow)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").option("--no-follow","Fetch logs once and exit (default: tail)").option("--lines <n>","Max log lines per fetch (default: 200)").action(async(o,t)=>{const{logsCommand:n}=await import("../commands/workflows/logs.js");return n(o,t)}),e.command("run-workflow").description("Run a deployed workflow from S3 sources (used by ECS containers)").action(async()=>{const{runWorkflowCommand:o}=await import("../commands/workflows/run.js");return o()});const a=e.command("memory").description("Test memory database \u2014 version-controlled knowledge from runs");a.command("stats").description("Show memory database statistics").action(async()=>{const{memoryStatsCommand:o}=await import("../commands/memory.js");return o()}),a.command("init").description("Initialize the memory database (Dolt)").action(async()=>{const{memoryInitCommand:o}=await import("../commands/memory.js");return o()}),a.command("compact").description("Prune old data and run Dolt GC to reclaim storage").option("--max-runs <n>","Keep last N runs per spec (default: 50)",parseInt).option("--max-age <days>","Remove data older than N days (default: 90)",parseInt).action(async o=>{const{memoryCompactCommand:t}=await import("../commands/memory.js");return t(o)}),a.command("reset").description("Wipe the memory database").option("-f, --force","Confirm reset").action(async o=>{const{memoryResetCommand:t}=await import("../commands/memory.js");return t(o)});const c=e.command("workflow").description("Manage workflow graphs (download, upload, list)");c.command("download").description("Download a workflow graph from Zibby Cloud to .zibby/").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--type <type>","Workflow type: analysis, implementation, run_test").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").option("--output <dir>","Output directory (default: .zibby/)").option("--include-default","Download the built-in default graph if no custom one exists").action(async o=>{const{workflowDownloadCommand:t}=await import("../commands/workflow.js");return t(o)}),c.command("list").description("List all workflows for a project").option("--project <id>","Project ID (or ZIBBY_PROJECT_ID env)").option("--api-key <key>","API key (or ZIBBY_API_KEY env)").action(async o=>{const{workflowListCommand:t}=await import("../commands/workflow.js");return t(o)});const B=e.command("run-queue").description("Parallel capacity \u2014 on-disk wait queue (see parallel.waitWhenAtCapacity in .zibby.config.mjs)");B.command("list").description("List CLI processes waiting for a run slot").option("--config <path>","Path to config file",".zibby.config.mjs").action(async o=>{const{runCapacityQueueListCommand:t}=await import("../commands/run-capacity-queue-cli.js");await t(o),process.exit(0)}),e.command("uninstall").description("Remove all Zibby data: global CLI, ~/.zibby, Cursor MCP config, Studio, and current project").option("--dry-run","Show what would be deleted without deleting").option("--deep","Also remove npx cache dirs containing zibby").action(async o=>{const{uninstallCommand:t}=await import("../commands/uninstall.js");await t(o),process.exit(0)}),e.parse();
@@ -1,53 +1,57 @@
1
- import{mkdir as _,writeFile as g,readFile as w}from"fs/promises";import{existsSync as f,readdirSync as j}from"fs";import{join as i,resolve as L,dirname as N}from"path";import{homedir as C}from"os";import D from"inquirer";import e from"chalk";import v from"ora";import{spawn as S,execSync as T}from"child_process";import{fileURLToPath as H}from"url";import{createRequire as O}from"module";const G=H(import.meta.url),F=N(G),Z=O(import.meta.url);async function W(){try{const d=process.platform==="win32",n=T("npm config get prefix",{encoding:"utf-8"}).trim(),y=d?n:`${n}/bin`,m=y.replace(/^~/,C()),a=d?";":":";if(process.env.PATH.split(a).some(I=>{const z=I.replace(/^~/,C());return z===m||z===y}))return;if(process.env.CI||process.env.ZIBBY_CI||!process.stdin.isTTY){console.log(e.yellow("\u26A0\uFE0F npm global bin not in PATH")),console.log(e.gray(` Location: ${m}`)),console.log(e.gray(` Add it manually: export PATH="${m}:$PATH"
2
- `));return}console.log(e.yellow("\u26A0\uFE0F npm global bin not in PATH")),console.log(e.gray(` Location: ${m}`)),console.log();const{shouldAddPath:s}=await D.prompt([{type:"confirm",name:"shouldAddPath",message:"Add npm global bin to your shell PATH automatically?",default:!0}]);if(!s){d?(console.log(e.gray(`
1
+ import{mkdir as S,writeFile as p,readFile as w}from"fs/promises";import{existsSync as h,readdirSync as G}from"fs";import{join as a,resolve as O,dirname as H}from"path";import{homedir as P}from"os";import Y from"inquirer";import e from"chalk";import I from"ora";import{spawn as K,execSync as R}from"child_process";import{fileURLToPath as V}from"url";import{createRequire as Z}from"module";import{AGENT_KEY_MAP as D,saveAgentApiKey as F,readGlobalConfig as W}from"../utils/agent-credentials.js";const U=V(import.meta.url),q=H(U),N=Z(import.meta.url);async function X(){try{const d=process.platform==="win32",o=R("npm config get prefix",{encoding:"utf-8"}).trim(),y=d?o:`${o}/bin`,m=y.replace(/^~/,P()),l=d?";":":";if(process.env.PATH.split(l).some(f=>{const x=f.replace(/^~/,P());return x===m||x===y}))return;if(process.env.CI||process.env.ZIBBY_CI||!process.stdin.isTTY){console.log(e.yellow("\u26A0\uFE0F npm global bin not in PATH")),console.log(e.gray(` Location: ${m}`)),console.log(e.gray(` Add it manually: export PATH="${m}:$PATH"
2
+ `));return}console.log(e.yellow("\u26A0\uFE0F npm global bin not in PATH")),console.log(e.gray(` Location: ${m}`)),console.log();const{shouldAddPath:s}=await Y.prompt([{type:"confirm",name:"shouldAddPath",message:"Add npm global bin to your shell PATH automatically?",default:!0}]);if(!s){d?(console.log(e.gray(`
3
3
  \u{1F4A1} To add manually on Windows:`)),console.log(e.gray(' 1. Search "Environment Variables" in Start menu')),console.log(e.gray(' 2. Edit "Path" in User variables')),console.log(e.gray(` 3. Add: ${m}
4
4
  `))):(console.log(e.gray(`
5
5
  \u{1F4A1} To add manually, run:`)),console.log(e.gray(` echo 'export PATH="${m}:$PATH"' >> ~/.zshrc`)),console.log(e.gray(` source ~/.zshrc
6
6
  `)));return}if(d){console.log(e.yellow("\u26A0\uFE0F Cannot auto-add PATH on Windows")),console.log(e.gray(" Please add manually:")),console.log(e.gray(' 1. Search "Environment Variables" in Start menu')),console.log(e.gray(' 2. Edit "Path" in User variables')),console.log(e.gray(` 3. Add: ${m}
7
- `));return}const x=process.env.SHELL||"";let p="";if(x.includes("zsh"))p=i(C(),".zshrc");else if(x.includes("bash"))p=f(i(C(),".bashrc"))?i(C(),".bashrc"):i(C(),".bash_profile");else{console.log(e.yellow(`\u26A0\uFE0F Unknown shell: ${x}`)),console.log(e.gray(" Please add manually:")),console.log(e.gray(` export PATH="${m}:$PATH"`));return}if(f(p)){const I=await w(p,"utf-8");if(I.includes(m)||I.includes("npm")&&I.includes("global")&&I.includes("bin")){console.log(e.yellow(`\u26A0\uFE0F PATH entry found in ${p} but not active`)),console.log(e.gray(` Run: source ${p}
8
- `));return}}const b=`
7
+ `));return}const A=process.env.SHELL||"";let g="";if(A.includes("zsh"))g=a(P(),".zshrc");else if(A.includes("bash"))g=h(a(P(),".bashrc"))?a(P(),".bashrc"):a(P(),".bash_profile");else{console.log(e.yellow(`\u26A0\uFE0F Unknown shell: ${A}`)),console.log(e.gray(" Please add manually:")),console.log(e.gray(` export PATH="${m}:$PATH"`));return}if(h(g)){const f=await w(g,"utf-8");if(f.includes(m)||f.includes("npm")&&f.includes("global")&&f.includes("bin")){console.log(e.yellow(`\u26A0\uFE0F PATH entry found in ${g} but not active`)),console.log(e.gray(` Run: source ${g}
8
+ `));return}}const v=`
9
9
  # npm global bin (added by zibby)
10
10
  export PATH="${m}:$PATH"
11
- `;await g(p,(f(p)?await w(p,"utf-8"):"")+b),console.log(e.green(`\u2705 Added to ${p}`)),console.log(e.yellow(`
12
- \u{1F4A1} Run this to activate in current session:`)),console.log(e.gray(` source ${p}
13
- `))}catch{}}async function ue(d,n){console.log(e.bold.cyan(`
11
+ `;await p(g,(h(g)?await w(g,"utf-8"):"")+v),console.log(e.green(`\u2705 Added to ${g}`)),console.log(e.yellow(`
12
+ \u{1F4A1} Run this to activate in current session:`)),console.log(e.gray(` source ${g}
13
+ `))}catch{}}async function be(d,o){console.log(e.bold.cyan(`
14
14
  \u{1F3AD} Welcome to Zibby Test Automation!
15
- `));const y=!n.skipMemory,m=["dolt","mem0"].includes(String(n.memoryBackend||"").toLowerCase())?String(n.memoryBackend).toLowerCase():"dolt";await W();const a=d?L(process.cwd(),d):process.cwd(),E=d||"zibby-tests",h=!!d;h&&f(a)&&(console.log(e.red(`
15
+ `));const y=!o.skipMemory,m=["dolt","mem0"].includes(String(o.memoryBackend||"").toLowerCase())?String(o.memoryBackend).toLowerCase():"dolt";await X();const l=d?O(process.cwd(),d):process.cwd(),$=d||"zibby-tests",b=!!d;b&&h(l)&&(console.log(e.red(`
16
16
  \u274C Directory "${d}" already exists!
17
- `)),process.exit(1)),!h&&f(i(a,".zibby.config.mjs"))&&!n.force&&(console.log(e.yellow(`
17
+ `)),process.exit(1)),!b&&h(a(l,".zibby.config.mjs"))&&!o.force&&(console.log(e.yellow(`
18
18
  \u26A0\uFE0F Zibby is already initialized in this directory!
19
19
  `)),console.log(e.white("Config file found: .zibby.config.mjs")),console.log(e.gray(`Use --force or -f to reinitialize
20
- `)),process.exit(0)),n.force&&!h&&console.log(e.cyan(`
20
+ `)),process.exit(0)),o.force&&!b&&console.log(e.cyan(`
21
21
  Reinitializing Zibby configuration...
22
- `));let s;if(n.agent&&(n.headed||n.headless))console.log(e.cyan(`Setting up with provided options...
23
- `)),s={agent:n.agent,browserMode:n.headless?"headless":"headed",apiKey:n.apiKey||null,cloudSync:!!(n.cloudSync||n.apiKey)};else{const b=[];n.agent||b.push({type:"select",name:"agent",message:"Which AI agent do you prefer?",choices:[{name:"Cursor",value:"cursor"},{name:"Claude (Anthropic)",value:"claude"},{name:"Codex (OpenAI)",value:"codex"},{name:"Gemini (Google)",value:"gemini"}],default:"cursor"}),!n.headed&&!n.headless&&b.push({type:"select",name:"browserMode",message:"Browser mode during live AI execution?",choices:[{name:"Headed - Visible browser (recommended for development)",value:"headed"},{name:"Headless - Hidden browser (for CI/CD)",value:"headless"}],default:"headed"}),n.apiKey||b.push({type:"input",name:"apiKey",message:"Enable cloud sync? Enter project ZIBBY_API_KEY (or press Enter to skip):"}),s=b.length>0?await D.prompt(b):{},s.agent=n.agent||s.agent,s.browserMode=n.headless?"headless":n.headed?"headed":s.browserMode,s.apiKey=n.apiKey||s.apiKey,s.cloudSync=!!(n.cloudSync||n.apiKey||s.apiKey&&s.apiKey.trim())}s.mcp="playwright",s.setupMcp=s.agent==="cursor";const p=v("Setting up Zibby...").start();try{if(h&&await _(a,{recursive:!0}),await _(i(a,"test-specs/examples"),{recursive:!0}),await _(i(a,"tests"),{recursive:!0}),await _(i(a,".zibby/output"),{recursive:!0}),await _(i(a,".zibby/commands"),{recursive:!0}),y&&m==="dolt")try{const{initMemory:t,DoltDB:r}=await import("@zibby/memory");if(r.isAvailable()){const{created:o}=t(a);o&&(p.text="Initialized test memory database (Dolt)...")}else p.text="Dolt not found \u2014 skipping memory database (brew install dolt)"}catch{}p.text="Scaffolding workflow graph...";const{TemplateFactory:b}=await import("@zibby/core/templates"),I=n.template||"browser-test-automation";try{const{graphPath:t,nodesPath:r,readmePath:o,resultHandlerPath:c,template:l}=b.getTemplateFiles(I),u=i(a,".zibby"),k=await w(t,"utf-8");if(await g(i(u,"graph.mjs"),k),c){const A=await w(c,"utf-8");await g(i(u,"result-handler.mjs"),A)}const M=await w(o,"utf-8");await g(i(u,"README.md"),M),await _(i(u,"nodes"),{recursive:!0});const{readdirSync:P}=await import("fs"),$=P(r);for(const A of $){let K=await w(i(r,A),"utf-8");!y&&A==="execute-live.mjs"&&(K=K.replace("skills: [SKILLS.BROWSER, SKILLS.MEMORY],","skills: [SKILLS.BROWSER],")),await g(i(u,"nodes",A),K)}const R=i(l.path,"chat.mjs");if(f(R)){const A=await w(R,"utf-8");await g(i(u,"chat.mjs"),A)}}catch(t){throw p.fail(`Failed to scaffold template: ${t.message}`),t}p.text="Generating configuration files...";const z=U(s,n,{memoryBackend:m});if(await g(i(a,".zibby.config.mjs"),z),await g(i(a,".env.example"),V(s,m)),s.apiKey&&s.apiKey.trim()){const t=i(a,".env"),r=s.apiKey.trim();if(f(t)){let o=await w(t,"utf8");/^ZIBBY_API_KEY=/m.test(o)?o=o.replace(/^ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):/^#\s*ZIBBY_API_KEY=/m.test(o)?o=o.replace(/^#\s*ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):o=`${o.trimEnd()}
22
+ `));let s;if(o.agent&&(o.headed||o.headless))console.log(e.cyan(`Setting up with provided options...
23
+ `)),s={agent:o.agent,browserMode:o.headless?"headless":"headed",apiKey:o.apiKey||null,cloudSync:!!(o.cloudSync||o.apiKey)};else{if(o.agent)s={agent:o.agent};else{const{agent:C}=await Y.prompt([{type:"select",name:"agent",message:"Which AI agent do you prefer?",choices:[{name:"Cursor",value:"cursor"},{name:"Claude (Anthropic)",value:"claude"},{name:"Codex (OpenAI)",value:"codex"},{name:"Gemini (Google)",value:"gemini"}],default:"cursor"}]);s={agent:C}}const f=D[s.agent];if(f&&!o.agentKey&&!(process.env.CI||process.env.ZIBBY_CI||!process.stdin.isTTY)){const C=process.env[f.envVar]||W().agentApiKey,B=C?`***${C.slice(-4)}`:null,E=B?`${f.label} found (${B}). Press Enter to keep, or paste a new key:`:`Enter ${f.label} (from ${f.url}, or press Enter to skip):`,{agentKey:t}=await Y.prompt([{type:"password",name:"agentKey",message:E,mask:"*"}]);t&&t.trim()&&(s.agentKey=t.trim())}const x=[];if(!o.headed&&!o.headless&&x.push({type:"select",name:"browserMode",message:"Browser mode during live AI execution?",choices:[{name:"Headed - Visible browser (recommended for development)",value:"headed"},{name:"Headless - Hidden browser (for CI/CD)",value:"headless"}],default:"headed"}),o.apiKey||x.push({type:"input",name:"apiKey",message:"Enable cloud sync? Enter project ZIBBY_API_KEY (or press Enter to skip):"}),x.length>0){const C=await Y.prompt(x);Object.assign(s,C)}s.browserMode=o.headless?"headless":o.headed?"headed":s.browserMode,s.apiKey=o.apiKey||s.apiKey,s.cloudSync=!!(o.cloudSync||o.apiKey||s.apiKey&&s.apiKey.trim())}o.agentKey&&(s.agentKey=o.agentKey);const g=D[s.agent];s.agentKey&&(F(s.agentKey,s.agent),console.log(e.gray(" \u2713 Agent API key saved to ~/.zibby/config.json"))),s.mcp="playwright",s.setupMcp=s.agent==="cursor";const v=I("Setting up Zibby...").start();try{if(b&&await S(l,{recursive:!0}),await S(a(l,"test-specs/examples"),{recursive:!0}),await S(a(l,"tests"),{recursive:!0}),await S(a(l,".zibby/output"),{recursive:!0}),await S(a(l,".zibby/commands"),{recursive:!0}),y&&m==="dolt")try{const{initMemory:t,DoltDB:i}=await import("@zibby/memory");if(i.isAvailable()){const{created:n}=t(l);n&&(v.text="Initialized test memory database (Dolt)...")}else v.text="Dolt not found \u2014 skipping memory database (brew install dolt)"}catch{}v.text="Scaffolding workflow graph...";const{TemplateFactory:f}=await import("@zibby/core/templates"),x=o.template||"browser-test-automation";try{const{graphPath:t,nodesPath:i,readmePath:n,resultHandlerPath:c,template:r}=f.getTemplateFiles(x),u=a(l,".zibby"),M=await w(t,"utf-8");if(await p(a(u,"graph.mjs"),M),c){const k=await w(c,"utf-8");await p(a(u,"result-handler.mjs"),k)}const z=await w(n,"utf-8");await p(a(u,"README.md"),z),await S(a(u,"nodes"),{recursive:!0});const{readdirSync:_}=await import("fs"),T=_(i);for(const k of T){let j=await w(a(i,k),"utf-8");!y&&k==="execute-live.mjs"&&(j=j.replace("skills: [SKILLS.BROWSER, SKILLS.MEMORY],","skills: [SKILLS.BROWSER],")),await p(a(u,"nodes",k),j)}const L=a(r.path,"chat.mjs");if(h(L)){const k=await w(L,"utf-8");await p(a(u,"chat.mjs"),k)}}catch(t){throw v.fail(`Failed to scaffold template: ${t.message}`),t}v.text="Generating configuration files...";const C=J(s,o,{memoryBackend:m});if(await p(a(l,".zibby.config.mjs"),C),await p(a(l,".env.example"),Q(s,m)),s.apiKey&&s.apiKey.trim()){const t=a(l,".env"),i=s.apiKey.trim();if(h(t)){let n=await w(t,"utf8");/^ZIBBY_API_KEY=/m.test(n)?n=n.replace(/^ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${i}`):/^#\s*ZIBBY_API_KEY=/m.test(n)?n=n.replace(/^#\s*ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${i}`):n=`${n.trimEnd()}
24
24
 
25
25
  # Zibby Cloud Sync
26
- ZIBBY_API_KEY=${r}
27
- `,await g(t,o)}else await g(t,q(s,r,m))}if(h){const t=X(E,s,{memoryBackend:m});await g(i(a,"package.json"),t)}if(!f(i(a,".gitignore"))){const t=J();await g(i(a,".gitignore"),t)}if(!f(i(a,"playwright.config.js"))){const t=Q("on");await g(i(a,"playwright.config.js"),t)}if(!f(i(a,"test-specs/examples/example-domain.txt"))){const t=ee();await g(i(a,"test-specs/examples/example-domain.txt"),t)}const Y=L(F,"../../../../examples/.zibby/commands");if(f(Y)){const t=j(Y).filter(r=>r.toLowerCase().endsWith(".md"));for(const r of t){const o=i(a,".zibby/commands",r);if(n.force||!f(o)){const c=await w(i(Y,r),"utf-8");await g(o,c)}}}if(!f(i(a,".zibby/commands/example.md"))){const t=te();await g(i(a,".zibby/commands/example.md"),t)}if(h){const t=ne(E,s);await g(i(a,"README.md"),t)}if(p.succeed(h?"Project created!":"Zibby initialized!"),h&&!n.skipInstall){const t=v("Installing dependencies...").start();await new Promise((r,o)=>{S("npm",["install"],{cwd:a,stdio:"pipe"}).on("close",l=>{l===0?(t.succeed("Dependencies installed!"),r()):(t.fail("Failed to install dependencies"),o(new Error("npm install failed")))})})}else if(!h){const t=["@zibby/cli","@zibby/core"],r=[];for(const o of t)try{O(i(a,"package.json")).resolve(`${o}/package.json`)}catch{r.push(o)}if(r.length>0){const o=Z("../../package.json"),c=r.map(l=>l==="@zibby/cli"?`${l}@latest`:l==="@zibby/core"?`${l}@${o.dependencies?.["@zibby/core"]||"^0.1.29"}`:l);if(n.skipInstall)console.log(e.yellow(`
28
- Missing required Zibby dependencies in this project:`)),console.log(e.white(` ${r.join(", ")}`)),console.log(e.gray("Install them manually to avoid runtime errors:")),console.log(e.white(` npm install --save-dev ${c.join(" ")}
29
- `));else{const l=v(`Installing ${c.join(", ")}...`).start();await new Promise(k=>{S("npm",["install","--save-dev",...c],{cwd:a,stdio:"pipe"}).on("close",P=>k(P??1))})===0?l.succeed("Installed missing Zibby dependencies"):(l.warn("Could not auto-install Zibby dependencies"),console.log(e.gray("Run this manually:")),console.log(e.white(` npm install --save-dev ${c.join(" ")}
30
- `)))}}}if(!h&&y&&m==="mem0"&&(console.log(e.yellow(`
26
+ ZIBBY_API_KEY=${i}
27
+ `,await p(t,n)}else await p(t,ee(s,i,m))}if(s.agentKey&&g){const t=a(l,".env"),i=s.agentKey;if(h(t)){let n=await w(t,"utf8");const c=new RegExp(`^${g.envVar}=.*`,"m"),r=new RegExp(`^#\\s*${g.envVar}=.*`,"m");c.test(n)?n=n.replace(c,`${g.envVar}=${i}`):r.test(n)?n=n.replace(r,`${g.envVar}=${i}`):n=`${n.trimEnd()}
28
+
29
+ # AI Agent Key
30
+ ${g.envVar}=${i}
31
+ `,await p(t,n)}}if(b){const t=te($,s,{memoryBackend:m});await p(a(l,"package.json"),t)}if(!h(a(l,".gitignore"))){const t=ne();await p(a(l,".gitignore"),t)}if(!h(a(l,"playwright.config.js"))){const t=oe("on");await p(a(l,"playwright.config.js"),t)}if(!h(a(l,"test-specs/examples/example-domain.txt"))){const t=se();await p(a(l,"test-specs/examples/example-domain.txt"),t)}const B=O(q,"../../../../examples/.zibby/commands");if(h(B)){const t=G(B).filter(i=>i.toLowerCase().endsWith(".md"));for(const i of t){const n=a(l,".zibby/commands",i);if(o.force||!h(n)){const c=await w(a(B,i),"utf-8");await p(n,c)}}}if(!h(a(l,".zibby/commands/example.md"))){const t=ie();await p(a(l,".zibby/commands/example.md"),t)}if(b){const t=ae($,s);await p(a(l,"README.md"),t)}if(v.succeed(b?"Project created!":"Zibby initialized!"),b&&!o.skipInstall){const t=I("Installing dependencies...").start();await new Promise((i,n)=>{K("npm",["install"],{cwd:l,stdio:"pipe"}).on("close",r=>{r===0?(t.succeed("Dependencies installed!"),i()):(t.fail("Failed to install dependencies"),n(new Error("npm install failed")))})})}else if(!b){const t=["@zibby/cli","@zibby/core"],i=[];for(const n of t)try{Z(a(l,"package.json")).resolve(`${n}/package.json`)}catch{i.push(n)}if(i.length>0){const n=N("../../package.json"),c=i.map(r=>r==="@zibby/cli"?`${r}@latest`:r==="@zibby/core"?`${r}@${n.dependencies?.["@zibby/core"]||"^0.1.29"}`:r);if(o.skipInstall)console.log(e.yellow(`
32
+ Missing required Zibby dependencies in this project:`)),console.log(e.white(` ${i.join(", ")}`)),console.log(e.gray("Install them manually to avoid runtime errors:")),console.log(e.white(` npm install --save-dev ${c.join(" ")}
33
+ `));else{const r=I(`Installing ${c.join(", ")}...`).start();await new Promise(M=>{K("npm",["install","--save-dev",...c],{cwd:l,stdio:"pipe"}).on("close",_=>M(_??1))})===0?r.succeed("Installed missing Zibby dependencies"):(r.warn("Could not auto-install Zibby dependencies"),console.log(e.gray("Run this manually:")),console.log(e.white(` npm install --save-dev ${c.join(" ")}
34
+ `)))}}}if(!b&&y&&m==="mem0"&&(console.log(e.yellow(`
31
35
  Using mem0 backend requires mem0ai in your project dependencies.`)),console.log(e.gray("Run this manually in your project when ready:")),console.log(e.white(` npm install mem0ai
32
- `))),!n.skipInstall){const t=v("Installing Playwright browsers...").start();await new Promise(r=>{const o=S("npx",["playwright","install","chromium"],{cwd:a,stdio:"pipe"});let c="";o.stdout.on("data",l=>{c+=l.toString()}),o.stderr.on("data",l=>{c+=l.toString()}),o.on("close",l=>{l===0?(c.includes("already installed")||c.includes("up to date")?t.succeed("Playwright browsers already installed"):t.succeed("Playwright browsers installed!"),r()):(t.warn("Could not verify Playwright browsers"),console.log(e.yellow(`
36
+ `))),!o.skipInstall){const t=I("Installing Playwright browsers...").start();await new Promise(i=>{const n=K("npx",["playwright","install","chromium"],{cwd:l,stdio:"pipe"});let c="";n.stdout.on("data",r=>{c+=r.toString()}),n.stderr.on("data",r=>{c+=r.toString()}),n.on("close",r=>{r===0?(c.includes("already installed")||c.includes("up to date")?t.succeed("Playwright browsers already installed"):t.succeed("Playwright browsers installed!"),i()):(t.warn("Could not verify Playwright browsers"),console.log(e.yellow(`
33
37
  \u26A0\uFE0F If tests fail, run: npx playwright install
34
- `)),r())})})}if(s.agent==="cursor"&&!n.skipInstall){const t=v("Checking cursor-agent CLI...").start();try{T("cursor-agent --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("cursor-agent CLI already installed")}catch{t.text="Installing cursor-agent CLI...";try{await new Promise((r,o)=>{S("bash",["-c","curl https://cursor.com/install -fsS | bash"],{stdio:"pipe"}).on("close",l=>{if(l===0){const u=i(C(),".local","bin");process.env.PATH.includes(u)||(process.env.PATH=`${u}:${process.env.PATH}`),t.succeed("cursor-agent CLI installed!"),r()}else o(new Error("cursor-agent install failed"))})})}catch{t.fail("Could not install cursor-agent CLI"),console.log(e.yellow(` Install manually: curl https://cursor.com/install -fsS | bash
35
- `))}}}if(s.agent==="codex"&&!n.skipInstall){const t=v("Checking Codex CLI...").start();try{T("codex --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("Codex CLI already installed")}catch{t.text="Installing Codex CLI...";try{await new Promise((r,o)=>{S("npm",["install","-g","@openai/codex"],{stdio:"pipe"}).on("close",l=>{l===0?(t.succeed("Codex CLI installed!"),r()):o(new Error("npm install -g @openai/codex failed"))})})}catch{t.fail("Could not install Codex CLI"),console.log(e.yellow(` Install manually: npm install -g @openai/codex
36
- `))}}}if(s.agent==="gemini"&&!n.skipInstall){const t=v("Checking Gemini CLI...").start();try{T("gemini --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("Gemini CLI already installed")}catch{t.text="Installing Gemini CLI...";try{await new Promise((o,c)=>{S("npm",["install","-g","@google/gemini-cli"],{stdio:"pipe"}).on("close",u=>{u===0?(t.succeed("Gemini CLI installed!"),o()):c(new Error("npm install -g @google/gemini-cli failed"))})})}catch{t.fail("Could not install Gemini CLI"),console.log(e.yellow(` Install manually: npm install -g @google/gemini-cli
37
- `))}}const r=v("Configuring Gemini MCP servers...").start();try{const o=i(C(),".gemini"),c=i(o,"settings.json");f(o)||await _(o,{recursive:!0});let l={mcpServers:{}};if(f(c))try{const $=await w(c,"utf-8");l=JSON.parse($),l.mcpServers||(l.mcpServers={})}catch{}let u;try{const{createRequire:$}=await import("module");u=$(import.meta.url).resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js")}catch{u="@playwright/mcp"}const k=s.browserMode!=="headless",M=u==="@playwright/mcp"?["-y","@playwright/mcp","--isolated","--save-video=1280x720","--viewport-size=1280x720","--output-dir","test-results"]:[u,"--isolated","--save-video=1280x720","--viewport-size=1280x720","--output-dir=test-results"];k||M.push("--headless"),l.mcpServers["playwright-official"]={command:u==="@playwright/mcp"?"npx":"node",args:M},await g(c,`${JSON.stringify(l,null,2)}
38
- `,"utf-8");let P="Gemini MCP configured";k?P+=" (headed mode - visible browser)":P+=" (headless mode - hidden browser)",r.succeed(P)}catch(o){r.fail("MCP setup failed"),console.log(e.yellow(" You may need to configure ~/.gemini/settings.json manually")),console.log(e.gray(` Error: ${o.message}
39
- `))}}if(s.agent==="cursor"&&s.setupMcp){const t=v("Setting up Playwright MCP...").start();try{const{setupPlaywrightMcpCommand:r}=await import("./setup-scripts.js"),o=s.cloudSync||!1,c=s.browserMode!=="headless";await r({headed:c,cloudSync:o,video:"on",viewport:{width:1280,height:720}});let l="Playwright MCP configured";c?l+=" (headed mode - visible browser)":l+=" (headless mode - hidden browser)",o&&(l+=" + Zibby MCP (cloud sync)"),t.succeed(l),o&&!(s.apiKey&&s.apiKey.trim())&&console.log(e.gray(`
38
+ `)),i())})})}if(s.agent==="cursor"&&!o.skipInstall){const t=I("Checking cursor-agent CLI...").start();try{R("cursor-agent --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("cursor-agent CLI already installed")}catch{t.text="Installing cursor-agent CLI...";try{await new Promise((i,n)=>{K("bash",["-c","curl https://cursor.com/install -fsS | bash"],{stdio:"pipe"}).on("close",r=>{if(r===0){const u=a(P(),".local","bin");process.env.PATH.includes(u)||(process.env.PATH=`${u}:${process.env.PATH}`),t.succeed("cursor-agent CLI installed!"),i()}else n(new Error("cursor-agent install failed"))})})}catch{t.fail("Could not install cursor-agent CLI"),console.log(e.yellow(` Install manually: curl https://cursor.com/install -fsS | bash
39
+ `))}}}if(s.agent==="codex"&&!o.skipInstall){const t=I("Checking Codex CLI...").start();try{R("codex --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("Codex CLI already installed")}catch{t.text="Installing Codex CLI...";try{await new Promise((i,n)=>{K("npm",["install","-g","@openai/codex"],{stdio:"pipe"}).on("close",r=>{r===0?(t.succeed("Codex CLI installed!"),i()):n(new Error("npm install -g @openai/codex failed"))})})}catch{t.fail("Could not install Codex CLI"),console.log(e.yellow(` Install manually: npm install -g @openai/codex
40
+ `))}}}if(s.agent==="gemini"&&!o.skipInstall){const t=I("Checking Gemini CLI...").start();try{R("gemini --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),t.succeed("Gemini CLI already installed")}catch{t.text="Installing Gemini CLI...";try{await new Promise((n,c)=>{K("npm",["install","-g","@google/gemini-cli"],{stdio:"pipe"}).on("close",u=>{u===0?(t.succeed("Gemini CLI installed!"),n()):c(new Error("npm install -g @google/gemini-cli failed"))})})}catch{t.fail("Could not install Gemini CLI"),console.log(e.yellow(` Install manually: npm install -g @google/gemini-cli
41
+ `))}}const i=I("Configuring Gemini MCP servers...").start();try{const n=a(P(),".gemini"),c=a(n,"settings.json");h(n)||await S(n,{recursive:!0});let r={mcpServers:{}};if(h(c))try{const T=await w(c,"utf-8");r=JSON.parse(T),r.mcpServers||(r.mcpServers={})}catch{}let u;try{const{createRequire:T}=await import("module");u=T(import.meta.url).resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js")}catch{u="@playwright/mcp"}const M=s.browserMode!=="headless",z=u==="@playwright/mcp"?["-y","@playwright/mcp","--isolated","--viewport-size=1280x720","--output-dir","test-results"]:[u,"--isolated","--viewport-size=1280x720","--output-dir=test-results"];M||z.push("--headless"),r.mcpServers["playwright-official"]={command:u==="@playwright/mcp"?"npx":"node",args:z},await p(c,`${JSON.stringify(r,null,2)}
42
+ `,"utf-8");let _="Gemini MCP configured";M?_+=" (headed mode - visible browser)":_+=" (headless mode - hidden browser)",i.succeed(_)}catch(n){i.fail("MCP setup failed"),console.log(e.yellow(" You may need to configure ~/.gemini/settings.json manually")),console.log(e.gray(` Error: ${n.message}
43
+ `))}}if(s.agent==="cursor"&&s.setupMcp){const t=I("Setting up Playwright MCP...").start();try{const{setupPlaywrightMcpCommand:i}=await import("./setup-scripts.js"),n=s.cloudSync||!1,c=s.browserMode!=="headless";await i({headed:c,cloudSync:n,viewport:{width:1280,height:720}});let r="Playwright MCP configured";c?r+=" (headed mode - visible browser)":r+=" (headless mode - hidden browser)",n&&(r+=" + Zibby MCP (cloud sync)"),t.succeed(r),n&&!(s.apiKey&&s.apiKey.trim())&&console.log(e.gray(`
40
44
  Copy .env.example to .env and set ZIBBY_API_KEY to enable uploads
41
- `))}catch(r){t.fail("MCP setup script failed"),console.log(e.yellow(" Check if MCP is already configured:")),console.log(e.gray(' \u2022 Open Cursor settings \u2192 Check "playwright-official" MCP')),console.log(e.gray(" \u2022 Run: cursor-agent mcp list")),console.log(e.gray(` \u2022 Or run manually: zibby setup-playwright
42
- `)),console.log(e.gray(` Error: ${r.message}
45
+ `))}catch(i){t.fail("MCP setup script failed"),console.log(e.yellow(" Check if MCP is already configured:")),console.log(e.gray(' \u2022 Open Cursor settings \u2192 Check "playwright-official" MCP')),console.log(e.gray(" \u2022 Run: cursor-agent mcp list")),console.log(e.gray(` \u2022 Or run manually: zibby setup-playwright
46
+ `)),console.log(e.gray(` Error: ${i.message}
43
47
  `))}}console.log(e.bold.green(`
44
48
  \u{1F389} All set!
45
- `)),console.log(e.cyan("Start the Zibby Chat Agent:")),h&&console.log(e.white(` cd ${d}`)),console.log(e.white(` zibby
49
+ `)),console.log(e.cyan("Start the Zibby Chat Agent:")),b&&console.log(e.white(` cd ${d}`)),console.log(e.white(` zibby
46
50
  `)),console.log(e.cyan("Or run a test directly:")),console.log(e.white(` zibby run test-specs/examples/example-domain.txt
47
- `)),console.log(e.cyan("Next steps:"));let B=1;console.log(e.white(` ${B++}. cp .env.example .env ${e.gray("# then add your API keys")}`)),y&&console.log(e.white(` ${B++}. Set ${e.bold("ZIBBY_MEMORY_BACKEND")} in .env ${e.gray(`# currently: ${m}`)}`)),console.log(e.white(` ${B++}. Type ${e.bold("zibby")} to chat with the AI testing assistant`)),console.log(e.white(` ${B++}. Write test specs in test-specs/`)),console.log(e.white(` ${B++}. Run: npx zibby run <spec-file>
48
- `))}catch(b){p.fail("Failed to create project"),console.error(e.red(`
49
- \u274C Error: ${b.message}
50
- `)),process.exit(1)}}function U(d,n={},y={}){const m=["dolt","mem0"].includes(String(y.memoryBackend||"").toLowerCase())?String(y.memoryBackend).toLowerCase():"dolt",a={claude:`
51
+ `)),console.log(e.cyan("Next steps:"));let E=1;console.log(e.white(` ${E++}. cp .env.example .env ${e.gray("# then add your API keys")}`)),y&&console.log(e.white(` ${E++}. Set ${e.bold("ZIBBY_MEMORY_BACKEND")} in .env ${e.gray(`# currently: ${m}`)}`)),console.log(e.white(` ${E++}. Type ${e.bold("zibby")} to chat with the AI testing assistant`)),console.log(e.white(` ${E++}. Write test specs in test-specs/`)),console.log(e.white(` ${E++}. Run: npx zibby run <spec-file>
52
+ `))}catch(f){v.fail("Failed to create project"),console.error(e.red(`
53
+ \u274C Error: ${f.message}
54
+ `)),process.exit(1)}}function J(d,o={},y={}){const m=["dolt","mem0"].includes(String(y.memoryBackend||"").toLowerCase())?String(y.memoryBackend).toLowerCase():"dolt",l={claude:`
51
55
  claude: {
52
56
  model: 'auto', // Options: 'auto', 'sonnet-4.6', 'opus-4.6', 'sonnet-4.5', 'opus-4.5'
53
57
  maxTokens: 4096,
@@ -60,13 +64,13 @@ Using mem0 backend requires mem0ai in your project dependencies.`)),console.log(
60
64
  },`,gemini:`
61
65
  gemini: {
62
66
  model: 'gemini-2.5-pro', // Options: 'auto', 'gemini-2.5-pro', 'gemini-2.5-flash'
63
- },`},E=d.agent,h=Object.entries(a).filter(([s])=>s!==E).map(([,s])=>s.split(`
64
- `).map(x=>x.trim()?` // ${x.trimStart()}`:x).join(`
67
+ },`},$=d.agent,b=Object.entries(l).filter(([s])=>s!==$).map(([,s])=>s.split(`
68
+ `).map(A=>A.trim()?` // ${A.trimStart()}`:A).join(`
65
69
  `)).join(`
66
70
  `);return`export default {
67
71
  // AI agent settings
68
- agent: {${a[E]}
69
- ${h}
72
+ agent: {${l[$]}
73
+ ${b}
70
74
  strictMode: false,
71
75
  },
72
76
 
@@ -129,7 +133,7 @@ ${h}
129
133
  // Cloud sync - auto-upload test results & videos (requires ZIBBY_API_KEY in .env)
130
134
  cloudSync: ${d.cloudSync||!1}
131
135
  };
132
- `}function V(d,n="dolt"){return`# Zibby Test Automation - Environment Variables
136
+ `}function Q(d,o="dolt"){return`# Zibby Test Automation - Environment Variables
133
137
 
134
138
  # AI Provider Keys
135
139
  ${{claude:"ANTHROPIC_API_KEY=sk-ant-your_key_here",cursor:`# Cursor Agent uses cursor-agent CLI \u2014 no API key needed
@@ -145,14 +149,14 @@ ${{claude:"ANTHROPIC_API_KEY=sk-ant-your_key_here",cursor:`# Cursor Agent uses c
145
149
  # ZIBBY_MEMORY_COMPACT_EVERY=1500 # Auto-compact every N runs (0 to disable)
146
150
 
147
151
  # Chat memory backend
148
- ZIBBY_MEMORY_BACKEND=${n}
149
- `}function q(d,n,y="dolt"){return`# Zibby Test Automation - Environment Variables
152
+ ZIBBY_MEMORY_BACKEND=${o}
153
+ `}function ee(d,o,y="dolt"){return`# Zibby Test Automation - Environment Variables
150
154
 
151
155
  # AI Provider Keys
152
156
  ${{claude:"ANTHROPIC_API_KEY=sk-ant-your_key_here",cursor:"# Cursor Agent uses cursor-agent CLI \u2014 no API key needed",codex:"OPENAI_API_KEY=sk-your_key_here",gemini:"GEMINI_API_KEY=your_key_here"}[d.agent]||""}
153
157
 
154
158
  # Zibby Cloud Sync
155
- ZIBBY_API_KEY=${n}
159
+ ZIBBY_API_KEY=${o}
156
160
 
157
161
  # Test Memory (Dolt DB) - Auto-compaction settings
158
162
  # ZIBBY_MEMORY_MAX_RUNS=3000 # Max test runs to keep per spec
@@ -161,7 +165,7 @@ ZIBBY_API_KEY=${n}
161
165
 
162
166
  # Chat memory backend
163
167
  ZIBBY_MEMORY_BACKEND=${y}
164
- `}function X(d,n,y={}){const a={"@zibby/cli":"latest","@zibby/core":Z("../../package.json").dependencies?.["@zibby/core"]||"^0.1.29"};return y.memoryBackend==="mem0"&&(a.mem0ai="^2.4.6"),JSON.stringify({name:d,version:"1.0.0",type:"module",private:!0,scripts:{"test:spec":"zibby run",test:"playwright test","test:headed":"playwright test --headed"},dependencies:a,devDependencies:{"@playwright/test":"^1.49.0",dotenv:"^17.2.3"}},null,2)}function J(){return`# Dependencies
168
+ `}function te(d,o,y={}){const l={"@zibby/cli":"latest","@zibby/core":N("../../package.json").dependencies?.["@zibby/core"]||"^0.1.29"};return y.memoryBackend==="mem0"&&(l.mem0ai="^2.4.6"),JSON.stringify({name:d,version:"1.0.0",type:"module",private:!0,scripts:{"test:spec":"zibby run",test:"playwright test","test:headed":"playwright test --headed"},dependencies:l,devDependencies:{"@playwright/test":"^1.49.0",dotenv:"^17.2.3"}},null,2)}function ne(){return`# Dependencies
165
169
  node_modules/
166
170
 
167
171
  # Test artifacts
@@ -189,11 +193,11 @@ Thumbs.db
189
193
  # Zibby memory (Dolt DB synced separately via dolt remote, not git)
190
194
  .zibby/memory/
191
195
  .zibby/memory-context.md
192
- `}function Q(d="off",n=null){return`import { defineConfig} from '@playwright/test';
196
+ `}function oe(d="off",o=null){return`import { defineConfig} from '@playwright/test';
193
197
 
194
198
  export default defineConfig({
195
199
  testDir: './tests',
196
- ${n?` outputDir: '${n}',
200
+ ${o?` outputDir: '${o}',
197
201
  `:` outputDir: 'test-results/playwright', // Keep Playwright artifacts separate from Zibby workflow output
198
202
  `} timeout: 30000,
199
203
  retries: 0,
@@ -211,7 +215,7 @@ ${n?` outputDir: '${n}',
211
215
  ['list']
212
216
  ],
213
217
  });
214
- `}function ee(){return`Test Specification: Example Domain Navigation
218
+ `}function se(){return`Test Specification: Example Domain Navigation
215
219
  ==============================================
216
220
 
217
221
  Application: Example Domain
@@ -248,7 +252,7 @@ Notes:
248
252
  - Uses a stable, public domain (example.com) maintained by IANA
249
253
  - No authentication required
250
254
  - Suitable for CI/CD smoke testing
251
- `}function te(){return`# Example command template
255
+ `}function ie(){return`# Example command template
252
256
 
253
257
  Use this command when the user asks for a concise testing task breakdown.
254
258
 
@@ -259,7 +263,7 @@ Required output format:
259
263
  4. Expected result
260
264
 
261
265
  Keep the response actionable and short.
262
- `}function ne(d,n){return`# ${d}
266
+ `}function ae(d,o){return`# ${d}
263
267
 
264
268
  AI-powered test automation with Zibby.
265
269
 
@@ -273,7 +277,7 @@ npm install
273
277
  2. Configure environment:
274
278
  \`\`\`bash
275
279
  cp .env.example .env
276
- # Edit .env and add your ${{claude:"ANTHROPIC_API_KEY",codex:"OPENAI_API_KEY",gemini:"GEMINI_API_KEY",cursor:"CURSOR_API_KEY (optional)"}[n.agent]}
280
+ # Edit .env and add your ${{claude:"ANTHROPIC_API_KEY",codex:"OPENAI_API_KEY",gemini:"GEMINI_API_KEY",cursor:"CURSOR_API_KEY (optional)"}[o.agent]}
277
281
  \`\`\`
278
282
 
279
283
  3. Run example test:
@@ -324,7 +328,7 @@ npx playwright test --ui
324
328
  Edit \`.zibby.config.mjs\` to customize:
325
329
  - Agent settings (model, temperature)
326
330
  - Browser settings (headless, viewport)
327
- - Cloud sync${n.cloudSync?" (enabled)":" (disabled)"}
331
+ - Cloud sync${o.cloudSync?" (enabled)":" (disabled)"}
328
332
  - Self-healing behavior
329
333
 
330
334
  ## Project Structure
@@ -348,4 +352,4 @@ ${d}/
348
352
 
349
353
  - Documentation: https://docs.zibby.dev
350
354
  - Examples: https://github.com/zibby/examples
351
- `}export{ue as initCommand};
355
+ `}export{be as initCommand};
@@ -1,15 +1,15 @@
1
- import{spawn as y,execSync as w}from"child_process";import{join as l,dirname as v,resolve as P}from"path";import{readFileSync as C,writeFileSync as S,existsSync as u,mkdirSync as x}from"fs";import{homedir as j}from"os";import{createRequire as A}from"module";import c from"chalk";const b=A(import.meta.url);function B(){try{return v(b.resolve("@zibby/core/package.json"))}catch{return null}}function $(e){try{return w(`command -v ${e}`,{stdio:"pipe"}),!0}catch{return!1}}function E(){try{const e=b.resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js");if(u(e))return e}catch{}try{const e=b.resolve("@playwright/mcp/package.json"),r=v(e),o=JSON.parse(C(e,"utf-8")),n=typeof o.bin=="string"?o.bin:o.bin?.["playwright-mcp"];if(n){const s=l(r,n);if(u(s))return s}const t=l(r,"cli.js");if(u(t))return t}catch{}return null}function k(e,r,o={}){return new Promise((n,t)=>{const s=y(e,r,{stdio:"inherit",...o});s.on("close",p=>n(p??1)),s.on("error",t)})}async function N(e){return $("playwright")?await k("playwright",["install","ffmpeg"],{cwd:e})===0:await k("npx",["--yes","playwright","install","ffmpeg"],{cwd:e})===0}function O({headed:e=!1,video:r="on",viewport:o=null}){const n=process.execPath,t=v(n),s=E(),p=Number(o?.width)||1280,d=Number(o?.height)||720,f=`${p}x${d}`,i=[];r!=="off"&&i.push(`--save-video=${f}`),i.push(`--viewport-size=${f}`,"--output-dir=test-results"),e||i.push("--headless");let g;s?g={command:n,args:[s,...i],env:{PATH:`${t}:/usr/bin:/bin:/usr/sbin:/sbin`}}:g={command:"npx",args:["-y","@playwright/mcp",...i],env:{PATH:`${t}:/usr/bin:/bin:/usr/sbin:/sbin`}},g.description="Zibby MCP Browser - Playwright MCP with Zibby defaults";const h=l(j(),".cursor"),m=l(h,"mcp.json");u(h)||x(h,{recursive:!0});let a={mcpServers:{}};if(u(m))try{a=JSON.parse(C(m,"utf-8")),(!a.mcpServers||typeof a.mcpServers!="object")&&(a.mcpServers={})}catch{a={mcpServers:{}}}a.mcpServers["playwright-official"]=g,S(m,`${JSON.stringify(a,null,2)}
2
- `,"utf-8")}async function R(){const e=B();if(!e)return;const r=l(e,"scripts","patch-cursor-mcp.js");if(u(r))try{await new Promise((o,n)=>{const t=y(process.execPath,[r],{cwd:process.cwd(),stdio:"ignore"});t.on("close",s=>o(s??0)),t.on("error",n)})}catch{}}function z(e){return P(e).replace(/^\/+/,"").replace(/\//g,"-")}function J(e){const r=String(e||""),o=r.split(`
3
- `).find(s=>/playwright-official/i.test(s)&&/APPROVAL|🔑/i.test(s));if(o){const s=o.match(/=>\s*(\S+)/);if(s)return s[1]}const n=r.match(/playwright-official[^=]*=>\s*(\S+)/);if(n)return n[1];const t=r.match(/playwright-official-[a-f0-9]+/i);return t?t[0]:null}function T(e,r){const o=z(e),n=l(j(),".cursor","projects",o);x(n,{recursive:!0});const t=l(n,"mcp-approvals.json");return S(t,`${JSON.stringify([r],null,2)}
4
- `,"utf-8"),t}function M(e){try{return w("cursor-agent mcp list",{cwd:e,encoding:"utf-8",stdio:["ignore","pipe","pipe"],env:process.env,maxBuffer:2*1024*1024})}catch(r){const o=r.stdout!=null?String(r.stdout):"",n=r.stderr!=null?String(r.stderr):"";return o+n}}function D(e){try{return w("cursor-agent mcp enable playwright-official",{cwd:e,encoding:"utf-8",stdio:["ignore","pipe","pipe"],env:process.env,maxBuffer:1024*1024}),!0}catch{return!1}}async function F(e={}){const r=!!e.headed,o=e.video||"on",n=e.viewport||{width:1280,height:720};if(!process.execPath)throw new Error("Node.js runtime not found");const t=await N(process.cwd());console.log(t?c.gray(" \u2713 ffmpeg installed for Playwright video"):c.yellow(" \u26A0 Could not install ffmpeg (video may be unavailable)")),O({headed:r,video:o,viewport:n}),await R();const s=P(process.cwd());if($("cursor-agent")){M(s);const p=D(s);p&&console.log(c.gray(" \u2713 cursor-agent: mcp enable playwright-official"));const d=M(s),f=J(d);if(f)try{const i=T(s,f);console.log(c.gray(` \u2713 MCP auto-approval key saved (${i})`))}catch(i){console.log(c.yellow(` \u26A0 Could not write mcp-approvals.json: ${i.message}`))}else p||(console.log(c.yellow(" \u26A0 Could not auto-approve MCP (mcp enable failed and no approval key in cursor-agent output)")),console.log(c.gray(" Try: cursor-agent mcp enable playwright-official")))}else console.log(c.yellow(" \u26A0 cursor-agent not found; install it to use Cursor MCP tools"))}async function L(e){try{console.log(c.cyan(`
1
+ import{spawn as m,execSync as y}from"child_process";import{join as l,dirname as w,resolve as b}from"path";import{readFileSync as P,writeFileSync as C,existsSync as p,mkdirSync as S}from"fs";import{homedir as x}from"os";import{createRequire as M}from"module";import c from"chalk";const v=M(import.meta.url);function A(){try{return w(v.resolve("@zibby/core/package.json"))}catch{return null}}function j(e){try{return y(`command -v ${e}`,{stdio:"pipe"}),!0}catch{return!1}}function B(){try{const e=v.resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js");if(p(e))return e}catch{}try{const e=v.resolve("@playwright/mcp/package.json"),r=w(e),o=JSON.parse(P(e,"utf-8")),n=typeof o.bin=="string"?o.bin:o.bin?.["playwright-mcp"];if(n){const s=l(r,n);if(p(s))return s}const t=l(r,"cli.js");if(p(t))return t}catch{}return null}function $(e,r,o={}){return new Promise((n,t)=>{const s=m(e,r,{stdio:"inherit",...o});s.on("close",u=>n(u??1)),s.on("error",t)})}async function E(e){return j("playwright")?await $("playwright",["install","ffmpeg"],{cwd:e})===0:await $("npx",["--yes","playwright","install","ffmpeg"],{cwd:e})===0}function N({headed:e=!1,viewport:r=null}){const o=process.execPath,n=w(o),t=B(),s=Number(r?.width)||1280,u=Number(r?.height)||720,f=`${s}x${u}`,i=[];i.push(`--viewport-size=${f}`,"--output-dir=test-results"),e||i.push("--headless");let g;t?g={command:o,args:[t,...i],env:{PATH:`${n}:/usr/bin:/bin:/usr/sbin:/sbin`}}:g={command:"npx",args:["-y","@playwright/mcp",...i],env:{PATH:`${n}:/usr/bin:/bin:/usr/sbin:/sbin`}},g.description="Zibby MCP Browser - Playwright MCP with Zibby defaults";const d=l(x(),".cursor"),h=l(d,"mcp.json");p(d)||S(d,{recursive:!0});let a={mcpServers:{}};if(p(h))try{a=JSON.parse(P(h,"utf-8")),(!a.mcpServers||typeof a.mcpServers!="object")&&(a.mcpServers={})}catch{a={mcpServers:{}}}a.mcpServers["playwright-official"]=g,C(h,`${JSON.stringify(a,null,2)}
2
+ `,"utf-8")}async function O(){const e=A();if(!e)return;const r=l(e,"scripts","patch-cursor-mcp.js");if(p(r))try{await new Promise((o,n)=>{const t=m(process.execPath,[r],{cwd:process.cwd(),stdio:"ignore"});t.on("close",s=>o(s??0)),t.on("error",n)})}catch{}}function R(e){return b(e).replace(/^\/+/,"").replace(/\//g,"-")}function z(e){const r=String(e||""),o=r.split(`
3
+ `).find(s=>/playwright-official/i.test(s)&&/APPROVAL|🔑/i.test(s));if(o){const s=o.match(/=>\s*(\S+)/);if(s)return s[1]}const n=r.match(/playwright-official[^=]*=>\s*(\S+)/);if(n)return n[1];const t=r.match(/playwright-official-[a-f0-9]+/i);return t?t[0]:null}function J(e,r){const o=R(e),n=l(x(),".cursor","projects",o);S(n,{recursive:!0});const t=l(n,"mcp-approvals.json");return C(t,`${JSON.stringify([r],null,2)}
4
+ `,"utf-8"),t}function k(e){try{return y("cursor-agent mcp list",{cwd:e,encoding:"utf-8",stdio:["ignore","pipe","pipe"],env:process.env,maxBuffer:2*1024*1024})}catch(r){const o=r.stdout!=null?String(r.stdout):"",n=r.stderr!=null?String(r.stderr):"";return o+n}}function T(e){try{return y("cursor-agent mcp enable playwright-official",{cwd:e,encoding:"utf-8",stdio:["ignore","pipe","pipe"],env:process.env,maxBuffer:1024*1024}),!0}catch{return!1}}async function D(e={}){const r=!!e.headed,o=e.viewport||{width:1280,height:720};if(!process.execPath)throw new Error("Node.js runtime not found");const n=await E(process.cwd());console.log(n?c.gray(" \u2713 ffmpeg installed for Playwright video"):c.yellow(" \u26A0 Could not install ffmpeg (video may be unavailable)")),N({headed:r,viewport:o}),await O();const t=b(process.cwd());if(j("cursor-agent")){k(t);const s=T(t);s&&console.log(c.gray(" \u2713 cursor-agent: mcp enable playwright-official"));const u=k(t),f=z(u);if(f)try{const i=J(t,f);console.log(c.gray(` \u2713 MCP auto-approval key saved (${i})`))}catch(i){console.log(c.yellow(` \u26A0 Could not write mcp-approvals.json: ${i.message}`))}else s||(console.log(c.yellow(" \u26A0 Could not auto-approve MCP (mcp enable failed and no approval key in cursor-agent output)")),console.log(c.gray(" Try: cursor-agent mcp enable playwright-official")))}else console.log(c.yellow(" \u26A0 cursor-agent not found; install it to use Cursor MCP tools"))}async function K(e){try{console.log(c.cyan(`
5
5
  \u{1F680} Running CI setup...
6
- `)),await F({headed:!1,cloudSync:!0,video:"on",viewport:{width:1280,height:720}}),console.log(c.green(`
6
+ `)),await D({headed:!1,cloudSync:!0,viewport:{width:1280,height:720}}),console.log(c.green(`
7
7
  \u2705 CI/CD setup complete!
8
8
  `))}catch(r){console.log(c.red(`
9
9
  \u274C Error: ${r.message}
10
- `)),process.exit(1)}}async function Z(e,r){try{const o=["playwright","test",e||"tests/","--project=chromium"];r.headed&&o.push("--headed"),console.log(c.cyan(`
10
+ `)),process.exit(1)}}async function L(e,r){try{const o=["playwright","test",e||"tests/","--project=chromium"];r.headed&&o.push("--headed"),console.log(c.cyan(`
11
11
  \u{1F3A5} Running tests with video recording...
12
- `)),console.log(c.gray("\u2501".repeat(50)));const n=y("npx",o,{cwd:process.cwd(),stdio:"inherit"});n.on("close",t=>{t===0?console.log(c.green(`
12
+ `)),console.log(c.gray("\u2501".repeat(50)));const n=m("npx",o,{cwd:process.cwd(),stdio:"inherit"});n.on("close",t=>{t===0?console.log(c.green(`
13
13
  \u2705 Tests complete!
14
14
  `)):(console.log(c.red(`
15
15
  \u274C Tests failed with code ${t}
@@ -17,4 +17,4 @@ import{spawn as y,execSync as w}from"child_process";import{join as l,dirname as
17
17
  \u274C Error: ${t.message}
18
18
  `)),process.exit(1)})}catch(o){console.log(c.red(`
19
19
  \u274C Error: ${o.message}
20
- `)),process.exit(1)}}export{L as setupCiCommand,F as setupPlaywrightMcpCommand,Z as testWithVideoCommand};
20
+ `)),process.exit(1)}}export{K as setupCiCommand,D as setupPlaywrightMcpCommand,L as testWithVideoCommand};
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import It from"express";import{createServer as Mt}from"http";import{WebSocketServer as Kt}from"ws";import{spawn as st,execSync as Y}from"child_process";import{readFileSync as _,existsSync as u,readdirSync as z,statSync as L,createReadStream as Yt,createWriteStream as Ht,mkdirSync as bt,openSync as Zt,readSync as Vt,closeSync as qt,writeFileSync as H,unlinkSync as Qt}from"fs";import{join as a,resolve as O,dirname as jt,basename as Xt}from"path";import{homedir as te}from"os";import{inspect as ee}from"util";import{fileURLToPath as se,pathToFileURL as ne}from"url";import oe from"dotenv";import{promptAndInstallStudio as re,isStudioInstalled as Pt}from"../utils/studio-installer.js";import{launchStudio as ie}from"../utils/studio-launcher.js";import{mergeSessionRunState as D,listRunningSessionStatesFromSessionsRoot as ce}from"@zibby/core/utils/run-state-session.js";import{liveRunsFromSessionStateRows as ae}from"@zibby/core/utils/session-state-live-runs.js";import{findLatestLiveFrameFileSync as vt,readLatestLiveFramePayloadSync as wt}from"@zibby/core/utils/live-frame-discovery.js";import{getApiUrl as Tt}from"../config/environments.js";const le=se(import.meta.url),ue=jt(le);function Nt(v){const m=a(v,"video.webm");if(u(m))return m;const y=a(v,"execute_live");if(!u(y))return null;try{const k=z(y).filter(I=>I.endsWith(".webm"));return k.length===0?null:k.map(I=>{const w=a(y,I);try{return{p:w,mtime:L(w).mtimeMs}}catch{return{p:w,mtime:0}}}).sort((I,w)=>w.mtime-I.mtime)[0].p}catch{return null}}function At(v){if(process.platform==="win32")try{const m=Y("netstat -ano",{encoding:"utf8"}),y=new Set;for(const k of m.split(`
3
- `)){if(!k.includes("LISTENING"))continue;const E=k.trim().split(/\s+/);if(!(E[1]||"").endsWith(`:${v}`))continue;const w=E[E.length-1];/^\d+$/.test(w)&&y.add(parseInt(w,10))}return[...y].filter(k=>k!==process.pid)}catch{return[]}try{const m=Y(`lsof -tiTCP:${v} -sTCP:LISTEN`,{encoding:"utf8"}).trim();return[...new Set(m.split(`
4
- `).filter(Boolean))].map(y=>parseInt(y,10)).filter(y=>!Number.isNaN(y)&&y!==process.pid)}catch{return[]}}async function _t(v){let m=At(v);if(m.length!==0){console.log(`[Studio] Port ${v} in use \u2014 stopping previous Studio listener(s): ${m.join(", ")}`);for(const y of m)try{process.kill(y,"SIGTERM")}catch{}await new Promise(y=>setTimeout(y,450)),m=At(v);for(const y of m)try{process.kill(y,"SIGKILL")}catch{}await new Promise(y=>setTimeout(y,200))}}async function ke(v={}){const m=It(),y=Mt(m),k=new Kt({server:y}),E=v.port||3847,I=process.cwd(),w=te();process.env.DOTENV_CONFIG_QUIET="true";const kt=process.env.NODE_ENV||"development";[O(I,".env.local"),O(I,`.env.${kt}`),O(I,".env")].forEach(s=>{u(s)&&oe.config({path:s,override:!1})});const R=new Map,G=new Map;let Et=null;const U=96e3,nt="studio-cli.log",W="studio-run.json",Ot=".zibby-studio-stop",Z=512*1024;m.use(It.json()),m.get("/api/session-run-states",(s,t)=>{try{let e=x();try{const r=new URL(s.url,"http://zibby.studio").searchParams.get("sessionsRoot");if(r&&String(r).trim()){const c=O(decodeURIComponent(String(r).trim()));u(c)&&L(c).isDirectory()&&(e=c)}}catch{}const o=ce(e),{liveIdList:n,progressByKey:i}=ae(o);t.json({rows:o,liveIdList:n,progressByKey:i,sessionsRoot:e,unavailable:!1})}catch(e){t.status(500).json({error:e.message,rows:[],liveIdList:[],progressByKey:{},unavailable:!0})}}),m.use((s,t,e)=>(t.header("Access-Control-Allow-Origin","*"),t.header("Access-Control-Allow-Methods","GET, POST, PUT, DELETE"),t.header("Access-Control-Allow-Headers","Content-Type"),e()));const V=a(ue,"../../../../studio"),ot=u(a(V,"package.json"));function xt(){const s=a(V,"node_modules",".bin",process.platform==="win32"?"electron.cmd":"electron"),t=u(s),n=st(t?s:"npx",t?["."]:["electron","."],{cwd:V,detached:!0,stdio:"ignore",shell:!1,env:{...process.env,ZIBBY_STUDIO_PROJECT_ROOT:I,ZIBBY_STUDIO_API_BASE:`http://localhost:${E}/api`}});n.unref(),Et=n}async function Rt(){if(ot){xt();return}if(v.update||!Pt()){if(v.update&&Pt()){console.log(`
2
+ import bt from"express";import{createServer as Kt}from"http";import{WebSocketServer as Yt}from"ws";import{spawn as st,execSync as Y}from"child_process";import{readFileSync as _,existsSync as u,readdirSync as z,statSync as L,createReadStream as Ht,createWriteStream as Zt,mkdirSync as jt,openSync as qt,readSync as Vt,closeSync as Qt,writeFileSync as H,unlinkSync as Xt}from"fs";import{join as a,resolve as E,dirname as ot,basename as te}from"path";import{homedir as ee}from"os";import{inspect as se}from"util";import{fileURLToPath as oe,pathToFileURL as Pt}from"url";import ne from"dotenv";import{promptAndInstallStudio as re,isStudioInstalled as vt}from"../utils/studio-installer.js";import{launchStudio as ie}from"../utils/studio-launcher.js";import{mergeSessionRunState as G,listRunningSessionStatesFromSessionsRoot as ce}from"@zibby/core/utils/run-state-session.js";import{liveRunsFromSessionStateRows as ae}from"@zibby/core/utils/session-state-live-runs.js";import{findLatestLiveFrameFileSync as wt,readLatestLiveFramePayloadSync as Tt}from"@zibby/core/utils/live-frame-discovery.js";import{getApiUrl as Nt}from"../config/environments.js";const le=oe(import.meta.url),ue=ot(le);function At(v){const m=a(v,"video.webm");if(u(m))return m;const y=a(v,"execute_live");if(!u(y))return null;try{const k=z(y).filter(S=>S.endsWith(".webm"));return k.length===0?null:k.map(S=>{const w=a(y,S);try{return{p:w,mtime:L(w).mtimeMs}}catch{return{p:w,mtime:0}}}).sort((S,w)=>w.mtime-S.mtime)[0].p}catch{return null}}function _t(v){if(process.platform==="win32")try{const m=Y("netstat -ano",{encoding:"utf8"}),y=new Set;for(const k of m.split(`
3
+ `)){if(!k.includes("LISTENING"))continue;const O=k.trim().split(/\s+/);if(!(O[1]||"").endsWith(`:${v}`))continue;const w=O[O.length-1];/^\d+$/.test(w)&&y.add(parseInt(w,10))}return[...y].filter(k=>k!==process.pid)}catch{return[]}try{const m=Y(`lsof -tiTCP:${v} -sTCP:LISTEN`,{encoding:"utf8"}).trim();return[...new Set(m.split(`
4
+ `).filter(Boolean))].map(y=>parseInt(y,10)).filter(y=>!Number.isNaN(y)&&y!==process.pid)}catch{return[]}}async function kt(v){let m=_t(v);if(m.length!==0){console.log(`[Studio] Port ${v} in use \u2014 stopping previous Studio listener(s): ${m.join(", ")}`);for(const y of m)try{process.kill(y,"SIGTERM")}catch{}await new Promise(y=>setTimeout(y,450)),m=_t(v);for(const y of m)try{process.kill(y,"SIGKILL")}catch{}await new Promise(y=>setTimeout(y,200))}}async function ke(v={}){const m=bt(),y=Kt(m),k=new Yt({server:y}),O=v.port||3847,S=process.cwd(),w=ee();process.env.DOTENV_CONFIG_QUIET="true";const Ot=process.env.NODE_ENV||"development";[E(S,".env.local"),E(S,`.env.${Ot}`),E(S,".env")].forEach(s=>{u(s)&&ne.config({path:s,override:!1})});const x=new Map,D=new Map;let Et=null;const U=96e3,nt="studio-cli.log",J="studio-run.json",Rt=".zibby-studio-stop",Z=512*1024;m.use(bt.json()),m.get("/api/session-run-states",(s,t)=>{try{let e=R();try{const r=new URL(s.url,"http://zibby.studio").searchParams.get("sessionsRoot");if(r&&String(r).trim()){const c=E(decodeURIComponent(String(r).trim()));u(c)&&L(c).isDirectory()&&(e=c)}}catch{}const n=ce(e),{liveIdList:o,progressByKey:i}=ae(n);t.json({rows:n,liveIdList:o,progressByKey:i,sessionsRoot:e,unavailable:!1})}catch(e){t.status(500).json({error:e.message,rows:[],liveIdList:[],progressByKey:{},unavailable:!0})}}),m.get("/api/workflow/graph",async(s,t)=>{try{let e=null;const n=a(S,".zibby","graph.mjs");if(u(n))try{const o=await import(Pt(n).href),i=o.BrowserTestAutomationAgent||o.default;if(i&&typeof i=="function"){const r=new i().buildGraph();r&&typeof r.serialize=="function"&&(e=r.serialize())}}catch{}if(!e)try{const{createRequire:o}=await import("module"),i=o(import.meta.url),l=a(ot(i.resolve("@zibby/core/package.json")),"templates","browser-test-automation","run_test.json");u(l)&&(e=JSON.parse(_(l,"utf-8")))}catch{}t.json({graph:e})}catch{t.json({graph:null})}}),m.use((s,t,e)=>(t.header("Access-Control-Allow-Origin","*"),t.header("Access-Control-Allow-Methods","GET, POST, PUT, DELETE"),t.header("Access-Control-Allow-Headers","Content-Type"),e()));const q=a(ue,"../../../../studio"),rt=u(a(q,"package.json"));function xt(){const s=a(q,"node_modules",".bin",process.platform==="win32"?"electron.cmd":"electron"),t=u(s),o=st(t?s:"npx",t?["."]:["electron","."],{cwd:q,detached:!0,stdio:"ignore",shell:!1,env:{...process.env,ZIBBY_STUDIO_PROJECT_ROOT:S,ZIBBY_STUDIO_API_BASE:`http://localhost:${O}/api`}});o.unref(),Et=o}async function Ct(){if(rt){xt();return}if(v.update||!vt()){if(v.update&&vt()){console.log(`
5
5
  Updating Zibby Studio...
6
6
  `);const{installStudio:s}=await import("../utils/studio-installer.js");await s()}else if(!await re()){y.close(),process.exit(0);return}}console.log(`
7
7
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
8
8
  \u2551 Zibby Studio \u2551
9
9
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
10
- `),await ie({projectRoot:I}),setTimeout(()=>{y.close(),process.exit(0)},1e3)}function rt(){const s=a(I,".zibby.config.mjs");return u(s)?s:a(w,".zibby.config.mjs")}function x(){rt();const s=a(I,".zibby","output","sessions");return u(s)?s:a(w,".zibby","output","sessions")}function q(s,t){let e=null,o=null,n=null;const i=a(s,"codegen");if(u(i)){const r=a(i,"test.spec.ts"),c=a(i,"generated-test.spec.js");e=u(r)?r:u(c)?c:null;const f=a(i,"test.selenium.py"),h=a(i,"generated-test-selenium.js");o=u(f)?f:u(h)?h:null;const d=a(i,"trace.zip");n=u(d)?d:null}const l=a(s,"generate_script","result.json");if(!e&&u(l))try{const c=JSON.parse(_(l,"utf8"))?.scriptPath;if(typeof c=="string"&&c.trim()){const f=c.trim(),h=f.startsWith("/")||process.platform==="win32"&&/^[A-Za-z]:[\\/]/.test(f)?f:a(t,f);u(h)&&(e=h)}}catch(r){console.warn("[Studio API] generate_script result.json:",r.message)}return!e&&!o&&!n?null:{playwrightFile:e,seleniumFile:o,tracePath:n}}function Ct(s,t,e){try{H(a(s,W),JSON.stringify({sessionId:t,pid:e??null,startedAt:Date.now()},null,2),"utf8")}catch(o){console.error("[Studio API] writeStudioRunMeta:",o.message)}}function B(s){try{const t=a(s,W);u(t)&&Qt(t)}catch{}}function Ft(s){const t=Number(s);if(!Number.isFinite(t)||t<=0)return[];try{const e=Y(`pgrep -P ${t}`,{encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:524288}).trim();return e?e.split(/\n/).map(o=>parseInt(o.trim(),10)).filter(o=>Number.isFinite(o)&&o>0):[]}catch{return[]}}function J(s,t){const e=Number(s);if(!Number.isFinite(e)||e<=0)return;const o=new Set;function n(i){if(!o.has(i)){o.add(i);for(const l of Ft(i))n(l);try{process.kill(i,t)}catch{}}}n(e)}function it(s){const t=Number(s);if(!(!Number.isFinite(t)||t<=0))try{Y(`taskkill /PID ${t} /T /F`,{stdio:"ignore",windowsHide:!0})}catch{}}function ct(s){if(!Number.isFinite(s)||s<=0)return;if(process.platform==="win32"){it(s);return}J(s,"SIGTERM");const t=setTimeout(()=>{J(s,"SIGKILL")},800);typeof t.unref=="function"&&t.unref()}function Lt(s,t){const o=C(s)||a(x(),s);try{bt(o,{recursive:!0}),H(a(o,Ot),JSON.stringify({requestedAt:Date.now()}),"utf8")}catch(r){console.error("[Studio API] write studio stop request:",r.message)}const n=R.get(s);if(n){const r=n.pid;if(process.platform==="win32")it(r);else{J(r,"SIGTERM");const c=setTimeout(()=>{J(r,"SIGKILL")},800);typeof c.unref=="function"&&c.unref()}return R.delete(s),B(o),!0}const i=a(o,W);if(u(i)){let r=null;try{const c=JSON.parse(_(i,"utf8"));r=Number(c.pid)}catch(c){console.error("[Studio API] studio-run.json read:",c.message)}if(B(o),Number.isFinite(r)&&r>0)return ct(r),!0}const l=Number(t);return Number.isFinite(l)&&l>0?(ct(l),B(o),!0):!1}function M(s){try{const t=s?.query?.sessionsRoot;if(typeof t!="string")return"";const e=t.trim();if(!e)return"";const o=decodeURIComponent(e),n=O(o);return u(n)?n:""}catch{return""}}function C(s,t=""){const e=typeof s=="string"?decodeURIComponent(s).trim():String(s||"").trim();if(!e)return null;const o=[e],n=e.split("_")[0]?.trim()||"";n&&n!==e&&o.push(n);const i=new Set,l=[],r=new Set,c=[],f=g=>{if(!g)return;const p=O(g);r.has(p)||(r.add(p),c.push(p))},h=g=>{if(g){f(g);for(const p of o){const S=a(g,p);i.has(S)||(i.add(S),l.push(S))}}};h(t),h(x()),h(a(w,".zibby","output","sessions"));let d;try{d=z(I)}catch{d=[]}for(const g of d){if(g.startsWith("."))continue;const p=a(I,g);try{if(!L(p).isDirectory())continue}catch{continue}h(a(p,".zibby","output","sessions"));let S;try{S=z(p,{withFileTypes:!0})}catch{S=[]}for(const N of S)N?.isDirectory?.()&&String(N.name||"").startsWith(".zibby")&&h(a(p,N.name,"output","sessions"))}const T=l.filter(g=>u(g));if(T.length===0)for(const g of c){if(!u(g))continue;let p;try{p=z(g,{withFileTypes:!0})}catch{p=[]}for(const S of p){if(!S?.isDirectory?.())continue;const N=String(S.name||"");if(N===e||N===n||N.startsWith(`${e}_`)||n&&N.startsWith(`${n}_`)){const F=a(g,N);if(i.has(F))continue;i.add(F),T.push(F)}}}if(T.length===0)return null;if(T.length===1)return T[0];const $=g=>{let p=0;u(a(g,"execute_live"))&&(p+=20),vt(g)&&(p+=15),u(a(g,"execute_live","events.json"))&&(p+=8),u(a(g,"execute_live","result.json"))&&(p+=6),u(a(g,W))&&(p+=4),u(a(g,".session-info.json"))&&(p+=2);try{const S=a(g,"execute_live");u(S)&&(p+=Math.min(L(S).mtimeMs/1e12,3))}catch{}return p};return T.sort((g,p)=>$(p)-$(g)),T[0]}function $t(s,t){try{const e=typeof s=="string"?decodeURIComponent(s).trim():String(s||"").trim();if(!e||!t)return null;const o=(e.split("_")[0]||"").trim(),n=jt(t);if(!o||!n||!u(n))return null;let i=[];try{i=z(n,{withFileTypes:!0})}catch{i=[]}let l=null;for(const r of i){if(!r?.isDirectory?.())continue;const c=String(r.name||"");if(!(c===e||c===o||c.startsWith(`${e}_`)||c.startsWith(`${o}_`)))continue;const f=a(n,c),h=wt(f);h&&(!l||Number(h.mtime||0)>Number(l.mtime||0))&&(l=h)}return l}catch{return null}}m.get("/api/config/check",(s,t)=>{const e=rt();t.json({exists:u(e),path:e,isProjectLevel:e.startsWith(I)})}),m.get("/api/projects",(s,t)=>{try{const e=a(w,".zibby","config.json");if(!u(e))return t.json({projects:[]});const o=JSON.parse(_(e,"utf8")),n=typeof o.sessionToken=="string"&&o.sessionToken.trim()!==""?o.sessionToken.trim():"",i=Array.isArray(o.projects)?o.projects:[];if(!n)return t.json({projects:i});const l=String(Tt()).replace(/\/$/,"");fetch(`${l}/projects`,{headers:{Authorization:`Bearer ${n}`}}).then(r=>r.json().catch(()=>({})).then(c=>({ok:r.ok,status:r.status,body:c}))).then(({ok:r,body:c})=>{if(!r)return t.json({projects:i});const h=(Array.isArray(c?.projects)?c.projects:[]).map(d=>({name:d?.name,projectId:d?.projectId,apiToken:d?.apiToken||null,createdAt:d?.createdAt||null,updatedAt:d?.updatedAt||null}));o.projects=h;try{H(e,`${JSON.stringify(o,null,2)}
11
- `,"utf8")}catch{}return t.json({projects:h})}).catch(()=>t.json({projects:i}))}catch(e){t.status(500).json({error:e.message})}}),m.post("/api/projects/create",async(s,t)=>{try{const e=typeof s.body?.name=="string"?s.body.name.trim():"";if(!e)return t.status(400).json({error:"Project name is required"});const o=a(w,".zibby","config.json");if(!u(o))return t.status(401).json({error:"Not logged in"});const n=JSON.parse(_(o,"utf8")),i=typeof n.sessionToken=="string"&&n.sessionToken.trim()!==""?n.sessionToken.trim():"";if(!i)return t.status(401).json({error:"Not logged in"});const l=String(Tt()).replace(/\/$/,""),r=await fetch(`${l}/projects`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i}`},body:JSON.stringify({name:e})}),c=await r.json().catch(()=>({}));if(!r.ok)return t.status(r.status).json({error:c.error||c.message||`HTTP ${r.status}`});const f=c?.project&&typeof c.project=="object"?c.project:null;if(f?.projectId){const d=(Array.isArray(n.projects)?n.projects:[]).filter(T=>String(T?.projectId||"")!==String(f.projectId));d.push({name:f.name||e,projectId:f.projectId,apiToken:f.apiToken||c?.apiToken||null,createdAt:f.createdAt||null,updatedAt:f.updatedAt||null}),n.projects=d;try{H(o,`${JSON.stringify(n,null,2)}
12
- `,"utf8")}catch{}}return t.json(c)}catch(e){return t.status(500).json({error:e.message||String(e)})}});function zt(s,t){const e=Array.isArray(t)?t.filter(f=>typeof f=="string"):[],o=process.argv[1]?O(process.argv[1]):"",n=o?Xt(o):"";let i;try{i=!!o&&u(o)&&L(o).isFile()&&!/\.(cmd|ps1|bat)$/i.test(o)&&(/\.(m|c)?js$/i.test(o)||n==="zibby")}catch{i=!1}if(i){const f=[o,"test",s,...e],h=[process.execPath,...f].map(d=>/\s/.test(String(d))?`"${String(d).replace(/"/g,'\\"')}"`:d).join(" ");return{useShell:!1,cmd:process.execPath,args:f,display:h}}const l=s.replace(/"/g,'\\"'),r=e.join(" "),c=`zibby test "${l}" ${r}`.trim();return{useShell:!0,cmd:c,args:[],display:c}}m.get("/api/run/result/:sessionId",(s,t)=>{const{sessionId:e}=s.params,o=G.get(e);if(!o)return t.status(404).json({pending:!0,error:"unknown_session"});t.json(o)}),m.post("/api/run",async(s,t)=>{const{task:e,args:o=[],sessionId:n,studioTestCaseId:i}=s.body;if(!e||!n)return t.status(400).json({success:!1,error:"task and sessionId are required"});const l=x(),r=a(l,n),c=O(r),f=i!=null&&String(i).trim()!==""?String(i).trim():String(n);G.set(n,{pending:!0});let h=!1,d=null;const T=()=>{if(d&&!d.destroyed)try{d.end()}catch{}d=null},$=(p,S)=>{G.delete(n),T(),h||t.status(p).json(S)},g=()=>{const p=[];return k.clients.forEach(S=>{S.sessionId===n&&p.push(S)}),p};try{const p=zt(e,o);console.log("[Studio API] Running:",p.display),p.useShell?console.warn('[Studio API] Falling back to shell "zibby" from PATH \u2014 if runs do nothing, run Studio from this repo or ensure `zibby` points to the same install.'):console.log("[Studio API] Using same CLI as this server (argv[1]):",process.argv[1]),bt(r,{recursive:!0}),d=Ht(a(r,nt),{flags:"w"}),D(c,{sessionId:String(n),studioTestCaseId:f,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:c});const S=st(p.cmd,p.args,{cwd:I,shell:p.useShell,env:{...process.env,ZIBBY_SESSION_ID:n,ZIBBY_SESSION_PATH:O(r)}});R.set(n,S),Ct(r,n,S.pid),D(c,{sessionId:String(n),studioTestCaseId:f,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:c,pid:S.pid??null});let N="",F="",dt=!1,ft=0;const pt=setInterval(()=>{try{const b=vt(r);if(!b||b.mtime<=ft)return;ft=b.mtime;const j=_(b.p).toString("base64"),P=b.p.toLowerCase().endsWith(".png")?"image/png":"image/jpeg";g().forEach(K=>{try{K.send(JSON.stringify({type:"video-frame",sessionId:n,frame:j,mime:P}))}catch{}})}catch{}},320),mt=b=>{if(dt)return;dt=!0,T();const j={pending:!1,...b};typeof j.stdout=="string"&&j.stdout.length>U&&(j.stdout=`\u2026(truncated)
10
+ `),await ie({projectRoot:S}),setTimeout(()=>{y.close(),process.exit(0)},1e3)}function it(){const s=a(S,".zibby.config.mjs");return u(s)?s:a(w,".zibby.config.mjs")}function R(){it();const s=a(S,".zibby","output","sessions");return u(s)?s:a(w,".zibby","output","sessions")}function V(s,t){let e=null,n=null,o=null;const i=a(s,"codegen");if(u(i)){const r=a(i,"test.spec.ts"),c=a(i,"generated-test.spec.js");e=u(r)?r:u(c)?c:null;const f=a(i,"test.selenium.py"),h=a(i,"generated-test-selenium.js");n=u(f)?f:u(h)?h:null;const d=a(i,"trace.zip");o=u(d)?d:null}const l=a(s,"generate_script","result.json");if(!e&&u(l))try{const c=JSON.parse(_(l,"utf8"))?.scriptPath;if(typeof c=="string"&&c.trim()){const f=c.trim(),h=f.startsWith("/")||process.platform==="win32"&&/^[A-Za-z]:[\\/]/.test(f)?f:a(t,f);u(h)&&(e=h)}}catch(r){console.warn("[Studio API] generate_script result.json:",r.message)}return!e&&!n&&!o?null:{playwrightFile:e,seleniumFile:n,tracePath:o}}function Ft(s,t,e){try{H(a(s,J),JSON.stringify({sessionId:t,pid:e??null,startedAt:Date.now()},null,2),"utf8")}catch(n){console.error("[Studio API] writeStudioRunMeta:",n.message)}}function B(s){try{const t=a(s,J);u(t)&&Xt(t)}catch{}}function Lt(s){const t=Number(s);if(!Number.isFinite(t)||t<=0)return[];try{const e=Y(`pgrep -P ${t}`,{encoding:"utf8",stdio:["ignore","pipe","ignore"],maxBuffer:524288}).trim();return e?e.split(/\n/).map(n=>parseInt(n.trim(),10)).filter(n=>Number.isFinite(n)&&n>0):[]}catch{return[]}}function W(s,t){const e=Number(s);if(!Number.isFinite(e)||e<=0)return;const n=new Set;function o(i){if(!n.has(i)){n.add(i);for(const l of Lt(i))o(l);try{process.kill(i,t)}catch{}}}o(e)}function ct(s){const t=Number(s);if(!(!Number.isFinite(t)||t<=0))try{Y(`taskkill /PID ${t} /T /F`,{stdio:"ignore",windowsHide:!0})}catch{}}function at(s){if(!Number.isFinite(s)||s<=0)return;if(process.platform==="win32"){ct(s);return}W(s,"SIGTERM");const t=setTimeout(()=>{W(s,"SIGKILL")},800);typeof t.unref=="function"&&t.unref()}function $t(s,t){const n=C(s)||a(R(),s);try{jt(n,{recursive:!0}),H(a(n,Rt),JSON.stringify({requestedAt:Date.now()}),"utf8")}catch(r){console.error("[Studio API] write studio stop request:",r.message)}const o=x.get(s);if(o){const r=o.pid;if(process.platform==="win32")ct(r);else{W(r,"SIGTERM");const c=setTimeout(()=>{W(r,"SIGKILL")},800);typeof c.unref=="function"&&c.unref()}return x.delete(s),B(n),!0}const i=a(n,J);if(u(i)){let r=null;try{const c=JSON.parse(_(i,"utf8"));r=Number(c.pid)}catch(c){console.error("[Studio API] studio-run.json read:",c.message)}if(B(n),Number.isFinite(r)&&r>0)return at(r),!0}const l=Number(t);return Number.isFinite(l)&&l>0?(at(l),B(n),!0):!1}function M(s){try{const t=s?.query?.sessionsRoot;if(typeof t!="string")return"";const e=t.trim();if(!e)return"";const n=decodeURIComponent(e),o=E(n);return u(o)?o:""}catch{return""}}function C(s,t=""){const e=typeof s=="string"?decodeURIComponent(s).trim():String(s||"").trim();if(!e)return null;const n=[e],o=e.split("_")[0]?.trim()||"";o&&o!==e&&n.push(o);const i=new Set,l=[],r=new Set,c=[],f=g=>{if(!g)return;const p=E(g);r.has(p)||(r.add(p),c.push(p))},h=g=>{if(g){f(g);for(const p of n){const I=a(g,p);i.has(I)||(i.add(I),l.push(I))}}};h(t),h(R()),h(a(w,".zibby","output","sessions"));let d;try{d=z(S)}catch{d=[]}for(const g of d){if(g.startsWith("."))continue;const p=a(S,g);try{if(!L(p).isDirectory())continue}catch{continue}h(a(p,".zibby","output","sessions"));let I;try{I=z(p,{withFileTypes:!0})}catch{I=[]}for(const N of I)N?.isDirectory?.()&&String(N.name||"").startsWith(".zibby")&&h(a(p,N.name,"output","sessions"))}const T=l.filter(g=>u(g));if(T.length===0)for(const g of c){if(!u(g))continue;let p;try{p=z(g,{withFileTypes:!0})}catch{p=[]}for(const I of p){if(!I?.isDirectory?.())continue;const N=String(I.name||"");if(N===e||N===o||N.startsWith(`${e}_`)||o&&N.startsWith(`${o}_`)){const F=a(g,N);if(i.has(F))continue;i.add(F),T.push(F)}}}if(T.length===0)return null;if(T.length===1)return T[0];const $=g=>{let p=0;u(a(g,"execute_live"))&&(p+=20),wt(g)&&(p+=15),u(a(g,"execute_live","events.json"))&&(p+=8),u(a(g,"execute_live","result.json"))&&(p+=6),u(a(g,J))&&(p+=4),u(a(g,".session-info.json"))&&(p+=2);try{const I=a(g,"execute_live");u(I)&&(p+=Math.min(L(I).mtimeMs/1e12,3))}catch{}return p};return T.sort((g,p)=>$(p)-$(g)),T[0]}function zt(s,t){try{const e=typeof s=="string"?decodeURIComponent(s).trim():String(s||"").trim();if(!e||!t)return null;const n=(e.split("_")[0]||"").trim(),o=ot(t);if(!n||!o||!u(o))return null;let i=[];try{i=z(o,{withFileTypes:!0})}catch{i=[]}let l=null;for(const r of i){if(!r?.isDirectory?.())continue;const c=String(r.name||"");if(!(c===e||c===n||c.startsWith(`${e}_`)||c.startsWith(`${n}_`)))continue;const f=a(o,c),h=Tt(f);h&&(!l||Number(h.mtime||0)>Number(l.mtime||0))&&(l=h)}return l}catch{return null}}m.get("/api/config/check",(s,t)=>{const e=it();t.json({exists:u(e),path:e,isProjectLevel:e.startsWith(S)})}),m.get("/api/projects",(s,t)=>{try{const e=a(w,".zibby","config.json");if(!u(e))return t.json({projects:[]});const n=JSON.parse(_(e,"utf8")),o=typeof n.sessionToken=="string"&&n.sessionToken.trim()!==""?n.sessionToken.trim():"",i=Array.isArray(n.projects)?n.projects:[];if(!o)return t.json({projects:i});const l=String(Nt()).replace(/\/$/,"");fetch(`${l}/projects`,{headers:{Authorization:`Bearer ${o}`}}).then(r=>r.json().catch(()=>({})).then(c=>({ok:r.ok,status:r.status,body:c}))).then(({ok:r,body:c})=>{if(!r)return t.json({projects:i});const h=(Array.isArray(c?.projects)?c.projects:[]).map(d=>({name:d?.name,projectId:d?.projectId,apiToken:d?.apiToken||null,createdAt:d?.createdAt||null,updatedAt:d?.updatedAt||null}));n.projects=h;try{H(e,`${JSON.stringify(n,null,2)}
11
+ `,"utf8")}catch{}return t.json({projects:h})}).catch(()=>t.json({projects:i}))}catch(e){t.status(500).json({error:e.message})}}),m.post("/api/projects/create",async(s,t)=>{try{const e=typeof s.body?.name=="string"?s.body.name.trim():"";if(!e)return t.status(400).json({error:"Project name is required"});const n=a(w,".zibby","config.json");if(!u(n))return t.status(401).json({error:"Not logged in"});const o=JSON.parse(_(n,"utf8")),i=typeof o.sessionToken=="string"&&o.sessionToken.trim()!==""?o.sessionToken.trim():"";if(!i)return t.status(401).json({error:"Not logged in"});const l=String(Nt()).replace(/\/$/,""),r=await fetch(`${l}/projects`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i}`},body:JSON.stringify({name:e})}),c=await r.json().catch(()=>({}));if(!r.ok)return t.status(r.status).json({error:c.error||c.message||`HTTP ${r.status}`});const f=c?.project&&typeof c.project=="object"?c.project:null;if(f?.projectId){const d=(Array.isArray(o.projects)?o.projects:[]).filter(T=>String(T?.projectId||"")!==String(f.projectId));d.push({name:f.name||e,projectId:f.projectId,apiToken:f.apiToken||c?.apiToken||null,createdAt:f.createdAt||null,updatedAt:f.updatedAt||null}),o.projects=d;try{H(n,`${JSON.stringify(o,null,2)}
12
+ `,"utf8")}catch{}}return t.json(c)}catch(e){return t.status(500).json({error:e.message||String(e)})}});function Bt(s,t){const e=Array.isArray(t)?t.filter(f=>typeof f=="string"):[],n=process.argv[1]?E(process.argv[1]):"",o=n?te(n):"";let i;try{i=!!n&&u(n)&&L(n).isFile()&&!/\.(cmd|ps1|bat)$/i.test(n)&&(/\.(m|c)?js$/i.test(n)||o==="zibby")}catch{i=!1}if(i){const f=[n,"test",s,...e],h=[process.execPath,...f].map(d=>/\s/.test(String(d))?`"${String(d).replace(/"/g,'\\"')}"`:d).join(" ");return{useShell:!1,cmd:process.execPath,args:f,display:h}}const l=s.replace(/"/g,'\\"'),r=e.join(" "),c=`zibby test "${l}" ${r}`.trim();return{useShell:!0,cmd:c,args:[],display:c}}m.get("/api/run/result/:sessionId",(s,t)=>{const{sessionId:e}=s.params,n=D.get(e);if(!n)return t.status(404).json({pending:!0,error:"unknown_session"});t.json(n)}),m.post("/api/run",async(s,t)=>{const{task:e,args:n=[],sessionId:o,studioTestCaseId:i}=s.body;if(!e||!o)return t.status(400).json({success:!1,error:"task and sessionId are required"});const l=R(),r=a(l,o),c=E(r),f=i!=null&&String(i).trim()!==""?String(i).trim():String(o);D.set(o,{pending:!0});let h=!1,d=null;const T=()=>{if(d&&!d.destroyed)try{d.end()}catch{}d=null},$=(p,I)=>{D.delete(o),T(),h||t.status(p).json(I)},g=()=>{const p=[];return k.clients.forEach(I=>{I.sessionId===o&&p.push(I)}),p};try{const p=Bt(e,n);console.log("[Studio API] Running:",p.display),p.useShell?console.warn('[Studio API] Falling back to shell "zibby" from PATH \u2014 if runs do nothing, run Studio from this repo or ensure `zibby` points to the same install.'):console.log("[Studio API] Using same CLI as this server (argv[1]):",process.argv[1]),jt(r,{recursive:!0}),d=Zt(a(r,nt),{flags:"w"}),G(c,{sessionId:String(o),studioTestCaseId:f,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:S,outputBase:".zibby/output",sessionPathAbs:c});const I=st(p.cmd,p.args,{cwd:S,shell:p.useShell,env:{...process.env,ZIBBY_SESSION_ID:o,ZIBBY_SESSION_PATH:E(r)}});x.set(o,I),Ft(r,o,I.pid),G(c,{sessionId:String(o),studioTestCaseId:f,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:S,outputBase:".zibby/output",sessionPathAbs:c,pid:I.pid??null});let N="",F="",ft=!1,pt=0;const mt=setInterval(()=>{try{const b=wt(r);if(!b||b.mtime<=pt)return;pt=b.mtime;const j=_(b.p).toString("base64"),P=b.p.toLowerCase().endsWith(".png")?"image/png":"image/jpeg";g().forEach(K=>{try{K.send(JSON.stringify({type:"video-frame",sessionId:o,frame:j,mime:P}))}catch{}})}catch{}},320),gt=b=>{if(ft)return;ft=!0,T();const j={pending:!1,...b};typeof j.stdout=="string"&&j.stdout.length>U&&(j.stdout=`\u2026(truncated)
13
13
  ${j.stdout.slice(-U)}`),typeof j.stderr=="string"&&j.stderr.length>U&&(j.stderr=`\u2026(truncated)
14
- ${j.stderr.slice(-U)}`),G.set(n,j),g().forEach(P=>{try{P.send(JSON.stringify({type:"run-complete",sessionId:n,result:b}))}catch{}})};S.stdout.on("data",b=>{const j=b.toString();if(N+=j,d&&!d.destroyed)try{d.write(j)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}g().forEach(P=>{try{P.send(JSON.stringify({type:"stdout",data:j}))}catch{}})}),S.stderr.on("data",b=>{const j=b.toString();if(F+=j,d&&!d.destroyed)try{d.write(`[stderr] ${j}`)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}g().forEach(P=>{try{P.send(JSON.stringify({type:"stderr",data:j}))}catch{}})}),S.on("error",b=>{clearInterval(pt),R.delete(n),D(c,{sessionId:String(n),studioTestCaseId:f,status:"failed",runSource:"studio",activeNode:null,activeStageIndex:null,errorMessage:b.message,cwd:I,outputBase:".zibby/output",sessionPathAbs:c}),console.error("[Studio API] Spawn error:",b.message),mt({heldUntilExit:!0,success:!1,exitCode:null,error:b.message,stderr:F,stdout:N,runName:null,videoPath:null,eventsPath:null,codegenFiles:null,metadata:{sessionId:n,task:e}})}),S.on("close",b=>{clearInterval(pt),R.delete(n),D(c,{sessionId:String(n),studioTestCaseId:f,status:b===0?"completed":b===130||b===143?"interrupted":"failed",runSource:"studio",activeNode:null,activeStageIndex:null,exitCode:b,cwd:I,outputBase:".zibby/output",sessionPathAbs:c}),B(r),g().forEach(A=>{try{A.send(JSON.stringify({type:"exit",code:b}))}catch{}});const j=b===0;let P=null;const K=a(r,".session-info.json"),gt=a(r,"result.json");if(u(K))try{const A=JSON.parse(_(K,"utf8"));P=A.name||A.task}catch(A){console.error("[Studio API] Failed to read session info:",A.message)}else if(u(gt))try{const A=JSON.parse(_(gt,"utf8"));P=A.name||A.task}catch(A){console.error("[Studio API] Failed to read result:",A.message)}const Wt=[/\[SessionRecorder\] Run name set: (.+)/,/Run name: (.+)/];if(!P)for(const A of Wt){const St=N.match(A);if(St){P=St[1].trim();break}}const Jt=q(r,I),tt=Nt(r),ht=a(r,"events.json"),yt=a(r,"execute_live","events.json"),et=u(ht)?ht:u(yt)?yt:null;mt({heldUntilExit:!0,success:j,exitCode:b,stdout:N,stderr:F,runName:P,videoPath:tt&&u(tt)?tt:null,eventsPath:et&&u(et)?et:null,codegenFiles:Jt,metadata:{sessionId:n,task:e}})}),h=!0,t.status(202).json({accepted:!0,sessionId:n,pid:S.pid!=null&&Number.isFinite(Number(S.pid))?Number(S.pid):null,pollPath:`/api/run/result/${n}`,message:"Run started. Poll GET /api/run/result/:sessionId until pending is false."})}catch(p){$(500,{success:!1,error:p.message})}}),m.post("/api/stop/:sessionId",(s,t)=>{const{sessionId:e}=s.params,o=s.body&&s.body.pid!=null?s.body.pid:null;if(Lt(e,o))return t.json({success:!0});t.status(404).json({success:!1,error:"Session not found"})}),m.get("/api/recordings",(s,t)=>{try{const e=x();if(!u(e))return t.json({recordings:[],path:e});const n=z(e).map(i=>{const l=a(e,i),r=a(l,".session-info.json"),c=a(l,"result.json");let f={};return u(r)?f=JSON.parse(_(r,"utf8")):u(c)&&(f=JSON.parse(_(c,"utf8"))),{sessionId:i,...f,path:l}});t.json({path:e,recordings:n})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/cli-log",(s,t)=>{try{const{sessionId:e}=s.params;let o=parseInt(String(s.query.offset||"0"),10);(Number.isNaN(o)||o<0)&&(o=0);const n=M(s),i=C(e,n),l=a(i||a(x(),e),nt);if(!u(l))return t.json({exists:!1,content:"",size:0,fromByte:0,nextOffset:0});const r=L(l).size;o>r&&(o=r);let c=o,f=r-c;f>Z&&(c=r-Z,f=Z);const h=Buffer.alloc(f),d=Zt(l,"r");try{Vt(d,h,0,f,c)}finally{qt(d)}t.json({exists:!0,content:h.toString("utf8"),size:r,fromByte:c,nextOffset:r})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/events",(s,t)=>{try{const{sessionId:e}=s.params,o=M(s),n=C(e,o);if(!n)return t.json({events:[]});let i=a(n,"events.json");if(!u(i)){const r=a(n,"execute_live","events.json");if(u(r))i=r;else return t.json({events:[]})}const l=JSON.parse(_(i,"utf8"));t.json({events:l})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/live-preview",(s,t)=>{try{const{sessionId:e}=s.params,o=M(s),n=C(e,o);if(!n)return t.json({ok:!1,error:"session_not_found"});const l=wt(n)||$t(e,n);if(!l)return t.json({ok:!1,error:"no_frame",sessionId:e,sessionPath:n});t.json({ok:!0,frame:l.base64,mime:l.mime,mtime:l.mtime,path:l.path})}catch(e){t.status(500).json({ok:!1,error:e.message})}}),m.get("/api/sessions/:sessionId/video",(s,t)=>{try{const{sessionId:e}=s.params,o=M(s),n=C(e,o);if(!n)return t.status(404).json({error:"Session not found"});const i=Nt(n);if(!i||!u(i))return t.status(404).json({error:"Video not found"});const l=L(i);t.writeHead(200,{"Content-Type":"video/webm","Content-Length":l.size}),Yt(i).pipe(t)}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/codegen/playwright",(s,t)=>{try{const{sessionId:e}=s.params,o=C(e)||a(x(),e);if(!u(o))return t.status(404).type("text/plain").send("Session not found");const n=q(o,I);if(!n?.playwrightFile||!u(n.playwrightFile))return t.status(404).type("text/plain").send("No Playwright artifact");t.type("text/plain; charset=utf-8").send(_(n.playwrightFile,"utf8"))}catch(e){t.status(500).type("text/plain").send(e.message)}}),m.get("/api/sessions/:sessionId/codegen/selenium",(s,t)=>{try{const{sessionId:e}=s.params,o=C(e)||a(x(),e);if(!u(o))return t.status(404).type("text/plain").send("Session not found");const n=q(o,I);if(!n?.seleniumFile||!u(n.seleniumFile))return t.status(404).type("text/plain").send("No Selenium artifact");t.type("text/plain; charset=utf-8").send(_(n.seleniumFile,"utf8"))}catch(e){t.status(500).type("text/plain").send(e.message)}}),m.post("/api/init",async(s,t)=>{const{agentType:e,apiKey:o}=s.body;try{const n=`zibby init --agent ${e} --headed`,i=st(n,[],{cwd:I,env:{...process.env,CURSOR_API_KEY:e==="cursor"?o:process.env.CURSOR_API_KEY,ANTHROPIC_API_KEY:e==="claude"?o:process.env.ANTHROPIC_API_KEY,OPENAI_API_KEY:e==="codex"?o:process.env.OPENAI_API_KEY,GEMINI_API_KEY:e==="gemini"?o:process.env.GEMINI_API_KEY,GOOGLE_API_KEY:e==="gemini"?o:process.env.GOOGLE_API_KEY,CI:"true",ZIBBY_CI:"true"},shell:!0});let l="",r="";i.stdout.on("data",d=>{l+=d.toString()}),i.stderr.on("data",d=>{r+=d.toString()});let c=!1;const f=(d,T)=>{if(!c)if(c=!0,d===0)t.json({success:!0,stdout:l});else{const $=T||r||"zibby init failed";t.status(500).json({success:!1,error:$,stdout:l})}},h=setTimeout(()=>{try{i.kill()}catch{}f(1,`zibby init timed out after 3 minutes. stdout so far:
14
+ ${j.stderr.slice(-U)}`),D.set(o,j),g().forEach(P=>{try{P.send(JSON.stringify({type:"run-complete",sessionId:o,result:b}))}catch{}})};I.stdout.on("data",b=>{const j=b.toString();if(N+=j,d&&!d.destroyed)try{d.write(j)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}g().forEach(P=>{try{P.send(JSON.stringify({type:"stdout",data:j}))}catch{}})}),I.stderr.on("data",b=>{const j=b.toString();if(F+=j,d&&!d.destroyed)try{d.write(`[stderr] ${j}`)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}g().forEach(P=>{try{P.send(JSON.stringify({type:"stderr",data:j}))}catch{}})}),I.on("error",b=>{clearInterval(mt),x.delete(o),G(c,{sessionId:String(o),studioTestCaseId:f,status:"failed",runSource:"studio",activeNode:null,activeStageIndex:null,errorMessage:b.message,cwd:S,outputBase:".zibby/output",sessionPathAbs:c}),console.error("[Studio API] Spawn error:",b.message),gt({heldUntilExit:!0,success:!1,exitCode:null,error:b.message,stderr:F,stdout:N,runName:null,videoPath:null,eventsPath:null,codegenFiles:null,metadata:{sessionId:o,task:e}})}),I.on("close",b=>{clearInterval(mt),x.delete(o),G(c,{sessionId:String(o),studioTestCaseId:f,status:b===0?"completed":b===130||b===143?"interrupted":"failed",runSource:"studio",activeNode:null,activeStageIndex:null,exitCode:b,cwd:S,outputBase:".zibby/output",sessionPathAbs:c}),B(r),g().forEach(A=>{try{A.send(JSON.stringify({type:"exit",code:b}))}catch{}});const j=b===0;let P=null;const K=a(r,".session-info.json"),ht=a(r,"result.json");if(u(K))try{const A=JSON.parse(_(K,"utf8"));P=A.name||A.task}catch(A){console.error("[Studio API] Failed to read session info:",A.message)}else if(u(ht))try{const A=JSON.parse(_(ht,"utf8"));P=A.name||A.task}catch(A){console.error("[Studio API] Failed to read result:",A.message)}const Wt=[/\[SessionRecorder\] Run name set: (.+)/,/Run name: (.+)/];if(!P)for(const A of Wt){const It=N.match(A);if(It){P=It[1].trim();break}}const Mt=V(r,S),tt=At(r),yt=a(r,"events.json"),St=a(r,"execute_live","events.json"),et=u(yt)?yt:u(St)?St:null;gt({heldUntilExit:!0,success:j,exitCode:b,stdout:N,stderr:F,runName:P,videoPath:tt&&u(tt)?tt:null,eventsPath:et&&u(et)?et:null,codegenFiles:Mt,metadata:{sessionId:o,task:e}})}),h=!0,t.status(202).json({accepted:!0,sessionId:o,pid:I.pid!=null&&Number.isFinite(Number(I.pid))?Number(I.pid):null,pollPath:`/api/run/result/${o}`,message:"Run started. Poll GET /api/run/result/:sessionId until pending is false."})}catch(p){$(500,{success:!1,error:p.message})}}),m.post("/api/stop/:sessionId",(s,t)=>{const{sessionId:e}=s.params,n=s.body&&s.body.pid!=null?s.body.pid:null;if($t(e,n))return t.json({success:!0});t.status(404).json({success:!1,error:"Session not found"})}),m.get("/api/recordings",(s,t)=>{try{const e=R();if(!u(e))return t.json({recordings:[],path:e});const o=z(e).map(i=>{const l=a(e,i),r=a(l,".session-info.json"),c=a(l,"result.json");let f={};return u(r)?f=JSON.parse(_(r,"utf8")):u(c)&&(f=JSON.parse(_(c,"utf8"))),{sessionId:i,...f,path:l}});t.json({path:e,recordings:o})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/cli-log",(s,t)=>{try{const{sessionId:e}=s.params;let n=parseInt(String(s.query.offset||"0"),10);(Number.isNaN(n)||n<0)&&(n=0);const o=M(s),i=C(e,o),l=a(i||a(R(),e),nt);if(!u(l))return t.json({exists:!1,content:"",size:0,fromByte:0,nextOffset:0});const r=L(l).size;n>r&&(n=r);let c=n,f=r-c;f>Z&&(c=r-Z,f=Z);const h=Buffer.alloc(f),d=qt(l,"r");try{Vt(d,h,0,f,c)}finally{Qt(d)}t.json({exists:!0,content:h.toString("utf8"),size:r,fromByte:c,nextOffset:r})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/events",(s,t)=>{try{const{sessionId:e}=s.params,n=M(s),o=C(e,n);if(!o)return t.json({events:[]});let i=a(o,"events.json");if(!u(i)){const r=a(o,"execute_live","events.json");if(u(r))i=r;else return t.json({events:[]})}const l=JSON.parse(_(i,"utf8"));t.json({events:l})}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/live-preview",(s,t)=>{try{const{sessionId:e}=s.params,n=M(s),o=C(e,n);if(!o)return t.json({ok:!1,error:"session_not_found"});const l=Tt(o)||zt(e,o);if(!l)return t.json({ok:!1,error:"no_frame",sessionId:e,sessionPath:o});t.json({ok:!0,frame:l.base64,mime:l.mime,mtime:l.mtime,path:l.path})}catch(e){t.status(500).json({ok:!1,error:e.message})}}),m.get("/api/sessions/:sessionId/video",(s,t)=>{try{const{sessionId:e}=s.params,n=M(s),o=C(e,n);if(!o)return t.status(404).json({error:"Session not found"});const i=At(o);if(!i||!u(i))return t.status(404).json({error:"Video not found"});const l=L(i);t.writeHead(200,{"Content-Type":"video/webm","Content-Length":l.size}),Ht(i).pipe(t)}catch(e){t.status(500).json({error:e.message})}}),m.get("/api/sessions/:sessionId/codegen/playwright",(s,t)=>{try{const{sessionId:e}=s.params,n=C(e)||a(R(),e);if(!u(n))return t.status(404).type("text/plain").send("Session not found");const o=V(n,S);if(!o?.playwrightFile||!u(o.playwrightFile))return t.status(404).type("text/plain").send("No Playwright artifact");t.type("text/plain; charset=utf-8").send(_(o.playwrightFile,"utf8"))}catch(e){t.status(500).type("text/plain").send(e.message)}}),m.get("/api/sessions/:sessionId/codegen/selenium",(s,t)=>{try{const{sessionId:e}=s.params,n=C(e)||a(R(),e);if(!u(n))return t.status(404).type("text/plain").send("Session not found");const o=V(n,S);if(!o?.seleniumFile||!u(o.seleniumFile))return t.status(404).type("text/plain").send("No Selenium artifact");t.type("text/plain; charset=utf-8").send(_(o.seleniumFile,"utf8"))}catch(e){t.status(500).type("text/plain").send(e.message)}}),m.post("/api/init",async(s,t)=>{const{agentType:e,apiKey:n}=s.body;try{const o=`zibby init --agent ${e} --headed`,i=st(o,[],{cwd:S,env:{...process.env,CURSOR_API_KEY:e==="cursor"?n:process.env.CURSOR_API_KEY,ANTHROPIC_API_KEY:e==="claude"?n:process.env.ANTHROPIC_API_KEY,OPENAI_API_KEY:e==="codex"?n:process.env.OPENAI_API_KEY,GEMINI_API_KEY:e==="gemini"?n:process.env.GEMINI_API_KEY,GOOGLE_API_KEY:e==="gemini"?n:process.env.GOOGLE_API_KEY,CI:"true",ZIBBY_CI:"true"},shell:!0});let l="",r="";i.stdout.on("data",d=>{l+=d.toString()}),i.stderr.on("data",d=>{r+=d.toString()});let c=!1;const f=(d,T)=>{if(!c)if(c=!0,d===0)t.json({success:!0,stdout:l});else{const $=T||r||"zibby init failed";t.status(500).json({success:!1,error:$,stdout:l})}},h=setTimeout(()=>{try{i.kill()}catch{}f(1,`zibby init timed out after 3 minutes. stdout so far:
15
15
  ${l}
16
16
  stderr:
17
- ${r}`)},18e4);i.on("close",d=>{clearTimeout(h),f(d)}),i.on("error",d=>{clearTimeout(h),f(1,d.message)})}catch(n){t.status(500).json({success:!1,error:n.message})}});const Q=(s,t)=>{const e={type:s,message:t,time:new Date().toLocaleTimeString()};k.clients.forEach(o=>{if(o.listenType==="console")try{o.send(JSON.stringify(e))}catch{}})},Bt=console.log,Dt=console.error,Gt=console.warn,X=s=>s.map(t=>{if(t==null)return String(t);if(typeof t=="object")try{return ee(t,{depth:4,colors:!1,breakLength:100})}catch{return String(t)}return String(t)}).join(" ");console.log=(...s)=>{Bt(...s),Q("info",X(s))},console.error=(...s)=>{Dt(...s),Q("error",X(s))},console.warn=(...s)=>{Gt(...s),Q("warn",X(s))},k.on("connection",(s,t)=>{const e=t.url;if(e.includes("/console"))s.listenType="console",console.log("[WebSocket] Console listener connected"),s.send(JSON.stringify({type:"success",message:"\u{1F3AD} Connected to Studio server logs",time:new Date().toLocaleTimeString()}));else{let o=e.split("/").pop()||"";try{o=decodeURIComponent(o)}catch{}s.sessionId=o,s.listenType="session",console.log(`[WebSocket] Session listener connected: ${o}`)}s.on("close",()=>{s.listenType==="console"?console.log("[WebSocket] Console listener disconnected"):console.log(`[WebSocket] Session listener disconnected: ${s.sessionId}`)})}),m.get("/api/workflow/graph",async(s,t)=>{try{const e=a(I,".zibby","graph.mjs");if(!u(e))return console.warn("[Workflow Graph API] Graph file not found at",e),t.json({graph:null,error:"No graph.mjs found in .zibby folder"});const o=await import(`${ne(e).href}?t=${Date.now()}`),n=o.default||Object.values(o)[0];if(!n||typeof n!="function")return console.error("[Workflow Graph API] Invalid graph module, got:",typeof n),t.json({graph:null,error:"Invalid graph module"});const l=new n().buildGraph(),r=l.toJSON?l.toJSON():l.serialize();console.log(`[Workflow Graph API] graph OK nodes=${r.nodes?.length??0} edges=${r.edges?.length??0} (${e})`),t.json({graph:r})}catch(e){console.error("[Workflow Graph API] Failed to load workflow graph:",e),t.status(500).json({graph:null,error:e.message})}});const Ut=()=>{ot&&console.log(`
17
+ ${r}`)},18e4);i.on("close",d=>{clearTimeout(h),f(d)}),i.on("error",d=>{clearTimeout(h),f(1,d.message)})}catch(o){t.status(500).json({success:!1,error:o.message})}});const Q=(s,t)=>{const e={type:s,message:t,time:new Date().toLocaleTimeString()};k.clients.forEach(n=>{if(n.listenType==="console")try{n.send(JSON.stringify(e))}catch{}})},Gt=console.log,Dt=console.error,Ut=console.warn,X=s=>s.map(t=>{if(t==null)return String(t);if(typeof t=="object")try{return se(t,{depth:4,colors:!1,breakLength:100})}catch{return String(t)}return String(t)}).join(" ");console.log=(...s)=>{Gt(...s),Q("info",X(s))},console.error=(...s)=>{Dt(...s),Q("error",X(s))},console.warn=(...s)=>{Ut(...s),Q("warn",X(s))},k.on("connection",(s,t)=>{const e=t.url;if(e.includes("/console"))s.listenType="console",console.log("[WebSocket] Console listener connected"),s.send(JSON.stringify({type:"success",message:"\u{1F3AD} Connected to Studio server logs",time:new Date().toLocaleTimeString()}));else{let n=e.split("/").pop()||"";try{n=decodeURIComponent(n)}catch{}s.sessionId=n,s.listenType="session",console.log(`[WebSocket] Session listener connected: ${n}`)}s.on("close",()=>{s.listenType==="console"?console.log("[WebSocket] Console listener disconnected"):console.log(`[WebSocket] Session listener disconnected: ${s.sessionId}`)})}),m.get("/api/workflow/graph",async(s,t)=>{try{const e=a(S,".zibby","graph.mjs");if(!u(e))return console.warn("[Workflow Graph API] Graph file not found at",e),t.json({graph:null,error:"No graph.mjs found in .zibby folder"});const n=await import(`${Pt(e).href}?t=${Date.now()}`),o=n.default||Object.values(n)[0];if(!o||typeof o!="function")return console.error("[Workflow Graph API] Invalid graph module, got:",typeof o),t.json({graph:null,error:"Invalid graph module"});const l=new o().buildGraph(),r=l.toJSON?l.toJSON():l.serialize();console.log(`[Workflow Graph API] graph OK nodes=${r.nodes?.length??0} edges=${r.edges?.length??0} (${e})`),t.json({graph:r})}catch(e){console.error("[Workflow Graph API] Failed to load workflow graph:",e),t.status(500).json({graph:null,error:e.message})}});const Jt=()=>{rt&&console.log(`
18
18
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
19
19
  \u2551 Zibby Studio \u2551
20
20
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
21
21
 
22
- API: http://localhost:${E}/api
23
- Project: ${I}
22
+ API: http://localhost:${O}/api
23
+ Project: ${S}
24
24
 
25
25
  Press Ctrl+C to stop
26
- `),v.open!==!1&&Rt().catch(s=>{console.error(`Failed to launch Zibby Studio: ${s.message}`)})};let at=!1;function lt(){const s=async t=>{if(y.removeListener("error",s),t.code==="EADDRINUSE"&&!at){at=!0,console.log(`[Studio] Port ${E} still busy \u2014 forcing cleanup and retrying...`),await _t(E),lt();return}console.error("[Studio] Server error:",t.message),process.exit(1)};y.once("error",s),y.listen(E,()=>{y.removeListener("error",s),y.on("error",t=>{console.error("[Studio] Runtime error:",t.message)}),Ut()})}await _t(E),lt();const ut=()=>{console.log(`
27
- Stopping Studio...`);const s=x();for(const t of R.keys()){const e=O(a(s,t));try{D(e,{status:"interrupted",runSource:"studio",activeNode:null,activeStageIndex:null,exitReason:"studio-shutdown"})}catch{}try{B(a(s,t))}catch{}}R.forEach(t=>t.kill()),y.close(()=>{console.log("Studio stopped"),process.exit(0)})};process.on("SIGINT",ut),process.on("SIGTERM",ut)}export{ke as studioCommand};
26
+ `),v.open!==!1&&Ct().catch(s=>{console.error(`Failed to launch Zibby Studio: ${s.message}`)})};let lt=!1;function ut(){const s=async t=>{if(y.removeListener("error",s),t.code==="EADDRINUSE"&&!lt){lt=!0,console.log(`[Studio] Port ${O} still busy \u2014 forcing cleanup and retrying...`),await kt(O),ut();return}console.error("[Studio] Server error:",t.message),process.exit(1)};y.once("error",s),y.listen(O,()=>{y.removeListener("error",s),y.on("error",t=>{console.error("[Studio] Runtime error:",t.message)}),Jt()})}await kt(O),ut();const dt=()=>{console.log(`
27
+ Stopping Studio...`);const s=R();for(const t of x.keys()){const e=E(a(s,t));try{G(e,{status:"interrupted",runSource:"studio",activeNode:null,activeStageIndex:null,exitReason:"studio-shutdown"})}catch{}try{B(a(s,t))}catch{}}x.forEach(t=>t.kill()),y.close(()=>{console.log("Studio stopped"),process.exit(0)})};process.on("SIGINT",dt),process.on("SIGTERM",dt)}export{ke as studioCommand};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Zibby CLI - Test automation generator and runner",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,2 @@
1
+ import{existsSync as a,readFileSync as f,writeFileSync as y,mkdirSync as l}from"fs";import{join as i}from"path";import{homedir as s}from"os";const p={cursor:{envVar:"CURSOR_API_KEY",label:"Cursor API Key",url:"https://cursor.com/settings"},claude:{envVar:"ANTHROPIC_API_KEY",label:"Anthropic API Key",url:"https://console.anthropic.com/settings/keys"},codex:{envVar:"OPENAI_API_KEY",label:"OpenAI API Key",url:"https://platform.openai.com/api-keys"},gemini:{envVar:"GEMINI_API_KEY",label:"Gemini API Key",url:"https://aistudio.google.com/app/apikey"}};function u(){return i(s(),".zibby","config.json")}function g(){try{const n=u();return a(n)?JSON.parse(f(n,"utf-8")):{}}catch{return{}}}function d(n,r){const t=i(s(),".zibby");l(t,{recursive:!0});const o=u(),e=g(),c=r?p[r]:null;c&&((!e.agentKeys||typeof e.agentKeys!="object")&&(e.agentKeys={}),e.agentKeys[c.envVar]=String(n).trim()),delete e.agentApiKey,y(o,`${JSON.stringify(e,null,2)}
2
+ `,"utf-8")}function A(n){const r=[i(n,".zibby.config.mjs"),i(s(),".zibby.config.mjs")];for(const t of r)if(a(t))try{const o=f(t,"utf-8");for(const e of["cursor","claude","codex","gemini"])if(new RegExp(`agent\\s*:\\s*\\{[^}]*${e}\\s*:`,"s").test(o))return e}catch{}return null}function v(n){const r=A(n||process.cwd());if(!r)return;const t=p[r];if(!t||process.env[t.envVar])return;const e=g().agentKeys?.[t.envVar];e&&(process.env[t.envVar]=e)}export{p as AGENT_KEY_MAP,v as bootstrapAgentEnv,A as detectAgentType,g as readGlobalConfig,d as saveAgentApiKey};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Zibby CLI - Test automation generator and runner",
5
5
  "type": "module",
6
6
  "bin": {