@zibby/cli 0.1.39 → 0.1.40

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,2 +1,2 @@
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 s}from"commander";import{initCommand as c}from"../commands/init.js";import{runCommand as p}from"../commands/run.js";import{videoCommand as d}from"../commands/video.js";import{uploadCommand as m}from"../commands/upload.js";import{ciSetupCommand as l}from"../commands/ci-setup.js";import{setupPlaywrightMcpCommand as u,setupCiCommand as f,testWithVideoCommand as y}from"../commands/setup-scripts.js";import{readFileSync as w}from"fs";import{fileURLToPath as g}from"url";import{dirname as h,join as b}from"path";const k=g(import.meta.url),C=h(k),v=JSON.parse(w(b(C,"../package.json"),"utf-8")),i=process.argv.slice(2),I=i.includes("-h")||i.includes("--help"),P=i.includes("-V")||i.includes("--version"),S=i[0]==="chat",_=["--verbose","-v","--agent","--stream","-s"],j=i.length>0&&i.every(o=>_.includes(o)||i[i.indexOf(o)-1]==="--agent"),D=i.length===0||S||j;if(D&&!I&&!P){const o={},t=i.indexOf("--agent");t!==-1&&i[t+1]&&(o.agent=i[t+1]),(i.includes("--verbose")||i.includes("-v"))&&(o.verbose=!0),(i.includes("--stream")||i.includes("-s"))&&(o.stream=!0);const{chatCommand:r}=await import("../commands/chat.js");await r(o),process.exit(0)}const e=new s;e.name("zibby").description("Zibby Test Automation - AI-powered test generation").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(c),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"),p(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(d),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(m),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(l),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 u({...o,viewport:t})}),e.command("setup-ci-full").description("Complete CI/CD setup from scratch").action(f),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(y),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").action(async o=>{const{studioCommand:t}=await import("../commands/studio.js");return t(o)});const n=e.command("memory").description("Test memory database \u2014 version-controlled knowledge from runs");n.command("stats").description("Show memory database statistics").action(async()=>{const{memoryStatsCommand:o}=await import("../commands/memory.js");return o()}),n.command("init").description("Initialize the memory database (Dolt)").action(async()=>{const{memoryInitCommand:o}=await import("../commands/memory.js");return o()}),n.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)}),n.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 a=e.command("workflow").description("Manage workflow graphs (download, upload, list)");a.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)}),a.command("upload").description("Upload a local workflow graph to Zibby Cloud (validates before upload)").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("--file <path>","Path to workflow JSON (default: .zibby/workflow-<type>.json)").action(async o=>{const{workflowUploadCommand:t}=await import("../commands/workflow.js");return t(o)}),a.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 x=e.command("run-queue").description("Parallel capacity \u2014 on-disk wait queue (see parallel.waitWhenAtCapacity in .zibby.config.mjs)");x.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.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 s}from"commander";import{initCommand as c}from"../commands/init.js";import{runCommand as p}from"../commands/run.js";import{videoCommand as d}from"../commands/video.js";import{uploadCommand as m}from"../commands/upload.js";import{ciSetupCommand as l}from"../commands/ci-setup.js";import{setupPlaywrightMcpCommand as u,setupCiCommand as f,testWithVideoCommand as y}from"../commands/setup-scripts.js";import{readFileSync as w}from"fs";import{fileURLToPath as g}from"url";import{dirname as h,join as b}from"path";const k=g(import.meta.url),C=h(k),v=JSON.parse(w(b(C,"../package.json"),"utf-8")),i=process.argv.slice(2),I=i.includes("-h")||i.includes("--help"),P=i.includes("-V")||i.includes("--version"),S=i[0]==="chat",_=["--verbose","-v","--agent","--stream","-s"],j=i.length>0&&i.every(o=>_.includes(o)||i[i.indexOf(o)-1]==="--agent"),D=i.length===0||S||j;if(D&&!I&&!P){const o={},t=i.indexOf("--agent");t!==-1&&i[t+1]&&(o.agent=i[t+1]),(i.includes("--verbose")||i.includes("-v"))&&(o.verbose=!0),(i.includes("--stream")||i.includes("-s"))&&(o.stream=!0);const{chatCommand:r}=await import("../commands/chat.js");await r(o),process.exit(0)}const e=new s;e.name("zibby").description("Zibby Test Automation - AI-powered test generation").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(c),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"),p(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(d),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(m),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(l),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 u({...o,viewport:t})}),e.command("setup-ci-full").description("Complete CI/CD setup from scratch").action(f),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(y),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 n=e.command("memory").description("Test memory database \u2014 version-controlled knowledge from runs");n.command("stats").description("Show memory database statistics").action(async()=>{const{memoryStatsCommand:o}=await import("../commands/memory.js");return o()}),n.command("init").description("Initialize the memory database (Dolt)").action(async()=>{const{memoryInitCommand:o}=await import("../commands/memory.js");return o()}),n.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)}),n.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 a=e.command("workflow").description("Manage workflow graphs (download, upload, list)");a.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)}),a.command("upload").description("Upload a local workflow graph to Zibby Cloud (validates before upload)").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("--file <path>","Path to workflow JSON (default: .zibby/workflow-<type>.json)").action(async o=>{const{workflowUploadCommand:t}=await import("../commands/workflow.js");return t(o)}),a.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 x=e.command("run-queue").description("Parallel capacity \u2014 on-disk wait queue (see parallel.waitWhenAtCapacity in .zibby.config.mjs)");x.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.parse();
@@ -19,7 +19,7 @@ export PATH="${d}:$PATH"
19
19
  `)),process.exit(0)),o.force&&!f&&console.log(e.cyan(`
20
20
  Reinitializing Zibby configuration...
21
21
  `));let n;if(o.agent&&(o.headed||o.headless))console.log(e.cyan(`Setting up with provided options...
22
- `)),n={agent:o.agent,browserMode:o.headless?"headless":"headed",apiKey:o.apiKey||null,cloudSync:!!(o.cloudSync||o.apiKey)};else{const b=[];o.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"}),!o.headed&&!o.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"}),o.apiKey||b.push({type:"input",name:"apiKey",message:"Enable cloud sync? Enter project ZIBBY_API_KEY (or press Enter to skip):"}),n=b.length>0?await O.prompt(b):{},n.agent=o.agent||n.agent,n.browserMode=o.headless?"headless":o.headed?"headed":n.browserMode,n.apiKey=o.apiKey||n.apiKey,n.cloudSync=!!(o.cloudSync||o.apiKey||n.apiKey&&n.apiKey.trim())}n.mcp="playwright",n.setupMcp=n.agent==="cursor";const p=P("Setting up Zibby...").start();try{if(f&&await C(a,{recursive:!0}),await C(s(a,"test-specs/examples"),{recursive:!0}),await C(s(a,"tests"),{recursive:!0}),await C(s(a,".zibby/output"),{recursive:!0}),await C(s(a,".zibby/commands"),{recursive:!0}),g&&d==="dolt")try{const{initMemory:t,DoltDB:r}=await import("@zibby/memory");if(r.isAvailable()){const{created:i}=t(a);i&&(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"),v=o.template||"browser-test-automation";try{const{graphPath:t,nodesPath:r,readmePath:i,resultHandlerPath:m,template:c}=b.getTemplateFiles(v),h=s(a,".zibby"),B=await w(t,"utf-8");if(await u(s(h,"graph.mjs"),B),m){const I=await w(m,"utf-8");await u(s(h,"result-handler.mjs"),I)}const z=await w(i,"utf-8");await u(s(h,"README.md"),z),await C(s(h,"nodes"),{recursive:!0});const{readdirSync:k}=await import("fs"),S=k(r);for(const I of S){let R=await w(s(r,I),"utf-8");!g&&I==="execute-live.mjs"&&(R=R.replace("skills: [SKILLS.BROWSER, SKILLS.MEMORY],","skills: [SKILLS.BROWSER],")),await u(s(h,"nodes",I),R)}const T=s(c.path,"chat.mjs");if(y(T)){const I=await w(T,"utf-8");await u(s(h,"chat.mjs"),I)}}catch(t){throw p.fail(`Failed to scaffold template: ${t.message}`),t}p.text="Generating configuration files...";const M=F(n,o,{memoryBackend:d});if(await u(s(a,".zibby.config.mjs"),M),await u(s(a,".env.example"),W(n,d)),n.apiKey&&n.apiKey.trim()){const t=s(a,".env"),r=n.apiKey.trim();if(y(t)){let i=await w(t,"utf8");/^ZIBBY_API_KEY=/m.test(i)?i=i.replace(/^ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):/^#\s*ZIBBY_API_KEY=/m.test(i)?i=i.replace(/^#\s*ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):i=i.trimEnd()+`
22
+ `)),n={agent:o.agent,browserMode:o.headless?"headless":"headed",apiKey:o.apiKey||null,cloudSync:!!(o.cloudSync||o.apiKey)};else{const b=[];o.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"}),!o.headed&&!o.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"}),o.apiKey||b.push({type:"input",name:"apiKey",message:"Enable cloud sync? Enter project ZIBBY_API_KEY (or press Enter to skip):"}),n=b.length>0?await O.prompt(b):{},n.agent=o.agent||n.agent,n.browserMode=o.headless?"headless":o.headed?"headed":n.browserMode,n.apiKey=o.apiKey||n.apiKey,n.cloudSync=!!(o.cloudSync||o.apiKey||n.apiKey&&n.apiKey.trim())}n.mcp="playwright",n.setupMcp=n.agent==="cursor";const p=P("Setting up Zibby...").start();try{if(f&&await C(a,{recursive:!0}),await C(s(a,"test-specs/examples"),{recursive:!0}),await C(s(a,"tests"),{recursive:!0}),await C(s(a,".zibby/output"),{recursive:!0}),await C(s(a,".zibby/commands"),{recursive:!0}),g&&d==="dolt")try{const{initMemory:t,DoltDB:r}=await import("@zibby/memory");if(r.isAvailable()){const{created:i}=t(a);i&&(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"),v=o.template||"browser-test-automation";try{const{graphPath:t,nodesPath:r,readmePath:i,resultHandlerPath:m,template:c}=b.getTemplateFiles(v),h=s(a,".zibby"),B=await w(t,"utf-8");if(await u(s(h,"graph.mjs"),B),m){const I=await w(m,"utf-8");await u(s(h,"result-handler.mjs"),I)}const z=await w(i,"utf-8");await u(s(h,"README.md"),z),await C(s(h,"nodes"),{recursive:!0});const{readdirSync:k}=await import("fs"),S=k(r);for(const I of S){let R=await w(s(r,I),"utf-8");!g&&I==="execute-live.mjs"&&(R=R.replace("skills: [SKILLS.BROWSER, SKILLS.MEMORY],","skills: [SKILLS.BROWSER],")),await u(s(h,"nodes",I),R)}const T=s(c.path,"chat.mjs");if(y(T)){const I=await w(T,"utf-8");await u(s(h,"chat.mjs"),I)}}catch(t){throw p.fail(`Failed to scaffold template: ${t.message}`),t}p.text="Generating configuration files...";const M=F(n,o,{memoryBackend:d});if(await u(s(a,".zibby.config.mjs"),M),await u(s(a,".env.example"),W(n,d)),n.apiKey&&n.apiKey.trim()){const t=s(a,".env"),r=n.apiKey.trim();if(y(t)){let i=await w(t,"utf8");/^ZIBBY_API_KEY=/m.test(i)?i=i.replace(/^ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):/^#\s*ZIBBY_API_KEY=/m.test(i)?i=i.replace(/^#\s*ZIBBY_API_KEY=.*/m,`ZIBBY_API_KEY=${r}`):i=`${i.trimEnd()}
23
23
 
24
24
  # Zibby Cloud Sync
25
25
  ZIBBY_API_KEY=${r}
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import It from"express";import{createServer as Jt}from"http";import{WebSocketServer as Mt}from"ws";import{spawn as st,execSync as Y}from"child_process";import{readFileSync as _,existsSync as u,readdirSync as $,statSync as L,createReadStream as Kt,createWriteStream as Yt,mkdirSync as bt,openSync as Ht,readSync as Zt,closeSync as Vt,writeFileSync as H,unlinkSync as qt}from"fs";import{join as a,resolve as O,dirname as jt,basename as Qt}from"path";import{homedir as Xt}from"os";import{inspect as te}from"util";import{fileURLToPath as ee,pathToFileURL as se}from"url";import ne from"dotenv";import{promptAndInstallStudio as oe,isStudioInstalled as re}from"../utils/studio-installer.js";import{launchStudio as ie}from"../utils/studio-launcher.js";import{mergeSessionRunState as B,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 Pt,readLatestLiveFramePayloadSync as vt}from"@zibby/core/utils/live-frame-discovery.js";import{getApiUrl as wt}from"../config/environments.js";const le=ee(import.meta.url),ue=jt(le);function Tt(A){const g=a(A,"video.webm");if(u(g))return g;const y=a(A,"execute_live");if(!u(y))return null;try{const k=$(y).filter(I=>I.endsWith(".webm"));return k.length===0?null:k.map(I=>{const v=a(y,I);try{return{p:v,mtime:L(v).mtimeMs}}catch{return{p:v,mtime:0}}}).sort((I,v)=>v.mtime-I.mtime)[0].p}catch{return null}}function Nt(A){if(process.platform==="win32")try{const g=Y("netstat -ano",{encoding:"utf8"}),y=new Set;for(const k of g.split(`
3
- `)){if(!k.includes("LISTENING"))continue;const E=k.trim().split(/\s+/);if(!(E[1]||"").endsWith(`:${A}`))continue;const v=E[E.length-1];/^\d+$/.test(v)&&y.add(parseInt(v,10))}return[...y].filter(k=>k!==process.pid)}catch{return[]}try{const g=Y(`lsof -tiTCP:${A} -sTCP:LISTEN`,{encoding:"utf8"}).trim();return[...new Set(g.split(`
4
- `).filter(Boolean))].map(y=>parseInt(y,10)).filter(y=>!Number.isNaN(y)&&y!==process.pid)}catch{return[]}}async function At(A){let g=Nt(A);if(g.length!==0){console.log(`[Studio] Port ${A} in use \u2014 stopping previous Studio listener(s): ${g.join(", ")}`);for(const y of g)try{process.kill(y,"SIGTERM")}catch{}await new Promise(y=>setTimeout(y,450)),g=Nt(A);for(const y of g)try{process.kill(y,"SIGKILL")}catch{}await new Promise(y=>setTimeout(y,200))}}async function ke(A={}){const g=It(),y=Jt(g),k=new Mt({server:y}),E=A.port||3847,I=process.cwd(),v=Xt();process.env.DOTENV_CONFIG_QUIET="true";const _t=process.env.NODE_ENV||"development";[O(I,".env.local"),O(I,`.env.${_t}`),O(I,".env")].forEach(s=>{u(s)&&ne.config({path:s,override:!1})});const R=new Map,D=new Map;let kt=null;const G=96e3,nt="studio-cli.log",U="studio-run.json",Et=".zibby-studio-stop",Z=512*1024;g.use(It.json()),g.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 i=O(decodeURIComponent(String(r).trim()));u(i)&&L(i).isDirectory()&&(e=i)}}catch{}const o=ce(e),{liveIdList:n,progressByKey:c}=ae(o);t.json({rows:o,liveIdList:n,progressByKey:c,sessionsRoot:e,unavailable:!1})}catch(e){t.status(500).json({error:e.message,rows:[],liveIdList:[],progressByKey:{},unavailable:!0})}}),g.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 Ot(){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(),kt=n}async function xt(){if(ot){Ot();return}if(!re()&&!await oe()){y.close(),process.exit(0);return}console.log(`
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 $,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 B,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 g=a(v,"video.webm");if(u(g))return g;const y=a(v,"execute_live");if(!u(y))return null;try{const k=$(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 g=Y("netstat -ano",{encoding:"utf8"}),y=new Set;for(const k of g.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 g=Y(`lsof -tiTCP:${v} -sTCP:LISTEN`,{encoding:"utf8"}).trim();return[...new Set(g.split(`
4
+ `).filter(Boolean))].map(y=>parseInt(y,10)).filter(y=>!Number.isNaN(y)&&y!==process.pid)}catch{return[]}}async function _t(v){let g=At(v);if(g.length!==0){console.log(`[Studio] Port ${v} in use \u2014 stopping previous Studio listener(s): ${g.join(", ")}`);for(const y of g)try{process.kill(y,"SIGTERM")}catch{}await new Promise(y=>setTimeout(y,450)),g=At(v);for(const y of g)try{process.kill(y,"SIGKILL")}catch{}await new Promise(y=>setTimeout(y,200))}}async function ke(v={}){const g=It(),y=Mt(g),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,D=new Map;let Et=null;const G=96e3,nt="studio-cli.log",U="studio-run.json",Ot=".zibby-studio-stop",Z=512*1024;g.use(It.json()),g.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 i=O(decodeURIComponent(String(r).trim()));u(i)&&L(i).isDirectory()&&(e=i)}}catch{}const o=ce(e),{liveIdList:n,progressByKey:c}=ae(o);t.json({rows:o,liveIdList:n,progressByKey:c,sessionsRoot:e,unavailable:!1})}catch(e){t.status(500).json({error:e.message,rows:[],liveIdList:[],progressByKey:{},unavailable:!0})}}),g.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(`
5
+ Updating Zibby Studio...
6
+ `);const{installStudio:s}=await import("../utils/studio-installer.js");await s()}else if(!await re()){y.close(),process.exit(0);return}}console.log(`
5
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
6
8
  \u2551 Zibby Studio \u2551
7
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
8
- `),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(v,".zibby.config.mjs")}function x(){rt();const s=a(I,".zibby","output","sessions");return u(s)?s:a(v,".zibby","output","sessions")}function q(s,t){let e=null,o=null,n=null;const c=a(s,"codegen");if(u(c)){const r=a(c,"test.spec.ts"),i=a(c,"generated-test.spec.js");e=u(r)?r:u(i)?i:null;const d=a(c,"test.selenium.py"),p=a(c,"generated-test-selenium.js");o=u(d)?d:u(p)?p:null;const m=a(c,"trace.zip");n=u(m)?m:null}const l=a(s,"generate_script","result.json");if(!e&&u(l))try{const i=JSON.parse(_(l,"utf8"))?.scriptPath;if(typeof i=="string"&&i.trim()){const d=i.trim(),p=d.startsWith("/")||process.platform==="win32"&&/^[A-Za-z]:[\\/]/.test(d)?d:a(t,d);u(p)&&(e=p)}}catch(r){console.warn("[Studio API] generate_script result.json:",r.message)}return!e&&!o&&!n?null:{playwrightFile:e,seleniumFile:o,tracePath:n}}function Rt(s,t,e){try{H(a(s,U),JSON.stringify({sessionId:t,pid:e??null,startedAt:Date.now()},null,2),"utf8")}catch(o){console.error("[Studio API] writeStudioRunMeta:",o.message)}}function z(s){try{const t=a(s,U);u(t)&&qt(t)}catch{}}function Ct(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 W(s,t){const e=Number(s);if(!Number.isFinite(e)||e<=0)return;const o=new Set;function n(c){if(!o.has(c)){o.add(c);for(const l of Ct(c))n(l);try{process.kill(c,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}W(s,"SIGTERM");const t=setTimeout(()=>{W(s,"SIGKILL")},800);typeof t.unref=="function"&&t.unref()}function Ft(s,t){const o=C(s)||a(x(),s);try{bt(o,{recursive:!0}),H(a(o,Et),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{W(r,"SIGTERM");const i=setTimeout(()=>{W(r,"SIGKILL")},800);typeof i.unref=="function"&&i.unref()}return R.delete(s),z(o),!0}const c=a(o,U);if(u(c)){let r=null;try{const i=JSON.parse(_(c,"utf8"));r=Number(i.pid)}catch(i){console.error("[Studio API] studio-run.json read:",i.message)}if(z(o),Number.isFinite(r)&&r>0)return ct(r),!0}const l=Number(t);return Number.isFinite(l)&&l>0?(ct(l),z(o),!0):!1}function J(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 c=new Set,l=[],r=new Set,i=[],d=h=>{if(!h)return;const f=O(h);r.has(f)||(r.add(f),i.push(f))},p=h=>{if(h){d(h);for(const f of o){const S=a(h,f);c.has(S)||(c.add(S),l.push(S))}}};p(t),p(x()),p(a(v,".zibby","output","sessions"));let m;try{m=$(I)}catch{m=[]}for(const h of m){if(h.startsWith("."))continue;const f=a(I,h);try{if(!L(f).isDirectory())continue}catch{continue}p(a(f,".zibby","output","sessions"));let S;try{S=$(f,{withFileTypes:!0})}catch{S=[]}for(const T of S)T?.isDirectory?.()&&String(T.name||"").startsWith(".zibby")&&p(a(f,T.name,"output","sessions"))}const w=l.filter(h=>u(h));if(w.length===0)for(const h of i){if(!u(h))continue;let f;try{f=$(h,{withFileTypes:!0})}catch{f=[]}for(const S of f){if(!S?.isDirectory?.())continue;const T=String(S.name||"");if(T===e||T===n||T.startsWith(`${e}_`)||n&&T.startsWith(`${n}_`)){const F=a(h,T);if(c.has(F))continue;c.add(F),w.push(F)}}}if(w.length===0)return null;if(w.length===1)return w[0];const M=h=>{let f=0;u(a(h,"execute_live"))&&(f+=20),Pt(h)&&(f+=15),u(a(h,"execute_live","events.json"))&&(f+=8),u(a(h,"execute_live","result.json"))&&(f+=6),u(a(h,U))&&(f+=4),u(a(h,".session-info.json"))&&(f+=2);try{const S=a(h,"execute_live");u(S)&&(f+=Math.min(L(S).mtimeMs/1e12,3))}catch{}return f};return w.sort((h,f)=>M(f)-M(h)),w[0]}function Lt(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 c=[];try{c=$(n,{withFileTypes:!0})}catch{c=[]}let l=null;for(const r of c){if(!r?.isDirectory?.())continue;const i=String(r.name||"");if(!(i===e||i===o||i.startsWith(`${e}_`)||i.startsWith(`${o}_`)))continue;const d=a(n,i),p=vt(d);p&&(!l||Number(p.mtime||0)>Number(l.mtime||0))&&(l=p)}return l}catch{return null}}g.get("/api/config/check",(s,t)=>{const e=rt();t.json({exists:u(e),path:e,isProjectLevel:e.startsWith(I)})}),g.get("/api/projects",(s,t)=>{try{const e=a(v,".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():"",c=Array.isArray(o.projects)?o.projects:[];if(!n)return t.json({projects:c});const l=String(wt()).replace(/\/$/,"");fetch(`${l}/projects`,{headers:{Authorization:`Bearer ${n}`}}).then(r=>r.json().catch(()=>({})).then(i=>({ok:r.ok,status:r.status,body:i}))).then(({ok:r,body:i})=>{if(!r)return t.json({projects:c});const p=(Array.isArray(i?.projects)?i.projects:[]).map(m=>({name:m?.name,projectId:m?.projectId,apiToken:m?.apiToken||null,createdAt:m?.createdAt||null,updatedAt:m?.updatedAt||null}));o.projects=p;try{H(e,`${JSON.stringify(o,null,2)}
9
- `,"utf8")}catch{}return t.json({projects:p})}).catch(()=>t.json({projects:c}))}catch(e){t.status(500).json({error:e.message})}}),g.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(v,".zibby","config.json");if(!u(o))return t.status(401).json({error:"Not logged in"});const n=JSON.parse(_(o,"utf8")),c=typeof n.sessionToken=="string"&&n.sessionToken.trim()!==""?n.sessionToken.trim():"";if(!c)return t.status(401).json({error:"Not logged in"});const l=String(wt()).replace(/\/$/,""),r=await fetch(`${l}/projects`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${c}`},body:JSON.stringify({name:e})}),i=await r.json().catch(()=>({}));if(!r.ok)return t.status(r.status).json({error:i.error||i.message||`HTTP ${r.status}`});const d=i?.project&&typeof i.project=="object"?i.project:null;if(d?.projectId){const m=(Array.isArray(n.projects)?n.projects:[]).filter(w=>String(w?.projectId||"")!==String(d.projectId));m.push({name:d.name||e,projectId:d.projectId,apiToken:d.apiToken||i?.apiToken||null,createdAt:d.createdAt||null,updatedAt:d.updatedAt||null}),n.projects=m;try{H(o,`${JSON.stringify(n,null,2)}
10
- `,"utf8")}catch{}}return t.json(i)}catch(e){return t.status(500).json({error:e.message||String(e)})}});function $t(s,t){const e=Array.isArray(t)?t.filter(d=>typeof d=="string"):[],o=process.argv[1]?O(process.argv[1]):"",n=o?Qt(o):"";let c;try{c=!!o&&u(o)&&L(o).isFile()&&!/\.(cmd|ps1|bat)$/i.test(o)&&(/\.(m|c)?js$/i.test(o)||n==="zibby")}catch{c=!1}if(c){const d=[o,"test",s,...e],p=[process.execPath,...d].map(m=>/\s/.test(String(m))?`"${String(m).replace(/"/g,'\\"')}"`:m).join(" ");return{useShell:!1,cmd:process.execPath,args:d,display:p}}const l=s.replace(/"/g,'\\"'),r=e.join(" "),i=`zibby test "${l}" ${r}`.trim();return{useShell:!0,cmd:i,args:[],display:i}}g.get("/api/run/result/:sessionId",(s,t)=>{const{sessionId:e}=s.params,o=D.get(e);if(!o)return t.status(404).json({pending:!0,error:"unknown_session"});t.json(o)}),g.post("/api/run",async(s,t)=>{const{task:e,args:o=[],sessionId:n,studioTestCaseId:c}=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),i=O(r),d=c!=null&&String(c).trim()!==""?String(c).trim():String(n);D.set(n,{pending:!0});let p=!1,m=null;const w=()=>{if(m&&!m.destroyed)try{m.end()}catch{}m=null},M=(f,S)=>{D.delete(n),w(),p||t.status(f).json(S)},h=()=>{const f=[];return k.clients.forEach(S=>{S.sessionId===n&&f.push(S)}),f};try{const f=$t(e,o);console.log("[Studio API] Running:",f.display),f.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}),m=Yt(a(r,nt),{flags:"w"}),B(i,{sessionId:String(n),studioTestCaseId:d,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:i});const S=st(f.cmd,f.args,{cwd:I,shell:f.useShell,env:{...process.env,ZIBBY_SESSION_ID:n,ZIBBY_SESSION_PATH:O(r)}});R.set(n,S),Rt(r,n,S.pid),B(i,{sessionId:String(n),studioTestCaseId:d,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:i,pid:S.pid??null});let T="",F="",dt=!1,ft=0;const pt=setInterval(()=>{try{const b=Pt(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";h().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,w();const j={pending:!1,...b};typeof j.stdout=="string"&&j.stdout.length>G&&(j.stdout=`\u2026(truncated)
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 c=a(s,"codegen");if(u(c)){const r=a(c,"test.spec.ts"),i=a(c,"generated-test.spec.js");e=u(r)?r:u(i)?i:null;const d=a(c,"test.selenium.py"),p=a(c,"generated-test-selenium.js");o=u(d)?d:u(p)?p:null;const m=a(c,"trace.zip");n=u(m)?m:null}const l=a(s,"generate_script","result.json");if(!e&&u(l))try{const i=JSON.parse(_(l,"utf8"))?.scriptPath;if(typeof i=="string"&&i.trim()){const d=i.trim(),p=d.startsWith("/")||process.platform==="win32"&&/^[A-Za-z]:[\\/]/.test(d)?d:a(t,d);u(p)&&(e=p)}}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,U),JSON.stringify({sessionId:t,pid:e??null,startedAt:Date.now()},null,2),"utf8")}catch(o){console.error("[Studio API] writeStudioRunMeta:",o.message)}}function z(s){try{const t=a(s,U);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 W(s,t){const e=Number(s);if(!Number.isFinite(e)||e<=0)return;const o=new Set;function n(c){if(!o.has(c)){o.add(c);for(const l of Ft(c))n(l);try{process.kill(c,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}W(s,"SIGTERM");const t=setTimeout(()=>{W(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{W(r,"SIGTERM");const i=setTimeout(()=>{W(r,"SIGKILL")},800);typeof i.unref=="function"&&i.unref()}return R.delete(s),z(o),!0}const c=a(o,U);if(u(c)){let r=null;try{const i=JSON.parse(_(c,"utf8"));r=Number(i.pid)}catch(i){console.error("[Studio API] studio-run.json read:",i.message)}if(z(o),Number.isFinite(r)&&r>0)return ct(r),!0}const l=Number(t);return Number.isFinite(l)&&l>0?(ct(l),z(o),!0):!1}function J(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 c=new Set,l=[],r=new Set,i=[],d=h=>{if(!h)return;const f=O(h);r.has(f)||(r.add(f),i.push(f))},p=h=>{if(h){d(h);for(const f of o){const S=a(h,f);c.has(S)||(c.add(S),l.push(S))}}};p(t),p(x()),p(a(w,".zibby","output","sessions"));let m;try{m=$(I)}catch{m=[]}for(const h of m){if(h.startsWith("."))continue;const f=a(I,h);try{if(!L(f).isDirectory())continue}catch{continue}p(a(f,".zibby","output","sessions"));let S;try{S=$(f,{withFileTypes:!0})}catch{S=[]}for(const N of S)N?.isDirectory?.()&&String(N.name||"").startsWith(".zibby")&&p(a(f,N.name,"output","sessions"))}const T=l.filter(h=>u(h));if(T.length===0)for(const h of i){if(!u(h))continue;let f;try{f=$(h,{withFileTypes:!0})}catch{f=[]}for(const S of f){if(!S?.isDirectory?.())continue;const N=String(S.name||"");if(N===e||N===n||N.startsWith(`${e}_`)||n&&N.startsWith(`${n}_`)){const F=a(h,N);if(c.has(F))continue;c.add(F),T.push(F)}}}if(T.length===0)return null;if(T.length===1)return T[0];const M=h=>{let f=0;u(a(h,"execute_live"))&&(f+=20),vt(h)&&(f+=15),u(a(h,"execute_live","events.json"))&&(f+=8),u(a(h,"execute_live","result.json"))&&(f+=6),u(a(h,U))&&(f+=4),u(a(h,".session-info.json"))&&(f+=2);try{const S=a(h,"execute_live");u(S)&&(f+=Math.min(L(S).mtimeMs/1e12,3))}catch{}return f};return T.sort((h,f)=>M(f)-M(h)),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 c=[];try{c=$(n,{withFileTypes:!0})}catch{c=[]}let l=null;for(const r of c){if(!r?.isDirectory?.())continue;const i=String(r.name||"");if(!(i===e||i===o||i.startsWith(`${e}_`)||i.startsWith(`${o}_`)))continue;const d=a(n,i),p=wt(d);p&&(!l||Number(p.mtime||0)>Number(l.mtime||0))&&(l=p)}return l}catch{return null}}g.get("/api/config/check",(s,t)=>{const e=rt();t.json({exists:u(e),path:e,isProjectLevel:e.startsWith(I)})}),g.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():"",c=Array.isArray(o.projects)?o.projects:[];if(!n)return t.json({projects:c});const l=String(Tt()).replace(/\/$/,"");fetch(`${l}/projects`,{headers:{Authorization:`Bearer ${n}`}}).then(r=>r.json().catch(()=>({})).then(i=>({ok:r.ok,status:r.status,body:i}))).then(({ok:r,body:i})=>{if(!r)return t.json({projects:c});const p=(Array.isArray(i?.projects)?i.projects:[]).map(m=>({name:m?.name,projectId:m?.projectId,apiToken:m?.apiToken||null,createdAt:m?.createdAt||null,updatedAt:m?.updatedAt||null}));o.projects=p;try{H(e,`${JSON.stringify(o,null,2)}
11
+ `,"utf8")}catch{}return t.json({projects:p})}).catch(()=>t.json({projects:c}))}catch(e){t.status(500).json({error:e.message})}}),g.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")),c=typeof n.sessionToken=="string"&&n.sessionToken.trim()!==""?n.sessionToken.trim():"";if(!c)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 ${c}`},body:JSON.stringify({name:e})}),i=await r.json().catch(()=>({}));if(!r.ok)return t.status(r.status).json({error:i.error||i.message||`HTTP ${r.status}`});const d=i?.project&&typeof i.project=="object"?i.project:null;if(d?.projectId){const m=(Array.isArray(n.projects)?n.projects:[]).filter(T=>String(T?.projectId||"")!==String(d.projectId));m.push({name:d.name||e,projectId:d.projectId,apiToken:d.apiToken||i?.apiToken||null,createdAt:d.createdAt||null,updatedAt:d.updatedAt||null}),n.projects=m;try{H(o,`${JSON.stringify(n,null,2)}
12
+ `,"utf8")}catch{}}return t.json(i)}catch(e){return t.status(500).json({error:e.message||String(e)})}});function zt(s,t){const e=Array.isArray(t)?t.filter(d=>typeof d=="string"):[],o=process.argv[1]?O(process.argv[1]):"",n=o?Xt(o):"";let c;try{c=!!o&&u(o)&&L(o).isFile()&&!/\.(cmd|ps1|bat)$/i.test(o)&&(/\.(m|c)?js$/i.test(o)||n==="zibby")}catch{c=!1}if(c){const d=[o,"test",s,...e],p=[process.execPath,...d].map(m=>/\s/.test(String(m))?`"${String(m).replace(/"/g,'\\"')}"`:m).join(" ");return{useShell:!1,cmd:process.execPath,args:d,display:p}}const l=s.replace(/"/g,'\\"'),r=e.join(" "),i=`zibby test "${l}" ${r}`.trim();return{useShell:!0,cmd:i,args:[],display:i}}g.get("/api/run/result/:sessionId",(s,t)=>{const{sessionId:e}=s.params,o=D.get(e);if(!o)return t.status(404).json({pending:!0,error:"unknown_session"});t.json(o)}),g.post("/api/run",async(s,t)=>{const{task:e,args:o=[],sessionId:n,studioTestCaseId:c}=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),i=O(r),d=c!=null&&String(c).trim()!==""?String(c).trim():String(n);D.set(n,{pending:!0});let p=!1,m=null;const T=()=>{if(m&&!m.destroyed)try{m.end()}catch{}m=null},M=(f,S)=>{D.delete(n),T(),p||t.status(f).json(S)},h=()=>{const f=[];return k.clients.forEach(S=>{S.sessionId===n&&f.push(S)}),f};try{const f=zt(e,o);console.log("[Studio API] Running:",f.display),f.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}),m=Ht(a(r,nt),{flags:"w"}),B(i,{sessionId:String(n),studioTestCaseId:d,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:i});const S=st(f.cmd,f.args,{cwd:I,shell:f.useShell,env:{...process.env,ZIBBY_SESSION_ID:n,ZIBBY_SESSION_PATH:O(r)}});R.set(n,S),Ct(r,n,S.pid),B(i,{sessionId:String(n),studioTestCaseId:d,status:"running",runSource:"studio",activeStageIndex:0,activeNode:"preflight",cwd:I,outputBase:".zibby/output",sessionPathAbs:i,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";h().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>G&&(j.stdout=`\u2026(truncated)
11
13
  ${j.stdout.slice(-G)}`),typeof j.stderr=="string"&&j.stderr.length>G&&(j.stderr=`\u2026(truncated)
12
- ${j.stderr.slice(-G)}`),D.set(n,j),h().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(T+=j,m&&!m.destroyed)try{m.write(j)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}h().forEach(P=>{try{P.send(JSON.stringify({type:"stdout",data:j}))}catch{}})}),S.stderr.on("data",b=>{const j=b.toString();if(F+=j,m&&!m.destroyed)try{m.write(`[stderr] ${j}`)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}h().forEach(P=>{try{P.send(JSON.stringify({type:"stderr",data:j}))}catch{}})}),S.on("error",b=>{clearInterval(pt),R.delete(n),B(i,{sessionId:String(n),studioTestCaseId:d,status:"failed",runSource:"studio",activeNode:null,activeStageIndex:null,errorMessage:b.message,cwd:I,outputBase:".zibby/output",sessionPathAbs:i}),console.error("[Studio API] Spawn error:",b.message),mt({heldUntilExit:!0,success:!1,exitCode:null,error:b.message,stderr:F,stdout:T,runName:null,videoPath:null,eventsPath:null,codegenFiles:null,metadata:{sessionId:n,task:e}})}),S.on("close",b=>{clearInterval(pt),R.delete(n),B(i,{sessionId:String(n),studioTestCaseId:d,status:b===0?"completed":b===130||b===143?"interrupted":"failed",runSource:"studio",activeNode:null,activeStageIndex:null,exitCode:b,cwd:I,outputBase:".zibby/output",sessionPathAbs:i}),z(r),h().forEach(N=>{try{N.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 N=JSON.parse(_(K,"utf8"));P=N.name||N.task}catch(N){console.error("[Studio API] Failed to read session info:",N.message)}else if(u(gt))try{const N=JSON.parse(_(gt,"utf8"));P=N.name||N.task}catch(N){console.error("[Studio API] Failed to read result:",N.message)}const Ut=[/\[SessionRecorder\] Run name set: (.+)/,/Run name: (.+)/];if(!P)for(const N of Ut){const St=T.match(N);if(St){P=St[1].trim();break}}const Wt=q(r,I),tt=Tt(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:T,stderr:F,runName:P,videoPath:tt&&u(tt)?tt:null,eventsPath:et&&u(et)?et:null,codegenFiles:Wt,metadata:{sessionId:n,task:e}})}),p=!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(f){M(500,{success:!1,error:f.message})}}),g.post("/api/stop/:sessionId",(s,t)=>{const{sessionId:e}=s.params,o=s.body&&s.body.pid!=null?s.body.pid:null;if(Ft(e,o))return t.json({success:!0});t.status(404).json({success:!1,error:"Session not found"})}),g.get("/api/recordings",(s,t)=>{try{const e=x();if(!u(e))return t.json({recordings:[],path:e});const n=$(e).map(c=>{const l=a(e,c),r=a(l,".session-info.json"),i=a(l,"result.json");let d={};return u(r)?d=JSON.parse(_(r,"utf8")):u(i)&&(d=JSON.parse(_(i,"utf8"))),{sessionId:c,...d,path:l}});t.json({path:e,recordings:n})}catch(e){t.status(500).json({error:e.message})}}),g.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=J(s),c=C(e,n),l=a(c||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 i=o,d=r-i;d>Z&&(i=r-Z,d=Z);const p=Buffer.alloc(d),m=Ht(l,"r");try{Zt(m,p,0,d,i)}finally{Vt(m)}t.json({exists:!0,content:p.toString("utf8"),size:r,fromByte:i,nextOffset:r})}catch(e){t.status(500).json({error:e.message})}}),g.get("/api/sessions/:sessionId/events",(s,t)=>{try{const{sessionId:e}=s.params,o=J(s),n=C(e,o);if(!n)return t.json({events:[]});let c=a(n,"events.json");if(!u(c)){const r=a(n,"execute_live","events.json");if(u(r))c=r;else return t.json({events:[]})}const l=JSON.parse(_(c,"utf8"));t.json({events:l})}catch(e){t.status(500).json({error:e.message})}}),g.get("/api/sessions/:sessionId/live-preview",(s,t)=>{try{const{sessionId:e}=s.params,o=J(s),n=C(e,o);if(!n)return t.json({ok:!1,error:"session_not_found"});const l=vt(n)||Lt(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})}}),g.get("/api/sessions/:sessionId/video",(s,t)=>{try{const{sessionId:e}=s.params,o=J(s),n=C(e,o);if(!n)return t.status(404).json({error:"Session not found"});const c=Tt(n);if(!c||!u(c))return t.status(404).json({error:"Video not found"});const l=L(c);t.writeHead(200,{"Content-Type":"video/webm","Content-Length":l.size}),Kt(c).pipe(t)}catch(e){t.status(500).json({error:e.message})}}),g.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)}}),g.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)}}),g.post("/api/init",async(s,t)=>{const{agentType:e,apiKey:o}=s.body;try{const n=`zibby init --agent ${e} --headed`,c=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="";c.stdout.on("data",p=>{l+=p.toString()}),c.stderr.on("data",p=>{r+=p.toString()});let i=!1;const d=(p,m)=>{if(!i)if(i=!0,p===0)t.json({success:!0,stdout:l});else{const w=m||r||"zibby init failed";t.status(500).json({success:!1,error:w,stdout:l})}};c.on("close",p=>d(p)),c.on("error",p=>d(1,p.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{}})},zt=console.log,Bt=console.error,Dt=console.warn,X=s=>s.map(t=>{if(t==null)return String(t);if(typeof t=="object")try{return te(t,{depth:4,colors:!1,breakLength:100})}catch{return String(t)}return String(t)}).join(" ");console.log=(...s)=>{zt(...s),Q("info",X(s))},console.error=(...s)=>{Bt(...s),Q("error",X(s))},console.warn=(...s)=>{Dt(...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}`)})}),g.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(`${se(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 Gt=()=>{ot&&console.log(`
14
+ ${j.stderr.slice(-G)}`),D.set(n,j),h().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,m&&!m.destroyed)try{m.write(j)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}h().forEach(P=>{try{P.send(JSON.stringify({type:"stdout",data:j}))}catch{}})}),S.stderr.on("data",b=>{const j=b.toString();if(F+=j,m&&!m.destroyed)try{m.write(`[stderr] ${j}`)}catch(P){console.error("[Studio API] studio-cli.log write failed:",P.message)}h().forEach(P=>{try{P.send(JSON.stringify({type:"stderr",data:j}))}catch{}})}),S.on("error",b=>{clearInterval(pt),R.delete(n),B(i,{sessionId:String(n),studioTestCaseId:d,status:"failed",runSource:"studio",activeNode:null,activeStageIndex:null,errorMessage:b.message,cwd:I,outputBase:".zibby/output",sessionPathAbs:i}),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),B(i,{sessionId:String(n),studioTestCaseId:d,status:b===0?"completed":b===130||b===143?"interrupted":"failed",runSource:"studio",activeNode:null,activeStageIndex:null,exitCode:b,cwd:I,outputBase:".zibby/output",sessionPathAbs:i}),z(r),h().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}})}),p=!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(f){M(500,{success:!1,error:f.message})}}),g.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"})}),g.get("/api/recordings",(s,t)=>{try{const e=x();if(!u(e))return t.json({recordings:[],path:e});const n=$(e).map(c=>{const l=a(e,c),r=a(l,".session-info.json"),i=a(l,"result.json");let d={};return u(r)?d=JSON.parse(_(r,"utf8")):u(i)&&(d=JSON.parse(_(i,"utf8"))),{sessionId:c,...d,path:l}});t.json({path:e,recordings:n})}catch(e){t.status(500).json({error:e.message})}}),g.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=J(s),c=C(e,n),l=a(c||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 i=o,d=r-i;d>Z&&(i=r-Z,d=Z);const p=Buffer.alloc(d),m=Zt(l,"r");try{Vt(m,p,0,d,i)}finally{qt(m)}t.json({exists:!0,content:p.toString("utf8"),size:r,fromByte:i,nextOffset:r})}catch(e){t.status(500).json({error:e.message})}}),g.get("/api/sessions/:sessionId/events",(s,t)=>{try{const{sessionId:e}=s.params,o=J(s),n=C(e,o);if(!n)return t.json({events:[]});let c=a(n,"events.json");if(!u(c)){const r=a(n,"execute_live","events.json");if(u(r))c=r;else return t.json({events:[]})}const l=JSON.parse(_(c,"utf8"));t.json({events:l})}catch(e){t.status(500).json({error:e.message})}}),g.get("/api/sessions/:sessionId/live-preview",(s,t)=>{try{const{sessionId:e}=s.params,o=J(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})}}),g.get("/api/sessions/:sessionId/video",(s,t)=>{try{const{sessionId:e}=s.params,o=J(s),n=C(e,o);if(!n)return t.status(404).json({error:"Session not found"});const c=Nt(n);if(!c||!u(c))return t.status(404).json({error:"Video not found"});const l=L(c);t.writeHead(200,{"Content-Type":"video/webm","Content-Length":l.size}),Yt(c).pipe(t)}catch(e){t.status(500).json({error:e.message})}}),g.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)}}),g.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)}}),g.post("/api/init",async(s,t)=>{const{agentType:e,apiKey:o}=s.body;try{const n=`zibby init --agent ${e} --headed`,c=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="";c.stdout.on("data",p=>{l+=p.toString()}),c.stderr.on("data",p=>{r+=p.toString()});let i=!1;const d=(p,m)=>{if(!i)if(i=!0,p===0)t.json({success:!0,stdout:l});else{const T=m||r||"zibby init failed";t.status(500).json({success:!1,error:T,stdout:l})}};c.on("close",p=>d(p)),c.on("error",p=>d(1,p.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}`)})}),g.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(`
13
15
  \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
14
16
  \u2551 Zibby Studio \u2551
15
17
  \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
@@ -18,5 +20,5 @@ ${j.stderr.slice(-G)}`),D.set(n,j),h().forEach(P=>{try{P.send(JSON.stringify({ty
18
20
  Project: ${I}
19
21
 
20
22
  Press Ctrl+C to stop
21
- `),A.open!==!1&&xt().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 At(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)}),Gt()})}await At(E),lt();const ut=()=>{console.log(`
23
+ `),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(`
22
24
  Stopping Studio...`);const s=x();for(const t of R.keys()){const e=O(a(s,t));try{B(e,{status:"interrupted",runSource:"studio",activeNode:null,activeStageIndex:null,exitReason:"studio-shutdown"})}catch{}try{z(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};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Zibby CLI - Test automation generator and runner",
5
5
  "type": "module",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Zibby CLI - Test automation generator and runner",
5
5
  "type": "module",
6
6
  "bin": {