ripplo 0.7.7 → 0.7.9
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-A6RTZMN6.js → chunk-63KRWFSY.js} +74 -74
- package/dist/daemon-4ULJF2OG.js +60 -0
- package/dist/index.js +188 -173
- package/package.json +11 -6
- package/dist/daemon-7Z53WGU3.js +0 -60
package/dist/index.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{$ as
|
|
2
|
+
import{$ as Hr,$a as ft,A as S,Aa as et,B as c,Ba as rt,C as $r,Ca as tt,D as x,Da as nt,E as Ir,Ea as ot,F as jr,Fa as it,G as Ar,Ga as st,H as Tr,Ha as at,I,Ia as lt,J as A,Ja as Se,K as Lr,Ka as dt,L as w,La as ct,M as ke,P as F,Q as Dr,R as _r,Ra as pt,S as se,Sa as le,T as Or,Ta as ut,U as Nr,Ua as mt,V as we,W as Fr,X as Ur,Y as ve,Z as P,_ as Mr,_a as de,a as fr,aa as Wr,ab as Ye,b as O,ca as qr,cb as gt,d as gr,da as be,e as hr,eb as ht,f as u,g as ye,gb as yt,h as yr,i as Ke,ia as Br,ib as kt,j as N,k as kr,ka as Vr,kb as wt,l as wr,lb as vt,m as vr,ma as Gr,mb as bt,n as br,na as zr,nb as St,o as Sr,oa as ae,ob as xt,p as xr,pa as v,pb as Ct,q as V,qa as Jr,r as Cr,ra as Kr,rb as Rt,s as Rr,sa as Qr,t as R,u as z,ua as Yr,v as Pr,va as Xr,w as M,x as $,xa as J,y as D,ya as Qe,z as Er,za as Zr}from"./chunk-63KRWFSY.js";import Gd from"update-notifier";import zd from"yargs";import{hideBin as Jd}from"yargs/helpers";function xe({current:e,latest:r}){return r==null?`ripplo v${e} (latest: unknown)`:r===e?`ripplo v${e} (latest)`:`ripplo v${e} (latest: v${r} \u2014 run \`npx ripplo update\`)`}function Pt(){return"Update available {currentVersion} \u2192 {latestVersion}\nRun `npx ripplo update`"}import{graphql as li}from"gql.tada";import{exec as Yo}from"child_process";import{createAuthClient as Ko}from"better-auth/client";import{deviceAuthorizationClient as Qo}from"better-auth/client/plugins";function Et({baseURL:e}){return Ko({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[Qo()]})}import{err as $t,ok as Xo}from"neverthrow";var Zo=5e3,It="ripplo-cli";async function jt({onDeviceCode:e,url:r}){let t=r??Lr().RIPPLO_SERVER_URL,n=Et({baseURL:t}),o=await n.device.code({client_id:It});if(o.error!=null)return $t({description:o.error.error_description,kind:"oauth-device-code-failed"});let{device_code:i,user_code:a,verification_uri_complete:s}=o.data;return e({userCode:a,verificationUrl:s}),ii(s),(await ei({authClient:n,deviceCode:i})).map(d=>(kr(d),d))}async function ei({authClient:e,deviceCode:r}){for(;;){await ni(Zo);let t=await e.device.token({client_id:It,device_code:r,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(t.data?.access_token!=null)return Xo(t.data.access_token);if(t.error==null)continue;if(!ti(t.error.error))return $t({code:t.error.error,description:t.error.error_description,kind:"oauth-authorization-failed"})}}var ri=new Set(["authorization_pending","slow_down"]);function ti(e){return ri.has(e)}function ni(e){return new Promise(r=>{setTimeout(r,e)})}function oi(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function ii(e){let r=oi();Yo(`${r} "${e}"`,()=>{})}function H({serverUrl:e,token:r}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:r,webhookSecret:""}}import si from"fs";import ai from"path";function m(e){return si.existsSync(ai.join(e,".ripplo"))}var di=li(`
|
|
3
3
|
query AuthViewer {
|
|
4
4
|
currentUser {
|
|
5
5
|
name
|
|
6
6
|
email
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
`);async function
|
|
9
|
+
`);async function At(){let e=A(),r=N();if(r!=null&&await ci(e,r))return;let n=(await jt({url:e,onDeviceCode:s=>{process.stdout.write(`Opening your browser to finish sign-in.
|
|
10
10
|
`),process.stdout.write(`If it didn't open, visit: ${s.verificationUrl}
|
|
11
11
|
`),process.stdout.write(`Verification code: ${s.userCode}
|
|
12
12
|
|
|
13
|
-
`),process.stdout.write(`Waiting for
|
|
14
|
-
`)}})).match(s=>s,s=>{process.stderr.write(`${
|
|
15
|
-
`),process.exit(1)}),o=await
|
|
16
|
-
`),process.stdout.write(o.kind==="ok"?`${a}, ${
|
|
13
|
+
`),process.stdout.write(`Waiting for approval...
|
|
14
|
+
`)}})).match(s=>s,s=>{process.stderr.write(`${I(s)}
|
|
15
|
+
`),process.exit(1)}),o=await Xe({serverUrl:e,token:n}),i=m(process.cwd()),a=i?"Welcome back":"Welcome to Ripplo";process.stdout.write(`
|
|
16
|
+
`),process.stdout.write(o.kind==="ok"?`${a}, ${pi(o.viewer)}.
|
|
17
17
|
`:`${a}.
|
|
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):(
|
|
21
|
-
`),process.exit(1)),process.stdout.write(`Your
|
|
18
|
+
`),process.stdout.write(`Session saved to ${Ke("token")}.
|
|
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 ci(e,r){let t=await Xe({serverUrl:e,token:r});return t.kind==="ok"?(process.stdout.write(`Already signed in as ${t.viewer.email}. Run \`npx ripplo auth logout\` to switch accounts.
|
|
20
|
+
`),!0):(t.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${t.message}). Your saved session may still be valid \u2014 fix connectivity and retry.
|
|
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(`
|
|
26
|
-
`)}function
|
|
27
|
-
`);return}process.stdout.write(`Removed ${
|
|
28
|
-
`)}async function
|
|
29
|
-
`),process.exit(1));let n=
|
|
30
|
-
`);return}process.stderr.write(`${
|
|
31
|
-
`),process.exit(1)}await
|
|
32
|
-
`)}import{graphql as
|
|
23
|
+
`),!1)}async function Tt(){let e=N();e==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login`.\n"),process.exit(1));let r=A(),t=await Xe({serverUrl:r,token:e});t.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${r} (${t.message}). The token may still be valid \u2014 check the server / network and retry.
|
|
24
|
+
`),process.exit(1)),t.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 ${t.viewer.name} <${t.viewer.email}> (${r})
|
|
26
|
+
`)}function Lt(){if(!wr()){process.stdout.write(`Already signed out.
|
|
27
|
+
`);return}process.stdout.write(`Signed out. Removed ${Ke("token")}.
|
|
28
|
+
`)}async function Xe({serverUrl:e,token:r}){try{let n=(await u({config:H({serverUrl:e,token:r}),document:di,variables:void 0})).currentUser;return n==null?{kind:"rejected"}:{kind:"ok",viewer:{email:n.email,name:n.name}}}catch(t){return hr(t)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:t instanceof Error?t.message:String(t)}}}function pi(e){let r=e.name.trim().split(/\s+/)[0];return r!=null&&r.length>0?r:e.email}import{readFile as ui,writeFile as mi}from"fs/promises";import fi from"path";async function Dt(e){let r=process.cwd(),t=await S(r);t.isErr()&&(process.stderr.write(`${x(t.error)}
|
|
29
|
+
`),process.exit(1));let n=z(M,t.value),o=fi.join(r,D);if(e.check){let i=await ui(o,"utf8").catch(()=>null);if(i===n){process.stdout.write(`${Ir()}
|
|
30
|
+
`);return}process.stderr.write(`${jr(i==null?"missing":"stale")}
|
|
31
|
+
`),process.exit(1)}await mi(o,n),process.stdout.write(`${Ar()}
|
|
32
|
+
`)}import{graphql as _t}from"gql.tada";function Ze(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 gi}from"gql.tada";var hi=gi(`
|
|
33
33
|
query DevSessionCheckPreflight($projectId: String!, $cwd: String!) {
|
|
34
34
|
project(id: $projectId) {
|
|
35
35
|
id
|
|
@@ -38,26 +38,26 @@ import{$ as Rt,$a as Qt,A as d,Aa as Ft,B as dt,Ba as Nt,C as x,Ca as Ut,D as pt
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
`);function
|
|
42
|
-
`),process.stderr.write(`${
|
|
43
|
-
`),process.exit(1)})}async function
|
|
44
|
-
`),process.exit(1))}var
|
|
41
|
+
`);function T(){return w(process.cwd()).match(e=>e,e=>{process.stderr.write(`${I(e)}
|
|
42
|
+
`),process.stderr.write(`${c("setup")}
|
|
43
|
+
`),process.exit(1)})}async function ce(e){(await u({config:e,document:hi,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 yi=_t(`
|
|
45
45
|
mutation CliUpdateMaxLocalConcurrentRuns($value: Int!) {
|
|
46
46
|
updateMaxLocalConcurrentRuns(value: $value) {
|
|
47
47
|
id
|
|
48
48
|
maxLocalConcurrentRuns
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
`),
|
|
51
|
+
`),ki=_t(`
|
|
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 Ot({value:e}){let r=T();if(e==null){let n=await u({config:r,document:ki,variables:{}});process.stdout.write(`${Ze(n.currentUser?.maxLocalConcurrentRuns)}
|
|
59
|
+
`);return}let t=await u({config:r,document:yi,variables:{value:e}});process.stdout.write(`${Ze(t.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
|
|
60
|
+
`)}import{graphql as wi}from"gql.tada";var vi=wi(`
|
|
61
61
|
query DevSessionCheckHealth($projectId: String!, $cwd: String!) {
|
|
62
62
|
project(id: $projectId) {
|
|
63
63
|
id
|
|
@@ -66,45 +66,46 @@ import{$ as Rt,$a as Qt,A as d,Aa as Ft,B as dt,Ba as Nt,C as x,Ca as Ut,D as pt
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
`);function
|
|
70
|
-
${e.missing.map(
|
|
69
|
+
`);function Nt(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":return"\u2717 Dev session: no daemon running. 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."}}async function Ft(e){let r=ke(e),t=Dr(e),n=w(e).unwrapOr(void 0);return n==null?{gitMidOperation:t,status:r?"starting":"missing",type:"dev-session"}:(await u({config:n,document:vi,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null))?.project?.devSession!=null?{gitMidOperation:t,status:"active",type:"dev-session"}:{gitMidOperation:t,status:r?"starting":"missing",type:"dev-session"}}function Ut(e){switch(e.type){case"settings":return bi(e);case"env-files":return Si(e);case"token":return xi(e);case"dev-server":return Ci(e);case"dev-session":return Nt(e);case"preconditions":return Ri(e);case"webhook-verification":return Pi(e);case"preconditions-validation":return Ei(e);case"workflows":return $i(e);case"browser":return ji(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 Ai(e);case"lockfile":return Ti(e);case"pre-commit-hook":return Li(e);case"plugin-version":return Di(e)}}function bi(e){return e.valid?"\u2713 Settings: project configured":`\u2717 Settings: missing fields: ${e.missingFields.join(", ")}`}function Si(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
|
|
70
|
+
${e.missing.map(t=>` ${t}`).join(`
|
|
71
71
|
`)}
|
|
72
|
-
|
|
73
|
-
${
|
|
74
|
-
`)}`}function
|
|
75
|
-
${
|
|
76
|
-
`)}`}function
|
|
77
|
-
`+
|
|
78
|
-
`)}function
|
|
72
|
+
In a git worktree? Copy the env file from the main checkout, or symlink to a shared file outside the working tree.`}function xi(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 ${A()} to validate the token.`}}function Ci(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Start your dev server.`}function Ri(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 Pi(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 Ei(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: valid";let r=e.errors.map(t=>` - ${t.path===""?"":t.path+": "}${t.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
|
|
73
|
+
${r.join(`
|
|
74
|
+
`)}`}function $i(e){if(e.total===0)return"! Tests: none defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let r=e.invalidWorkflows.map(t=>Ii(t));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
|
|
75
|
+
${r.join(`
|
|
76
|
+
`)}`}function Ii(e){let r=e.errors.map(t=>" - "+(t.path===""?"":t.path+": ")+t.message);return" "+e.name+`:
|
|
77
|
+
`+r.join(`
|
|
78
|
+
`)}function ji(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run `npx playwright install chromium`."}function Ai(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 Ti(e){switch(e.status){case"match":return`\u2713 Lockfile: ${D} is up to date`;case"missing":return`\u2717 Lockfile: ${D} is missing \u2014 run \`npx ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${D} is out of date \u2014 run \`npx ripplo compile\` and commit it`}}function Li(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 Di(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 K from"fs";import Mi from"os";import er from"path";import{z as B}from"zod";import _i from"latest-version";import Mt from"semver";async function Ce(e){try{return await Promise.race([_i("ripplo"),new Promise(r=>setTimeout(()=>{r(void 0)},e))])}catch{return}}function Ht(e){return e.filter(r=>Mt.valid(r)!=null).toSorted(Mt.rcompare)[0]}function W(e){let r=we(e),t=qr(e);return[...Ur(r,t.invariants).map(n=>({gap:n,kind:"cascade-gap"})),...Fr(t.laws).map(n=>({conflict:n,kind:"law-conflict"}))]}function C(e,r,t){let n=e===1?r:t??`${r}s`;return`${String(e)} ${n}`}function Wt(e){return`${P.good("ok")} \u2014 no static model violations (${C(e,"test")})`}function q(e){return[`${P.bad("fail")} \u2014 ${C(e.length,"static model violation")}:`,...e.map(r=>Hr(r))].join(`
|
|
79
79
|
|
|
80
|
-
`)}function
|
|
81
|
-
`)}function
|
|
80
|
+
`)}function Re(e){return[`${P.warn("warn")} \u2014 ${C(e.length,"model coverage gap")} (not blocking):`,...e.map(r=>` ${Oi(r)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${c("discover")}`].join(`
|
|
81
|
+
`)}function Oi(e){return e.kind==="entity-never-given"?`${e.entity} \u2014 declared but no implemented test seeds it, so no flow exercises this state`:`${e.entity} \u2014 seeded but never asserted created/updated/deleted, so mutations to it ship unchecked`}import{graphql as Ni}from"gql.tada";var Fi=Ni(`
|
|
82
82
|
query DoctorAuthViewer {
|
|
83
83
|
currentUser {
|
|
84
84
|
name
|
|
85
85
|
email
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
`),
|
|
88
|
+
`),Ui="Failed to connect to Ripplo server";async function qt({serverUrl:e,token:r}){try{let t=await u({config:H({serverUrl:e,token:r}),document:Fi,variables:void 0});return t.currentUser==null?{kind:"invalid"}:{email:t.currentUser.email,kind:"valid"}}catch(t){return(t instanceof Error?t.message:String(t)).startsWith(Ui)?{kind:"unreachable"}:{kind:"invalid"}}}function Bt(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 Vt(e){let r=await qi(e),t={missing:Rr(e),type:"env-files"},n=await Bi(),o=await Vi(),i=await S(e),a=Gi(i),s=zi(i),l=await Ji(e,i),d=Qi(e),h=Wi(e),p=await Yi(r,i);return[r,t,n,...p,a,s,l,d,o,...h==null?[]:[h]]}var Hi=B.object({plugins:B.record(B.string(),B.array(B.object({projectPath:B.string().optional(),scope:B.string().optional(),version:B.string()})))});function Wi(e){let r=er.join(Mi.homedir(),".claude","plugins","installed_plugins.json"),t=K.existsSync(r)?K.readFileSync(r,"utf8"):null;if(t==null)return null;let n=Hi.safeParse(JSON.parse(t));if(!n.success)return null;let o=Object.entries(n.data.plugins).filter(([a])=>a.startsWith("ripplo@")).flatMap(([,a])=>a).filter(a=>a.scope==="user"||a.projectPath===e).map(a=>a.version),i=Ht(o);return i==null?null:{cliVersion:R(),installed:i,type:"plugin-version"}}async function qi(e){let r=await _r(e);return r.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:r.errors.map(n=>n.path).filter(n=>n.length>0),type:"settings",valid:!1}}async function Bi(){let e=N();if(e==null||e.length===0)return{email:void 0,status:"missing",type:"token"};let r=await qt({serverUrl:A(),token:e});return r.kind==="valid"?{email:r.email,status:"valid",type:"token"}:{email:void 0,status:r.kind,type:"token"}}async function Vi(){let{chromium:e}=await import("playwright"),r=e.executablePath();return{installed:K.existsSync(r),type:"browser"}}function Gi(e){if(e.isErr())return{errorCount:1,errors:[{message:x(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let r=W(e.value);return{errorCount:r.length,errors:r.length===0?[]:[{message:q(r),path:""}],found:!0,type:"preconditions-validation",valid:r.length===0}}function zi(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let r=e.value.tests.length;return{invalidNames:[],invalidWorkflows:[],total:r,type:"workflows",valid:r}}async function Ji(e,r){if(r.isErr())return{status:"missing",type:"lockfile"};let t=z(M,r.value),n=await K.promises.readFile(er.join(e,D),"utf8").catch(()=>null);return{status:Ki(n,t),type:"lockfile"}}function Ki(e,r){return e==null?"missing":e===r?"match":"stale"}function Qi(e){let r=er.join(e,".git","hooks","pre-commit");return K.existsSync(r)?{installed:K.readFileSync(r,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function Yi(e,r){if(!e.valid||r.isErr())return[];let t=Cr().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(t==null)return[];let n=[],o=await se(t.appUrl)==null;n.push({appUrl:t.appUrl,reachable:o,type:"dev-server"});let i=await Ft(process.cwd());n.push(i);let a=await Xi(t,r);return n.push(...a),n}async function Xi(e,r){let t=r.isOk()?r.value.entities.length:0,n=e.engineUrl.length>0,o={configured:n,count:t,endpointReachable:void 0,type:"preconditions"};if(t===0||!n)return[o];let i=es(e.appUrl,e.engineUrl);if(i==null)return[o];let a=await se(i)==null,s=[];if(s.push({configured:!0,count:t,endpointReachable:a,type:"preconditions"}),Gt(e.engineUrl)&&s.push({reachable:a,type:"engine-endpoint",url:i}),!a)return s;let l=await Or({appUrl:e.appUrl,engineUrl:e.engineUrl});s.push({rejectsUnsigned:l==null,type:"webhook-verification"});let d=await Zi({engineUrl:i,webhookSecret:e.webhookSecret});return s.push({status:d,type:"adapter-enabled",url:i}),s}async function Zi({engineUrl:e,webhookSecret:r}){if(r.length===0)return"no-secret";let t=JSON.stringify({});try{let n=await fetch(`${e}/setup`,{body:t,headers:{"Content-Type":"application/json",...br({body:t,secret:r})},method:"PUT",signal:AbortSignal.timeout(5e3)});return n.status===404?"disabled":n.status===401||n.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function Gt(e){return e.startsWith("http://")||e.startsWith("https://")}function es(e,r){return Gt(r)?r:`${e}${r}`}var rs=3e3;async function zt(){let e=process.cwd(),[r,t]=await Promise.all([Ce(rs),Vt(e)]);process.stdout.write(`${xe({current:R(),latest:r})}
|
|
89
|
+
`);let n=t.map(i=>Ut(i));process.stdout.write(n.join(`
|
|
89
90
|
`)+`
|
|
90
|
-
`);let
|
|
91
|
+
`);let o=t.some(i=>Bt(i));process.exit(o?1:0)}import Pe from"fs";import{graphql as ts}from"gql.tada";var ns=ts(`
|
|
91
92
|
mutation CliSetHooksPaused($projectId: String!, $paused: Boolean!) {
|
|
92
93
|
setHooksPaused(projectId: $projectId, paused: $paused) {
|
|
93
94
|
id
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
|
-
`);async function
|
|
97
|
-
`);return}
|
|
98
|
-
`)}async function
|
|
99
|
-
`)})}import
|
|
100
|
-
`)[0]??
|
|
97
|
+
`);async function Jt(){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();ye(e);let r=ae(e);if(Pe.existsSync(r)){process.stdout.write("Hooks already paused. Run `npx ripplo hooks resume` to re-enable.\n");return}Pe.writeFileSync(r,""),await Qt(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 Kt(){let e=process.cwd(),r=ae(e);if(!Pe.existsSync(r)){process.stdout.write(`Hooks already active.
|
|
98
|
+
`);return}Pe.unlinkSync(r),await Qt(e,!1),process.stdout.write(`Hooks resumed.
|
|
99
|
+
`)}async function Qt(e,r){let t=w(e).unwrapOr(void 0);t!=null&&await u({config:t,document:ns,variables:{paused:r,projectId:t.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.
|
|
100
|
+
`)})}import X from"fs";import Z from"path";import{input as nn,select as on}from"@inquirer/prompts";import{graphql as Ds}from"gql.tada";import{exec as ss,execFile as as}from"child_process";import{err as ls,ok as Yt}from"neverthrow";import y from"fs";import{createRequire as ds}from"module";import g from"path";import{promisify as Zt}from"util";import{writeFile as os}from"fs/promises";import is from"path";async function Q(e){let r=await S(e);return r.isOk()&&await os(is.join(e,D),z(M,r.value)),r}function U(e){return e.tests.filter(r=>r.stub).map(r=>r.name)}var cs=["@ripplo/testing","@ripplo/instrument"],Xt=".ripplo/ripplo.lock linguist-generated=true",ps=[".ripplo/debug/",".ripplo/.local/"],us=Zt(ss),ms=Zt(as);async function en({cwd:e,onStep:r}){r("Scaffolding project files..."),Cs({cwd:e}),r("Updating .gitignore..."),Rs(e),r("Marking ripplo.lock as generated..."),ys(e),r("Installing dependencies...");let t=await gs(e),n=[];if(t.ok||n.push({manualCommand:t.cmd,message:`Couldn't auto-install dev dependencies (${t.reason}). Run the command below, then run \`npx ripplo lint\` to compile the lockfile.`}),t.ok){r("Compiling initial lockfile...");let i=await hs(e);i!=null&&n.push({manualCommand:void 0,message:i})}return r("Setting up browser..."),(await fs()).map(()=>n)}async function fs(){let{chromium:e}=await import("playwright"),r=e.executablePath();if(y.existsSync(r))return Yt(void 0);O.info("Chromium not found. Installing via Playwright...");let t=ds(import.meta.url),n=g.dirname(t.resolve("playwright/package.json")),o=g.join(n,"cli.js");return await ms(process.execPath,[o,"install","chromium"]),y.existsSync(r)?Yt(void 0):ls({kind:"playwright-install-failed"})}async function gs(e){let r=ks({cwd:e,pm:xs(e)});O.info("Installing dependencies: %s",r);try{return await us(r,{cwd:e}),{ok:!0}}catch(t){let n=t instanceof Error?t.message.split(`
|
|
101
|
+
`)[0]??t.message:String(t);return O.warn("Install failed (%s): %s",r,n),{cmd:r,ok:!1,reason:n}}}async function hs(e){try{await Q(e);return}catch(r){return`Couldn't compile initial lockfile: ${r instanceof Error?r.message:String(r)}.`}}function ys(e){let r=g.join(e,".gitattributes"),t=y.existsSync(r)?y.readFileSync(r,"utf8"):"";if(t.includes(Xt))return;let n=t.length===0||t.endsWith(`
|
|
101
102
|
`)?"":`
|
|
102
|
-
`;y.writeFileSync(
|
|
103
|
-
`)}function
|
|
103
|
+
`;y.writeFileSync(r,`${t}${n}${Xt}
|
|
104
|
+
`)}function ks({cwd:e,pm:r}){let t=cs.join(" ");return r==="pnpm"?ws(e)?`pnpm add -wD ${t}`:`pnpm add -D ${t}`:r==="yarn"?vs({cwd:e,deps:t}):r==="bun"?`bun add -d ${t}`:`npm install -D ${t}`}function ws(e){return y.existsSync(g.join(e,"pnpm-workspace.yaml"))||y.existsSync(g.join(e,"pnpm-workspace.yml"))}function vs({cwd:e,deps:r}){return bs(e)?`yarn add -D ${r}`:Ss(e)?`yarn add -WD ${r}`:`yarn add -D ${r}`}function bs(e){if(y.existsSync(g.join(e,".yarnrc.yml"))||y.existsSync(g.join(e,".pnp.cjs"))||y.existsSync(g.join(e,".pnp.js")))return!0;let r=g.join(e,"package.json");if(!y.existsSync(r))return!1;try{let t=JSON.parse(y.readFileSync(r,"utf8"));if(t==null||typeof t!="object"||!("packageManager"in t))return!1;let n=t.packageManager;if(typeof n!="string")return!1;let o=/^yarn@(\d+)/.exec(n);return o!=null&&Number(o[1])>=2}catch{return!1}}function Ss(e){let r=g.join(e,"package.json");if(!y.existsSync(r))return!1;try{let t=JSON.parse(y.readFileSync(r,"utf8"));if(t==null||typeof t!="object"||!("workspaces"in t))return!1;let n=t.workspaces;return Array.isArray(n)||n!=null&&typeof n=="object"}catch{return!1}}function xs(e){return y.existsSync(g.join(e,"pnpm-lock.yaml"))?"pnpm":y.existsSync(g.join(e,"yarn.lock"))?"yarn":y.existsSync(g.join(e,"bun.lockb"))||y.existsSync(g.join(e,"bun.lock"))?"bun":"npm"}function Cs({cwd:e}){let r=g.join(e,".ripplo"),t=g.join(r,"entities"),n=g.join(r,"singletons"),o=g.join(r,"worlds"),i=g.join(r,"tests");[t,n,o,i].forEach(a=>{y.mkdirSync(a,{recursive:!0})}),Y(g.join(r,"index.ts"),Ps),Y(g.join(t,"index.ts"),Es),Y(g.join(n,"index.ts"),$s),Y(g.join(o,"index.ts"),Is),Y(g.join(i,"index.ts"),js),Y(g.join(r,"tsconfig.json"),As)}function Y(e,r){y.existsSync(e)||y.writeFileSync(e,r)}function Rs(e){let r=g.join(e,".gitignore");if(!y.existsSync(r))return;let t=y.readFileSync(r,"utf8"),n=ps.filter(i=>!t.includes(i));if(n.length===0)return;let o=t.endsWith(`
|
|
104
105
|
`)?"":`
|
|
105
|
-
`;y.writeFileSync(t
|
|
106
|
+
`;y.writeFileSync(r,t+o+n.join(`
|
|
106
107
|
`)+`
|
|
107
|
-
`)}var
|
|
108
|
+
`)}var Ps=`import { createRipplo } from "@ripplo/testing";
|
|
108
109
|
import { entities } from "./entities/index";
|
|
109
110
|
import { singletons } from "./singletons/index";
|
|
110
111
|
import { tests } from "./tests/index";
|
|
@@ -114,7 +115,7 @@ export default createRipplo({
|
|
|
114
115
|
singletons,
|
|
115
116
|
tests,
|
|
116
117
|
});
|
|
117
|
-
`,
|
|
118
|
+
`,Es=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
|
|
118
119
|
// app's engine funnel (createEngine). See /ripplo:create "Adding an entity".
|
|
119
120
|
//
|
|
120
121
|
// Example:
|
|
@@ -130,7 +131,7 @@ export default createRipplo({
|
|
|
130
131
|
// export const entities = [Project] as const;
|
|
131
132
|
|
|
132
133
|
export const entities = [] as const;
|
|
133
|
-
|
|
134
|
+
`,$s=`// Client/global state (e.g. localStorage flags) modeled as singletons.
|
|
134
135
|
//
|
|
135
136
|
// Example:
|
|
136
137
|
// import { singleton, v } from "@ripplo/testing";
|
|
@@ -145,7 +146,7 @@ export const entities = [] as const;
|
|
|
145
146
|
// export const singletons = [onboardingDismissed];
|
|
146
147
|
|
|
147
148
|
export const singletons = [];
|
|
148
|
-
`,
|
|
149
|
+
`,Is=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
|
|
149
150
|
// for tests. Compose from other worlds. See /ripplo:create "Adding a world".
|
|
150
151
|
//
|
|
151
152
|
// Example:
|
|
@@ -157,7 +158,7 @@ export const singletons = [];
|
|
|
157
158
|
// const project = Project.of({ name: arbitrary(Project.field.name), ownerId: me.id });
|
|
158
159
|
// return { me, project };
|
|
159
160
|
// };
|
|
160
|
-
`,
|
|
161
|
+
`,js=`// Each test file under ./tests exports a test. Import them here and add to the \`tests\`
|
|
161
162
|
// array \u2014 that's what createRipplo({ ..., tests }) receives. Stub with \`test("Intent")\`
|
|
162
163
|
// (no body); implement later with \`test("Intent", () => ({ given, steps }))\`.
|
|
163
164
|
//
|
|
@@ -166,7 +167,7 @@ export const singletons = [];
|
|
|
166
167
|
// export const tests = [createProject] as const;
|
|
167
168
|
|
|
168
169
|
export const tests = [] as const;
|
|
169
|
-
`,
|
|
170
|
+
`,As=`{
|
|
170
171
|
"compilerOptions": {
|
|
171
172
|
"strict": true,
|
|
172
173
|
"noUncheckedIndexedAccess": true,
|
|
@@ -182,67 +183,69 @@ export const tests = [] as const;
|
|
|
182
183
|
"include": ["*.ts", "entities/**/*.ts", "singletons/**/*.ts", "worlds/**/*.ts", "tests/**/*.ts"],
|
|
183
184
|
"exclude": ["node_modules"]
|
|
184
185
|
}
|
|
185
|
-
`;var
|
|
186
|
+
`;import Ee from"fs";import rn from"path";import{z as rr}from"zod";var Ts={autoUpdate:!0,source:{repo:"ripplo/claude-plugin",source:"github"}},tn=rr.record(rr.string(),rr.unknown());function $e(e){let r=rn.join(e,".claude","settings.json"),t=Ls(r);if(t==null)return"unparseable";let n=tn.safeParse(t.extraKnownMarketplaces??{});if(!n.success)return"unparseable";if(n.data.ripplo!=null)return"already-present";let o={...t,extraKnownMarketplaces:{...n.data,ripplo:Ts}};return Ee.mkdirSync(rn.dirname(r),{recursive:!0}),Ee.writeFileSync(r,`${JSON.stringify(o,null,2)}
|
|
187
|
+
`),"written"}function Ls(e){if(!Ee.existsSync(e))return{};try{let r=tn.safeParse(JSON.parse(Ee.readFileSync(e,"utf8")));return r.success?r.data:void 0}catch{return}}function Ie(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 _s=Ds(`
|
|
186
188
|
query InitProjects {
|
|
187
189
|
projects {
|
|
188
190
|
id
|
|
189
191
|
name
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
|
-
`),
|
|
193
|
-
`),process.exit(1));let o=await
|
|
194
|
-
`)
|
|
195
|
-
`)
|
|
196
|
-
`),
|
|
194
|
+
`),tr=["../.env.local","../.env"];async function sn(e=Fs()){let r=process.cwd(),t=N();t==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1));let n=A();Os(r)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${r}. To re-init, delete it first.
|
|
195
|
+
`),process.exit(1));let o=await Ns({serverUrl:n,token:t});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 Ms(o,e.projectId),a=await Hs(r,e.envFile),s=await qs(e.appUrl),l=Us(s,e.engineUrl),d=Z.resolve(Z.join(r,".ripplo"),a);xr({cwd:r,envFiles:[a],projectId:i}),Vs({appUrl:s,engineUrl:l,filePath:d}),process.stdout.write(`${Ie($e(r))}
|
|
196
|
+
`);let p=(await en({cwd:r,onStep:k=>{process.stdout.write(` ${k}
|
|
197
|
+
`)}})).match(k=>k,k=>{process.stderr.write(`${I(k)}
|
|
198
|
+
`),process.exit(1)});if(p.length>0){process.stdout.write(`Done with warnings:
|
|
199
|
+
`),p.forEach(k=>{process.stdout.write(` - ${k.message}
|
|
197
200
|
`),k.manualCommand!=null&&process.stdout.write(` run: ${k.manualCommand}
|
|
198
|
-
`)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function
|
|
199
|
-
`),process.exit(1)}return
|
|
200
|
-
`),process.exit(1)),process.stdout.write(`Using project: ${
|
|
201
|
-
`),
|
|
202
|
-
`),
|
|
203
|
-
`),process.exit(1)),
|
|
204
|
-
`),process.exit(1)}return e}return
|
|
201
|
+
`)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function Os(e){return X.existsSync(Z.join(e,".ripplo","index.ts"))}async function Ns({serverUrl:e,token:r}){return((await u({config:H({serverUrl:e,token:r}),document:_s,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function Fs(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function Us(e,r){if(r!=null){try{new URL(r)}catch{process.stdout.write(`--engine-url is not a valid URL: ${r}
|
|
202
|
+
`),process.exit(1)}return r}return`${e.replace(/\/$/,"")}/ripplo`}async function Ms(e,r){if(r!=null){let t=e.find(n=>n.id===r);return t==null&&(process.stdout.write(`Unknown project id: ${r}
|
|
203
|
+
`),process.exit(1)),process.stdout.write(`Using project: ${t.name} (${t.id})
|
|
204
|
+
`),t.id}if(e.length===1){let t=e[0];if(t==null)throw new Error("unreachable");return process.stdout.write(`Using project: ${t.name} (${t.id})
|
|
205
|
+
`),t.id}return on({choices:e.map(t=>({name:t.name,value:t.id})),message:"Select a project"})}async function Hs(e,r){return r!=null?(r.trim().length===0&&(process.stdout.write(`--env must not be empty
|
|
206
|
+
`),process.exit(1)),r):Ws(e)}async function Ws(e){let r=Z.join(e,".ripplo"),t=tr.find(o=>X.existsSync(Z.resolve(r,o))),n=await on({choices:[...tr.map(o=>({name:t===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:t??tr[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:nn({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function qs(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
|
|
207
|
+
`),process.exit(1)}return e}return Bs()}async function Bs(){return nn({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 Vs({appUrl:e,engineUrl:r,filePath:t}){X.mkdirSync(Z.dirname(t),{recursive:!0});let n=X.existsSync(t)?X.readFileSync(t,"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=${r}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(n)||o.push(`RIPPLO_WEBHOOK_SECRET=${Sr()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
|
|
205
208
|
`)?"":`
|
|
206
|
-
`;
|
|
209
|
+
`;X.writeFileSync(t,`${n}${i}${o.join(`
|
|
207
210
|
`)}
|
|
208
|
-
`)}function
|
|
209
|
-
`),process.stdout.write(`${
|
|
210
|
-
`);return}process.stderr.write(`${
|
|
211
|
-
`),process.exit(1)}function
|
|
212
|
-
`),process.exit(1)}import{CancellationTokenSource as
|
|
213
|
-
`).trim()}function
|
|
214
|
-
`)}function
|
|
215
|
-
${
|
|
216
|
-
findings land in .ripplo/.local/explore-ledger.jsonl`}}function
|
|
217
|
-
`)}function
|
|
218
|
-
`)}function
|
|
219
|
-
trail: ${
|
|
220
|
-
`)}}}function
|
|
221
|
-
${
|
|
222
|
-
`);case"error":return`${
|
|
223
|
-
`)}case"selection-conflicting-flags":return
|
|
224
|
-
Verify the dev session is live (\`npx ripplo doctor\`, ${
|
|
225
|
-
`);case"connection-lost":return["
|
|
226
|
-
`);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function
|
|
227
|
-
`);return}let n=
|
|
228
|
-
`),process.exit(1));let o=
|
|
229
|
-
`)},
|
|
230
|
-
`),process.exit(1)})}async function
|
|
231
|
-
`),process.exit(1));let
|
|
232
|
-
`),process.exit(1));let n=
|
|
233
|
-
`),n.spawned&&await
|
|
234
|
-
`),Promise.resolve(1)));n.socket.destroy(),process.exit(a)}async function
|
|
235
|
-
`)},onTrail:(
|
|
236
|
-
`)}});process.stdout.write(`${
|
|
237
|
-
`),(
|
|
211
|
+
`)}function je(e){let r=e.tests.filter(o=>!o.stub);if(r.length===0)return[];let t=new Set(r.flatMap(o=>Gs(o))),n=new Set(r.flatMap(o=>zs(o)));return[...e.entities.filter(o=>!t.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>t.has(o.name)&&!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"}))]}function Gs(e){return[...e.world,...e.maybe].map(r=>r.entity)}function zs(e){return e.steps.flatMap(r=>r.expect.flatMap(t=>pe(t)))}function pe(e){return e.kind==="state"?[e.entity]:e.kind==="not"?pe(e.predicate):e.kind==="and"?e.predicates.flatMap(r=>pe(r)):e.kind==="when"?[...pe(e.condition),...pe(e.consequence)]:[]}async function an(){let e=process.cwd(),t=(await S(e)).match(o=>o,o=>Js(x(o))),n=W(t);if(n.length===0){let o=je(t);o.length>0&&process.stdout.write(`${Re(o)}
|
|
212
|
+
`),process.stdout.write(`${Wt(t.tests.length)}
|
|
213
|
+
`);return}process.stderr.write(`${q(n)}
|
|
214
|
+
`),process.exit(1)}function Js(e){process.stderr.write(`${e}
|
|
215
|
+
`),process.exit(1)}import{CancellationTokenSource as ba}from"vscode-jsonrpc/node";import Zs from"path";import{randomUUID as Ks}from"crypto";async function Ae({assign:e,config:r,headed:t,lockfile:n,session:o,signal:i}){let a=we(n),s=await Ys({assign:e,corpus:a,lockfile:n});if(s==null)return{kind:"error",reason:"depot-arrange-failed",rows:[],trail:[]};if(s.firings.length===0)return{kind:"error",reason:"empty-trail",rows:[],trail:[]};let l=Qs(a,s.firings),d=await zr({attemptTimeoutMs:Qr,corpus:a,depot:{name:e.depotTest.name,test:e.depotTest},lensId:e.lensId,lockfile:n,lockfileHash:e.lockfileHash,options:{baseUrl:r.appUrl,engineUrl:r.engineUrl,generate:ve,headed:t,secret:r.webhookSecret},session:o,shrinkBudget:e.shrinkBudget,trail:s,now:()=>new Date().toISOString(),runIdFor:h=>`explore-${Ks()}-${String(h)}`},i);return Xs(d,l)}function Qs(e,r){return r.flatMap(t=>{let n=e[t.idx];return n==null?[]:[{actions:[...n.nav,...n.steps].map(o=>Wr(o.action)),label:`${n.test}#${String(n.index)}`}]})}async function Ys({assign:e,corpus:r,lockfile:t}){return(await Gr(t,{name:e.depotTest.name,test:e.depotTest},{generate:ve,materialize:Nr(ve,t.valueSpaces),params:void 0})).match(o=>Vr({actionHashes:r.map(i=>Br(t,i)),corpus:r,covered:new Set,depotSnapshot:o.snapshot,lens:Kr(t),lensId:e.lensId,maxLength:e.maxLength,witnessTrail:e.firings}),()=>null)}function Xs(e,r){return e.kind==="error"?{kind:e.kind,reason:`runtime:${e.error.kind}`,rows:[],trail:[]}:e.kind==="timeout"?{kind:"error",reason:"trail-timeout",rows:[],trail:[]}:e.kind==="aborted"?{kind:e.kind,rows:[],trail:[]}:{kind:e.kind,rows:[...e.rows],trail:[...r]}}var ea={covered:0,deferred:0,findings:0,saturated:!1,total:0};async function ln(e){let r=w(e.cwd).match(d=>({config:d}),d=>({failure:d}));if("failure"in r)return{failure:r.failure,kind:"config-failed"};let t=r.config,n=await Er(e.cwd);if(n.result.isErr())return{error:n.result.error,kind:"compile-failed"};let o={fingerprint:n.fingerprint,lockfile:n.result.value},i=Jr({onChange:()=>{}});if(!i.holder())return i.stop(),{kind:"explorer-busy"};let a=be({clientVersion:R(),debugDir:Zs.join(e.cwd,".ripplo","debug"),headed:e.headed,writeOtlpPortFile:!1}),s={executed:0,status:()=>ea},l=Yr({cwd:e.cwd,maxTrailLength:e.maxLength,executeTrail:(d,h)=>Ae({assign:d,config:t,headed:e.headed,lockfile:o.lockfile,session:a,signal:h}),loadLockfile:()=>Promise.resolve(o),notifyWork:()=>{},onTrailDone:d=>{s.executed+=1,e.onTrail(s.executed,d,s.status())},probeApp:async()=>await se(t.appUrl)==null});s.status=()=>l.status();try{await l.ready(),e.onReady(l.status());let d=await ra(l,e),h=l.status();return await l.stop(),await a.close(),d===0&&h.total===0?{kind:"no-work"}:{executed:d,kind:"completed",progress:h}}finally{i.stop()}}async function ra(e,r){let t=new AbortController().signal,n=async o=>{if(o>=r.trails)return o;let i=e.next();return i==null?o:(await i.run(t),n(o+1))};return n(0)}import{spawn as ta}from"child_process";import dn from"fs";import na from"net";import{setTimeout as nr}from"timers/promises";import{err as ee,ok as ue}from"neverthrow";import{ResponseError as oa}from"vscode-jsonrpc/node";var pn=12e4,un=300,cn=5e3;async function re({cliEntry:e,cwd:r}){let t=J(r),n=await G(t);return n!=null?ia({cliEntry:e,connection:De(n,!1),cwd:r}):mn({cliEntry:e,cwd:r,versionNote:void 0})}async function mn({cliEntry:e,cwd:r,versionNote:t}){let n=pa({cliEntry:e,cwd:r});if(n!=null)return ee(n);let o=await ua(J(r));return o==null?ee({deadlineMs:pn,kind:"connect-timeout",logPath:Qe(r)}):ue({...De(o,!0),versionNote:t})}async function ia({cliEntry:e,connection:r,cwd:t}){let n=await hn(r);if(n==null||n.version===R())return ue(r);let o={daemonVersion:n.version};return await sa(r)?(r.socket.destroy(),await la(J(t))?mn({cliEntry:e,cwd:t,versionNote:{...o,kind:"restarted"}}):ee({kind:"connection-lost"})):ue({...r,versionNote:{...o,kind:"stale-busy"}})}async function sa(e){try{return await e.rpc.sendRequest(Se)}catch{return!1}}var aa=1e4;async function la(e){let r=Date.now()+aa,t=await G(e);for(;t!=null&&Date.now()<r;)t.destroy(),await nr(un),t=await G(e);return t?.destroy(),t==null}async function Te({connection:e,onEvent:r,request:t,token:n}){let o=await da({connection:e,onEvent:r,request:t,token:n});return o.kind==="transport"?ee(o.error):ue(o)}async function fn({connection:e,findingId:r,token:t}){try{let n=await e.rpc.sendRequest(st,{findingId:r},t),o=it.safeParse(n);return o.success?ue(o.data):ee({kind:"bad-frame"})}catch{return ee({kind:"connection-lost"})}}async function gn(e){let r=await G(J(e));if(r==null)return!1;let t=De(r,!1);try{return await t.rpc.sendRequest(Se)}catch{return!1}finally{r.destroy()}}async function te(e){try{await e.rpc.sendRequest(Se)}catch{}}async function Le(e){let r=await G(J(e));if(r==null)return{kind:"not-running"};let t=De(r,!1),n=await Promise.race([hn(t),nr(cn).then(()=>null)]);return r.destroy(),n==null?{kind:"unresponsive",timeoutMs:cn}:{kind:"running",status:n}}async function hn(e){try{let r=await e.rpc.sendRequest(lt),t=rt.safeParse(r);return t.success?t.data:null}catch{return null}}function da({connection:e,onEvent:r,request:t,token:n}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(dt,s=>{let l=tt.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}r(l.data.event)}),o.onNotification(ct,s=>{let l=nt.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(at,t,n).then(s=>{et.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(ca(s))})})}function ca(e){if(e instanceof oa){let r=ot.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 De(e,r){let t=Zr(e);return t.listen(),{rpc:t,socket:e,spawned:r,versionNote:void 0}}function G(e){return new Promise(r=>{let t=na.connect(e);t.once("connect",()=>{r(t)}),t.once("error",()=>{r(null)})})}function pa({cliEntry:e,cwd:r}){try{ye(r);let t=dn.openSync(Qe(r),"a");return ta(process.execPath,[e,"daemon"],{cwd:r,detached:!0,stdio:["ignore",t,t]}).unref(),dn.closeSync(t),null}catch(t){return{kind:"spawn-failed",message:t instanceof Error?t.message:String(t)}}}async function ua(e){let r=Date.now()+pn,t=await G(e);for(;t==null&&Date.now()<r;)await nr(un),t=await G(e);return t}function yn(e){if(e.pending.length===0&&e.recurrentFlaky.length===0)return"explore: no pending findings";let r=e.pending.length===0?[]:[`explore: ${C(e.pending.length,"pending finding")} \u2014 triage in layer order, top first`,...e.pending.map(o=>ma(o))],t=e.recurrentFlaky.length===0?[]:[`recurrent flaky-candidates (same divergence ${String(e.recurrentFlaky.length)}x, no deterministic repro \u2014 triage after findings):`,...e.recurrentFlaky.map(o=>ha(o))],n=c("fuzz","triaging each finding (evidence -> classify -> fix -> replay)");return[...r,...t,"","replay after a fix: npx ripplo explore replay <id>",n].join(`
|
|
216
|
+
`).trim()}function kn(e,r){switch(r.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current model`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current model \u2014 finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let t=r.runId==null?"":` (fresh evidence: run ${r.runId})`;return`explore: ${e} still reproduces \u2014 same divergence signature${t}`}case"diverged":{let t=r.runId==null?"":` (captured run ${r.runId})`;return`explore: ${e} failed with a different divergence signature \u2014 new finding recorded${t}`}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 wn({executed:e,progress:r,trail:t}){let n=t.trail.flatMap(o=>[` ${o.label}`,...o.actions.map(i=>` ${i}`)]);return[`trail ${String(e)} ${t.kind} \u2014 ${le(r)}`,...n,` state: ${t.label}`].join(`
|
|
217
|
+
`)}function vn(e){switch(e.kind){case"config-failed":return I(e.failure);case"compile-failed":return x(e.error);case"no-work":return"explore: nothing to explore \u2014 no transitions harvested from the suite";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`}
|
|
218
|
+
${le(e.progress)}
|
|
219
|
+
findings land in .ripplo/.local/explore-ledger.jsonl`}}function bn(e){let r=e.evidence.map(n=>` ${n}`),t=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.oracleLayer} depot=${e.depot}`,` seen ${String(e.occurrences)}x between ${or(e.firstSeen)} and ${or(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...r,...t,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
|
|
220
|
+
`)}function Sn(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function ma(e){let r=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.oracleLayer} seen ${String(e.occurrences)}x (last ${or(e.lastSeen)}) depot=${e.depot} ${r}`,` diverged: ${fa(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
|
|
221
|
+
`)}function or(e){return e.slice(0,16).replace("T"," ")}function fa(e){let r=e.at(0);if(r==null)return"no parts recorded";let t=e.length>1?` (+${String(e.length-1)} more)`:"";return`${ga(r)}${t}`}function ga(e){switch(e.kind){case"consistency":{let r="entity"in e.divergence?e.divergence.entity:e.divergence.singleton,t=e.step==null?"":` at "${e.step.intent}"`;return`${e.divergence.kind} on ${r}${t}`}case"obligation":return`${e.source} check failed at "${e.step.intent}"`;case"unfireable":return`step "${e.intent}" unfireable (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"illegal-transition":return`illegal transition at "${e.step}"`}}function ha(e){let r=e.trail.map(t=>t.split("|").at(0)??t);return` ${e.id} seen ${String(e.occurrences)}x depot=${e.depot}
|
|
222
|
+
trail: ${r.join(" -> ")}`}import ka from"fuse.js";var ya=3e3;async function _e(e){V(e);let r=w(e).unwrapOr(void 0);if(r==null)return null;try{return await fetch(`${r.ripploServerUrl}/health`,{signal:AbortSignal.timeout(ya)}),null}catch(t){return{detail:t instanceof Error?t.message:String(t),serverUrl:r.ripploServerUrl}}}function Oe(e){return e.includes("localhost")||e.includes("127.0.0.1")}function ir(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/tests files are dirty. Pass test ids, add tests to scope (${c("run")}), or use --all.`;case"unknown-ids":{let r=e.known.map(o=>$(o)),t=e.unknown.flatMap(o=>wa(o,r)),n=t.length>0?[`Did you mean: ${[...new Set(t)].join(", ")}`]:[];return[`Unknown test ${e.unknown.length===1?"id":"ids"}: ${e.unknown.join(", ")}`,...n,"Test ids are the slugs from run output (quoted intent strings also work). Known ids:",...r.map(o=>` ${o}`)].join(`
|
|
223
|
+
`)}}}function xn({failed:e,notRun:r,passed:t}){let n=r>0?`, ${String(r)} not run`:"",o=e>0?`
|
|
224
|
+
${c("debug")}`:"";return`${String(t)} passed, ${String(e)} failed${n} (${String(t+e+r)} total)${o}`}function Ne({debugDir:e,event:r}){if(r.kind==="test-started")return`${P.dim("run ")} ${r.testName}`;switch(r.outcome.kind){case"pass":return`${P.good("pass")} ${r.testName}`;case"findings":return[`${P.bad("fail")} ${r.testName} \u2014 ${C(r.outcome.findingLines.length,"finding")}`,...r.outcome.findingLines,Mr({debugDir:e,runId:r.runId})].join(`
|
|
225
|
+
`);case"error":return`${P.bad("error")} ${r.testName} \u2014 ${r.outcome.detail}`;case"dispatch-error":return`${P.bad("error")} ${r.testName} \u2014 failed to dispatch (${r.outcome.detail})`;case"infra-error":return`${P.bad("error")} ${r.testName} \u2014 not run: Ripplo server unreachable (server-side, not local): ${r.outcome.detail}`}}function Fe({detail:e,serverUrl:r}){return Oe(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 Ue(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(`
|
|
226
|
+
`)}case"selection-conflicting-flags":return ir({kind:"conflicting-flags"});case"selection-nothing-selected":return ir({kind:"nothing-selected"});case"selection-unknown-ids":return ir({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}
|
|
227
|
+
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 L(e){switch(e.kind){case"spawn-failed":return`Failed to start \`npx ripplo daemon\`: ${va(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(`
|
|
228
|
+
`);case"connection-lost":return["Lost the daemon connection mid-run \u2014 it likely crashed or was killed.","Check .ripplo/.local/daemon.log, then rerun (`npx ripplo run` auto-starts the daemon)."].join(`
|
|
229
|
+
`);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function wa(e,r){return new ka(r,{includeScore:!0,threshold:.5}).search($(e)).slice(0,3).map(n=>n.item)}function va(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}async function Cn({findingId:e,json:r}){await Xr(process.cwd()).match(t=>{if(e==null){let i=r?JSON.stringify(t,null,2):yn(t);process.stdout.write(`${i}
|
|
230
|
+
`);return}let n=t.pending.find(i=>i.id===e);n==null&&(process.stderr.write(`${Sn(e)}
|
|
231
|
+
`),process.exit(1));let o=r?JSON.stringify(n,null,2):bn(n);process.stdout.write(`${o}
|
|
232
|
+
`)},t=>{process.stderr.write(`explore: ledger unreadable (${t.kind})
|
|
233
|
+
`),process.exit(1)})}async function Rn({findingId:e}){let r=process.argv[1];r==null&&(process.stderr.write(`${L({kind:"spawn-failed",message:"process.argv[1] missing"})}
|
|
234
|
+
`),process.exit(1));let t=await re({cliEntry:r,cwd:process.cwd()});t.isErr()&&(process.stderr.write(`${L(t.error)}
|
|
235
|
+
`),process.exit(1));let n=t.value,o=new ba;process.once("SIGINT",()=>{o.cancel(),n.socket.destroy(),process.exit(130)});let a=await(await fn({connection:n,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${kn(e,s)}
|
|
236
|
+
`),n.spawned&&await te(n),s.kind==="resolved"||s.kind==="unreachable"?0:1),s=>(process.stderr.write(`${L(s)}
|
|
237
|
+
`),Promise.resolve(1)));n.socket.destroy(),process.exit(a)}async function Pn(e){let r=await ln({cwd:process.cwd(),headed:e.headed,maxLength:e.maxLength,trails:e.trails,onReady:t=>{process.stdout.write(`${le(t)}
|
|
238
|
+
`)},onTrail:(t,n,o)=>{process.stdout.write(`${wn({executed:t,progress:o,trail:n})}
|
|
239
|
+
`)}});process.stdout.write(`${vn(r)}
|
|
240
|
+
`),(r.kind==="config-failed"||r.kind==="compile-failed")&&process.exit(1)}import{graphql as Sa}from"gql.tada";var xa=Sa(`
|
|
238
241
|
query ProjectsList {
|
|
239
242
|
projects {
|
|
240
243
|
id
|
|
241
244
|
name
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
|
-
`);async function
|
|
245
|
-
`)}import{graphql as
|
|
247
|
+
`);async function En(){let e=N();e==null&&(process.stderr.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1));let r=A(),n=((await u({config:H({serverUrl:r,token:e}),document:xa,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
|
|
248
|
+
`)}import{graphql as Ca}from"gql.tada";function $n({id:e,kind:r,title:t}){return`Caught bug reported (${r}): "${t}" [${e}]`}var Ra=Ca(`
|
|
246
249
|
mutation ReportCaughtBug(
|
|
247
250
|
$projectId: String!
|
|
248
251
|
$kind: CaughtBugKind!
|
|
@@ -272,41 +275,53 @@ Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}), or pass t
|
|
|
272
275
|
}
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
|
-
`);async function
|
|
276
|
-
`),process.exit(1)}process.stdout.write(`${
|
|
277
|
-
`)}import
|
|
278
|
-
`)
|
|
279
|
-
`),r.
|
|
280
|
-
`),
|
|
281
|
-
`).
|
|
278
|
+
`);async function In(e){let r=T(),n=(await u({config:r,document:Ra,variables:{kind:e.kind,projectId:r.projectId,rootCause:e.rootCause,runId:e.runId??null,surfacedBy:e.surfacedBy,title:e.title,workflowSlug:e.testId==null?null:$(e.testId)}})).reportCaughtBug;if(n?.__typename!=="CaughtBug"){let o=n?.__typename==="WorkflowNotFoundError"?n.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
|
|
279
|
+
`),process.exit(1)}process.stdout.write(`${$n({id:n.id,kind:e.kind,title:e.title})}
|
|
280
|
+
`)}import Pa from"path";import{CancellationTokenSource as Ea}from"vscode-jsonrpc/node";async function jn({all:e,headed:r,ids:t,keepAlive:n}){let o=process.cwd(),i=process.argv[1];i==null&&me(L({kind:"spawn-failed",message:"process.argv[1] missing"}));let a=await _e(o);a!=null&&me(Fe(a));let l=(await re({cliEntry:i,cwd:o})).match(j=>j,j=>me(L(j)));l.versionNote!=null&&process.stderr.write(`${ut(l.versionNote)}
|
|
281
|
+
`);let d=new Ea;process.once("SIGINT",()=>{d.cancel(),l.socket.destroy(),process.exit(130)});let h=Pa.join(o,".ripplo","debug"),k=(await Te({connection:l,request:{all:e,headed:r,tests:[...t]},token:d.token,onEvent:j=>{let E=Ne({debugDir:h,event:j});E!=null&&process.stdout.write(`${E}
|
|
282
|
+
`)}})).match(j=>j,j=>me(L(j)));await $a({connection:l,keepAlive:n,result:k})}async function $a({connection:e,keepAlive:r,result:t}){e.spawned&&!r&&await te(e),await new Promise(n=>{e.socket.end(n)}),t.kind==="daemon-error"&&me(Ue(t.error)),process.stdout.write(`${xn(t)}
|
|
283
|
+
`),t.failed>0&&process.exit(1),process.exit(t.notRun>0?2:0)}function me(e){process.stderr.write(`${e}
|
|
284
|
+
`),process.exit(1)}import Ia from"path";import{err as He,ok as sr}from"neverthrow";async function An(){let e=process.cwd(),r=O.child({worker:process.pid}),t=Rt(),n=be({clientVersion:R(),debugDir:Ia.join(e,".ripplo","debug"),headed:!1,writeOtlpPortFile:!1}),o={entry:void 0},i=a=>{n.close().catch(()=>{}).then(()=>{process.exit(a)})};process.on("disconnect",()=>{i(1)}),process.on("unhandledRejection",a=>{r.error({err:a},"worker unhandled rejection")}),process.on("uncaughtException",a=>{r.error({err:a},"worker uncaught exception"),i(1)}),t.onRequest(vt,async(a,s)=>{let l=ht.safeParse(a);if(!l.success)return Me("bad-run-assign");let d=l.data;V(e);let h=w(e).match(E=>E,E=>E.kind);if(typeof h=="string")return Me(`config:${h}`);let p=await Tn(d.lockfileFingerprint,o,t);if(p.isErr())return Me(`lockfile-unavailable:${p.error}`);let k=p.value,j=k.tests.find(E=>$(E.name)===d.workflowSlug);if(j==null)return Me(`no-test:${d.workflowSlug}`);try{let E=await gt({config:h,cwd:e,headed:d.headed,lockfile:k,runId:d.runId,session:n,signal:Ye(s),test:j});return{outcome:mt(E),serverNotified:!0}}catch(E){if(E instanceof gr)return{outcome:{detail:E.message,kind:"infra-error"},serverNotified:!1};throw E}}),t.onRequest(St,(a,s)=>ja({cache:o,connection:t,cwd:e,raw:a,session:n,token:s})),t.onNotification(xt,a=>{let s=yt.safeParse(a);s.success&&n.injectSpan(s.data.runId,s.data.span)}),t.onNotification(Ct,()=>{i(0)}),t.sendNotification(wt),await new Promise(()=>{})}function Me(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function ja({cache:e,connection:r,cwd:t,raw:n,session:o,token:i}){let a=kt.safeParse(n);if(!a.success)return{kind:"error",reason:"bad-assign",rows:[],trail:[]};let s=a.data;V(t);let l=w(t).match(p=>p,p=>p.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,rows:[],trail:[]};let d=await Tn(s.lockfileFingerprint,e,r);if(d.isErr())return{kind:"error",reason:`lockfile:${d.error}`,rows:[],trail:[]};let h=d.value;return Ae({assign:s,config:l,headed:!1,lockfile:h,session:o,signal:Ye(i)})}async function Tn(e,r,t){return r.entry!=null&&r.entry.fingerprint===e?sr(r.entry.lockfile):(await Aa(t,e)).andThen(o=>o.unavailable!=null?He(o.unavailable):o.lockfileJson==null?He("empty-reply"):Ta(o.lockfileJson).map(i=>(r.entry={fingerprint:e,lockfile:i},i)))}async function Aa(e,r){try{return sr(await e.sendRequest(bt,{fingerprint:r}))}catch(t){return He(`transport:${t instanceof Error?t.message:"unknown"}`)}}function Ta(e){try{return sr(Pr(M,e))}catch(r){return He(`worker-decode-failed:${r instanceof Error?r.message.slice(0,200):"unknown"}`)}}async function Ln(){await An()}import{createRequire as La}from"module";import{existsSync as Da}from"fs";import{readFile as _n,writeFile as _a}from"fs/promises";import{fileURLToPath as Oa}from"url";import ar from"path";import{err as fe,ok as Na}from"neverthrow";import{z as _}from"zod";var Fa={height:800,width:1280};async function On({cwd:e,moment:r,runId:t}){let n=ar.join(e,".ripplo","debug",t),o=ar.join(n,"behavior.jsonl");if(!Da(o))return fe({kind:"run-not-found",runId:t});let i=await Ma(o),a=i[0],s=i.at(-1);if(a==null||s==null)return fe({kind:"no-rrweb-events",runId:t});let l=s.timestamp-a.timestamp,d=r.kind==="offset"?r.offsetMs:r.at-a.timestamp;if(d<0||d>l)return fe({durationMs:l,firstTimestamp:a.timestamp,kind:"moment-out-of-range",lastTimestamp:s.timestamp,moment:r});let h=ar.join(n,`snapshot-${String(Math.round(d))}ms.png`);return(await Ga({events:i,offsetMs:d,pngPath:h})).map(()=>({durationMs:l,offsetMs:d,pngPath:h}))}var Ua=_.object({event:_.looseObject({timestamp:_.number(),type:_.number()}),kind:_.literal("rrweb")});async function Ma(e){return(await _n(e,"utf8")).split(`
|
|
285
|
+
`).filter(t=>t.length>0).map(t=>Ha(t)).map(t=>Ua.safeParse(t)).flatMap(t=>t.success?[t.data.event]:[])}function Ha(e){try{return JSON.parse(e)}catch{return null}}var Wa=_.object({data:_.looseObject({height:_.number(),width:_.number()}),type:_.literal(4)});function qa(e){let r=e.flatMap(t=>{let n=Wa.safeParse(t);return n.success?[n.data]:[]})[0];return r==null?Fa:{height:r.data.height,width:r.data.width}}var Ba=`<!doctype html><html><head><style>
|
|
282
286
|
html, body { margin: 0; padding: 0; }
|
|
283
287
|
.replayer-wrapper { position: relative; }
|
|
284
288
|
.replayer-mouse, .replayer-mouse-tail { display: none; }
|
|
285
289
|
iframe { border: none; }
|
|
286
|
-
</style></head><body></body></html>`,
|
|
290
|
+
</style></head><body></body></html>`,Va="*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }";async function Ga({events:e,offsetMs:r,pngPath:t}){let n=await import("playwright").then(({chromium:o})=>o.launch({headless:!0})).catch(()=>null);if(n==null)return fe({detail:"chromium launch",kind:"browser-failed"});try{let o=await za({browser:n,events:e,offsetMs:r});return await _a(t,o),Na(void 0)}catch(o){return fe({detail:o instanceof Error?o.message:String(o),kind:"browser-failed"})}finally{await n.close()}}async function za({browser:e,events:r,offsetMs:t}){let n=qa(r),o=await e.newPage({viewport:n});await o.setContent(Ba),await o.addScriptTag({content:await Qa()}),await o.evaluate(Ja({events:r,offsetMs:t})),await o.evaluate(Ka());let i=o.locator(".replayer-wrapper iframe").first();return(await i.count()>0?i:o).screenshot({type:"png"})}function Ja({events:e,offsetMs:r}){return`(() => {
|
|
287
291
|
const replayer = new globalThis.__RipploReplayer(${JSON.stringify(e)}, {
|
|
288
|
-
insertStyleRules: [${JSON.stringify(
|
|
292
|
+
insertStyleRules: [${JSON.stringify(Va)}],
|
|
289
293
|
mouseTail: false,
|
|
290
294
|
root: document.body,
|
|
291
295
|
showWarning: false,
|
|
292
296
|
});
|
|
293
|
-
replayer.pause(${String(
|
|
294
|
-
})()`}function
|
|
297
|
+
replayer.pause(${String(r)});
|
|
298
|
+
})()`}function Ka(){return`(async () => {
|
|
295
299
|
const doc = document.querySelector(".replayer-wrapper iframe")?.contentDocument;
|
|
296
300
|
if (doc?.fonts != null) {
|
|
297
301
|
await doc.fonts.ready;
|
|
298
302
|
}
|
|
299
303
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
300
|
-
})()`}var
|
|
301
|
-
`)}function
|
|
302
|
-
`),process.exit(1)),(await
|
|
303
|
-
`)},i=>{process.stderr.write(`${
|
|
304
|
-
`),process.exit(1)})}function
|
|
305
|
-
`),process.exit(1)}),n=
|
|
306
|
-
`)}catch(
|
|
307
|
-
`),process.stderr.write(`${
|
|
308
|
-
`),process.exit(1)}}
|
|
309
|
-
`)
|
|
304
|
+
})()`}var Dn;function Qa(){return Dn??=_n(Ya(),"utf8"),Dn}function Ya(){let e=La(import.meta.url);try{return e.resolve("@ripplo/rrweb-bundle/replay")}catch{return Oa(new URL("assets/rrweb-replay.js",import.meta.url))}}function Nn(e){return[`${P.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(`
|
|
305
|
+
`)}function Fn(e){return`${P.bad("fail")} \u2014 ${Xa(e)}`}function Un(){return`${P.bad("fail")} \u2014 pass exactly one of --at <epoch-ms from behavior.jsonl> or --offset <ms from the start of the recording>.`}function Xa(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${c("debug")}`;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 Mn({at:e,offset:r,runId:t}){let n=Za({at:e,offset:r});n==null&&(process.stderr.write(`${Un()}
|
|
306
|
+
`),process.exit(1)),(await On({cwd:process.cwd(),moment:n,runId:t})).match(i=>{process.stdout.write(`${Nn(i)}
|
|
307
|
+
`)},i=>{process.stderr.write(`${Fn(i)}
|
|
308
|
+
`),process.exit(1)})}function Za({at:e,offset:r}){if(e!=null&&r==null)return{at:e,kind:"absolute"};if(r!=null&&e==null)return{kind:"offset",offsetMs:r}}async function Hn(){let e=T();try{let t=(await de(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${I(i)}
|
|
309
|
+
`),process.exit(1)}),n=t.lockfile.tests.length,o=t.lockfile.entities.length;process.stdout.write(`Synced ${C(n,"test")} and ${C(o,"entity","entities")} to dev session ${t.devSessionId}
|
|
310
|
+
`)}catch(r){let t=r instanceof Error?r.message:String(r);process.stderr.write(`ripplo sync failed: ${t}
|
|
311
|
+
`),process.stderr.write(`${c("setup","verify auth + server reachability")}
|
|
312
|
+
`),process.exit(1)}}import{spawnSync as tl}from"child_process";import nl from"fs";import ol from"path";import il from"semver";import We from"fs";import el from"os";import ge from"path";function Wn(e){return e.split(ge.sep).includes("_npx")?"npx":e.includes(ge.join("packages","cli"))?"workspace":"global"}function qn(){let e=ge.join(el.homedir(),".npm","_npx");return We.existsSync(e)?We.readdirSync(e).map(r=>ge.join(e,r)).filter(r=>We.existsSync(ge.join(r,"node_modules","ripplo"))).map(r=>(We.rmSync(r,{force:!0,recursive:!0}),r)):[]}import{spawnSync as Bn}from"child_process";var rl=["user","project","local"];function lr(){return Bn("claude",["--version"],{stdio:"ignore"}).error!=null?"claude-missing":rl.find(t=>Bn("claude",["plugin","update","ripplo","--scope",t],{stdio:"ignore"}).status===0)==null?"not-installed":"updated"}function Vn(e){return`ripplo v${e} is already the latest version.`}function Gn(e){return`ripplo v${e}: could not reach the npm registry to check for updates.`}function dr({evicted:e,latest:r,mode:t}){return t==="npx"?`ripplo: ${e===0?"npx cache had no stale copy":`cleared ${C(e,"stale npx cache entry","stale npx cache entries")}`} \u2014 the next \`npx ripplo\` command runs v${r}.`:`ripplo: updated global install to v${r}.`}function zn(){return"ripplo: this is a workspace build (packages/cli/dist) \u2014 update via git pull + `pnpm --filter ripplo build`."}function Jn(e){return`ripplo: \`npm install -g ripplo@latest\` failed: ${e}`}function cr(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 Kn(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 sl=1e4;async function Qn(){let e=R(),r=process.argv[1];al(process.cwd());let t=await Ce(sl);process.stdout.write(`${xe({current:e,latest:t})}
|
|
313
|
+
`),t==null&&(process.stderr.write(`${Gn(e)}
|
|
314
|
+
`),process.exit(1)),il.gt(t,e)||(process.stdout.write(`${Vn(e)}
|
|
315
|
+
`),process.stdout.write(`${cr(lr())}
|
|
316
|
+
`),process.exit(0));let n=r==null?"global":Wn(r);ll({latest:t,mode:n}),process.stdout.write(`${cr(lr())}
|
|
317
|
+
`),await cl(process.cwd()),process.exit(0)}function al(e){if(!nl.existsSync(ol.join(e,".ripplo")))return;let r=$e(e);r==="written"&&process.stdout.write(`${Ie(r).trim()}
|
|
318
|
+
`)}function ll({latest:e,mode:r}){if(r==="workspace"&&(process.stdout.write(`${zn()}
|
|
319
|
+
`),process.exit(0)),r==="npx"){let t=qn();process.stdout.write(`${dr({evicted:t.length,latest:e,mode:r})}
|
|
320
|
+
`);return}dl(e)}function dl(e){let r=`ripplo@${e}`,t=tl("npm",["install","-g",r],{stdio:"inherit"});if(t.status!==0){let n=`exit ${String(t.status)}`;process.stderr.write(`${Jn(n)}
|
|
321
|
+
`),process.exit(1)}process.stdout.write(`${dr({evicted:0,latest:e,mode:"global"})}
|
|
322
|
+
`)}async function cl(e){if((await Le(e)).kind==="not-running")return;let t=await gn(e);process.stdout.write(`${Kn(t)}
|
|
323
|
+
`)}async function Yn({explore:e,exploreConcurrency:r}){let{runDaemon:t}=await import("./daemon-4ULJF2OG.js");await t({explore:e,exploreConcurrency:r})}import{graphql as qe}from"gql.tada";function Xn(){return"No scope items added \u2014 the matched tests are already in scope (check `npx ripplo scope status`)."}function Zn(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(`
|
|
324
|
+
`)}import{graphql as pl}from"gql.tada";var eo=pl(`
|
|
310
325
|
query ScopeStatus($projectId: String!, $cwd: String!) {
|
|
311
326
|
project(id: $projectId) {
|
|
312
327
|
id
|
|
@@ -326,7 +341,7 @@ iframe { border: none; }
|
|
|
326
341
|
}
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
|
-
`);var
|
|
344
|
+
`);var ul=qe(`
|
|
330
345
|
query ScopeWorkflowBySlug($projectId: String!, $cwd: String!, $slug: String!) {
|
|
331
346
|
project(id: $projectId) {
|
|
332
347
|
id
|
|
@@ -339,7 +354,7 @@ iframe { border: none; }
|
|
|
339
354
|
}
|
|
340
355
|
}
|
|
341
356
|
}
|
|
342
|
-
`),
|
|
357
|
+
`),ml=qe(`
|
|
343
358
|
mutation ScopeAddDirtyTests($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
344
359
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
345
360
|
__typename
|
|
@@ -361,38 +376,38 @@ iframe { border: none; }
|
|
|
361
376
|
}
|
|
362
377
|
}
|
|
363
378
|
}
|
|
364
|
-
`),
|
|
379
|
+
`),fl=qe(`
|
|
365
380
|
mutation ScopeLink($id: ID!, $workflowId: String!) {
|
|
366
381
|
linkScopeItem(id: $id, workflowId: $workflowId) {
|
|
367
382
|
id
|
|
368
383
|
}
|
|
369
384
|
}
|
|
370
|
-
`),
|
|
385
|
+
`),gl=qe(`
|
|
371
386
|
mutation ScopeRemoveMany($ids: [ID!]!) {
|
|
372
387
|
removeScopeItems(ids: $ids)
|
|
373
388
|
}
|
|
374
|
-
`);async function
|
|
375
|
-
`);return}if(n.length===0){process.stdout.write("No scope items. Add
|
|
389
|
+
`);async function ro(e){let r=T();await ce(r);let n=(await u({config:r,document:eo,variables:{cwd:r.cwd,projectId:r.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
|
|
390
|
+
`);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??""}
|
|
376
391
|
`);return}let a=i.spec==null?"stub":"implemented";process.stdout.write(` [${a}] (${o.id}) ${i.slug} \u2014 ${i.name}
|
|
377
|
-
`)})}async function
|
|
378
|
-
`),process.exit(1)});let o=(await
|
|
379
|
-
`),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${
|
|
380
|
-
`),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${
|
|
381
|
-
`);return}let a=i.map(s=>s.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${
|
|
382
|
-
`)}async function
|
|
383
|
-
`),process.exit(1)});let o=await
|
|
384
|
-
`)}async function
|
|
385
|
-
`)}async function
|
|
386
|
-
`),process.stderr.write(`${
|
|
387
|
-
`),process.exit(1)),n.id}async function
|
|
388
|
-
`),process.exit(1));let n=
|
|
389
|
-
`),process.stdout.write(`${
|
|
392
|
+
`)})}async function to({testIds:e}){let r=T();await ce(r),(await de(r.cwd,r)).match(()=>{},s=>{process.stderr.write(`${I(s)}
|
|
393
|
+
`),process.exit(1)});let o=(await u({config:r,document:ml,variables:{cwd:r.cwd,projectId:r.projectId,workflowSlugs:e.map(s=>$(s))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
|
|
394
|
+
`),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${Zn(o.slugs)}
|
|
395
|
+
`),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${Xn()}
|
|
396
|
+
`);return}let a=i.map(s=>s.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${C(i.length,"scope item")}: ${a}
|
|
397
|
+
`)}async function no({id:e,testId:r}){let t=T();await ce(t),(await de(t.cwd,t)).match(()=>{},i=>{process.stderr.write(`${I(i)}
|
|
398
|
+
`),process.exit(1)});let o=await hl({cfg:t,slug:r});await u({config:t,document:fl,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${r}
|
|
399
|
+
`)}async function oo({ids:e}){let r=T();await ce(r);let n=(await u({config:r,document:gl,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${C(n,"scope item")}
|
|
400
|
+
`)}async function hl({cfg:e,slug:r}){let n=(await u({config:e,document:ul,variables:{cwd:e.cwd,projectId:e.projectId,slug:r}})).project?.devSession?.workflows?.[0];return n==null&&(process.stderr.write(`No workflow found with id "${r}". Create a stub first via the testing DSL.
|
|
401
|
+
`),process.stderr.write(`${c("create")}
|
|
402
|
+
`),process.exit(1)),n.id}async function io(e){let r=process.cwd(),t=await S(r);t.isErr()&&(process.stderr.write(`${x(t.error)}
|
|
403
|
+
`),process.exit(1));let n=U(t.value),o=await Le(r);if(e.format==="summary"){n.length>0&&process.stdout.write(`stub tests: ${n.join(", ")}
|
|
404
|
+
`),process.stdout.write(`${pt(o)}
|
|
390
405
|
`);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(a=>({id:a,implemented:!1}))};process.stdout.write(`${JSON.stringify(i,null,2)}
|
|
391
|
-
`)}import
|
|
392
|
-
`),hookEventName:"UserPromptSubmit"}}});import
|
|
393
|
-
`).filter(
|
|
394
|
-
`).filter(
|
|
395
|
-
`).filter(n=>n.length>0).filter(n=>
|
|
406
|
+
`)}import Be from"fs";import kl from"os";import so from"path";import{z as yl}from"zod";function f(e,r){let t=yl.custom(n=>typeof n=="object"&&n!==null&&"hook_event_name"in n&&n.hook_event_name===e);return{event:e,run:async n=>await r(t.parse(n))??void 0}}var ao=f("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!v(e.cwd))return;let r=wl();if(r==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/tests/ test per affected flow. ${c("discover")}`,hookEventName:"PreToolUse"}};let t=Be.readFileSync(r,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(t))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/tests/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${c("discover")}`}}});function wl(){let e=so.join(kl.homedir(),".claude","plans");return Be.existsSync(e)?Be.readdirSync(e).filter(t=>t.endsWith(".md")).map(t=>so.join(e,t)).map(t=>({full:t,mtime:Be.statSync(t).mtimeMs})).sort((t,n)=>n.mtime-t.mtime)[0]?.full??null:null}var lo=f("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!m(e.cwd)||!v(e.cwd))return;let r=await S(e.cwd);if(r.isErr())return;let t=U(r.value),n=['Plan must include "Tests to implement" with a .ripplo/tests/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `test("Intent")` (no body).'];return t.length>0&&n.push(`Existing stubs: ${t.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
|
|
407
|
+
`),hookEventName:"UserPromptSubmit"}}});import Tl from"path";import ko from"picomatch";import{z as wo}from"zod";import{mkdirSync as Cl,readFileSync as Rl,writeFileSync as Pl}from"fs";import El from"path";import{z as b}from"zod";import{createHash as _g}from"crypto";import Ve from"picomatch";function vl(e){return F(["diff","--name-only","HEAD"],e).split(`
|
|
408
|
+
`).filter(r=>r.length>0)}function co({cwd:e,ignoreGlobs:r,watchGlobs:t}){let n=Ve([...t]),o=Ve([...r]);return vl(e).filter(i=>n(i)&&!o(i))}function bl(e){return F(["ls-files","--others","--exclude-standard"],e).split(`
|
|
409
|
+
`).filter(r=>r.length>0)}function po({cwd:e,ignoreGlobs:r,watchGlobs:t}){let n=Ve([...t]),o=Ve([...r]);return bl(e).filter(i=>n(i)&&!o(i))}var Sl=["**/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 ne(){return{ignorePaths:xl,watchPaths:Sl}}var $l=b.object({label:b.string().nullable(),slug:b.string().nullable(),status:b.enum(["intent","stub","implemented"])}),Il=b.object({intent:b.string(),name:b.string(),sourcePath:b.string().nullable(),stub:b.boolean()}),jl=b.object({changedAppFiles:b.array(b.string()).readonly(),scope:b.object({available:b.boolean(),items:b.array($l).readonly()}),tests:b.array(Il).readonly().default([]),untrackedAppFiles:b.array(b.string()).readonly()});function uo({cwd:e,scope:r}){let t=fo(e);ho(e,{...yo(e),scope:r,tests:t?.tests??[]})}function mo({cwd:e,tests:r}){let t=fo(e);ho(e,{...yo(e),scope:t?.scope??{available:!1,items:[]},tests:r??t?.tests??[]})}function fo(e){let r=jl.safeParse(Al(go(e)));return r.success?r.data:null}function Al(e){try{return JSON.parse(Rl(e,"utf8"))}catch{return null}}function go(e){return yr(e,"coverage-context.json")}function ho(e,r){let t=go(e);Cl(El.dirname(t),{recursive:!0}),Pl(t,JSON.stringify(r,null,2))}function yo(e){let{ignorePaths:r,watchPaths:t}=ne(),n={cwd:e,ignoreGlobs:r,watchGlobs:t};return{changedAppFiles:co(n),untrackedAppFiles:po(n)}}var Ll=wo.looseObject({file_path:wo.string()}),vo=f("PostToolUse",async e=>{let r=Ll.safeParse(e.tool_input);if(!r.success)return;let t=r.data.file_path,{cwd:n}=e;if(!m(n)||!v(n))return;let o=Tl.relative(n,t);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:a}=ne(),s=ko([...a]),l=ko([...i]);if(!s(o)||l(o))return;let d=await S(n);if(mo({cwd:n,tests:d.isOk()?d.value.tests.map(p=>({intent:p.intent,name:p.name,sourcePath:p.sourcePath??null,stub:p.stub})):void 0}),d.isErr())return;let h=U(d.value);if(h.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub tests still unimplemented \u2014 ${h.join(", ")}. Implement with \`test("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as ql}from"crypto";import{z as Ro}from"zod";import{createHash as Dl}from"crypto";import{mkdirSync as _l,readFileSync as bo,writeFileSync as Ol}from"fs";import pr from"path";var Nl=[".ts",".tsx",".js",".jsx"];function Ge(e){let r=Dl("sha256");return r.update(F(["rev-parse","HEAD"],e)),r.update("\0"),r.update(F(["diff","HEAD"],e)),r.update("\0"),F(["ls-files","--others","--exclude-standard"],e).split(`
|
|
410
|
+
`).filter(n=>n.length>0).filter(n=>Nl.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{r.update(n),r.update("\0"),r.update(Fl(pr.join(e,n))),r.update("\0")}),r.digest("hex")}function oe(e,r){try{return bo(So(e,r),"utf8").trim()}catch{return null}}function ie(e,r,t){let n=So(e,r);_l(pr.dirname(n),{recursive:!0}),Ol(n,t)}function Fl(e){try{return bo(e)}catch{return Buffer.alloc(0)}}function So(e,r){return pr.join(e,".ripplo",".local",`${r}.hash`)}import{graphql as Ul}from"gql.tada";var Ml=Ul(`
|
|
396
411
|
mutation AutoScopeAddDirty($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
|
|
397
412
|
addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
|
|
398
413
|
__typename
|
|
@@ -403,11 +418,11 @@ iframe { border: none; }
|
|
|
403
418
|
}
|
|
404
419
|
}
|
|
405
420
|
}
|
|
406
|
-
`);async function
|
|
407
|
-
`).map(
|
|
408
|
-
${
|
|
409
|
-
${
|
|
410
|
-
`),hookEventName:"PostToolUse"}}}function
|
|
421
|
+
`);async function Co({cwd:e,lockfile:r}){if(!v(e))return{addedSlugs:[]};let t=Wl(e);if(t.length===0)return{addedSlugs:[]};let n=new Set(t),o=r.tests.filter(l=>l.sourcePath!=null&&n.has(l.sourcePath)).map(l=>$(l.name));if(o.length===0)return{addedSlugs:[]};let i=w(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await ft({config:i,cwd:e,lockfile:r}).catch(l=>(O.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await Hl({cfg:i,slugs:o})?o:[]}}async function Hl({cfg:e,slugs:r}){let n=(await u({config:e,document:Ml,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...r]}}).catch(o=>(O.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return n?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:n.data.length>0}var xo=".ripplo/tests/";function Wl(e){let r;try{r=F(["status","--porcelain","--",".ripplo/tests"],e)}catch{return[]}return r.split(`
|
|
422
|
+
`).map(t=>t.slice(3).trim()).filter(t=>t.startsWith(xo)&&t.endsWith(".ts")).map(t=>t.slice(xo.length)).filter(t=>t!=="index.ts"&&!t.endsWith("/index.ts"))}var Bl=Ro.looseObject({file_path:Ro.string()}),Po=f("PostToolUse",async e=>{let r=Bl.safeParse(e.tool_input);if(!r.success||!/\/\.ripplo\/.*\.ts$/.test(r.data.file_path))return;let{cwd:t}=e;if(!m(t))return;if(!v(t))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 Q(t);if(n.isErr())return{decision:"block",reason:`${x(n.error)}
|
|
423
|
+
${c("create","DSL authoring + lint rules")}`};let o=W(n.value);if(o.length>0)return{decision:"block",reason:`${q(o)}
|
|
424
|
+
${c("create")}`};let{addedSlugs:i}=await Co({cwd:t,lockfile:n.value});return Vl([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Gl(t,n.value)])});function Vl(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
|
|
425
|
+
`),hookEventName:"PostToolUse"}}}function Gl(e,r){let t=je(r),n=ql("sha256").update(JSON.stringify(t)).digest("hex");return oe(e,"coverage-warn")===n?[]:(ie(e,"coverage-warn",n),t.length===0?[]:[Re(t)])}import{z as Eo}from"zod";var zl=Eo.looseObject({command:Eo.string()}),Jl=/\bripplo\s+hooks\s+pause\b/,$o=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=zl.safeParse(e.tool_input);if(!r.success||!Jl.test(r.data.command))return;let{cwd:t}=e;if(m(t))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 Zl}from"shell-quote";import{z as jo}from"zod";import{existsSync as Kl,mkdirSync as Ql,rmSync as Oh,writeFileSync as Yl}from"fs";import ur from"path";function ze(e,r,t){let n=Io(e,r,t);Ql(ur.dirname(n),{recursive:!0}),Yl(n,"")}function Je(e,r,t){return Kl(Io(e,r,t))}function Io(e,r,t){return ur.join(Xl(e,r),t)}function Xl(e,r){return ur.join(e,".ripplo",".local","skills-loaded",r)}var ed=jo.looseObject({command:jo.string()}),rd="debug",td=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),To=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=ed.safeParse(e.tool_input);if(!r.success)return;let t=sd(r.data.command),n=nd(t);if(n==null)return;let{cwd:o}=e;if(!m(o)||!v(o))return;if(!Je(o,e.session_id,n.skill))return Ao(n.reason);let i=n.kind==="run"?ld(t):null;if(i!=null)return Ao(`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 nd(e){return od(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"}:id(e)?{kind:"run",reason:"Running `ripplo run` requires the `/ripplo:debug` skill loaded first. Load `/ripplo:debug` then retry \u2014 it carries the artifact-read order and the no-grep-piping guidance for run failures.",skill:rd}:null}function od(e){return e.some((r,t)=>r==="ripplo"&&e[t+1]==="explore"&&e[t+2]==="replay")}function id(e){return e.some((r,t)=>r==="ripplo"&&e[t+1]==="run")}function sd(e){try{return Zl(e)}catch{return[]}}function ad(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function ld(e){let r=e.find((t,n)=>{let o=e[n-1];return o!=null&&ad(o)&&typeof t=="string"&&td.has(t)});return typeof r=="string"?r:null}function Ao(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import dd from"path";import{z as Lo}from"zod";var cd=new Set(["Edit","Write","NotebookEdit"]),pd=Lo.looseObject({file_path:Lo.string()}),ud="create",Do=f("PreToolUse",e=>{if(!cd.has(e.tool_name))return;let r=pd.safeParse(e.tool_input);if(!r.success)return;let{cwd:t}=e;if(!m(t)||!v(t))return;let n=dd.relative(t,r.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!Je(t,e.session_id,ud))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 md from"path";import _o from"picomatch";import{graphql as fd}from"gql.tada";import{z as Oo}from"zod";var gd=new Set(["Edit","Write","NotebookEdit"]),hd=Oo.looseObject({file_path:Oo.string()}),yd=fd(`
|
|
411
426
|
query PreEditScopeGate($projectId: String!, $cwd: String!) {
|
|
412
427
|
project(id: $projectId) {
|
|
413
428
|
id
|
|
@@ -419,9 +434,9 @@ ${d("create")}`};let{addedSlugs:i}=await Vn({cwd:r,lockfile:n.value});return za(
|
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
}
|
|
422
|
-
`),
|
|
437
|
+
`),No=f("PreToolUse",async e=>{if(!gd.has(e.tool_name))return;let r=hd.safeParse(e.tool_input);if(!r.success)return;let{cwd:t}=e;if(!m(t)||!v(t))return;let n=md.relative(t,r.data.file_path);if(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!kd(n))return;let o=await wd(t);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). ${$r(["run","create"])}`}}});function kd(e){let{ignorePaths:r,watchPaths:t}=ne(),n=_o([...t]),o=_o([...r]);return n(e)&&!o(e)}async function wd(e){let r=w(e).unwrapOr(void 0);if(r==null)return{degradedReason:"no project config \u2014 `npx ripplo init` not run here",populated:!0};let t=await u({config:r,document:yd,variables:{cwd:r.cwd,projectId:r.projectId}}).catch(()=>null);return t==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(t.project?.devSession?.scopeItems??[]).length>0}}import vd from"fs";import bd from"path";import{z as Fo}from"zod";var Sd=new Set(["Edit","Write","NotebookEdit"]),xd=Fo.looseObject({file_path:Fo.string()}),Uo=f("PreToolUse",async e=>{if(!Sd.has(e.tool_name))return;let r=Cd(e);if(r==null)return;let{cwd:t}=e;if(vd.existsSync(ae(t))||ke(t))return;let n=await S(t);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:
|
|
423
438
|
${x(n.error)}
|
|
424
|
-
${
|
|
439
|
+
${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running \u2014 this edit to \`${r}\` 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 Cd(e){let r=xd.safeParse(e.tool_input);if(!r.success||!m(e.cwd))return null;let t=bd.relative(e.cwd,r.data.file_path);return t.startsWith("..")||t!==".ripplo"&&!t.startsWith(".ripplo/")?null:t}import{graphql as Rd}from"gql.tada";var Pd=Rd(`
|
|
425
440
|
query ScopeReminder($projectId: String!, $cwd: String!) {
|
|
426
441
|
project(id: $projectId) {
|
|
427
442
|
id
|
|
@@ -438,15 +453,15 @@ ${d("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookS
|
|
|
438
453
|
}
|
|
439
454
|
}
|
|
440
455
|
}
|
|
441
|
-
`),
|
|
456
|
+
`),Mo=f("UserPromptSubmit",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let t=Ge(r);if(oe(r,"scope-reminder")===t)return;let n=w(r).unwrapOr(void 0);if(n==null)return;let o=await u({config:n,document:Pd,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);ie(r,"scope-reminder",t);let i=o?.project?.devSession?.scopeItems??[];return uo({cwd:r,scope:{available:o!=null,items:i.map(s=>Ed(s))}}),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)}):
|
|
442
457
|
${i.map(s=>{let l=s.workflow;return l==null?` [intent] (${s.id}) ${s.label??""}`:` [${l.spec==null?"stub":"implemented"}] (${s.id}) ${l.slug}`}).join(`
|
|
443
|
-
`)}`,hookEventName:"UserPromptSubmit"}}});function
|
|
458
|
+
`)}`,hookEventName:"UserPromptSubmit"}}});function Ed(e){let r=e.workflow;return r==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:r.slug,status:r.spec==null?"stub":"implemented"}}var $d="# 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, oracle-based backend 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 and managing the testing scope this session is responsible for.\n- Load `/ripplo:debug` skill for instructions on a failed run \u2014 read artifacts in `.ripplo/debug/<runId>/` before re-running.\n- Load `/ripplo:fuzz` skill for instructions on triaging findings from the background explorer's ledger.\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 the oracle checks observed-vs-model. 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 oracle's 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, oracle). Full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",Ho=f("SessionStart",e=>{if(m(e.cwd))return{hookSpecificOutput:{additionalContext:$d,hookEventName:"SessionStart"}}});import Id from"path";import jd from"process";import{CancellationTokenSource as Ad}from"vscode-jsonrpc/node";import{graphql as Td}from"gql.tada";function Wo(e){return`--- Ripplo Run Failures (scope) ---
|
|
444
459
|
${e.join(`
|
|
445
460
|
`)}
|
|
446
|
-
Artifacts: .ripplo/debug/<runId>/. ${
|
|
447
|
-
`)}function
|
|
461
|
+
Artifacts: .ripplo/debug/<runId>/. ${c("debug")}`}function mr({lines:e,retried:r}){let t=r?"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).",t,"Wait a moment, then stop again to re-verify."].filter(n=>n.length>0).join(`
|
|
462
|
+
`)}function he(e){return`--- Ripplo Run Could Not Execute ---
|
|
448
463
|
${e}
|
|
449
|
-
Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${
|
|
464
|
+
Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${c("start")}`}var Ld=Td(`
|
|
450
465
|
query ScopeEnforce($projectId: String!, $cwd: String!) {
|
|
451
466
|
project(id: $projectId) {
|
|
452
467
|
id
|
|
@@ -465,26 +480,26 @@ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`)
|
|
|
465
480
|
}
|
|
466
481
|
}
|
|
467
482
|
}
|
|
468
|
-
`),
|
|
483
|
+
`),Bo=f("Stop",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let t=Ge(r),n=oe(r,"stop-enforce")===t;if(n&&!e.stop_hook_active)return;let o=await Dd(r);if(ie(r,"stop-enforce",t),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:
|
|
469
484
|
${o.errors.join(`
|
|
470
485
|
|
|
471
486
|
`)}`}:{decision:"block",reason:o.errors.join(`
|
|
472
487
|
|
|
473
|
-
`)}});async function
|
|
474
|
-
${x(
|
|
475
|
-
${
|
|
476
|
-
${
|
|
477
|
-
${
|
|
478
|
-
${
|
|
488
|
+
`)}});async function Dd(e){let r=await Q(e);if(r.isErr())return{errors:[`--- Compilation failed ---
|
|
489
|
+
${x(r.error)}
|
|
490
|
+
${c("create")}`],infraOnly:!1};let t=r.value,n=await Nd(e,t),o=_d(t),i=Od(t),a=n.runnableSlugs.length>0?await Ud(e):null,s=[o,i,a?.error??null,n.error].filter(d=>d!=null),l=s.length>0&&o==null&&i==null&&n.error==null&&(a?.infra??!1);return{errors:s,infraOnly:l}}function _d(e){let r=W(e);return r.length===0?null:`--- Ripplo Lint ---
|
|
491
|
+
${q(r)}
|
|
492
|
+
${c("create")}`}function Od(e){let r=U(e);return r.length===0?null:`--- Unimplemented stubs ---
|
|
493
|
+
${r.join(", ")}
|
|
479
494
|
Implement the stub now with \`test("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.
|
|
480
|
-
${
|
|
481
|
-
No project config \u2014 \`ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${
|
|
482
|
-
Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${
|
|
495
|
+
${c("create")}`}async function Nd(e,r){let t=new Set(U(r).map(p=>$(p))),n=new Set(r.tests.map(p=>$(p.name))),o=(p,k)=>n.has(p)?t.has(p):Fd(k),i=w(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (not checked) ---
|
|
496
|
+
No project config \u2014 \`npx ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${c("setup")}`,runnableSlugs:[]};let a=await u({config:i,document:Ld,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(a==null)return{error:`--- Testing Scope (not checked) ---
|
|
497
|
+
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=a.project?.devSession?.scopeItems??[],l=s.flatMap(p=>{let k=p.workflow;return k==null?[` [intent] ${p.label??"(no label)"} \u2014 write a test for this flow`]:o(k.slug,k.spec)?[` [stub] ${k.slug} \u2014 implement \`${k.name}\``]:[]}),d=s.flatMap(p=>p.workflow!=null&&!o(p.workflow.slug,p.workflow.spec)?[p.workflow.slug]:[]);return{error:l.length===0?null:`--- Testing Scope ---
|
|
483
498
|
${l.join(`
|
|
484
499
|
`)}
|
|
485
|
-
${
|
|
500
|
+
${c("create")}`,runnableSlugs:d}}function Fd(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function Ud(e){let r=jd.argv[1];if(r==null)return{error:he("CLI entry missing (process.argv[1])"),infra:!1};let t=await _e(e);if(t!=null){let i=Fe(t);return Oe(t.serverUrl)?{error:he(i),infra:!1}:{error:mr({lines:[i],retried:!1}),infra:!0}}let n=await re({cliEntry:r,cwd:e});if(n.isErr())return{error:he(L(n.error)),infra:!1};let o=n.value;try{return await Md({connection:o,cwd:e})}finally{o.spawned&&await te(o),o.socket.destroy()}}async function Md({connection:e,cwd:r}){let t=await qo({connection:e,cwd:r,tests:[]});if(t.kind==="transport")return{error:he(t.message),infra:!1};let n=t.notRun.length>0?await qo({connection:e,cwd:r,tests:t.notRun.map(a=>a.testName)}):null,o=n==null||n.kind==="transport"?t.notRun:n.notRun,i=[...t.failedLines,...n!=null&&n.kind==="done"?n.failedLines:[]];return Hd({failedLines:i,notRun:o,retried:n!=null})}function Hd({failedLines:e,notRun:r,retried:t}){if(e.length===0&&r.length===0)return{error:null,infra:!1};let n=e.length>0?Wo(e):null,o=r.length>0?mr({lines:r.map(i=>i.line),retried:t}):null;return{error:[n,o].filter(i=>i!=null).join(`
|
|
486
501
|
|
|
487
|
-
`),infra:n==null}}async function
|
|
488
|
-
`),process.exit(1));let
|
|
489
|
-
`),process.exit(1)});function
|
|
490
|
-
`))}export{
|
|
502
|
+
`),infra:n==null}}async function qo({connection:e,cwd:r,tests:t}){let n=[],o=[],i=Id.join(r,".ripplo","debug"),a=new Ad;return(await Te({connection:e,request:{all:!1,headed:!1,tests:[...t]},token:a.token,onEvent:l=>{Wd({debugDir:i,event:l,failedLines:n,notRun:o})}})).match(l=>l.kind==="daemon-error"?{kind:"transport",message:Ue(l.error)}:{failedLines:n,kind:"done",notRun:o},l=>({kind:"transport",message:L(l)}))}function Wd({debugDir:e,event:r,failedLines:t,notRun:n}){if(r.kind!=="test-outcome"||r.outcome.kind==="pass")return;let o=Ne({debugDir:e,event:r})??r.testName;if(r.outcome.kind==="dispatch-error"||r.outcome.kind==="infra-error"){n.push({line:o,testName:r.testName});return}t.push(o)}import{z as Vo}from"zod";var qd=Vo.looseObject({skill:Vo.string()}),Go=f("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let r=qd.safeParse(e.tool_input);if(!r.success)return;let t=/^ripplo:(.+)$/.exec(r.data.skill);if(t==null)return;let n=t[1];n!=null&&m(e.cwd)&&ze(e.cwd,e.session_id,n)});var Bd=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,zo=f("UserPromptSubmit",e=>{m(e.cwd)&&[...e.prompt.matchAll(Bd)].map(r=>r[1]).filter(r=>r!=null).forEach(r=>{ze(e.cwd,e.session_id,r)})});ac();V(process.cwd());vr();var Jo={"exit-plan-gate":ao,"plan-reminder":lo,"post-edit-flag-stubs":vo,"post-edit-lint":Po,"pre-bash-hooks-pause-gate":$o,"pre-bash-run-gate":To,"pre-edit-ripplo-skill-gate":Do,"pre-edit-scope-gate":No,"pre-edit-watch-gate":Uo,"scope-reminder":Mo,"session-preamble":Ho,"stop-enforce":Bo,"track-skill-load":Go,"track-skill-prompt":zo};async function Kd(){Gd({pkg:{name:"ripplo",version:R()}}).notify({message:Pt()}),await zd(Jd(process.argv)).scriptName("ripplo").version(R()).command(Zd()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>Ot({value:e.value})).command("auth <subcommand>","Manage authentication",sc).command("projects <subcommand>","Inspect Ripplo projects",ic).command("hooks <subcommand>","Pause or resume Ripplo hooks",oc).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=>sn({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests locally via the daemon (auto-starts it if absent)",tc,e=>jn({all:e.all,headed:e.headed,ids:e.ids,keepAlive:e["keep-alive"]})).command(rc()).command(Xd()).command(ec()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>an()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>Hn()).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=>Dt({check:e.check})).command("update","Update ripplo to the latest published version (and hand the daemon off to it)",()=>{},()=>Qn()).command("doctor","Check project health",()=>{},()=>zt()).command("status","Report stub tests and daemon status",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>io({format:e.format})).command("scope <subcommand>","Manage testing scope",nc).command("run-worker",!1,()=>{},()=>Ln()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(Jo),demandOption:!0,type:"string"}),e=>Qd(e.name)).strict().help().parse()}async function Qd(e){let r=Jo[e];r==null&&(process.stderr.write(`Unknown hook: ${e}
|
|
503
|
+
`),process.exit(1));let t=await Yd(),n=t.trim()===""?{}:JSON.parse(t),o=await r.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function Yd(){return new Promise((e,r)=>{if(process.stdin.isTTY){e("");return}let t=[];process.stdin.on("data",n=>t.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(t).toString("utf8"))}),process.stdin.on("error",r)})}Kd().catch(e=>{process.stderr.write(`${Tr(e)}
|
|
504
|
+
`),process.exit(1)});function Xd(){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",{describe:"Run id where it surfaced",type:"string"}).option("test",{describe:"Test id that surfaced it",type:"string"}),handler:e=>In({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],testId:e.test,title:e.title})}}function Zd(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.option("explore",{default:!1,describe:"Opt in to background exploration when idle (experimental)",type:"boolean"}).option("exploreConcurrency",{default:2,describe:"Max concurrent background exploration trails (keeps the machine responsive)",type:"number"}),handler:e=>Yn({explore:e.explore,exploreConcurrency:e.exploreConcurrency})}}function ec(){return{command:"explore",describe:"Run state-space exploration trails in the foreground (app + engine must be running)",builder:e=>e.option("trails",{default:10,describe:"Number of trails to execute before stopping (stops early on saturation)",type:"number"}).option("headed",{default:!1,describe:"Watch trails in a visible browser (stop the daemon explorer first if it holds the machine lock)",type:"boolean"}).option("max-length",{describe:"Max firings per trail (default 12; lower = shorter, easier-to-watch trails)",type:"number"}).command("findings [findingId]","List pending exploration findings, or show one finding's full detail",r=>r.positional("findingId",{type:"string"}).option("json",{default:!1,describe:"Emit the findings list as JSON",type:"boolean"}),r=>Cn({findingId:r.findingId,json:r.json})).command("replay <findingId>","Replay a finding's minimal trail; a clean replay resolves the finding and covers its targets",r=>r.positional("findingId",{demandOption:!0,type:"string"}),r=>Rn({findingId:r.findingId})),handler:e=>Pn({headed:e.headed,maxLength:e["max-length"],trails:e.trails})}}function rc(){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=>Mn({at:e.at,offset:e.offset,runId:e.runId})}}function tc(e){let r=[];return e.positional("ids",{array:!0,default:r,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"}).option("keep-alive",{default:!1,describe:"Leave an auto-started daemon running after the batch",type:"boolean"})}function nc(e){return e.command("status","Print the current scope",r=>r.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),r=>ro({format:r.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",r=>{let t=[];return r.positional("test-ids",{array:!0,default:t,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},r=>to({testIds:r["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",r=>r.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),r=>no({id:r.id,testId:r["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",r=>{let t=[];return r.positional("ids",{array:!0,default:t,demandOption:!0,describe:"Scope item ids",type:"string"})},r=>oo({ids:r.ids})).demandCommand(1)}function oc(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>Jt()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>Kt()).demandCommand(1)}function ic(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>En()).demandCommand(1)}function sc(e){return e.command("login","Authenticate via device flow",()=>{},()=>At()).command("status","Show authentication status",()=>{},()=>Tt()).command("logout","Remove the saved token",()=>{},()=>{Lt()}).demandCommand(1)}function ac(){let e=process.cwd(),r=fr(e);r!=null&&r!==e&&(process.chdir(r),process.stderr.write(`ripplo: resolved .ripplo/ at ${r}
|
|
505
|
+
`))}export{Kd as main};
|