ripplo 0.4.12 → 0.4.14
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/index.js +214 -181
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
`)}function
|
|
2
|
+
var on=Object.defineProperty;var cs=Object.getOwnPropertyDescriptor;var ds=Object.getOwnPropertyNames;var us=Object.prototype.hasOwnProperty;var ps=(e,t)=>{for(var r in t)on(e,r,{get:t[r],enumerable:!0})},nn=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ds(t))!us.call(e,o)&&o!==r&&on(e,o,{get:()=>t[o],enumerable:!(n=cs(t,o))||n.enumerable});return e},Kt=(e,t,r)=>(nn(e,t,"default"),r&&nn(r,t,"default"));import{execFileSync as ms}from"child_process";import fs from"fs";import dt from"path";function Jt(e){let t=ys(e);return t??sn(e)}function sn(e){let t=dt.dirname(e);return gs(dt.join(e,".ripplo"))?e:t===e?null:sn(t)}function gs(e){try{return fs.statSync(e).isDirectory()}catch{return!1}}function ys(e){let t=hs(["rev-parse","--show-toplevel"],e);if(t==null)return null;let r=t.trim();return r.length===0?null:dt.isAbsolute(r)?r:dt.resolve(e,r)}function hs(e,t){try{return ms("git",e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","ignore"]})}catch{return null}}import cn from"fs";import mt from"path";import{config as vs}from"dotenv";import{z as pt}from"zod";import ze from"fs";import Yt from"path";import{z as ut}from"zod";var an=Yt.join(".ripplo","project.json"),Qt=[".env",".env.local"],ws=ut.object({envFiles:ut.array(ut.string().min(1)).optional(),projectId:ut.string().min(1)});function Xt(e){return Yt.join(e,an)}function Zt(e){let t=Xt(e);if(!ze.existsSync(t))throw new Error(`ripplo: missing ${an}. Run \`ripplo init\` to create it.`);let r=JSON.parse(ze.readFileSync(t,"utf8")),n=ws.parse(r);return{envFiles:n.envFiles??Qt,projectId:n.projectId}}function er(e){let t=Xt(e);if(ze.existsSync(t))try{return Zt(e)}catch{return}}function ln({cwd:e,envFiles:t,projectId:r}){let n=Xt(e);ze.mkdirSync(Yt.dirname(n),{recursive:!0});let o={projectId:r};t!=null&&!bs(t)&&(o.envFiles=[...t]),ze.writeFileSync(n,JSON.stringify(o,null,2)+`
|
|
3
|
+
`)}function bs(e){return e.length!==Qt.length?!1:e.every((t,r)=>t===Qt[r])}function dn(e){let r=er(e)?.envFiles??[".env",".env.local"],n=mt.join(e,".ripplo");r.forEach(o=>{let i=mt.resolve(n,o);cn.existsSync(i)&&vs({override:!0,path:i,quiet:!0})}),tr=void 0}var ks=pt.object({RIPPLO_APP_URL:pt.url(),RIPPLO_ENGINE_URL:pt.url(),RIPPLO_WEBHOOK_SECRET:pt.string().min(1)}),tr;function Ee(){return tr??=Ss(),tr}function Ss(){let e=ks.safeParse(process.env);if(!e.success){let r=e.error.issues.map(s=>` ${s.path.join(".")}: ${s.message}`).join(`
|
|
4
4
|
`),n=rr(process.cwd()),o=n.map(s=>` ${s}`).join(`
|
|
5
5
|
`),i=n.length>0?`
|
|
6
6
|
|
|
@@ -9,23 +9,23 @@ ${o}
|
|
|
9
9
|
If you're in a git worktree, the env file may not have been copied from the main checkout \u2014 recreate it (or symlink to a shared file outside the working tree).`:"";throw new Error(`ripplo: env config invalid:
|
|
10
10
|
${r}
|
|
11
11
|
|
|
12
|
-
Add missing values to the env file(s) declared in .ripplo/project.json.${i}`)}let t=e.data;return{appUrl:t.RIPPLO_APP_URL,engineUrl:t.RIPPLO_ENGINE_URL,webhookSecret:t.RIPPLO_WEBHOOK_SECRET}}function rr(e){let r=er(e)?.envFiles??[],n=mt.join(e,".ripplo");return r.map(o=>mt.resolve(n,o)).filter(o=>!
|
|
13
|
-
`&&(p(),l="",c=0,r=!1);if(!
|
|
12
|
+
Add missing values to the env file(s) declared in .ripplo/project.json.${i}`)}let t=e.data;return{appUrl:t.RIPPLO_APP_URL,engineUrl:t.RIPPLO_ENGINE_URL,webhookSecret:t.RIPPLO_WEBHOOK_SECRET}}function rr(e){let r=er(e)?.envFiles??[],n=mt.join(e,".ripplo");return r.map(o=>mt.resolve(n,o)).filter(o=>!cn.existsSync(o))}import Yg from"yargs";import{hideBin as Xg}from"yargs/helpers";import{graphql as _c}from"gql.tada";async function ft(e,t,r){let n;try{return n=await e.context().newCDPSession(e),await r(n)}catch{return t}finally{await n?.detach().catch(()=>{})}}async function z({awaitPromise:e,expression:t,page:r}){return ft(r,void 0,async n=>{let o=await n.send("Runtime.evaluate",{awaitPromise:e,expression:t,returnByValue:!0});if(o.exceptionDetails==null)return o.result.value})}var Rs=Object.prototype.toString,Ie=Array.isArray||function(t){return Rs.call(t)==="[object Array]"};function or(e){return typeof e=="function"}function Ps(e){return Ie(e)?"array":typeof e}function nr(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function un(e,t){return e!=null&&typeof e=="object"&&t in e}function xs(e,t){return e!=null&&typeof e!="object"&&e.hasOwnProperty&&e.hasOwnProperty(t)}var Cs=RegExp.prototype.test;function Es(e,t){return Cs.call(e,t)}var As=/\S/;function Is(e){return!Es(As,e)}var Ts={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function $s(e){return String(e).replace(/[&<>"'`=\/]/g,function(r){return Ts[r]})}var js=/\s*/,Ns=/\s+/,pn=/\s*=/,Os=/\s*\}/,Ds=/#|\^|\/|>|\{|&|=|!/;function Ls(e,t){if(!e)return[];var r=!1,n=[],o=[],i=[],s=!1,a=!1,l="",c=0;function p(){if(s&&!a)for(;i.length;)delete o[i.pop()];else i=[];s=!1,a=!1}var m,k,j;function N(re){if(typeof re=="string"&&(re=re.split(Ns,2)),!Ie(re)||re.length!==2)throw new Error("Invalid tags: "+re);m=new RegExp(nr(re[0])+"\\s*"),k=new RegExp("\\s*"+nr(re[1])),j=new RegExp("\\s*"+nr("}"+re[1]))}N(t||X.tags);for(var f=new Ke(e),W,$,V,te,Ce,oe;!f.eos();){if(W=f.pos,V=f.scanUntil(m),V)for(var qe=0,B=V.length;qe<B;++qe)te=V.charAt(qe),Is(te)?(i.push(o.length),l+=te):(a=!0,r=!0,l+=" "),o.push(["text",te,W,W+1]),W+=1,te===`
|
|
13
|
+
`&&(p(),l="",c=0,r=!1);if(!f.scan(m))break;if(s=!0,$=f.scan(Ds)||"name",f.scan(js),$==="="?(V=f.scanUntil(pn),f.scan(pn),f.scanUntil(k)):$==="{"?(V=f.scanUntil(j),f.scan(Os),f.scanUntil(k),$="&"):V=f.scanUntil(k),!f.scan(k))throw new Error("Unclosed tag at "+f.pos);if($==">"?Ce=[$,V,W,f.pos,l,c,r]:Ce=[$,V,W,f.pos],c++,o.push(Ce),$==="#"||$==="^")n.push(Ce);else if($==="/"){if(oe=n.pop(),!oe)throw new Error('Unopened section "'+V+'" at '+W);if(oe[1]!==V)throw new Error('Unclosed section "'+oe[1]+'" at '+W)}else $==="name"||$==="{"||$==="&"?a=!0:$==="="&&N(V)}if(p(),oe=n.pop(),oe)throw new Error('Unclosed section "'+oe[1]+'" at '+f.pos);return Us(_s(o))}function _s(e){for(var t=[],r,n,o=0,i=e.length;o<i;++o)r=e[o],r&&(r[0]==="text"&&n&&n[0]==="text"?(n[1]+=r[1],n[3]=r[3]):(t.push(r),n=r));return t}function Us(e){for(var t=[],r=t,n=[],o,i,s=0,a=e.length;s<a;++s)switch(o=e[s],o[0]){case"#":case"^":r.push(o),n.push(o),r=o[4]=[];break;case"/":i=n.pop(),i[5]=o[2],r=n.length>0?n[n.length-1][4]:t;break;default:r.push(o)}return t}function Ke(e){this.string=e,this.tail=e,this.pos=0}Ke.prototype.eos=function(){return this.tail===""};Ke.prototype.scan=function(t){var r=this.tail.match(t);if(!r||r.index!==0)return"";var n=r[0];return this.tail=this.tail.substring(n.length),this.pos+=n.length,n};Ke.prototype.scanUntil=function(t){var r=this.tail.search(t),n;switch(r){case-1:n=this.tail,this.tail="";break;case 0:n="";break;default:n=this.tail.substring(0,r),this.tail=this.tail.substring(r)}return this.pos+=n.length,n};function Ae(e,t){this.view=e,this.cache={".":this.view},this.parent=t}Ae.prototype.push=function(t){return new Ae(t,this)};Ae.prototype.lookup=function(t){var r=this.cache,n;if(r.hasOwnProperty(t))n=r[t];else{for(var o=this,i,s,a,l=!1;o;){if(t.indexOf(".")>0)for(i=o.view,s=t.split("."),a=0;i!=null&&a<s.length;)a===s.length-1&&(l=un(i,s[a])||xs(i,s[a])),i=i[s[a++]];else i=o.view[t],l=un(o.view,t);if(l){n=i;break}o=o.parent}r[t]=n}return or(n)&&(n=n.call(this.view)),n};function H(){this.templateCache={_cache:{},set:function(t,r){this._cache[t]=r},get:function(t){return this._cache[t]},clear:function(){this._cache={}}}}H.prototype.clearCache=function(){typeof this.templateCache<"u"&&this.templateCache.clear()};H.prototype.parse=function(t,r){var n=this.templateCache,o=t+":"+(r||X.tags).join(":"),i=typeof n<"u",s=i?n.get(o):void 0;return s==null&&(s=Ls(t,r),i&&n.set(o,s)),s};H.prototype.render=function(t,r,n,o){var i=this.getConfigTags(o),s=this.parse(t,i),a=r instanceof Ae?r:new Ae(r,void 0);return this.renderTokens(s,a,n,t,o)};H.prototype.renderTokens=function(t,r,n,o,i){for(var s="",a,l,c,p=0,m=t.length;p<m;++p)c=void 0,a=t[p],l=a[0],l==="#"?c=this.renderSection(a,r,n,o,i):l==="^"?c=this.renderInverted(a,r,n,o,i):l===">"?c=this.renderPartial(a,r,n,i):l==="&"?c=this.unescapedValue(a,r):l==="name"?c=this.escapedValue(a,r,i):l==="text"&&(c=this.rawValue(a)),c!==void 0&&(s+=c);return s};H.prototype.renderSection=function(t,r,n,o,i){var s=this,a="",l=r.lookup(t[1]);function c(k){return s.render(k,r,n,i)}if(l){if(Ie(l))for(var p=0,m=l.length;p<m;++p)a+=this.renderTokens(t[4],r.push(l[p]),n,o,i);else if(typeof l=="object"||typeof l=="string"||typeof l=="number")a+=this.renderTokens(t[4],r.push(l),n,o,i);else if(or(l)){if(typeof o!="string")throw new Error("Cannot use higher-order sections without the original template");l=l.call(r.view,o.slice(t[3],t[5]),c),l!=null&&(a+=l)}else a+=this.renderTokens(t[4],r,n,o,i);return a}};H.prototype.renderInverted=function(t,r,n,o,i){var s=r.lookup(t[1]);if(!s||Ie(s)&&s.length===0)return this.renderTokens(t[4],r,n,o,i)};H.prototype.indentPartial=function(t,r,n){for(var o=r.replace(/[^ \t]/g,""),i=t.split(`
|
|
14
14
|
`),s=0;s<i.length;s++)i[s].length&&(s>0||!n)&&(i[s]=o+i[s]);return i.join(`
|
|
15
|
-
`)};B.prototype.renderPartial=function(t,r,n,o){if(n){var i=this.getConfigTags(o),s=or(n)?n(t[1]):n[t[1]];if(s!=null){var a=t[6],l=t[5],c=t[4],p=s;l==0&&c&&(p=this.indentPartial(s,c,a));var m=this.parse(p,i);return this.renderTokens(m,r,n,p,o)}}};B.prototype.unescapedValue=function(t,r){var n=r.lookup(t[1]);if(n!=null)return n};B.prototype.escapedValue=function(t,r,n){var o=this.getConfigEscape(n)||X.escape,i=r.lookup(t[1]);if(i!=null)return typeof i=="number"&&o===X.escape?String(i):o(i)};B.prototype.rawValue=function(t){return t[1]};B.prototype.getConfigTags=function(t){return je(t)?t:t&&typeof t=="object"?t.tags:void 0};B.prototype.getConfigEscape=function(t){if(t&&typeof t=="object"&&!je(t))return t.escape};var X={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(e){Ge.templateCache=e},get templateCache(){return Ge.templateCache}},Ge=new B;X.clearCache=function(){return Ge.clearCache()};X.parse=function(t,r){return Ge.parse(t,r)};X.render=function(t,r,n,o){if(typeof t!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+Es(t)+'" was given as the first argument for mustache#render(template, view, partials)');return Ge.render(t,r,n,o)};X.escape=Os;X.Scanner=Ke;X.Context=Ae;X.Writer=B;var hn=X;function ir({defs:e}){return e==null?{}:Object.fromEntries(Object.entries(e).map(([t,r])=>[t,Ws({def:r,name:t})]))}var Bs={boolean:!1,number:0,string:""};function Ws({def:e,name:t}){if(e.type==="env"){let r=process.env[e.key];if(r==null)throw new Error(`Environment variable "${e.key}" not set for variable "${t}"`);return r}return e.default??Bs[e.type]??""}function gt({ref:e,variables:t}){if(e.type==="static")return e.value;let r=t[e.name];if(r==null||typeof r=="object")throw new Error(`Variable "${e.name}" is not defined`);return r}function M({ref:e,variables:t}){let r=String(gt({ref:e,variables:t}));return ht({raw:r,variables:t})}function yt({ref:e,variables:t}){let r=gt({ref:e,variables:t});return typeof r=="string"?ht({raw:r,variables:t}):r}function ht({raw:e,variables:t}){return hn.render(e,t,{},{escape:r=>r})}function sr({ref:e,variables:t}){let r=gt({ref:e,variables:t});if(typeof r=="number")return r;let n=Number(r);if(Number.isNaN(n))throw new TypeError(`Cannot convert "${String(r)}" to number`);return n}function Te({name:e,store:t,value:r}){return{...t,[e]:r}}function O({locator:e,page:t,variables:r}){switch(e.by){case"testId":return t.getByTestId(wn(e.value,r));case"role":return t.getByRole(e.role,{exact:!0,name:e.name==null?void 0:wn(e.name,r)})}}function wn(e,t){return t==null?e:ht({raw:e,variables:t})}async function bn({node:e,page:t,timeout:r}){await O({locator:e.source,page:t,variables:void 0}).dragTo(O({locator:e.target,page:t,variables:void 0}),{timeout:r})}async function vn({node:e,page:t,timeout:r}){await O({locator:e.locator,page:t,variables:void 0}).click({button:"right",timeout:r})}function kn({node:e,page:t,variables:r}){return t.once("dialog",async n=>{e.action==="accept"?await n.accept(e.promptText??void 0):await n.dismiss()}),{variables:r}}async function Sn({node:e,page:t,variables:r}){if(e.action==="write"){if(e.value==null)throw new Error("clipboard write requires a value");let o=M({ref:e.value,variables:r}),i=JSON.stringify(o);return await z({awaitPromise:!0,expression:`navigator.clipboard.writeText(${i})`,page:t}),{variables:r}}if(e.variable==null)throw new Error("clipboard read requires a variable name to store the result");let n=await z({awaitPromise:!0,expression:"navigator.clipboard.readText()",page:t})??"";return{variables:Te({name:e.variable,store:r,value:n})}}async function Rn({node:e,page:t}){let r=t.context();e.state==="granted"?await r.grantPermissions([e.permission]):await r.clearPermissions()}async function ar({node:e,page:t,timeout:r,variables:n}){switch(e.type){case"click":case"check":case"uncheck":case"clear":case"dblclick":case"focus":case"hover":case"scrollIntoView":return qs({node:e,page:t,timeout:r,variables:n});case"rightClick":return await vn({node:e,page:t,timeout:r}),{variables:n};case"goto":{let o=M({ref:e.url,variables:n});return await t.goto(o,{timeout:r,waitUntil:"domcontentloaded"}),await t.waitForLoadState("load",{timeout:r}),await t.waitForFunction("document.body != null && document.body.childElementCount > 0 && (document.body.textContent?.trim().length ?? 0) > 0",void 0,{timeout:r}),await z({awaitPromise:!0,expression:"new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(() => resolve(undefined))); })",page:t}),{variables:n}}case"fill":return Gs({node:e,page:t,timeout:r,variables:n});case"select":return Ks({node:e,page:t,timeout:r,variables:n});case"type":return Js({node:e,page:t,timeout:r,variables:n});case"press":return await zs({node:e,page:t,timeout:r}),{variables:n};case"drag":return await bn({node:e,page:t,timeout:r}),{variables:n};case"upload":return Qs({node:e,page:t,timeout:r,variables:n});case"extractText":return Ys({node:e,page:t,timeout:r,variables:n});case"setVariable":{let o=yt({ref:e.value,variables:n});return{variables:Te({name:e.variable,store:n,value:o})}}case"fail":throw new Error(e.message);case"setViewport":return await t.setViewportSize({height:e.height,width:e.width}),{variables:n};case"handleDialog":return kn({node:e,page:t,variables:n});case"clipboard":return Sn({node:e,page:t,variables:n});case"setPermission":return await Rn({node:e,page:t}),{variables:n}}}var Ms={check:"check",clear:"clear",click:"click",dblclick:"dblclick",focus:"focus",hover:"hover",scrollIntoView:"scrollIntoViewIfNeeded",uncheck:"uncheck"};async function qs({node:e,page:t,timeout:r,variables:n}){return await O({locator:e.locator,page:t,variables:n})[Ms[e.type]]({timeout:r}),{variables:n}}async function zs({node:e,page:t,timeout:r}){e.locator==null?await t.keyboard.press(e.key):await O({locator:e.locator,page:t,variables:void 0}).press(e.key,{timeout:r})}async function Gs({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).fill(o,{timeout:r}),{variables:n}}async function Ks({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).selectOption(o,{timeout:r}),{variables:n}}async function Js({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).pressSequentially(o,{timeout:r}),{variables:n}}async function Qs({node:e,page:t,timeout:r,variables:n}){return await O({locator:e.locator,page:t,variables:n}).setInputFiles(e.files,{timeout:r}),{variables:n}}async function Ys({node:e,page:t,timeout:r,variables:n}){let o=await O({locator:e.locator,page:t,variables:n}).textContent({timeout:r});if(o==null)throw new Error("extractText: element had no text content");return{variables:Te({name:e.variable,store:n,value:o})}}var H={};gs(H,{default:()=>Xs});Kt(H,Uy);import*as Uy from"playwright/test";import{default as Xs}from"playwright/test";async function lr({expected:e,locator:t,operator:r,timeout:n}){await Je({expected:e,operator:r,timeout:n,read:async()=>await t.textContent()??""});let o=await Qe(async()=>await t.textContent()??"");return $e({actual:o,expected:e,label:"Text",operator:r})}async function cr({expected:e,operator:t,page:r,timeout:n}){return await Je({expected:e,operator:t,timeout:n,read:()=>Promise.resolve(r.url())}),$e({actual:r.url(),expected:e,label:"URL",operator:t})}async function Pn({expected:e,operator:t,page:r,timeout:n}){await Je({expected:e,operator:t,timeout:n,read:()=>r.title()});let o=await Qe(()=>r.title());return $e({actual:o,expected:e,label:"Title",operator:t})}async function ur({expected:e,locator:t,operator:r,timeout:n}){await H.expect.poll(async()=>On({actual:await t.count(),expected:e,operator:r}),{timeout:n}).toBe(!0).catch(()=>{});let o=await t.count();return Nn({actual:o,expected:e,label:"Count",operator:r})}async function dr({expected:e,locator:t,operator:r,timeout:n}){await Je({expected:e,operator:r,timeout:n,read:()=>t.inputValue()});let o=await Qe(()=>t.inputValue());return $e({actual:o,expected:e,label:"Value",operator:r})}async function pr({locator:e,timeout:t}){return ge({description:"Element is visible",failureDetail:"Element not visible",run:()=>(0,H.expect)(e).toBeVisible({timeout:t})})}async function mr({locator:e,timeout:t}){return ge({description:"Element is not visible",failureDetail:"Element still visible",run:()=>(0,H.expect)(e).toBeHidden({timeout:t})})}async function xn({attribute:e,expected:t,locator:r,operator:n,timeout:o}){await Je({expected:t,operator:n,timeout:o,read:async()=>await r.getAttribute(e)??""});let i=await Qe(async()=>await r.getAttribute(e)??"");return $e({actual:i,expected:t,label:`Attribute "${e}"`,operator:n})}async function Cn({locator:e,timeout:t}){return ge({description:"Element is enabled",failureDetail:"Element is disabled or not found",run:()=>(0,H.expect)(e).toBeEnabled({timeout:t})})}async function En({locator:e,timeout:t}){return ge({description:"Element is disabled",failureDetail:"Element is enabled or not found",run:()=>(0,H.expect)(e).toBeDisabled({timeout:t})})}async function In({locator:e,timeout:t}){return ge({description:"Element is checked",failureDetail:"Element is not checked",run:()=>(0,H.expect)(e).toBeChecked({timeout:t})})}async function An({locator:e,timeout:t}){return ge({description:"Element is not checked",failureDetail:"Element is checked",run:()=>(0,H.expect)(e).not.toBeChecked({timeout:t})})}async function jn({locator:e,timeout:t}){return ge({description:"Element is focused",failureDetail:"Element is not focused",run:()=>(0,H.expect)(e).toBeFocused({timeout:t})})}async function Tn({locator:e,timeout:t}){return ge({description:"Element is not focused",failureDetail:"Element is focused",run:()=>(0,H.expect)(e).not.toBeFocused({timeout:t})})}async function ge({description:e,failureDetail:t,run:r}){try{return await r(),{description:e,detail:void 0,status:"passed"}}catch{return{description:e,detail:t,status:"failed"}}}async function Je({expected:e,operator:t,read:r,timeout:n}){await H.expect.poll(async()=>{let o=await Qe(r);return $n({actual:o,expected:e,operator:t})},{timeout:n}).toBe(!0).catch(()=>{})}async function Qe(e){return e().catch(()=>"")}function $e(e){let t=`${e.label} ${e.operator} "${e.expected}"`,r=$n(e),n=r?void 0:`Got: "${e.actual}"`;return{description:t,detail:n,status:r?"passed":"failed"}}function $n({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="notEquals"?e!==t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):new RegExp(t).test(e)}function Nn(e){let t=`${e.label} ${e.operator} ${String(e.expected)}`,r=On(e),n=r?void 0:`Got: ${String(e.actual)}`;return{description:t,detail:n,status:r?"passed":"failed"}}function On({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="notEquals"?e!==t:r==="greaterThan"?e>t:r==="greaterThanOrEqual"?e>=t:r==="lessThan"?e<t:e<=t}import{execFile as ea}from"child_process";import{createRequire as ta}from"module";import Dn from"fs";import Ln from"path";import{chromium as _n}from"playwright";import Wy from"fs";import qy from"path";import Zs from"pino";var f=Zs({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});async function Ye({headed:e}){process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY="1";try{return await _n.launch({headless:!e})}catch(t){throw ra(t)?new Error(`Playwright browsers are not installed. Run:
|
|
15
|
+
`)};H.prototype.renderPartial=function(t,r,n,o){if(n){var i=this.getConfigTags(o),s=or(n)?n(t[1]):n[t[1]];if(s!=null){var a=t[6],l=t[5],c=t[4],p=s;l==0&&c&&(p=this.indentPartial(s,c,a));var m=this.parse(p,i);return this.renderTokens(m,r,n,p,o)}}};H.prototype.unescapedValue=function(t,r){var n=r.lookup(t[1]);if(n!=null)return n};H.prototype.escapedValue=function(t,r,n){var o=this.getConfigEscape(n)||X.escape,i=r.lookup(t[1]);if(i!=null)return typeof i=="number"&&o===X.escape?String(i):o(i)};H.prototype.rawValue=function(t){return t[1]};H.prototype.getConfigTags=function(t){return Ie(t)?t:t&&typeof t=="object"?t.tags:void 0};H.prototype.getConfigEscape=function(t){if(t&&typeof t=="object"&&!Ie(t))return t.escape};var X={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(e){Ge.templateCache=e},get templateCache(){return Ge.templateCache}},Ge=new H;X.clearCache=function(){return Ge.clearCache()};X.parse=function(t,r){return Ge.parse(t,r)};X.render=function(t,r,n,o){if(typeof t!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+Ps(t)+'" was given as the first argument for mustache#render(template, view, partials)');return Ge.render(t,r,n,o)};X.escape=$s;X.Scanner=Ke;X.Context=Ae;X.Writer=H;var mn=X;function ir({defs:e}){return e==null?{}:Object.fromEntries(Object.entries(e).map(([t,r])=>[t,Fs({def:r,name:t})]))}var Vs={boolean:!1,number:0,string:""};function Fs({def:e,name:t}){if(e.type==="env"){let r=process.env[e.key];if(r==null)throw new Error(`Environment variable "${e.key}" not set for variable "${t}"`);return r}return e.default??Vs[e.type]??""}function gt({ref:e,variables:t}){if(e.type==="static")return e.value;let r=t[e.name];if(r==null||typeof r=="object")throw new Error(`Variable "${e.name}" is not defined`);return r}function M({ref:e,variables:t}){let r=String(gt({ref:e,variables:t}));return ht({raw:r,variables:t})}function yt({ref:e,variables:t}){let r=gt({ref:e,variables:t});return typeof r=="string"?ht({raw:r,variables:t}):r}function ht({raw:e,variables:t}){return mn.render(e,t,{},{escape:r=>r})}function sr({ref:e,variables:t}){let r=gt({ref:e,variables:t});if(typeof r=="number")return r;let n=Number(r);if(Number.isNaN(n))throw new TypeError(`Cannot convert "${String(r)}" to number`);return n}function Te({name:e,store:t,value:r}){return{...t,[e]:r}}function O({locator:e,page:t,variables:r}){switch(e.by){case"testId":return t.getByTestId(fn(e.value,r));case"role":return t.getByRole(e.role,{exact:!0,name:e.name==null?void 0:fn(e.name,r)})}}function fn(e,t){return t==null?e:ht({raw:e,variables:t})}async function gn({node:e,page:t,timeout:r}){await O({locator:e.source,page:t,variables:void 0}).dragTo(O({locator:e.target,page:t,variables:void 0}),{timeout:r})}async function yn({node:e,page:t,timeout:r}){await O({locator:e.locator,page:t,variables:void 0}).click({button:"right",timeout:r})}function hn({node:e,page:t,variables:r}){return t.once("dialog",async n=>{e.action==="accept"?await n.accept(e.promptText??void 0):await n.dismiss()}),{variables:r}}async function wn({node:e,page:t,variables:r}){if(e.action==="write"){if(e.value==null)throw new Error("clipboard write requires a value");let o=M({ref:e.value,variables:r}),i=JSON.stringify(o);return await z({awaitPromise:!0,expression:`navigator.clipboard.writeText(${i})`,page:t}),{variables:r}}if(e.variable==null)throw new Error("clipboard read requires a variable name to store the result");let n=await z({awaitPromise:!0,expression:"navigator.clipboard.readText()",page:t})??"";return{variables:Te({name:e.variable,store:r,value:n})}}async function bn({node:e,page:t}){let r=t.context();e.state==="granted"?await r.grantPermissions([e.permission]):await r.clearPermissions()}async function ar({node:e,page:t,timeout:r,variables:n}){switch(e.type){case"click":case"check":case"uncheck":case"clear":case"dblclick":case"focus":case"hover":case"scrollIntoView":return Ws({node:e,page:t,timeout:r,variables:n});case"rightClick":return await yn({node:e,page:t,timeout:r}),{variables:n};case"goto":{let o=M({ref:e.url,variables:n});return await t.goto(o,{timeout:r,waitUntil:"domcontentloaded"}),await t.waitForLoadState("load",{timeout:r}),await t.waitForFunction("document.body != null && document.body.childElementCount > 0 && (document.body.textContent?.trim().length ?? 0) > 0",void 0,{timeout:r}),await z({awaitPromise:!0,expression:"new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(() => resolve(undefined))); })",page:t}),{variables:n}}case"fill":return Ms({node:e,page:t,timeout:r,variables:n});case"select":return qs({node:e,page:t,timeout:r,variables:n});case"type":return zs({node:e,page:t,timeout:r,variables:n});case"press":return await Bs({node:e,page:t,timeout:r}),{variables:n};case"drag":return await gn({node:e,page:t,timeout:r}),{variables:n};case"upload":return Gs({node:e,page:t,timeout:r,variables:n});case"extractText":return Ks({node:e,page:t,timeout:r,variables:n});case"setVariable":{let o=yt({ref:e.value,variables:n});return{variables:Te({name:e.variable,store:n,value:o})}}case"fail":throw new Error(e.message);case"setViewport":return await t.setViewportSize({height:e.height,width:e.width}),{variables:n};case"handleDialog":return hn({node:e,page:t,variables:n});case"clipboard":return wn({node:e,page:t,variables:n});case"setPermission":return await bn({node:e,page:t}),{variables:n}}}var Hs={check:"check",clear:"clear",click:"click",dblclick:"dblclick",focus:"focus",hover:"hover",scrollIntoView:"scrollIntoViewIfNeeded",uncheck:"uncheck"};async function Ws({node:e,page:t,timeout:r,variables:n}){return await O({locator:e.locator,page:t,variables:n})[Hs[e.type]]({timeout:r}),{variables:n}}async function Bs({node:e,page:t,timeout:r}){e.locator==null?await t.keyboard.press(e.key):await O({locator:e.locator,page:t,variables:void 0}).press(e.key,{timeout:r})}async function Ms({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).fill(o,{timeout:r}),{variables:n}}async function qs({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).selectOption(o,{timeout:r}),{variables:n}}async function zs({node:e,page:t,timeout:r,variables:n}){let o=M({ref:e.value,variables:n});return await O({locator:e.locator,page:t,variables:n}).pressSequentially(o,{timeout:r}),{variables:n}}async function Gs({node:e,page:t,timeout:r,variables:n}){return await O({locator:e.locator,page:t,variables:n}).setInputFiles(e.files,{timeout:r}),{variables:n}}async function Ks({node:e,page:t,timeout:r,variables:n}){let o=await O({locator:e.locator,page:t,variables:n}).textContent({timeout:r});if(o==null)throw new Error("extractText: element had no text content");return{variables:Te({name:e.variable,store:n,value:o})}}var F={};ps(F,{default:()=>Js});Kt(F,_y);import*as _y from"playwright/test";import{default as Js}from"playwright/test";async function lr({expected:e,locator:t,operator:r,timeout:n}){await Je({expected:e,operator:r,timeout:n,read:async()=>await t.textContent()??""});let o=await Qe(async()=>await t.textContent()??"");return $e({actual:o,expected:e,label:"Text",operator:r})}async function cr({expected:e,operator:t,page:r,timeout:n}){return await Je({expected:e,operator:t,timeout:n,read:()=>Promise.resolve(r.url())}),$e({actual:r.url(),expected:e,label:"URL",operator:t})}async function vn({expected:e,operator:t,page:r,timeout:n}){await Je({expected:e,operator:t,timeout:n,read:()=>r.title()});let o=await Qe(()=>r.title());return $e({actual:o,expected:e,label:"Title",operator:t})}async function dr({expected:e,locator:t,operator:r,timeout:n}){await F.expect.poll(async()=>Tn({actual:await t.count(),expected:e,operator:r}),{timeout:n}).toBe(!0).catch(()=>{});let o=await t.count();return In({actual:o,expected:e,label:"Count",operator:r})}async function ur({expected:e,locator:t,operator:r,timeout:n}){await Je({expected:e,operator:r,timeout:n,read:()=>t.inputValue()});let o=await Qe(()=>t.inputValue());return $e({actual:o,expected:e,label:"Value",operator:r})}async function pr({locator:e,timeout:t}){return me({description:"Element is visible",failureDetail:"Element not visible",run:()=>(0,F.expect)(e).toBeVisible({timeout:t})})}async function mr({locator:e,timeout:t}){return me({description:"Element is not visible",failureDetail:"Element still visible",run:()=>(0,F.expect)(e).toBeHidden({timeout:t})})}async function kn({attribute:e,expected:t,locator:r,operator:n,timeout:o}){await Je({expected:t,operator:n,timeout:o,read:async()=>await r.getAttribute(e)??""});let i=await Qe(async()=>await r.getAttribute(e)??"");return $e({actual:i,expected:t,label:`Attribute "${e}"`,operator:n})}async function Sn({locator:e,timeout:t}){return me({description:"Element is enabled",failureDetail:"Element is disabled or not found",run:()=>(0,F.expect)(e).toBeEnabled({timeout:t})})}async function Rn({locator:e,timeout:t}){return me({description:"Element is disabled",failureDetail:"Element is enabled or not found",run:()=>(0,F.expect)(e).toBeDisabled({timeout:t})})}async function Pn({locator:e,timeout:t}){return me({description:"Element is checked",failureDetail:"Element is not checked",run:()=>(0,F.expect)(e).toBeChecked({timeout:t})})}async function xn({locator:e,timeout:t}){return me({description:"Element is not checked",failureDetail:"Element is checked",run:()=>(0,F.expect)(e).not.toBeChecked({timeout:t})})}async function Cn({locator:e,timeout:t}){return me({description:"Element is focused",failureDetail:"Element is not focused",run:()=>(0,F.expect)(e).toBeFocused({timeout:t})})}async function En({locator:e,timeout:t}){return me({description:"Element is not focused",failureDetail:"Element is focused",run:()=>(0,F.expect)(e).not.toBeFocused({timeout:t})})}async function me({description:e,failureDetail:t,run:r}){try{return await r(),{description:e,detail:void 0,status:"passed"}}catch{return{description:e,detail:t,status:"failed"}}}async function Je({expected:e,operator:t,read:r,timeout:n}){await F.expect.poll(async()=>{let o=await Qe(r);return An({actual:o,expected:e,operator:t})},{timeout:n}).toBe(!0).catch(()=>{})}async function Qe(e){return e().catch(()=>"")}function $e(e){let t=`${e.label} ${e.operator} "${e.expected}"`,r=An(e),n=r?void 0:`Got: "${e.actual}"`;return{description:t,detail:n,status:r?"passed":"failed"}}function An({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="notEquals"?e!==t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):new RegExp(t).test(e)}function In(e){let t=`${e.label} ${e.operator} ${String(e.expected)}`,r=Tn(e),n=r?void 0:`Got: ${String(e.actual)}`;return{description:t,detail:n,status:r?"passed":"failed"}}function Tn({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="notEquals"?e!==t:r==="greaterThan"?e>t:r==="greaterThanOrEqual"?e>=t:r==="lessThan"?e<t:e<=t}import{execFile as Ys}from"child_process";import{createRequire as Xs}from"module";import $n from"fs";import jn from"path";import{chromium as Nn}from"playwright";import Wy from"fs";import My from"path";import Qs from"pino";var y=Qs({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});async function Ye({headed:e}){process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY="1";try{return await Nn.launch({headless:!e})}catch(t){throw Zs(t)?new Error(`Playwright browsers are not installed. Run:
|
|
16
16
|
|
|
17
17
|
npx playwright install chromium
|
|
18
|
-
`):t}}async function fr(){let e=
|
|
18
|
+
`):t}}async function fr(){let e=Nn.executablePath();if($n.existsSync(e))return;y.info("Chromium not found. Installing via Playwright...");let t=Xs(import.meta.url),r=jn.dirname(t.resolve("playwright/package.json")),n=jn.join(r,"cli.js");if(await new Promise((o,i)=>{Ys(process.execPath,[n,"install","chromium"],s=>{if(s!=null){i(new Error(`Playwright install failed: ${s.message}`));return}o()})}),!$n.existsSync(e))throw new Error(`Playwright browser installation failed. Try running manually:
|
|
19
19
|
|
|
20
20
|
npx playwright install chromium
|
|
21
|
-
`);
|
|
21
|
+
`);y.info("\u2713 Chromium installed")}function Zs(e){return e instanceof Error?e.message.includes("Executable doesn't exist"):!1}import{readdir as ia,rm as sa,stat as aa}from"fs/promises";import oh from"os";import Ln from"path";import{graphql as la}from"gql.tada";import{print as ea}from"graphql";var ta=15e3;async function g(e){let t=ea(e.document),r=JSON.stringify({query:t,variables:e.variables}),n=`${e.config.ripploServerUrl}/graphql`,o;try{o=await fetch(n,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST",signal:AbortSignal.timeout(ta)})}catch(s){let a=s instanceof Error?s.message:String(s);throw new Error(`Failed to connect to Ripplo server at ${e.config.ripploServerUrl}. Make sure \`npx ripplo\` is running and your internet connection is active. Details: ${a}`)}let i=await ra({res:o,url:n});if(!na(i))throw new Error("Invalid GraphQL response");if(oa(i))throw new Error(i.errors.map(s=>s.message).join(", "));if(i.data==null)throw new Error("No data returned from server");return i.data}async function ra({res:e,url:t}){let r=await e.text();if(r.length===0)throw new Error(`Empty response from Ripplo server at ${t} (status ${String(e.status)}). The server may have restarted or the request was rejected before a body was sent.`);try{return JSON.parse(r)}catch{throw new Error(`Non-JSON response from Ripplo server at ${t} (status ${String(e.status)}): ${r.slice(0,200)}`)}}function na(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function oa(e){return Array.isArray(e.errors)&&e.errors.length>0}var On=Ln.join(process.cwd(),".ripplo","debug"),Dn=360*60*1e3,ch=la(`
|
|
22
22
|
mutation RevokeCurrentCliToken {
|
|
23
23
|
revokeCurrentCliToken
|
|
24
24
|
}
|
|
25
|
-
`);async function gr({maxRuns:e}){try{let r=(await
|
|
26
|
-
`))}}import tt from"fs";import
|
|
27
|
-
`),t}function et(e,t){return Xe.join(hr(e),t)}function wr(){return Xe.join(
|
|
28
|
-
`,{mode:384})}function kr(){let e=
|
|
25
|
+
`);async function gr({maxRuns:e}){try{let r=(await ia(On,{withFileTypes:!0})).filter(c=>c.isDirectory()),n=Date.now(),o=await Promise.all(r.map(async c=>{let p=Ln.join(On,c.name),m=await aa(p);return{dirPath:p,mtime:m.mtimeMs}})),i=o.filter(c=>n-c.mtime>Dn),a=o.filter(c=>n-c.mtime<=Dn).toSorted((c,p)=>p.mtime-c.mtime).slice(e),l=[...i,...a];if(l.length===0)return;await Promise.allSettled(l.map(c=>sa(c.dirPath,{force:!0,recursive:!0}))),y.info("Pruned %d old debug run(s)",l.length)}catch{y.warn("Debug run pruning failed, ignoring")}}var ca=5e3;async function yr({appUrl:e}){try{await fetch(e,{method:"HEAD",signal:AbortSignal.timeout(ca)})}catch(t){let r=t instanceof Error?t.message:String(t);throw y.debug("App URL reachability check failed: %s",r),new Error([`Could not reach your dev server at ${e}.`,"","Troubleshooting:",` 1. Make sure your app is running at ${e}`," 2. Check RIPPLO_APP_URL in the env file declared in .ripplo/project.json"," 3. If you're in a git worktree, RIPPLO_APP_URL and RIPPLO_ENGINE_URL must point at this worktree's dev server port (not main's)"].join(`
|
|
26
|
+
`))}}import tt from"fs";import Pe from"fs";import da from"os";import Xe from"path";var ua=".local",pa=".ripplo";function hr(e){return Xe.join(e,".ripplo",ua)}function Ze(e){let t=hr(e);Pe.existsSync(t)||Pe.mkdirSync(t,{recursive:!0});let r=Xe.join(t,".gitignore");return Pe.existsSync(r)||Pe.writeFileSync(r,`*
|
|
27
|
+
`),t}function et(e,t){return Xe.join(hr(e),t)}function wr(){return Xe.join(da.homedir(),pa)}function br(){let e=wr();return Pe.existsSync(e)?Pe.chmodSync(e,448):Pe.mkdirSync(e,{mode:448,recursive:!0}),e}function fe(e){return Xe.join(wr(),e)}function Z(){let e=process.env.RIPPLO_TOKEN;if(e!=null&&e.trim().length>0)return e.trim();let t=fe("token");if(!tt.existsSync(t))return null;let r=tt.readFileSync(t,"utf8").trim();return r.length===0?null:r}function vr(e){br(),tt.writeFileSync(fe("token"),e+`
|
|
28
|
+
`,{mode:384})}function kr(){let e=fe("token");return tt.existsSync(e)?(tt.unlinkSync(e),!0):!1}import{z as cc}from"zod";import{z as wt}from"zod";var ma=wt.object({__codec:wt.string().min(1),data:wt.unknown(),version:wt.number().int().positive()}),bt=class extends Error{codec;currentVersion;gotVersion;constructor(t){super(`Unsupported ${t.codec} version ${String(t.gotVersion)} (current ${String(t.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`),this.name="CodecVersionError",this.codec=t.codec,this.currentVersion=t.currentVersion,this.gotVersion=t.gotVersion}},vt=class extends Error{constructor(t){super(`Codec mismatch: expected "${t.expected}", got "${t.got}"`),this.name="CodecMismatchError"}};function kt(e){return _n({legacy:void 0,migrators:[],name:e,schemas:[]})}function _n(e){return{initial:t=>Sr(t,{...e,schemas:[t]}),legacy:t=>_n({...e,legacy:t})}}function Sr(e,t){return{build:()=>fa(e,t),legacy:r=>Sr(e,{...t,legacy:r}),upgrade:(r,n)=>{let o=n;return Sr(r,{...t,migrators:[...t.migrators,o],schemas:[...t.schemas,r]})}}}function fa(e,t){let r=t.schemas.length;return{currentVersion:r,name:t.name,decode:n=>ga(e,t,n),encode:n=>({__codec:t.name,data:n,version:r})}}function ga(e,t,r){let{data:n,version:o}=ya(t,r),i=Un(t,n,o);return e.parse(i)}function ya(e,t){let r=ma.safeParse(t);if(r.success){if(r.data.__codec!==e.name)throw new vt({expected:e.name,got:r.data.__codec});return{data:r.data.data,version:r.data.version}}if(e.legacy!=null&&e.legacy.detect(t))return{data:t,version:e.legacy.assumedVersion};throw new Error(`Cannot decode "${e.name}": value is not a codec envelope and no legacy detector matched.`)}function Un(e,t,r){let n=e.schemas.length;if(r>n)throw new bt({codec:e.name,currentVersion:n,gotVersion:r});if(r===n)return t;let o=e.schemas[r-1];if(o==null)throw new Error(`Codec "${e.name}" missing schema for v${String(r)}; cannot migrate.`);let i=o.parse(t),s=e.migrators[r-1];if(s==null)throw new Error(`Codec "${e.name}" missing migrator v${String(r)} \u2192 v${String(r+1)}.`);return Un(e,s(i),r+1)}import{z as Wn}from"zod";import{z as je}from"zod";var Rr=je.object({depends:je.array(je.string().min(1)).optional().describe("Names of other preconditions that must be satisfied first. Resolved via topological sort; cycles are rejected at validation time."),description:je.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:je.array(je.string().min(1)).optional().describe("Keys that the execute response's data field will contain. e.g. ['projectId', 'workflowId']. These are used for route param interpolation ({{projectId}}) and workflow variables. Declared here so generated types are strongly typed per precondition.")}).describe("A named precondition declared at the graph level. States reference these by name.");import{z as d}from"zod";import{z as ge}from"zod";var ha=ge.object({by:ge.literal("testId"),value:ge.string().min(1)}),wa=ge.object({by:ge.literal("role"),name:ge.string().optional(),role:ge.string().min(1)}),S=ge.discriminatedUnion("by",[ha,wa]);import{z as Vn}from"zod";var ye=Vn.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),St=Vn.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as A}from"zod";var ba=A.object({type:A.literal("static"),value:A.union([A.string(),A.number(),A.boolean()])}),Rt=A.object({name:A.string().min(1),type:A.literal("variable")}),Pt=A.discriminatedUnion("type",[ba,Rt]),G=A.discriminatedUnion("type",[A.object({type:A.literal("static"),value:A.string()}),Rt]),xt=A.discriminatedUnion("type",[A.object({type:A.literal("static"),value:A.number().int().nonnegative()}),Rt]),Pr=A.discriminatedUnion("type",[A.object({type:A.literal("static"),value:A.union([A.string(),A.number(),A.boolean()])}),Rt]);import{z as K}from"zod";var Ct=K.discriminatedUnion("type",[K.object({default:K.string().optional(),type:K.literal("string")}),K.object({default:K.number().optional(),type:K.literal("number")}),K.object({default:K.boolean().optional(),type:K.literal("boolean")}),K.object({key:K.string().min(1),type:K.literal("env")})]);var b={id:d.string().min(1).max(200),label:d.string().max(500).optional(),next:d.string().max(200).optional(),uiOnly:d.boolean().optional()},Fn=500,va=d.object({...b,type:d.literal("goto"),url:G}),ka=d.object({...b,locator:S,type:d.literal("click")}),Sa=d.object({...b,locator:S,type:d.literal("fill"),value:G}),Ra=d.object({...b,locator:S,type:d.literal("select"),value:G}),Pa=d.object({...b,locator:S,type:d.literal("hover")}),xa=d.object({...b,key:d.string().min(1),locator:S.optional(),type:d.literal("press")}),Ca=d.object({...b,locator:S,type:d.literal("check")}),Ea=d.object({...b,locator:S,type:d.literal("uncheck")}),Aa=d.object({...b,height:d.number().int().positive(),type:d.literal("setViewport"),width:d.number().int().positive()}),Ia=d.object({...b,message:d.string().min(1),type:d.literal("fail")}),Ta=d.object({...b,type:d.literal("setVariable"),value:Pt,variable:d.string().min(1)}),$a=d.object({...b,locator:S,type:d.literal("extractText"),variable:d.string().min(1)}),ja=d.object({...b,files:d.array(d.string()).min(1),locator:S,type:d.literal("upload")}),Na=d.object({...b,locator:S,type:d.literal("dblclick")}),Oa=d.object({...b,source:S,target:S,type:d.literal("drag")}),Da=d.object({...b,locator:S,type:d.literal("scrollIntoView")}),La=d.object({...b,locator:S,type:d.literal("type"),value:G}),_a=d.object({...b,locator:S,type:d.literal("focus")}),Ua=d.object({...b,locator:S,type:d.literal("clear")}),Va=d.object({...b,locator:S,type:d.literal("rightClick")}),Fa=d.object({...b,action:d.enum(["accept","dismiss"]),promptText:d.string().optional(),type:d.literal("handleDialog")}),Ha=d.object({...b,action:d.enum(["read","write"]),type:d.literal("clipboard"),value:G.optional(),variable:d.string().min(1).optional()}),Wa=d.object({...b,permission:d.string().min(1),state:d.enum(["granted","prompt"]),type:d.literal("setPermission")}),Ba=d.object({...b,locator:S,type:d.literal("assertVisible")}),Ma=d.object({...b,locator:S,type:d.literal("assertNotVisible")}),qa=d.object({...b,expected:G,locator:S,operator:ye,type:d.literal("assertText")}),za=d.object({...b,expected:G,operator:ye,type:d.literal("assertUrl")}),Ga=d.object({...b,expected:xt,locator:S,operator:St,type:d.literal("assertCount")}),Ka=d.object({...b,expected:G,locator:S,operator:ye,type:d.literal("assertValue")}),Ja=d.object({...b,attribute:d.string().min(1),expected:G,locator:S,operator:ye,type:d.literal("assertAttribute")}),Qa=d.object({...b,locator:S,type:d.literal("assertEnabled")}),Ya=d.object({...b,locator:S,type:d.literal("assertDisabled")}),Xa=d.object({...b,expected:G,operator:ye,type:d.literal("assertTitle")}),Za=d.object({...b,locator:S,type:d.literal("assertChecked")}),el=d.object({...b,locator:S,type:d.literal("assertNotChecked")}),tl=d.object({...b,locator:S,type:d.literal("assertFocused")}),rl=d.object({...b,locator:S,type:d.literal("assertNotFocused")}),nl=d.object({...b,budget:d.enum(["fast","slow","async"]),observer:d.string().min(1).max(200),params:d.record(d.string().max(200),Pr),type:d.literal("assertObserver")}),Hn=d.discriminatedUnion("type",[va,ka,Sa,Ra,Pa,xa,Ca,Ea,Ba,Ma,qa,za,Ga,Ka,Ja,Qa,Ya,Aa,Ia,Ta,$a,ja,Na,Oa,Da,La,_a,Ua,Va,Fa,Ha,Wa,Xa,Za,el,tl,rl,nl]),Et=d.object({entryNode:d.string().min(1).max(200),nodes:d.record(d.string().max(200),Hn).refine(e=>Object.keys(e).length<=Fn,`Workflow has more than ${String(Fn)} nodes`),uiOnly:d.boolean().optional(),variableNamespaces:d.record(d.string().max(200),d.string().max(500)).optional(),variables:d.record(d.string().max(200),Ct).optional()});var ol=Wn.record(Wn.string().max(200),Rr),il={assumedVersion:1,detect:e=>typeof e=="object"&&e!==null&&"entryNode"in e&&"nodes"in e},sl={assumedVersion:1,detect:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)&&!("__codec"in e)},xr=kt("workflow-spec").legacy(il).initial(Et).build(),al=kt("precondition-map").legacy(sl).initial(ol).build();import{z as Cr}from"zod";var Bn=["fast","slow","async"],ll=Cr.object({budget:Cr.enum(Bn).describe("Polling budget tier: fast | slow | async"),description:Cr.string().min(1).describe("Human-readable description of what this observer checks")}).describe("A named backend state observer. Tests assert against it with assert.backend(observer, params). Implementation lives on the user's server.");import{graphql as eo,readFragment as dc}from"gql.tada";import{mkdir as Er,writeFile as he}from"fs/promises";import ne from"path";async function Mn({context:e,page:t,runId:r,stepIndex:n,stepResult:o}){try{let i=ne.join(process.cwd(),".ripplo","debug",r,"steps",String(n));await Er(i,{recursive:!0});let s={assertions:o.assertions,detail:o.detail,duration:o.duration,nodeId:o.nodeId,nodeType:o.nodeType,snapshotTimestamp:o.snapshotTimestamp,status:o.status,stepIndex:o.stepIndex,title:o.title,url:o.url};await Promise.all([he(ne.join(i,"info.json"),JSON.stringify(s,null,2)),cl({context:e,page:t,stepDir:i})])}catch(i){let s=i instanceof Error?i.message:String(i);y.warn("Failed to write step debug data for step %d: %s",n,s)}}async function cl({context:e,page:t,stepDir:r}){let[n,o,i,s]=await Promise.all([dl(t),ul(t),fl({context:e,page:t}),pl(t)]);await Promise.all([he(ne.join(r,"dom.html"),n),he(ne.join(r,"accessibility-tree.txt"),o),he(ne.join(r,"storage.json"),JSON.stringify(i,null,2)),s==null?Promise.resolve():he(ne.join(r,"screenshot.png"),s)])}async function dl(e){return await z({awaitPromise:!1,expression:"document.documentElement.outerHTML",page:e})??"(unable to capture DOM)"}async function ul(e){return ft(e,"(unable to capture accessibility tree)",async t=>{let r=await t.send("Accessibility.getFullAXTree");return JSON.stringify(r,null,2)})}async function pl(e){return ft(e,void 0,async t=>{let r=await t.send("Page.captureScreenshot",{format:"png"});return Buffer.from(r.data,"base64")})}var ml=`(() => {
|
|
29
29
|
function dump(s) {
|
|
30
30
|
var out = {};
|
|
31
31
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -35,39 +35,43 @@ Add missing values to the env file(s) declared in .ripplo/project.json.${i}`)}le
|
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
37
|
return { localStorage: dump(localStorage), sessionStorage: dump(sessionStorage) };
|
|
38
|
-
})()`;async function
|
|
39
|
-
`)}async function
|
|
38
|
+
})()`;async function fl({context:e,page:t}){let[r,n]=await Promise.all([z({awaitPromise:!1,expression:ml,page:t}),e.cookies().catch(()=>[])]);return{cookies:n,localStorage:r?.localStorage??{},sessionStorage:r?.sessionStorage??{}}}async function qn({runId:e,steps:t,summary:r}){try{let n=ne.join(process.cwd(),".ripplo","debug",e);await Er(n,{recursive:!0}),await he(ne.join(n,"summary.txt"),gl({steps:t,summary:r})),y.info("Debug artifacts written to .ripplo/debug/%s/",e)}catch(n){let o=n instanceof Error?n.message:String(n);y.warn("Failed to write run debug summary: %s",o)}}function gl({steps:e,summary:t}){return[`Run ID: ${t.runId}`,`Workflow: ${t.workflowName}`,`Status: ${t.status}`,`Duration: ${String(t.duration)}ms`,`Passed: ${String(t.passCount)}`,`Failed: ${String(t.failCount)}`,"","Steps:",...e.map(n=>` [${String(n.stepIndex)}] ${n.status==="passed"?"\u2713":"\u2717"} ${n.title} (${String(n.duration)}ms) \u2014 ${n.detail??"ok"}`)].join(`
|
|
39
|
+
`)}async function zn({error:e,runId:t,stack:r,workflowName:n}){try{let o=ne.join(process.cwd(),".ripplo","debug",t);await Er(o,{recursive:!0});let i=r==null?e:`${e}
|
|
40
40
|
|
|
41
41
|
${r}`,s=[`Run ID: ${t}`,`Workflow: ${n}`,"Status: failed","",`Error: ${e}`,"",r==null?"":`Stack trace:
|
|
42
42
|
${r}`].join(`
|
|
43
|
-
`);await Promise.all([
|
|
44
|
-
`),
|
|
45
|
-
`+
|
|
46
|
-
`),
|
|
47
|
-
\u2192 /ripplo:debug \u2014 check .ripplo/debug/<runId>/ artifacts`,status:"failed"}}function
|
|
48
|
-
Load \`/ripplo:setup\` skill for instructions on engine endpoint + webhook secret wiring.`,status:"failed"}:t.kind==="pass"?{description:`observer "${e}"`,detail:n.pollCount>1?`passed after ${String(n.pollCount)} polls (${
|
|
49
|
-
Load \`/ripplo:create\` skill for instructions.`,status:"failed"}:(n.lastReason=t.reason,null)}async function Ol({apiUrl:e,observer:t,params:r,webhookSecret:n}){let o=JSON.stringify({observer:t,params:r});try{let i=await fetch(`${e}/execute-observer`,{body:o,headers:{"Content-Type":"application/json",...ve({body:o,secret:n})},method:"PUT",signal:AbortSignal.timeout(Al)});if(!i.ok){let l=await i.text().catch(()=>"");return{kind:"transport-error",reason:`status ${String(i.status)}: ${l.slice(0,200)}`}}let s=await i.json(),a=$l.safeParse(s);return a.success?!a.data.success||a.data.outcome==null?{kind:"transport-error",reason:a.data.error??"engine reported failure without outcome"}:a.data.outcome:{kind:"transport-error",reason:`invalid response shape: ${a.error.message}`}}catch(i){let s=i instanceof Error?i.message:String(i);return f.error("observer request failed: %s",s),{kind:"transport-error",reason:s}}}function Dl({backoff:e,pollCount:t}){let r=Math.min(t-1,e.length-1);return e[r]??1e3}function to(e){return`${String(Math.round(performance.now()-e))}ms`}async function Ll(e){return new Promise(t=>setTimeout(t,e))}var _l=1e4;async function Ar({failFast:e,getRecordingOffsetMs:t,observerTransport:r,onStep:n,page:o,spec:i,variables:s}){let a=performance.now();return Vl({currentId:i.entryNode,failFast:e,getRecordingOffsetMs:t,observerTransport:r,onStep:n,page:o,runStartTime:a,spec:i,variables:s})}function Ul({onStep:e,steps:t,target:r}){t.forEach(n=>{let o={...n,stepIndex:r.length};r.push(o),e?.(o)})}async function Vl({currentId:e,failFast:t,getRecordingOffsetMs:r,observerTransport:n,onStep:o,page:i,runStartTime:s,spec:a,variables:l}){let c=[],p=l,m=e;for(;m!=null;){let k=a.nodes[m];if(k==null)throw new Error(`Node "${m}" not found in spec`);let L=m,j=k.label??`${k.type} (${m})`;f.info("Executing node: %s [%s]",j,k.type);let y=await Fl({getRecordingOffsetMs:r,node:k,nodeId:L,observerTransport:n,page:i,runStartTime:s,variables:p});if(Ul({onStep:o,steps:[y],target:c}),p=y.variables,t&&y.status==="failed")break;m=k.next}return{steps:c,variables:p}}async function Fl({getRecordingOffsetMs:e,node:t,nodeId:r,observerTransport:n,page:o,runStartTime:i,variables:s}){let a=performance.now(),l=e?.(),c=t.label??`${t.type} (${t.id})`,p=_l;try{let m=await Hl({node:t,observerTransport:n,page:o,timeout:p,variables:s}),k=Math.round(performance.now()-a),L=m.assertions.some(y=>y.status==="failed"),j=e?.();return{assertions:m.assertions,detail:void 0,duration:k,nodeId:r,nodeType:t.type,recordingEndMs:j,recordingStartMs:l,snapshotTimestamp:Math.round(performance.now()-i),status:L?"failed":"passed",stepIndex:0,title:c,url:o.url(),variables:m.variables}}catch(m){let k=Math.round(performance.now()-a),L=m instanceof Error?m.message:String(m),j=e?.();return{assertions:[],detail:L,duration:k,nodeId:r,nodeType:t.type,recordingEndMs:j,recordingStartMs:l,snapshotTimestamp:Math.round(performance.now()-i),status:"failed",stepIndex:0,title:c,url:o.url(),variables:s}}}async function Hl({node:e,observerTransport:t,page:r,timeout:n,variables:o}){if(Bl(e)){let s=await ar({node:e,page:r,timeout:n,variables:o});return{assertions:[],variables:s.variables}}return{assertions:await Ml({node:e,observerTransport:t,page:r,timeout:n,variables:o}),variables:o}}function Bl(e){return["goto","click","dblclick","fill","select","hover","press","check","uncheck","clear","rightClick","handleDialog","clipboard","setPermission","setViewport","fail","setVariable","extractText","upload","drag","scrollIntoView","type","focus"].includes(e.type)}function Wl(e){return e.type.startsWith("assert")}async function Ml({node:e,observerTransport:t,page:r,timeout:n,variables:o}){if(!Wl(e))throw new Error(`Unknown node type: ${e.type}`);let i=e;switch(i.type){case"assertVisible":case"assertNotVisible":case"assertEnabled":case"assertDisabled":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertNotFocused":return zl({assertNode:i,page:r,timeout:n,variables:o});case"assertText":{let s=M({ref:i.expected,variables:o});return[await lr({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertUrl":{let s=M({ref:i.expected,variables:o});return[await cr({expected:s,operator:i.operator,page:r,timeout:n})]}case"assertTitle":{let s=M({ref:i.expected,variables:o});return[await Pn({expected:s,operator:i.operator,page:r,timeout:n})]}case"assertCount":{let s=sr({ref:i.expected,variables:o});return[await ur({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertValue":{let s=M({ref:i.expected,variables:o});return[await dr({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertAttribute":{let s=M({ref:i.expected,variables:o});return[await xn({attribute:i.attribute,expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertObserver":{if(t==null)return[{description:`observer "${i.observer}"`,detail:"observer transport not configured \u2014 set engineUrl + webhookSecret in ripplo.config",status:"failed"}];let s={};return Object.entries(i.params).forEach(([a,l])=>{s[a]=yt({ref:l,variables:o})}),[await eo({apiUrl:t.apiUrl,budget:i.budget,observer:i.observer,params:s,webhookSecret:t.webhookSecret})]}}}var ql={assertChecked:In,assertDisabled:En,assertEnabled:Cn,assertFocused:jn,assertNotChecked:An,assertNotFocused:Tn,assertNotVisible:mr,assertVisible:pr};async function zl({assertNode:e,page:t,timeout:r,variables:n}){let o=ql[e.type];return[await o({locator:O({locator:e.locator,page:t,variables:n}),timeout:r})]}var ro=1e4;async function jr({baseUrl:e,browser:t,cookies:r,extraVariables:n,observerTransport:o,onRecordingChunk:i,onStep:s,runId:a,spec:l}){let c=await t.newContext({baseURL:e});c.setDefaultTimeout(ro),c.setDefaultNavigationTimeout(ro),r.length>0&&await c.addCookies(r);let p=await c.newPage(),m=await Xn(p);async function k(){let W=await m.takeChunk();W!=null&&i?.(W)}let L=setInterval(()=>{k()},2e3),j=[],y=!1;function N(W){s?.(W),y||(y=!0,m.start()),j.push(Kn({context:c,page:p,runId:a,stepIndex:W.stepIndex,stepResult:W}))}let T=Gl(n,l.variableNamespaces),F={...ir({defs:l.variables}),...T},te=await Ar({failFast:!0,getRecordingOffsetMs:m.currentOffsetMs,observerTransport:o,onStep:N,page:p,spec:l,variables:F});clearInterval(L);let Ee=te.steps.filter(W=>W.status==="passed").length,oe=te.steps.filter(W=>W.status==="failed").length,qe={duration:te.steps.reduce((W,re)=>W+re.duration,0),failCount:oe,passCount:Ee,runId:a,status:oe>0?"failed":"passed",workflowName:""};return await Promise.all([Promise.allSettled(j).then(()=>Jn({runId:a,steps:te.steps,summary:qe})),m.detach().then(k)]),await c.close(),te.steps}function Gl(e,t){if(e==null||t==null)return{};let r=new Map;Object.entries(t).forEach(([o,i])=>{r.set(i,o)});let n={};return Object.entries(e).forEach(([o,i])=>{let s=r.get(o);s!=null&&(n[s]={...n[s],...i})}),n}async function ue({fn:e,label:t}){let r=performance.now(),n=await e();return f.info("%s: %dms",t,Math.round(performance.now()-r)),n}import{parseSetCookie as Kl}from"set-cookie-parser";import{z as J}from"zod";var no=3e4,Jl=J.object({data:J.record(J.string(),J.record(J.string(),J.string())),error:J.string().optional(),executed:J.array(J.string()),runId:J.string(),success:J.boolean()});async function Tr({project:e,webhookSecret:t,workflowSlug:r}){let n={apiUrl:void 0,cookies:[],data:{},executed:[]},o=e.workflows.find(a=>a.slug===r);if(o==null)return n;let i=e.engineBaseUrl.length===0?void 0:e.engineBaseUrl,s=o.preconditions;return s.length===0?{...n,apiUrl:i}:i==null?n:oo({apiUrl:i,preconditionNames:s,webhookSecret:t})}async function $r({apiUrl:e,cookies:t,data:r,executed:n,webhookSecret:o}){if(n.length!==0)try{let i=JSON.stringify({data:r,preconditions:n}),s=await fetch(`${e}/teardown-preconditions`,{body:i,headers:{"Content-Type":"application/json",...Zl({cookies:t}),...ve({body:i,secret:o})},method:"PUT",signal:AbortSignal.timeout(no)});s.ok||f.error("Teardown returned %s",String(s.status))}catch{f.error("Teardown request failed")}}async function oo({apiUrl:e,preconditionNames:t,webhookSecret:r}){let n=performance.now(),o=JSON.stringify({preconditions:t}),i=await fetch(`${e}/execute-preconditions`,{body:o,headers:{"Content-Type":"application/json",...ve({body:o,secret:r})},method:"PUT",signal:AbortSignal.timeout(no)});if(!i.ok){let c=await Yl(i);throw new Error(`execute-preconditions returned ${String(i.status)}: ${c}`)}let s=await i.json(),a=Jl.safeParse(s);if(!a.success)throw new Error(`execute-preconditions response has invalid shape: ${a.error.message}`);if(!a.data.success){let c=a.data.error??"no error detail";throw new Error(`Precondition batch failed: ${c}`)}let l=ec({domain:new URL(e).hostname,res:i});return f.info("Preconditions resolved: %s (%dms)",a.data.executed.join(", "),Math.round(performance.now()-n)),{apiUrl:e,cookies:l,data:a.data.data,executed:a.data.executed}}var Ql=J.object({error:J.string()});async function Yl(e){let t=await e.text().catch(()=>"");if(t.length===0)return"no body";let r=Xl(t),n=r==null?null:Ql.safeParse(r);return n!=null&&n.success?n.data.error:t.slice(0,500)}function Xl(e){try{return JSON.parse(e)}catch{return null}}function Zl({cookies:e}){return e.length===0?{}:{Cookie:e.map(r=>`${r.name}=${r.value}`).join("; ")}}function ec({domain:e,res:t}){let r=t.headers.getSetCookie();return Kl(r,{decodeValues:!1}).map(o=>tc({cookie:o,domain:e}))}function tc({cookie:e,domain:t}){let r={domain:e.domain??t,httpOnly:e.httpOnly??!1,name:e.name,path:e.path??"/",secure:e.secure??!1,value:e.value};return e.sameSite!=null&&(r.sameSite=nc({raw:e.sameSite})),e.expires!=null?r.expires=Math.floor(e.expires.getTime()/1e3):e.maxAge!=null&&(r.expires=Math.floor(Date.now()/1e3)+e.maxAge),r}var rc={lax:"Lax",none:"None",strict:"Strict"};function nc({raw:e}){return rc[e.toLowerCase()]??"Lax"}async function Nr({engineBaseUrl:e,preconditionNames:t,webhookSecret:r}){let n=e.length===0?void 0:e;return n==null||t.length===0?{apiUrl:n,cookies:[],data:{},executed:[]}:oo({apiUrl:n,preconditionNames:t,webhookSecret:r})}import{graphql as rt}from"gql.tada";async function Or({config:e,runId:t}){let r=await g({config:e,document:ac,variables:{platform:"chromium",runId:t}});if(r.startRun==null)throw new Error("Failed to start run");let n=r.startRun.id,o=[];function i(c){let p=ic({config:e,runResultId:n,steps:[c]}).catch(m=>{f.error(m,"Failed to submit step %d",c.stepIndex)});o.push(p)}function s(c){let p=oc({chunk:c,config:e,runResultId:n}).catch(m=>{f.error(m,"Failed to submit recording chunk %d",c.chunkIndex)});o.push(p)}async function a(){await Promise.all(o)}async function l({statusOverride:c,steps:p,summary:m}){await ue({label:`flushSteps (${String(o.length)} steps)`,fn:()=>Promise.all(o)});let k=p.filter(N=>N.status==="passed").length,L=p.filter(N=>N.status==="failed").length,j=p.reduce((N,T)=>N+T.duration,0),y=c??(L>0?"failed":"passed");await ue({label:"completeRun",fn:()=>g({config:e,document:uc,variables:{duration:j,failCount:L,passCount:k,runResultId:n,status:y,summary:m??null,warnCount:0}})})}return{complete:l,enqueueRecordingChunk:s,enqueueStep:i,flushSteps:a,runResultId:n}}async function oc({chunk:e,config:t,runResultId:r}){await g({config:t,document:cc,variables:{input:{bodyBase64:e.body.toString("base64"),chunkIndex:e.chunkIndex,endTimestamp:e.endTimestamp,eventCount:e.eventCount,runResultId:r,startTimestamp:e.startTimestamp}}})}async function ic({config:e,runResultId:t,steps:r}){let n=r.map(o=>({assertions:o.assertions.map(i=>({description:i.description,detail:i.detail,status:i.status})),detail:o.detail,duration:o.duration,nodeType:o.nodeType,recordingEndMs:o.recordingEndMs??null,recordingStartMs:o.recordingStartMs??null,snapshotTimestamp:o.snapshotTimestamp,status:o.status,stepIndex:o.stepIndex,title:o.title,url:o.url}));await g({config:e,document:lc,variables:{runResultId:t,steps:n}})}async function Dr({config:e,reason:t,runId:r}){await g({config:e,document:sc,variables:{reason:t,runId:r}})}var sc=rt(`
|
|
43
|
+
`);await Promise.all([he(ne.join(o,"error.txt"),i),he(ne.join(o,"summary.txt"),s)]),y.info("Debug error artifacts written to .ripplo/debug/%s/",t)}catch(o){let i=o instanceof Error?o.message:String(o);y.warn("Failed to write error debug artifacts: %s",i)}}import{readFileSync as yl}from"fs";import{createRequire as hl}from"module";import Gn from"path";import{gzipSync as wl}from"zlib";var bl=hl(import.meta.url),vl=bl.resolve("rrweb"),kl=Gn.join(Gn.dirname(vl),"rrweb.umd.min.cjs"),Sl=yl(kl,"utf8"),Rl=[";(function() {"," try {"," if (window.__ripploRrwebLoaded) return;"," window.__ripploRrwebLoaded = true;"," window.__ripploRrwebBuffer = [];"," window.__ripploRrwebDrain = function() {"," var out = window.__ripploRrwebBuffer;"," window.__ripploRrwebBuffer = [];"," return out;"," };"," var r = window.rrweb;"," if (r == null || r.record == null) {"," console.warn('[ripplo] rrweb global not found');"," return;"," }"," function startRecording() {"," if (window.__ripploRrwebStartedAt != null) return;"," window.__ripploRrwebStartedAt = Date.now();"," var stop = r.record({"," emit: function(event) { window.__ripploRrwebBuffer.push(event); },"," recordCanvas: false,"," recordCrossOriginIframes: false,"," });"," window.__ripploRrwebStop = stop;"," }"," window.__ripploRrwebStart = startRecording;"," // Auto-start on every bootstrap run so agent-driven tests (which don't"," // emit a 'first step' signal before navigating) capture from the start."," startRecording();"," } catch (err) {"," console.error('[ripplo] rrweb bootstrap failed', err && err.message ? err.message : err);"," }","})();"].join(`
|
|
44
|
+
`),Pl=Sl+`
|
|
45
|
+
`+Rl;async function Kn(e){let t,r=0;async function n(){await e.addScriptTag({content:Pl}).catch(()=>{})}await n(),e.on("load",()=>{n()});async function o(){await z({awaitPromise:!1,expression:"globalThis.__ripploRrwebStart && globalThis.__ripploRrwebStart()",page:e});let l=await z({awaitPromise:!1,expression:"globalThis.__ripploRrwebStartedAt",page:e});typeof l=="number"&&(t??=l)}function i(){if(t!=null)return Date.now()-t}async function s(){let l=await z({awaitPromise:!1,expression:"globalThis.__ripploRrwebDrain ? globalThis.__ripploRrwebDrain() : []",page:e})??[],c=l[0],p=l.at(-1);if(c==null||p==null)return;t??=c.timestamp;let m=t,k=l.map(f=>JSON.stringify(f)).join(`
|
|
46
|
+
`),N={body:wl(Buffer.from(k,"utf8")),chunkIndex:r,endTimestamp:p.timestamp-m,eventCount:l.length,startTimestamp:c.timestamp-m};return r+=1,N}async function a(){await z({awaitPromise:!1,expression:"globalThis.__ripploRrwebStop && globalThis.__ripploRrwebStop()",page:e})}return{currentOffsetMs:i,detach:a,start:o,takeChunk:s}}import{z as ee}from"zod";import Jn from"crypto";import{Webhook as xl}from"standardwebhooks";function we({body:e,secret:t}){let r=new xl(t),n=`msg_${Jn.randomUUID()}`,o=new Date,i=r.sign(n,o,e),s=Math.floor(o.getTime()/1e3);return{"webhook-id":n,"webhook-signature":i,"webhook-timestamp":String(s)}}function Ar(){return`whsec_${Jn.randomBytes(24).toString("base64")}`}var Cl=15e4,El={async:{backoffMs:[500,1e3,2e3,5e3],timeoutMs:12e4},fast:{backoffMs:[100,250,500,1e3],timeoutMs:5e3},slow:{backoffMs:[250,500,1e3,2e3],timeoutMs:3e4}},Al=ee.discriminatedUnion("kind",[ee.object({kind:ee.literal("pass")}),ee.object({kind:ee.literal("retry"),reason:ee.string()}),ee.object({kind:ee.literal("fail"),reason:ee.string()})]),Il=ee.object({error:ee.string().optional(),outcome:Al.optional(),success:ee.boolean()});async function Qn({apiUrl:e,budget:t,observer:r,params:n,webhookSecret:o}){let i=El[t],s=performance.now(),a={lastReason:void 0,pollCount:0};for(;performance.now()-s<i.timeoutMs;){a.pollCount+=1;let l=await $l({apiUrl:e,observer:r,params:n,webhookSecret:o}),c=Tl({observer:r,outcome:l,start:s,state:a});if(c!=null)return c;let p=i.timeoutMs-(performance.now()-s);if(p<=0)break;let m=Math.min(jl({backoff:i.backoffMs,pollCount:a.pollCount}),p);await Nl(m)}return{description:`observer "${r}"`,detail:`budget "${t}" exhausted after ${String(a.pollCount)} poll(s) (${Yn(s)}); last: ${a.lastReason??"no retry reason"}
|
|
47
|
+
\u2192 /ripplo:debug \u2014 check .ripplo/debug/<runId>/ artifacts`,status:"failed"}}function Tl({observer:e,outcome:t,start:r,state:n}){return t.kind==="transport-error"?{description:`observer "${e}"`,detail:`transport error after ${String(n.pollCount)} poll(s): ${t.reason}
|
|
48
|
+
Load \`/ripplo:setup\` skill for instructions on engine endpoint + webhook secret wiring.`,status:"failed"}:t.kind==="pass"?{description:`observer "${e}"`,detail:n.pollCount>1?`passed after ${String(n.pollCount)} polls (${Yn(r)})`:void 0,status:"passed"}:t.kind==="fail"?{description:`observer "${e}"`,detail:`failed (invariant): ${t.reason} (after ${String(n.pollCount)} poll(s)) \u2014 ctx.fail stops polling immediately and is reserved for invariant violations (wrong shape, contradictory/forbidden state). For transient conditions like "not yet committed", "status not yet X", or "row not found", use ctx.retry(reason) so the runtime keeps polling until the budget expires.
|
|
49
|
+
Load \`/ripplo:create\` skill for instructions.`,status:"failed"}:(n.lastReason=t.reason,null)}async function $l({apiUrl:e,observer:t,params:r,webhookSecret:n}){let o=JSON.stringify({observer:t,params:r});try{let i=await fetch(`${e}/execute-observer`,{body:o,headers:{"Content-Type":"application/json",...we({body:o,secret:n})},method:"PUT",signal:AbortSignal.timeout(Cl)});if(!i.ok){let l=await i.text().catch(()=>"");return{kind:"transport-error",reason:`status ${String(i.status)}: ${l.slice(0,200)}`}}let s=await i.json(),a=Il.safeParse(s);return a.success?!a.data.success||a.data.outcome==null?{kind:"transport-error",reason:a.data.error??"engine reported failure without outcome"}:a.data.outcome:{kind:"transport-error",reason:`invalid response shape: ${a.error.message}`}}catch(i){let s=i instanceof Error?i.message:String(i);return y.error("observer request failed: %s",s),{kind:"transport-error",reason:s}}}function jl({backoff:e,pollCount:t}){let r=Math.min(t-1,e.length-1);return e[r]??1e3}function Yn(e){return`${String(Math.round(performance.now()-e))}ms`}async function Nl(e){return new Promise(t=>setTimeout(t,e))}var Ol=1e4;async function Ir({failFast:e,getRecordingOffsetMs:t,observerTransport:r,onStep:n,page:o,spec:i,variables:s}){let a=performance.now();return Ll({currentId:i.entryNode,failFast:e,getRecordingOffsetMs:t,observerTransport:r,onStep:n,page:o,runStartTime:a,spec:i,variables:s})}function Dl({onStep:e,steps:t,target:r}){t.forEach(n=>{let o={...n,stepIndex:r.length};r.push(o),e?.(o)})}async function Ll({currentId:e,failFast:t,getRecordingOffsetMs:r,observerTransport:n,onStep:o,page:i,runStartTime:s,spec:a,variables:l}){let c=[],p=l,m=e;for(;m!=null;){let k=a.nodes[m];if(k==null)throw new Error(`Node "${m}" not found in spec`);let j=m,N=k.label??`${k.type} (${m})`;y.info("Executing node: %s [%s]",N,k.type);let f=await _l({getRecordingOffsetMs:r,node:k,nodeId:j,observerTransport:n,page:i,runStartTime:s,variables:p});if(Dl({onStep:o,steps:[f],target:c}),p=f.variables,t&&f.status==="failed")break;m=k.next}return{steps:c,variables:p}}async function _l({getRecordingOffsetMs:e,node:t,nodeId:r,observerTransport:n,page:o,runStartTime:i,variables:s}){let a=performance.now(),l=e?.(),c=t.label??`${t.type} (${t.id})`,p=Ol;try{let m=await Ul({node:t,observerTransport:n,page:o,timeout:p,variables:s}),k=Math.round(performance.now()-a),j=m.assertions.some(f=>f.status==="failed"),N=e?.();return{assertions:m.assertions,detail:void 0,duration:k,nodeId:r,nodeType:t.type,recordingEndMs:N,recordingStartMs:l,snapshotTimestamp:Math.round(performance.now()-i),status:j?"failed":"passed",stepIndex:0,title:c,url:o.url(),variables:m.variables}}catch(m){let k=Math.round(performance.now()-a),j=m instanceof Error?m.message:String(m),N=e?.();return{assertions:[],detail:j,duration:k,nodeId:r,nodeType:t.type,recordingEndMs:N,recordingStartMs:l,snapshotTimestamp:Math.round(performance.now()-i),status:"failed",stepIndex:0,title:c,url:o.url(),variables:s}}}async function Ul({node:e,observerTransport:t,page:r,timeout:n,variables:o}){if(Vl(e)){let s=await ar({node:e,page:r,timeout:n,variables:o});return{assertions:[],variables:s.variables}}return{assertions:await Hl({node:e,observerTransport:t,page:r,timeout:n,variables:o}),variables:o}}function Vl(e){return["goto","click","dblclick","fill","select","hover","press","check","uncheck","clear","rightClick","handleDialog","clipboard","setPermission","setViewport","fail","setVariable","extractText","upload","drag","scrollIntoView","type","focus"].includes(e.type)}function Fl(e){return e.type.startsWith("assert")}async function Hl({node:e,observerTransport:t,page:r,timeout:n,variables:o}){if(!Fl(e))throw new Error(`Unknown node type: ${e.type}`);let i=e;switch(i.type){case"assertVisible":case"assertNotVisible":case"assertEnabled":case"assertDisabled":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertNotFocused":return Bl({assertNode:i,page:r,timeout:n,variables:o});case"assertText":{let s=M({ref:i.expected,variables:o});return[await lr({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertUrl":{let s=M({ref:i.expected,variables:o});return[await cr({expected:s,operator:i.operator,page:r,timeout:n})]}case"assertTitle":{let s=M({ref:i.expected,variables:o});return[await vn({expected:s,operator:i.operator,page:r,timeout:n})]}case"assertCount":{let s=sr({ref:i.expected,variables:o});return[await dr({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertValue":{let s=M({ref:i.expected,variables:o});return[await ur({expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertAttribute":{let s=M({ref:i.expected,variables:o});return[await kn({attribute:i.attribute,expected:s,locator:O({locator:i.locator,page:r,variables:o}),operator:i.operator,timeout:n})]}case"assertObserver":{if(t==null)return[{description:`observer "${i.observer}"`,detail:"observer transport not configured \u2014 set engineUrl + webhookSecret in ripplo.config",status:"failed"}];let s={};return Object.entries(i.params).forEach(([a,l])=>{s[a]=yt({ref:l,variables:o})}),[await Qn({apiUrl:t.apiUrl,budget:i.budget,observer:i.observer,params:s,webhookSecret:t.webhookSecret})]}}}var Wl={assertChecked:Pn,assertDisabled:Rn,assertEnabled:Sn,assertFocused:Cn,assertNotChecked:xn,assertNotFocused:En,assertNotVisible:mr,assertVisible:pr};async function Bl({assertNode:e,page:t,timeout:r,variables:n}){let o=Wl[e.type];return[await o({locator:O({locator:e.locator,page:t,variables:n}),timeout:r})]}var Xn=1e4;async function Tr({baseUrl:e,browser:t,cookies:r,extraVariables:n,observerTransport:o,onRecordingChunk:i,onStep:s,runId:a,spec:l}){let c=await t.newContext({baseURL:e});c.setDefaultTimeout(Xn),c.setDefaultNavigationTimeout(Xn),r.length>0&&await c.addCookies(r);let p=await c.newPage(),m=await Kn(p);async function k(){let B=await m.takeChunk();B!=null&&i?.(B)}let j=setInterval(()=>{k()},2e3),N=[],f=!1;function W(B){s?.(B),f||(f=!0,m.start()),N.push(Mn({context:c,page:p,runId:a,stepIndex:B.stepIndex,stepResult:B}))}let $=Ml(n,l.variableNamespaces),V={...ir({defs:l.variables}),...$},te=await Ir({failFast:!0,getRecordingOffsetMs:m.currentOffsetMs,observerTransport:o,onStep:W,page:p,spec:l,variables:V});clearInterval(j);let Ce=te.steps.filter(B=>B.status==="passed").length,oe=te.steps.filter(B=>B.status==="failed").length,qe={duration:te.steps.reduce((B,re)=>B+re.duration,0),failCount:oe,passCount:Ce,runId:a,status:oe>0?"failed":"passed",workflowName:""};return await Promise.all([Promise.allSettled(N).then(()=>qn({runId:a,steps:te.steps,summary:qe})),m.detach().then(k)]),await c.close(),te.steps}function Ml(e,t){if(e==null||t==null)return{};let r=new Map;Object.entries(t).forEach(([o,i])=>{r.set(i,o)});let n={};return Object.entries(e).forEach(([o,i])=>{let s=r.get(o);s!=null&&(n[s]={...n[s],...i})}),n}async function ce({fn:e,label:t}){let r=performance.now(),n=await e();return y.info("%s: %dms",t,Math.round(performance.now()-r)),n}import{parseSetCookie as ql}from"set-cookie-parser";import{z as J}from"zod";var Zn=3e4,zl=J.object({data:J.record(J.string(),J.record(J.string(),J.string())),error:J.string().optional(),executed:J.array(J.string()),runId:J.string(),success:J.boolean()});async function $r({apiUrl:e,cookies:t,data:r,executed:n,webhookSecret:o}){if(n.length!==0)try{let i=JSON.stringify({data:r,preconditions:n}),s=await fetch(`${e}/teardown-preconditions`,{body:i,headers:{"Content-Type":"application/json",...Yl({cookies:t}),...we({body:i,secret:o})},method:"PUT",signal:AbortSignal.timeout(Zn)});s.ok||y.error("Teardown returned %s",String(s.status))}catch{y.error("Teardown request failed")}}async function Gl({apiUrl:e,preconditionNames:t,webhookSecret:r}){let n=performance.now(),o=JSON.stringify({preconditions:t}),i=await fetch(`${e}/execute-preconditions`,{body:o,headers:{"Content-Type":"application/json",...we({body:o,secret:r})},method:"PUT",signal:AbortSignal.timeout(Zn)});if(!i.ok){let c=await Jl(i);throw new Error(`execute-preconditions returned ${String(i.status)}: ${c}`)}let s=await i.json(),a=zl.safeParse(s);if(!a.success)throw new Error(`execute-preconditions response has invalid shape: ${a.error.message}`);if(!a.data.success){let c=a.data.error??"no error detail";throw new Error(`Precondition batch failed: ${c}`)}let l=Xl({domain:new URL(e).hostname,res:i});return y.info("Preconditions resolved: %s (%dms)",a.data.executed.join(", "),Math.round(performance.now()-n)),{apiUrl:e,cookies:l,data:a.data.data,executed:a.data.executed}}var Kl=J.object({error:J.string()});async function Jl(e){let t=await e.text().catch(()=>"");if(t.length===0)return"no body";let r=Ql(t),n=r==null?null:Kl.safeParse(r);return n!=null&&n.success?n.data.error:t.slice(0,500)}function Ql(e){try{return JSON.parse(e)}catch{return null}}function Yl({cookies:e}){return e.length===0?{}:{Cookie:e.map(r=>`${r.name}=${r.value}`).join("; ")}}function Xl({domain:e,res:t}){let r=t.headers.getSetCookie();return ql(r,{decodeValues:!1}).map(o=>Zl({cookie:o,domain:e}))}function Zl({cookie:e,domain:t}){let r={domain:e.domain??t,httpOnly:e.httpOnly??!1,name:e.name,path:e.path??"/",secure:e.secure??!1,value:e.value};return e.sameSite!=null&&(r.sameSite=tc({raw:e.sameSite})),e.expires!=null?r.expires=Math.floor(e.expires.getTime()/1e3):e.maxAge!=null&&(r.expires=Math.floor(Date.now()/1e3)+e.maxAge),r}var ec={lax:"Lax",none:"None",strict:"Strict"};function tc({raw:e}){return ec[e.toLowerCase()]??"Lax"}async function At({engineBaseUrl:e,preconditionNames:t,webhookSecret:r}){let n=e.length===0?void 0:e;return n==null||t.length===0?{apiUrl:n,cookies:[],data:{},executed:[]}:Gl({apiUrl:n,preconditionNames:t,webhookSecret:r})}import{graphql as Ne}from"gql.tada";async function jr({config:e,runId:t}){if((await g({config:e,document:ic,variables:{runId:t}})).startRun==null)throw new Error("Failed to start run");let n=[];function o(l){let c=nc({config:e,runId:t,steps:[l]}).catch(p=>{y.error(p,"Failed to submit step %d",l.stepIndex)});n.push(c)}function i(l){let c=rc({chunk:l,config:e,runId:t}).catch(p=>{y.error(p,"Failed to submit recording chunk %d",l.chunkIndex)});n.push(c)}async function s(){await Promise.all(n)}async function a({statusOverride:l,steps:c,summary:p}){await ce({label:`flushSteps (${String(n.length)} steps)`,fn:()=>Promise.all(n)});let m=c.filter(f=>f.status==="passed").length,k=c.filter(f=>f.status==="failed").length,j=c.reduce((f,W)=>f+W.duration,0),N=l??(k>0?"failed":"passed");await ce({label:"completeRun",fn:()=>g({config:e,document:lc,variables:{duration:j,failCount:k,passCount:m,runId:t,status:N,summary:p??null,warnCount:0}})})}return{complete:a,enqueueRecordingChunk:i,enqueueStep:o,flushSteps:s,runId:t}}async function rc({chunk:e,config:t,runId:r}){await g({config:t,document:ac,variables:{input:{bodyBase64:e.body.toString("base64"),chunkIndex:e.chunkIndex,endTimestamp:e.endTimestamp,eventCount:e.eventCount,runId:r,startTimestamp:e.startTimestamp}}})}async function nc({config:e,runId:t,steps:r}){let n=r.map(o=>({assertions:o.assertions.map(i=>({description:i.description,detail:i.detail,status:i.status})),detail:o.detail,duration:o.duration,nodeType:o.nodeType,recordingEndMs:o.recordingEndMs??null,recordingStartMs:o.recordingStartMs??null,snapshotTimestamp:o.snapshotTimestamp,status:o.status,stepIndex:o.stepIndex,title:o.title,url:o.url}));await g({config:e,document:sc,variables:{runId:t,steps:n}})}async function Nr({config:e,reason:t,runId:r}){await g({config:e,document:oc,variables:{reason:t,runId:r}})}var Kw=Ne(`
|
|
50
|
+
mutation MarkRunArtifactsUploadedCLI($runId: String!) {
|
|
51
|
+
markRunArtifactsUploaded(runId: $runId)
|
|
52
|
+
}
|
|
53
|
+
`),oc=Ne(`
|
|
50
54
|
mutation FailRunCLI($runId: String!, $reason: String!) {
|
|
51
55
|
failRun(runId: $runId, reason: $reason)
|
|
52
56
|
}
|
|
53
|
-
`),
|
|
54
|
-
mutation StartRunCLI($runId: String
|
|
55
|
-
startRun(runId: $runId
|
|
57
|
+
`),ic=Ne(`
|
|
58
|
+
mutation StartRunCLI($runId: String!) {
|
|
59
|
+
startRun(runId: $runId) {
|
|
56
60
|
id
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
|
-
`),
|
|
60
|
-
mutation SubmitRunStepsCLI($
|
|
61
|
-
submitRunSteps(
|
|
63
|
+
`),sc=Ne(`
|
|
64
|
+
mutation SubmitRunStepsCLI($runId: String!, $steps: [StepInput!]!) {
|
|
65
|
+
submitRunSteps(runId: $runId, steps: $steps)
|
|
62
66
|
}
|
|
63
|
-
`),
|
|
67
|
+
`),ac=Ne(`
|
|
64
68
|
mutation SubmitRunRecordingChunkCLI($input: SubmitRunRecordingChunkInput!) {
|
|
65
69
|
submitRunRecordingChunk(input: $input)
|
|
66
70
|
}
|
|
67
|
-
`),
|
|
71
|
+
`),lc=Ne(`
|
|
68
72
|
mutation CompleteRunCLI(
|
|
69
|
-
$
|
|
70
|
-
$status:
|
|
73
|
+
$runId: String!
|
|
74
|
+
$status: RunStatus!
|
|
71
75
|
$duration: Int!
|
|
72
76
|
$passCount: Int!
|
|
73
77
|
$failCount: Int!
|
|
@@ -75,7 +79,7 @@ Load \`/ripplo:create\` skill for instructions.`,status:"failed"}:(n.lastReason=
|
|
|
75
79
|
$summary: String
|
|
76
80
|
) {
|
|
77
81
|
completeRun(
|
|
78
|
-
|
|
82
|
+
runId: $runId
|
|
79
83
|
status: $status
|
|
80
84
|
duration: $duration
|
|
81
85
|
passCount: $passCount
|
|
@@ -87,7 +91,7 @@ Load \`/ripplo:create\` skill for instructions.`,status:"failed"}:(n.lastReason=
|
|
|
87
91
|
status
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
|
-
`);var
|
|
94
|
+
`);var uc=12e4,to=eo(`
|
|
91
95
|
fragment WorkflowRun on Workflow {
|
|
92
96
|
id
|
|
93
97
|
slug
|
|
@@ -99,45 +103,69 @@ Load \`/ripplo:create\` skill for instructions.`,status:"failed"}:(n.lastReason=
|
|
|
99
103
|
preconditionName
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
|
-
`),
|
|
103
|
-
|
|
104
|
-
id
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
`),pc=eo(`
|
|
107
|
+
query RunWorkflowSpec($runId: String!) {
|
|
108
|
+
run(id: $runId) {
|
|
109
|
+
id
|
|
110
|
+
workflow {
|
|
111
|
+
...WorkflowRun
|
|
112
|
+
project {
|
|
113
|
+
id
|
|
114
|
+
engineBaseUrl
|
|
115
|
+
}
|
|
116
|
+
}
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
|
-
`,[At]),pc=
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
`,[to]);async function Or({baseUrl:e,browser:t,config:r,headed:n,preconditionNames:o,runId:i,webhookSecret:s}){let a="unknown",l,c,p=!1;try{p=t==null,c=mc({externalBrowser:t,headed:n});let m=o==null?void 0:At({engineBaseUrl:r.engineUrl,preconditionNames:o,webhookSecret:s}),[{engineBaseUrl:k,workflow:j},N]=await Promise.all([ce({label:"Run context resolved",fn:()=>wc({config:r,runId:i})}),ce({label:"Streaming run started",fn:()=>jr({config:r,runId:i})}),yr({appUrl:e})]);a=j.name,l=N;let f=bc(j.spec);y.info("Executing workflow: %s",j.name),await Sc(gc({baseUrl:e,browserPromise:c,engineBaseUrl:k,preconditionPromise:m,runId:i,specData:f,streaming:l,webhookSecret:s,workflow:j}),kc())}catch(m){throw await fc({browserPromise:c,config:r,error:m,ownsBrowser:p,runId:i,streaming:l,workflowName:a}),m}}function mc({externalBrowser:e,headed:t}){return e!=null?Promise.resolve(e):ce({label:"Browser launched",fn:()=>Ye({headed:t})})}async function fc({browserPromise:e,config:t,error:r,ownsBrowser:n,runId:o,streaming:i,workflowName:s}){let a=r instanceof Error?r.message:String(r),l=r instanceof Error?r.stack:void 0;await zn({error:a,runId:o,stack:l,workflowName:s}),i==null?await Nr({config:t,reason:a,runId:o}).catch(()=>{}):await i.complete({statusOverride:"failed",steps:[],summary:a}).catch(()=>{}),n&&e!=null&&await(await e.catch(()=>{}))?.close()}async function gc({baseUrl:e,browserPromise:t,engineBaseUrl:r,preconditionPromise:n,runId:o,specData:i,streaming:s,webhookSecret:a,workflow:l}){let c=n??At({engineBaseUrl:r,preconditionNames:l.preconditions,webhookSecret:a}),[p,m]=await Promise.all([c,t]);await yc({baseUrl:e,browser:m,preconditionResult:p,runId:o,specData:i,streaming:s,webhookSecret:a})}async function yc({baseUrl:e,browser:t,preconditionResult:r,runId:n,specData:o,streaming:i,webhookSecret:s}){let a=await ce({label:"Spec executed",fn:()=>Tr({baseUrl:e,browser:t,cookies:r.cookies,extraVariables:r.data,observerTransport:r.apiUrl==null?void 0:{apiUrl:r.apiUrl,webhookSecret:s},onRecordingChunk:i.enqueueRecordingChunk,onStep:i.enqueueStep,runId:n,spec:o})});await hc({preconditionResult:r,webhookSecret:s}),await i.complete({statusOverride:void 0,steps:a,summary:void 0});let l=a.filter(p=>p.status==="passed").length,c=a.filter(p=>p.status==="failed").length;y.info("Run complete: %d passed, %d failed",l,c)}async function hc({preconditionResult:e,webhookSecret:t}){e.apiUrl!=null&&await $r({apiUrl:e.apiUrl,cookies:e.cookies,data:e.data,executed:e.executed,webhookSecret:t})}async function wc({config:e,runId:t}){let r=await g({config:e,document:pc,variables:{runId:t}});if(r.run?.workflow==null)throw new Error(`Run ${t} not found`);let n=dc(to,r.run.workflow),o=r.run.workflow.project?.engineBaseUrl??"";return{engineBaseUrl:e.engineUrl.length>0?e.engineUrl:o,workflow:n}}function bc(e){if(e==null)throw new Error("Workflow has no spec");return xr.decode(e)}var vc=cc.coerce.number().int().positive().catch(uc);function kc(){return vc.parse(process.env.RIPPLO_RUN_TIMEOUT_MS)}async function Sc(e,t){let r,n=new Promise((o,i)=>{r=setTimeout(()=>{i(new Error(`Run timed out after ${String(t)}ms`))},t)});try{await Promise.race([e,n])}finally{r!=null&&clearTimeout(r)}}import{graphql as It}from"gql.tada";var ub=It(`
|
|
120
|
+
query AgentReviewContextWorker($reviewId: String!) {
|
|
121
|
+
agentReview(id: $reviewId) {
|
|
122
|
+
id
|
|
123
|
+
runId
|
|
124
|
+
agentProfile {
|
|
125
|
+
id
|
|
126
|
+
name
|
|
127
|
+
description
|
|
128
|
+
output
|
|
129
|
+
successCriteria
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
|
-
|
|
132
|
+
}
|
|
133
|
+
`);var pb=It(`
|
|
134
|
+
mutation StartAgentReviewWorker($reviewId: String!) {
|
|
135
|
+
startAgentReview(reviewId: $reviewId)
|
|
136
|
+
}
|
|
137
|
+
`);var mb=It(`
|
|
138
|
+
mutation CompleteAgentReviewWorker($reviewId: String!, $markdown: String!) {
|
|
139
|
+
completeAgentReview(reviewId: $reviewId, markdown: $markdown)
|
|
140
|
+
}
|
|
141
|
+
`);var fb=It(`
|
|
142
|
+
mutation FailAgentReviewWorker($reviewId: String!, $reason: String!) {
|
|
143
|
+
failAgentReview(reviewId: $reviewId, reason: $reason)
|
|
144
|
+
}
|
|
145
|
+
`);import{exec as Ec}from"child_process";import{createAuthClient as Rc}from"better-auth/client";import{deviceAuthorizationClient as Pc}from"better-auth/client/plugins";function ro({baseURL:e}){return Rc({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[Pc()]})}import{z as Dr}from"zod";var xc="https://ripplo.ai";function Q(){return Tt().RIPPLO_SERVER_URL}function oo(){return Tt().RIPPLO_PROJECT_ID}var Cc=Dr.object({RIPPLO_PROJECT_ID:Dr.string().min(1).optional(),RIPPLO_SERVER_URL:Dr.string().min(1).default(xc)}),no;function Tt(){return no??=Cc.parse(process.env),no}var Ac=5e3,io="ripplo-cli";async function so({onDeviceCode:e,url:t}){let r=t??Tt().RIPPLO_SERVER_URL,n=ro({baseURL:r}),o=await n.device.code({client_id:io});if(o.error!=null)throw new Error(`Failed to request device code: ${o.error.error_description}`);let{device_code:i,user_code:s,verification_uri_complete:a}=o.data;e({userCode:s,verificationUrl:a}),Oc(a);let l=await Ic({authClient:n,deviceCode:i});return vr(l),l}async function Ic({authClient:e,deviceCode:t}){for(;;){await jc(Ac);let r=await e.device.token({client_id:io,device_code:t,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(r.data?.access_token!=null)return r.data.access_token;if(r.error==null)continue;if(!$c(r.error.error))throw new Error(`Authorization failed: ${r.error.error_description}`)}}var Tc=new Set(["authorization_pending","slow_down"]);function $c(e){return Tc.has(e)}function jc(e){return new Promise(t=>{setTimeout(t,e)})}function Nc(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function Oc(e){let t=Nc();Ec(`${t} "${e}"`,()=>{})}function be({serverUrl:e,token:t}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:t,webhookSecret:""}}import Dc from"fs";import Lc from"path";function x(e){return Dc.existsSync(Lc.join(e,".ripplo"))}var Uc=_c(`
|
|
118
146
|
query AuthViewer {
|
|
119
147
|
currentUser {
|
|
120
148
|
name
|
|
121
149
|
email
|
|
122
150
|
}
|
|
123
151
|
}
|
|
124
|
-
`);async function
|
|
152
|
+
`);async function ao(){let e=Q(),t=Z();if(t!=null){let s=await Lr({serverUrl:e,token:t});if(s!=null){process.stdout.write(`Already signed in as ${s.email}. Run \`ripplo auth logout\` to switch.
|
|
125
153
|
`);return}process.stdout.write(`Your saved session expired \u2014 let's sign you back in.
|
|
126
154
|
|
|
127
|
-
`)}let r=await
|
|
155
|
+
`)}let r=await so({url:e,onDeviceCode:s=>{process.stdout.write(`Opening your browser to finish sign-in.
|
|
128
156
|
`),process.stdout.write(`If it didn't open, visit: ${s.verificationUrl}
|
|
129
157
|
`),process.stdout.write(`Verification code: ${s.userCode}
|
|
130
158
|
|
|
131
159
|
`),process.stdout.write(`Waiting for you to approve...
|
|
132
|
-
`)}}),n=await
|
|
160
|
+
`)}}),n=await Lr({serverUrl:e,token:r}),o=x(process.cwd()),i=o?"Welcome back":"Welcome to Ripplo";process.stdout.write(`
|
|
133
161
|
`),process.stdout.write(n==null?`${i}.
|
|
134
|
-
`:`${i}, ${
|
|
135
|
-
`),process.stdout.write(`Session saved to ${
|
|
136
|
-
`),o||(process.stdout.write("\nNext: run `/ripplo:setup` in Claude Code to wire Ripplo into this project,\n"),process.stdout.write("or `npx ripplo init` to set it up by hand.\n"))}function
|
|
137
|
-
`)}function
|
|
138
|
-
`);return}process.stdout.write(`Removed ${
|
|
139
|
-
`)}import
|
|
140
|
-
`}async function Nt({cwd:e}){let t=Br.join(e,se),r=await Yu(t);return r==null?null:qc(So,r)}async function ae({cwd:e,result:t}){let r=Ro(t),n=Br.join(e,se);await Hr.mkdir(Br.dirname(n),{recursive:!0}),await Hr.writeFile(n,Mr(r),"utf8")}function Ot({compiled:e,existing:t}){if(t==null)return"missing";let r=Mr(Ro(e)),n=Mr(t);return r===n?"match":"stale"}async function Yu(e){try{return await Hr.readFile(e,"utf8")}catch(t){if(Xu(t)&&t.code==="ENOENT")return null;throw t}}function Xu(e){return e instanceof Error&&"code"in e}function Zu(e){return function(r,n){return n===e||!ed(n)?n:Object.fromEntries(Object.entries(n).toSorted(([o],[i])=>o.localeCompare(i)))}}function ed(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}import ot from"fs";import _t from"path";var zr=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],Re=["**/*.gen.*","**/generated/**","**/*.d.ts","**/*.test.*","**/*.spec.*","**/node_modules/**","**/dist/**","**/build/**",".ripplo/**","**/*.md","**/.next/**","**/.turbo/**","**/.vercel/**","**/.svelte-kit/**","**/.nuxt/**","**/.astro/**","**/coverage/**","**/storybook-static/**","**/*.stories.*","**/*.story.*","**/cli/**","**/scripts/**","**/tools/**","**/__tests__/**","**/__mocks__/**","**/__fixtures__/**","**/*.config.*","**/*.setup.*","**/public/**","**/static/**","**/assets/**","**/migrations/**","**/prisma/migrations/**","**/scripts/**"];function Po(e){return e.data}function xo(e){return{as(t){return{data:{label:t,node:e}}}}}function Dt(e){let t=e.getPreconditions(),r=e.getObservers(),n=e.getTests();td(n);let o={};t.forEach(a=>{o[a.name]={depends:[...a.dependsOn],description:a.description,returns:[...a.returns]}});let i={};r.forEach(a=>{i[a.name]={budget:a.budget,description:a.description}});let s=n.map(a=>rd(a,t));return{observers:i,preconditions:o,tests:s}}function td(e){let t=new Map;e.forEach(r=>{let n=t.get(r.id);if(n!=null)throw new Error(`Duplicate test id "${r.id}" used by "${n}" and "${r.name}"`);t.set(r.id,r.name)})}function rd(e,t){let r=e.id,{accessedKeys:n,vars:o}=od(e.requiresKeys),i=e.startsAtFn==null?void 0:e.startsAtFn(o),s=e.stepsFn==null?[]:e.stepsFn(o),a=i==null?s:[nd(i),...s],l=id(a,n,e.requiresKeys,e.uiOnly),c=[];Object.keys(e.requiresKeys).length>0&&n.size===0&&e.implemented&&c.push("Test requires preconditions but never references their data \u2014 destructure and use precondition data in steps()");let m=ad(e.requires,t);return{additionalChecks:[],coverage:e.coverage,description:e.description,expectedOutcome:e.expectedOutcome,implemented:e.implemented,name:e.name,preconditions:m,requiresKeys:{...e.requiresKeys},slug:r,spec:l,warnings:c}}function nd(e){return xo({type:"goto",url:{type:"static",value:e}}).as(`navigate to ${e}`)}function od(e){let t=new Set,r={};return Object.keys(e).forEach(n=>{r[n]=new Proxy({},{get(o,i){if(typeof i=="string"){let s=`${n}.${i}`;return t.add(s),`{{${s}}}`}}})}),{accessedKeys:t,vars:r}}function id(e,t,r,n){let o={};e.forEach((a,l)=>{let c=`step-${String(l)}`,p=l<e.length-1?`step-${String(l+1)}`:void 0;o[c]=sd(a,c,p)});let i={};t.forEach(a=>{i[a]={default:`test-${a}`,type:"string"}});let s={...r};return{entryNode:"step-0",nodes:o,uiOnly:n,variableNamespaces:s,variables:i}}function sd(e,t,r){let{label:n,node:o}=Po(e);return{...o,id:t,label:n,next:r}}function ad(e,t){let r=new Map(t.map(s=>[s.name,s])),n=[],o=new Set;function i(s){o.has(s)||(o.add(s),r.get(s)?.dependsOn.forEach(a=>{i(a)}),n.push(s))}return e.forEach(s=>{i(s)}),n}import{Webhook as xv,WebhookVerificationError as Cv}from"standardwebhooks";import{z as R}from"zod";var Iv=R.object({preconditions:R.array(R.string().min(1))}),Av=R.object({data:R.record(R.string(),R.record(R.string(),R.string())),preconditions:R.array(R.string().min(1))}),jv=R.object({observer:R.string().min(1).max(200),params:R.record(R.string().max(200),R.union([R.string(),R.number(),R.boolean()]))}),ld=R.discriminatedUnion("kind",[R.object({kind:R.literal("pass")}),R.object({kind:R.literal("retry"),reason:R.string()}),R.object({kind:R.literal("fail"),reason:R.string()})]),Tv=R.object({error:R.string().optional(),outcome:ld.optional(),success:R.boolean()});function De(e){let t=[];return e.tests.forEach(r=>{let n=cd(r),o=i=>{t.push({...i,test:r.slug})};Td.forEach(i=>{i(n,r,o)})}),{diagnostics:t}}function cd(e){let t=[],r=e.spec.entryNode,n=new Set;for(;r!=null&&!n.has(r);){n.add(r);let o=e.spec.nodes[r];if(o==null)break;t.push(o),r=o.next}return t}function ud(e,t,r){e.forEach(n=>{n.type==="assertText"&&"operator"in n&&n.operator!=="equals"&&r({message:`${n.type} uses operator "${n.operator}" \u2014 only "equals" is allowed for determinism`,rule:"exact-text-match",step:n.label??n.id})})}function dd(e,t,r){Object.keys(t.spec.variables??{}).length>0&&e.forEach(o=>{if(o.type==="fill"&&Eo(o.value)){let i=o.value.value;!Io(i)&&wd(i)&&r({message:`fill() uses hardcoded value "${i}" \u2014 consider using precondition data via {{namespace.key}}`,rule:"no-hardcoded-data",step:o.label??o.id})}})}function pd(e,t,r){if(Object.keys(t.spec.variables??{}).length===0)return;e.some(i=>JSON.stringify(i).includes("{{"))||r({message:"Test requires preconditions but steps() never references precondition data \u2014 destructure and use it",rule:"prefer-precondition-data",step:void 0})}function md(e,t,r){e.forEach(n=>{(n.label==null||n.label.length===0)&&r({message:`Step "${n.id}" lacks .as("...") label \u2014 every step must be labeled`,rule:"missing-label",step:n.id})})}function fd(e,t,r){let n=new Map;e.forEach(o=>{if(o.label==null)return;let i=n.get(o.label);i==null?n.set(o.label,o.id):r({message:`Duplicate label "${o.label}" \u2014 also used by ${i}`,rule:"no-duplicate-labels",step:o.label})})}function gd(e,t,r){let n=0;e.forEach((o,i)=>{if(!(i===0&&o.type==="goto")){if(Le(o)){n=0;return}n++,n===3&&r({message:"3+ consecutive actions without an assertion \u2014 add verification between actions",rule:"assert-after-action",step:o.label??o.id})}})}function yd(e,t,r){if(e.length===0)return;let n=e.at(-1);n!=null&&!Le(n)&&r({message:"Last step is an action, not an assertion \u2014 expectedOutcome should be verified by assertions at the end",rule:"assert-matches-outcome",step:n.label??n.id})}function hd(e,t,r){t.implemented&&e.length===0&&r({message:"Test has zero steps",rule:"no-empty-steps",step:void 0})}function Eo(e){return!(typeof e!="object"||e==null||!("type"in e)||e.type!=="static"||!("value"in e)||typeof e.value!="string")}function Io(e){return e.includes("{{")}function wd(e){return e.includes("@")||/\b[a-f0-9]{8,}\b/.test(e)||/^(test|ripplo|example|sample|demo)/i.test(e)}function Le(e){return e.type.startsWith("assert")}var bd=new Set(["assertText","assertValue","assertCount","assertUrl","assertNotVisible","assertChecked","assertNotChecked","assertAttribute","assertDisabled","assertEnabled"]);function vd(e){return bd.has(e.type)}function kd(e,t,r){!t.implemented||e.length===0||e.some(n=>Le(n))||r({message:"Test has zero assertion steps \u2014 cannot verify expectedOutcome. Add assert.* steps.",rule:"no-assertions",step:void 0})}function Sd(e,t,r){if(!t.implemented||e.length<=3)return;let n=e.filter(i=>Le(i)).length;if(n===0)return;let o=n/e.length;if(o<.15){let i=Math.round(o*100);r({message:`Only ${String(n)}/${String(e.length)} steps are assertions (${String(i)}%) \u2014 add more verification between actions`,rule:"low-assertion-ratio",step:void 0})}}function Gr(e){if(!("locator"in e)||e.locator==null)return;let t=e.locator;return t.by==="role"?`role:${t.role}:${t.name??""}`:`testId:${t.value}`}function Rd(e,t,r){e.forEach((n,o)=>{if(n.type!=="click")return;let i=Gr(n);if(i==null)return;let s=e.slice(o+1,o+4);s.find(c=>c.type==="assertVisible"&&Gr(c)===i)==null||s.some(c=>vd(c)||Le(c)&&Gr(c)!==i)||r({message:`click "${n.label??n.id}" is followed only by assert.visible on the same locator \u2014 verifies nothing about the click's effect`,rule:"tautological-post-click-assert",step:n.label??n.id})})}var Pd=new Set(["the","is","a","an","and","or","of","to","in","on","at","for","that","this","with","be","are","was","were","it","its","as","by","from","after","before","should","will","can","still","but","not","no","so","if","then","than","user","users","page","view","shows","show","see","click","clicks","clicked","clickable","visible","appears","appear","displayed","stays","remains"]);function Kr(e){return e.toLowerCase().split(/[^a-z0-9]+/).filter(t=>t.length>=3&&!Pd.has(t))}function xd(e){let t=e.label==null?[]:Kr(e.label);if(!("locator"in e)||e.locator==null)return t;let r=e.locator,n=r.by==="role"?r.name??"":r.value;return[...t,...Kr(n)]}function Cd(e,t,r){if(!t.implemented||e.length===0)return;let n=new Set(Kr(t.expectedOutcome));if(n.size===0)return;let o=e.filter(s=>Le(s));if(o.length===0)return;o.some(s=>xd(s).some(a=>n.has(a)))||r({message:`No assertion references any keyword from expectedOutcome (${[...n].join(", ")}) \u2014 the outcome may not actually be verified`,rule:"expected-outcome-keyword-coverage",step:void 0})}var Ed=/save|submit|create|delete|remove|send|invite|update|confirm|publish|apply|pay|subscribe|upgrade|cancel|archive|rename/i;function Co(e){if(e.type==="upload")return!0;if(e.type!=="click")return!1;let t=e.locator,r=t.by==="role"?t.name??"":t.value;return Ed.test(r)}function Id(e,t,r){!t.implemented||t.spec.uiOnly===!0||e.forEach((n,o)=>{if(!Co(n)||"uiOnly"in n&&n.uiOnly===!0)return;let i=e.slice(o+1),s=i.findIndex(p=>Co(p)),a=s===-1?i.length:s;i.slice(0,a).some(p=>p.type==="assertObserver")||r({message:`"${n.label??n.id}" looks like it mutates backend state but no assert.backend(...) observer verifies it \u2014 the test can pass while the server is broken. You need to add an observer that checks the persisted result (see .ripplo/observers/ for examples). Only if this action truly does NOT change any server state (pure client-side interaction, presentation toggle, etc.) should you fall back to marking the step { uiOnly: true } to silence this rule.`,rule:"mutation-without-observer-coverage",step:n.label??n.id})})}function Ad(e,t,r){Object.keys(t.spec.variables??{}).length!==0&&e.forEach(o=>{if(o.type!=="assertObserver")return;let i=Object.entries(o.params);i.length===0||i.some(([,a])=>Eo(a)&&Io(a.value))||r({message:`assert.backend "${o.label??o.id}" passes only hardcoded params while the test defines precondition variables \u2014 observer assertions should reference {{namespace.key}} so they match the actual precondition data`,rule:"observer-params-reference-variables",step:o.label??o.id})})}function jd(e,t,r){let n=new Set(Object.keys(t.spec.variables??{})),o=/\{\{([^{}]+?)\}\}/g;e.forEach(i=>{let a=[...JSON.stringify(i).matchAll(o)],l=[...new Set(a.map(m=>m[1]).filter(m=>m!=null&&!n.has(m)))];if(l.length===0)return;let c=l.map(m=>`{{${m}}}`).join(", "),p=[...new Set(l.map(m=>m.split(".")[0]??m))].join(", ");r({message:`"${i.label??i.id}" contains literal template string(s) ${c} \u2014 destructure the proxy in .steps(({ ${p} }) => \u2026) and pass the proxy value (e.g. \`${l[0]??""}\`) directly. Writing the template as a string bypasses type-checking and silently accepts typos.`,rule:"no-literal-template-strings",step:i.label??i.id})})}var Td=[ud,dd,jd,pd,md,fd,gd,yd,hd,kd,Sd,Rd,Cd,Id,Ad];import rp from"picomatch";import{execFileSync as $d,spawnSync as Nd}from"child_process";function q(e,t){return $d("git",[...e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function Ao(e,t){let r=Nd("git",["show",e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]});if(r.status===0)return r.stdout;if(r.status===128)return null;throw new Error(`git show ${e} failed (${String(r.status)}): ${r.stderr}`)}import h from"typescript";import*as jo from"remeda";var To=[["onClick","click"],["onContextMenu","click"],["onKeyDown","click"],["onKeyPress","click"],["onKeyUp","click"],["onSelect","click"],["onOpenChange","click"],["onPress","click"],["onActivate","click"],["onToggle","click"],["onOpen","click"],["onClose","click"],["onDismiss","click"],["onConfirm","click"],["onCancel","click"],["onApply","click"],["onClear","click"],["onSubmit","submit"],["onReset","submit"],["onDrop","drag"],["onDragEnd","drag"],["onDragEnter","drag"],["onDragLeave","drag"],["onDragOver","drag"],["onDragStart","drag"],["onValueChange","select"],["onSelectionChange","select"],["onChange","input"],["onCheckedChange","input"],["onInput","input"]],Od=new Map([["button","click"],["link","navigate"],["textbox","input"],["searchbox","input"],["combobox","select"],["listbox","select"]]),Dd=[[/(?:^|[a-z])Button$/,"click"],[/(?:^|[a-z])(?:Link|NavLink|Anchor)$/,"navigate"],[/(?:^|[a-z])(?:Textarea|TextArea|Input|Field|SearchBox)$/,"input"],[/(?:^|[a-z])(?:Select|Combobox|ComboBox|Dropdown)$/,"select"],[/(?:^|[a-z])Form$/,"submit"]];function Lt({filePath:e,source:t}){let r=h.createSourceFile(e,t,h.ScriptTarget.Latest,!0,h.ScriptKind.TSX);return r.statements.flatMap(n=>Ld({filePath:e,sf:r,stmt:n}))}function Ld({filePath:e,sf:t,stmt:r}){let n=Ud(r);if(n==null||_d(n))return[];let o=$o(r,t).map(i=>({id:`${e}#${n}.${i.kind}[${i.label}]`,line:i.line}));return jo.uniqueBy(o,i=>i.id)}function _d(e){return e.endsWith("Fragment")||e.endsWith("Skeleton")||/^use[A-Z]/.test(e)||/^[A-Z][A-Z0-9_]*$/.test(e)}function Ud(e){if(h.isExportAssignment(e))return Vd(e.expression);if(!Fd(e))return null;if(h.isFunctionDeclaration(e)||h.isClassDeclaration(e))return e.name?.text??null;if(h.isVariableStatement(e)){let t=e.declarationList.declarations[0];return t!=null&&h.isIdentifier(t.name)?t.name.text:null}return null}function Vd(e){return h.isIdentifier(e)?e.text:(h.isFunctionExpression(e)||h.isClassExpression(e))&&e.name!=null?e.name.text:"default"}function Fd(e){if(!h.canHaveModifiers(e))return!1;let t=h.getModifiers(e);return t!=null&&t.some(r=>r.kind===h.SyntaxKind.ExportKeyword)}function $o(e,t){let r=Hd(e,t),n=e.getChildren(t).flatMap(o=>$o(o,t));return r==null?n:[r,...n]}function Hd(e,t){let r=Bd(e);if(r==null)return null;let n=Wd(r);if(n==null)return null;let o=Kd(r,n);return o==null?null:{kind:n,label:o,line:tp(t,e.getStart(t))}}function Bd(e){return h.isJsxSelfClosingElement(e)&&h.isIdentifier(e.tagName)?{attrs:e.attributes,children:[],tag:e.tagName.text}:h.isJsxElement(e)&&h.isIdentifier(e.openingElement.tagName)?{attrs:e.openingElement.attributes,children:e.children,tag:e.openingElement.tagName.text}:null}function Wd({attrs:e,tag:t}){let r=le(e,"role");return r!=null?Od.get(r)??null:qd(t,e)??zd(t)??Gd(e)}var Md=new Map([["button",()=>"click"],["a",e=>le(e,"href")==null?null:"navigate"],["input",e=>le(e,"type")==="file"?"upload":"input"],["textarea",()=>"input"],["select",()=>"select"],["form",()=>"submit"]]);function qd(e,t){return Md.get(e)?.(t)??null}function zd(e){return/^[A-Z]/.test(e)?Dd.find(([t])=>t.test(e))?.[1]??null:null}function Gd(e){return To.find(([t])=>Yd(e,t))?.[1]??null}function Kd({attrs:e,children:t,tag:r},n){return n==="navigate"?le(e,"href")??le(e,"to"):le(e,"aria-label")??le(e,"label")??le(e,"title")??le(e,"name")??le(e,"placeholder")??(Jd(r)?void 0:ep(t))??Qd(e,n)}function Jd(e){return e==="input"||e==="textarea"||e==="select"}function Qd(e,t){let r=To.find(([,o])=>o===t)?.[0];if(r==null)return;let n=Xd(e,r);if(n!=null){if(h.isIdentifier(n))return n.text;if(h.isArrowFunction(n)||h.isFunctionExpression(n))return No(n.body)}}function No(e){if(h.isCallExpression(e)){let t=e.expression;return h.isIdentifier(t)?t.text:h.isPropertyAccessExpression(t)?t.name.text:void 0}return h.forEachChild(e,No)}function Yd(e,t){return e.properties.some(r=>h.isJsxAttribute(r)&&h.isIdentifier(r.name)&&r.name.text===t)}function Oo(e,t){return e.properties.find(r=>h.isJsxAttribute(r)&&h.isIdentifier(r.name)&&r.name.text===t)}function le(e,t){let r=Oo(e,t);return r==null?void 0:Zd(r.initializer)}function Xd(e,t){let r=Oo(e,t);if(!(r?.initializer==null||!h.isJsxExpression(r.initializer)))return r.initializer.expression}function Zd(e){if(e==null)return"";if(h.isStringLiteral(e))return e.text;if(h.isJsxExpression(e))return Lo(e.expression)}function ep(e){let t=e.flatMap(r=>Do(r)).join(" ").trim();return t.length===0?void 0:t}function Do(e){if(h.isJsxText(e)){let t=e.text.trim();return t.length===0?[]:[t]}if(h.isJsxExpression(e)){let t=Lo(e.expression);return t==null?[]:[t]}return h.isJsxElement(e)?e.children.flatMap(t=>Do(t)):[]}function Lo(e){if(e!=null){if(h.isStringLiteral(e)||h.isNoSubstitutionTemplateLiteral(e))return e.text;if(h.isCallExpression(e)&&e.arguments.length===1){let t=e.arguments[0];if(t!=null&&(h.isStringLiteral(t)||h.isNoSubstitutionTemplateLiteral(t)))return t.text}}}function tp(e,t){return e.getLineAndCharacterOfPosition(t).line+1}var np=_t.join(".ripplo","coverage.d.ts");function _e({cwd:e}){op({cwd:e,ignorePaths:Re})}function op({cwd:e,ignorePaths:t}){let r=Ue({cwd:e,ignorePaths:t});ap({content:sp(r),cwd:e})}function Ue({cwd:e,ignorePaths:t}){let n=ip({cwd:e,ignorePaths:t}).flatMap(o=>Jr({cwd:e,file:o}));return[...new Set(n)].sort((o,i)=>o.localeCompare(i))}function ip({cwd:e,ignorePaths:t}){let r=q(["ls-files","--cached","--others","--exclude-standard","-z"],e),n=rp([...t]);return r.split("\0").filter(o=>o.length>0&&/\.(tsx|jsx)$/.test(o)).filter(o=>!n(o)).filter(o=>ot.existsSync(_t.join(e,o)))}function Jr({cwd:e,file:t}){let r=ot.readFileSync(_t.join(e,t),"utf8");return Lt({filePath:t,source:r}).map(n=>n.id)}function sp(e){let t=`// GENERATED \u2014 do not edit
|
|
162
|
+
`:`${i}, ${Vc(n)}.
|
|
163
|
+
`),process.stdout.write(`Session saved to ${fe("token")}.
|
|
164
|
+
`),o||(process.stdout.write("\nNext: run `/ripplo:setup` in Claude Code to wire Ripplo into this project,\n"),process.stdout.write("or `npx ripplo init` to set it up by hand.\n"))}function Vc(e){let t=e.name.trim().split(/\s+/)[0];return t!=null&&t.length>0?t:e.email}async function Lr({serverUrl:e,token:t}){try{let n=(await g({config:be({serverUrl:e,token:t}),document:Uc,variables:void 0})).currentUser;return n==null?null:{email:n.email,name:n.name}}catch{return null}}async function lo(){let e=Z();e==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login`.\n"),process.exit(1));let t=Q(),r=await Lr({serverUrl:t,token:e});r==null&&(process.stdout.write("Token rejected. Run `ripplo auth login` to re-authenticate.\n"),process.exit(1)),process.stdout.write(`Authenticated as ${r.name} <${r.email}> (${t})
|
|
165
|
+
`)}function co(){if(!kr()){process.stdout.write(`No token to remove.
|
|
166
|
+
`);return}process.stdout.write(`Removed ${fe("token")}
|
|
167
|
+
`)}import Ur from"fs/promises";import Vr from"path";import{z as $t}from"zod";import{z as uo}from"zod";import{z as Oe}from"zod";import{z as u}from"zod";import{z as ve}from"zod";import{z as yo}from"zod";import{z as I}from"zod";import{z as Y}from"zod";import{z as _r}from"zod";import{z as U}from"zod";var Fc=$t.object({__codec:$t.string().min(1),data:$t.unknown(),version:$t.number().int().positive()}),Hc=class extends Error{codec;currentVersion;gotVersion;constructor(e){super(`Unsupported ${e.codec} version ${String(e.gotVersion)} (current ${String(e.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`),this.name="CodecVersionError",this.codec=e.codec,this.currentVersion=e.currentVersion,this.gotVersion=e.gotVersion}},Wc=class extends Error{constructor(e){super(`Codec mismatch: expected "${e.expected}", got "${e.got}"`),this.name="CodecMismatchError"}};function Wr(e){return mo({legacy:void 0,migrators:[],name:e,schemas:[]})}function Bc(e,t){let r=JSON.parse(t);return e.decode(r)}function mo(e){return{initial:t=>Fr(t,{...e,schemas:[t]}),legacy:t=>mo({...e,legacy:t})}}function Fr(e,t){return{build:()=>Mc(e,t),legacy:r=>Fr(e,{...t,legacy:r}),upgrade:(r,n)=>{let o=n;return Fr(r,{...t,migrators:[...t.migrators,o],schemas:[...t.schemas,r]})}}}function Mc(e,t){let r=t.schemas.length;return{currentVersion:r,name:t.name,decode:n=>qc(e,t,n),encode:n=>({__codec:t.name,data:n,version:r})}}function qc(e,t,r){let{data:n,version:o}=zc(t,r),i=fo(t,n,o);return e.parse(i)}function zc(e,t){let r=Fc.safeParse(t);if(r.success){if(r.data.__codec!==e.name)throw new Wc({expected:e.name,got:r.data.__codec});return{data:r.data.data,version:r.data.version}}if(e.legacy!=null&&e.legacy.detect(t))return{data:t,version:e.legacy.assumedVersion};throw new Error(`Cannot decode "${e.name}": value is not a codec envelope and no legacy detector matched.`)}function fo(e,t,r){let n=e.schemas.length;if(r>n)throw new Hc({codec:e.name,currentVersion:n,gotVersion:r});if(r===n)return t;let o=e.schemas[r-1];if(o==null)throw new Error(`Codec "${e.name}" missing schema for v${String(r)}; cannot migrate.`);let i=o.parse(t),s=e.migrators[r-1];if(s==null)throw new Error(`Codec "${e.name}" missing migrator v${String(r)} \u2192 v${String(r+1)}.`);return fo(e,s(i),r+1)}var go=Oe.object({depends:Oe.array(Oe.string().min(1)).optional().describe("Names of other preconditions that must be satisfied first. Resolved via topological sort; cycles are rejected at validation time."),description:Oe.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:Oe.array(Oe.string().min(1)).optional().describe("Keys that the execute response's data field will contain. e.g. ['projectId', 'workflowId']. These are used for route param interpolation ({{projectId}}) and workflow variables. Declared here so generated types are strongly typed per precondition.")}).describe("A named precondition declared at the graph level. States reference these by name."),Gc=ve.object({by:ve.literal("testId"),value:ve.string().min(1)}),Kc=ve.object({by:ve.literal("role"),name:ve.string().optional(),role:ve.string().min(1)}),P=ve.discriminatedUnion("by",[Gc,Kc]),rt=yo.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),Jc=yo.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]),Qc=I.object({type:I.literal("static"),value:I.union([I.string(),I.number(),I.boolean()])}),jt=I.object({name:I.string().min(1),type:I.literal("variable")}),Yc=I.discriminatedUnion("type",[Qc,jt]),ie=I.discriminatedUnion("type",[I.object({type:I.literal("static"),value:I.string()}),jt]),Xc=I.discriminatedUnion("type",[I.object({type:I.literal("static"),value:I.number().int().nonnegative()}),jt]),Zc=I.discriminatedUnion("type",[I.object({type:I.literal("static"),value:I.union([I.string(),I.number(),I.boolean()])}),jt]),ed=Y.discriminatedUnion("type",[Y.object({default:Y.string().optional(),type:Y.literal("string")}),Y.object({default:Y.number().optional(),type:Y.literal("number")}),Y.object({default:Y.boolean().optional(),type:Y.literal("boolean")}),Y.object({key:Y.string().min(1),type:Y.literal("env")})]),v={id:u.string().min(1).max(200),label:u.string().max(500).optional(),next:u.string().max(200).optional(),uiOnly:u.boolean().optional()},po=500,td=u.object({...v,type:u.literal("goto"),url:ie}),rd=u.object({...v,locator:P,type:u.literal("click")}),nd=u.object({...v,locator:P,type:u.literal("fill"),value:ie}),od=u.object({...v,locator:P,type:u.literal("select"),value:ie}),id=u.object({...v,locator:P,type:u.literal("hover")}),sd=u.object({...v,key:u.string().min(1),locator:P.optional(),type:u.literal("press")}),ad=u.object({...v,locator:P,type:u.literal("check")}),ld=u.object({...v,locator:P,type:u.literal("uncheck")}),cd=u.object({...v,height:u.number().int().positive(),type:u.literal("setViewport"),width:u.number().int().positive()}),dd=u.object({...v,message:u.string().min(1),type:u.literal("fail")}),ud=u.object({...v,type:u.literal("setVariable"),value:Yc,variable:u.string().min(1)}),pd=u.object({...v,locator:P,type:u.literal("extractText"),variable:u.string().min(1)}),md=u.object({...v,files:u.array(u.string()).min(1),locator:P,type:u.literal("upload")}),fd=u.object({...v,locator:P,type:u.literal("dblclick")}),gd=u.object({...v,source:P,target:P,type:u.literal("drag")}),yd=u.object({...v,locator:P,type:u.literal("scrollIntoView")}),hd=u.object({...v,locator:P,type:u.literal("type"),value:ie}),wd=u.object({...v,locator:P,type:u.literal("focus")}),bd=u.object({...v,locator:P,type:u.literal("clear")}),vd=u.object({...v,locator:P,type:u.literal("rightClick")}),kd=u.object({...v,action:u.enum(["accept","dismiss"]),promptText:u.string().optional(),type:u.literal("handleDialog")}),Sd=u.object({...v,action:u.enum(["read","write"]),type:u.literal("clipboard"),value:ie.optional(),variable:u.string().min(1).optional()}),Rd=u.object({...v,permission:u.string().min(1),state:u.enum(["granted","prompt"]),type:u.literal("setPermission")}),Pd=u.object({...v,locator:P,type:u.literal("assertVisible")}),xd=u.object({...v,locator:P,type:u.literal("assertNotVisible")}),Cd=u.object({...v,expected:ie,locator:P,operator:rt,type:u.literal("assertText")}),Ed=u.object({...v,expected:ie,operator:rt,type:u.literal("assertUrl")}),Ad=u.object({...v,expected:Xc,locator:P,operator:Jc,type:u.literal("assertCount")}),Id=u.object({...v,expected:ie,locator:P,operator:rt,type:u.literal("assertValue")}),Td=u.object({...v,attribute:u.string().min(1),expected:ie,locator:P,operator:rt,type:u.literal("assertAttribute")}),$d=u.object({...v,locator:P,type:u.literal("assertEnabled")}),jd=u.object({...v,locator:P,type:u.literal("assertDisabled")}),Nd=u.object({...v,expected:ie,operator:rt,type:u.literal("assertTitle")}),Od=u.object({...v,locator:P,type:u.literal("assertChecked")}),Dd=u.object({...v,locator:P,type:u.literal("assertNotChecked")}),Ld=u.object({...v,locator:P,type:u.literal("assertFocused")}),_d=u.object({...v,locator:P,type:u.literal("assertNotFocused")}),Ud=u.object({...v,budget:u.enum(["fast","slow","async"]),observer:u.string().min(1).max(200),params:u.record(u.string().max(200),Zc),type:u.literal("assertObserver")}),Vd=u.discriminatedUnion("type",[td,rd,nd,od,id,sd,ad,ld,Pd,xd,Cd,Ed,Ad,Id,Td,$d,jd,cd,dd,ud,pd,md,fd,gd,yd,hd,wd,bd,vd,kd,Sd,Rd,Nd,Od,Dd,Ld,_d,Ud]),ho=u.object({entryNode:u.string().min(1).max(200),nodes:u.record(u.string().max(200),Vd).refine(e=>Object.keys(e).length<=po,`Workflow has more than ${String(po)} nodes`),uiOnly:u.boolean().optional(),variableNamespaces:u.record(u.string().max(200),u.string().max(500)).optional(),variables:u.record(u.string().max(200),ed).optional()}),Fd=uo.record(uo.string().max(200),go),Hd={assumedVersion:1,detect:e=>typeof e=="object"&&e!==null&&"entryNode"in e&&"nodes"in e},Wd={assumedVersion:1,detect:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)&&!("__codec"in e)},Ev=Wr("workflow-spec").legacy(Hd).initial(ho).build(),Av=Wr("precondition-map").legacy(Wd).initial(Fd).build(),Bd=["fast","slow","async"],Md=_r.object({budget:_r.enum(Bd).describe("Polling budget tier: fast | slow | async"),description:_r.string().min(1).describe("Human-readable description of what this observer checks")}).describe("A named backend state observer. Tests assert against it with assert.backend(observer, params). Implementation lives on the user's server."),se=".ripplo/ripplo.lock",qd=5e3,zd=U.record(U.string().max(200),U.string().max(200)),Gd=U.object({coverage:U.array(U.string().max(500)).max(2e3).default([]),expectedOutcome:U.string().max(2e3),name:U.string().max(200),preconditions:U.array(U.string().max(200)).max(1e3),requiresKeys:zd,slug:U.string().max(200),spec:ho}),Kd=U.object({observers:U.record(U.string().max(200),Md),preconditions:U.record(U.string().max(200),go),tests:U.array(Gd).max(qd)}),wo=Wr("ripplo-lockfile").initial(Kd).build();function bo(e){return{observers:e.observers,preconditions:e.preconditions,tests:e.tests.filter(t=>t.implemented).map(t=>({coverage:[...t.coverage],expectedOutcome:t.expectedOutcome,name:t.name,preconditions:[...t.preconditions],requiresKeys:{...t.requiresKeys},slug:t.slug,spec:t.spec}))}}function Hr(e){let t=wo.encode(e);return`${JSON.stringify(t,Yd(t),2)}
|
|
168
|
+
`}async function Nt({cwd:e}){let t=Vr.join(e,se),r=await Jd(t);return r==null?null:Bc(wo,r)}async function ae({cwd:e,result:t}){let r=bo(t),n=Vr.join(e,se);await Ur.mkdir(Vr.dirname(n),{recursive:!0}),await Ur.writeFile(n,Hr(r),"utf8")}function Ot({compiled:e,existing:t}){if(t==null)return"missing";let r=Hr(bo(e)),n=Hr(t);return r===n?"match":"stale"}async function Jd(e){try{return await Ur.readFile(e,"utf8")}catch(t){if(Qd(t)&&t.code==="ENOENT")return null;throw t}}function Qd(e){return e instanceof Error&&"code"in e}function Yd(e){return function(r,n){return n===e||!Xd(n)?n:Object.fromEntries(Object.entries(n).toSorted(([o],[i])=>o.localeCompare(i)))}}function Xd(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}import nt from"fs";import _t from"path";var Br=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],ke=["**/*.gen.*","**/generated/**","**/*.d.ts","**/*.test.*","**/*.spec.*","**/node_modules/**","**/dist/**","**/build/**",".ripplo/**","**/*.md","**/.next/**","**/.turbo/**","**/.vercel/**","**/.svelte-kit/**","**/.nuxt/**","**/.astro/**","**/coverage/**","**/storybook-static/**","**/*.stories.*","**/*.story.*","**/cli/**","**/scripts/**","**/tools/**","**/__tests__/**","**/__mocks__/**","**/__fixtures__/**","**/*.config.*","**/*.setup.*","**/public/**","**/static/**","**/assets/**","**/migrations/**","**/prisma/migrations/**","**/scripts/**"];function vo(e){return e.data}function ko(e){return{as(t){return{data:{label:t,node:e}}}}}function Dt(e){let t=e.getPreconditions(),r=e.getObservers(),n=e.getTests();Zd(n);let o={};t.forEach(a=>{o[a.name]={depends:[...a.dependsOn],description:a.description,returns:[...a.returns]}});let i={};r.forEach(a=>{i[a.name]={budget:a.budget,description:a.description}});let s=n.map(a=>eu(a,t));return{observers:i,preconditions:o,tests:s}}function Zd(e){let t=new Map;e.forEach(r=>{let n=t.get(r.id);if(n!=null)throw new Error(`Duplicate test id "${r.id}" used by "${n}" and "${r.name}"`);t.set(r.id,r.name)})}function eu(e,t){let r=e.id,{accessedKeys:n,vars:o}=ru(e.requiresKeys),i=e.startsAtFn==null?void 0:e.startsAtFn(o),s=e.stepsFn==null?[]:e.stepsFn(o),a=i==null?s:[tu(i),...s],l=nu(a,n,e.requiresKeys,e.uiOnly),c=[];Object.keys(e.requiresKeys).length>0&&n.size===0&&e.implemented&&c.push("Test requires preconditions but never references their data \u2014 destructure and use precondition data in steps()");let m=iu(e.requires,t);return{additionalChecks:[],coverage:e.coverage,description:e.description,expectedOutcome:e.expectedOutcome,implemented:e.implemented,name:e.name,preconditions:m,requiresKeys:{...e.requiresKeys},slug:r,spec:l,warnings:c}}function tu(e){return ko({type:"goto",url:{type:"static",value:e}}).as(`navigate to ${e}`)}function ru(e){let t=new Set,r={};return Object.keys(e).forEach(n=>{r[n]=new Proxy({},{get(o,i){if(typeof i=="string"){let s=`${n}.${i}`;return t.add(s),`{{${s}}}`}}})}),{accessedKeys:t,vars:r}}function nu(e,t,r,n){let o={};e.forEach((a,l)=>{let c=`step-${String(l)}`,p=l<e.length-1?`step-${String(l+1)}`:void 0;o[c]=ou(a,c,p)});let i={};t.forEach(a=>{i[a]={default:`test-${a}`,type:"string"}});let s={...r};return{entryNode:"step-0",nodes:o,uiOnly:n,variableNamespaces:s,variables:i}}function ou(e,t,r){let{label:n,node:o}=vo(e);return{...o,id:t,label:n,next:r}}function iu(e,t){let r=new Map(t.map(s=>[s.name,s])),n=[],o=new Set;function i(s){o.has(s)||(o.add(s),r.get(s)?.dependsOn.forEach(a=>{i(a)}),n.push(s))}return e.forEach(s=>{i(s)}),n}import{Webhook as Vv,WebhookVerificationError as Fv}from"standardwebhooks";import{z as R}from"zod";var Wv=R.object({preconditions:R.array(R.string().min(1))}),Bv=R.object({data:R.record(R.string(),R.record(R.string(),R.string())),preconditions:R.array(R.string().min(1))}),Mv=R.object({observer:R.string().min(1).max(200),params:R.record(R.string().max(200),R.union([R.string(),R.number(),R.boolean()]))}),su=R.discriminatedUnion("kind",[R.object({kind:R.literal("pass")}),R.object({kind:R.literal("retry"),reason:R.string()}),R.object({kind:R.literal("fail"),reason:R.string()})]),qv=R.object({error:R.string().optional(),outcome:su.optional(),success:R.boolean()});function De(e){let t=[];return e.tests.forEach(r=>{let n=au(r),o=i=>{t.push({...i,test:r.slug})};Iu.forEach(i=>{i(n,r,o)})}),{diagnostics:t}}function au(e){let t=[],r=e.spec.entryNode,n=new Set;for(;r!=null&&!n.has(r);){n.add(r);let o=e.spec.nodes[r];if(o==null)break;t.push(o),r=o.next}return t}function lu(e,t,r){e.forEach(n=>{n.type==="assertText"&&"operator"in n&&n.operator!=="equals"&&r({message:`${n.type} uses operator "${n.operator}" \u2014 only "equals" is allowed for determinism`,rule:"exact-text-match",step:n.label??n.id})})}function cu(e,t,r){Object.keys(t.spec.variables??{}).length>0&&e.forEach(o=>{if(o.type==="fill"&&Ro(o.value)){let i=o.value.value;!Po(i)&&yu(i)&&r({message:`fill() uses hardcoded value "${i}" \u2014 consider using precondition data via {{namespace.key}}`,rule:"no-hardcoded-data",step:o.label??o.id})}})}function du(e,t,r){if(Object.keys(t.spec.variables??{}).length===0)return;e.some(i=>JSON.stringify(i).includes("{{"))||r({message:"Test requires preconditions but steps() never references precondition data \u2014 destructure and use it",rule:"prefer-precondition-data",step:void 0})}function uu(e,t,r){e.forEach(n=>{(n.label==null||n.label.length===0)&&r({message:`Step "${n.id}" lacks .as("...") label \u2014 every step must be labeled`,rule:"missing-label",step:n.id})})}function pu(e,t,r){let n=new Map;e.forEach(o=>{if(o.label==null)return;let i=n.get(o.label);i==null?n.set(o.label,o.id):r({message:`Duplicate label "${o.label}" \u2014 also used by ${i}`,rule:"no-duplicate-labels",step:o.label})})}function mu(e,t,r){let n=0;e.forEach((o,i)=>{if(!(i===0&&o.type==="goto")){if(Le(o)){n=0;return}n++,n===3&&r({message:"3+ consecutive actions without an assertion \u2014 add verification between actions",rule:"assert-after-action",step:o.label??o.id})}})}function fu(e,t,r){if(e.length===0)return;let n=e.at(-1);n!=null&&!Le(n)&&r({message:"Last step is an action, not an assertion \u2014 expectedOutcome should be verified by assertions at the end",rule:"assert-matches-outcome",step:n.label??n.id})}function gu(e,t,r){t.implemented&&e.length===0&&r({message:"Test has zero steps",rule:"no-empty-steps",step:void 0})}function Ro(e){return!(typeof e!="object"||e==null||!("type"in e)||e.type!=="static"||!("value"in e)||typeof e.value!="string")}function Po(e){return e.includes("{{")}function yu(e){return e.includes("@")||/\b[a-f0-9]{8,}\b/.test(e)||/^(test|ripplo|example|sample|demo)/i.test(e)}function Le(e){return e.type.startsWith("assert")}var hu=new Set(["assertText","assertValue","assertCount","assertUrl","assertNotVisible","assertChecked","assertNotChecked","assertAttribute","assertDisabled","assertEnabled"]);function wu(e){return hu.has(e.type)}function bu(e,t,r){!t.implemented||e.length===0||e.some(n=>Le(n))||r({message:"Test has zero assertion steps \u2014 cannot verify expectedOutcome. Add assert.* steps.",rule:"no-assertions",step:void 0})}function vu(e,t,r){if(!t.implemented||e.length<=3)return;let n=e.filter(i=>Le(i)).length;if(n===0)return;let o=n/e.length;if(o<.15){let i=Math.round(o*100);r({message:`Only ${String(n)}/${String(e.length)} steps are assertions (${String(i)}%) \u2014 add more verification between actions`,rule:"low-assertion-ratio",step:void 0})}}function Mr(e){if(!("locator"in e)||e.locator==null)return;let t=e.locator;return t.by==="role"?`role:${t.role}:${t.name??""}`:`testId:${t.value}`}function ku(e,t,r){e.forEach((n,o)=>{if(n.type!=="click")return;let i=Mr(n);if(i==null)return;let s=e.slice(o+1,o+4);s.find(c=>c.type==="assertVisible"&&Mr(c)===i)==null||s.some(c=>wu(c)||Le(c)&&Mr(c)!==i)||r({message:`click "${n.label??n.id}" is followed only by assert.visible on the same locator \u2014 verifies nothing about the click's effect`,rule:"tautological-post-click-assert",step:n.label??n.id})})}var Su=new Set(["the","is","a","an","and","or","of","to","in","on","at","for","that","this","with","be","are","was","were","it","its","as","by","from","after","before","should","will","can","still","but","not","no","so","if","then","than","user","users","page","view","shows","show","see","click","clicks","clicked","clickable","visible","appears","appear","displayed","stays","remains"]);function qr(e){return e.toLowerCase().split(/[^a-z0-9]+/).filter(t=>t.length>=3&&!Su.has(t))}function Ru(e){let t=e.label==null?[]:qr(e.label);if(!("locator"in e)||e.locator==null)return t;let r=e.locator,n=r.by==="role"?r.name??"":r.value;return[...t,...qr(n)]}function Pu(e,t,r){if(!t.implemented||e.length===0)return;let n=new Set(qr(t.expectedOutcome));if(n.size===0)return;let o=e.filter(s=>Le(s));if(o.length===0)return;o.some(s=>Ru(s).some(a=>n.has(a)))||r({message:`No assertion references any keyword from expectedOutcome (${[...n].join(", ")}) \u2014 the outcome may not actually be verified`,rule:"expected-outcome-keyword-coverage",step:void 0})}var xu=/save|submit|create|delete|remove|send|invite|update|confirm|publish|apply|pay|subscribe|upgrade|cancel|archive|rename/i;function So(e){if(e.type==="upload")return!0;if(e.type!=="click")return!1;let t=e.locator,r=t.by==="role"?t.name??"":t.value;return xu.test(r)}function Cu(e,t,r){!t.implemented||t.spec.uiOnly===!0||e.forEach((n,o)=>{if(!So(n)||"uiOnly"in n&&n.uiOnly===!0)return;let i=e.slice(o+1),s=i.findIndex(p=>So(p)),a=s===-1?i.length:s;i.slice(0,a).some(p=>p.type==="assertObserver")||r({message:`"${n.label??n.id}" looks like it mutates backend state but no assert.backend(...) observer verifies it \u2014 the test can pass while the server is broken. You need to add an observer that checks the persisted result (see .ripplo/observers/ for examples). Only if this action truly does NOT change any server state (pure client-side interaction, presentation toggle, etc.) should you fall back to marking the step { uiOnly: true } to silence this rule.`,rule:"mutation-without-observer-coverage",step:n.label??n.id})})}function Eu(e,t,r){Object.keys(t.spec.variables??{}).length!==0&&e.forEach(o=>{if(o.type!=="assertObserver")return;let i=Object.entries(o.params);i.length===0||i.some(([,a])=>Ro(a)&&Po(a.value))||r({message:`assert.backend "${o.label??o.id}" passes only hardcoded params while the test defines precondition variables \u2014 observer assertions should reference {{namespace.key}} so they match the actual precondition data`,rule:"observer-params-reference-variables",step:o.label??o.id})})}function Au(e,t,r){let n=new Set(Object.keys(t.spec.variables??{})),o=/\{\{([^{}]+?)\}\}/g;e.forEach(i=>{let a=[...JSON.stringify(i).matchAll(o)],l=[...new Set(a.map(m=>m[1]).filter(m=>m!=null&&!n.has(m)))];if(l.length===0)return;let c=l.map(m=>`{{${m}}}`).join(", "),p=[...new Set(l.map(m=>m.split(".")[0]??m))].join(", ");r({message:`"${i.label??i.id}" contains literal template string(s) ${c} \u2014 destructure the proxy in .steps(({ ${p} }) => \u2026) and pass the proxy value (e.g. \`${l[0]??""}\`) directly. Writing the template as a string bypasses type-checking and silently accepts typos.`,rule:"no-literal-template-strings",step:i.label??i.id})})}var Iu=[lu,cu,Au,du,uu,pu,mu,fu,gu,bu,vu,ku,Pu,Cu,Eu];import ep from"picomatch";import{execFileSync as Tu,spawnSync as $u}from"child_process";function q(e,t){return Tu("git",[...e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function xo(e,t){let r=$u("git",["show",e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]});if(r.status===0)return r.stdout;if(r.status===128)return null;throw new Error(`git show ${e} failed (${String(r.status)}): ${r.stderr}`)}import h from"typescript";import*as Co from"remeda";var Eo=[["onClick","click"],["onContextMenu","click"],["onKeyDown","click"],["onKeyPress","click"],["onKeyUp","click"],["onSelect","click"],["onOpenChange","click"],["onPress","click"],["onActivate","click"],["onToggle","click"],["onOpen","click"],["onClose","click"],["onDismiss","click"],["onConfirm","click"],["onCancel","click"],["onApply","click"],["onClear","click"],["onSubmit","submit"],["onReset","submit"],["onDrop","drag"],["onDragEnd","drag"],["onDragEnter","drag"],["onDragLeave","drag"],["onDragOver","drag"],["onDragStart","drag"],["onValueChange","select"],["onSelectionChange","select"],["onChange","input"],["onCheckedChange","input"],["onInput","input"]],ju=new Map([["button","click"],["link","navigate"],["textbox","input"],["searchbox","input"],["combobox","select"],["listbox","select"]]),Nu=[[/(?:^|[a-z])Button$/,"click"],[/(?:^|[a-z])(?:Link|NavLink|Anchor)$/,"navigate"],[/(?:^|[a-z])(?:Textarea|TextArea|Input|Field|SearchBox)$/,"input"],[/(?:^|[a-z])(?:Select|Combobox|ComboBox|Dropdown)$/,"select"],[/(?:^|[a-z])Form$/,"submit"]];function Lt({filePath:e,source:t}){let r=h.createSourceFile(e,t,h.ScriptTarget.Latest,!0,h.ScriptKind.TSX);return r.statements.flatMap(n=>Ou({filePath:e,sf:r,stmt:n}))}function Ou({filePath:e,sf:t,stmt:r}){let n=Lu(r);if(n==null||Du(n))return[];let o=Ao(r,t).map(i=>({id:`${e}#${n}.${i.kind}[${i.label}]`,line:i.line}));return Co.uniqueBy(o,i=>i.id)}function Du(e){return e.endsWith("Fragment")||e.endsWith("Skeleton")||/^use[A-Z]/.test(e)||/^[A-Z][A-Z0-9_]*$/.test(e)}function Lu(e){if(h.isExportAssignment(e))return _u(e.expression);if(!Uu(e))return null;if(h.isFunctionDeclaration(e)||h.isClassDeclaration(e))return e.name?.text??null;if(h.isVariableStatement(e)){let t=e.declarationList.declarations[0];return t!=null&&h.isIdentifier(t.name)?t.name.text:null}return null}function _u(e){return h.isIdentifier(e)?e.text:(h.isFunctionExpression(e)||h.isClassExpression(e))&&e.name!=null?e.name.text:"default"}function Uu(e){if(!h.canHaveModifiers(e))return!1;let t=h.getModifiers(e);return t!=null&&t.some(r=>r.kind===h.SyntaxKind.ExportKeyword)}function Ao(e,t){let r=Vu(e,t),n=e.getChildren(t).flatMap(o=>Ao(o,t));return r==null?n:[r,...n]}function Vu(e,t){let r=Fu(e);if(r==null)return null;let n=Hu(r);if(n==null)return null;let o=zu(r,n);return o==null?null:{kind:n,label:o,line:Zu(t,e.getStart(t))}}function Fu(e){return h.isJsxSelfClosingElement(e)&&h.isIdentifier(e.tagName)?{attrs:e.attributes,children:[],tag:e.tagName.text}:h.isJsxElement(e)&&h.isIdentifier(e.openingElement.tagName)?{attrs:e.openingElement.attributes,children:e.children,tag:e.openingElement.tagName.text}:null}function Hu({attrs:e,tag:t}){let r=le(e,"role");return r!=null?ju.get(r)??null:Bu(t,e)??Mu(t)??qu(e)}var Wu=new Map([["button",()=>"click"],["a",e=>le(e,"href")==null?null:"navigate"],["input",e=>le(e,"type")==="file"?"upload":"input"],["textarea",()=>"input"],["select",()=>"select"],["form",()=>"submit"]]);function Bu(e,t){return Wu.get(e)?.(t)??null}function Mu(e){return/^[A-Z]/.test(e)?Nu.find(([t])=>t.test(e))?.[1]??null:null}function qu(e){return Eo.find(([t])=>Ju(e,t))?.[1]??null}function zu({attrs:e,children:t,tag:r},n){return n==="navigate"?le(e,"href")??le(e,"to"):le(e,"aria-label")??le(e,"label")??le(e,"title")??le(e,"name")??le(e,"placeholder")??(Gu(r)?void 0:Xu(t))??Ku(e,n)}function Gu(e){return e==="input"||e==="textarea"||e==="select"}function Ku(e,t){let r=Eo.find(([,o])=>o===t)?.[0];if(r==null)return;let n=Qu(e,r);if(n!=null){if(h.isIdentifier(n))return n.text;if(h.isArrowFunction(n)||h.isFunctionExpression(n))return Io(n.body)}}function Io(e){if(h.isCallExpression(e)){let t=e.expression;return h.isIdentifier(t)?t.text:h.isPropertyAccessExpression(t)?t.name.text:void 0}return h.forEachChild(e,Io)}function Ju(e,t){return e.properties.some(r=>h.isJsxAttribute(r)&&h.isIdentifier(r.name)&&r.name.text===t)}function To(e,t){return e.properties.find(r=>h.isJsxAttribute(r)&&h.isIdentifier(r.name)&&r.name.text===t)}function le(e,t){let r=To(e,t);return r==null?void 0:Yu(r.initializer)}function Qu(e,t){let r=To(e,t);if(!(r?.initializer==null||!h.isJsxExpression(r.initializer)))return r.initializer.expression}function Yu(e){if(e==null)return"";if(h.isStringLiteral(e))return e.text;if(h.isJsxExpression(e))return jo(e.expression)}function Xu(e){let t=e.flatMap(r=>$o(r)).join(" ").trim();return t.length===0?void 0:t}function $o(e){if(h.isJsxText(e)){let t=e.text.trim();return t.length===0?[]:[t]}if(h.isJsxExpression(e)){let t=jo(e.expression);return t==null?[]:[t]}return h.isJsxElement(e)?e.children.flatMap(t=>$o(t)):[]}function jo(e){if(e!=null){if(h.isStringLiteral(e)||h.isNoSubstitutionTemplateLiteral(e))return e.text;if(h.isCallExpression(e)&&e.arguments.length===1){let t=e.arguments[0];if(t!=null&&(h.isStringLiteral(t)||h.isNoSubstitutionTemplateLiteral(t)))return t.text}}}function Zu(e,t){return e.getLineAndCharacterOfPosition(t).line+1}var tp=_t.join(".ripplo","coverage.d.ts");function _e({cwd:e}){rp({cwd:e,ignorePaths:ke})}function rp({cwd:e,ignorePaths:t}){let r=Ue({cwd:e,ignorePaths:t});ip({content:op(r),cwd:e})}function Ue({cwd:e,ignorePaths:t}){let n=np({cwd:e,ignorePaths:t}).flatMap(o=>zr({cwd:e,file:o}));return[...new Set(n)].sort((o,i)=>o.localeCompare(i))}function np({cwd:e,ignorePaths:t}){let r=q(["ls-files","--cached","--others","--exclude-standard","-z"],e),n=ep([...t]);return r.split("\0").filter(o=>o.length>0&&/\.(tsx|jsx)$/.test(o)).filter(o=>!n(o)).filter(o=>nt.existsSync(_t.join(e,o)))}function zr({cwd:e,file:t}){let r=nt.readFileSync(_t.join(e,t),"utf8");return Lt({filePath:t,source:r}).map(n=>n.id)}function op(e){let t=`// GENERATED \u2014 do not edit
|
|
141
169
|
import "@ripplo/testing";
|
|
142
170
|
|
|
143
171
|
declare module "@ripplo/testing" {
|
|
@@ -148,22 +176,22 @@ declare module "@ripplo/testing" {
|
|
|
148
176
|
`:`${t}${r}
|
|
149
177
|
}
|
|
150
178
|
}
|
|
151
|
-
`}function
|
|
152
|
-
`).map(a=>
|
|
179
|
+
`}function ip({content:e,cwd:t}){let r=_t.join(t,tp);nt.existsSync(r)&&nt.readFileSync(r,"utf8")===e||nt.writeFileSync(r,e)}import No from"path";import{createJiti as sp}from"jiti";var ap=["getObservers","getPreconditions","getTests","getUnimplemented"];function lp(e){return e==null||typeof e!="object"?!1:ap.every(t=>typeof Reflect.get(e,t)=="function")}function cp(e){return e===null?"null":Array.isArray(e)?"array":typeof e}async function C(e){let t=No.join(e,".ripplo","index.ts"),r=No.join(e,".ripplo");try{let o=await sp(import.meta.url,{moduleCache:!1,sourceMaps:!0}).import(t),i=o!=null&&typeof o=="object"&&"default"in o?Reflect.get(o,"default"):o;if(!lp(i))return{error:`${t} must default-export a RipploBuilder (got ${cp(i)})`,ok:!1};let s=Dt(i);return{builder:i,ok:!0,result:s}}catch(n){return{error:up(n,r),ok:!1}}}var dp=/\(?([^\s()]{1,500}\.ts):(\d{1,10}):(\d{1,10})\)?$/;function up(e,t){if(!(e instanceof Error))return String(e);let r=pp(e.stack,t);return r==null?e.message:`${r} \u2014 ${e.message}`}function pp(e,t){if(e==null)return;let n=e.split(`
|
|
180
|
+
`).map(a=>dp.exec(a)).find(a=>a!=null&&a[1]!=null&&a[2]!=null&&a[3]!=null&&a[1].startsWith(t));if(n==null)return;let[,o,i,s]=n;return`${o??""}:${i??""}:${s??""}`}function w(e,t){return t==null?`Load \`/ripplo:${e}\` skill for instructions.`:`Load \`/ripplo:${e}\` skill for instructions on ${t}.`}function Oo(e){return e.length===1?w(e[0]):`REQUIRED before proceeding: load ${e.map(r=>`\`/ripplo:${r}\``).join(" AND ")} skills (load every one \u2014 each carries rules the others don't).`}async function Do(e){let t=process.cwd(),r=await C(t);if(r.ok||(process.stderr.write(`Compilation failed: ${r.error}
|
|
153
181
|
`),process.stderr.write(`${w("create","DSL authoring + lint rules")}
|
|
154
182
|
`),process.exit(1)),e.check){let n=await Nt({cwd:t}),o=Ot({compiled:r.result,existing:n});if(o==="match"){process.stdout.write(`${se} is up to date
|
|
155
183
|
`);return}let i=o==="missing"?"missing":"out of date";process.stderr.write(`${se} is ${i} \u2014 run \`ripplo compile\` and commit the result
|
|
156
184
|
`),process.stderr.write(`${w("setup")}
|
|
157
185
|
`),process.exit(1)}await ae({cwd:t,result:r.result}),_e({cwd:t}),process.stdout.write(`wrote ${se}
|
|
158
|
-
`)}import
|
|
159
|
-
`),
|
|
160
|
-
`),o.length>0&&(
|
|
186
|
+
`)}import de from"process";import mp from"fs";import fp from"path";import gp from"picomatch";function Lo({compileResult:e,cwd:t,ignorePaths:r}){let n=new Set(Ue({cwd:t,ignorePaths:r})),o=Vo(e),i=yp({cwd:t,ignorePaths:r});return Uo({acknowledged:o,allStatements:n,unacked:i})}function _o({compileResult:e,cwd:t,ignorePaths:r}){let n=new Set(Ue({cwd:t,ignorePaths:r})),o=Vo(e);return Uo({acknowledged:o,allStatements:n,unacked:n})}function Uo({acknowledged:e,allStatements:t,unacked:r}){let n=[...r].filter(i=>!e.has(i)).map(i=>({id:i,kind:"unacknowledged"})),o=[...e].filter(i=>!t.has(i)).map(i=>({id:i,kind:"stale"}));return[...n,...o]}function Vo(e){return new Set(e.tests.flatMap(t=>t.coverage))}function yp({cwd:e,ignorePaths:t}){let r=hp({cwd:e,ignorePaths:t}),n=new Set(r.flatMap(i=>wp({cwd:e,file:i}))),o=new Set(r.flatMap(i=>bp({cwd:e,file:i})?zr({cwd:e,file:i}):[]));return new Set([...o].filter(i=>!n.has(i)))}function hp({cwd:e,ignorePaths:t}){let r=q(["diff","--name-only","HEAD","-z"],e),n=q(["ls-files","--others","--exclude-standard","-z"],e),o=gp([...t]);return[...new Set([...r.split("\0"),...n.split("\0")])].filter(i=>i.length>0&&/\.(tsx|jsx)$/.test(i)).filter(i=>!o(i))}function wp({cwd:e,file:t}){let r=xo(`HEAD:${t}`,e);return r==null?[]:Lt({filePath:t,source:r}).map(n=>n.id)}function bp({cwd:e,file:t}){return mp.existsSync(fp.join(e,t))}async function Fo(){let e=de.cwd(),t=await C(e);t.ok||(de.stderr.write(`Compilation failed: ${t.error}
|
|
187
|
+
`),de.exit(1));let r=Ue({cwd:e,ignorePaths:ke}),n=_o({compileResult:t.result,cwd:e,ignorePaths:ke}),o=n.filter(a=>a.kind==="unacknowledged"),i=n.filter(a=>a.kind==="stale"),s=r.length-o.length;de.stdout.write(`Coverage: ${String(s)}/${String(r.length)} statements acknowledged
|
|
188
|
+
`),o.length>0&&(de.stdout.write(`
|
|
161
189
|
Unacknowledged (${String(o.length)}):
|
|
162
|
-
`),o.forEach(a=>
|
|
163
|
-
`))),i.length>0&&(
|
|
190
|
+
`),o.forEach(a=>de.stdout.write(` ${a.id}
|
|
191
|
+
`))),i.length>0&&(de.stdout.write(`
|
|
164
192
|
Stale claims (${String(i.length)}):
|
|
165
|
-
`),i.forEach(a=>
|
|
166
|
-
`))),n.length>0&&
|
|
193
|
+
`),i.forEach(a=>de.stdout.write(` ${a.id}
|
|
194
|
+
`))),n.length>0&&de.exit(1)}import{graphql as kp}from"gql.tada";import Ho from"fs";import Gr from"path";function Wo(e){let t=Ho.realpathSync(e),r=t,n=Gr.dirname(r);for(;n!==r;){if(Ho.existsSync(Gr.join(r,".git")))return r;r=n,n=Gr.dirname(r)}return t}function L(e){let t=Zt(e),r=Ee(),n=Z();if(n==null)throw new Error("ripplo: not authenticated. Run `ripplo auth login`.");return{appUrl:r.appUrl,cwd:Wo(e),engineUrl:r.engineUrl,projectId:oo()??t.projectId,ripploServerUrl:Q(),token:n,webhookSecret:r.webhookSecret}}import ot from"fs";var vp="dev.pid";function Bo(e){return et(e,vp)}function Ve(e){let t=Bo(e);if(!ot.existsSync(t))return!1;let r=ot.readFileSync(t,"utf8").trim(),n=Number.parseInt(r,10);if(!Number.isFinite(n)||n<=0)return!1;try{return process.kill(n,0),!0}catch{return!1}}function Mo(e){Ze(e);let t=Bo(e);ot.writeFileSync(t,String(process.pid));let r=!1;return()=>{if(!r){r=!0;try{let n=ot.readFileSync(t,"utf8").trim();Number.parseInt(n,10)===process.pid&&ot.unlinkSync(t)}catch{}}}}var Sp=kp(`
|
|
167
195
|
query DevSessionCheckHealth($projectId: String!, $cwd: String!) {
|
|
168
196
|
project(id: $projectId) {
|
|
169
197
|
id
|
|
@@ -172,25 +200,25 @@ Stale claims (${String(i.length)}):
|
|
|
172
200
|
}
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
|
-
`);function
|
|
203
|
+
`);function qo(e){switch(e.status){case"active":return"\u2713 Dev session: ripplo watch is live for this directory";case"starting":return"! Dev session: `ripplo watch` is running locally but hasn't registered with the server yet. Wait a few seconds and re-run `npx ripplo doctor`. If it persists, check the watch process output for auth/network errors.";case"missing":return"\u2717 Dev session: no active ripplo watch. Start `npx ripplo watch` as a background process (separate from your app's dev server). Without it, `ripplo run` will refuse to dispatch and scope/coverage hooks won't arm."}}async function zo(e){let t=Ve(e),r;try{r=L(e)}catch{return{status:t?"starting":"missing",type:"dev-session"}}return(await g({config:r,document:Sp,variables:{cwd:r.cwd,projectId:r.projectId}}).catch(()=>null))?.project?.devSession!=null?{status:"active",type:"dev-session"}:{status:t?"starting":"missing",type:"dev-session"}}function Go(e){switch(e.type){case"settings":return Cp(e);case"env-files":return Ep(e);case"token":return Ap(e);case"dev-server":return Ip(e);case"dev-session":return qo(e);case"preconditions":return jp(e);case"webhook-verification":return Tp(e);case"preconditions-validation":return Np(e);case"workflows":return Op(e);case"browser":return $p(e);case"engine-endpoint":return e.reachable?`\u2713 Engine endpoint: ${e.url} is reachable`:`\u2717 Engine endpoint: ${e.url} is not reachable`;case"adapter-enabled":return Rp(e);case"lockfile":return Pp(e);case"pre-commit-hook":return xp(e)}}function Rp(e){switch(e.status){case"enabled":return`\u2713 Adapter: enabled at ${e.url}`;case"disabled":return"\u2717 Adapter: disabled (handler returned 404). Set ENABLE_RIPPLO_TESTING=true in your app's env (e.g. apps/<app>/.env.local for Next.js) and restart the dev server.";case"bad-secret":return"\u2717 Adapter: webhook signature rejected. RIPPLO_WEBHOOK_SECRET seen by your dev server does not match the one in your env file.";case"unreachable":return`\u2717 Adapter: ${e.url} could not be reached for signed probe.`;case"no-secret":return"\u2717 Adapter: RIPPLO_WEBHOOK_SECRET is not set in your env file (declared in .ripplo/project.json)."}}function Pp(e){switch(e.status){case"match":return`\u2713 Lockfile: ${se} is up to date`;case"missing":return`\u2717 Lockfile: ${se} is missing \u2014 run \`ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${se} is out of date \u2014 run \`ripplo compile\` and commit it`}}function xp(e){return e.installed?"\u2713 Pre-commit hook: .git/hooks/pre-commit runs `ripplo compile --check`":"! Pre-commit hook: .git/hooks/pre-commit does not run `ripplo compile --check` \u2014 see the setup skill for the snippet"}function Cp(e){return e.valid?"\u2713 Settings: Project configured":`\u2717 Settings: Missing fields: ${e.missingFields.join(", ")}`}function Ep(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
|
|
176
204
|
${e.missing.map(r=>` ${r}`).join(`
|
|
177
205
|
`)}
|
|
178
|
-
If you're in a git worktree, copy the env file from the main checkout (or symlink to a shared file outside the working tree).`}function
|
|
206
|
+
If you're in a git worktree, copy the env file from the main checkout (or symlink to a shared file outside the working tree).`}function Ap(e){switch(e.status){case"valid":return`\u2713 Auth: Signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: No token found. Run `ripplo` to authenticate.";case"invalid":return"\u2717 Auth: Saved token rejected by server. Run `ripplo auth login` to re-authenticate.";case"unreachable":return`! Auth: Could not reach ${Q()} to validate token.`}}function Ip(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Make sure your dev server is running.`}function Tp(e){return e.rejectsUnsigned?"\u2713 Webhook verification: Unsigned requests are correctly rejected":"\u2717 Webhook verification: Endpoint accepted an unsigned request \u2014 webhook signature verification may be missing or misconfigured."}function $p(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run: npx playwright install chromium"}function jp(e){return e.count===0?"! Preconditions: None defined":e.configured?e.endpointReachable===void 0?`! Preconditions: ${String(e.count)} defined (could not verify endpoint)`:`\u2713 Preconditions: ${String(e.count)} defined, endpoint configured`:`\u2717 Preconditions: ${String(e.count)} defined but RIPPLO_ENGINE_URL is not set in your env file`}function Np(e){if(!e.found)return"\u2717 State graph: DSL compilation failed";if(e.valid)return"\u2713 State graph: Valid";let t=e.errors.map(r=>` - ${r.path===""?"":r.path+": "}${r.message}`);return`\u2717 State graph: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
|
|
179
207
|
${t.join(`
|
|
180
|
-
`)}`}function
|
|
208
|
+
`)}`}function Op(e){if(e.total===0)return"! Tests: No tests defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let t=e.invalidWorkflows.map(r=>Dp(r));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
|
|
181
209
|
${t.join(`
|
|
182
|
-
`)}`}function
|
|
210
|
+
`)}`}function Dp(e){let t=e.errors.map(r=>" - "+(r.path===""?"":r.path+": ")+r.message);return" "+e.name+`:
|
|
183
211
|
`+t.join(`
|
|
184
|
-
`)}import
|
|
212
|
+
`)}import Jr from"fs";import Hp from"path";import{chromium as Wp}from"playwright";import Lp from"fs";import _p from"path";async function Ko(e){let t=_p.join(e,".ripplo","index.ts");if(!Lp.existsSync(t))return{appUrl:void 0,engineUrl:void 0,errors:[{message:".ripplo/index.ts not found",path:""}],valid:!1,warnings:[]};let r=await C(e);if(!r.ok)return{appUrl:void 0,engineUrl:void 0,errors:[{message:r.error,path:".ripplo/"}],valid:!1,warnings:[]};try{let n=Ee();return{appUrl:n.appUrl,engineUrl:n.engineUrl,errors:[],valid:!0,warnings:[]}}catch(n){return{appUrl:void 0,engineUrl:void 0,errors:[{message:n instanceof Error?n.message:String(n),path:""}],valid:!1,warnings:[]}}}async function Kr(e){try{await fetch(e,{method:"HEAD",signal:AbortSignal.timeout(5e3)});return}catch{return`appUrl (${e}) is not responding. Make sure your dev server is running on that port.`}}async function Jo({appUrl:e,engineUrl:t}){let r=t.startsWith("http://")||t.startsWith("https://")?t:`${e}${t}`;try{return(await fetch(`${r}/execute-preconditions`,{body:JSON.stringify({preconditions:[]}),headers:{"Content-Type":"application/json"},method:"PUT",signal:AbortSignal.timeout(5e3)})).ok?"Engine endpoint accepted an unsigned request \u2014 webhook signature verification may be missing.":void 0}catch{return}}import{graphql as Up}from"gql.tada";var Vp=Up(`
|
|
185
213
|
query DoctorAuthViewer {
|
|
186
214
|
currentUser {
|
|
187
215
|
name
|
|
188
216
|
email
|
|
189
217
|
}
|
|
190
218
|
}
|
|
191
|
-
`),
|
|
219
|
+
`),Fp="Failed to connect to Ripplo server";async function Qo({serverUrl:e,token:t}){try{let r=await g({config:be({serverUrl:e,token:t}),document:Vp,variables:void 0});return r.currentUser==null?{kind:"invalid"}:{email:r.currentUser.email,kind:"valid"}}catch(r){return(r instanceof Error?r.message:String(r)).startsWith(Fp)?{kind:"unreachable"}:{kind:"invalid"}}}function Yo(e){switch(e.type){case"settings":return!e.valid;case"env-files":return e.missing.length>0;case"token":return e.status==="missing"||e.status==="invalid";case"dev-server":return!e.reachable;case"dev-session":return e.status!=="active";case"preconditions":return e.count>0&&!e.configured;case"engine-endpoint":return!e.reachable;case"adapter-enabled":return e.status!=="enabled";case"webhook-verification":return!e.rejectsUnsigned;case"preconditions-validation":return!e.valid;case"workflows":return e.invalidNames.length>0;case"browser":return!e.installed;case"lockfile":return e.status!=="match";case"pre-commit-hook":return!e.installed}}async function Xo(e){let t=await qp(e),r={missing:rr(e),type:"env-files"},n=await zp(),o=Gp(),i=await C(e),s=Kp(i),a=Yp(i),l=await Bp(e,i),c=Mp(e),p=await Xp(t,i);return[t,r,n,...p,s,a,l,c,o]}async function Bp(e,t){if(!t.ok)return{status:"missing",type:"lockfile"};let r=await Nt({cwd:e});return{status:Ot({compiled:t.result,existing:r}),type:"lockfile"}}function Mp(e){let t=Hp.join(e,".git","hooks","pre-commit");return Jr.existsSync(t)?{installed:Jr.readFileSync(t,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function qp(e){let t=await Ko(e);return t.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:t.errors.map(n=>n.path).filter(n=>n.length>0),type:"settings",valid:!1}}async function zp(){let e=Z();if(e==null||e.length===0)return{email:void 0,status:"missing",type:"token"};let t=await Qo({serverUrl:Q(),token:e});return t.kind==="valid"?{email:t.email,status:"valid",type:"token"}:{email:void 0,status:t.kind,type:"token"}}function Gp(){let e=Wp.executablePath();return{installed:Jr.existsSync(e),type:"browser"}}function Kp(e){if(!e.ok)return{errorCount:1,errors:[{message:e.error,path:""}],found:!1,type:"preconditions-validation",valid:!1};let t=Jp(e.result.preconditions,e.result.tests);return{errorCount:t.length,errors:t,found:!0,type:"preconditions-validation",valid:t.length===0}}function Jp(e,t){let r=[],n=new Set(Object.keys(e));return Object.entries(e).forEach(([o,i])=>{(i.depends??[]).forEach(s=>{n.has(s)||r.push({message:`Depends on non-existent precondition "${s}"`,path:`preconditions.${o}.depends`})})}),t.forEach(o=>{Object.entries(o.requiresKeys).forEach(([i,s])=>{n.has(s)||r.push({message:`Test "${o.slug}" requires non-existent precondition "${s}" (as ${i})`,path:`tests.${o.slug}.requiresKeys.${i}`})})}),Qp(e).forEach(o=>{r.push({message:o,path:"preconditions"})}),r}function Qp(e){let t=new Set,r=new Set,n=[];function o(i){if(r.has(i)){n.push(`Precondition "${i}" has a circular dependency`);return}t.has(i)||(t.add(i),r.add(i),(e[i]?.depends??[]).forEach(s=>{o(s)}),r.delete(i))}return Object.keys(e).forEach(i=>{o(i)}),n}function Yp(e){if(!e.ok)return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let t=e.result.tests.length;return{invalidNames:[],invalidWorkflows:[],total:t,type:"workflows",valid:t}}async function Xp(e,t){if(!e.valid||!t.ok)return[];let r;try{let a=Ee();r={appUrl:a.appUrl,engineUrl:a.engineUrl,webhookSecret:a.webhookSecret}}catch{return[]}let n=[],o=await Kr(r.appUrl)==null;n.push({appUrl:r.appUrl,reachable:o,type:"dev-server"});let i=await zo(process.cwd());n.push(i);let s=await Zp(r,t);return n.push(...s),n}async function Zp(e,t){let r=t.ok?Object.keys(t.result.preconditions).length:0,n=e.engineUrl.length>0,o={configured:n,count:r,endpointReachable:void 0,type:"preconditions"};if(r===0||!n)return[o];let i=tm(e.appUrl,e.engineUrl);if(i==null)return[o];let s=await Kr(i)==null,a=[];if(a.push({configured:!0,count:r,endpointReachable:s,type:"preconditions"}),Zo(e.engineUrl)&&a.push({reachable:s,type:"engine-endpoint",url:i}),!s)return a;let l=await Jo({appUrl:e.appUrl,engineUrl:e.engineUrl});a.push({rejectsUnsigned:l==null,type:"webhook-verification"});let c=await em({engineUrl:i,webhookSecret:e.webhookSecret});return a.push({status:c,type:"adapter-enabled",url:i}),a}async function em({engineUrl:e,webhookSecret:t}){if(t.length===0)return"no-secret";let r=JSON.stringify({preconditions:[]});try{let n=await fetch(`${e}/execute-preconditions`,{body:r,headers:{"Content-Type":"application/json",...we({body:r,secret:t})},method:"PUT",signal:AbortSignal.timeout(5e3)});return n.status===404?"disabled":n.status===401||n.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function Zo(e){return e.startsWith("http://")||e.startsWith("https://")}function tm(e,t){return Zo(t)?t:`${e}${t}`}async function ei(){let e=process.cwd(),t=await Xo(e),r=t.map(o=>Go(o));process.stdout.write(r.join(`
|
|
192
220
|
`)+`
|
|
193
|
-
`);let n=t.some(o=>
|
|
221
|
+
`);let n=t.some(o=>Yo(o));process.exit(n?1:0)}import{graphql as at,readFragment as cm}from"gql.tada";import nm from"crypto";import{graphql as om}from"gql.tada";import Qr from"fs";import Yr from"path";function Ut(e){try{let t=Yr.join(rm(e),"HEAD"),r=Qr.readFileSync(t,"utf8").trim(),n="ref: refs/heads/";return r.startsWith(n)?r.slice(n.length):r.length===0?void 0:r.slice(0,7)}catch{return}}function rm(e){let t=Yr.join(e,".git");if(Qr.statSync(t).isDirectory())return t;let n=Qr.readFileSync(t,"utf8").trim(),o="gitdir:";if(!n.startsWith(o))throw new Error(`unrecognized .git file at ${t}`);let i=n.slice(o.length).trim();if(i.length===0)throw new Error(`empty gitdir pointer at ${t}`);return Yr.resolve(e,i)}var im=om(`
|
|
194
222
|
mutation SyncDevSession(
|
|
195
223
|
$projectId: String!
|
|
196
224
|
$cwd: String!
|
|
@@ -206,61 +234,64 @@ ${t.join(`
|
|
|
206
234
|
workflows: $workflows
|
|
207
235
|
) {
|
|
208
236
|
id
|
|
237
|
+
hooksPaused
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
|
-
`);async function
|
|
240
|
+
`);async function it(e){let t=sm(e.compiled);return lm({config:e.config,cwd:e.cwd,payload:t})}async function xe(e,t){let r=await C(e);if(!r.ok)throw new Error(`DSL compilation failed: ${r.error}`);let n=await it({compiled:r.result,config:t,cwd:e});return{compiled:r.result,devSessionId:n.devSessionId}}function sm(e){return{preconditions:Object.entries(e.preconditions).map(([t,r])=>({depends:[...r.depends??[]],description:r.description,name:t,returns:[...r.returns??[]]})),workflows:e.tests.map(t=>({expectedOutcome:t.expectedOutcome,name:t.name,preconditions:[...t.preconditions],requiresKeys:Object.entries(t.requiresKeys).map(([r,n])=>({namespace:r,preconditionName:n})),slug:t.slug,spec:t.implemented?JSON.stringify(t.spec):null}))}}function am(e){return nm.createHash("sha256").update(JSON.stringify(e)).digest("hex")}async function lm({config:e,cwd:t,payload:r}){let n=am(r),o=Ut(t),i=await g({config:e,document:im,variables:{branch:o??null,cwd:t,preconditions:r.preconditions.map(s=>({depends:[...s.depends],description:s.description,name:s.name,returns:[...s.returns]})),projectId:e.projectId,workflows:r.workflows.map(s=>({expectedOutcome:s.expectedOutcome,name:s.name,preconditions:[...s.preconditions],requiresKeys:s.requiresKeys.map(a=>({namespace:a.namespace,preconditionName:a.preconditionName})),slug:s.slug,spec:s.spec??null}))}});if(i.syncDevSession==null)throw new Error("syncDevSession returned null");return{devSessionId:i.syncDevSession.id,hash:n,hooksPaused:i.syncDevSession.hooksPaused}}import ti from"figures";var st={cross:ti.cross,tick:ti.tick};var ni=at(`
|
|
212
241
|
fragment WorkflowIdMCP on Workflow {
|
|
213
242
|
id
|
|
214
243
|
slug
|
|
215
244
|
}
|
|
216
|
-
`),
|
|
245
|
+
`),dm=at(`
|
|
217
246
|
query ProjectWorkflowIdsMCP($projectId: String!, $cwd: String!) {
|
|
218
247
|
project(id: $projectId) {
|
|
219
248
|
id
|
|
220
249
|
devSession(cwd: $cwd) {
|
|
221
250
|
id
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
251
|
+
workflows {
|
|
252
|
+
...WorkflowIdMCP
|
|
253
|
+
}
|
|
225
254
|
}
|
|
226
255
|
}
|
|
227
256
|
}
|
|
228
|
-
`,[
|
|
229
|
-
mutation
|
|
230
|
-
|
|
257
|
+
`,[ni]),um=at(`
|
|
258
|
+
mutation SetProjectViewCLI($projectId: String!, $devSessionId: String!) {
|
|
259
|
+
setProjectView(projectId: $projectId, devSessionId: $devSessionId) {
|
|
260
|
+
id
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`),pm=at(`
|
|
264
|
+
mutation CreateRunMCP($workflowId: String!) {
|
|
265
|
+
createRun(workflowId: $workflowId) {
|
|
231
266
|
id
|
|
232
267
|
status
|
|
233
268
|
}
|
|
234
269
|
}
|
|
235
|
-
`),
|
|
270
|
+
`),mm=at(`
|
|
236
271
|
query RunStatusMCP($id: String!) {
|
|
237
272
|
run(id: $id) {
|
|
238
273
|
id
|
|
239
274
|
status
|
|
240
|
-
|
|
241
|
-
|
|
275
|
+
duration
|
|
276
|
+
passCount
|
|
277
|
+
failCount
|
|
278
|
+
warnCount
|
|
279
|
+
summary
|
|
280
|
+
traceEntries {
|
|
281
|
+
title
|
|
242
282
|
status
|
|
283
|
+
detail
|
|
243
284
|
duration
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
warnCount
|
|
247
|
-
summary
|
|
248
|
-
traceEntries {
|
|
249
|
-
title
|
|
285
|
+
assertions {
|
|
286
|
+
description
|
|
250
287
|
status
|
|
251
288
|
detail
|
|
252
|
-
duration
|
|
253
|
-
assertions {
|
|
254
|
-
description
|
|
255
|
-
status
|
|
256
|
-
detail
|
|
257
|
-
}
|
|
258
289
|
}
|
|
259
290
|
}
|
|
260
291
|
}
|
|
261
292
|
}
|
|
262
|
-
`);async function
|
|
263
|
-
${w("setup","check auth + config")}`};let{entry:a,runId:l}=s;try{let c=await
|
|
293
|
+
`);async function Vt({config:e,ids:t}){let r=await fm({config:e,ids:t}),n=await Promise.all(r.map(async s=>{try{let a=await bm({config:e,workflowId:s.workflowId});return{entry:s,ok:!0,runId:a}}catch(a){let l=a instanceof Error?a.message:String(a);return{entry:s,message:l,ok:!1}}})),o=await Promise.all(n.map(async s=>{if(!s.ok)return{id:s.entry.id,runId:"",summary:`ERROR creating run: ${s.message}
|
|
294
|
+
${w("setup","check auth + config")}`};let{entry:a,runId:l}=s;try{let c=await km({config:e,runId:l}),p=wm({config:e,id:a.id,runId:l}),m=`.ripplo/debug/${l}/`;return{id:a.id,runId:l,summary:`View run: ${p}
|
|
264
295
|
Debug artifacts: ${m}
|
|
265
296
|
${w("debug","read summary.txt, accessibility-tree, network.jsonl before guessing")}
|
|
266
297
|
|
|
@@ -270,10 +301,10 @@ ${s.summary}`).join(`
|
|
|
270
301
|
|
|
271
302
|
---
|
|
272
303
|
|
|
273
|
-
`);return{results:o,summary:i}}async function
|
|
304
|
+
`);return{results:o,summary:i}}async function fm({config:e,ids:t}){let r=await xe(e.cwd,e),n=await g({config:e,document:dm,variables:{cwd:e.cwd,projectId:e.projectId}});if(n.project==null)throw new Error("Project not found");let o=n.project.devSession;if(o==null)throw new Error("No active dev session. Start one by running `ripplo` (or `npx ripplo`) in your terminal, then try again.");await g({config:e,document:um,variables:{devSessionId:o.id,projectId:e.projectId}});let i=(o.workflows??[]).map(a=>cm(ni,a));if(t.length===0)return i.map(a=>({id:a.slug,workflowId:a.id}));let s=r.compiled.tests.map(a=>a.slug);return t.map(a=>{let l=i.find(c=>c.slug===a);if(l==null)throw new Error(gm({compiledSlugs:s,slug:a}));return{id:a,workflowId:l.id}})}function gm({compiledSlugs:e,slug:t}){if(e.includes(t))return`Test "${t}" was synced from .ripplo/ but the server didn't return it. This is likely a server-state bug \u2014 check the server logs for the SyncDevSession mutation.`;let r=ym({compiledSlugs:e,limit:3,slug:t}),n=r.length>0?`
|
|
274
305
|
Did you mean: ${r.join(", ")}?`:"";return`Test "${t}" is not registered in this dev session.${n}
|
|
275
|
-
Tests must be exported from .ripplo/tests/index.ts. (ripplo run already syncs on demand \u2014 restarting the dev server will not help.)`}function
|
|
276
|
-
`)}function
|
|
306
|
+
Tests must be exported from .ripplo/tests/index.ts. (ripplo run already syncs on demand \u2014 restarting the dev server will not help.)`}function ym({compiledSlugs:e,limit:t,slug:r}){let n=new Set(r.split("-"));return e.map(i=>({candidate:i,score:hm(n,i)})).filter(i=>i.score>0).toSorted((i,s)=>s.score-i.score).slice(0,t).map(i=>i.candidate)}function hm(e,t){return t.split("-").filter(r=>e.has(r)).length}function wm({config:e,id:t,runId:r}){let n=e.ripploServerUrl.replace(/:3000\b/,":3001"),o=new URL(n.endsWith("/")?n:`${n}/`);return new URL(`projects/${e.projectId}/workflows/${t}/runs/${r}`,o).toString()}async function bm({config:e,workflowId:t}){let r=await g({config:e,document:pm,variables:{concurrency:1,platforms:["chromium"],workflowId:t}});if(r.createRun==null)throw new Error("Failed to create run");return r.createRun.id}var vm=2e3,ri=150;async function km({config:e,runId:t}){let r=0;for(;r<ri;){await Rm(vm),r+=1;let o=(await g({config:e,document:mm,variables:{id:t}})).run;if(o==null)throw new Error(`Run ${t} not found`);if(!(o.status==="pending"||o.status==="running"))return Sm({run:o,runId:t})}return`Run ${t} timed out after polling ${String(ri)} times.`}function Sm({run:e,runId:t}){let r=[`Run ${t}: ${e.status.toUpperCase()}`];return r.push(` ${e.status}: ${String(e.passCount)} passed, ${String(e.failCount)} failed (${String(e.duration??0)}ms)`),e.summary!=null&&r.push(` Summary: ${e.summary}`),e.traceEntries.forEach(n=>{let o=n.status==="passed"?st.tick:st.cross,i=n.detail==null?"":` \u2014 ${n.detail}`;r.push(` ${o} ${n.title} (${String(n.duration)}ms)${i}`),n.assertions.forEach(s=>{let a=s.status==="passed"?st.tick:st.cross,l=s.detail==null?"":` \u2014 ${s.detail}`;r.push(` ${a} ${s.description}${l}`)})}),r.join(`
|
|
307
|
+
`)}function Rm(e){return new Promise(t=>{setTimeout(t,e)})}import{graphql as Pm}from"gql.tada";var xm=Pm(`
|
|
277
308
|
query DevSessionCheckPreflight($projectId: String!, $cwd: String!) {
|
|
278
309
|
project(id: $projectId) {
|
|
279
310
|
id
|
|
@@ -282,36 +313,38 @@ Tests must be exported from .ripplo/tests/index.ts. (ripplo run already syncs on
|
|
|
282
313
|
}
|
|
283
314
|
}
|
|
284
315
|
}
|
|
285
|
-
`);function
|
|
316
|
+
`);function ue(){try{return L(process.cwd())}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`${t}
|
|
286
317
|
`),process.stderr.write(`${w("setup")}
|
|
287
|
-
`),process.exit(1)}}async function
|
|
288
|
-
`),process.exit(1))}async function
|
|
289
|
-
`);let n=await Promise.allSettled(Array.from({length:t},()=>
|
|
318
|
+
`),process.exit(1)}}async function pe(e){(await g({config:e,document:xm,variables:{cwd:e.cwd,projectId:e.projectId}})).project?.devSession==null&&(process.stderr.write("No active dev session. Start `npx ripplo watch` as a background process (your app's dev server must also be running), then retry. Or run `/ripplo:start` in Claude Code.\n"),process.stderr.write(`${w("setup")}
|
|
319
|
+
`),process.exit(1))}async function oi({id:e,runs:t}){let r=ue();await pe(r),process.stdout.write(`flake-detect "${e}" x${String(t)}...
|
|
320
|
+
`);let n=await Promise.allSettled(Array.from({length:t},()=>Vt({config:r,ids:[e]}))),o=0,i=0,s=new Map;n.forEach(l=>{if(l.status==="rejected"){i++;let p=l.reason instanceof Error?l.reason.message:"unknown error";s.set(p,(s.get(p)??0)+1);return}let c=l.value.results[0];if(c==null){i++;return}if(c.summary.includes("FAILED")||c.summary.includes("ERROR")){i++;let p=c.summary.split(`
|
|
290
321
|
`).find(m=>m.includes("FAILED")||m.includes("ERROR"))??"unknown failure";s.set(p,(s.get(p)??0)+1)}else o++});let a=t>0?i/t*100:0;process.stdout.write(`${String(o)} passed / ${String(i)} failed (${a.toFixed(1)}% flake)
|
|
291
322
|
`),s.size>0&&s.forEach((l,c)=>{process.stdout.write(` ${String(l)}x: ${c}
|
|
292
323
|
`)}),i>0&&process.stderr.write(`${w("flake-detect","parallel safety + race conditions")}
|
|
293
|
-
`),process.exit(i>0?1:0)}import
|
|
324
|
+
`),process.exit(i>0?1:0)}import Ft from"fs";import{graphql as Em}from"gql.tada";import Cm from"fs";function Se(e){return et(e,"hooks-paused")}function T(e){return Cm.existsSync(Se(e))?!1:Ve(e)}var Am=Em(`
|
|
294
325
|
mutation CliSetHooksPaused($projectId: String!, $paused: Boolean!) {
|
|
295
|
-
setHooksPaused(projectId: $projectId, paused: $paused)
|
|
326
|
+
setHooksPaused(projectId: $projectId, paused: $paused) {
|
|
327
|
+
id
|
|
328
|
+
}
|
|
296
329
|
}
|
|
297
|
-
`);async function
|
|
298
|
-
`);return}
|
|
299
|
-
`)}async function
|
|
300
|
-
`)[0]??r.message:String(r);return
|
|
330
|
+
`);async function ii(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` must be invoked interactively from a terminal \u2014 refusing non-TTY invocation. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();Ze(e);let t=Se(e);if(Ft.existsSync(t)){process.stdout.write("Hooks already paused. Run `ripplo hooks resume` to re-enable.\n");return}Ft.writeFileSync(t,""),await ai(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `ripplo hooks resume`.\n")}async function si(){let e=process.cwd(),t=Se(e);if(!Ft.existsSync(t)){process.stdout.write(`Hooks already active.
|
|
331
|
+
`);return}Ft.unlinkSync(t),await ai(e,!1),process.stdout.write(`Hooks resumed.
|
|
332
|
+
`)}async function ai(e,t){let r;try{r=L(e)}catch{return}await g({config:r,document:Am,variables:{paused:t,projectId:r.projectId}}).catch(()=>null)}import Fe from"fs";import He from"path";import{input as di,select as ui}from"@inquirer/prompts";import{graphql as Km}from"gql.tada";import{exec as Im}from"child_process";import D from"fs";import _ from"path";import{promisify as Tm}from"util";var $m=["@ripplo/testing"],li=".ripplo/ripplo.lock linguist-generated=true",jm=[".ripplo/debug/",".ripplo/.local/"],Nm=Tm(Im);async function ci({cwd:e,onStep:t}){t("Scaffolding project files..."),Hm({cwd:e}),t("Updating .gitignore..."),Wm(e),t("Marking ripplo.lock as generated..."),Lm(e),t("Installing dependencies...");let r=await Om(e),n=[];if(r.ok||n.push({manualCommand:r.cmd,message:`Couldn't auto-install dev dependencies (${r.reason}). Run the command below, then run \`ripplo lint\` to compile the lockfile.`}),r.ok){t("Compiling initial lockfile...");let o=await Dm(e);o!=null&&n.push({manualCommand:void 0,message:o})}return t("Setting up browser..."),await fr(),n}async function Om(e){let t=_m({cwd:e,pm:Fm(e)});y.info("Installing dependencies: %s",t);try{return await Nm(t,{cwd:e}),{ok:!0}}catch(r){let n=r instanceof Error?r.message.split(`
|
|
333
|
+
`)[0]??r.message:String(r);return y.warn("Install failed (%s): %s",t,n),{cmd:t,ok:!1,reason:n}}}async function Dm(e){try{let t=await C(e);if(!t.ok)return;await ae({cwd:e,result:t.result});return}catch(t){return`Couldn't compile initial lockfile: ${t instanceof Error?t.message:String(t)}.`}}function Lm(e){let t=_.join(e,".gitattributes"),r=D.existsSync(t)?D.readFileSync(t,"utf8"):"";if(r.includes(li))return;let n=r.length===0||r.endsWith(`
|
|
301
334
|
`)?"":`
|
|
302
|
-
`;D.writeFileSync(t,`${r}${n}${
|
|
303
|
-
`)}function
|
|
335
|
+
`;D.writeFileSync(t,`${r}${n}${li}
|
|
336
|
+
`)}function _m({cwd:e,pm:t}){let r=$m.join(" ");return t==="pnpm"?Um(e)?`pnpm add -wD ${r}`:`pnpm add -D ${r}`:t==="yarn"?Vm(e)?`yarn add -WD ${r}`:`yarn add -D ${r}`:t==="bun"?`bun add -d ${r}`:`npm install -D ${r}`}function Um(e){return D.existsSync(_.join(e,"pnpm-workspace.yaml"))||D.existsSync(_.join(e,"pnpm-workspace.yml"))}function Vm(e){let t=_.join(e,"package.json");if(!D.existsSync(t))return!1;try{let r=JSON.parse(D.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("workspaces"in r))return!1;let n=r.workspaces;return Array.isArray(n)||n!=null&&typeof n=="object"}catch{return!1}}function Fm(e){return D.existsSync(_.join(e,"pnpm-lock.yaml"))?"pnpm":D.existsSync(_.join(e,"yarn.lock"))?"yarn":D.existsSync(_.join(e,"bun.lockb"))||D.existsSync(_.join(e,"bun.lock"))?"bun":"npm"}function Hm({cwd:e}){let t=_.join(e,".ripplo"),r=_.join(t,"tests"),n=_.join(t,"preconditions"),o=_.join(t,"observers");D.mkdirSync(r,{recursive:!0}),D.mkdirSync(n,{recursive:!0}),D.mkdirSync(o,{recursive:!0}),lt(_.join(t,"index.ts"),Bm),lt(_.join(n,"index.ts"),Mm),lt(_.join(o,"index.ts"),qm),lt(_.join(r,"index.ts"),zm),lt(_.join(t,"tsconfig.json"),Gm)}function lt(e,t){D.existsSync(e)||D.writeFileSync(e,t)}function Wm(e){let t=_.join(e,".gitignore");if(!D.existsSync(t))return;let r=D.readFileSync(t,"utf8"),n=jm.filter(i=>!r.includes(i));if(n.length===0)return;let o=r.endsWith(`
|
|
304
337
|
`)?"":`
|
|
305
338
|
`;D.writeFileSync(t,r+o+n.join(`
|
|
306
339
|
`)+`
|
|
307
|
-
`)}var
|
|
340
|
+
`)}var Bm=`import { createRipplo } from "@ripplo/testing";
|
|
308
341
|
import { preconditions } from "./preconditions/index";
|
|
309
342
|
import { observers } from "./observers/index";
|
|
310
343
|
import { tests } from "./tests/index";
|
|
311
344
|
|
|
312
345
|
export { preconditions, observers, tests };
|
|
313
346
|
export default createRipplo({ preconditions, observers, tests });
|
|
314
|
-
`,
|
|
347
|
+
`,Mm=`// Declare preconditions with \`precondition(name)\` from @ripplo/testing and collect them
|
|
315
348
|
// in the \`preconditions\` registry below. Every handle in this registry must be implemented
|
|
316
349
|
// in your app server's \`createEngine(ripplo, { preconditions: {...}, observers: {...} })\` call.
|
|
317
350
|
//
|
|
@@ -325,7 +358,7 @@ export default createRipplo({ preconditions, observers, tests });
|
|
|
325
358
|
// export const preconditions = { authLoggedIn };
|
|
326
359
|
|
|
327
360
|
export const preconditions = {};
|
|
328
|
-
`,
|
|
361
|
+
`,qm=`// Declare observers with \`observer(name)\` from @ripplo/testing and collect them in
|
|
329
362
|
// the \`observers\` registry below. Use them in tests via \`assert.backend(handle, params)\`.
|
|
330
363
|
//
|
|
331
364
|
// Example:
|
|
@@ -338,7 +371,7 @@ export const preconditions = {};
|
|
|
338
371
|
// export const observers = { orgNameIs };
|
|
339
372
|
|
|
340
373
|
export const observers = {};
|
|
341
|
-
`,
|
|
374
|
+
`,zm=`// Each test file under ./tests exports a TestDefinition. Import them here and add them
|
|
342
375
|
// to the \`tests\` array \u2014 that's what \`createRipplo({ ..., tests })\` receives.
|
|
343
376
|
//
|
|
344
377
|
// Example:
|
|
@@ -346,7 +379,7 @@ export const observers = {};
|
|
|
346
379
|
// export const tests = [myTest] as const;
|
|
347
380
|
|
|
348
381
|
export const tests = [] as const;
|
|
349
|
-
`,
|
|
382
|
+
`,Gm=`{
|
|
350
383
|
"compilerOptions": {
|
|
351
384
|
"strict": true,
|
|
352
385
|
"noUncheckedIndexedAccess": true,
|
|
@@ -362,68 +395,68 @@ export const tests = [] as const;
|
|
|
362
395
|
"include": ["*.ts", "coverage.d.ts", "observers/**/*.ts", "preconditions/**/*.ts", "tests/**/*.ts"],
|
|
363
396
|
"exclude": ["node_modules"]
|
|
364
397
|
}
|
|
365
|
-
`;var
|
|
398
|
+
`;var Jm=Km(`
|
|
366
399
|
query InitProjects {
|
|
367
400
|
projects {
|
|
368
401
|
id
|
|
369
402
|
name
|
|
370
403
|
}
|
|
371
404
|
}
|
|
372
|
-
`),
|
|
373
|
-
`),process.exit(1));let o=await
|
|
405
|
+
`),Xr=["../.env.local","../.env"];async function pi(e=Xm()){let t=process.cwd(),r=Z();r==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let n=Q();Qm(t)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${t}. Delete it first if you want to re-init.
|
|
406
|
+
`),process.exit(1));let o=await Ym({serverUrl:n,token:r});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `ripplo init`.\n"),process.exit(1));let i=await ef(o,e.projectId),s=await tf(t,e.envFile),a=await rf(e.appUrl),l=Zm(a,e.engineUrl),c=He.resolve(He.join(t,".ripplo"),s);ln({cwd:t,envFiles:[s],projectId:i}),sf({appUrl:a,engineUrl:l,filePath:c});let p=await ci({cwd:t,onStep:m=>{process.stdout.write(` ${m}
|
|
374
407
|
`)}});if(p.length>0){process.stdout.write(`Done with warnings:
|
|
375
408
|
`),p.forEach(m=>{process.stdout.write(` - ${m.message}
|
|
376
409
|
`),m.manualCommand!=null&&process.stdout.write(` run: ${m.manualCommand}
|
|
377
|
-
`)});return}process.stdout.write("Ready. Start `npx ripplo watch` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function
|
|
378
|
-
`),process.exit(1)}return t}return`${e.replace(/\/$/,"")}/ripplo`}async function
|
|
410
|
+
`)});return}process.stdout.write("Ready. Start `npx ripplo watch` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function Qm(e){return Fe.existsSync(He.join(e,".ripplo","index.ts"))}async function Ym({serverUrl:e,token:t}){return((await g({config:be({serverUrl:e,token:t}),document:Jm,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function Xm(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function Zm(e,t){if(t!=null){try{new URL(t)}catch{process.stdout.write(`--engine-url is not a valid URL: ${t}
|
|
411
|
+
`),process.exit(1)}return t}return`${e.replace(/\/$/,"")}/ripplo`}async function ef(e,t){if(t!=null){let r=e.find(n=>n.id===t);return r==null&&(process.stdout.write(`Unknown project id: ${t}
|
|
379
412
|
`),process.exit(1)),process.stdout.write(`Using project: ${r.name} (${r.id})
|
|
380
413
|
`),r.id}if(e.length===1){let r=e[0];if(r==null)throw new Error("unreachable");return process.stdout.write(`Using project: ${r.name} (${r.id})
|
|
381
|
-
`),r.id}return
|
|
382
|
-
`),process.exit(1)),t):
|
|
383
|
-
`),process.exit(1)}return e}return
|
|
414
|
+
`),r.id}return ui({choices:e.map(r=>({name:r.name,value:r.id})),message:"Select a project"})}async function tf(e,t){return t!=null?(t.trim().length===0&&(process.stdout.write(`--env must not be empty
|
|
415
|
+
`),process.exit(1)),t):nf(e)}async function rf(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
|
|
416
|
+
`),process.exit(1)}return e}return of()}async function nf(e){let t=He.join(e,".ripplo"),r=Xr.find(o=>Fe.existsSync(He.resolve(t,o))),n=await ui({choices:[...Xr.map(o=>({name:r===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:r??Xr[0],message:"Which env file should ripplo write RIPPLO_APP_URL, RIPPLO_ENGINE_URL, RIPPLO_WEBHOOK_SECRET, and ENABLE_RIPPLO_TESTING to?"});return n!=="__custom__"?n:di({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function of(){return di({default:"http://localhost:3000",message:"Where does your dev server run? (RIPPLO_APP_URL)",validate:e=>{try{return new URL(e),!0}catch{return"must be a valid URL"}}})}function sf({appUrl:e,engineUrl:t,filePath:r}){Fe.mkdirSync(He.dirname(r),{recursive:!0});let n=Fe.existsSync(r)?Fe.readFileSync(r,"utf8"):"",o=[];if(/^RIPPLO_APP_URL=/m.test(n)||o.push(`RIPPLO_APP_URL=${e}`),/^RIPPLO_ENGINE_URL=/m.test(n)||o.push(`RIPPLO_ENGINE_URL=${t}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(n)||o.push(`RIPPLO_WEBHOOK_SECRET=${Ar()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
|
|
384
417
|
`)?"":`
|
|
385
418
|
`;Fe.writeFileSync(r,`${n}${i}${o.join(`
|
|
386
419
|
`)}
|
|
387
|
-
`)}import
|
|
420
|
+
`)}import Zr from"fs";import mi from"path";import*as fi from"remeda";import af from"path";import{graphql as lf}from"gql.tada";var cf=lf(`
|
|
388
421
|
mutation AutoScopeAddDirty($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
389
422
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
390
423
|
id
|
|
391
424
|
}
|
|
392
425
|
}
|
|
393
|
-
`);async function
|
|
394
|
-
`).map(r=>r.slice(3).trim()).filter(r=>/^\.ripplo\/tests\/[^/]+\.ts$/.test(r)).map(r=>
|
|
426
|
+
`);async function Ht({compileResult:e,cwd:t}){if(!T(t))return{addedSlugs:[]};let r=df(t);if(r.length===0)return{addedSlugs:[]};let n=new Set(e.result.tests.map(c=>c.slug)),o=r.filter(c=>n.has(c));if(o.length===0)return{addedSlugs:[]};let i;try{i=L(t)}catch{return{addedSlugs:[]}}if(await it({compiled:e.result,config:i,cwd:t}).catch(c=>(y.warn("auto-sync failed: %s",c instanceof Error?c.message:String(c)),null))==null)return{addedSlugs:[]};let a=await g({config:i,document:cf,variables:{cwd:i.cwd,projectId:i.projectId,workflowSlugs:o}}).catch(c=>(y.warn("auto-scope failed: %s",c instanceof Error?c.message:String(c)),null));return a==null?{addedSlugs:[]}:(a.addDirtyTestsToScope??[]).length===0?{addedSlugs:[]}:{addedSlugs:o}}function df(e){let t;try{t=q(["status","--porcelain","--",".ripplo/tests"],e)}catch{return[]}return t.split(`
|
|
427
|
+
`).map(r=>r.slice(3).trim()).filter(r=>/^\.ripplo\/tests\/[^/]+\.ts$/.test(r)).map(r=>af.basename(r,".ts")).filter(r=>r!=="index")}async function gi(e){let{ids:t,requireImplemented:r}=e,n=process.cwd(),o=await C(n);o.ok||(process.stderr.write(`Compilation failed: ${o.error}
|
|
395
428
|
`),process.stderr.write(`${w("create","DSL authoring + lint rules")}
|
|
396
|
-
`),process.exit(1)),await ae({cwd:n,result:o.result});let i=De(o.result),s=t.length===0?i.diagnostics:i.diagnostics.filter(
|
|
397
|
-
${
|
|
398
|
-
`),
|
|
399
|
-
`),process.stderr.write(` ${
|
|
400
|
-
`)})});let l=new Set(o.builder.getUnimplemented().tests),c=r.filter(
|
|
429
|
+
`),process.exit(1)),await ae({cwd:n,result:o.result});let i=De(o.result),s=t.length===0?i.diagnostics:i.diagnostics.filter(f=>t.includes(f.test)),a=fi.groupBy(s,f=>f.test);Object.entries(a).forEach(([f,W])=>{process.stderr.write(`
|
|
430
|
+
${f}
|
|
431
|
+
`),W.forEach($=>{let V=$.step==null?"":` at "${$.step}"`;process.stderr.write(` [${$.rule}]${V}
|
|
432
|
+
`),process.stderr.write(` ${$.message}
|
|
433
|
+
`)})});let l=new Set(o.builder.getUnimplemented().tests),c=r.filter(f=>l.has(f));c.length>0&&(process.stderr.write(`
|
|
401
434
|
not-implemented
|
|
402
|
-
`),c.forEach(
|
|
403
|
-
`)}));let p=
|
|
435
|
+
`),c.forEach(f=>{process.stderr.write(` ${f} is still marked .notImplemented()
|
|
436
|
+
`)}));let p=ff(n);p.length>0&&(process.stderr.write(`
|
|
404
437
|
preconditions
|
|
405
|
-
`),p.forEach(
|
|
406
|
-
`),process.stderr.write(` ${
|
|
438
|
+
`),p.forEach(f=>{process.stderr.write(` [hardcoded-identifier] ${f.file}
|
|
439
|
+
`),process.stderr.write(` ${f.message}
|
|
407
440
|
`)}));let m=s.length+c.length+p.length;m>0&&(process.stderr.write(`
|
|
408
441
|
${String(m)} error(s)
|
|
409
|
-
`),
|
|
410
|
-
`);let{addedSlugs:
|
|
411
|
-
`)}function
|
|
412
|
-
`))}var
|
|
442
|
+
`),uf({diagnostics:s,hardcodeWarnings:p,stillStubbed:c}),process.exit(1));let k=o.result.tests.length,j=t.length===0?"all tests":`${String(t.length)} test(s)`;process.stdout.write(`\u2713 ripplo lint: ${j} passed (${String(k)} compiled, lockfile written)
|
|
443
|
+
`);let{addedSlugs:N}=await Ht({compileResult:o,cwd:n});N.length>0&&process.stdout.write(`\u2713 Auto-scoped ${N.join(", ")} (dirty tests).
|
|
444
|
+
`)}function uf({diagnostics:e,hardcodeWarnings:t,stillStubbed:r}){let n=new Set;(e.length>0||r.length>0)&&n.add("create"),t.length>0&&n.add("explore"),n.forEach(o=>process.stderr.write(`${w(o)}
|
|
445
|
+
`))}var pf=/['"`][\w.+-]+@[\w.-]+\.[a-z]{2,}['"`]/i,mf=/['"`][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}['"`]/i;function ff(e){let t=mi.join(e,".ripplo","preconditions");return Zr.existsSync(t)?Zr.readdirSync(t).filter(n=>n.endsWith(".ts")).flatMap(n=>gf(mi.join(t,n))):[]}function gf(e){let t=Zr.readFileSync(e,"utf8"),r=t.includes("ctx.uniqueEmail")||t.includes("ctx.uniqueId"),n=[],o=pf.exec(t);o!=null&&!r&&n.push({file:e,message:`hardcoded email literal ${o[0]} \u2014 use ctx.uniqueEmail() so parallel runs don't collide`});let i=mf.exec(t);return i!=null&&!r&&n.push({file:e,message:`hardcoded UUID-like literal ${i[0]} \u2014 use ctx.uniqueId() so parallel runs don't collide`}),n}import{graphql as yf}from"gql.tada";var hf=yf(`
|
|
413
446
|
query ProjectsList {
|
|
414
447
|
projects {
|
|
415
448
|
id
|
|
416
449
|
name
|
|
417
450
|
}
|
|
418
451
|
}
|
|
419
|
-
`);async function
|
|
420
|
-
`)}async function
|
|
452
|
+
`);async function yi(){let e=Z();e==null&&(process.stderr.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let t=Q(),n=((await g({config:be({serverUrl:t,token:e}),document:hf,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
|
|
453
|
+
`)}async function hi(e){let t=ue();await pe(t);let{results:r,summary:n}=await Vt({config:t,ids:e});process.stdout.write(`${n}
|
|
421
454
|
`);let o=r.some(i=>i.summary.includes("FAILED")||i.summary.includes("ERROR"));o&&(process.stderr.write(`Artifacts in .ripplo/debug/<runId>/ \u2014 read before re-running.
|
|
422
455
|
`),process.stderr.write(`${w("debug")}
|
|
423
|
-
`)),process.exit(o?1:0)}async function
|
|
456
|
+
`)),process.exit(o?1:0)}async function wi(){let e=process.cwd(),t=L(e);try{let r=await xe(t.cwd,t),n=r.compiled.tests.length,o=Object.keys(r.compiled.preconditions).length;process.stdout.write(`synced ${String(n)} tests, ${String(o)} preconditions to dev session ${r.devSessionId}
|
|
424
457
|
`)}catch(r){let n=r instanceof Error?r.message:String(r);process.stderr.write(`ripplo sync failed: ${n}
|
|
425
458
|
`),process.stderr.write(`${w("setup","verify auth + server reachability")}
|
|
426
|
-
`),process.exit(1)}}import
|
|
459
|
+
`),process.exit(1)}}import Wt from"fs";import{createClient as Pf}from"graphql-sse";import{graphql as Bt}from"gql.tada";import{print as vi}from"graphql";import{availableParallelism as wf}from"os";import bf from"p-limit";var vf=Math.max(1,Math.min(10,Math.floor(wf()/2))),kf=bf(vf),en=new Set;async function bi(e){if(!en.has(e.runId)){en.add(e.runId);try{await kf(()=>Sf(e))}finally{en.delete(e.runId)}}}async function Sf({config:e,cwd:t,runId:r,workflowSlug:n}){let o=await Ye({headed:!1});try{await gr({maxRuns:100});let i=t!=null&&n!=null?await Rf({cwd:t,workflowSlug:n}):void 0;await Or({baseUrl:e.appUrl,browser:o,config:e,headed:!1,preconditionNames:i,runId:r,webhookSecret:e.webhookSecret})}catch(i){y.error(i,"Runner error")}finally{await o.close()}}async function Rf({cwd:e,workflowSlug:t}){let r=await C(e);if(!r.ok)return;let n=r.result.tests.find(o=>o.slug===t);if(n!=null)return n.preconditions}var xf=15e3,Cf=Bt(`
|
|
427
460
|
subscription RunRequestedWatch($devSessionId: String!) {
|
|
428
461
|
runRequested(devSessionId: $devSessionId) {
|
|
429
462
|
runId
|
|
@@ -431,30 +464,30 @@ ${String(m)} error(s)
|
|
|
431
464
|
workflowSlug
|
|
432
465
|
}
|
|
433
466
|
}
|
|
434
|
-
`),
|
|
467
|
+
`),Ef=Bt(`
|
|
435
468
|
subscription HooksPausedWatch($projectId: String!) {
|
|
436
469
|
hooksPausedRequested(projectId: $projectId) {
|
|
437
470
|
paused
|
|
438
471
|
projectId
|
|
439
472
|
}
|
|
440
473
|
}
|
|
441
|
-
`),Af=
|
|
474
|
+
`),Af=Bt(`
|
|
442
475
|
mutation HeartbeatDevSessionWatch($id: String!) {
|
|
443
476
|
heartbeatDevSession(id: $id) {
|
|
444
477
|
id
|
|
445
478
|
}
|
|
446
479
|
}
|
|
447
|
-
`),
|
|
480
|
+
`),If=Bt(`
|
|
448
481
|
mutation EndDevSessionWatch($projectId: String!, $cwd: String!) {
|
|
449
482
|
endDevSession(projectId: $projectId, cwd: $cwd) {
|
|
450
483
|
id
|
|
451
484
|
}
|
|
452
485
|
}
|
|
453
|
-
`);async function
|
|
454
|
-
`),process.exit(1));let n=await
|
|
455
|
-
`);let p=async()=>{c(),a(),l(),await Promise.race([g({config:e,document:
|
|
456
|
-
`),
|
|
457
|
-
`),process.exit(1)}}var
|
|
486
|
+
`);async function ki(){Ff();let e=Nf(process.cwd()),t=e.cwd,r=await C(t);r.ok||(process.stderr.write(`ripplo: DSL compilation failed: ${r.error}
|
|
487
|
+
`),process.exit(1));let n=await Lf(()=>it({compiled:r.result,config:e,cwd:t}));Si({cwd:t,paused:n.hooksPaused});let o=Ut(t);o!=null&&y.info("watching branch %s in %s",o,t);let i=Mo(t),s=Pf({headers:{Authorization:`Bearer ${e.token}`},retryAttempts:1/0,url:`${e.ripploServerUrl}/graphql`}),a=Tf({config:e,cwd:t,devSessionId:n.devSessionId,sseClient:s}),l=$f({cwd:t,projectId:e.projectId,sseClient:s}),c=jf({config:e,devSessionId:n.devSessionId});process.stdout.write(`ripplo watch: ready as local executor for session ${n.devSessionId} (sync happens on each \`ripplo run\`; Ctrl-C to stop)
|
|
488
|
+
`);let p=async()=>{c(),a(),l(),await Promise.race([g({config:e,document:If,variables:{cwd:e.cwd,projectId:e.projectId}}).catch(()=>{}),new Promise(k=>setTimeout(k,3e3))]),i()},m=()=>{p().finally(()=>process.exit(0))};process.on("SIGINT",m),process.on("SIGTERM",m),process.on("SIGHUP",m),process.on("SIGBREAK",m),await new Promise(()=>{})}function Tf({config:e,cwd:t,devSessionId:r,sseClient:n}){return n.subscribe({query:vi(Cf),variables:{devSessionId:r}},{complete:()=>{},error:o=>{y.error(o,"runRequested subscription error")},next:o=>{let i=o.data?.runRequested;i!=null&&(process.stdout.write(`ripplo: run ${i.runId} (${i.workflowSlug})
|
|
489
|
+
`),bi({config:e,cwd:t,runId:i.runId,workflowId:i.workflowId,workflowSlug:i.workflowSlug}).catch(s=>{y.error({err:s,runId:i.runId,workflowSlug:i.workflowSlug},"run execution failed; watch continuing")}))}})}function Si({cwd:e,paused:t}){let r=Se(e);if(t&&!Wt.existsSync(r)){Wt.writeFileSync(r,"");return}!t&&Wt.existsSync(r)&&Wt.unlinkSync(r)}function $f({cwd:e,projectId:t,sseClient:r}){return r.subscribe({query:vi(Ef),variables:{projectId:t}},{complete:()=>{},error:n=>{y.error(n,"hooksPaused subscription error")},next:n=>{let o=n.data?.hooksPausedRequested?.paused;o!=null&&Si({cwd:e,paused:o})}})}function jf({config:e,devSessionId:t}){let r=setInterval(()=>{g({config:e,document:Af,variables:{id:t}}).catch(n=>{y.warn("heartbeat failed: %s",n instanceof Error?n.message:String(n))})},xf);return()=>{clearInterval(r)}}function Nf(e){try{return L(e)}catch(t){let r=t instanceof Error?t.message:String(t);process.stderr.write(`${r}
|
|
490
|
+
`),process.exit(1)}}var Of=1e3,Df=6e4;async function Lf(e){let t=Date.now()+Df,r;for(;Date.now()<t;){let n=await _f(e);if(n.ok)return n.value;if(!Uf(n.message))throw n.error;r=n.error,await Vf(Of)}throw r instanceof Error?r:new Error("server never became ready")}async function _f(e){try{return{ok:!0,value:await e()}}catch(t){let r=t instanceof Error?t.message:String(t);return{error:t,message:r,ok:!1}}}function Uf(e){return e.includes("status 502")||e.includes("ECONNREFUSED")||e.includes("Failed to connect")||e.includes("fetch failed")}function Vf(e){return new Promise(t=>setTimeout(t,e))}function Ff(){process.on("unhandledRejection",e=>{y.error({err:e},"unhandledRejection in watch; continuing")}),process.on("uncaughtException",e=>{y.error({err:e},"uncaughtException in watch; continuing")})}import{graphql as ct}from"gql.tada";var Hf=ct(`
|
|
458
491
|
query ScopeStatus($projectId: String!, $cwd: String!) {
|
|
459
492
|
project(id: $projectId) {
|
|
460
493
|
id
|
|
@@ -487,7 +520,7 @@ ${String(m)} error(s)
|
|
|
487
520
|
}
|
|
488
521
|
}
|
|
489
522
|
}
|
|
490
|
-
`),
|
|
523
|
+
`),Bf=ct(`
|
|
491
524
|
mutation ScopeAddDirtyTests($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
492
525
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
493
526
|
id
|
|
@@ -497,36 +530,36 @@ ${String(m)} error(s)
|
|
|
497
530
|
}
|
|
498
531
|
}
|
|
499
532
|
}
|
|
500
|
-
`),
|
|
533
|
+
`),Mf=ct(`
|
|
501
534
|
mutation ScopeLink($id: ID!, $workflowId: String!) {
|
|
502
535
|
linkScopeItem(id: $id, workflowId: $workflowId) {
|
|
503
536
|
id
|
|
504
537
|
}
|
|
505
538
|
}
|
|
506
|
-
`),
|
|
539
|
+
`),qf=ct(`
|
|
507
540
|
mutation ScopeRemoveMany($ids: [ID!]!) {
|
|
508
541
|
removeScopeItems(ids: $ids)
|
|
509
542
|
}
|
|
510
|
-
`);async function
|
|
543
|
+
`);async function Ri(e){let t=ue();await pe(t);let n=(await g({config:t,document:Hf,variables:{cwd:t.cwd,projectId:t.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
|
|
511
544
|
`);return}if(n.length===0){process.stdout.write("No scope items. Add via `ripplo scope add <test-ids..>` (variadic) or from the dashboard.\n");return}n.forEach(o=>{let i=o.workflow;if(i==null){process.stdout.write(` [intent] (${o.id}) ${o.label??""}
|
|
512
545
|
`);return}let s=i.spec==null?"stub":"implemented";process.stdout.write(` [${s}] (${o.id}) ${i.slug} \u2014 ${i.name}
|
|
513
|
-
`)})}async function
|
|
546
|
+
`)})}async function Pi({testIds:e}){let t=ue();await pe(t),await xe(t.cwd,t);let n=(await g({config:t,document:Bf,variables:{cwd:t.cwd,projectId:t.projectId,workflowSlugs:[...e]}})).addDirtyTestsToScope??[];if(n.length===0){process.stdout.write(`No scope items added.
|
|
514
547
|
`);return}let o=n.map(i=>i.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${String(n.length)} scope item(s): ${o}
|
|
515
|
-
`)}async function
|
|
516
|
-
`)}async function
|
|
517
|
-
`)}async function
|
|
548
|
+
`)}async function xi({id:e,testId:t}){let r=ue();await pe(r),await xe(r.cwd,r);let n=await zf({cfg:r,slug:t});await g({config:r,document:Mf,variables:{id:e,workflowId:n}}),process.stdout.write(`Linked scope item ${e} to ${t}
|
|
549
|
+
`)}async function Ci({ids:e}){let t=ue();await pe(t);let n=(await g({config:t,document:qf,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${String(n)} scope item(s)
|
|
550
|
+
`)}async function zf({cfg:e,slug:t}){let n=(await g({config:e,document:Wf,variables:{cwd:e.cwd,projectId:e.projectId,slug:t}})).project?.devSession?.workflows?.[0];return n==null&&(process.stderr.write(`No workflow found with id "${t}". Create a stub first via the testing DSL.
|
|
518
551
|
`),process.stderr.write(`${w("create")}
|
|
519
|
-
`),process.exit(1)),n.id}async function
|
|
552
|
+
`),process.exit(1)),n.id}async function Ei(e){let t=process.cwd(),r=await C(t);r.ok||(process.stderr.write(`Compilation failed: ${r.error}
|
|
520
553
|
`),process.exit(1));let{tests:n}=r.builder.getUnimplemented();if(e.format==="summary"){n.length>0&&process.stdout.write(`tests: ${n.join(", ")}
|
|
521
554
|
`);return}let o={tests:n.map(i=>({id:i,implemented:!1}))};process.stdout.write(`${JSON.stringify(o,null,2)}
|
|
522
|
-
`)}import{z as
|
|
523
|
-
`).filter(t=>t.length>0)}function
|
|
524
|
-
`).filter(r=>r.length>0)}import{createHash as
|
|
525
|
-
`).filter(n=>n.length>0).filter(n=>
|
|
526
|
-
`),hookEventName:"UserPromptSubmit"}}});import
|
|
527
|
-
${w("create","DSL authoring + lint rules")}`};await ae({cwd:n,result:o.result}),_e({cwd:n});let{diagnostics:i}=De(o.result);if(i.length===0){let{addedSlugs:a}=await
|
|
555
|
+
`)}import{z as Gf}from"zod";function E(e,t){let r=Gf.custom(n=>typeof n=="object"&&n!==null&&"hook_event_name"in n&&n.hook_event_name===e);return{event:e,run:async n=>await t(r.parse(n))??void 0}}import{createHash as qP}from"crypto";import Ai from"picomatch";function Kf(e){return q(["diff","--name-only","HEAD"],e).split(`
|
|
556
|
+
`).filter(t=>t.length>0)}function Ii({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ai([...r]),o=Ai([...t]);return Kf(e).filter(i=>n(i)&&!o(i))}function Mt(e,...t){return q(["diff","--name-only","HEAD","--",...t],e).split(`
|
|
557
|
+
`).filter(r=>r.length>0)}import{createHash as Jf}from"crypto";import{mkdirSync as Qf,readFileSync as Ti,writeFileSync as Yf}from"fs";import tn from"path";var Xf=[".ts",".tsx",".js",".jsx"];function We(e){let t=Jf("sha256");return t.update(q(["rev-parse","HEAD"],e)),t.update("\0"),t.update(q(["diff","HEAD"],e)),t.update("\0"),q(["ls-files","--others","--exclude-standard"],e).split(`
|
|
558
|
+
`).filter(n=>n.length>0).filter(n=>Xf.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{t.update(n),t.update("\0"),t.update(Zf(tn.join(e,n))),t.update("\0")}),t.digest("hex")}function Be(e,t){try{return Ti($i(e,t),"utf8").trim()}catch{return null}}function Me(e,t,r){let n=$i(e,t);Qf(tn.dirname(n),{recursive:!0}),Yf(n,r)}function $i(e,t){return tn.join(e,".ripplo",".local",`${t}.hash`)}function Zf(e){try{return Ti(e)}catch{return Buffer.alloc(0)}}function Re(){return{ignorePaths:ke,watchPaths:Br}}var ji=E("UserPromptSubmit",e=>{let{cwd:t}=e;if(!x(t)||!T(t))return;let r=We(t);if(Be(t,"coverage-nudge")===r)return;let{ignorePaths:n,watchPaths:o}=Re(),i=Ii({cwd:t,ignoreGlobs:n,watchGlobs:o});if(Me(t,"coverage-nudge",r),!(i.length===0||Mt(t,".ripplo/tests",".ripplo/preconditions",".ripplo/observers").length>0))return{hookSpecificOutput:{additionalContext:`SCOPE DRIFT: ${String(i.length)} app file(s) changed without .ripplo/tests updates. ${w("scope")}`,hookEventName:"UserPromptSubmit"}}});import qt from"fs";import eg from"os";import Ni from"path";var Oi=E("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!T(e.cwd))return;let t=tg();if(t==null)return;let r=qt.readFileSync(t,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(r))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/tests/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${w("explore")}`}}});function tg(){let e=Ni.join(eg.homedir(),".claude","plans");return qt.existsSync(e)?qt.readdirSync(e).filter(r=>r.endsWith(".md")).map(r=>Ni.join(e,r)).map(r=>({full:r,mtime:qt.statSync(r).mtimeMs})).sort((r,n)=>n.mtime-r.mtime)[0]?.full??null:null}var Di=E("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!x(e.cwd)||!T(e.cwd))return;let t=await C(e.cwd);if(!t.ok)return;let{tests:r}=t.builder.getUnimplemented(),n=['Plan must include "Tests to implement" with .ripplo/tests/<id>.ts per affected flow (ExitPlanMode blocks otherwise).'];return r.length>0&&n.push(`Existing stubs: ${r.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
|
|
559
|
+
`),hookEventName:"UserPromptSubmit"}}});import rg from"path";import Li from"picomatch";import{z as _i}from"zod";var ng=_i.looseObject({file_path:_i.string()}),Ui=E("PostToolUse",async e=>{let t=ng.safeParse(e.tool_input);if(!t.success)return;let r=t.data.file_path,{cwd:n}=e;if(!x(n)||!T(n))return;let o=rg.relative(n,r);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:s}=Re(),a=Li([...s]),l=Li([...i]);if(!a(o)||l(o))return;let c=await C(n);if(!c.ok)return;let{tests:p}=c.builder.getUnimplemented();if(p.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: .notImplemented() stubs still present \u2014 tests: ${p.join(", ")}`,hookEventName:"PostToolUse"}}});import{z as Vi}from"zod";var og=Vi.looseObject({file_path:Vi.string()}),Fi=E("PostToolUse",async e=>{let t=og.safeParse(e.tool_input);if(!t.success)return;let r=t.data.file_path;if(!/\/\.ripplo\/.*\.ts$/.test(r))return;let{cwd:n}=e;if(!x(n)||!T(n))return;let o=await C(n);if(!o.ok)return{decision:"block",reason:`Compilation failed: ${o.error}
|
|
560
|
+
${w("create","DSL authoring + lint rules")}`};await ae({cwd:n,result:o.result}),_e({cwd:n});let{diagnostics:i}=De(o.result);if(i.length===0){let{addedSlugs:a}=await Ht({compileResult:o,cwd:n});return a.length===0?void 0:{hookSpecificOutput:{additionalContext:`Auto-scoped ${a.join(", ")} (dirty tests).`,hookEventName:"PostToolUse"}}}return{decision:"block",reason:`${i.map(a=>{let l=a.step==null?"":` at "${a.step}"`;return` [${a.rule}]${l} ${a.test}: ${a.message}`}).join(`
|
|
528
561
|
`)}
|
|
529
|
-
${w("create")}`}});import{z as
|
|
562
|
+
${w("create")}`}});import{z as Hi}from"zod";var ig=Hi.looseObject({command:Hi.string()}),sg=/\bripplo\s+hooks\s+pause\b/,Wi=E("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=ig.safeParse(e.tool_input);if(!t.success||!sg.test(t.data.command))return;let{cwd:r}=e;if(x(r))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"`ripplo hooks pause` is a human-only escape hatch \u2014 agents can't bypass Ripplo guardrails on their own. If watch genuinely can't start (auth, server down, intentional offline work), surface the blocker to the user and ask them to run `npx ripplo hooks pause` themselves from their terminal."}}});import{parse as ug}from"shell-quote";import{z as qi}from"zod";import{existsSync as ag,mkdirSync as lg,rmSync as Gx,writeFileSync as cg}from"fs";import rn from"path";function Bi(e,t,r){let n=Mi(e,t,r);lg(rn.dirname(n),{recursive:!0}),cg(n,"")}function zt(e,t,r){return ag(Mi(e,t,r))}function dg(e,t){return rn.join(e,".ripplo",".local","skills-loaded",t)}function Mi(e,t,r){return rn.join(dg(e,t),r)}var pg=qi.looseObject({command:qi.string()}),mg="debug",fg=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),Gi=E("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=pg.safeParse(e.tool_input);if(!t.success)return;let r=gg(t.data.command);if(!yg(r))return;let{cwd:n}=e;if(!x(n)||!T(n))return;let o=wg(r);if(o!=null)return zi(`Don't pipe \`ripplo run\` through \`${o}\` \u2014 buffering filters hold all stdout until EOF, hiding live progress; if you kill the pipeline mid-run the run is orphaned on the server. Redirect to a file instead: \`ripplo run <id> > /tmp/run.log 2>&1\` and Read the file, or use \`run_in_background: true\`.`);if(!zt(n,e.session_id,mg))return zi("Running `ripplo run` requires the `/ripplo:debug` skill loaded first. Load `/ripplo:debug` then retry \u2014 it carries the artifact-read order, the 3-strike rule, and the no-grep-piping guidance for run failures.")});function gg(e){try{return ug(e)}catch{return[]}}function yg(e){return e.some((t,r)=>t==="ripplo"&&e[r+1]==="run")}function hg(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function wg(e){let t=e.find((r,n)=>{let o=e[n-1];return o!=null&&hg(o)&&typeof r=="string"&&fg.has(r)});return typeof t=="string"?t:null}function zi(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import bg from"path";import{z as Ki}from"zod";var vg=new Set(["Edit","Write","NotebookEdit"]),kg=Ki.looseObject({file_path:Ki.string()}),Sg="create",Ji=E("PreToolUse",e=>{if(!vg.has(e.tool_name))return;let t=kg.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!x(r)||!T(r))return;let n=bg.relative(r,t.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!zt(r,e.session_id,Sg))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Editing \`.ripplo/\` files (${n}) requires the \`/ripplo:create\` skill loaded first. Load \`/ripplo:create\` then retry \u2014 it carries the DSL builder shape, lint rules, and the parallelization guidance you'll need.`}}});import Rg from"path";import Qi from"picomatch";import{graphql as Pg}from"gql.tada";import{z as Yi}from"zod";var xg=new Set(["Edit","Write","NotebookEdit"]),Cg=Yi.looseObject({file_path:Yi.string()}),Eg=Pg(`
|
|
530
563
|
query PreEditScopeGate($projectId: String!, $cwd: String!) {
|
|
531
564
|
project(id: $projectId) {
|
|
532
565
|
id
|
|
@@ -538,7 +571,7 @@ ${w("create")}`}});import{z as Mi}from"zod";var sg=Mi.looseObject({command:Mi.st
|
|
|
538
571
|
}
|
|
539
572
|
}
|
|
540
573
|
}
|
|
541
|
-
`),
|
|
574
|
+
`),Xi=E("PreToolUse",async e=>{if(!xg.has(e.tool_name))return;let t=Cg.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!x(r)||!T(r))return;let n=Rg.relative(r,t.data.file_path);if(!(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!Ag(n)||await Ig(r)))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Scope empty; edit touches app code (${n}). Stub a test or \`scope add\` an existing one before proceeding \u2014 or acknowledge "no user-facing behavior" if pure refactor. Hook re-fires until scope is populated (or hooks paused via the web UI). ${Oo(["scope","create"])}`}}});function Ag(e){let{ignorePaths:t,watchPaths:r}=Re(),n=Qi([...r]),o=Qi([...t]);return n(e)&&!o(e)}async function Ig(e){let t;try{t=L(e)}catch{return!0}let r=await g({config:t,document:Eg,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null);return r==null?!0:(r.project?.devSession?.scopeItems??[]).length>0}import Tg from"fs";import $g from"path";import{z as Zi}from"zod";var jg=new Set(["Edit","Write","NotebookEdit"]),Ng=Zi.looseObject({file_path:Zi.string()}),es=E("PreToolUse",e=>{if(!jg.has(e.tool_name))return;let t=Ng.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!x(r))return;let n=$g.relative(r,t.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!Tg.existsSync(Se(r))&&!Ve(r))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo watch\` is not running, so this edit to \`${n}\` won't sync to the server and dev-mode guardrails won't fire. Run \`/ripplo:start\` (or spawn \`npx ripplo watch\` directly via Bash with run_in_background), then retry. The app's dev server also needs to be running \u2014 \`npx ripplo doctor\` reports both. If watch can't start (auth, server down, intentional offline work), the user can run \`npx ripplo hooks pause\` to bypass this gate; \`npx ripplo hooks resume\` re-enables it. ${w("start")}`}}});import{graphql as Og}from"gql.tada";var Dg=Og(`
|
|
542
575
|
query ScopeReminder($projectId: String!, $cwd: String!) {
|
|
543
576
|
project(id: $projectId) {
|
|
544
577
|
id
|
|
@@ -555,9 +588,9 @@ ${w("create")}`}});import{z as Mi}from"zod";var sg=Mi.looseObject({command:Mi.st
|
|
|
555
588
|
}
|
|
556
589
|
}
|
|
557
590
|
}
|
|
558
|
-
`),
|
|
591
|
+
`),ts=E("UserPromptSubmit",async e=>{let{cwd:t}=e;if(!x(t)||!T(t))return;let r=We(t);if(Be(t,"scope-reminder")===r)return;let n;try{n=L(t)}catch{return}let o=await g({config:n,document:Dg,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);if(o==null)return;Me(t,"scope-reminder",r);let i=o.project?.devSession?.scopeItems??[];return{hookSpecificOutput:{additionalContext:i.length===0?`RIPPLO SCOPE: empty. ${w("scope")}`:`RIPPLO SCOPE (${String(i.length)}):
|
|
559
592
|
${i.map(a=>{let l=a.workflow;return l==null?` [intent] (${a.id}) ${a.label??""}`:` [${l.spec==null?"stub":"implemented"}] (${a.id}) ${l.slug}`}).join(`
|
|
560
|
-
`)}`,hookEventName:"UserPromptSubmit"}}});var
|
|
593
|
+
`)}`,hookEventName:"UserPromptSubmit"}}});var Lg="# Ripplo \u2014 always-on session context\n\nEvery user-facing change in this repo ships with a deterministic, backend-aware test that proves it works end-to-end.\n\n## Load the right skill before acting \u2014 this is critical\n\nThe skills below carry the procedural detail you need to do Ripplo work correctly: precondition isolation, observer budget tiers, coverage IDs, scope discipline, artifact-read order on failures, parallelization patterns. **None of that is reproduced in this preamble** \u2014 only the always-on guardrails are. If you act on a Ripplo task without loading the matching skill, you will skip rules that exist to prevent specific past failure modes (cross-run data leakage, \"passing\" tests that never asserted backend state, cleared coverage gates without proof, etc.).\n\nMatch the task to a skill from the triage table; if unsure between two, load both. If completely unsure, load `/ripplo:explore` or `/ripplo:create`. Loading is cheap; acting under-informed is not.\n\n## Skill triage\n\n- Load `/ripplo:setup` skill for instructions on initializing Ripplo in a project; `ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:explore` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\n- Load `/ripplo:scope` skill for instructions on managing the working set of tests this session is responsible for.\n- Load `/ripplo:create` skill for instructions on authoring a single test spec for a user flow.\n- Load `/ripplo:run` skill for instructions on executing existing tests.\n- Load `/ripplo:debug` skill for instructions on a failed run; read artifacts in `.ripplo/debug/<runId>/` before re-running.\n- Load `/ripplo:flake-detect` skill for instructions on intermittent failures; parallelizes N runs (only use with evidence of flake).\n\n## Universal rules\n\n- **Two background processes must be running before feature work.** (1) The app's dev server, so the app is reachable; (2) `npx ripplo watch` (run `/ripplo:start` to spawn it, or invoke it directly via Bash with `run_in_background`), so the dev session is live. Without watch, scope/coverage hooks don't arm and `ripplo run` refuses to dispatch. Without the dev server, runs fail when they try to hit the app. `npx ripplo doctor` reports both.\n- **Two funnels.** Definitions \u2192 `createRipplo({ preconditions, observers, tests })` in `.ripplo/index.ts`. Implementations \u2192 `createEngine(ripplo, { preconditions, observers })` in your app server's `test/engine.ts`. Never call either elsewhere. TS enforces exhaustiveness across both.\n- **\"Done\" = app code delivers the behavior AND a passing test proves it.** Both halves. Shipping without a test isn't done; writing a test against broken UI/API isn't done.\n- **Scope is your job.** For any non-trivial change, enumerate every flow it could affect and either `scope add` existing tests or stub `.notImplemented()` tests. Don't wait to be told.\n- **`scope remove` is never for size/effort.** Valid only when an item is genuinely out of scope (wrong flow, duplicate, user said \"not this session,\" feature cut). \"Too many stubs\" \u2192 parallelize with subagents. Don't present \"implement vs. remove\" as a neutral A/B.\n- **Stub gates are not a question.** When `stop-enforce` blocks on `.notImplemented()` stubs, implement them \u2014 don't ask the user \"implement or defer?\", don't propose pausing hooks as option B. The fix isn't done until the test is. New scaffolding (precondition, observer, engine impl) is in-scope work, not follow-up.\n- **`uiOnly: true` is not a stub.** Observer wiring is in-scope for mutation flows; don't use it to silence observer lint.\n- **Never weaken a test to make it pass.** No `contains`/regex for exact text, no removed assertions, no fabricated locators. App lacks an accessible name \u2192 add one to the app, don't fall back to `testId()`. App bug \u2192 report with evidence.\n- **Artifacts first, re-run last.** Failed runs produce DOM, a11y tree, console, network, screenshots under `.ripplo/debug/<runId>/`. Read them. Never pipe `ripplo run` through `grep`/`tail`/`head`. Form a hypothesis citing an artifact line, make ONE change, re-run once.\n- **3-strike rule.** Same failure after 3 targeted fixes \u2192 stop and report. Repeated failure on the same step almost always means the diagnosis is wrong.\n- **`.ripplo/ripplo.lock` is committed, never hand-edited.** `ripplo lint` / `ripplo compile` regenerates it. Pre-commit runs `ripplo compile --check`.\n- **Scratch files live in `.ripplo/.local/`.** Never loose in `.ripplo/`.\n- **Worktrees are self-contained.** Each worktree has its own `.ripplo/` checkout, DevSession, scope, and debug artifacts. Auth and projectId are shared globally. Env files (typically gitignored) won't carry over to a fresh worktree \u2014 copy from main or point at a shared file. **If sibling worktrees run dev servers on different ports, the worktree's env file MUST update both `RIPPLO_APP_URL` and `RIPPLO_ENGINE_URL` to match the port that worktree's dev server is bound to** (e.g. main on `:3000`, this worktree on `:3001` \u2192 set `RIPPLO_APP_URL=http://localhost:3001` and `RIPPLO_ENGINE_URL=http://localhost:3001/ripplo` in the worktree's env file). Mismatched ports = `npx ripplo watch` (whether started via `/ripplo:start` or manually) talks to the wrong server, runs silently fail or hit the sibling worktree's app.\n- **DSL locators are semantic.** No `css` / `placeholder` / `waitFor` / `group`.\n- **New backend assertion?** Declare in `.ripplo/observers/index.ts`, implement in your app server's `engine.ts` (TS flags the missing impl).\n\n## Key files\n\n- `.ripplo/index.ts` \u2014 `createRipplo` call.\n- `.ripplo/{preconditions,observers,tests}/index.ts` \u2014 registry aggregators.\n- `.ripplo/project.json` \u2014 project id + env-file pointers.\n- `<app>/src/test/engine.ts` \u2014 single impl funnel via `createEngine`.\n- `packages/testing/README.md` (or the npm page for `@ripplo/testing`) \u2014 DSL reference.\n",rs=E("SessionStart",e=>{if(x(e.cwd))return{hookSpecificOutput:{additionalContext:Lg,hookEventName:"SessionStart"}}});import{execFileSync as _g}from"child_process";import Ug from"path";import ns from"process";import{graphql as Vg}from"gql.tada";import{z as Gt}from"zod";var Fg=Vg(`
|
|
561
594
|
query ScopeEnforce($projectId: String!, $cwd: String!) {
|
|
562
595
|
project(id: $projectId) {
|
|
563
596
|
id
|
|
@@ -576,14 +609,14 @@ ${i.map(a=>{let l=a.workflow;return l==null?` [intent] (${a.id}) ${a.label??""}
|
|
|
576
609
|
}
|
|
577
610
|
}
|
|
578
611
|
}
|
|
579
|
-
`),
|
|
612
|
+
`),is=E("Stop",async e=>{let{cwd:t}=e;if(!x(t)||!T(t))return;let r=We(t),n=Be(t,"stop-enforce")===r;if(n&&!e.stop_hook_active)return;let o=Hg(t),i=await Wg(t,o);if(Me(t,"stop-enforce",r),i.length!==0)return n&&e.stop_hook_active?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts; agent appears stuck. Errors:
|
|
580
613
|
${i.join(`
|
|
581
614
|
|
|
582
615
|
`)}`}:{decision:"block",reason:i.join(`
|
|
583
616
|
|
|
584
|
-
`)}});function
|
|
617
|
+
`)}});function Hg(e){return Mt(e,".ripplo/tests").filter(t=>t.endsWith(".ts")).map(t=>Ug.basename(t,".ts")).filter(t=>t!=="index")}async function Wg(e,t){let r=await C(e);if(!r.ok)return[`--- Compilation failed ---
|
|
585
618
|
${r.error}
|
|
586
|
-
${w("create")}`];await ae({cwd:e,result:r.result}),_e({cwd:e});let n=await
|
|
619
|
+
${w("create")}`];await ae({cwd:e,result:r.result}),_e({cwd:e});let n=await Mg(e),o=[...new Set([...t,...n.scopedSlugsToRun])],i=qg(r,t),s=zg(r),a=Bg(e,r),l=Gg(e,o);return[i,s,a,l,n.error].filter(c=>c!=null)}function Bg(e,t){let{ignorePaths:r}=Re(),n=Lo({compileResult:t.result,cwd:e,ignorePaths:r});if(n.length===0)return null;let o=n.filter(a=>a.kind==="unacknowledged"),i=n.filter(a=>a.kind==="stale"),s=[];if(o.length>0){let a=o.map(l=>` ${l.id}`).join(`
|
|
587
620
|
`);s.push(`Unacknowledged interactions (add to a test's .coverage(...)):
|
|
588
621
|
${a}`)}if(i.length>0){let a=i.map(l=>` ${l.id}`).join(`
|
|
589
622
|
`);s.push(`Stale .coverage(...) claims (id no longer exists in source):
|
|
@@ -591,23 +624,23 @@ ${a}`)}return`--- Exhaustiveness ---
|
|
|
591
624
|
${s.join(`
|
|
592
625
|
|
|
593
626
|
`)}
|
|
594
|
-
${w("create")}`}async function
|
|
627
|
+
${w("create")}`}async function Mg(e){let t;try{t=L(e)}catch{return{error:null,scopedSlugsToRun:[]}}let n=(await g({config:t,document:Fg,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null))?.project?.devSession?.scopeItems??[],o=n.flatMap(a=>{let l=a.workflow;return l==null?[` [intent] ${a.label??"(no label)"} \u2014 stub a test for this flow`]:l.spec==null?[` [stub] ${l.slug} \u2014 implement \`${l.name}\``]:[]}),i=n.flatMap(a=>a.workflow?.spec==null?[]:[a.workflow.slug]);return{error:o.length===0?null:`--- Testing Scope ---
|
|
595
628
|
${o.join(`
|
|
596
629
|
`)}
|
|
597
|
-
${w("create")}`,scopedSlugsToRun:i}}function
|
|
630
|
+
${w("create")}`,scopedSlugsToRun:i}}function qg(e,t){let{diagnostics:r}=De(e.result),n=new Set(e.builder.getUnimplemented().tests),o=t.filter(a=>n.has(a));if(r.length===0&&o.length===0)return null;let i=r.map(a=>{let l=a.step==null?"":` at "${a.step}"`;return` [${a.rule}]${l} ${a.test}: ${a.message}`}),s=o.map(a=>` [not-implemented] ${a} is still marked .notImplemented()`);return`--- Ripplo Lint ---
|
|
598
631
|
${[...i,...s].join(`
|
|
599
632
|
`)}
|
|
600
|
-
${w("create")}`}function
|
|
633
|
+
${w("create")}`}function zg(e){let{tests:t}=e.builder.getUnimplemented();return t.length===0?null:`--- Unimplemented stubs ---
|
|
601
634
|
${t.join(", ")}
|
|
602
635
|
Implement the stub now. Do not ask the user "implement or defer?" \u2014 that framing is forbidden by /ripplo:create. New scaffolding (precondition, observer, engine impl) is in-scope, not follow-up.
|
|
603
|
-
${w("create")}`}function
|
|
636
|
+
${w("create")}`}function Gg(e,t){if(t.length===0)return null;let r=Jg(["run",...t],e);if(r.code===0)return null;let n=r.output.split(`
|
|
604
637
|
`).filter(o=>/FAILED/.test(o)).join(`
|
|
605
638
|
`);return n.length===0?null:`--- Ripplo Run Failures (${t.join(" ")}) ---
|
|
606
639
|
${n}
|
|
607
|
-
Artifacts: .ripplo/debug/<runId>/. ${w("debug")}`}var
|
|
608
|
-
`),process.exit(1));let r=await
|
|
609
|
-
`),process.exit(1)});function
|
|
610
|
-
`))}export{
|
|
640
|
+
Artifacts: .ripplo/debug/<runId>/. ${w("debug")}`}var Kg=Gt.object({status:Gt.number().nullish(),stderr:Gt.unknown().optional(),stdout:Gt.unknown().optional()});function os(e){return e instanceof Buffer?e.toString("utf8"):typeof e=="string"?e:""}function Jg(e,t){let r=ns.argv[1];if(r==null)return{code:1,output:""};try{return{code:0,output:_g(ns.execPath,[r,...e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}}catch(n){let o=Kg.safeParse(n);if(!o.success)return{code:1,output:""};let i=`${os(o.data.stdout)}${os(o.data.stderr)}`;return{code:o.data.status??1,output:i}}}import{z as ss}from"zod";var Qg=ss.looseObject({skill:ss.string()}),as=E("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=Qg.safeParse(e.tool_input);if(!t.success)return;let r=/^ripplo:(.+)$/.exec(t.data.skill);if(r==null)return;let n=r[1];n!=null&&x(e.cwd)&&Bi(e.cwd,e.session_id,n)});sy();dn(process.cwd());var ls={"coverage-nudge":ji,"exit-plan-gate":Oi,"plan-reminder":Di,"post-edit-flag-stubs":Ui,"post-edit-lint":Fi,"pre-bash-hooks-pause-gate":Wi,"pre-bash-run-gate":Gi,"pre-edit-ripplo-skill-gate":Ji,"pre-edit-scope-gate":Xi,"pre-edit-watch-gate":es,"scope-reminder":ts,"session-preamble":rs,"stop-enforce":is,"track-skill-load":as};async function Zg(){await Yg(Xg(process.argv)).scriptName("ripplo").version("0.4.14").command("watch","Watch for run requests and execute locally",()=>{},()=>ki()).command("auth <subcommand>","Manage authentication",iy).command("projects <subcommand>","Inspect Ripplo projects",oy).command("hooks <subcommand>","Pause or resume Ripplo hooks",ny).command("init","Scaffold .ripplo/ in this project",e=>e.option("project",{type:"string"}).option("env",{type:"string"}).option("app-url",{type:"string"}).option("engine-url",{type:"string"}),e=>pi({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests in parallel",e=>{let t=[];return e.positional("ids",{array:!0,default:t,describe:"Test ids to run (all if omitted)",type:"string"})},e=>hi(e.ids)).command("lint [ids..]","Compile and lint tests (all or specific ids)",e=>{let t=[];return e.positional("ids",{array:!0,default:t,describe:"Test ids to lint (all if omitted)",type:"string"}).option("require-implemented",{array:!0,default:t,describe:"Test ids that must not be .notImplemented() \u2014 fails if any still are",type:"string"})},e=>gi({ids:e.ids,requireImplemented:e["require-implemented"]})).command("flake-detect <id>","Run N times to detect flakiness",e=>e.positional("id",{demandOption:!0,type:"string"}).option("runs",{default:10,type:"number"}),e=>oi({id:e.id,runs:e.runs})).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>wi()).command("compile","Compile the DSL and write .ripplo/ripplo.lock",e=>e.option("check",{default:!1,describe:"Exit non-zero if the lockfile is missing or stale (does not write)",type:"boolean"}),e=>Do({check:e.check})).command("cover","Audit all coverage statements",()=>{},()=>Fo()).command("doctor","Check project health",()=>{},()=>ei()).command("status","Report unimplemented tests and preconditions",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>Ei({format:e.format})).command("scope <subcommand>","Manage testing scope",ry).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(ls),demandOption:!0,type:"string"}),e=>ey(e.name)).strict().help().parse()}async function ey(e){let t=ls[e];t==null&&(process.stderr.write(`Unknown hook: ${e}
|
|
641
|
+
`),process.exit(1));let r=await ty(),n=r.trim()===""?{}:JSON.parse(r),o=await t.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function ty(){return new Promise((e,t)=>{if(process.stdin.isTTY){e("");return}let r=[];process.stdin.on("data",n=>r.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(r).toString("utf8"))}),process.stdin.on("error",t)})}Zg().catch(e=>{process.stderr.write(`${e instanceof Error?e.message:String(e)}
|
|
642
|
+
`),process.exit(1)});function ry(e){return e.command("status","Print the current scope",t=>t.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),t=>Ri({format:t.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",t=>{let r=[];return t.positional("test-ids",{array:!0,default:r,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},t=>Pi({testIds:t["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",t=>t.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),t=>xi({id:t.id,testId:t["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",t=>{let r=[];return t.positional("ids",{array:!0,default:r,demandOption:!0,describe:"Scope item ids",type:"string"})},t=>Ci({ids:t.ids})).demandCommand(1)}function ny(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>ii()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>si()).demandCommand(1)}function oy(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>yi()).demandCommand(1)}function iy(e){return e.command("login","Authenticate via device flow",()=>{},()=>ao()).command("status","Show authentication status",()=>{},()=>lo()).command("logout","Remove the saved token",()=>{},()=>{co()}).demandCommand(1)}function sy(){let e=process.cwd(),t=Jt(e);t!=null&&t!==e&&(process.chdir(t),process.stderr.write(`ripplo: resolved .ripplo/ at ${t}
|
|
643
|
+
`))}export{Zg as main};
|
|
611
644
|
/*! Bundled license information:
|
|
612
645
|
|
|
613
646
|
mustache/mustache.mjs:
|