@zibby/cli 0.1.25 → 0.1.27
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/README.md +44 -44
- package/dist/auth/cli-login.js +18 -0
- package/dist/bin/zibby.js +2 -0
- package/dist/commands/agent-reliability.js +8 -0
- package/dist/commands/analyze-graph.js +18 -0
- package/dist/commands/chat-session-store.js +1 -0
- package/dist/commands/chat.js +79 -0
- package/dist/commands/ci-setup.js +18 -0
- package/dist/commands/generate.js +69 -0
- package/dist/commands/implement.js +65 -0
- package/dist/commands/init.js +344 -0
- package/dist/commands/list-projects.js +11 -0
- package/dist/commands/memory.js +39 -0
- package/dist/commands/run-capacity-queue-cli.js +4 -0
- package/dist/commands/run.js +112 -0
- package/dist/commands/setup-scripts.js +15 -0
- package/dist/commands/studio.js +33 -0
- package/dist/commands/upload.js +22 -0
- package/dist/commands/video.js +6 -0
- package/dist/commands/workflow.js +45 -0
- package/dist/config/config.js +1 -0
- package/dist/config/environments.js +1 -0
- package/dist/utils/chat-run-lifecycle.js +3 -0
- package/dist/utils/execution-context.js +1 -0
- package/dist/utils/progress-reporter.js +1 -0
- package/dist/utils/studio-cli-log-mirror.js +1 -0
- package/dist/utils/studio-installer.js +7 -0
- package/dist/utils/studio-launcher.js +1 -0
- package/package.json +19 -16
- package/bin/zibby.js +0 -269
- package/src/auth/cli-login.js +0 -404
- package/src/commands/analyze-graph.js +0 -336
- package/src/commands/ci-setup.js +0 -65
- package/src/commands/implement.js +0 -664
- package/src/commands/init.js +0 -851
- package/src/commands/list-projects.js +0 -77
- package/src/commands/memory.js +0 -171
- package/src/commands/run.js +0 -919
- package/src/commands/setup-scripts.js +0 -114
- package/src/commands/upload.js +0 -162
- package/src/commands/video.js +0 -30
- package/src/commands/workflow.js +0 -368
- package/src/config/config.js +0 -117
- package/src/config/environments.js +0 -145
- package/src/utils/execution-context.js +0 -25
- package/src/utils/progress-reporter.js +0 -155
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import{invokeAgent as v,JIRA_TOOLS as T}from"@zibby/core";import{readFileSync as R,writeFileSync as x,existsSync as w,mkdirSync as A}from"fs";import{resolve as $,join as O}from"path";import{z as s}from"zod";import o from"chalk";const L=s.object({title:s.string(),testObjective:s.string(),url:s.string().optional(),testCredentials:s.array(s.object({field:s.string(),value:s.string()})).optional(),testSteps:s.array(s.string()),expectedResults:s.array(s.string()),priority:s.enum(["Critical","High","Medium","Low"]).optional(),category:s.string().optional()}),N=s.object({testCases:s.array(L),summary:s.object({totalTests:s.number(),coverageNotes:s.string().optional()}).optional()});function j(e,c){const t=[];if(t.push(`# ${e.title}`),c&&t.push(`# Ticket: ${c}`),e.priority&&t.push(`# Priority: ${e.priority}`),e.category&&t.push(`# Category: ${e.category}`),t.push(""),e.testObjective&&(t.push(`OBJECTIVE: ${e.testObjective}`),t.push("")),e.url&&(t.push(`START URL: ${e.url}`),t.push("")),e.testCredentials?.length>0){t.push("CREDENTIALS:");for(const i of e.testCredentials)t.push(` ${i.field}: ${i.value}`);t.push("")}if(t.push("STEPS:"),e.testSteps.forEach((i,a)=>{t.push(`${a+1}. ${i}`)}),t.push(""),e.expectedResults?.length>0){t.push("EXPECTED RESULTS:");for(const i of e.expectedResults)t.push(`- ${i}`)}return t.join(`
|
|
2
|
+
`).trim()}function P(e,c){return`You are an expert test engineer. Your job is to analyze a ticket and the local codebase, then generate detailed, actionable test case specifications.
|
|
3
|
+
|
|
4
|
+
WORKSPACE: ${c}
|
|
5
|
+
|
|
6
|
+
TICKET INFORMATION:
|
|
7
|
+
${e}
|
|
8
|
+
|
|
9
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
10
|
+
STEP 1: EXPLORE THE CODEBASE (MANDATORY)
|
|
11
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
12
|
+
|
|
13
|
+
You MUST explore the codebase before generating tests. Use your tools to:
|
|
14
|
+
|
|
15
|
+
1. Find and read routing files (App.tsx/jsx, routes.ts/js, pages/, app/)
|
|
16
|
+
2. Understand the navigation hierarchy (auth \u2192 home \u2192 list \u2192 detail \u2192 sub-feature)
|
|
17
|
+
3. Find authentication components and login flows
|
|
18
|
+
4. Read page components referenced in routes
|
|
19
|
+
5. Find related test files for patterns and conventions
|
|
20
|
+
6. Check package.json for the framework and dependencies
|
|
21
|
+
7. Look for .env.example or config files for URLs and settings
|
|
22
|
+
|
|
23
|
+
CRITICAL:
|
|
24
|
+
- Routes with :parameters (like /users/:id) require navigating to the parent list first
|
|
25
|
+
- Base ALL test steps on ACTUAL navigation paths from the codebase
|
|
26
|
+
- Use REAL URLs, component names, and navigation elements you find
|
|
27
|
+
|
|
28
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
29
|
+
STEP 2: GENERATE TEST CASES
|
|
30
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
31
|
+
|
|
32
|
+
For each test case, provide:
|
|
33
|
+
- title: Clear test name
|
|
34
|
+
- testObjective: One sentence \u2014 what does this verify?
|
|
35
|
+
- url: Start URL (from codebase, e.g. http://localhost:3000)
|
|
36
|
+
- testCredentials: Login credentials if auth is needed
|
|
37
|
+
- testSteps: Plain English steps \u2014 full navigation from login to action
|
|
38
|
+
- expectedResults: What should happen after the steps
|
|
39
|
+
- priority: Critical / High / Medium / Low
|
|
40
|
+
- category: Smoke Test / Regression / Integration / Edge Case
|
|
41
|
+
|
|
42
|
+
Write 4-10 test cases covering:
|
|
43
|
+
- Happy path (the main flow the ticket describes)
|
|
44
|
+
- Edge cases (empty states, invalid input, boundary values)
|
|
45
|
+
- Error handling (what happens when things go wrong)
|
|
46
|
+
- Related features that might be affected
|
|
47
|
+
|
|
48
|
+
IMPORTANT: Write steps in natural language. Don't use CSS selectors or technical identifiers.
|
|
49
|
+
Example step: "Click the 'Submit' button" NOT "Click button[type=submit]"`}function S(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,60)}async function D(e={}){const c=process.cwd(),t=e.agent||process.env.AGENT_TYPE,i=$(c,e.output||"test-specs");t||(console.error(o.red("\u274C No agent specified. Use --agent <codex|claude|cursor|gemini> or set AGENT_TYPE env")),process.exit(1));let a="",d=e.ticket||"unknown";if(e.input){const r=$(c,e.input);w(r)||(console.error(o.red(`\u274C Input file not found: ${r}`)),process.exit(1)),a=R(r,"utf-8")}else if(e.description)a=e.description;else if(e.ticket)try{const{getSkill:r}=await import("@zibby/core/framework/skill-registry.js"),l=r("jira");if(l?.handleToolCall){console.log(o.cyan(`\u{1F4CB} Fetching ticket ${e.ticket} from Jira...`));const n=JSON.parse(await l.handleToolCall(T.GET_ISSUE,{issueKey:e.ticket}));n.error&&(console.error(o.red(`\u274C Jira error: ${n.error}`)),process.exit(1)),d=n.key||e.ticket,a=`Key: ${n.key}
|
|
50
|
+
Summary: ${n.summary}
|
|
51
|
+
Type: ${n.type}
|
|
52
|
+
Priority: ${n.priority}
|
|
53
|
+
Status: ${n.status}
|
|
54
|
+
`,n.description&&(a+=`
|
|
55
|
+
Description:
|
|
56
|
+
${n.description}
|
|
57
|
+
`),n.acceptanceCriteria&&(a+=`
|
|
58
|
+
Acceptance Criteria:
|
|
59
|
+
${n.acceptanceCriteria}
|
|
60
|
+
`);const u=JSON.parse(await l.handleToolCall(T.GET_COMMENTS,{issueKey:e.ticket}));if(u.comments?.length>0){a+=`
|
|
61
|
+
Comments:
|
|
62
|
+
`;for(const g of u.comments)a+=` [${g.author}]: ${g.body}
|
|
63
|
+
`}console.log(o.green(`\u2705 Fetched: ${n.key} \u2014 ${n.summary}`))}else console.error(o.red("\u274C Jira skill not available. Use --input or --description instead.")),process.exit(1)}catch(r){console.error(o.red(`\u274C Failed to fetch ticket: ${r.message}`)),process.exit(1)}else console.error(o.red('\u274C Provide ticket info: --ticket SCRUM-123, --input file.txt, or --description "..."')),process.exit(1);const h=e.repo?$(c,e.repo):c,C=P(a,h);console.log(o.cyan(`
|
|
64
|
+
\u{1F9EA} Generating test specs with ${t} agent...`)),console.log(o.gray(` Workspace: ${h}`)),console.log(o.gray(` Output: ${i}`));const E={state:{agentType:t,workspace:h,config:{}}},k={workspace:h,schema:N,model:e.model||"gpt-4o-2024-08-06"};let f;try{f=await v(C,E,k)}catch(r){console.error(o.red(`
|
|
65
|
+
\u274C Agent failed: ${r.message}`)),process.exit(1)}const m=f?.structured||f,y=m?.testCases||[];y.length===0&&(console.error(o.yellow("\u26A0\uFE0F Agent returned no test cases")),process.exit(1)),A(i,{recursive:!0});const p=[];for(let r=0;r<y.length;r++){const l=y[r],n=S(l.title),u=`${S(d)}-${r+1}-${n}.txt`,g=O(i,u),b=j(l,d);x(g,b,"utf-8"),p.push({path:g,title:l.title,priority:l.priority}),console.log(o.green(` \u2705 ${u}`))}console.log(o.green.bold(`
|
|
66
|
+
\u2713 Generated ${p.length} test specs in ${i}`)),m?.summary?.coverageNotes&&console.log(o.gray(`
|
|
67
|
+
\u{1F4DD} ${m.summary.coverageNotes}`)),console.log(o.cyan(`
|
|
68
|
+
Next: run them with:`));for(const r of p)console.log(o.gray(` zibby run ${r.path}`));return console.log(o.cyan(`
|
|
69
|
+
Or from chat: the test runner will pick up these spec files automatically.`)),{files:p,ticketKey:d,total:p.length}}export{D as generateCommand};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import{existsSync as f,readFileSync as I}from"fs";import{join as o,dirname as b}from"path";import{fileURLToPath as x}from"url";import{invokeAgent as j}from"@zibby/core";import{fetchExecutionContext as D}from"../utils/execution-context.js";import{reportProgress as S,reportFinalStatus as O}from"../utils/progress-reporter.js";const L=x(import.meta.url),K=b(L);async function B(t){const{EXECUTION_ID:i,TICKET_KEY:l,PROJECT_ID:n,REPOS:e,_PRIMARY_REPO:s,_GITHUB_TOKEN:y,MODEL:k}=process.env;(!i||!l||!n)&&(console.error("\u274C Missing required environment variables:"),console.error(" EXECUTION_ID, TICKET_KEY, PROJECT_ID"),process.exit(1));const a=await D(i,n),g=a.ticketContext,m=e?JSON.parse(e):a.repos,R=m.find(_=>_.isPrimary)||m[0],h=process.cwd(),p={status:"running",steps:[]};try{await w("Start Environment",async()=>{}),await w("Clone Repositories",async()=>{const r=process.env.GITHUB_TOKEN,c=process.env.GITLAB_TOKEN||"",u=process.env.GITLAB_URL||"";for(const d of m){const T=o(h,d.name);let E=d.url;const P=d.provider==="gitlab"||u&&d.url.includes(new URL(u).host);if((d.provider==="github"||d.url.includes("github.com"))&&r)E=d.url.replace("https://github.com",`https://${r}@github.com`);else if(P&&c&&u)try{const v=new URL(u).host;E=d.url.replace(`https://${v}`,`https://oauth2:${c}@${v}`)}catch(v){console.warn(`\u26A0\uFE0F Failed to parse GITLAB_URL: ${v.message}`)}if(C(["git","clone",E,T],h),C(["git","checkout",d.branch],T),d.isPrimary){const v=`feature/${l.toLowerCase()}`;C(["git","checkout","-b",v],T)}}p.steps.push({name:"clone",status:"success",repoCount:m.length})});const _=await w("Load Ticket Context",async()=>(p.steps.push({name:"load_ticket",status:"success"}),g));await w("Install Dependencies",async()=>{for(const r of m){const c=o(h,r.name),u=U(c);try{C(u.installCommand,c)}catch{}}p.steps.push({name:"install_deps",status:"success"})});const A=await w("Detect Dev Command",async()=>{const r=o(h,R.name),c=["docker-compose.yml","docker-compose.yaml","compose.yml","compose.yaml"];for(const P of c)if(f(o(r,P)))return p.steps.push({name:"detect_dev",status:"success",command:"docker-compose up",type:"docker-compose"}),{command:"docker-compose up",type:"docker-compose",configFile:P};const u=o(r,"package.json");if(!f(u))return console.log(" \u26A0\uFE0F No package.json or docker-compose found"),p.steps.push({name:"detect_dev",status:"skipped"}),null;const T=JSON.parse(I(u,"utf-8")).scripts||{};let E=null;return T.dev?E="npm run dev":T.start?E="npm start":T["dev:local"]&&(E="npm run dev:local"),E?(p.steps.push({name:"detect_dev",status:"success",command:E,type:"npm"}),{command:E,type:"npm"}):(p.steps.push({name:"detect_dev",status:"skipped"}),null)});await w("Start Dev Server",async()=>{const r=o(h,R.name),c="docker-compose.test.yml";return f(o(r,c))?(C(["docker","compose","-f",c,"up","-d"],r),await new Promise(u=>setTimeout(u,1e4)),p.steps.push({name:"start_server",status:"success"}),!0):(console.log(` \u26A0\uFE0F No ${c} found, skipping server startup`),p.steps.push({name:"start_server",status:"skipped"}),null)}),await w("Run AI Agent Implementation",async()=>{const r=m.map(d=>{const T=o(h,d.name);return{...d,...U(T)}}),c=F(_,r,A),u=o(h,".cursor-prompt.md");require("fs").writeFileSync(u,c),await j(c,{state:{model:k,workspace:h}},{print:!0}),p.steps.push({name:"ai_agent",status:"success"})});const G=await w("Run E2E Tests",async()=>{const r=o(h,R.name);if(!f(o(r,"playwright.config.js"))&&!f(o(r,"playwright.config.ts")))return p.steps.push({name:"e2e_tests",status:"skipped"}),null;try{return C("npx playwright test --reporter=json",r),p.steps.push({name:"e2e_tests",status:"success"}),{passed:!0}}catch(c){throw C("docker compose -f docker-compose.test.yml down",r,{allowFailure:!0}),new Error(`E2E tests failed: ${c.message}`,{cause:c})}});try{C("docker compose -f docker-compose.test.yml down",o(h,R.name),{allowFailure:!0})}catch{}const $=await w("Create Pull Request",async()=>{const r=o(h,R.name),c=`feature/${l.toLowerCase()}`;return C(["git","add","."],r),C(["git","commit","-m",`feat(${l}): ${_.summary}`],r),C(["git","push","origin",c],r),console.log(" \u26A0\uFE0F PR creation via API removed (using SQS flow)"),p.steps.push({name:"create_pr",status:"skipped"}),null});await w("Report Results",async()=>{const r=o(h,R.name),c=o(r,"test-results"),u=[];f(c),p.status="completed",p.prUrl=$,p.videoUrls=u,await O(N(),{status:"completed",artifacts:{prUrl:$,videoUrls:u}})}),process.exit(0)}catch(_){console.error(""),console.error("\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),console.error("\u2551 \u274C FAILED! \u2551"),console.error("\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"),console.error(""),console.error("Error:",_.message),console.error("Stack:",_.stack);try{await O(N(),{status:"failed",error:_.message})}catch(A){console.error("Failed to report error:",A.message)}process.exit(1)}}function N(){return{EXECUTION_ID:process.env.EXECUTION_ID,PROGRESS_API_URL:process.env.PROGRESS_API_URL,PROGRESS_QUEUE_URL:process.env.PROGRESS_QUEUE_URL,SQS_AUTH_TOKEN:process.env.SQS_AUTH_TOKEN,PROJECT_API_TOKEN:process.env.PROJECT_API_TOKEN}}async function w(t,i){const l=Date.now(),n=[];let e="";const s=console.log;console.log=(...a)=>{const g=a.join(" ");n.push(g),s(...a)};const y=N(),k=setInterval(()=>{const a=n.join(`
|
|
2
|
+
`);a!==e&&a.length>0&&(e=a,S(t,"running",a,y).catch(()=>{}))},2e3);try{await S(t,"running","",y);const a=await i(),g=`${((Date.now()-l)/1e3).toFixed(1)}s`;clearInterval(k),console.log=s;const m=n.join(`
|
|
3
|
+
`);return await S(t,"success",m||`Completed in ${g}`,y),a}catch(a){clearInterval(k),console.log=s;const g=n.join(`
|
|
4
|
+
`);throw await S(t,"failed",`${g}
|
|
5
|
+
|
|
6
|
+
Error: ${a.message}`,y),a}}function C(t,i,l={}){try{const{spawnSync:n}=require("child_process");let e;if(Array.isArray(t)){const[s,...y]=t;e=n(s,y,{cwd:i,encoding:"utf-8",stdio:["pipe","pipe","pipe"]})}else e=n(t,{cwd:i,shell:!0,encoding:"utf-8",stdio:["pipe","pipe","pipe"]});if(e.stdout&&console.log(e.stdout),e.stderr&&console.log(e.stderr),e.status!==0&&!l.allowFailure){const s=Array.isArray(t)?t.join(" "):t;throw new Error(`Command failed with exit code ${e.status}: ${s}`)}return e.stdout||e.stderr}catch(n){if(l.allowFailure)return null;throw n}}function U(t){const i=o(t,".zibby.yml");if(f(i))try{const e=require("js-yaml").load(I(i,"utf-8"));return{name:e.name||"Custom Project",framework:e.framework||"Custom",language:e.language||"Custom",testCommand:e.test||"make test",installCommand:e.install||"make install",custom:!0}}catch{console.warn("Invalid .zibby.yml, falling back to auto-detection")}const l=o(t,"package.json");if(f(l)){const n=JSON.parse(I(l,"utf-8")),e={...n.dependencies,...n.devDependencies};let s="Node.js";return e.next?s="Next.js":e["react-scripts"]?s="Create React App":e.vite&&e.react?s="React + Vite":e["@angular/core"]?s="Angular":e.vue?s="Vue.js":e.express&&(s="Express.js"),{name:n.name||"Unknown Project",framework:s,language:"JavaScript/TypeScript",testCommand:n.scripts?.test||"npm test",installCommand:"npm install"}}return f(o(t,"requirements.txt"))||f(o(t,"pyproject.toml"))?{name:"Python Project",framework:f(o(t,"manage.py"))?"Django":f(o(t,"app.py"))?"Flask":"Python",language:"Python",testCommand:"pytest",installCommand:"pip install -r requirements.txt"}:f(o(t,"Gemfile"))?{name:"Ruby Project",framework:"Rails",language:"Ruby",testCommand:"bundle exec rspec",installCommand:"bundle install"}:f(o(t,"go.mod"))?{name:"Go Project",framework:"Go",language:"Go",testCommand:"go test ./...",installCommand:"go mod download"}:f(o(t,"pom.xml"))?{name:"Java Project",framework:"Spring Boot",language:"Java",testCommand:"./mvnw test",installCommand:"./mvnw install"}:{name:"Unknown Project",framework:"Unknown",language:"Unknown",testCommand:"make test",installCommand:"make install"}}function F(t,i,l){const n=o(K,"../../prompts/implement-ticket.md");let e;try{e=I(n,"utf-8")}catch{e=`
|
|
7
|
+
# Implement Ticket: {{TICKET_KEY}}
|
|
8
|
+
|
|
9
|
+
## Project Context
|
|
10
|
+
{{PROJECT_CONTEXT}}
|
|
11
|
+
|
|
12
|
+
## Ticket Summary
|
|
13
|
+
{{TICKET_SUMMARY}}
|
|
14
|
+
|
|
15
|
+
## Description
|
|
16
|
+
{{TICKET_DESCRIPTION}}
|
|
17
|
+
|
|
18
|
+
## Acceptance Criteria
|
|
19
|
+
{{ACCEPTANCE_CRITERIA}}
|
|
20
|
+
|
|
21
|
+
{{#if ADDITIONAL_CONTEXT}}
|
|
22
|
+
## Additional Context from User
|
|
23
|
+
{{ADDITIONAL_CONTEXT}}
|
|
24
|
+
|
|
25
|
+
{{/if}}
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
You are implementing this ticket. Follow these steps:
|
|
29
|
+
|
|
30
|
+
1. Read the existing codebase and understand the patterns
|
|
31
|
+
2. Implement the feature as described
|
|
32
|
+
3. Write tests to verify the functionality
|
|
33
|
+
4. Ensure all tests pass
|
|
34
|
+
5. Fix any linter errors
|
|
35
|
+
|
|
36
|
+
Now implement this ticket completely!
|
|
37
|
+
`.trim()}const s=i.find(g=>g.isPrimary)||i[0];let y;l?.type==="docker-compose"?y=`\`docker-compose up\` (using ${l.configFile})`:l?.command?y=`\`cd ${s.name} && ${l.command}\``:y="`npm run dev` (or check package.json scripts)";let k;if(i.length===1)k=`
|
|
38
|
+
You are working in **${s.name}**, a ${s.framework} project.
|
|
39
|
+
|
|
40
|
+
**Commands:**
|
|
41
|
+
- Dev server: ${y}
|
|
42
|
+
- Run tests: \`cd ${s.name} && ${s.testCommand}\`
|
|
43
|
+
|
|
44
|
+
You have full access to the codebase in the current directory.
|
|
45
|
+
`.trim();else{const g=i.map(m=>`- **${m.name}/** (${m.framework})${m.isPrimary?" \u2190 **MAKE CHANGES HERE**":" (reference only)"}`).join(`
|
|
46
|
+
`);k=`
|
|
47
|
+
You are working in a **multi-repository** setup with ${i.length} repositories:
|
|
48
|
+
|
|
49
|
+
${g}
|
|
50
|
+
|
|
51
|
+
**Primary Repository:** ${s.name}
|
|
52
|
+
- This is where you should implement the feature
|
|
53
|
+
- Framework: ${s.framework}
|
|
54
|
+
- Dev server: ${y}
|
|
55
|
+
- Run tests: \`cd ${s.name} && ${s.testCommand}\`
|
|
56
|
+
|
|
57
|
+
**Other Repositories:**
|
|
58
|
+
${i.filter(m=>!m.isPrimary).map(m=>`- **${m.name}**: You can read code from here for reference (shared libraries, services, etc.)`).join(`
|
|
59
|
+
`)||"(none)"}
|
|
60
|
+
|
|
61
|
+
**Important:** Make all code changes in the \`${s.name}/\` directory only.
|
|
62
|
+
`.trim()}let a=e.replace(/\{\{TICKET_KEY\}\}/g,t.ticketKey||t.key||"UNKNOWN").replace(/\{\{PROJECT_CONTEXT\}\}/g,k).replace(/\{\{TICKET_SUMMARY\}\}/g,t.summary||"No summary").replace(/\{\{TICKET_DESCRIPTION\}\}/g,t.description||"No description provided").replace(/\{\{ACCEPTANCE_CRITERIA\}\}/g,t.acceptanceCriteria||"Not specified");if(t.additionalContext){const g=`## Additional Context from User
|
|
63
|
+
${t.additionalContext}
|
|
64
|
+
|
|
65
|
+
`;a=a.replace(/\{\{#if ADDITIONAL_CONTEXT\}\}[\s\S]*?\{\{\/if\}\}/g,g)}else a=a.replace(/\{\{#if ADDITIONAL_CONTEXT\}\}[\s\S]*?\{\{\/if\}\}/g,"");return a}export{B as implementCommand};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import{mkdir as A,writeFile as g,readFile as w}from"fs/promises";import{existsSync as h,readdirSync as j}from"fs";import{join as i,resolve as Y,dirname as N}from"path";import{homedir as I}from"os";import K from"inquirer";import e from"chalk";import P from"ora";import{spawn as T,execSync as O}from"child_process";import{fileURLToPath as Z}from"url";const G=Z(import.meta.url),H=N(G);async function F(){try{const r=process.platform==="win32",t=O("npm config get prefix",{encoding:"utf-8"}).trim(),u=r?t:`${t}/bin`,d=u.replace(/^~/,I()),s=r?";":":";if(process.env.PATH.split(s).some(v=>{const M=v.replace(/^~/,I());return M===d||M===u}))return;console.log(e.yellow("\u26A0\uFE0F npm global bin not in PATH")),console.log(e.gray(` Location: ${d}`)),console.log();const{shouldAddPath:n}=await K.prompt([{type:"confirm",name:"shouldAddPath",message:"Add npm global bin to your shell PATH automatically?",default:!0}]);if(!n){r?(console.log(e.gray(`
|
|
2
|
+
\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: ${d}
|
|
3
|
+
`))):(console.log(e.gray(`
|
|
4
|
+
\u{1F4A1} To add manually, run:`)),console.log(e.gray(` echo 'export PATH="${d}:$PATH"' >> ~/.zshrc`)),console.log(e.gray(` source ~/.zshrc
|
|
5
|
+
`)));return}if(r){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: ${d}
|
|
6
|
+
`));return}const x=process.env.SHELL||"";let p="";if(x.includes("zsh"))p=i(I(),".zshrc");else if(x.includes("bash"))p=h(i(I(),".bashrc"))?i(I(),".bashrc"):i(I(),".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="${d}:$PATH"`));return}if(h(p)){const v=await w(p,"utf-8");if(v.includes(d)||v.includes("npm")&&v.includes("global")&&v.includes("bin")){console.log(e.yellow(`\u26A0\uFE0F PATH entry found in ${p} but not active`)),console.log(e.gray(` Run: source ${p}
|
|
7
|
+
`));return}}const b=`
|
|
8
|
+
# npm global bin (added by zibby)
|
|
9
|
+
export PATH="${d}:$PATH"
|
|
10
|
+
`;await g(p,(h(p)?await w(p,"utf-8"):"")+b),console.log(e.green(`\u2705 Added to ${p}`)),console.log(e.yellow(`
|
|
11
|
+
\u{1F4A1} Run this to activate in current session:`)),console.log(e.gray(` source ${p}
|
|
12
|
+
`))}catch{}}async function me(r,t){console.log(e.bold.cyan(`
|
|
13
|
+
\u{1F3AD} Welcome to Zibby Test Automation!
|
|
14
|
+
`));const u=!t.skipMemory,d=["dolt","mem0"].includes(String(t.memoryBackend||"").toLowerCase())?String(t.memoryBackend).toLowerCase():"dolt";await F();const s=r?Y(process.cwd(),r):process.cwd(),E=r||"zibby-tests",y=!!r;y&&h(s)&&(console.log(e.red(`
|
|
15
|
+
\u274C Directory "${r}" already exists!
|
|
16
|
+
`)),process.exit(1)),!y&&h(i(s,".zibby.config.mjs"))&&!t.force&&(console.log(e.yellow(`
|
|
17
|
+
\u26A0\uFE0F Zibby is already initialized in this directory!
|
|
18
|
+
`)),console.log(e.white("Config file found: .zibby.config.mjs")),console.log(e.gray(`Use --force or -f to reinitialize
|
|
19
|
+
`)),process.exit(0)),t.force&&!y&&console.log(e.cyan(`
|
|
20
|
+
Reinitializing Zibby configuration...
|
|
21
|
+
`));let n;if(t.agent&&(t.headed||t.headless))console.log(e.cyan(`Setting up with provided options...
|
|
22
|
+
`)),n={agent:t.agent,browserMode:t.headless?"headless":"headed",apiKey:t.apiKey||null,cloudSync:!!(t.cloudSync||t.apiKey)};else{const b=[];t.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"}),!t.headed&&!t.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"}),t.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 K.prompt(b):{},n.agent=t.agent||n.agent,n.browserMode=t.headless?"headless":t.headed?"headed":n.browserMode,n.apiKey=t.apiKey||n.apiKey,n.cloudSync=!!(t.cloudSync||t.apiKey||n.apiKey&&n.apiKey.trim())}n.mcp="playwright",n.setupMcp=n.agent==="cursor";const p=P("Setting up Zibby...").start();try{if(y&&await A(s,{recursive:!0}),await A(i(s,"test-specs/examples"),{recursive:!0}),await A(i(s,"tests"),{recursive:!0}),await A(i(s,".zibby/output"),{recursive:!0}),await A(i(s,".zibby/commands"),{recursive:!0}),u&&d==="dolt")try{const{initMemory:o,DoltDB:l}=await import("@zibby/memory");if(l.isAvailable()){const{created:a}=o(s);a&&(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=t.template||"browser-test-automation";try{const{graphPath:o,nodesPath:l,readmePath:a,resultHandlerPath:m,template:c}=b.getTemplateFiles(v),f=i(s,".zibby"),z=await w(o,"utf-8");if(await g(i(f,"graph.mjs"),z),m){const C=await w(m,"utf-8");await g(i(f,"result-handler.mjs"),C)}const B=await w(a,"utf-8");await g(i(f,"README.md"),B),await A(i(f,"nodes"),{recursive:!0});const{readdirSync:k}=await import("fs"),S=k(l);for(const C of S){let D=await w(i(l,C),"utf-8");!u&&C==="execute-live.mjs"&&(D=D.replace("skills: [SKILLS.BROWSER, SKILLS.MEMORY],","skills: [SKILLS.BROWSER],")),await g(i(f,"nodes",C),D)}const $=i(c.path,"chat.mjs");if(h($)){const C=await w($,"utf-8");await g(i(f,"chat.mjs"),C)}}catch(o){throw p.fail(`Failed to scaffold template: ${o.message}`),o}p.text="Generating configuration files...";const M=W(n,t,{memoryBackend:d});await g(i(s,".zibby.config.mjs"),M);const L=n.apiKey&&n.apiKey.trim()?V(n,n.apiKey.trim(),d):U(n,d);if(await g(i(s,".env.example"),L),y){const o=q(E,n,{memoryBackend:d});await g(i(s,"package.json"),o)}if(!h(i(s,".gitignore"))){const o=X();await g(i(s,".gitignore"),o)}if(!h(i(s,"playwright.config.js"))){const o=J("on");await g(i(s,"playwright.config.js"),o)}if(!h(i(s,"test-specs/examples/example-domain.txt"))){const o=Q();await g(i(s,"test-specs/examples/example-domain.txt"),o)}const R=Y(H,"../../../../examples/.zibby/commands");if(h(R)){const o=j(R).filter(l=>l.toLowerCase().endsWith(".md"));for(const l of o){const a=i(s,".zibby/commands",l);if(t.force||!h(a)){const m=await w(i(R,l),"utf-8");await g(a,m)}}}if(!h(i(s,".zibby/commands/example.md"))){const o=ee();await g(i(s,".zibby/commands/example.md"),o)}if(y){const o=te(E,n);await g(i(s,"README.md"),o)}if(p.succeed(y?"Project created!":"Zibby initialized!"),y&&!t.skipInstall){const o=P("Installing dependencies...").start();await new Promise((l,a)=>{T("npm",["install"],{cwd:s,stdio:"pipe"}).on("close",c=>{c===0?(o.succeed("Dependencies installed!"),l()):(o.fail("Failed to install dependencies"),a(new Error("npm install failed")))})})}else y||console.log(e.gray(`
|
|
23
|
+
Make sure @zibby/cli is installed in your package.json
|
|
24
|
+
`));if(!y&&u&&d==="mem0"&&(console.log(e.yellow(`
|
|
25
|
+
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
|
|
26
|
+
`))),!t.skipInstall){const o=P("Installing Playwright browsers...").start();await new Promise(l=>{const a=T("npx",["playwright","install","chromium"],{cwd:s,stdio:"pipe"});let m="";a.stdout.on("data",c=>{m+=c.toString()}),a.stderr.on("data",c=>{m+=c.toString()}),a.on("close",c=>{c===0?(m.includes("already installed")||m.includes("up to date")?o.succeed("Playwright browsers already installed"):o.succeed("Playwright browsers installed!"),l()):(o.warn("Could not verify Playwright browsers"),console.log(e.yellow(`
|
|
27
|
+
\u26A0\uFE0F If tests fail, run: npx playwright install
|
|
28
|
+
`)),l())})})}if(n.agent==="codex"&&!t.skipInstall){const o=P("Checking Codex CLI...").start();try{O("codex --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),o.succeed("Codex CLI already installed")}catch{o.text="Installing Codex CLI...";try{await new Promise((l,a)=>{T("npm",["install","-g","@openai/codex"],{stdio:"pipe"}).on("close",c=>{c===0?(o.succeed("Codex CLI installed!"),l()):a(new Error("npm install -g @openai/codex failed"))})})}catch{o.fail("Could not install Codex CLI"),console.log(e.yellow(` Install manually: npm install -g @openai/codex
|
|
29
|
+
`))}}}if(n.agent==="gemini"&&!t.skipInstall){const o=P("Checking Gemini CLI...").start();try{O("gemini --version",{encoding:"utf-8",timeout:5e3,stdio:"pipe"}),o.succeed("Gemini CLI already installed")}catch{o.text="Installing Gemini CLI...";try{await new Promise((a,m)=>{T("npm",["install","-g","@google/gemini-cli"],{stdio:"pipe"}).on("close",f=>{f===0?(o.succeed("Gemini CLI installed!"),a()):m(new Error("npm install -g @google/gemini-cli failed"))})})}catch{o.fail("Could not install Gemini CLI"),console.log(e.yellow(` Install manually: npm install -g @google/gemini-cli
|
|
30
|
+
`))}}const l=P("Configuring Gemini MCP servers...").start();try{const a=i(I(),".gemini"),m=i(a,"settings.json");h(a)||await A(a,{recursive:!0});let c={mcpServers:{}};if(h(m))try{const S=await w(m,"utf-8");c=JSON.parse(S),c.mcpServers||(c.mcpServers={})}catch{}let f;try{const{createRequire:S}=await import("module");f=S(import.meta.url).resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js")}catch{f="@playwright/mcp"}const z=n.browserMode!=="headless",B=f==="@playwright/mcp"?["-y","@playwright/mcp","--isolated","--save-video=1280x720","--viewport-size=1280x720","--output-dir","test-results"]:[f,"--isolated","--save-video=1280x720","--viewport-size=1280x720","--output-dir=test-results"];z||B.push("--headless"),c.mcpServers["playwright-official"]={command:f==="@playwright/mcp"?"npx":"node",args:B},await g(m,`${JSON.stringify(c,null,2)}
|
|
31
|
+
`,"utf-8");let k="Gemini MCP configured";z?k+=" (headed mode - visible browser)":k+=" (headless mode - hidden browser)",l.succeed(k)}catch(a){l.fail("MCP setup failed"),console.log(e.yellow(" You may need to configure ~/.gemini/settings.json manually")),console.log(e.gray(` Error: ${a.message}
|
|
32
|
+
`))}}if(n.agent==="cursor"&&n.setupMcp){const o=P("Setting up Playwright MCP...").start();try{const{setupPlaywrightMcpCommand:l}=await import("./setup-scripts.js"),a=n.cloudSync||!1,m=n.browserMode!=="headless";await l({headed:m,cloudSync:a,video:"on",viewport:{width:1280,height:720}});let c="Playwright MCP configured";m?c+=" (headed mode - visible browser)":c+=" (headless mode - hidden browser)",a&&(c+=" + Zibby MCP (cloud sync)"),o.succeed(c),a&&console.log(e.gray(`
|
|
33
|
+
Copy .env.example to .env and set ZIBBY_API_KEY to enable uploads
|
|
34
|
+
`))}catch(l){o.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
|
|
35
|
+
`)),console.log(e.gray(` Error: ${l.message}
|
|
36
|
+
`))}}console.log(e.bold.green(`
|
|
37
|
+
\u{1F389} All set!
|
|
38
|
+
`)),console.log(e.cyan("Start the Zibby Chat Agent:")),y&&console.log(e.white(` cd ${r}`)),console.log(e.white(` zibby
|
|
39
|
+
`)),console.log(e.cyan("Or run a test directly:")),console.log(e.white(` zibby run test-specs/examples/example-domain.txt
|
|
40
|
+
`)),console.log(e.cyan("Next steps:"));let _=1;console.log(e.white(` ${_++}. cp .env.example .env ${e.gray("# then add your API keys")}`)),u&&console.log(e.white(` ${_++}. Set ${e.bold("ZIBBY_MEMORY_BACKEND")} in .env ${e.gray(`# currently: ${d}`)}`)),console.log(e.white(` ${_++}. Type ${e.bold("zibby")} to chat with the AI testing assistant`)),console.log(e.white(` ${_++}. Write test specs in test-specs/`)),console.log(e.white(` ${_++}. Run: npx zibby run <spec-file>
|
|
41
|
+
`))}catch(b){p.fail("Failed to create project"),console.error(e.red(`
|
|
42
|
+
\u274C Error: ${b.message}
|
|
43
|
+
`)),process.exit(1)}}function W(r,t={},u={}){const d=["dolt","mem0"].includes(String(u.memoryBackend||"").toLowerCase())?String(u.memoryBackend).toLowerCase():"dolt",s={claude:`
|
|
44
|
+
claude: {
|
|
45
|
+
model: 'auto', // Options: 'auto', 'sonnet-4.6', 'opus-4.6', 'sonnet-4.5', 'opus-4.5'
|
|
46
|
+
maxTokens: 4096,
|
|
47
|
+
},`,cursor:`
|
|
48
|
+
cursor: {
|
|
49
|
+
model: 'auto', // Options: 'auto', 'opus-4.5', 'opus-4.5-thinking', 'sonnet-4.5', 'sonnet-4.5-thinking', 'composer-1', 'gpt-5.2-codex', 'gpt-5.2', 'gpt-5.3', 'gpt-5.4', 'gemini-3-pro', 'gemini-3-flash'
|
|
50
|
+
},`,codex:`
|
|
51
|
+
codex: {
|
|
52
|
+
model: 'gpt-5.2-codex', // Options: 'auto', 'o4-mini', 'o3', 'codex-mini', 'gpt-4o', 'gpt-5.2-codex', 'gpt-5.2', 'gpt-5.3', 'gpt-5.4'
|
|
53
|
+
},`,gemini:`
|
|
54
|
+
gemini: {
|
|
55
|
+
model: 'gemini-2.5-pro', // Options: 'auto', 'gemini-2.5-pro', 'gemini-2.5-flash'
|
|
56
|
+
},`},E=r.agent,y=Object.entries(s).filter(([n])=>n!==E).map(([,n])=>n.split(`
|
|
57
|
+
`).map(x=>x.trim()?` // ${x.trimStart()}`:x).join(`
|
|
58
|
+
`)).join(`
|
|
59
|
+
`);return`export default {
|
|
60
|
+
// AI agent settings
|
|
61
|
+
agent: {${s[E]}
|
|
62
|
+
${y}
|
|
63
|
+
strictMode: false,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Browser for Zibby MCP (execute_live). Wizard headed/headless is stored here for \`zibby run\` logs
|
|
67
|
+
// and workflow config. Runtime strategies attach MCP per run (no global Gemini settings mutation).
|
|
68
|
+
browser: {
|
|
69
|
+
mcp: 'playwright',
|
|
70
|
+
headless: ${r.browserMode==="headless"},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Chat memory backend adapter (dolt | mem0)
|
|
74
|
+
memory: {
|
|
75
|
+
backend: '${d}',
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Advanced: Override models per node (optional)
|
|
79
|
+
// models: {
|
|
80
|
+
// default: 'auto', // Fallback for all nodes
|
|
81
|
+
// execute_live: 'claude-opus-4', // Override specific node
|
|
82
|
+
// },
|
|
83
|
+
|
|
84
|
+
// Folder paths
|
|
85
|
+
paths: {
|
|
86
|
+
specs: 'test-specs', // Where your .txt test specs are
|
|
87
|
+
generated: 'tests', // Where generated .spec.js files go
|
|
88
|
+
output: '.zibby/output', // Where workflow execution results are saved (default: .zibby/output)
|
|
89
|
+
// sessionPrefix: 'run', // Optional: prefix for session folders (e.g., run_1772788458045)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Context discovery - auto-discovers CONTEXT.md & AGENTS.md files (cascades from root \u2192 spec directory)
|
|
93
|
+
// Override filenames to search for different files
|
|
94
|
+
context: {
|
|
95
|
+
filenames: ['CONTEXT.md', 'AGENTS.md'], // Auto-discover these files (walks up from spec directory)
|
|
96
|
+
discovery: {
|
|
97
|
+
env: \`env-\${process.env.ENV || 'local'}.js\`, // Additional explicit files
|
|
98
|
+
// fixtures: 'fixtures.js', // Add more as needed
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Video recording (affects playwright.config.js generation)
|
|
103
|
+
video: 'on', // Options: 'off', 'on', 'retain-on-failure', 'on-first-retry'
|
|
104
|
+
|
|
105
|
+
// Browser viewport size for test execution
|
|
106
|
+
// Default: 1280x720 (works well on most screens)
|
|
107
|
+
// For larger displays: { width: 1920, height: 1080 }
|
|
108
|
+
// For mobile testing: { width: 375, height: 667 } (iPhone SE)
|
|
109
|
+
viewport: { width: 1280, height: 720 },
|
|
110
|
+
|
|
111
|
+
// Playwright artifacts (screenshots, traces, videos)
|
|
112
|
+
// Traces contain EXACT selectors for 100% accurate script generation
|
|
113
|
+
// Default: true (stored in test-results/playwright, separate from workflow output)
|
|
114
|
+
playwrightArtifacts: true,
|
|
115
|
+
|
|
116
|
+
// Parallel runs: max concurrent \`zibby run\` processes from the chat agent (run_test) and
|
|
117
|
+
// the number of lanes on Studio Mission Control (when it can read this project\u2019s config).
|
|
118
|
+
parallel: {
|
|
119
|
+
maxConcurrentRuns: 8,
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Cloud sync - auto-upload test results & videos (requires ZIBBY_API_KEY in .env)
|
|
123
|
+
cloudSync: ${r.cloudSync||!1}
|
|
124
|
+
};
|
|
125
|
+
`}function U(r,t="dolt"){return`# Zibby Test Automation - Environment Variables
|
|
126
|
+
|
|
127
|
+
# AI Provider Keys
|
|
128
|
+
${{claude:"ANTHROPIC_API_KEY=sk-ant-your_key_here",cursor:`# Cursor Agent uses cursor-agent CLI \u2014 no API key needed
|
|
129
|
+
# Install: curl https://cursor.com/install -fsS | bash`,codex:"OPENAI_API_KEY=sk-your_key_here",gemini:"GEMINI_API_KEY=your_key_here"}[r.agent]||""}
|
|
130
|
+
|
|
131
|
+
# Zibby Cloud Sync (for uploading test results & videos)
|
|
132
|
+
# Get your API key from: https://zibby.app/settings/tokens
|
|
133
|
+
# ZIBBY_API_KEY=zby_your_api_key_here
|
|
134
|
+
|
|
135
|
+
# Test Memory (Dolt DB) - Auto-compaction settings
|
|
136
|
+
# ZIBBY_MEMORY_MAX_RUNS=3000 # Max test runs to keep per spec
|
|
137
|
+
# ZIBBY_MEMORY_MAX_AGE=1095 # Max age in days for stale data (~3 years)
|
|
138
|
+
# ZIBBY_MEMORY_COMPACT_EVERY=1500 # Auto-compact every N runs (0 to disable)
|
|
139
|
+
|
|
140
|
+
# Chat memory backend
|
|
141
|
+
ZIBBY_MEMORY_BACKEND=${t}
|
|
142
|
+
`}function V(r,t,u="dolt"){return`# Zibby Test Automation - Environment Variables
|
|
143
|
+
|
|
144
|
+
# AI Provider Keys
|
|
145
|
+
${{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"}[r.agent]||""}
|
|
146
|
+
|
|
147
|
+
# Zibby Cloud Sync
|
|
148
|
+
ZIBBY_API_KEY=${t}
|
|
149
|
+
|
|
150
|
+
# Test Memory (Dolt DB) - Auto-compaction settings
|
|
151
|
+
# ZIBBY_MEMORY_MAX_RUNS=3000 # Max test runs to keep per spec
|
|
152
|
+
# ZIBBY_MEMORY_MAX_AGE=1095 # Max age in days for stale data (~3 years)
|
|
153
|
+
# ZIBBY_MEMORY_COMPACT_EVERY=1500 # Auto-compact every N runs (0 to disable)
|
|
154
|
+
|
|
155
|
+
# Chat memory backend
|
|
156
|
+
ZIBBY_MEMORY_BACKEND=${u}
|
|
157
|
+
`}function q(r,t,u={}){const d={"@zibby/cli":"^0.1.25","@zibby/core":"^0.1.20"};return u.memoryBackend==="mem0"&&(d.mem0ai="^2.4.6"),JSON.stringify({name:r,version:"1.0.0",type:"module",private:!0,scripts:{"test:spec":"zibby run",test:"playwright test","test:headed":"playwright test --headed"},dependencies:d,devDependencies:{"@playwright/test":"^1.49.0",dotenv:"^17.2.3"}},null,2)}function X(){return`# Dependencies
|
|
158
|
+
node_modules/
|
|
159
|
+
|
|
160
|
+
# Test artifacts
|
|
161
|
+
.zibby/output/
|
|
162
|
+
playwright-report/
|
|
163
|
+
tests/
|
|
164
|
+
*.webm
|
|
165
|
+
*.mp4
|
|
166
|
+
|
|
167
|
+
# Environment variables
|
|
168
|
+
.env
|
|
169
|
+
|
|
170
|
+
# OS
|
|
171
|
+
.DS_Store
|
|
172
|
+
Thumbs.db
|
|
173
|
+
|
|
174
|
+
# IDE
|
|
175
|
+
.vscode/
|
|
176
|
+
.idea/
|
|
177
|
+
|
|
178
|
+
# Zibby cache
|
|
179
|
+
.zibby/cache/
|
|
180
|
+
.zibby/tenancy.json
|
|
181
|
+
|
|
182
|
+
# Zibby memory (Dolt DB synced separately via dolt remote, not git)
|
|
183
|
+
.zibby/memory/
|
|
184
|
+
.zibby/memory-context.md
|
|
185
|
+
`}function J(r="off",t=null){return`import { defineConfig} from '@playwright/test';
|
|
186
|
+
|
|
187
|
+
export default defineConfig({
|
|
188
|
+
testDir: './tests',
|
|
189
|
+
${t?` outputDir: '${t}',
|
|
190
|
+
`:` outputDir: 'test-results/playwright', // Keep Playwright artifacts separate from Zibby workflow output
|
|
191
|
+
`} timeout: 30000,
|
|
192
|
+
retries: 0,
|
|
193
|
+
workers: 1,
|
|
194
|
+
|
|
195
|
+
use: {
|
|
196
|
+
headless: process.env.PLAYWRIGHT_HEADLESS === '1' ? true : false,
|
|
197
|
+
viewport: { width: 1280, height: 720 },
|
|
198
|
+
screenshot: 'off',
|
|
199
|
+
video: 'off',
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
reporter: [
|
|
203
|
+
['html'],
|
|
204
|
+
['list']
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
`}function Q(){return`Test Specification: Example Domain Navigation
|
|
208
|
+
==============================================
|
|
209
|
+
|
|
210
|
+
Application: Example Domain
|
|
211
|
+
URL: https://example.com
|
|
212
|
+
Feature: Basic navigation and link verification
|
|
213
|
+
|
|
214
|
+
Test Objective:
|
|
215
|
+
---------------
|
|
216
|
+
Verify that the example.com website loads correctly, displays expected content,
|
|
217
|
+
and navigational links function as intended.
|
|
218
|
+
|
|
219
|
+
Test Steps:
|
|
220
|
+
-----------
|
|
221
|
+
1. Navigate to https://example.com
|
|
222
|
+
2. Verify the page title contains "Example Domain"
|
|
223
|
+
3. Verify the main heading is visible and contains "Example Domain"
|
|
224
|
+
4. Locate and verify the "More information..." link is present
|
|
225
|
+
5. Click the "More information..." link
|
|
226
|
+
6. Verify navigation to IANA domain information page
|
|
227
|
+
7. Verify the new page URL contains "iana.org"
|
|
228
|
+
8. Verify the page loaded successfully (no errors)
|
|
229
|
+
|
|
230
|
+
Expected Results:
|
|
231
|
+
-----------------
|
|
232
|
+
- Initial page loads within 3 seconds
|
|
233
|
+
- "Example Domain" heading is clearly visible
|
|
234
|
+
- "More information..." link is clickable and visible
|
|
235
|
+
- Navigation to IANA page succeeds
|
|
236
|
+
- No console errors or network failures
|
|
237
|
+
- Page title updates after navigation
|
|
238
|
+
|
|
239
|
+
Notes:
|
|
240
|
+
------
|
|
241
|
+
- Uses a stable, public domain (example.com) maintained by IANA
|
|
242
|
+
- No authentication required
|
|
243
|
+
- Suitable for CI/CD smoke testing
|
|
244
|
+
`}function ee(){return`# Example command template
|
|
245
|
+
|
|
246
|
+
Use this command when the user asks for a concise testing task breakdown.
|
|
247
|
+
|
|
248
|
+
Required output format:
|
|
249
|
+
1. Goal
|
|
250
|
+
2. Risks
|
|
251
|
+
3. Steps
|
|
252
|
+
4. Expected result
|
|
253
|
+
|
|
254
|
+
Keep the response actionable and short.
|
|
255
|
+
`}function te(r,t){return`# ${r}
|
|
256
|
+
|
|
257
|
+
AI-powered test automation with Zibby.
|
|
258
|
+
|
|
259
|
+
## Setup
|
|
260
|
+
|
|
261
|
+
1. Install dependencies:
|
|
262
|
+
\`\`\`bash
|
|
263
|
+
npm install
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
2. Configure environment:
|
|
267
|
+
\`\`\`bash
|
|
268
|
+
cp .env.example .env
|
|
269
|
+
# Edit .env and add your ${{claude:"ANTHROPIC_API_KEY",codex:"OPENAI_API_KEY",gemini:"GEMINI_API_KEY",cursor:"CURSOR_API_KEY (optional)"}[t.agent]}
|
|
270
|
+
\`\`\`
|
|
271
|
+
|
|
272
|
+
3. Run example test:
|
|
273
|
+
\`\`\`bash
|
|
274
|
+
npx zibby run test-specs/examples/example-domain.txt
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## Usage
|
|
278
|
+
|
|
279
|
+
### Run Test Specs
|
|
280
|
+
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
# Run a test spec (executes + generates + verifies)
|
|
283
|
+
npx zibby run test-specs/auth/login.txt
|
|
284
|
+
|
|
285
|
+
# Run in headless mode
|
|
286
|
+
npx zibby run test-specs/auth/login.txt --headless
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
### Write Test Specs
|
|
290
|
+
|
|
291
|
+
Create test specifications in \`test-specs/\`:
|
|
292
|
+
|
|
293
|
+
\`\`\`
|
|
294
|
+
test-specs/
|
|
295
|
+
auth/
|
|
296
|
+
login.txt
|
|
297
|
+
logout.txt
|
|
298
|
+
dashboard/
|
|
299
|
+
overview.txt
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
### Run Generated Tests
|
|
303
|
+
|
|
304
|
+
\`\`\`bash
|
|
305
|
+
# Run all generated tests
|
|
306
|
+
npx playwright test
|
|
307
|
+
|
|
308
|
+
# Run specific test
|
|
309
|
+
npx playwright test tests/auth/login.spec.js
|
|
310
|
+
|
|
311
|
+
# Run with UI
|
|
312
|
+
npx playwright test --ui
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
## Configuration
|
|
316
|
+
|
|
317
|
+
Edit \`.zibby.config.mjs\` to customize:
|
|
318
|
+
- Agent settings (model, temperature)
|
|
319
|
+
- Browser settings (headless, viewport)
|
|
320
|
+
- Cloud sync${t.cloudSync?" (enabled)":" (disabled)"}
|
|
321
|
+
- Self-healing behavior
|
|
322
|
+
|
|
323
|
+
## Project Structure
|
|
324
|
+
|
|
325
|
+
\`\`\`
|
|
326
|
+
${r}/
|
|
327
|
+
\u251C\u2500\u2500 .zibby/
|
|
328
|
+
\u2502 \u251C\u2500\u2500 graph.mjs # Workflow definition
|
|
329
|
+
\u2502 \u251C\u2500\u2500 nodes/ # Custom nodes
|
|
330
|
+
\u2502 \u2514\u2500\u2500 output/ # Workflow execution results (gitignored)
|
|
331
|
+
\u2502 \u2514\u2500\u2500 sessions/ # Session artifacts & recordings
|
|
332
|
+
\u251C\u2500\u2500 .zibby.config.mjs # Configuration
|
|
333
|
+
\u251C\u2500\u2500 .env.example # Environment template (cp to .env and configure)
|
|
334
|
+
\u251C\u2500\u2500 test-specs/ # Test specifications (committed)
|
|
335
|
+
\u2502 \u2514\u2500\u2500 examples/
|
|
336
|
+
\u2502 \u2514\u2500\u2500 example-domain.txt
|
|
337
|
+
\u2514\u2500\u2500 tests/ # Generated tests (gitignored)
|
|
338
|
+
\`\`\`
|
|
339
|
+
|
|
340
|
+
## Learn More
|
|
341
|
+
|
|
342
|
+
- Documentation: https://docs.zibby.dev
|
|
343
|
+
- Examples: https://github.com/zibby/examples
|
|
344
|
+
`}export{me as initCommand};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import t from"chalk";import k from"ora";import{getApiUrl as u}from"../config/environments.js";import{getSessionToken as y,getUserInfo as I,saveProjects as E}from"../config/config.js";async function z(){const s=k("Fetching projects...").start();try{const n=y(),r=I();n||(s.fail("Not logged in"),console.log(t.yellow(`
|
|
2
|
+
Please log in first:`)),console.log(t.gray(` zibby login
|
|
3
|
+
`)),process.exit(1)),r&&(console.log(t.gray(`Logged in as: ${r.email}`)),r.account_id&&console.log(t.gray(`Account ID: ${r.account_id}
|
|
4
|
+
`)));const p=u(),c=await fetch(`${p}/projects`,{headers:{Authorization:`Bearer ${n}`}});if(!c.ok){const o=await c.json().catch(()=>({}));throw new Error(o.error||"Failed to fetch projects")}const e=(await c.json()).projects||[],g=e.map(o=>({name:o.name,projectId:o.projectId,apiToken:o.apiToken}));if(E(g),s.stop(),e.length===0){console.log(`
|
|
5
|
+
No projects found`),console.log(`Create a project at: https://zibby.app/projects
|
|
6
|
+
`);return}const d=o=>o&&o.length>8?`${o.substring(0,8)}\u2022\u2022\u2022\u2022`:o||"-",a=Math.max(4,...e.map(o=>o.name.length)),i=36,l=14,j=` ${"Name".padEnd(a)} ${"Project ID".padEnd(i)} ${"API Token".padEnd(l)}`,m=` ${"\u2500".repeat(a)} ${"\u2500".repeat(i)} ${"\u2500".repeat(l)}`;console.log(`
|
|
7
|
+
Your Projects (${e.length})
|
|
8
|
+
`),console.log(j),console.log(m);for(const o of e){const f=o.name.padEnd(a),h=o.projectId.padEnd(i),$=d(o.apiToken).padEnd(l);console.log(` ${f} ${h} ${$}`)}console.log(""),console.log("\u{1F4A1} Use the Project ID with --project flag:"),console.log(` zibby run test-specs/login.txt --project <project-id> --sync
|
|
9
|
+
`)}catch(n){s.fail("Failed to fetch projects"),console.error(t.red(`
|
|
10
|
+
\u274C Error: ${n.message}
|
|
11
|
+
`)),process.exit(1)}}export{z as listProjectsCommand};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import o from"chalk";const y=`
|
|
2
|
+
Install @zibby/memory and Dolt to enable test memory:
|
|
3
|
+
|
|
4
|
+
npm install @zibby/memory # add the package
|
|
5
|
+
brew install dolt # macOS
|
|
6
|
+
# or: curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash
|
|
7
|
+
`;async function d(){try{return await import("@zibby/memory")}catch{return console.log(o.yellow(`
|
|
8
|
+
@zibby/memory is not installed.
|
|
9
|
+
`)),console.log(o.white(y)),null}}async function h(){const s=await d();if(!s)return;const l=process.cwd(),e=s.getStats(l);if(!e.available){console.log(o.red(`
|
|
10
|
+
Dolt is not installed.
|
|
11
|
+
`)),console.log(o.white(" Install Dolt to enable test memory:")),console.log(o.gray(" brew install dolt # macOS")),console.log(o.gray(` curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash # Linux
|
|
12
|
+
`));return}if(!e.initialized){console.log(o.yellow(`
|
|
13
|
+
Memory database not initialized.
|
|
14
|
+
`)),console.log(o.white(" Run `zibby init` or `zibby memory init` to set it up.\n"));return}console.log(o.bold.cyan(`
|
|
15
|
+
Zibby Test Memory
|
|
16
|
+
`)),console.log(o.gray(` Dolt: ${e.doltVersion}`));const a=o.gray(` ${"\u2500".repeat(40)}`);console.log(a);const n=e.counts;if(console.log(o.white(` Test runs: ${o.cyan(n.runs)} (${o.green(`${n.passed} passed`)}, ${n.failed>0?o.red(`${n.failed} failed`):o.gray(`${n.failed} failed`)})`)),console.log(o.white(` Selectors: ${o.cyan(n.selectors)} tracked`)),console.log(o.white(` Pages: ${o.cyan(n.pages)} discovered`)),console.log(o.white(` Transitions: ${o.cyan(n.transitions)} mapped`)),console.log(o.white(` Insights: ${o.cyan(n.insights||0)} saved`)),e.recentRuns.length>0){console.log(`
|
|
17
|
+
${a}`),console.log(o.white(" Recent runs:"));for(const t of e.recentRuns){const c=t.passed?o.green("\u2713"):o.red("\u2717"),r=t.duration_ms?o.gray(`${(t.duration_ms/1e3).toFixed(1)}s`):"";console.log(` ${c} ${o.white(t.spec_path)} ${r}`)}}if(e.topSelectors.length>0){console.log(`
|
|
18
|
+
${a}`),console.log(o.white(" Top selectors:"));for(const t of e.topSelectors){const c=t.success_count+t.failure_count,r=Math.round(t.success_count/c*100);console.log(` ${o.cyan(t.stable_id||"?")} \u2192 ${t.element_desc} (${r}%, ${c} uses)`)}}if(e.log){console.log(`
|
|
19
|
+
${a}`),console.log(o.white(" Recent commits:"));const t=e.log.split(`
|
|
20
|
+
`).filter(c=>c.startsWith("commit")||c.trim().startsWith("run ")).slice(0,10);for(const c of t)console.log(o.gray(` ${c.trim()}`))}console.log("")}async function f(s){const l=await d();if(!l)return;const e=process.cwd();if(!s.force){console.log(o.yellow(`
|
|
21
|
+
This will permanently delete the memory database.
|
|
22
|
+
`)),console.log(o.white(` Run with --force to confirm: zibby memory reset --force
|
|
23
|
+
`));return}const a=l.resetMemory(e);console.log(a?o.green(`
|
|
24
|
+
Memory database reset.
|
|
25
|
+
`):o.gray(`
|
|
26
|
+
No memory database found.
|
|
27
|
+
`))}async function b(s){const l=await d();if(!l)return;const e=process.cwd(),a=l.getStats(e);if(!a.initialized){console.log(o.yellow(`
|
|
28
|
+
Memory database not initialized.
|
|
29
|
+
`));return}const n=s.maxRuns||50,t=s.maxAge||90;console.log(o.white(`
|
|
30
|
+
Compacting memory (keep last ${n} runs/spec, prune data older than ${t}d)...`));const c=a.counts;if(l.compactMemory(e,{maxRuns:n,maxAgeDays:t}).pruned){const m=l.getStats(e).counts,i=g=>c[g]-m[g];console.log(o.green(" Done.")),i("runs")>0&&console.log(o.gray(` Pruned ${i("runs")} old runs`)),i("selectors")>0&&console.log(o.gray(` Pruned ${i("selectors")} stale selectors`)),i("insights")>0&&console.log(o.gray(` Pruned ${i("insights")} old insights`)),i("transitions")>0&&console.log(o.gray(` Pruned ${i("transitions")} stale transitions`)),console.log(o.gray(` Dolt GC completed
|
|
31
|
+
`))}else console.log(o.gray(` Nothing to compact.
|
|
32
|
+
`))}async function p(){const s=await d();if(!s)return;const l=process.cwd();if(!s.DoltDB.isAvailable()){console.log(o.red(`
|
|
33
|
+
Dolt is not installed.
|
|
34
|
+
`)),console.log(o.white(" Install Dolt:")),console.log(o.gray(" brew install dolt # macOS")),console.log(o.gray(` curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash # Linux
|
|
35
|
+
`));return}const{created:e,available:a}=s.initMemory(l);e?console.log(o.green(`
|
|
36
|
+
Memory database initialized at .zibby/memory/
|
|
37
|
+
`)):a&&console.log(o.gray(`
|
|
38
|
+
Memory database already initialized.
|
|
39
|
+
`))}export{b as memoryCompactCommand,p as memoryInitCommand,f as memoryResetCommand,h as memoryStatsCommand};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{resolve as p}from"path";import{existsSync as l}from"fs";import t from"chalk";import{readRunCapacityWaitSnapshot as u}from"@zibby/core/utils/run-capacity-queue.js";import{resolveRunCapacityPaths as g}from"@zibby/core/utils/run-capacity-coordinator.js";async function C(r){const n=process.cwd(),c=p(n,r.config||".zibby.config.mjs");let s={paths:{output:".zibby/output",specs:"test-specs",generated:"tests"}};if(l(c))try{const e=(await import(c)).default||{};s={...s,...e,paths:{...s.paths,...e.paths||{}}}}catch(o){console.log(t.yellow(`Could not load config: ${o.message}`))}const{outputAbs:i}=g(n,s),a=u(i);if(console.log(t.bold.cyan(`
|
|
2
|
+
Run capacity wait queue`)),console.log(t.gray(`Output: ${i}
|
|
3
|
+
`)),a.length===0){console.log(t.gray(`(empty \u2014 no CLI processes are waiting for capacity)
|
|
4
|
+
`));return}for(const o of a){const e=[t.cyan(String(o.id||"").slice(0,8)),o.specHint?t.white(o.specHint):"",o.source?t.gray(`[${o.source}]`):"",o.studioTestCaseId?t.gray(`tc:${String(o.studioTestCaseId).slice(0,8)}\u2026`):"",`pid ${o.pid}`].filter(Boolean).join(" ");console.log(e)}console.log()}export{C as runCapacityQueueListCommand};
|