ripplo 0.7.25 → 0.7.26
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/{chunk-EVYYSTR7.js → chunk-5VRJXHWH.js} +1 -1
- package/dist/chunk-MSUK2UBM.js +164 -0
- package/dist/{chunk-BJVAPI4P.js → chunk-RLXQYG7H.js} +1 -1
- package/dist/chunk-V252IVSN.js +218 -0
- package/dist/daemon-G75H2VRS.js +92 -0
- package/dist/daemon-PTZWFEBG.js +2 -0
- package/dist/{daemon-tunnel-AG6FGNXC.js → daemon-tunnel-DYA5LIEG.js} +1 -1
- package/dist/index.js +201 -197
- package/package.json +6 -6
- package/dist/chunk-D7HOYI2S.js +0 -206
- package/dist/chunk-NXJP3Q4N.js +0 -167
- package/dist/daemon-C7TOJ236.js +0 -72
- package/dist/daemon-RVYMMUB5.js +0 -2
package/dist/index.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
2
|
+
import{A as Be,B as At,C as re,D as w,E as V,F as Ve,G as Qt,H as Xt,I as Yt,J as Zt,K as er,L as tr,M as rr,N as nr,O as he,P as or,Q as ir,R as dr,T as cr,V as ur,W as pr,X as mr,Y as fr,Z as gr,_ as hr,a as wt,aa as yr,b as B,c as vt,d as bt,e as P,f as qe,g as T,ga as ne,ha as kr,i as S,ia as Ge,j as c,k as Rt,ka as wr,l as R,m as Ct,ma as ze,n as xt,na as vr,o as Pt,oa as br,p as Et,q as E,r as $,s as $t,t as k,u as ge,x as O,y as jt,z as It}from"./chunk-V252IVSN.js";import{B as Wt,C as qt,D as Bt,E as Vt,F as Gt,G as zt,H as Jt,K as Kt,T as sr,V as ar,W as lr,b as z,c as St,e as U,i as _,j as Tt,l as Lt,m as Dt,n as _t,o as Ot,q as C,r as Nt,s as Ut,t as Ft,v as Mt,x as Ht}from"./chunk-MSUK2UBM.js";import{a as ct,b as ut,c as pt,d as m,e as L,g as fe,h as mt,i as D,j as ft,k as gt,l as We,m as ht,n as yt,o as kt}from"./chunk-5VRJXHWH.js";import"./chunk-IHSHBPJY.js";import"./chunk-7UWDMECF.js";import zd from"update-notifier";import Jd from"yargs";import{hideBin as Kd}from"yargs/helpers";function ye({current:e,latest:t}){return t==null?`ripplo v${e} (latest: unknown)`:t===e?`ripplo v${e} (latest)`:`ripplo v${e} (latest: v${t} \u2014 run \`npx ripplo update\`)`}function Sr(){return"Update available {currentVersion} \u2192 {latestVersion}\nRun `npx ripplo update`"}import{graphql as gi}from"gql.tada";import{exec as oi}from"child_process";import{createAuthClient as ri}from"better-auth/client";import{deviceAuthorizationClient as ni}from"better-auth/client/plugins";function Rr({baseURL:e}){return ri({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[ni()]})}import{err as Cr,ok as ii}from"neverthrow";var si=5e3,xr="ripplo-cli";async function Pr({onDeviceCode:e,url:t}){let r=t??$t().RIPPLO_SERVER_URL,n=Rr({baseURL:r}),o=await n.device.code({client_id:xr});if(o.error!=null)return Cr({description:o.error.error_description,kind:"oauth-device-code-failed"});let{device_code:i,user_code:s,verification_uri_complete:a}=o.data;return e({userCode:s,verificationUrl:a}),pi(a),(await ai({authClient:n,deviceCode:i})).map(d=>(ft({serverUrl:r,token:d}),d))}async function ai({authClient:e,deviceCode:t}){for(;;){await ci(si);let r=await e.device.token({client_id:xr,device_code:t,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(r.data?.access_token!=null)return ii(r.data.access_token);if(r.error==null)continue;if(!di(r.error.error))return Cr({code:r.error.error,description:r.error.error_description,kind:"oauth-authorization-failed"})}}var li=new Set(["authorization_pending","slow_down"]);function di(e){return li.has(e)}function ci(e){return new Promise(t=>{setTimeout(t,e)})}function ui(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function pi(e){let t=ui();oi(`${t} "${e}"`,()=>{})}function F({serverUrl:e,token:t}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:t,tunnelAuth:void 0,webhookSecret:""}}import mi from"fs";import fi from"path";function f(e){return mi.existsSync(fi.join(e,".ripplo"))}var hi=gi(`
|
|
3
3
|
query AuthViewer {
|
|
4
4
|
currentUser {
|
|
5
5
|
name
|
|
6
6
|
email
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
`);async function
|
|
10
|
-
`),process.stdout.write(`If it didn't open, visit: ${
|
|
11
|
-
`),process.stdout.write(`Verification code: ${
|
|
9
|
+
`);async function Er(){let e=$(),t=D(e);if(t!=null&&await yi(e,t))return;let n=(await Pr({url:e,onDeviceCode:a=>{process.stdout.write(`Opening your browser to finish sign-in.
|
|
10
|
+
`),process.stdout.write(`If it didn't open, visit: ${a.verificationUrl}
|
|
11
|
+
`),process.stdout.write(`Verification code: ${a.userCode}
|
|
12
12
|
|
|
13
13
|
`),process.stdout.write(`Waiting for approval...
|
|
14
|
-
`)}})).match(
|
|
15
|
-
`),process.exit(1)}),o=await
|
|
16
|
-
`),process.stdout.write(o.kind==="ok"?`${
|
|
17
|
-
`:`${
|
|
18
|
-
`),process.stdout.write(`Session saved to ${
|
|
19
|
-
`),i||(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"))}async function
|
|
20
|
-
`),!0):(
|
|
14
|
+
`)}})).match(a=>a,a=>{process.stderr.write(`${E(a)}
|
|
15
|
+
`),process.exit(1)}),o=await Je({serverUrl:e,token:n}),i=f(process.cwd()),s=i?"Welcome back":"Welcome to Ripplo";process.stdout.write(`
|
|
16
|
+
`),process.stdout.write(o.kind==="ok"?`${s}, ${ki(o.viewer)}.
|
|
17
|
+
`:`${s}.
|
|
18
|
+
`),process.stdout.write(`Session saved to ${We(e)}.
|
|
19
|
+
`),i||(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"))}async function yi(e,t){let r=await Je({serverUrl:e,token:t});return r.kind==="ok"?(process.stdout.write(`Already signed in as ${r.viewer.email}. Run \`npx ripplo auth logout\` to switch accounts.
|
|
20
|
+
`),!0):(r.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${r.message}). Your saved session may still be valid \u2014 fix connectivity and retry.
|
|
21
21
|
`),process.exit(1)),process.stdout.write(`Your session expired \u2014 signing you back in.
|
|
22
22
|
|
|
23
|
-
`),!1)}async function
|
|
24
|
-
`),process.exit(1)),
|
|
25
|
-
`),process.exit(1)),process.stdout.write(`Signed in as ${
|
|
26
|
-
`)}function
|
|
27
|
-
`);return}process.stdout.write(`Signed out. Removed ${
|
|
28
|
-
`)}async function
|
|
29
|
-
`),process.exit(1));let
|
|
30
|
-
`);return}process.stderr.write(`${
|
|
31
|
-
`),process.exit(1)}await
|
|
32
|
-
`)}import{graphql as
|
|
23
|
+
`),!1)}async function $r(){let e=$(),t=D(e);t==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login`.\n"),process.exit(1));let r=await Je({serverUrl:e,token:t});r.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${r.message}). The token may still be valid \u2014 check the server / network and retry.
|
|
24
|
+
`),process.exit(1)),r.kind==="rejected"&&(process.stdout.write("Token rejected by the server. Run `npx ripplo auth login` to sign in again.\n"),process.stdout.write(`(Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)
|
|
25
|
+
`),process.exit(1)),process.stdout.write(`Signed in as ${r.viewer.name} <${r.viewer.email}> (${e})
|
|
26
|
+
`)}function jr(){let e=$();if(!gt(e)){process.stdout.write(`Already signed out.
|
|
27
|
+
`);return}process.stdout.write(`Signed out. Removed ${We(e)}.
|
|
28
|
+
`)}async function Je({serverUrl:e,token:t}){try{let n=(await m({config:F({serverUrl:e,token:t}),document:hi,variables:void 0})).currentUser;return n==null?{kind:"rejected"}:{kind:"ok",viewer:{email:n.email,name:n.name}}}catch(r){return pt(r)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:r instanceof Error?r.message:String(r)}}}function ki(e){let t=e.name.trim().split(/\s+/)[0];return t!=null&&t.length>0?t:e.email}import{readFile as wi,writeFile as vi}from"fs/promises";import bi from"path";async function Ir(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${R(r.error)}
|
|
29
|
+
`),process.exit(1));let n=z(U,r.value),o=bi.join(t,T);if(e.check){let i=await wi(o,"utf8").catch(()=>null);if(i===n){process.stdout.write(`${Ct()}
|
|
30
|
+
`);return}process.stderr.write(`${xt(i==null?"missing":"stale")}
|
|
31
|
+
`),process.exit(1)}await vi(o,n),process.stdout.write(`${Pt()}
|
|
32
|
+
`)}import{graphql as Ar}from"gql.tada";function Ke(e){return e==null?"Max local concurrent runs: unknown (not signed in?)":`Max local concurrent runs: ${String(e)} (the daemon applies changes live)`}import{graphql as Si}from"gql.tada";var Ri=Si(`
|
|
33
33
|
query DevSessionCheckPreflight($projectId: String!, $cwd: String!) {
|
|
34
34
|
project(id: $projectId) {
|
|
35
35
|
id
|
|
@@ -38,26 +38,26 @@ import{$ as Tn,A as oe,B as Hr,C as ie,D as b,E as un,G as mn,H as fn,I as gn,J
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
`);function
|
|
41
|
+
`);function I(){return k(process.cwd()).match(e=>e,e=>{process.stderr.write(`${E(e)}
|
|
42
42
|
`),process.stderr.write(`${c("setup")}
|
|
43
|
-
`),process.exit(1)})}async function
|
|
44
|
-
`),process.exit(1))}var
|
|
43
|
+
`),process.exit(1)})}async function oe(e){(await m({config:e,document:Ri,variables:{cwd:e.cwd,projectId:e.projectId}})).project?.devSession==null&&(process.stderr.write("No active dev session. Start `npx ripplo daemon` 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(`${c("setup")}
|
|
44
|
+
`),process.exit(1))}var Ci=Ar(`
|
|
45
45
|
mutation CliUpdateMaxLocalConcurrentRuns($value: Int!) {
|
|
46
46
|
updateMaxLocalConcurrentRuns(value: $value) {
|
|
47
47
|
id
|
|
48
48
|
maxLocalConcurrentRuns
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
`),
|
|
51
|
+
`),xi=Ar(`
|
|
52
52
|
query CliMaxLocalConcurrentRuns {
|
|
53
53
|
currentUser {
|
|
54
54
|
id
|
|
55
55
|
maxLocalConcurrentRuns
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
`);async function
|
|
59
|
-
`);return}let
|
|
60
|
-
`)}import{graphql as
|
|
58
|
+
`);async function Tr({value:e}){let t=I();if(e==null){let n=await m({config:t,document:xi,variables:{}});process.stdout.write(`${Ke(n.currentUser?.maxLocalConcurrentRuns)}
|
|
59
|
+
`);return}let r=await m({config:t,document:Ci,variables:{value:e}});process.stdout.write(`${Ke(r.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
|
|
60
|
+
`)}import{graphql as Pi}from"gql.tada";var Ei=Pi(`
|
|
61
61
|
query DevSessionCheckHealth($projectId: String!, $cwd: String!) {
|
|
62
62
|
activeDevSessions(projectId: $projectId) {
|
|
63
63
|
id
|
|
@@ -71,47 +71,47 @@ import{$ as Tn,A as oe,B as Hr,C as ie,D as b,E as un,G as mn,H as fn,I as gn,J
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
`);function
|
|
75
|
-
`)}}}async function
|
|
76
|
-
${e.missing.map(
|
|
74
|
+
`);function Lr(e){switch(e.status){case"active":return e.gitMidOperation?"\u2713 Dev session: ripplo daemon is live (hooks auto-paused while git is mid-merge/rebase/cherry-pick \u2014 they resume when the operation ends)":"\u2713 Dev session: ripplo daemon is live for this directory";case"starting":return"! Dev session: the daemon 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 daemon output for auth or network errors.";case"missing":{let t="\u2717 Dev session: no daemon running for this directory. Start `npx ripplo daemon` as a background process (separate from your app's dev server). Without it, `npx ripplo run` refuses to dispatch and scope/coverage hooks won't arm.";return e.elsewhere.length===0?t:[t," A daemon is running for a different directory:",...$i(e.elsewhere)].join(`
|
|
75
|
+
`)}}}async function Dr(e){let t=ge(e),r=jt(e),n=k(e).unwrapOr(void 0);if(n==null)return{elsewhere:[],gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"};let o=await m({config:n,document:Ei,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);return o?.project?.devSession!=null?{elsewhere:[],gitMidOperation:r,status:"active",type:"dev-session"}:{elsewhere:(o?.activeDevSessions??[]).filter(a=>a.cwd!==n.cwd).map(a=>({branch:a.branch,cwd:a.cwd})),gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"}}function $i(e){return e.map(t=>{let r=t.branch==null?"":` (${t.branch})`;return` - ${t.cwd}${r}`})}function _r(e){switch(e.type){case"settings":return ji(e);case"env-files":return Ii(e);case"token":return Ai(e);case"dev-server":return Ti(e);case"dev-session":return Lr(e);case"preconditions":return Li(e);case"webhook-verification":return Di(e);case"preconditions-validation":return _i(e);case"workflows":return Oi(e);case"browser":return Ui(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 Fi(e);case"lockfile":return Mi(e);case"pre-commit-hook":return Hi(e);case"plugin-version":return Wi(e)}}function ji(e){return e.valid?"\u2713 Settings: project configured":`\u2717 Settings: missing fields: ${e.missingFields.join(", ")}`}function Ii(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
|
|
76
|
+
${e.missing.map(r=>` ${r}`).join(`
|
|
77
77
|
`)}
|
|
78
|
-
In a git worktree? Copy the env file from the main checkout, or symlink to a shared file outside the working tree.`}function
|
|
79
|
-
${
|
|
80
|
-
`)}`}function
|
|
81
|
-
${
|
|
82
|
-
`)}`}function
|
|
83
|
-
`+
|
|
84
|
-
`)}function
|
|
78
|
+
In a git worktree? Copy the env file from the main checkout, or symlink to a shared file outside the working tree.`}function Ai(e){switch(e.status){case"valid":return`\u2713 Auth: signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: not signed in. Run `npx ripplo auth login`.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"invalid":return"\u2717 Auth: saved token rejected by the server. Run `npx ripplo auth login` to sign in again.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"unreachable":return`! Auth: could not reach ${$()} to validate the token.`}}function Ti(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Start your dev server.`}function Li(e){return e.count===0?"! Entities: none defined":e.configured?e.endpointReachable===void 0?`! Entities: ${String(e.count)} defined (could not verify engine endpoint)`:`\u2713 Entities: ${String(e.count)} defined, engine configured`:`\u2717 Entities: ${String(e.count)} defined but RIPPLO_ENGINE_URL is not set in your env file`}function Di(e){return e.rejectsUnsigned?"\u2713 Webhook verification: unsigned requests rejected":"\u2717 Webhook verification: endpoint accepted an unsigned request \u2014 signature verification may be missing or misconfigured."}function _i(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: valid";let t=e.errors.map(r=>` - ${r.path===""?"":r.path+": "}${r.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
|
|
79
|
+
${t.join(`
|
|
80
|
+
`)}`}function Oi(e){if(e.total===0)return"! Tests: none defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let t=e.invalidWorkflows.map(r=>Ni(r));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
|
|
81
|
+
${t.join(`
|
|
82
|
+
`)}`}function Ni(e){let t=e.errors.map(r=>" - "+(r.path===""?"":r.path+": ")+r.message);return" "+e.name+`:
|
|
83
|
+
`+t.join(`
|
|
84
|
+
`)}function Ui(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run `npx playwright install chromium`."}function Fi(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 Mi(e){switch(e.status){case"match":return`\u2713 Lockfile: ${T} is up to date`;case"missing":return`\u2717 Lockfile: ${T} is missing \u2014 run \`npx ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${T} is out of date \u2014 run \`npx ripplo compile\` and commit it`}}function Hi(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 Wi(e){return e.installed===e.cliVersion?`\u2713 Claude plugin: v${e.installed} matches CLI`:`! Claude plugin: installed v${e.installed}, CLI v${e.cliVersion} \u2014 run \`npx ripplo update\` (or /plugin in Claude Code)`}import J from"fs";import Ji from"os";import Qe from"path";import{z as W}from"zod";import qi from"latest-version";import Or from"semver";async function ke(e){try{return await Promise.race([qi("ripplo"),new Promise(t=>setTimeout(()=>{t(void 0)},e))])}catch{return}}function Nr(e){return e.filter(t=>Or.valid(t)!=null).toSorted(Or.rcompare)[0]}function M(e){let t=Dt(e),r=Mt(e);return[...Ot(t,r.dataRules).map(n=>({gap:n,kind:"cascade-gap"})),..._t(r.pageRules).map(n=>({conflict:n,kind:"page-rule-conflict"}))]}function x(e,t,r){let n=e===1?t:r??`${t}s`;return`${String(e)} ${n}`}function Ur(e){return`${C.good("ok")} \u2014 no static model violations (${x(e,"test")})`}function H(e){return[`${C.bad("fail")} \u2014 ${x(e.length,"static model violation")}:`,...e.map(t=>Ft(t))].join(`
|
|
85
85
|
|
|
86
|
-
`)}function
|
|
87
|
-
`)}function
|
|
86
|
+
`)}function we(e){return[`${C.warn("warn")} \u2014 ${x(e.length,"model coverage gap")} (not blocking):`,...e.map(t=>` ${Bi(t)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${c("discover")}`].join(`
|
|
87
|
+
`)}function Bi(e){switch(e.kind){case"entity-never-given":return`${e.entity} \u2014 declared but no implemented test seeds it, so no flow exercises this state`;case"entity-never-mutated":return`${e.entity} \u2014 seeded but never asserted created/updated/deleted, so mutations to it ship unchecked`;case"unmatchable-delete-key":return`test ${e.workflow}/${e.testSlug} asserts ${e.entity} deleted with a literal ${e.field} but no seeded ${e.entity} sets ${e.field} \u2014 the solver treats it as no-match, so counts may mispredict`}}import{graphql as Vi}from"gql.tada";var Gi=Vi(`
|
|
88
88
|
query DoctorAuthViewer {
|
|
89
89
|
currentUser {
|
|
90
90
|
name
|
|
91
91
|
email
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
`),
|
|
95
|
-
`);let
|
|
94
|
+
`),zi="Failed to connect to Ripplo server";async function Fr({serverUrl:e,token:t}){try{let r=await m({config:F({serverUrl:e,token:t}),document:Gi,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(zi)?{kind:"unreachable"}:{kind:"invalid"}}}function Mr(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;case"plugin-version":return!1}}async function Hr(e){let t=await Xi(e),r={missing:bt(e),type:"env-files"},n=await Yi(),o=await Zi(),i=await S(e),s=es(i),a=ts(i),l=await rs(e,i),d=os(e),v=Qi(e),p=await is(t,i);return[t,r,n,...p,s,a,l,d,o,...v==null?[]:[v]]}var Ki=W.object({plugins:W.record(W.string(),W.array(W.object({projectPath:W.string().optional(),scope:W.string().optional(),version:W.string()})))});function Qi(e){let t=Qe.join(Ji.homedir(),".claude","plugins","installed_plugins.json"),r=J.existsSync(t)?J.readFileSync(t,"utf8"):null;if(r==null)return null;let n=Ki.safeParse(JSON.parse(r));if(!n.success)return null;let o=Object.entries(n.data.plugins).filter(([s])=>s.startsWith("ripplo@")).flatMap(([,s])=>s).filter(s=>s.scope==="user"||s.projectPath===e).map(s=>s.version),i=Nr(o);return i==null?null:{cliVersion:P(),installed:i,type:"plugin-version"}}async function Xi(e){let t=await It(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 Yi(){let e=$(),t=D(e);if(t==null||t.length===0)return{email:void 0,status:"missing",type:"token"};let r=await Fr({serverUrl:e,token:t});return r.kind==="valid"?{email:r.email,status:"valid",type:"token"}:{email:void 0,status:r.kind,type:"token"}}async function Zi(){let{chromium:e}=await import("playwright"),t=e.executablePath();return{installed:J.existsSync(t),type:"browser"}}function es(e){if(e.isErr())return{errorCount:1,errors:[{message:R(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let t=M(e.value);return{errorCount:t.length,errors:t.length===0?[]:[{message:H(t),path:""}],found:!0,type:"preconditions-validation",valid:t.length===0}}function ts(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let t=e.value.workflows.length;return{invalidNames:[],invalidWorkflows:[],total:t,type:"workflows",valid:t}}async function rs(e,t){if(t.isErr())return{status:"missing",type:"lockfile"};let r=z(U,t.value),n=await J.promises.readFile(Qe.join(e,T),"utf8").catch(()=>null);return{status:ns(n,r),type:"lockfile"}}function ns(e,t){return e==null?"missing":e===t?"match":"stale"}function os(e){let t=Qe.join(e,".git","hooks","pre-commit");return J.existsSync(t)?{installed:J.readFileSync(t,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function is(e,t){let r=vt().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(r==null)return[];let n=[],o=await Be(r.appUrl)==null;n.push({appUrl:r.appUrl,reachable:o,type:"dev-server"});let i=await Dr(process.cwd());if(n.push(i),e.valid&&t.isOk()){let s=await ss(r,t);n.push(...s)}return n}async function ss(e,t){let r=t.isOk()?t.value.entities.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=ls(e.appUrl,e.engineUrl);if(i==null)return[o];let s=await Be(i)==null,a=[];if(a.push({configured:!0,count:r,endpointReachable:s,type:"preconditions"}),Wr(e.engineUrl)&&a.push({reachable:s,type:"engine-endpoint",url:i}),!s)return a;let l=await At({appUrl:e.appUrl,engineUrl:e.engineUrl});a.push({rejectsUnsigned:l==null,type:"webhook-verification"});let d=await as({engineUrl:i,webhookSecret:e.webhookSecret});return a.push({status:d,type:"adapter-enabled",url:i}),a}async function as({engineUrl:e,webhookSecret:t}){if(t.length===0)return"no-secret";let r=JSON.stringify({});try{let n=await fetch(`${e}/setup`,{body:r,headers:{"Content-Type":"application/json",...yt({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 Wr(e){return e.startsWith("http://")||e.startsWith("https://")}function ls(e,t){return Wr(t)?t:`${e}${t}`}var ds=3e3;async function qr(){let e=process.cwd(),[t,r]=await Promise.all([ke(ds),Hr(e)]);process.stdout.write(`${ye({current:P(),latest:t})}
|
|
95
|
+
`);let n=r.map(i=>_r(i));process.stdout.write(n.join(`
|
|
96
96
|
`)+`
|
|
97
|
-
`);let o=
|
|
97
|
+
`);let o=r.some(i=>Mr(i));process.exit(o?1:0)}import ve from"fs";import{graphql as cs}from"gql.tada";var us=cs(`
|
|
98
98
|
mutation CliSetHooksPaused($projectId: String!, $paused: Boolean!) {
|
|
99
99
|
setHooksPaused(projectId: $projectId, paused: $paused) {
|
|
100
100
|
id
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
`);async function
|
|
104
|
-
`);return}
|
|
105
|
-
`)}async function
|
|
106
|
-
`)})}import
|
|
107
|
-
`)[0]??
|
|
103
|
+
`);async function Br(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` only runs interactively from a terminal. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();fe(e);let t=re(e);if(ve.existsSync(t)){process.stdout.write("Hooks already paused. Run `npx ripplo hooks resume` to re-enable.\n");return}ve.writeFileSync(t,""),await Gr(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `npx ripplo hooks resume`.\n")}async function Vr(){let e=process.cwd(),t=re(e);if(!ve.existsSync(t)){process.stdout.write(`Hooks already active.
|
|
104
|
+
`);return}ve.unlinkSync(t),await Gr(e,!1),process.stdout.write(`Hooks resumed.
|
|
105
|
+
`)}async function Gr(e,t){let r=k(e).unwrapOr(void 0);r!=null&&await m({config:r,document:us,variables:{paused:t,projectId:r.projectId}}).catch(n=>{process.stderr.write(`Warning: could not push hook-pause state to the server (${n instanceof Error?n.message:String(n)}) \u2014 the dashboard may show stale hook status.
|
|
106
|
+
`)})}import X from"fs";import Y from"path";import{input as Zr,select as en}from"@inquirer/prompts";import{graphql as Ws}from"gql.tada";import{exec as fs,execFile as gs}from"child_process";import{err as hs,ok as zr}from"neverthrow";import y from"fs";import{createRequire as ys}from"module";import h from"path";import{promisify as Kr}from"util";import{writeFile as ps}from"fs/promises";import ms from"path";async function K(e){let t=await S(e);return t.isOk()&&await ps(ms.join(e,T),z(U,t.value)),t}function N(e){return e.workflows.filter(t=>t.stub).map(t=>t.name)}var ks=["@ripplo/testing","@ripplo/instrument"],Jr=".ripplo/ripplo.lock linguist-generated=true",ws=[".ripplo/debug/",".ripplo/.local/"],vs=Kr(fs),bs=Kr(gs);async function Qr({cwd:e,onStep:t}){t("Scaffolding project files..."),Ts({cwd:e}),t("Updating .gitignore..."),Ls(e),t("Marking ripplo.lock as generated..."),xs(e),t("Installing dependencies...");let r=await Rs(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 \`npx ripplo lint\` to compile the lockfile.`}),r.ok){t("Compiling initial lockfile...");let i=await Cs(e);i!=null&&n.push({manualCommand:void 0,message:i})}return t("Setting up browser..."),(await Ss()).map(()=>n)}async function Ss(){let{chromium:e}=await import("playwright"),t=e.executablePath();if(y.existsSync(t))return zr(void 0);L.info("Chromium not found. Installing via Playwright...");let r=ys(import.meta.url),n=h.dirname(r.resolve("playwright/package.json")),o=h.join(n,"cli.js");return await bs(process.execPath,[o,"install","chromium"]),y.existsSync(t)?zr(void 0):hs({kind:"playwright-install-failed"})}async function Rs(e){let t=Ps({cwd:e,pm:As(e)});L.info("Installing dependencies: %s",t);try{return await vs(t,{cwd:e}),{ok:!0}}catch(r){let n=r instanceof Error?r.message.split(`
|
|
107
|
+
`)[0]??r.message:String(r);return L.warn("Install failed (%s): %s",t,n),{cmd:t,ok:!1,reason:n}}}async function Cs(e){try{await K(e);return}catch(t){return`Couldn't compile initial lockfile: ${t instanceof Error?t.message:String(t)}.`}}function xs(e){let t=h.join(e,".gitattributes"),r=y.existsSync(t)?y.readFileSync(t,"utf8"):"";if(r.includes(Jr))return;let n=r.length===0||r.endsWith(`
|
|
108
108
|
`)?"":`
|
|
109
|
-
`;
|
|
110
|
-
`)}function
|
|
109
|
+
`;y.writeFileSync(t,`${r}${n}${Jr}
|
|
110
|
+
`)}function Ps({cwd:e,pm:t}){let r=ks.join(" ");return t==="pnpm"?Es(e)?`pnpm add -wD ${r}`:`pnpm add -D ${r}`:t==="yarn"?$s({cwd:e,deps:r}):t==="bun"?`bun add -d ${r}`:`npm install -D ${r}`}function Es(e){return y.existsSync(h.join(e,"pnpm-workspace.yaml"))||y.existsSync(h.join(e,"pnpm-workspace.yml"))}function $s({cwd:e,deps:t}){return js(e)?`yarn add -D ${t}`:Is(e)?`yarn add -WD ${t}`:`yarn add -D ${t}`}function js(e){if(y.existsSync(h.join(e,".yarnrc.yml"))||y.existsSync(h.join(e,".pnp.cjs"))||y.existsSync(h.join(e,".pnp.js")))return!0;let t=h.join(e,"package.json");if(!y.existsSync(t))return!1;try{let r=JSON.parse(y.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("packageManager"in r))return!1;let n=r.packageManager;if(typeof n!="string")return!1;let o=/^yarn@(\d+)/.exec(n);return o!=null&&Number(o[1])>=2}catch{return!1}}function Is(e){let t=h.join(e,"package.json");if(!y.existsSync(t))return!1;try{let r=JSON.parse(y.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 As(e){return y.existsSync(h.join(e,"pnpm-lock.yaml"))?"pnpm":y.existsSync(h.join(e,"yarn.lock"))?"yarn":y.existsSync(h.join(e,"bun.lockb"))||y.existsSync(h.join(e,"bun.lock"))?"bun":"npm"}function Ts({cwd:e}){let t=h.join(e,".ripplo"),r=h.join(t,"entities"),n=h.join(t,"singletons"),o=h.join(t,"worlds"),i=h.join(t,"workflows");[r,n,o,i].forEach(s=>{y.mkdirSync(s,{recursive:!0})}),Q(h.join(t,"index.ts"),Ds),Q(h.join(r,"index.ts"),_s),Q(h.join(n,"index.ts"),Os),Q(h.join(o,"index.ts"),Ns),Q(h.join(i,"index.ts"),Us),Q(h.join(t,"tsconfig.json"),Fs)}function Q(e,t){y.existsSync(e)||y.writeFileSync(e,t)}function Ls(e){let t=h.join(e,".gitignore");if(!y.existsSync(t))return;let r=y.readFileSync(t,"utf8"),n=ws.filter(i=>!r.includes(i));if(n.length===0)return;let o=r.endsWith(`
|
|
111
111
|
`)?"":`
|
|
112
|
-
`;
|
|
112
|
+
`;y.writeFileSync(t,r+o+n.join(`
|
|
113
113
|
`)+`
|
|
114
|
-
`)}var
|
|
114
|
+
`)}var Ds=`import { createRipplo, entity, field, id, v, singleton, arbitrary } from "@ripplo/testing";
|
|
115
115
|
import { entities } from "./entities/index";
|
|
116
116
|
import { singletons } from "./singletons/index";
|
|
117
117
|
import { workflows } from "./workflows/index";
|
|
@@ -121,7 +121,7 @@ export default createRipplo({
|
|
|
121
121
|
singletons,
|
|
122
122
|
workflows,
|
|
123
123
|
});
|
|
124
|
-
`,
|
|
124
|
+
`,_s=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
|
|
125
125
|
// app's engine funnel (createEngine). See /ripplo:create "Adding an entity".
|
|
126
126
|
//
|
|
127
127
|
// Example:
|
|
@@ -136,7 +136,7 @@ export default createRipplo({
|
|
|
136
136
|
// export const entities = [Project] as const;
|
|
137
137
|
|
|
138
138
|
export const entities = [] as const;
|
|
139
|
-
`,
|
|
139
|
+
`,Os=`// Client/global state (e.g. localStorage flags) modeled as singletons.
|
|
140
140
|
//
|
|
141
141
|
// Example:
|
|
142
142
|
// //
|
|
@@ -150,7 +150,7 @@ export const entities = [] as const;
|
|
|
150
150
|
// export const singletons = [onboardingDismissed];
|
|
151
151
|
|
|
152
152
|
export const singletons = [];
|
|
153
|
-
`,
|
|
153
|
+
`,Ns=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
|
|
154
154
|
// for workflows. Compose from other worlds. See /ripplo:create "Adding a world".
|
|
155
155
|
//
|
|
156
156
|
// Example:
|
|
@@ -161,7 +161,7 @@ export const singletons = [];
|
|
|
161
161
|
// const project = Project.of({ name: arbitrary(Project.field.name), ownerId: me.id });
|
|
162
162
|
// return { me, project };
|
|
163
163
|
// };
|
|
164
|
-
`,
|
|
164
|
+
`,Us=`// Each file under ./workflows exports a workflow. Import them here and add to the
|
|
165
165
|
// \`workflows\` array \u2014 that's what createRipplo({ ..., workflows }) receives. Stub with
|
|
166
166
|
// \`workflow("Intent")\` (no body); implement later with \`workflow("Intent", () => ({ given, steps }))\`.
|
|
167
167
|
//
|
|
@@ -170,7 +170,7 @@ export const singletons = [];
|
|
|
170
170
|
// export const workflows = [createProject] as const;
|
|
171
171
|
|
|
172
172
|
export const workflows = [] as const;
|
|
173
|
-
`,
|
|
173
|
+
`,Fs=`{
|
|
174
174
|
"compilerOptions": {
|
|
175
175
|
"strict": true,
|
|
176
176
|
"noUncheckedIndexedAccess": true,
|
|
@@ -186,75 +186,44 @@ export const workflows = [] as const;
|
|
|
186
186
|
"include": ["*.ts", "entities/**/*.ts", "singletons/**/*.ts", "worlds/**/*.ts", "workflows/**/*.ts"],
|
|
187
187
|
"exclude": ["node_modules"]
|
|
188
188
|
}
|
|
189
|
-
`;import
|
|
190
|
-
`),"written"}function
|
|
189
|
+
`;import be from"fs";import Xr from"path";import{z as Xe}from"zod";var Ms={autoUpdate:!0,source:{repo:"ripplo/claude-plugin",source:"github"}},Yr=Xe.record(Xe.string(),Xe.unknown());function Se(e){let t=Xr.join(e,".claude","settings.json"),r=Hs(t);if(r==null)return"unparseable";let n=Yr.safeParse(r.extraKnownMarketplaces??{});if(!n.success)return"unparseable";if(n.data.ripplo!=null)return"already-present";let o={...r,extraKnownMarketplaces:{...n.data,ripplo:Ms}};return be.mkdirSync(Xr.dirname(t),{recursive:!0}),be.writeFileSync(t,`${JSON.stringify(o,null,2)}
|
|
190
|
+
`),"written"}function Hs(e){if(!be.existsSync(e))return{};try{let t=Yr.safeParse(JSON.parse(be.readFileSync(e,"utf8")));return t.success?t.data:void 0}catch{return}}function Re(e){switch(e){case"written":return" registered ripplo plugin marketplace in .claude/settings.json (autoUpdate: true \u2014 Claude Code keeps the plugin current)";case"already-present":return" ripplo plugin marketplace already registered in .claude/settings.json";case"unparseable":return" ! .claude/settings.json could not be parsed \u2014 add the ripplo marketplace to extraKnownMarketplaces manually"}}var qs=Ws(`
|
|
191
191
|
query InitProjects {
|
|
192
192
|
projects {
|
|
193
193
|
id
|
|
194
194
|
name
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
`),
|
|
198
|
-
`),process.exit(1));let o=await
|
|
199
|
-
`);let p=(await
|
|
197
|
+
`),Ye=["../.env.local","../.env"];async function tn(e=Gs()){let t=process.cwd(),r=$(),n=D(r);n==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1)),Bs(t)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${t}. To re-init, delete it first.
|
|
198
|
+
`),process.exit(1));let o=await Vs({serverUrl:r,token:n});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `npx ripplo init`.\n"),process.exit(1));let i=await Js(o,e.projectId),s=await Ks(t,e.envFile),a=await Xs(e.appUrl),l=zs(a,e.engineUrl),d=Y.resolve(Y.join(t,".ripplo"),s);wt({cwd:t,envFiles:[s],projectId:i}),Zs({appUrl:a,engineUrl:l,filePath:d}),process.stdout.write(`${Re(Se(t))}
|
|
199
|
+
`);let p=(await Qr({cwd:t,onStep:u=>{process.stdout.write(` ${u}
|
|
200
200
|
`)}})).match(u=>u,u=>{process.stderr.write(`${E(u)}
|
|
201
201
|
`),process.exit(1)});if(p.length>0){process.stdout.write(`Done with warnings:
|
|
202
202
|
`),p.forEach(u=>{process.stdout.write(` - ${u.message}
|
|
203
203
|
`),u.manualCommand!=null&&process.stdout.write(` run: ${u.manualCommand}
|
|
204
|
-
`)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write workflows in `.ripplo/workflows/`.\n")}function
|
|
205
|
-
`),process.exit(1)}return
|
|
206
|
-
`),process.exit(1)),process.stdout.write(`Using project: ${
|
|
207
|
-
`),
|
|
208
|
-
`),
|
|
209
|
-
`),process.exit(1)),
|
|
210
|
-
`),process.exit(1)}return e}return
|
|
204
|
+
`)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write workflows in `.ripplo/workflows/`.\n")}function Bs(e){return X.existsSync(Y.join(e,".ripplo","index.ts"))}async function Vs({serverUrl:e,token:t}){return((await m({config:F({serverUrl:e,token:t}),document:qs,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function Gs(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function zs(e,t){if(t!=null){try{new URL(t)}catch{process.stdout.write(`--engine-url is not a valid URL: ${t}
|
|
205
|
+
`),process.exit(1)}return t}return`${e.replace(/\/$/,"")}/ripplo`}async function Js(e,t){if(t!=null){let r=e.find(n=>n.id===t);return r==null&&(process.stdout.write(`Unknown project id: ${t}
|
|
206
|
+
`),process.exit(1)),process.stdout.write(`Using project: ${r.name} (${r.id})
|
|
207
|
+
`),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})
|
|
208
|
+
`),r.id}return en({choices:e.map(r=>({name:r.name,value:r.id})),message:"Select a project"})}async function Ks(e,t){return t!=null?(t.trim().length===0&&(process.stdout.write(`--env must not be empty
|
|
209
|
+
`),process.exit(1)),t):Qs(e)}async function Qs(e){let t=Y.join(e,".ripplo"),r=Ye.find(o=>X.existsSync(Y.resolve(t,o))),n=await en({choices:[...Ye.map(o=>({name:r===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:r??Ye[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:Zr({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function Xs(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
|
|
210
|
+
`),process.exit(1)}return e}return Ys()}async function Ys(){return Zr({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 Zs({appUrl:e,engineUrl:t,filePath:r}){X.mkdirSync(Y.dirname(r),{recursive:!0});let n=X.existsSync(r)?X.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=${kt()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
|
|
211
211
|
`)?"":`
|
|
212
|
-
`;
|
|
212
|
+
`;X.writeFileSync(r,`${n}${i}${o.join(`
|
|
213
213
|
`)}
|
|
214
|
-
`)}function
|
|
215
|
-
`);let i=
|
|
216
|
-
`);return}process.stderr.write(`${H(
|
|
217
|
-
`),process.exit(1)}function
|
|
218
|
-
`),process.exit(1)}import{
|
|
219
|
-
`).trim()}function Tt(e,r){switch(r.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current workflows`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current workflows \u2014 finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let n=r.runId==null?"":` (fresh evidence: run ${r.runId})`;return`explore: ${e} still reproduces \u2014 same failure signature${n}`}case"diverged":{let n=r.runId==null?"":` (captured run ${r.runId})`;return`explore: ${e} failed with a different failure signature \u2014 new finding recorded${n}`}case"flaky":return`explore: ${e} did not reproduce deterministically \u2014 recorded as flaky-candidate, finding stays pending`;case"aborted":return`explore: replay of ${e} was aborted`;case"finding-not-found":return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`;case"unreplayable":return`explore: ${e} cannot be replayed (${r.reason})`;case"error":return`explore: replay of ${e} failed (${r.reason})`}}function Lt({executed:e,progress:r,trail:n}){let t=n.trail.flatMap(o=>[` ${o.label}`,...o.actions.map(i=>` ${i}`)]);return[`trail ${String(e)} ${n.kind} \u2014 ${se(r)}`,...t,` state: ${n.label}`].join(`
|
|
220
|
-
`)}function Dt(e){switch(e.kind){case"config-failed":return E(e.failure);case"compile-failed":return v(e.error);case"no-work":return"explore: nothing to explore \u2014 no runnable actions found in your workflows";case"explorer-busy":return"explore: another explorer holds the machine lock (likely the daemon's background explorer) \u2014 watch it with `npx ripplo status`, or stop it to explore in the foreground";case"completed":return`${`explore: ${String(e.executed)} trails executed`}
|
|
221
|
-
${se(e.progress)}
|
|
222
|
-
findings land in .ripplo/.local/explore-ledger.jsonl`}}function Ot(e){let r=e.evidence.map(t=>` ${t}`),n=e.runId==null?[" no captured run \u2014 replay to capture one"]:[` run: ${e.runId}`,` behavior: .ripplo/debug/${e.runId}/behavior.jsonl`];return[`${e.id} layer=${e.verifierLayer} start=${e.baseState}`,` seen ${String(e.occurrences)}x between ${lr(e.firstSeen)} and ${lr(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...r,...n,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
|
|
223
|
-
`)}function dr(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function _t(e){return e.kind==="not-found"?dr(e.id):`explore: dismissed finding ${e.id}. Prune the log to drop it: npx ripplo explore prune`}function Ft(e){if(e.removedStale+e.removedResolved===0)return"explore: nothing to prune \u2014 the findings log has no stale or dismissed entries.";let n=S(e.removedStale,"stale finding"),t=S(e.removedResolved,"dismissed finding");return`explore: pruned ${n} and ${t}, ${S(e.kept,"row")} kept.`}function Ya(e){let r=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.verifierLayer} seen ${String(e.occurrences)}x (last ${lr(e.lastSeen)}) start=${e.baseState} ${r}`,` mismatch: ${Za(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
|
|
224
|
-
`)}function lr(e){return e.slice(0,16).replace("T"," ")}function Za(e){let r=e.at(0);if(r==null)return"no parts recorded";let n=e.length>1?` (+${String(e.length-1)} more)`:"";return`${el(r)}${n}`}function el(e){switch(e.kind){case"consistency":{let r="entity"in e.mismatch?e.mismatch.entity:e.mismatch.singleton,n=e.step==null?"":` at "${e.step.intent}"`;return`${e.mismatch.kind} on ${r}${n}`}case"pending-check":return`${e.source} check failed at "${e.step.intent}"`;case"unrunnable":return`step "${e.intent}" could not run (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"impossible-action":return`impossible action at "${e.step}"`}}function rl(e){let r=e.trail.map(n=>n.split("|").at(0)??n);return` ${e.id} seen ${String(e.occurrences)}x start=${e.baseState}
|
|
225
|
-
trail: ${r.join(" -> ")}`}import{spawn as nl}from"child_process";import Nt from"fs";import tl from"net";import{setTimeout as cr}from"timers/promises";import{err as G,ok as ce}from"neverthrow";import{ResponseError as ol}from"vscode-jsonrpc/node";var Mt=12e4,Ht=300,Ut=5e3;async function Te({cliEntry:e,cwd:r}){let n=V(r),t=await q(n);return t!=null?Bt({cliEntry:e,connection:pe(t,!1),cwd:r}):qt({cliEntry:e,cwd:r,versionNote:void 0})}async function Wt({cliEntry:e,cwd:r}){let n=await q(V(r));return n==null?G({kind:"not-running"}):Bt({cliEntry:e,connection:pe(n,!1),cwd:r})}async function qt({cliEntry:e,cwd:r,versionNote:n}){let t=cl({cliEntry:e,cwd:r});if(t!=null)return G(t);let o=await pl(V(r));return o==null?G({deadlineMs:Mt,kind:"connect-timeout",logPath:rr(r)}):ce({...pe(o,!0),versionNote:n})}async function Bt({cliEntry:e,connection:r,cwd:n}){let t=await zt(r);if(t==null||t.version===P())return ce(r);let o={daemonVersion:t.version};return await il(r)?(r.socket.destroy(),await al(V(n))?qt({cliEntry:e,cwd:n,versionNote:{...o,kind:"restarted"}}):G({kind:"connection-lost"})):ce({...r,versionNote:{...o,kind:"stale-busy"}})}async function il(e){try{return await e.rpc.sendRequest(Se)}catch{return!1}}var sl=1e4;async function al(e){let r=Date.now()+sl,n=await q(e);for(;n!=null&&Date.now()<r;)n.destroy(),await cr(Ht),n=await q(e);return n?.destroy(),n==null}async function Le({connection:e,onEvent:r,request:n,token:t}){let o=await ll({connection:e,onEvent:r,request:n,token:t});return o.kind==="transport"?G(o.error):ce(o)}async function Vt({connection:e,findingId:r,token:n}){try{let t=await e.rpc.sendRequest(In,{findingId:r},n),o=$n.safeParse(t);return o.success?ce(o.data):G({kind:"bad-frame"})}catch{return G({kind:"connection-lost"})}}async function Gt(e){let r=await q(V(e));if(r==null)return!1;let n=pe(r,!1);try{return await n.rpc.sendRequest(Se)}catch{return!1}finally{r.destroy()}}async function De(e){try{await e.rpc.sendRequest(Se)}catch{}}async function Oe(e){let r=await q(V(e));if(r==null)return{kind:"not-running"};let n=pe(r,!1),t=await Promise.race([zt(n),cr(Ut).then(()=>null)]);return r.destroy(),t==null?{kind:"unresponsive",timeoutMs:Ut}:{kind:"running",status:t}}async function zt(e){try{let r=await e.rpc.sendRequest(An),n=Rn.safeParse(r);return n.success?n.data:null}catch{return null}}function ll({connection:e,onEvent:r,request:n,token:t}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(Tn,s=>{let l=Cn.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}r(l.data.event)}),o.onNotification(Ln,s=>{let l=Pn.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}a({failed:l.data.failed,kind:"done",notRun:l.data.notRun,passed:l.data.passed})}),o.onClose(()=>{a({error:{kind:"connection-lost"},kind:"transport"})}),o.sendRequest(jn,n,t).then(s=>{xn.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(dl(s))})})}function dl(e){if(e instanceof ol){let r=En.safeParse(e.data);return r.success?{error:r.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function pe(e,r){let n=Sn(e);return n.listen(),{rpc:n,socket:e,spawned:r,versionNote:void 0}}function q(e){return new Promise(r=>{let n=tl.connect(e);n.once("connect",()=>{r(n)}),n.once("error",()=>{r(null)})})}function cl({cliEntry:e,cwd:r}){try{he(r);let n=Nt.openSync(rr(r),"a");return nl(process.execPath,[e,"daemon"],{cwd:r,detached:!0,stdio:["ignore",n,n]}).unref(),Nt.closeSync(n),null}catch(n){return{kind:"spawn-failed",message:n instanceof Error?n.message:String(n)}}}async function pl(e){let r=Date.now()+Mt,n=await q(e);for(;n==null&&Date.now()<r;)await cr(Ht),n=await q(e);return n}import ml from"fuse.js";var ul=3e3;async function _e(e){B(e);let r=w(e).unwrapOr(void 0);if(r==null)return null;try{return await fetch(`${r.ripploServerUrl}/health`,{signal:AbortSignal.timeout(ul)}),null}catch(n){return{detail:n instanceof Error?n.message:String(n),serverUrl:r.ripploServerUrl}}}function Fe(e){return e.includes("localhost")||e.includes("127.0.0.1")}function pr(e){switch(e.kind){case"conflicting-flags":return"Pass either --all or test ids, not both.";case"nothing-selected":return`No tests selected \u2014 scope is empty and no .ripplo/workflows files are dirty. Pass test ids, add workflows to scope (${c("run")}), or use --all.`;case"unknown-ids":{let r=e.known,n=e.unknown.flatMap(o=>yl(o,r)),t=n.length>0?[`Did you mean: ${[...new Set(n)].join(", ")}`]:[];return[`Unknown ${e.unknown.length===1?"id":"ids"}: ${e.unknown.join(", ")}`,...t,"Pass a workflow slug to run all its tests, or workflow/test for one. Known workflows:",...r.map(o=>` ${o}`)].join(`
|
|
226
|
-
`)}}}function Jt({failed:e,notRun:r,passed:n}){let t=r>0?`, ${String(r)} not run`:"",o=e>0?`
|
|
227
|
-
${c("run")}`:"";return`${String(n)} passed, ${String(e)} failed${t} (${String(n+e+r)} total)${o}`}function Ne({debugDir:e,event:r}){let n=`${r.workflowName} \u2192 ${r.testName}`;if(r.kind==="test-started")return`${C.dim("run ")} ${n}`;switch(r.outcome.kind){case"pass":return`${C.good("pass")} ${n}`;case"findings":return[`${C.bad("fail")} ${n} \u2014 ${S(r.outcome.findingLines.length,"finding")}`,...r.outcome.findingLines,Jr({debugDir:e,runId:r.runId})].join(`
|
|
228
|
-
`);case"error":return`${C.bad("error")} ${n} \u2014 ${r.outcome.detail}`;case"dispatch-error":return`${C.bad("error")} ${n} \u2014 ${hl(r.outcome.reason)}`;case"dispatch-timeout":return`${C.bad("error")} ${n} \u2014 not run: dispatch timed out (transient): ${r.outcome.detail}`;case"infra-error":return`${C.bad("error")} ${n} \u2014 not run: Ripplo server unreachable (server-side, not local): ${r.outcome.detail}`}}var fl={MAX_AGENTS:"agent limit reached for your plan \u2014 upgrade to add more",MAX_CONCURRENT_RUNS:"concurrent run limit reached for your plan \u2014 upgrade or wait for runs to finish",MAX_PROJECTS:"project limit reached for your plan \u2014 upgrade to add more",MAX_RUNS:"run limit reached for your plan \u2014 enable overage or upgrade",OVERAGE_CAP:"overage cap reached \u2014 raise the cap or upgrade"};function Ue({detail:e,serverUrl:r}){return Fe(r)?`Ripplo server at ${r} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${r} is unreachable (${e}). Tests were not started. This is a server-side issue, not your local environment \u2014 wait a moment and re-run.`}function Me(e){switch(e.code){case"compile-failed":{let r=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...r].join(`
|
|
229
|
-
`)}case"selection-conflicting-flags":return pr({kind:"conflicting-flags"});case"selection-nothing-selected":return pr({kind:"nothing-selected"});case"selection-unknown-ids":return pr({kind:"unknown-ids",known:e.known,unknown:e.unknown});case"app-unreachable":return`Your dev server is not responding at ${e.url} (${e.detail}). Tests were not started \u2014 start your app and re-run. This is your local environment, not the Ripplo server.`;case"scope-failed":return`Could not resolve the dev-session scope: ${e.detail}
|
|
230
|
-
Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let r=/401|unauthor/i.test(e.detail)?"\nLooks like an auth failure \u2014 run `npx ripplo auth login` and retry.":"";return`Sync to the Ripplo server failed: ${e.detail}${r}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function T(e){switch(e.kind){case"spawn-failed":return`Failed to start \`npx ripplo daemon\`: ${gl(e.message)}`;case"connect-timeout":return[`Daemon did not come up within ${String(Math.round(e.deadlineMs/1e3))}s.`,`Check ${e.logPath} for startup errors.`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server unreachable."].join(`
|
|
231
|
-
`);case"connection-lost":return["Lost the daemon connection mid-run \u2014 it likely crashed or was killed.","Check .ripplo/.local/daemon.log, restart `npx ripplo daemon`, then rerun."].join(`
|
|
232
|
-
`);case"not-running":return["No ripplo daemon running for this directory \u2014 nothing to dispatch to.","Start it with `npx ripplo daemon` as a background process (separate from your app's dev server), then rerun.","A run needs a live daemon to dispatch and to register the result to your dev session."].join(`
|
|
233
|
-
`);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function gl(e){return e.includes("ENOENT")?`${e} \u2014 the node executable or CLI entry was not found on PATH.`:e.includes("EACCES")?`${e} \u2014 permission denied executing the CLI entry.`:e}function hl(e){return e.type==="limit"?fl[e.code]:`failed to dispatch (${e.detail})`}function yl(e,r){return new ml(r,{includeScore:!0,threshold:.5}).search(O(e)).slice(0,3).map(t=>t.item)}async function Kt({findingId:e,json:r}){await kn(process.cwd()).match(n=>{if(e==null){let i=r?JSON.stringify(n,null,2):At(n);process.stdout.write(`${i}
|
|
234
|
-
`);return}let t=n.pending.find(i=>i.id===e);t==null&&(process.stderr.write(`${dr(e)}
|
|
235
|
-
`),process.exit(1));let o=r?JSON.stringify(t,null,2):Ot(t);process.stdout.write(`${o}
|
|
236
|
-
`)},n=>{process.stderr.write(`explore: findings log unreadable (${n.kind})
|
|
237
|
-
`),process.exit(1)})}async function Qt({findingId:e}){let r=process.argv[1];r==null&&(process.stderr.write(`${T({kind:"spawn-failed",message:"process.argv[1] missing"})}
|
|
238
|
-
`),process.exit(1));let n=await Te({cliEntry:r,cwd:process.cwd()});n.isErr()&&(process.stderr.write(`${T(n.error)}
|
|
239
|
-
`),process.exit(1));let t=n.value,o=new kl;process.once("SIGINT",()=>{o.cancel(),t.socket.destroy(),process.exit(130)});let a=await(await Vt({connection:t,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${Tt(e,s)}
|
|
240
|
-
`),t.spawned&&await De(t),s.kind==="resolved"||s.kind==="unreachable"?0:1),s=>(process.stderr.write(`${T(s)}
|
|
241
|
-
`),Promise.resolve(1)));t.socket.destroy(),process.exit(a)}async function Xt({findingId:e}){let r=new Date().toISOString();await It(process.cwd(),e,r).match(async n=>{n.kind==="dismissed"&&await wl(n.signature),process.stdout.write(`${_t(n)}
|
|
242
|
-
`),process.exit(n.kind==="dismissed"?0:1)},n=>{process.stderr.write(`explore: findings log unreadable (${n.kind})
|
|
243
|
-
`),process.exit(1)})}async function Yt(){let e=await x(process.cwd());e.isErr()&&(process.stderr.write(`${v(e.error)}
|
|
244
|
-
`),process.exit(1)),await jt(process.cwd(),e.value).match(r=>{process.stdout.write(`${Ft(r)}
|
|
245
|
-
`)},r=>{process.stderr.write(`explore: findings log unreadable (${r.kind})
|
|
246
|
-
`),process.exit(1)})}async function wl(e){await w(process.cwd()).match(r=>fn(r,e,"dismissed").catch(n=>{$.warn({error:n},"explore finding status push failed")}),r=>{$.warn({failure:r},"explore finding dismiss not synced: config unavailable")})}async function Zt(e){let r=await $t({cwd:process.cwd(),headed:e.headed,maxLength:e.maxLength,trails:e.trails,onReady:n=>{process.stdout.write(`${se(n)}
|
|
247
|
-
`)},onTrail:(n,t,o)=>{process.stdout.write(`${Lt({executed:n,progress:o,trail:t})}
|
|
248
|
-
`)}});process.stdout.write(`${Dt(r)}
|
|
249
|
-
`),(r.kind==="config-failed"||r.kind==="compile-failed")&&process.exit(1)}import{graphql as vl}from"gql.tada";var bl=vl(`
|
|
214
|
+
`)}function Ce(e){let t=e.workflows.filter(o=>!o.stub);if(t.length===0)return[];let r=new Set(t.flatMap(o=>ea(o))),n=new Set(t.flatMap(o=>ta(o)));return[...e.entities.filter(o=>!r.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>r.has(o.name)&&!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"})),...t.flatMap(o=>o.tests.flatMap(i=>ra(i)))]}function ea(e){return[...e.world,...e.maybe].map(t=>t.entity)}function ta(e){return e.steps.flatMap(t=>t.expect.flatMap(r=>ie(r))).map(t=>t.entity)}function ra(e){let t=e.steps.flatMap(r=>r.expect.flatMap(n=>ie(n)));return na({states:t,test:e})}function na({states:e,test:t}){let r=e.filter(n=>n.assertion.kind==="deleted").flatMap(n=>oa({predicate:n,test:t})).map(({entity:n,field:o})=>({entity:n,field:o,kind:"unmatchable-delete-key",testSlug:t.slug,workflow:t.workflow}));return aa(r)}function oa({predicate:e,test:t}){let r=t.world.filter(n=>n.entity===e.entity);return r.length===0?[]:Object.entries(e.key).filter(([n,o])=>sa(o)&&!ia({field:n,setups:r})).map(([n])=>({entity:e.entity,field:n}))}function ia({field:e,setups:t}){return t.some(r=>Object.keys(r.set).includes(e))}function sa(e){return e===null||typeof e!="object"}function aa(e){return e.filter((t,r)=>e.findIndex(n=>JSON.stringify(n)===JSON.stringify(t))===r)}function ie(e){return e.kind==="state"?[e]:e.kind==="not"?ie(e.predicate):e.kind==="and"?e.predicates.flatMap(t=>ie(t)):e.kind==="when"?e.branches.flatMap(t=>t.consequence.flatMap(r=>ie(r))):[]}async function rn(){let e=process.cwd(),r=(await S(e)).match(o=>o,o=>la(R(o))),n=M(r);if(n.length===0){let o=Ce(r);o.length>0&&process.stdout.write(`${we(o)}
|
|
215
|
+
`);let i=r.workflows.reduce((s,a)=>s+a.tests.length,0);process.stdout.write(`${Ur(i)}
|
|
216
|
+
`);return}process.stderr.write(`${H(n)}
|
|
217
|
+
`),process.exit(1)}function la(e){process.stderr.write(`${e}
|
|
218
|
+
`),process.exit(1)}import{graphql as da}from"gql.tada";var ca=da(`
|
|
250
219
|
query ProjectsList {
|
|
251
220
|
projects {
|
|
252
221
|
id
|
|
253
222
|
name
|
|
254
223
|
}
|
|
255
224
|
}
|
|
256
|
-
`);async function
|
|
257
|
-
`)}import{graphql as
|
|
225
|
+
`);async function nn(){let e=$(),t=D(e);t==null&&(process.stderr.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1));let n=((await m({config:F({serverUrl:e,token:t}),document:ca,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
|
|
226
|
+
`)}import{graphql as ua}from"gql.tada";function on({id:e,kind:t,title:r}){return`Caught bug reported (${t}): "${r}" [${e}]`}var pa=ua(`
|
|
258
227
|
mutation ReportCaughtBug(
|
|
259
228
|
$projectId: String!
|
|
260
229
|
$kind: CaughtBugKind!
|
|
@@ -282,22 +251,57 @@ Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass t
|
|
|
282
251
|
}
|
|
283
252
|
}
|
|
284
253
|
}
|
|
285
|
-
`);async function
|
|
286
|
-
`),process.exit(1)}process.stdout.write(`${
|
|
287
|
-
`)}import
|
|
288
|
-
`)
|
|
289
|
-
|
|
290
|
-
`)
|
|
291
|
-
`)
|
|
254
|
+
`);async function sn(e){let t=I(),n=(await m({config:t,document:pa,variables:{kind:e.kind,projectId:t.projectId,rootCause:e.rootCause,runId:e.runId,surfacedBy:e.surfacedBy,title:e.title}})).reportCaughtBug;if(n?.__typename!=="CaughtBug"){let o=n?.__typename==="RunNotFoundError"?n.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
|
|
255
|
+
`),process.exit(1)}process.stdout.write(`${on({id:n.id,kind:e.kind,title:e.title})}
|
|
256
|
+
`)}import ja from"path";import{CancellationTokenSource as Ia}from"vscode-jsonrpc/node";import{spawn as ma}from"child_process";import an from"fs";import fa from"net";import{setTimeout as Ze}from"timers/promises";import{err as se,ok as xe}from"neverthrow";import{ResponseError as ga}from"vscode-jsonrpc/node";var dn=12e4,cn=300,ln=5e3;async function un({cliEntry:e,cwd:t}){let r=V(t),n=await q(r);return n!=null?fn({cliEntry:e,connection:ae(n,!1),cwd:t}):mn({cliEntry:e,cwd:t,versionNote:void 0})}async function pn({cliEntry:e,cwd:t}){let r=await q(V(t));return r==null?se({kind:"not-running"}):fn({cliEntry:e,connection:ae(r,!1),cwd:t})}async function mn({cliEntry:e,cwd:t,versionNote:r}){let n=ba({cliEntry:e,cwd:t});if(n!=null)return se(n);let o=await Sa(V(t));return o==null?se({deadlineMs:dn,kind:"connect-timeout",logPath:Ve(t)}):xe({...ae(o,!0),versionNote:r})}async function fn({cliEntry:e,connection:t,cwd:r}){let n=await yn(t);if(n==null||n.version===P())return xe(t);let o={daemonVersion:n.version};return await ha(t)?(t.socket.destroy(),await ka(V(r))?mn({cliEntry:e,cwd:r,versionNote:{...o,kind:"restarted"}}):se({kind:"connection-lost"})):xe({...t,versionNote:{...o,kind:"stale-busy"}})}async function ha(e){try{return await e.rpc.sendRequest(he)}catch{return!1}}var ya=1e4;async function ka(e){let t=Date.now()+ya,r=await q(e);for(;r!=null&&Date.now()<t;)r.destroy(),await Ze(cn),r=await q(e);return r?.destroy(),r==null}async function Pe({connection:e,onEvent:t,request:r,token:n}){let o=await wa({connection:e,onEvent:t,request:r,token:n});return o.kind==="transport"?se(o.error):xe(o)}async function gn(e){let t=await q(V(e));if(t==null)return!1;let r=ae(t,!1);try{return await r.rpc.sendRequest(he)}catch{return!1}finally{t.destroy()}}async function hn(e){try{await e.rpc.sendRequest(he)}catch{}}async function Ee(e){let t=await q(V(e));if(t==null)return{kind:"not-running"};let r=ae(t,!1),n=await Promise.race([yn(r),Ze(ln).then(()=>null)]);return t.destroy(),n==null?{kind:"unresponsive",timeoutMs:ln}:{kind:"running",status:n}}async function yn(e){try{let t=await e.rpc.sendRequest(nr),r=Yt.safeParse(t);return r.success?r.data:null}catch{return null}}function wa({connection:e,onEvent:t,request:r,token:n}){let{rpc:o}=e;return new Promise(i=>{let s=a=>{i(a)};o.onNotification(or,a=>{let l=Zt.safeParse(a);if(!l.success){s({error:{kind:"bad-frame"},kind:"transport"});return}t(l.data.event)}),o.onNotification(ir,a=>{let l=er.safeParse(a);if(!l.success){s({error:{kind:"bad-frame"},kind:"transport"});return}s({failed:l.data.failed,kind:"done",notRun:l.data.notRun,passed:l.data.passed})}),o.onClose(()=>{s({error:{kind:"connection-lost"},kind:"transport"})}),o.sendRequest(rr,r,n).then(a=>{Xt.safeParse(a).success||s({error:{kind:"bad-frame"},kind:"transport"})}).catch(a=>{s(va(a))})})}function va(e){if(e instanceof ga){let t=tr.safeParse(e.data);return t.success?{error:t.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function ae(e,t){let r=Qt(e);return r.listen(),{rpc:r,socket:e,spawned:t,versionNote:void 0}}function q(e){return new Promise(t=>{let r=fa.connect(e);r.once("connect",()=>{t(r)}),r.once("error",()=>{t(null)})})}function ba({cliEntry:e,cwd:t}){try{fe(t);let r=an.openSync(Ve(t),"a");return ma(process.execPath,[e,"daemon"],{cwd:t,detached:!0,stdio:["ignore",r,r]}).unref(),an.closeSync(r),null}catch(r){return{kind:"spawn-failed",message:r instanceof Error?r.message:String(r)}}}async function Sa(e){let t=Date.now()+dn,r=await q(e);for(;r==null&&Date.now()<t;)await Ze(cn),r=await q(e);return r}var Ra=3e3;async function $e(e){B(e);let t=k(e).unwrapOr(void 0);if(t==null)return null;try{return await fetch(`${t.ripploServerUrl}/health`,{signal:AbortSignal.timeout(Ra)}),null}catch(r){return{detail:r instanceof Error?r.message:String(r),serverUrl:t.ripploServerUrl}}}function je(e){return e.includes("localhost")||e.includes("127.0.0.1")}import Ca from"fuse.js";function et(e){switch(e.kind){case"conflicting-flags":return"Pass either --all or test ids, not both.";case"nothing-selected":return`No tests selected \u2014 scope is empty and no .ripplo/workflows files are dirty. Pass test ids, add workflows to scope (${c("run")}), or use --all.`;case"unknown-ids":{let t=e.known,r=e.unknown.flatMap(o=>$a(o,t)),n=r.length>0?[`Did you mean: ${[...new Set(r)].join(", ")}`]:[];return[`Unknown ${e.unknown.length===1?"id":"ids"}: ${e.unknown.join(", ")}`,...n,"Pass a workflow slug to run all its tests, or workflow/test for one. Known workflows:",...t.map(o=>` ${o}`)].join(`
|
|
257
|
+
`)}}}function kn({failed:e,notRun:t,passed:r}){let n=t>0?`, ${String(t)} not run`:"",o=e>0?`
|
|
258
|
+
${c("run")}`:"";return`${String(r)} passed, ${String(e)} failed${n} (${String(r+e+t)} total)${o}`}function Ie({debugDir:e,event:t}){let r=`${t.workflowName} \u2192 ${t.testName}`;if(t.kind==="test-started")return`${C.dim("run ")} ${r}`;switch(t.outcome.kind){case"pass":return`${C.good("pass")} ${r}`;case"findings":return[`${C.bad("fail")} ${r} \u2014 ${x(t.outcome.findingLines.length,"finding")}`,...t.outcome.findingLines,Ut({debugDir:e,runId:t.runId})].join(`
|
|
259
|
+
`);case"error":return`${C.bad("error")} ${r} \u2014 ${t.outcome.detail}`;case"dispatch-error":return`${C.bad("error")} ${r} \u2014 ${Ea(t.outcome.reason)}`;case"dispatch-timeout":return`${C.bad("error")} ${r} \u2014 not run: dispatch timed out (transient): ${t.outcome.detail}`;case"infra-error":return`${C.bad("error")} ${r} \u2014 not run: Ripplo server unreachable (server-side, not local): ${t.outcome.detail}`}}var xa={MAX_AGENTS:"agent limit reached for your plan \u2014 upgrade to add more",MAX_CONCURRENT_RUNS:"concurrent run limit reached for your plan \u2014 upgrade or wait for runs to finish",MAX_PROJECTS:"project limit reached for your plan \u2014 upgrade to add more",MAX_RUNS:"run limit reached for your plan \u2014 enable overage or upgrade",OVERAGE_CAP:"overage cap reached \u2014 raise the cap or upgrade"};function Ae({detail:e,serverUrl:t}){return je(t)?`Ripplo server at ${t} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${t} is unreachable (${e}). Tests were not started. This is a server-side issue, not your local environment \u2014 wait a moment and re-run.`}function Te(e){switch(e.code){case"compile-failed":{let t=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...t].join(`
|
|
260
|
+
`)}case"selection-conflicting-flags":return et({kind:"conflicting-flags"});case"selection-nothing-selected":return et({kind:"nothing-selected"});case"selection-unknown-ids":return et({kind:"unknown-ids",known:e.known,unknown:e.unknown});case"app-unreachable":return`Your dev server is not responding at ${e.url} (${e.detail}). Tests were not started \u2014 start your app and re-run. This is your local environment, not the Ripplo server.`;case"scope-failed":return`Could not resolve the dev-session scope: ${e.detail}
|
|
261
|
+
Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let t=/401|unauthor/i.test(e.detail)?"\nLooks like an auth failure \u2014 run `npx ripplo auth login` and retry.":"";return`Sync to the Ripplo server failed: ${e.detail}${t}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function G(e){switch(e.kind){case"spawn-failed":return`Failed to start \`npx ripplo daemon\`: ${Pa(e.message)}`;case"connect-timeout":return[`Daemon did not come up within ${String(Math.round(e.deadlineMs/1e3))}s.`,`Check ${e.logPath} for startup errors.`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server unreachable."].join(`
|
|
262
|
+
`);case"connection-lost":return["Lost the daemon connection mid-run \u2014 it likely crashed or was killed.","Check .ripplo/.local/daemon.log, restart `npx ripplo daemon`, then rerun."].join(`
|
|
263
|
+
`);case"not-running":return["No ripplo daemon running for this directory \u2014 nothing to dispatch to.","Start it with `npx ripplo daemon` as a background process (separate from your app's dev server), then rerun.","A run needs a live daemon to dispatch and to register the result to your dev session."].join(`
|
|
264
|
+
`);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function Pa(e){return e.includes("ENOENT")?`${e} \u2014 the node executable or CLI entry was not found on PATH.`:e.includes("EACCES")?`${e} \u2014 permission denied executing the CLI entry.`:e}function Ea(e){return e.type==="limit"?xa[e.code]:`failed to dispatch (${e.detail})`}function $a(e,t){return new Ca(t,{includeScore:!0,threshold:.5}).search(_(e)).slice(0,3).map(n=>n.item)}async function wn({all:e,headed:t,ids:r}){let n=process.cwd(),o=process.argv[1];o==null&&le(G({kind:"spawn-failed",message:"process.argv[1] missing"}));let i=await $e(n);i!=null&&le(Ae(i));let a=(await pn({cliEntry:o,cwd:n})).match(u=>u,u=>le(G(u)));a.versionNote!=null&&process.stderr.write(`${ar(a.versionNote)}
|
|
265
|
+
`);let l=new Ia;process.once("SIGINT",()=>{l.cancel(),a.socket.destroy(),process.exit(130)});let d=ja.join(n,".ripplo","debug"),p=(await Pe({connection:a,request:{all:e,headed:t,tests:[...r]},token:l.token,onEvent:u=>{let A=Ie({debugDir:d,event:u});A!=null&&process.stdout.write(`${A}
|
|
266
|
+
`)}})).match(u=>u,u=>le(G(u)));await Aa({connection:a,result:p})}async function Aa({connection:e,result:t}){await new Promise(r=>{e.socket.end(r)}),t.kind==="daemon-error"&&le(Te(t.error)),process.stdout.write(`${kn(t)}
|
|
267
|
+
`),t.failed>0&&process.exit(1),process.exit(t.notRun>0?2:0)}function le(e){process.stderr.write(`${e}
|
|
268
|
+
`),process.exit(1)}import de from"path";import{err as De,ok as rt}from"neverthrow";import{readdir as Ta,rm as La,stat as Da}from"fs/promises";import vn from"path";async function tt(e,t){let n=(await Ta(e,{withFileTypes:!0}).catch(()=>[])).filter(s=>s.isDirectory()).map(s=>s.name);if(n.length<=t)return;let i=(await Promise.all(n.map(async s=>({mtime:(await Da(vn.join(e,s)).catch(()=>null))?.mtimeMs??0,name:s})))).toSorted((s,a)=>a.mtime-s.mtime).slice(t);await Promise.all(i.map(s=>La(vn.join(e,s.name),{force:!0,recursive:!0}).catch(()=>{})))}var bn=50;async function Sn(){let e=process.cwd(),t=L.child({worker:process.pid}),r=yr(),n=Kt({clientVersion:P(),debugDir:de.join(e,".ripplo","debug"),headed:!1,writeOtlpPortFile:!1}),o={entry:void 0},i=s=>{n.close().catch(()=>{}).then(()=>{process.exit(s)})};process.on("disconnect",()=>{i(1)}),process.on("unhandledRejection",s=>{t.error({err:s},"worker unhandled rejection")}),process.on("uncaughtException",s=>{t.error({err:s},"worker uncaught exception"),i(1)}),r.onRequest(pr,async(s,a)=>{let l=dr.safeParse(s);if(!l.success)return Le("bad-run-assign");let d=l.data;B(e);let v=k(e).match(j=>j,j=>j.kind);if(typeof v=="string")return Le(`config:${v}`);let p=await Rn(d.lockfileFingerprint,o,r);if(p.isErr())return Le(`lockfile-unavailable:${p.error}`);let u=p.value,A=Lt(u).find(j=>j.ref===d.testRef);if(A==null)return Le(`no-test:${d.testRef}`);try{let j=await wr({config:v,cwd:e,fixturesDir:de.join(e,qe),headed:d.headed,lockfile:u,runId:d.runId,session:n,signal:Ge(a),test:A.test}),dt=de.join(e,".ripplo","debug");return await ze({config:v,debugDir:dt,runId:d.runId}),await tt(dt,bn),{outcome:lr(j),serverNotified:!0}}catch(j){if(j instanceof ut)return{outcome:{detail:j.message,kind:"infra-error"},serverNotified:!1};throw j}}),r.onRequest(fr,(s,a)=>_a({cache:o,connection:r,cwd:e,raw:s,session:n,token:a})),r.onNotification(gr,s=>{let a=cr.safeParse(s);a.success&&n.injectSpan(a.data.runId,a.data.span)}),r.onNotification(hr,()=>{i(0)}),r.sendNotification(ur),await new Promise(()=>{})}function Le(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function _a({cache:e,connection:t,cwd:r,raw:n,session:o,token:i}){let s=Ht.safeParse(n);if(!s.success)return{kind:"error",reason:"bad-assign",trail:[]};let a=s.data;B(r);let l=k(r).match(A=>A,A=>A.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,trail:[]};let d=await Rn(a.lockfileFingerprint,e,t);if(d.isErr())return{kind:"error",reason:`lockfile:${d.error}`,trail:[]};let v=d.value,p=de.join(l.cwd,".ripplo","debug"),u=await vr({assign:a,config:l,debugDir:p,devSessionId:void 0,fixturesDir:de.join(l.cwd,qe),headed:!1,lockfile:v,report:!0,session:o,signal:Ge(i)});return u.kind==="finding"&&u.runId!=null&&await ze({config:l,debugDir:p,runId:u.runId}),await tt(p,bn),u}async function Rn(e,t,r){return t.entry!=null&&t.entry.fingerprint===e?rt(t.entry.lockfile):(await Oa(r,e)).andThen(o=>o.unavailable!=null?De(o.unavailable):o.lockfileJson==null?De("empty-reply"):Na(o.lockfileJson).map(i=>(t.entry={fingerprint:e,lockfile:i},i)))}async function Oa(e,t){try{return rt(await e.sendRequest(mr,{fingerprint:t}))}catch(r){return De(`transport:${r instanceof Error?r.message:"unknown"}`)}}function Na(e){try{return rt(St(U,e))}catch(t){return De(`worker-decode-failed:${t instanceof Error?t.message.slice(0,200):"unknown"}`)}}async function Cn(){await Sn()}import{graphql as Ua}from"gql.tada";import{err as xn,ok as Fa}from"neverthrow";async function Pn(e){let t=k(e).match(n=>n,n=>n.kind);if(typeof t=="string")return xn({detail:t,kind:"config"});let r=await m({config:t,document:Ma,variables:{projectId:t.projectId}}).catch(()=>null);return r==null?xn({kind:"request-failed"}):Fa(r.exploreFindings.map(n=>({baseState:n.baseState,category:n.category,occurrences:n.occurrences,reproRunId:n.reproRun?.id??null,signature:n.signature})))}var Ma=Ua(`
|
|
269
|
+
query ExploreFindingsList($projectId: String!) {
|
|
270
|
+
exploreFindings(projectId: $projectId, status: open) {
|
|
271
|
+
id
|
|
272
|
+
baseState
|
|
273
|
+
category
|
|
274
|
+
occurrences
|
|
275
|
+
signature
|
|
276
|
+
reproRun {
|
|
277
|
+
id
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
`);function En(e){if(e.length===0)return"No open findings.";let t=`${x(e.length,"open finding")}. Pull a run's evidence with \`npx ripplo pull <runId>\`.`,r=e.map(n=>Ha(n));return[t,"",...r].join(`
|
|
282
|
+
`)}function $n(e){return e.kind==="config"?`Can't read project config (${e.detail}). Run this from your project root.`:"Couldn't reach the server. Check you're signed in with `npx ripplo auth`."}function Ha(e){let t=`${e.category} x${String(e.occurrences)}`.padEnd(14),r=e.reproRunId??"(no repro run)";return` ${t} ${r}
|
|
283
|
+
${e.baseState}`}async function jn({json:e}){(await Pn(process.cwd())).match(r=>{process.stdout.write(e?`${JSON.stringify(r,null,2)}
|
|
284
|
+
`:`${En(r)}
|
|
285
|
+
`)},r=>{process.stderr.write(`${$n(r)}
|
|
286
|
+
`),process.exit(1)})}import{mkdir as Wa,writeFile as qa}from"fs/promises";import In from"path";import{graphql as Ba}from"gql.tada";import{err as ce,ok as Va}from"neverthrow";async function An(e,t){let r=k(e).match(s=>s,s=>s.kind);if(typeof r=="string")return ce({detail:r,kind:"config"});let n=await m({config:r,document:Ga,variables:{id:t}}).catch(()=>null);if(n==null)return ce({kind:"request-failed"});if(n.run==null)return ce({kind:"not-found"});if(n.run.behaviorUrl==null)return ce({kind:"not-uploaded"});let o=await fetch(n.run.behaviorUrl);if(!o.ok)return ce({kind:"download-failed",status:o.status});let i=In.join(e,".ripplo","debug",t,"behavior.jsonl");return await Wa(In.dirname(i),{recursive:!0}),await qa(i,Buffer.from(await o.arrayBuffer())),Va({path:i,runId:t})}var Ga=Ba(`
|
|
287
|
+
query RunBehaviorUrl($id: String!) {
|
|
288
|
+
run(id: $id) {
|
|
289
|
+
id
|
|
290
|
+
behaviorUrl
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`);function Tn(e){return`Saved behavior.jsonl to ${e.path}`}function Ln(e){switch(e.kind){case"config":return`Can't read project config (${e.detail}). Run this from your project root.`;case"request-failed":return"Couldn't reach the server. Check you're signed in with `npx ripplo auth`.";case"not-found":return"No run with that id.";case"not-uploaded":return"This run has no uploaded artifacts yet \u2014 it may still be running, or predates artifact upload.";case"download-failed":return`Download failed (HTTP ${String(e.status)}). The link may have expired \u2014 try again.`}}async function Dn({runId:e}){(await An(process.cwd(),e)).match(r=>{process.stdout.write(`${Tn(r)}
|
|
294
|
+
`)},r=>{process.stderr.write(`${Ln(r)}
|
|
295
|
+
`),process.exit(1)})}import Un from"path";import{existsSync as za}from"fs";import{writeFile as _n}from"fs/promises";import On from"path";import{err as ue,ok as Ja}from"neverthrow";async function Nn({jsonlPath:e,moment:t,outDir:r,runId:n}){if(!za(e))return ue({kind:"run-not-found",runId:n});let o=await Wt(e),i=o[0],s=o.at(-1);if(i==null||s==null)return ue({kind:"no-rrweb-events",runId:n});let a=s.timestamp-i.timestamp,l=t.kind==="offset"?t.offsetMs:t.at-i.timestamp;if(l<0)return ue({durationMs:a,firstTimestamp:i.timestamp,kind:"moment-out-of-range",lastTimestamp:s.timestamp,moment:t});let d=Math.min(l,a),v=String(Math.round(d)),p=On.join(r,`snapshot-${v}ms.png`),u=On.join(r,`snapshot-${v}ms.html`);return(await Ka({events:o,htmlPath:u,offsetMs:d,pngPath:p})).map(()=>({durationMs:a,htmlPath:u,offsetMs:d,pngPath:p}))}async function Ka({events:e,htmlPath:t,offsetMs:r,pngPath:n}){let o=await zt();if(o==null)return ue({detail:"chromium launch",kind:"browser-failed"});try{let{html:i,png:s}=await Qa({browser:o,events:e,offsetMs:r});return await _n(n,s),await _n(t,i,"utf8"),Ja(void 0)}catch(i){return ue({detail:i instanceof Error?i.message:String(i),kind:"browser-failed"})}finally{await o.close()}}async function Qa({browser:e,events:t,offsetMs:r}){let n=qt(t),o=await e.newPage({viewport:n});await o.setContent(Bt(!1)),await o.addScriptTag({content:await Jt()}),await o.evaluate(Xa({events:t,offsetMs:r})),await o.evaluate(Gt());let i=o.locator(".replayer-wrapper iframe").first(),a=await(await i.count()>0?i:o).screenshot({type:"png"});return{html:await o.evaluate(Ya()),png:a}}function Xa({events:e,offsetMs:t}){return`(() => {
|
|
292
296
|
const replayer = new globalThis.__RipploReplayer(${JSON.stringify(e)}, {
|
|
293
|
-
insertStyleRules: [${JSON.stringify(
|
|
297
|
+
insertStyleRules: [${JSON.stringify(Vt)}],
|
|
294
298
|
mouseTail: false,
|
|
295
299
|
root: document.body,
|
|
296
300
|
showWarning: false,
|
|
297
301
|
});
|
|
298
|
-
replayer.pause(${String(
|
|
302
|
+
replayer.pause(${String(t+.5)});
|
|
299
303
|
globalThis.__ripploSnapshotReplayer = replayer;
|
|
300
|
-
})()`}function
|
|
304
|
+
})()`}function Ya(){return`(() => {
|
|
301
305
|
const replayer = globalThis.__ripploSnapshotReplayer;
|
|
302
306
|
if (replayer == null) {
|
|
303
307
|
return "";
|
|
@@ -315,33 +319,33 @@ Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass t
|
|
|
315
319
|
}
|
|
316
320
|
});
|
|
317
321
|
return "<!doctype html>\\n" + doc.documentElement.outerHTML;
|
|
318
|
-
})()`}function
|
|
319
|
-
`)}function
|
|
320
|
-
`),process.exit(1)),(await
|
|
321
|
-
`)},i=>{process.stderr.write(`${
|
|
322
|
-
`),process.exit(1)})}function
|
|
322
|
+
})()`}function Fn({cwd:e,moment:t,runId:r}){let n=Un.join(e,".ripplo","debug",r);return Nn({jsonlPath:Un.join(n,"behavior.jsonl"),moment:t,outDir:n,runId:r})}function Mn(e){return[`${C.good("ok")} \u2014 rendered the page at ${String(Math.round(e.offsetMs))}ms into the recording (duration ${String(Math.round(e.durationMs))}ms)`,e.pngPath,"Read the PNG above to see the page state. Layout and text are faithful \u2014 URL-referenced images may be blank if the dev server is down."].join(`
|
|
323
|
+
`)}function Hn(e){return`${C.bad("fail")} \u2014 ${Za(e)}`}function Wn(){return`${C.bad("fail")} \u2014 pass exactly one of --at <epoch-ms from behavior.jsonl> or --offset <ms from the start of the recording>.`}function Za(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${c("run")}`;case"no-rrweb-events":return`run ${e.runId} has a behavior.jsonl but no rrweb events \u2014 nothing to replay.`;case"moment-out-of-range":return`${e.moment.kind==="offset"?"--offset":"--at"} is outside the recording, which spans ${String(e.firstTimestamp)}\u2013${String(e.lastTimestamp)} (duration ${String(e.durationMs)}ms). Pass --at <epoch-ms from behavior.jsonl> or --offset <0\u2013${String(e.durationMs)}>.`;case"browser-failed":return`replay browser failed (${e.detail}). Is chromium installed? Try \`npx playwright install chromium\`.`}}async function qn({at:e,offset:t,runId:r}){let n=el({at:e,offset:t});n==null&&(process.stderr.write(`${Wn()}
|
|
324
|
+
`),process.exit(1)),(await Fn({cwd:process.cwd(),moment:n,runId:r})).match(i=>{process.stdout.write(`${Mn(i)}
|
|
325
|
+
`)},i=>{process.stderr.write(`${Hn(i)}
|
|
326
|
+
`),process.exit(1)})}function el({at:e,offset:t}){if(e!=null&&t==null)return{at:e,kind:"absolute"};if(t!=null&&e==null)return{kind:"offset",offsetMs:t}}function Bn(e,t){switch(e.kind){case"config-failed":return E(e.failure);case"compile-failed":return R(e.error);case"no-test":return`test "${e.ref}" not found \u2014 run npx ripplo lint to see available tests`;case"run-failed":return`teleport failed before reaching the step: ${Tt(e.error)}`;case"done":return tl(e.result,t)}}function Vn(e){return e.kind==="done"&&e.result.kind==="handed-over"}function tl(e,t){return e.kind==="failed"?`couldn't reach the step \u2014 the run failed first.
|
|
323
327
|
|
|
324
|
-
${e.findings.map(
|
|
328
|
+
${e.findings.map(n=>Nt({evidencePath:void 0,finding:n})).join(`
|
|
325
329
|
|
|
326
|
-
`)}`:`landed at step ${String(e.ran)} of ${
|
|
327
|
-
`),process.exit(
|
|
328
|
-
`),process.exit(1)}),
|
|
329
|
-
`)}catch(
|
|
330
|
+
`)}`:`landed at step ${String(e.ran)} of ${t} \u2014 the browser is yours, close the window to tear down`}async function Gn({ref:e,step:t}){let r=new AbortController;process.once("SIGINT",()=>{r.abort()});let n=await br({cwd:process.cwd(),ref:e,signal:r.signal,step:t});process.stdout.write(`${Bn(n,e)}
|
|
331
|
+
`),process.exit(Vn(n)?0:1)}async function zn(){let e=I();try{let r=(await ne(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${E(i)}
|
|
332
|
+
`),process.exit(1)}),n=r.lockfile.workflows.length,o=r.lockfile.entities.length;process.stdout.write(`Synced ${x(n,"workflow")} and ${x(o,"entity","entities")} to dev session ${r.devSessionId}
|
|
333
|
+
`)}catch(t){let r=t instanceof Error?t.message:String(t);process.stderr.write(`ripplo sync failed: ${r}
|
|
330
334
|
`),process.stderr.write(`${c("setup","verify auth + server reachability")}
|
|
331
|
-
`),process.exit(1)}}import{spawnSync as
|
|
332
|
-
`),
|
|
333
|
-
`),process.exit(1)),
|
|
334
|
-
`),process.stdout.write(`${
|
|
335
|
-
`),process.exit(0));let t
|
|
336
|
-
`),await
|
|
337
|
-
`)}function
|
|
338
|
-
`),process.exit(0)),
|
|
339
|
-
`);return}
|
|
340
|
-
`),process.exit(1)}process.stdout.write(`${
|
|
341
|
-
`)}async function
|
|
342
|
-
`)}async function
|
|
343
|
-
`),process.exit(1)),
|
|
344
|
-
`)}import{graphql as
|
|
335
|
+
`),process.exit(1)}}import{spawnSync as ol}from"child_process";import il from"fs";import sl from"path";import al from"semver";import _e from"fs";import rl from"os";import pe from"path";function Jn(e){return e.split(pe.sep).includes("_npx")?"npx":e.includes(pe.join("packages","cli"))?"workspace":"global"}function Kn(){let e=pe.join(rl.homedir(),".npm","_npx");return _e.existsSync(e)?_e.readdirSync(e).map(t=>pe.join(e,t)).filter(t=>_e.existsSync(pe.join(t,"node_modules","ripplo"))).map(t=>(_e.rmSync(t,{force:!0,recursive:!0}),t)):[]}import{spawnSync as Qn}from"child_process";var nl=["user","project","local"];function nt(){return Qn("claude",["--version"],{stdio:"ignore"}).error!=null?"claude-missing":nl.find(r=>Qn("claude",["plugin","update","ripplo","--scope",r],{stdio:"ignore"}).status===0)==null?"not-installed":"updated"}function Xn(e){return`ripplo v${e} is already the latest version.`}function Yn(e){return`ripplo v${e}: could not reach the npm registry to check for updates.`}function ot({evicted:e,latest:t,mode:r}){return r==="npx"?`ripplo: ${e===0?"npx cache had no stale copy":`cleared ${x(e,"stale npx cache entry","stale npx cache entries")}`} \u2014 the next \`npx ripplo\` command runs v${t}.`:`ripplo: updated global install to v${t}.`}function Zn(){return"ripplo: this is a workspace build (packages/cli/dist) \u2014 update via git pull + `pnpm --filter ripplo build`."}function eo(e){return`ripplo: \`npm install -g ripplo@latest\` failed: ${e}`}function it(e){switch(e){case"updated":return"ripplo: Claude plugin updated \u2014 restart Claude Code (or /reload-plugins) to apply.";case"not-installed":return"ripplo: Claude plugin not installed in this project \u2014 skipped.";case"claude-missing":return"ripplo: claude binary not found \u2014 skipped plugin update."}}function to(e){return e?"ripplo: idle daemon stopped \u2014 the next `npx ripplo run` restarts it on the new version.":"ripplo: daemon is busy \u2014 it picks up the new version once idle (the next `npx ripplo run` handles it)."}var ll=1e4;async function ro(){let e=P(),t=process.argv[1];dl(process.cwd());let r=await ke(ll);process.stdout.write(`${ye({current:e,latest:r})}
|
|
336
|
+
`),r==null&&(process.stderr.write(`${Yn(e)}
|
|
337
|
+
`),process.exit(1)),al.gt(r,e)||(process.stdout.write(`${Xn(e)}
|
|
338
|
+
`),process.stdout.write(`${it(nt())}
|
|
339
|
+
`),process.exit(0));let n=t==null?"global":Jn(t);cl({latest:r,mode:n}),process.stdout.write(`${it(nt())}
|
|
340
|
+
`),await pl(process.cwd()),process.exit(0)}function dl(e){if(!il.existsSync(sl.join(e,".ripplo")))return;let t=Se(e);t==="written"&&process.stdout.write(`${Re(t).trim()}
|
|
341
|
+
`)}function cl({latest:e,mode:t}){if(t==="workspace"&&(process.stdout.write(`${Zn()}
|
|
342
|
+
`),process.exit(0)),t==="npx"){let r=Kn();process.stdout.write(`${ot({evicted:r.length,latest:e,mode:t})}
|
|
343
|
+
`);return}ul(e)}function ul(e){let t=`ripplo@${e}`,r=ol("npm",["install","-g",t],{stdio:"inherit"});if(r.status!==0){let n=`exit ${String(r.status)}`;process.stderr.write(`${eo(n)}
|
|
344
|
+
`),process.exit(1)}process.stdout.write(`${ot({evicted:0,latest:e,mode:"global"})}
|
|
345
|
+
`)}async function pl(e){if((await Ee(e)).kind==="not-running")return;let r=await gn(e);process.stdout.write(`${to(r)}
|
|
346
|
+
`)}async function no({executor:e}){let{runDaemon:t}=await import("./daemon-G75H2VRS.js"),{TunnelProvisionFailedError:r}=await import("./daemon-tunnel-DYA5LIEG.js"),{renderTunnelProvisionFailed:n}=await import("./daemon-PTZWFEBG.js");try{await t({executor:e})}catch(o){throw o instanceof r&&(process.stderr.write(`${n(o.failure)}
|
|
347
|
+
`),process.exit(1)),o}}import{graphql as Oe}from"gql.tada";function oo(){return"No scope items added \u2014 the matched tests are already in scope (check `npx ripplo scope status`)."}function io(e){return[`No test found for: ${e.join(", ")}`,"Pass a test id (kebab-case slug of the test name) or the exact intent string.","List known tests with `npx ripplo status`. To add one, stub it first via the testing DSL.",c("create")].join(`
|
|
348
|
+
`)}import{graphql as ml}from"gql.tada";var so=ml(`
|
|
345
349
|
query ScopeStatus($projectId: String!, $cwd: String!) {
|
|
346
350
|
project(id: $projectId) {
|
|
347
351
|
id
|
|
@@ -361,7 +365,7 @@ ${e.findings.map(t=>zr({evidencePath:void 0,finding:t})).join(`
|
|
|
361
365
|
}
|
|
362
366
|
}
|
|
363
367
|
}
|
|
364
|
-
`);var
|
|
368
|
+
`);var fl=Oe(`
|
|
365
369
|
query ScopeWorkflowBySlug($projectId: String!, $cwd: String!, $slug: String!) {
|
|
366
370
|
project(id: $projectId) {
|
|
367
371
|
id
|
|
@@ -374,7 +378,7 @@ ${e.findings.map(t=>zr({evidencePath:void 0,finding:t})).join(`
|
|
|
374
378
|
}
|
|
375
379
|
}
|
|
376
380
|
}
|
|
377
|
-
`),
|
|
381
|
+
`),gl=Oe(`
|
|
378
382
|
mutation ScopeAddDirtyTests($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
379
383
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
380
384
|
__typename
|
|
@@ -396,38 +400,38 @@ ${e.findings.map(t=>zr({evidencePath:void 0,finding:t})).join(`
|
|
|
396
400
|
}
|
|
397
401
|
}
|
|
398
402
|
}
|
|
399
|
-
`),
|
|
403
|
+
`),hl=Oe(`
|
|
400
404
|
mutation ScopeLink($id: ID!, $workflowId: String!) {
|
|
401
405
|
linkScopeItem(id: $id, workflowId: $workflowId) {
|
|
402
406
|
id
|
|
403
407
|
}
|
|
404
408
|
}
|
|
405
|
-
`),
|
|
409
|
+
`),yl=Oe(`
|
|
406
410
|
mutation ScopeRemoveMany($ids: [ID!]!) {
|
|
407
411
|
removeScopeItems(ids: $ids)
|
|
408
412
|
}
|
|
409
|
-
`);async function
|
|
410
|
-
`);return}if(
|
|
411
|
-
`);return}let
|
|
412
|
-
`)})}async function
|
|
413
|
-
`),process.exit(1)});let o=(await m({config:
|
|
414
|
-
`),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${
|
|
415
|
-
`),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${
|
|
416
|
-
`);return}let
|
|
417
|
-
`)}async function
|
|
418
|
-
`),process.exit(1)});let o=await
|
|
419
|
-
`)}async function
|
|
420
|
-
`)}async function
|
|
413
|
+
`);async function ao(e){let t=I();await oe(t);let n=(await m({config:t,document:so,variables:{cwd:t.cwd,projectId:t.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
|
|
414
|
+
`);return}if(n.length===0){process.stdout.write("No scope items. Add some with `npx ripplo scope add <test-ids..>` or from the dashboard.\n");return}n.forEach(o=>{let i=o.workflow;if(i==null){process.stdout.write(` [intent] (${o.id}) ${o.label??""}
|
|
415
|
+
`);return}let s=i.spec==null?"stub":"implemented";process.stdout.write(` [${s}] (${o.id}) ${i.slug} \u2014 ${i.name}
|
|
416
|
+
`)})}async function lo({testIds:e}){let t=I();await oe(t),(await ne(t.cwd,t)).match(()=>{},a=>{process.stderr.write(`${E(a)}
|
|
417
|
+
`),process.exit(1)});let o=(await m({config:t,document:gl,variables:{cwd:t.cwd,projectId:t.projectId,workflowSlugs:e.map(a=>_(a))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
|
|
418
|
+
`),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${io(o.slugs)}
|
|
419
|
+
`),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${oo()}
|
|
420
|
+
`);return}let s=i.map(a=>a.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${x(i.length,"scope item")}: ${s}
|
|
421
|
+
`)}async function co({id:e,testId:t}){let r=I();await oe(r),(await ne(r.cwd,r)).match(()=>{},i=>{process.stderr.write(`${E(i)}
|
|
422
|
+
`),process.exit(1)});let o=await kl({cfg:r,slug:t});await m({config:r,document:hl,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${t}
|
|
423
|
+
`)}async function uo({ids:e}){let t=I();await oe(t);let n=(await m({config:t,document:yl,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${x(n,"scope item")}
|
|
424
|
+
`)}async function kl({cfg:e,slug:t}){let n=(await m({config:e,document:fl,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.
|
|
421
425
|
`),process.stderr.write(`${c("create")}
|
|
422
|
-
`),process.exit(1)),
|
|
423
|
-
`),process.exit(1));let
|
|
424
|
-
`),process.stdout.write(`${
|
|
425
|
-
`);return}let i={daemon:o.kind==="running"?{active:o.status.active,explorer:o.status.explorer,exploring:o.status.exploring,progress:o.status.progress,queued:o.status.queued,running:!0}:{running:o.kind==="unresponsive",state:o.kind},tests:
|
|
426
|
-
`)}import
|
|
427
|
-
`),hookEventName:"UserPromptSubmit"}}});import
|
|
428
|
-
`).filter(
|
|
429
|
-
`).filter(
|
|
430
|
-
`).filter(
|
|
426
|
+
`),process.exit(1)),n.id}async function po(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${R(r.error)}
|
|
427
|
+
`),process.exit(1));let n=N(r.value),o=await Ee(t);if(e.format==="summary"){n.length>0&&process.stdout.write(`stub workflows: ${n.join(", ")}
|
|
428
|
+
`),process.stdout.write(`${sr(o)}
|
|
429
|
+
`);return}let i={daemon:o.kind==="running"?{active:o.status.active,explorer:o.status.explorer,exploring:o.status.exploring,progress:o.status.progress,queued:o.status.queued,running:!0}:{running:o.kind==="unresponsive",state:o.kind},tests:n.map(s=>({id:s,implemented:!1}))};process.stdout.write(`${JSON.stringify(i,null,2)}
|
|
430
|
+
`)}import Ne from"fs";import vl from"os";import mo from"path";import{z as wl}from"zod";function g(e,t){let r=wl.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}}var fo=g("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!w(e.cwd))return;let t=bl();if(t==null)return{hookSpecificOutput:{additionalContext:`Ripplo plan gate: no plan file found \u2014 the "Tests to implement" requirement was not checked. Before implementing, stub a .ripplo/workflows/ workflow per affected flow. ${c("discover")}`,hookEventName:"PreToolUse"}};let r=Ne.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/workflows/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${c("discover")}`}}});function bl(){let e=mo.join(vl.homedir(),".claude","plans");return Ne.existsSync(e)?Ne.readdirSync(e).filter(r=>r.endsWith(".md")).map(r=>mo.join(e,r)).map(r=>({full:r,mtime:Ne.statSync(r).mtimeMs})).sort((r,n)=>n.mtime-r.mtime)[0]?.full??null:null}var go=g("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!f(e.cwd)||!w(e.cwd))return;let t=await S(e.cwd);if(t.isErr())return;let r=N(t.value),n=['Plan must include "Tests to implement" with a .ripplo/workflows/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `workflow("Intent")` (no body).'];return r.length>0&&n.push(`Existing stubs: ${r.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
|
|
431
|
+
`),hookEventName:"UserPromptSubmit"}}});import Dl from"path";import Co from"picomatch";import{z as xo}from"zod";import{mkdirSync as Pl,readFileSync as El,writeFileSync as $l}from"fs";import jl from"path";import{z as b}from"zod";import{createHash as Wg}from"crypto";import Ue from"picomatch";function Sl(e){return O(["diff","--name-only","HEAD"],e).split(`
|
|
432
|
+
`).filter(t=>t.length>0)}function ho({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ue([...r]),o=Ue([...t]);return Sl(e).filter(i=>n(i)&&!o(i))}function Rl(e){return O(["ls-files","--others","--exclude-standard"],e).split(`
|
|
433
|
+
`).filter(t=>t.length>0)}function yo({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ue([...r]),o=Ue([...t]);return Rl(e).filter(i=>n(i)&&!o(i))}var Cl=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],xl=["**/*.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/**"];function Z(){return{ignorePaths:xl,watchPaths:Cl}}var Il=b.object({label:b.string().nullable(),slug:b.string().nullable(),status:b.enum(["intent","stub","implemented"])}),Al=b.object({intent:b.string(),name:b.string(),sourcePath:b.string().nullable(),stub:b.boolean()}),Tl=b.object({changedAppFiles:b.array(b.string()).readonly(),scope:b.object({available:b.boolean(),items:b.array(Il).readonly()}),tests:b.array(Al).readonly().default([]),untrackedAppFiles:b.array(b.string()).readonly()});function ko({cwd:e,scope:t}){let r=vo(e);So(e,{...Ro(e),scope:t,tests:r?.tests??[]})}function wo({cwd:e,tests:t}){let r=vo(e);So(e,{...Ro(e),scope:r?.scope??{available:!1,items:[]},tests:t??r?.tests??[]})}function vo(e){let t=Tl.safeParse(Ll(bo(e)));return t.success?t.data:null}function Ll(e){try{return JSON.parse(El(e,"utf8"))}catch{return null}}function bo(e){return mt(e,"coverage-context.json")}function So(e,t){let r=bo(e);Pl(jl.dirname(r),{recursive:!0}),$l(r,JSON.stringify(t,null,2))}function Ro(e){let{ignorePaths:t,watchPaths:r}=Z(),n={cwd:e,ignoreGlobs:t,watchGlobs:r};return{changedAppFiles:ho(n),untrackedAppFiles:yo(n)}}var _l=xo.looseObject({file_path:xo.string()}),Po=g("PostToolUse",async e=>{let t=_l.safeParse(e.tool_input);if(!t.success)return;let r=t.data.file_path,{cwd:n}=e;if(!f(n)||!w(n))return;let o=Dl.relative(n,r);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:s}=Z(),a=Co([...s]),l=Co([...i]);if(!a(o)||l(o))return;let d=await S(n);if(wo({cwd:n,tests:d.isOk()?d.value.workflows.map(p=>({intent:p.intent,name:p.name,sourcePath:p.sourcePath??null,stub:p.stub})):void 0}),d.isErr())return;let v=N(d.value);if(v.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub workflows still unimplemented \u2014 ${v.join(", ")}. Implement with \`workflow("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as Vl}from"crypto";import{z as Ao}from"zod";import{createHash as Ol}from"crypto";import{mkdirSync as Nl,readFileSync as Eo,writeFileSync as Ul}from"fs";import st from"path";var Fl=[".ts",".tsx",".js",".jsx"];function Fe(e){let t=Ol("sha256");return t.update(O(["rev-parse","HEAD"],e)),t.update("\0"),t.update(O(["diff","HEAD"],e)),t.update("\0"),O(["ls-files","--others","--exclude-standard"],e).split(`
|
|
434
|
+
`).filter(n=>n.length>0).filter(n=>Fl.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{t.update(n),t.update("\0"),t.update(Ml(st.join(e,n))),t.update("\0")}),t.digest("hex")}function ee(e,t){try{return Eo($o(e,t),"utf8").trim()}catch{return null}}function te(e,t,r){let n=$o(e,t);Nl(st.dirname(n),{recursive:!0}),Ul(n,r)}function Ml(e){try{return Eo(e)}catch{return Buffer.alloc(0)}}function $o(e,t){return st.join(e,".ripplo",".local",`${t}.hash`)}import{graphql as Hl}from"gql.tada";var Wl=Hl(`
|
|
431
435
|
mutation AutoScopeAddDirty($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
432
436
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
433
437
|
__typename
|
|
@@ -438,11 +442,11 @@ ${e.findings.map(t=>zr({evidencePath:void 0,finding:t})).join(`
|
|
|
438
442
|
}
|
|
439
443
|
}
|
|
440
444
|
}
|
|
441
|
-
`);async function
|
|
442
|
-
`).map(
|
|
443
|
-
${c("create","DSL authoring + lint rules")}`};let o=M(
|
|
444
|
-
${c("create")}`};let{addedSlugs:i}=await
|
|
445
|
-
`),hookEventName:"PostToolUse"}}}function
|
|
445
|
+
`);async function Io({cwd:e,lockfile:t}){if(!w(e))return{addedSlugs:[]};let r=Bl(e);if(r.length===0)return{addedSlugs:[]};let n=new Set(r),o=t.workflows.filter(l=>l.sourcePath!=null&&n.has(l.sourcePath)).map(l=>_(l.name));if(o.length===0)return{addedSlugs:[]};let i=k(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await kr({config:i,cwd:e,lockfile:t}).catch(l=>(L.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await ql({cfg:i,slugs:o})?o:[]}}async function ql({cfg:e,slugs:t}){let n=(await m({config:e,document:Wl,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...t]}}).catch(o=>(L.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return n?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:n.data.length>0}var jo=".ripplo/workflows/";function Bl(e){let t;try{t=O(["status","--porcelain","--",".ripplo/workflows"],e)}catch{return[]}return t.split(`
|
|
446
|
+
`).map(r=>r.slice(3).trim()).filter(r=>r.startsWith(jo)&&r.endsWith(".ts")).map(r=>r.slice(jo.length)).filter(r=>r!=="index.ts"&&!r.endsWith("/index.ts"))}var Gl=Ao.looseObject({file_path:Ao.string()}),To=g("PostToolUse",async e=>{let t=Gl.safeParse(e.tool_input);if(!t.success||!/\/\.ripplo\/.*\.ts$/.test(t.data.file_path))return;let{cwd:r}=e;if(!f(r))return;if(!w(r))return{hookSpecificOutput:{additionalContext:"Ripplo hooks are paused \u2014 DSL lint and lockfile sync did not run for this edit. Resume with `npx ripplo hooks resume` when ready.",hookEventName:"PostToolUse"}};let n=await K(r);if(n.isErr())return{decision:"block",reason:`${R(n.error)}
|
|
447
|
+
${c("create","DSL authoring + lint rules")}`};let o=M(n.value);if(o.length>0)return{decision:"block",reason:`${H(o)}
|
|
448
|
+
${c("create")}`};let{addedSlugs:i}=await Io({cwd:r,lockfile:n.value});return zl([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Jl(r,n.value)])});function zl(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
|
|
449
|
+
`),hookEventName:"PostToolUse"}}}function Jl(e,t){let r=Ce(t),n=Vl("sha256").update(JSON.stringify(r)).digest("hex");return ee(e,"coverage-warn")===n?[]:(te(e,"coverage-warn",n),r.length===0?[]:[we(r)])}import{z as Lo}from"zod";var Kl=Lo.looseObject({command:Lo.string()}),Ql=/\bripplo\s+hooks\s+pause\b/,Do=g("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=Kl.safeParse(e.tool_input);if(!t.success||!Ql.test(t.data.command))return;let{cwd:r}=e;if(f(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 the daemon 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 td}from"shell-quote";import{z as Oo}from"zod";import{existsSync as Xl,mkdirSync as Yl,rmSync as qh,writeFileSync as Zl}from"fs";import at from"path";function Me(e,t,r){let n=_o(e,t,r);Yl(at.dirname(n),{recursive:!0}),Zl(n,"")}function He(e,t,r){return Xl(_o(e,t,r))}function _o(e,t,r){return at.join(ed(e,t),r)}function ed(e,t){return at.join(e,".ripplo",".local","skills-loaded",t)}var rd=Oo.looseObject({command:Oo.string()}),nd="run",od=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),Uo=g("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=rd.safeParse(e.tool_input);if(!t.success)return;let r=ld(t.data.command),n=id(r);if(n==null)return;let{cwd:o}=e;if(!f(o)||!w(o))return;if(!He(o,e.session_id,n.skill))return No(n.reason);let i=n.kind==="run"?cd(r):null;if(i!=null)return No(`Don't pipe \`ripplo run\` through \`${i}\` \u2014 buffering filters hide live progress, and killing the pipeline mid-run orphans the run on the server. Redirect to a file instead (\`npx ripplo run <id> > /tmp/run.log 2>&1\` and Read it), or use \`run_in_background: true\`.`)});function id(e){return sd(e)?{kind:"replay",reason:"Running `ripplo explore replay` requires the `/ripplo:fuzz` skill loaded first. Load `/ripplo:fuzz` then retry \u2014 it carries the triage loop and the add-vs-weaken guardrail for resolving findings.",skill:"fuzz"}:ad(e)?{kind:"run",reason:"Running `ripplo run` requires the `/ripplo:run` skill loaded first. Load `/ripplo:run` then retry \u2014 it carries the artifact-read order, the no-grep-piping guidance, the failure decision tree, and the caught-bug filing contract.",skill:nd}:null}function sd(e){return e.some((t,r)=>t==="ripplo"&&e[r+1]==="explore"&&e[r+2]==="replay")}function ad(e){return e.some((t,r)=>t==="ripplo"&&e[r+1]==="run")}function ld(e){try{return td(e)}catch{return[]}}function dd(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function cd(e){let t=e.find((r,n)=>{let o=e[n-1];return o!=null&&dd(o)&&typeof r=="string"&&od.has(r)});return typeof t=="string"?t:null}function No(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import ud from"path";import{z as Fo}from"zod";var pd=new Set(["Edit","Write","NotebookEdit"]),md=Fo.looseObject({file_path:Fo.string()}),fd="create",Mo=g("PreToolUse",e=>{if(!pd.has(e.tool_name))return;let t=md.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!f(r)||!w(r))return;let n=ud.relative(r,t.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!He(r,e.session_id,fd))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 gd from"path";import Ho from"picomatch";import{graphql as hd}from"gql.tada";import{z as Wo}from"zod";var yd=new Set(["Edit","Write","NotebookEdit"]),kd=Wo.looseObject({file_path:Wo.string()}),wd=hd(`
|
|
446
450
|
query PreEditScopeGate($projectId: String!, $cwd: String!) {
|
|
447
451
|
project(id: $projectId) {
|
|
448
452
|
id
|
|
@@ -454,9 +458,9 @@ ${c("create")}`};let{addedSlugs:i}=await ti({cwd:n,lockfile:t.value});return jd(
|
|
|
454
458
|
}
|
|
455
459
|
}
|
|
456
460
|
}
|
|
457
|
-
`),
|
|
458
|
-
${
|
|
459
|
-
${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running \u2014 this edit to \`${
|
|
461
|
+
`),qo=g("PreToolUse",async e=>{if(!yd.has(e.tool_name))return;let t=kd.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!f(r)||!w(r))return;let n=gd.relative(r,t.data.file_path);if(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!vd(n))return;let o=await bd(r);return o.populated?o.degradedReason!=null?{hookSpecificOutput:{additionalContext:`Scope check skipped (${o.degradedReason}) \u2014 edit allowed through, but the scope guardrail isn't enforcing on this edit.`,hookEventName:"PreToolUse"}}:void 0:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Scope is empty but this 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). ${Rt(["run","create"])}`}}});function vd(e){let{ignorePaths:t,watchPaths:r}=Z(),n=Ho([...r]),o=Ho([...t]);return n(e)&&!o(e)}async function bd(e){let t=k(e).unwrapOr(void 0);if(t==null)return{degradedReason:"no project config \u2014 `npx ripplo init` not run here",populated:!0};let r=await m({config:t,document:wd,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null);return r==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(r.project?.devSession?.scopeItems??[]).length>0}}import Sd from"fs";import Rd from"path";import{z as Bo}from"zod";var Cd=new Set(["Edit","Write","NotebookEdit"]),xd=Bo.looseObject({file_path:Bo.string()}),Vo=g("PreToolUse",async e=>{if(!Cd.has(e.tool_name))return;let t=Pd(e);if(t==null)return;let{cwd:r}=e;if(Sd.existsSync(re(r))||ge(r))return;let n=await S(r);return n.isErr()?{hookSpecificOutput:{additionalContext:`\`ripplo daemon\` isn't running and the DSL is currently failing to compile, so the daemon gate is letting this edit through \u2014 fix the compile error before relying on dev-mode guardrails. Compile error:
|
|
462
|
+
${R(n.error)}
|
|
463
|
+
${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running \u2014 this edit to \`${t}\` won't sync and dev-mode guardrails won't fire. Run \`/ripplo:start\`, then retry (\`npx ripplo doctor\` checks the daemon and dev server). If the daemon can't start, the user can bypass with \`npx ripplo hooks pause\`. ${c("start")}`}}});function Pd(e){let t=xd.safeParse(e.tool_input);if(!t.success||!f(e.cwd))return null;let r=Rd.relative(e.cwd,t.data.file_path);return r.startsWith("..")||r!==".ripplo"&&!r.startsWith(".ripplo/")?null:r}import{graphql as Ed}from"gql.tada";var $d=Ed(`
|
|
460
464
|
query ScopeReminder($projectId: String!, $cwd: String!) {
|
|
461
465
|
project(id: $projectId) {
|
|
462
466
|
id
|
|
@@ -473,16 +477,16 @@ ${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookS
|
|
|
473
477
|
}
|
|
474
478
|
}
|
|
475
479
|
}
|
|
476
|
-
`),
|
|
477
|
-
${i.map(
|
|
478
|
-
`)}`,hookEventName:"UserPromptSubmit"}}});function
|
|
480
|
+
`),Go=g("UserPromptSubmit",async e=>{let{cwd:t}=e;if(!f(t)||!w(t))return;let r=Fe(t);if(ee(t,"scope-reminder")===r)return;let n=k(t).unwrapOr(void 0);if(n==null)return;let o=await m({config:n,document:$d,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);te(t,"scope-reminder",r);let i=o?.project?.devSession?.scopeItems??[];return ko({cwd:t,scope:{available:o!=null,items:i.map(a=>jd(a))}}),o==null?{hookSpecificOutput:{additionalContext:`Ripplo scope: unknown \u2014 server unreachable, scope guardrails are not enforcing. Check the dev session (\`npx ripplo doctor\`). ${c("start")}`,hookEventName:"UserPromptSubmit"}}:{hookSpecificOutput:{additionalContext:i.length===0?`Ripplo scope: empty. ${c("run")}`:`Ripplo scope (${String(i.length)}):
|
|
481
|
+
${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(`
|
|
482
|
+
`)}`,hookEventName:"UserPromptSubmit"}}});function jd(e){let t=e.workflow;return t==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:t.slug,status:t.spec==null?"stub":"implemented"}}var Id="# 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: entity/world modeling, parallel-isolation in engine impls, backend state assertions, 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, etc.).\n\nMatch the task to a skill from the triage table \u2014 if unsure between two, load both. If completely unsure, load `/ripplo:discover` 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; `npx ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:discover` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\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 tests, managing the testing scope this session is responsible for, diagnosing a failed run (read artifacts in `.ripplo/debug/<runId>/` before re-running), and filing caught app bugs.\n- Load `/ripplo:fuzz` skill for instructions on triaging findings from the background explorer's findings log.\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 daemon` (run `/ripplo:start` to spawn it, or invoke it directly via Bash with `run_in_background`), so the dev session is live. Without the daemon, dev-mode hooks don't arm (`ripplo run` will auto-start one if absent). Without the dev server, runs fail when they try to hit the app. `npx ripplo doctor` reports both.\n- **Two funnels.** Definitions \u2192 `createRipplo({ entities, singletons, tests })` in `.ripplo/index.ts`. Implementations \u2192 `createEngine(ripplo, { entities: impls, singletons })` in your app server's `test/engine.ts` \u2014 one `seed`/`read` impl per entity. 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 new ones with `test(\"Intent\")` (no body). 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 unimplemented 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 (entity, world, engine impl) is in-scope work, not follow-up.\n- **Backend assertions are mandatory on mutations.** Every mutation step carries an `Entity.created/updated/deleted` in its `.expect(...)` \u2014 Ripplo checks your app's state against what the test expected. A UI-only check on a mutation ships the bug as green.\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 write `.ripplo/debug/<runId>/behavior.jsonl` \u2014 a causal stream of actions, assertions, rrweb DOM, console, network, and server spans \u2014 and the run output renders the findings. Read them. Never pipe `ripplo run` through `grep`/`tail`/`head`. Form a hypothesis citing an event, make one change, re-run once.\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 daemon` talks to the wrong server, runs silently fail or hit the sibling worktree's app.\n- **DSL locators are semantic.** Use `role`/`button`/`textbox`/`heading`/`link`; `testId` only when no ARIA role exists.\n- **New backend state?** Add an `entity(...)` in `.ripplo/entities/` and a `seed`/`read` impl in your app's engine funnel (TS flags the missing impl).\n\n## Key files\n\n- `.ripplo/index.ts` \u2014 `createRipplo` call.\n- `.ripplo/{entities,singletons,worlds,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- `/ripplo:create` skill \u2014 DSL reference (entities, worlds, tests, backend assertions). Full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",zo=g("SessionStart",e=>{if(f(e.cwd))return{hookSpecificOutput:{additionalContext:Id,hookEventName:"SessionStart"}}});import Ad from"path";import Td from"process";import{CancellationTokenSource as Ld}from"vscode-jsonrpc/node";import{graphql as Dd}from"gql.tada";function Jo(e){return`--- Ripplo Run Failures (scope) ---
|
|
479
483
|
${e.join(`
|
|
480
484
|
`)}
|
|
481
|
-
Artifacts: .ripplo/debug/<runId>/. ${c("run")}`}function
|
|
482
|
-
`)}function
|
|
483
|
-
`)}function
|
|
485
|
+
Artifacts: .ripplo/debug/<runId>/. ${c("run")}`}function lt({lines:e,retried:t}){let r=t?"Already retried once inside this gate \u2014 still unreachable.":"";return["--- Ripplo Run Not Verified (Ripplo server unreachable) ---",...e,"The Ripplo server was unreachable during these runs \u2014 a server-side transient. Don't debug the daemon, dev server, or auth (`npx ripplo doctor` will be green).",r,"Wait a moment, then stop again to re-verify."].filter(n=>n.length>0).join(`
|
|
486
|
+
`)}function Ko(e){return["--- Ripplo Run Rejected (server declined) ---",...e,"The server rejected these runs \u2014 not transient. Retrying won't clear it, and `npx ripplo doctor` will be green. Resolve the stated cause (plan limit, missing test, repo link), not the daemon or dev server."].join(`
|
|
487
|
+
`)}function me(e){return`--- Ripplo Run Could Not Execute ---
|
|
484
488
|
${e}
|
|
485
|
-
Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${c("start")}`}var
|
|
489
|
+
Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${c("start")}`}var _d=Dd(`
|
|
486
490
|
query ScopeEnforce($projectId: String!, $cwd: String!) {
|
|
487
491
|
project(id: $projectId) {
|
|
488
492
|
id
|
|
@@ -501,26 +505,26 @@ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`)
|
|
|
501
505
|
}
|
|
502
506
|
}
|
|
503
507
|
}
|
|
504
|
-
`),
|
|
508
|
+
`),Xo=g("Stop",async e=>{let{cwd:t}=e;if(!f(t)||!w(t))return;let r=Fe(t),n=ee(t,"stop-enforce")===r;if(n&&!e.stop_hook_active)return;let o=await Od(t);if(te(t,"stop-enforce",r),o.errors.length!==0)return n&&e.stop_hook_active&&!o.infraOnly?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts \u2014 agent appears stuck. Errors:
|
|
505
509
|
${o.errors.join(`
|
|
506
510
|
|
|
507
511
|
`)}`}:{decision:"block",reason:o.errors.join(`
|
|
508
512
|
|
|
509
|
-
`)}});async function
|
|
510
|
-
${
|
|
511
|
-
${c("create")}`],infraOnly:!1};let
|
|
512
|
-
${H(
|
|
513
|
-
${c("create")}`}function
|
|
514
|
-
${
|
|
513
|
+
`)}});async function Od(e){let t=await K(e);if(t.isErr())return{errors:[`--- Compilation failed ---
|
|
514
|
+
${R(t.error)}
|
|
515
|
+
${c("create")}`],infraOnly:!1};let r=t.value,n=await Fd(e,r),o=Nd(r),i=Ud(r),s=n.runnableSlugs.length>0?await Hd(e):null,a=[o,i,s?.error??null,n.error].filter(d=>d!=null),l=a.length>0&&o==null&&i==null&&n.error==null&&(s?.infra??!1);return{errors:a,infraOnly:l}}function Nd(e){let t=M(e);return t.length===0?null:`--- Ripplo Lint ---
|
|
516
|
+
${H(t)}
|
|
517
|
+
${c("create")}`}function Ud(e){let t=N(e);return t.length===0?null:`--- Unimplemented stubs ---
|
|
518
|
+
${t.join(", ")}
|
|
515
519
|
Implement the stub now with \`workflow("Intent", () => ({ given, steps }))\`. Do not ask the user "implement or defer?" \u2014 that framing is forbidden by /ripplo:create. New scaffolding (entity, world, engine impl) is in-scope, not follow-up.
|
|
516
|
-
${c("create")}`}async function
|
|
517
|
-
No project config \u2014 \`npx ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${c("setup")}`,runnableSlugs:[]};let
|
|
518
|
-
Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}) before declaring work done.`,runnableSlugs:[]};let s
|
|
520
|
+
${c("create")}`}async function Fd(e,t){let r=new Set(N(t).map(p=>_(p))),n=new Set(t.workflows.map(p=>_(p.name))),o=(p,u)=>n.has(p)?r.has(p):Md(u),i=k(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (not checked) ---
|
|
521
|
+
No project config \u2014 \`npx ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${c("setup")}`,runnableSlugs:[]};let s=await m({config:i,document:_d,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(s==null)return{error:`--- Testing Scope (not checked) ---
|
|
522
|
+
Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}) before declaring work done.`,runnableSlugs:[]};let a=s.project?.devSession?.scopeItems??[],l=a.flatMap(p=>{let u=p.workflow;return u==null?[` [intent] ${p.label??"(no label)"} \u2014 write a test for this flow`]:o(u.slug,u.spec)?[` [stub] ${u.slug} \u2014 implement \`${u.name}\``]:[]}),d=a.flatMap(p=>p.workflow!=null&&!o(p.workflow.slug,p.workflow.spec)?[p.workflow.slug]:[]);return{error:l.length===0?null:`--- Testing Scope ---
|
|
519
523
|
${l.join(`
|
|
520
524
|
`)}
|
|
521
|
-
${c("create")}`,runnableSlugs:d}}function
|
|
525
|
+
${c("create")}`,runnableSlugs:d}}function Md(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function Hd(e){let t=Td.argv[1];if(t==null)return{error:me("CLI entry missing (process.argv[1])"),infra:!1};let r=await $e(e);if(r!=null){let i=Ae(r);return je(r.serverUrl)?{error:me(i),infra:!1}:{error:lt({lines:[i],retried:!1}),infra:!0}}let n=await un({cliEntry:t,cwd:e});if(n.isErr())return{error:me(G(n.error)),infra:!1};let o=n.value;try{return await Wd({connection:o,cwd:e})}finally{o.spawned&&await hn(o),o.socket.destroy()}}async function Wd({connection:e,cwd:t}){let r=await Qo({connection:e,cwd:t,tests:[]});if(r.kind==="transport")return{error:me(r.message),infra:!1};let n=r.notRun.length>0?await Qo({connection:e,cwd:t,tests:r.notRun.map(l=>l.id)}):null,o=n==null||n.kind==="transport"?r.notRun:n.notRun,i=n!=null&&n.kind==="done",s=[...r.failedLines,...i?n.failedLines:[]],a=[...r.rejected,...i?n.rejected:[]];return qd({failedLines:s,notRun:o,rejected:a,retried:n!=null})}function qd({failedLines:e,notRun:t,rejected:r,retried:n}){if(e.length===0&&t.length===0&&r.length===0)return{error:null,infra:!1};let o=e.length>0?Jo(e):null,i=r.length>0?Ko(r):null,s=t.length>0?lt({lines:t.map(a=>a.line),retried:n}):null;return{error:[o,i,s].filter(a=>a!=null).join(`
|
|
522
526
|
|
|
523
|
-
`),infra:o==null}}async function
|
|
524
|
-
`),process.exit(1));let
|
|
525
|
-
`),process.exit(1)});function
|
|
526
|
-
`))}export{
|
|
527
|
+
`),infra:o==null}}async function Qo({connection:e,cwd:t,tests:r}){let n=[],o=[],i=[],s=Ad.join(t,".ripplo","debug"),a=new Ld;return(await Pe({connection:e,request:{all:!1,headed:!1,tests:[...r]},token:a.token,onEvent:d=>{Bd({debugDir:s,event:d,failedLines:n,notRun:o,rejected:i})}})).match(d=>d.kind==="daemon-error"?{kind:"transport",message:Te(d.error)}:{failedLines:n,kind:"done",notRun:o,rejected:i},d=>({kind:"transport",message:G(d)}))}function Bd({debugDir:e,event:t,failedLines:r,notRun:n,rejected:o}){if(t.kind!=="test-outcome"||t.outcome.kind==="pass")return;let i=Ie({debugDir:e,event:t})??t.testName;if(t.outcome.kind==="dispatch-error"){o.push(i);return}if(t.outcome.kind==="dispatch-timeout"||t.outcome.kind==="infra-error"){n.push({id:`${t.workflowName}/${t.testName}`,line:i});return}r.push(i)}import{z as Yo}from"zod";var Vd=Yo.looseObject({skill:Yo.string()}),Zo=g("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=Vd.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&&f(e.cwd)&&Me(e.cwd,e.session_id,n)});var Gd=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,ei=g("UserPromptSubmit",e=>{f(e.cwd)&&[...e.prompt.matchAll(Gd)].map(t=>t[1]).filter(t=>t!=null).forEach(t=>{Me(e.cwd,e.session_id,t)})});cc();B(process.cwd());ht();var ti={"exit-plan-gate":fo,"plan-reminder":go,"post-edit-flag-stubs":Po,"post-edit-lint":To,"pre-bash-hooks-pause-gate":Do,"pre-bash-run-gate":Uo,"pre-edit-ripplo-skill-gate":Mo,"pre-edit-scope-gate":qo,"pre-edit-watch-gate":Vo,"scope-reminder":Go,"session-preamble":zo,"stop-enforce":Xo,"track-skill-load":Zo,"track-skill-prompt":ei};async function Qd(){zd({pkg:{name:"ripplo",version:P()}}).notify({message:Sr()}),await Jd(Kd(process.argv)).scriptName("ripplo").version(P()).command(ec()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>Tr({value:e.value})).command("auth <subcommand>","Manage authentication",dc).command("projects <subcommand>","Inspect Ripplo projects",lc).command("hooks <subcommand>","Pause or resume Ripplo hooks",ac).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=>tn({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests locally via the daemon (start it first with `npx ripplo daemon`)",ic,e=>wn({all:e.all,headed:e.headed,ids:e.ids})).command(nc()).command(oc()).command(Zd()).command(rc()).command(tc()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>rn()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>zn()).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=>Ir({check:e.check})).command("update","Update ripplo to the latest published version (and hand the daemon off to it)",()=>{},()=>ro()).command("doctor","Check project health",()=>{},()=>qr()).command("status","Report stub tests and daemon status",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>po({format:e.format})).command("scope <subcommand>","Manage testing scope",sc).command("run-worker",!1,()=>{},()=>Cn()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(ti),demandOption:!0,type:"string"}),e=>Xd(e.name)).strict().help().parse()}async function Xd(e){let t=ti[e];t==null&&(process.stderr.write(`Unknown hook: ${e}
|
|
528
|
+
`),process.exit(1));let r=await Yd(),n=r.trim()===""?{}:JSON.parse(r),o=await t.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function Yd(){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)})}Qd().catch(e=>{process.stderr.write(`${Et(e)}
|
|
529
|
+
`),process.exit(1)});function Zd(){return{command:"report-bug",describe:"Report a critical application bug caught while building or testing",builder:e=>e.option("kind",{choices:["new_feature_bug","regression","latent_bug"],demandOption:!0,describe:"new_feature_bug: broke the new thing being built; regression: broke previously working behavior; latent_bug: pre-existing bug exposed by new test coverage"}).option("title",{demandOption:!0,describe:"Short bug name",type:"string"}).option("root-cause",{demandOption:!0,describe:"What was actually wrong",type:"string"}).option("surfaced-by",{demandOption:!0,describe:"How the test/run exposed it (cite evidence)",type:"string"}).option("run",{demandOption:!0,describe:"Run id where it surfaced \u2014 links the bug to its replay. For an exploration finding, pass the explore-\u2026 run id from the finding",type:"string"}),handler:e=>sn({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],title:e.title})}}function ec(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.option("executor",{choices:["local","cloud"],default:"local",describe:"Run executor (local pool, or cloud fleet via a per-session tunnel)"}),handler:e=>no({executor:e.executor})}}function tc(){return{command:"findings",describe:"List open explorer findings",builder:e=>e.option("json",{default:!1,describe:"Machine-readable output",type:"boolean"}),handler:e=>jn({json:e.json})}}function rc(){return{command:"pull <runId>",describe:"Download a run's behavior.jsonl from the server",builder:e=>e.positional("runId",{demandOption:!0,type:"string"}),handler:e=>Dn({runId:e.runId})}}function nc(){return{command:"snapshot <runId>",describe:"Render a PNG of the page at a point in a run's rrweb recording",builder:e=>e.positional("runId",{demandOption:!0,type:"string"}).option("at",{describe:"Moment to render: epoch-ms timestamp exactly as found in behavior.jsonl",type:"number"}).option("offset",{describe:"Moment to render: ms from the start of the recording (alternative to --at)",type:"number"}).conflicts("at","offset"),handler:e=>qn({at:e.at,offset:e.offset,runId:e.runId})}}function oc(){return{command:"teleport <ref>",describe:"Open a live browser at a step in a test, seeded with the run up to that point",builder:e=>e.positional("ref",{demandOption:!0,describe:"Test ref: <workflow-slug>/<test-slug>",type:"string"}).option("step",{demandOption:!0,describe:"Step to land on \u2014 runs the test up to and including this step, then hands over",type:"number"}),handler:e=>Gn({ref:e.ref,step:e.step})}}function ic(e){let t=[];return e.positional("ids",{array:!0,default:t,describe:"Test ids to run",type:"string"}).option("all",{default:!1,describe:"Run every test in the suite (expensive)",type:"boolean"}).option("headed",{default:!1,describe:"Run with a visible browser window",type:"boolean"})}function sc(e){return e.command("status","Print the current scope",t=>t.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),t=>ao({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=>lo({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=>co({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=>uo({ids:t.ids})).demandCommand(1)}function ac(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>Br()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>Vr()).demandCommand(1)}function lc(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>nn()).demandCommand(1)}function dc(e){return e.command("login","Authenticate via device flow",()=>{},()=>Er()).command("status","Show authentication status",()=>{},()=>$r()).command("logout","Remove the saved token",()=>{},()=>{jr()}).demandCommand(1)}function cc(){let e=process.cwd(),t=ct(e);t!=null&&t!==e&&(process.chdir(t),process.stderr.write(`ripplo: resolved .ripplo/ at ${t}
|
|
530
|
+
`))}export{Qd as main};
|