ripplo 0.5.11 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -53
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,82 +1,75 @@
|
|
|
1
1
|
# ripplo
|
|
2
2
|
|
|
3
|
-
CLI for [Ripplo](https://ripplo.ai)
|
|
3
|
+
CLI for [Ripplo](https://ripplo.ai). Typed end-to-end tests with real backend state.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Run via `npx ripplo <command>` so you always pick up the latest version. No global install needed.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npm install -g ripplo
|
|
9
|
-
# or: npx ripplo <subcommand>
|
|
10
|
-
```
|
|
7
|
+
## Recommended setup: Claude Code plugin
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
If you use Claude Code, this is the shortest path. The plugin handles auth, scaffolding, and adapter wiring, and adds hooks that keep tests in sync with code changes.
|
|
13
10
|
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add ripplo/claude-plugin
|
|
13
|
+
/plugin install ripplo
|
|
14
|
+
/ripplo:setup
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
That's it. Skip the rest of this README unless you want to know what `/ripplo:setup` is doing under the hood, or you're not on Claude Code.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## Manual setup
|
|
20
|
+
|
|
21
|
+
Three steps: authenticate, scaffold, mount the adapter in your app server.
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
npx ripplo
|
|
24
|
+
npx ripplo auth login # device-code flow, opens a browser
|
|
25
|
+
npx ripplo init # scaffolds .ripplo/ and writes RIPPLO_* env vars
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
`ripplo init` is interactive. It creates `.ripplo/{index.ts, project.json, preconditions/, observers/, tests/}`, appends `RIPPLO_APP_URL`, `RIPPLO_ENGINE_URL`, and `RIPPLO_WEBHOOK_SECRET` to your env file, and installs `@ripplo/testing`.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Then mount the adapter in your app server. Next.js App Router:
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
```ts
|
|
33
|
+
// app/ripplo/[action]/route.ts
|
|
34
|
+
import { createNextHandler } from "@ripplo/testing/nextjs";
|
|
35
|
+
import { engine } from "@/server/test/engine";
|
|
36
|
+
|
|
37
|
+
export const PUT = createNextHandler({
|
|
38
|
+
enabled: process.env.ENABLE_RIPPLO_TESTING === "true",
|
|
39
|
+
engine,
|
|
40
|
+
});
|
|
41
|
+
```
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
For Express, Fastify, Hono, Koa, NestJS, or Elysia, see [`@ripplo/testing`](https://www.npmjs.com/package/@ripplo/testing).
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
`engine` is what you write in `src/test/engine.ts` to provide the implementations for the preconditions and observers you defined in `.ripplo/`. The DSL reference covers it.
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
- **Dev server port + URL vars.** Two sibling worktrees can't both run `pnpm dev` on the same port. Pick a distinct port for this worktree, **and update both `RIPPLO_APP_URL` and `RIPPLO_ENGINE_URL`** in this worktree's env file to match — e.g. main on `:3000` → this worktree on `:3001` → set `RIPPLO_APP_URL=http://localhost:3001` and `RIPPLO_ENGINE_URL=http://localhost:3001/ripplo`. If the URL vars stay pointing at main's port, `ripplo watch` talks to the wrong dev server.
|
|
47
|
+
## Running tests
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
In one terminal, start the local executor:
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
| `ripplo auth status` | Show current authentication |
|
|
46
|
-
| `ripplo auth logout` | Remove the saved token |
|
|
47
|
-
| `ripplo init` | Scaffold `.ripplo/` and write `RIPPLO_*` env vars |
|
|
48
|
-
| `ripplo watch` | Subscribe to run requests; execute tests against your dev server |
|
|
49
|
-
| `ripplo run [ids..]` | Run tests in parallel (defaults to scope + dirty tests; `--all` for full suite) |
|
|
50
|
-
| `ripplo lint [ids..]` | Compile and lint; writes `.ripplo/ripplo.lock` |
|
|
51
|
-
| `ripplo compile [--check]` | Rebuild the lockfile (`--check` = exit non-zero if stale; for CI/hooks) |
|
|
52
|
-
| `ripplo sync` | Push the compiled `.ripplo/` resources to the server (no run; useful for debugging sync state) |
|
|
53
|
-
| `ripplo doctor` | Check lockfile freshness, auth, env |
|
|
54
|
-
| `ripplo status` | Report unimplemented tests and preconditions |
|
|
55
|
-
| `ripplo cover` | Audit coverage statements across the whole tree |
|
|
56
|
-
| `ripplo scope <sub>` | Manage testing scope (`add`, `link`, `remove`, `status`) |
|
|
57
|
-
| `ripplo flake-detect <id>` | Run a test N times in parallel to detect flakiness |
|
|
51
|
+
```sh
|
|
52
|
+
npx ripplo watch
|
|
53
|
+
```
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
Leave it running alongside your dev server. It subscribes to run requests and drives Playwright against your local app.
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
In another, run tests:
|
|
62
58
|
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
├── ripplo.lock # generated + committed; read by the Ripplo server on push
|
|
68
|
-
├── preconditions/index.ts # handles + `preconditions` registry
|
|
69
|
-
├── observers/index.ts # handles + `observers` registry
|
|
70
|
-
└── tests/
|
|
71
|
-
└── <test-id>.ts # one TestDefinition per file
|
|
59
|
+
```sh
|
|
60
|
+
npx ripplo run # scope + dirty tests
|
|
61
|
+
npx ripplo run --all # everything
|
|
62
|
+
npx ripplo run my-test # by id
|
|
72
63
|
```
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
Commit `.ripplo/ripplo.lock`. The Ripplo server reads it on every push webhook; `ripplo compile --check` in a pre-commit hook keeps it fresh.
|
|
66
|
+
|
|
67
|
+
## Worktrees
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
Ripplo is built for parallel feature work via `git worktree`. Each worktree gets its own dev session, scope, debug artifacts, and lockfile checkout — your auth token, projectId, and webhook secret are shared globally, so a fresh worktree needs no re-auth or re-init. Spin up as many as you want and run them concurrently.
|
|
77
70
|
|
|
78
|
-
|
|
71
|
+
Two things to know: env files are usually gitignored, so copy your `.env.local` into the new worktree (or point `envFiles` in `.ripplo/project.json` at a shared file outside the tree), and pick a distinct dev-server port — update both `RIPPLO_APP_URL` and `RIPPLO_ENGINE_URL` to match. `ripplo doctor` flags both.
|
|
79
72
|
|
|
80
|
-
|
|
73
|
+
## Other commands
|
|
81
74
|
|
|
82
|
-
|
|
75
|
+
`npx ripplo --help` lists everything. The ones you'll use beyond setup: `lint` (compile + typecheck), `doctor` (auth/env/lockfile health), `scope` (manage what the current session is testing), `flake-detect` (run a test in parallel N times).
|
package/dist/index.js
CHANGED
|
@@ -798,7 +798,7 @@ ${w("create")}`}function ub(e){let t=pb(["run"],e);if(t.code===0)return null;let
|
|
|
798
798
|
`).filter(n=>/FAILED/.test(n)).join(`
|
|
799
799
|
`);return r.length===0?null:`--- Ripplo Run Failures (scope) ---
|
|
800
800
|
${r}
|
|
801
|
-
Artifacts: .ripplo/debug/<runId>/. ${w("debug")}`}var db=wr.object({status:wr.number().nullish(),stderr:wr.unknown().optional(),stdout:wr.unknown().optional()});function Ra(e){return e instanceof Buffer?e.toString("utf8"):typeof e=="string"?e:""}function pb(e,t){let r=Pa.argv[1];if(r==null)return{code:1,output:""};try{return{code:0,output:eb(Pa.execPath,[r,...e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}}catch(n){let o=db.safeParse(n);if(!o.success)return{code:1,output:""};let i=`${Ra(o.data.stdout)}${Ra(o.data.stderr)}`;return{code:o.data.status??1,output:i}}}import{z as Ea}from"zod";var mb=Ea.looseObject({skill:Ea.string()}),Ca=I("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=mb.safeParse(e.tool_input);if(!t.success)return;let r=/^ripplo:(.+)$/.exec(t.data.skill);if(r==null)return;let n=r[1];n!=null&&A(e.cwd)&&yr(e.cwd,e.session_id,n)});var fb=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,Aa=I("UserPromptSubmit",e=>{A(e.cwd)&&[...e.prompt.matchAll(fb)].map(t=>t[1]).filter(t=>t!=null).forEach(t=>{yr(e.cwd,e.session_id,t)})});Rb();Yt(process.cwd());Ur();var Ia={"coverage-nudge":Zs,"exit-plan-gate":ta,"plan-reminder":ra,"post-edit-flag-stubs":ia,"post-edit-lint":aa,"pre-bash-hooks-pause-gate":ca,"pre-bash-run-gate":ma,"pre-edit-ripplo-skill-gate":ga,"pre-edit-scope-gate":wa,"pre-edit-watch-gate":va,"scope-reminder":Sa,"session-preamble":ka,"stop-enforce":xa,"track-skill-load":Ca,"track-skill-prompt":Aa};async function hb(){await gb(yb(process.argv)).scriptName("ripplo").version("0.
|
|
801
|
+
Artifacts: .ripplo/debug/<runId>/. ${w("debug")}`}var db=wr.object({status:wr.number().nullish(),stderr:wr.unknown().optional(),stdout:wr.unknown().optional()});function Ra(e){return e instanceof Buffer?e.toString("utf8"):typeof e=="string"?e:""}function pb(e,t){let r=Pa.argv[1];if(r==null)return{code:1,output:""};try{return{code:0,output:eb(Pa.execPath,[r,...e],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}}catch(n){let o=db.safeParse(n);if(!o.success)return{code:1,output:""};let i=`${Ra(o.data.stdout)}${Ra(o.data.stderr)}`;return{code:o.data.status??1,output:i}}}import{z as Ea}from"zod";var mb=Ea.looseObject({skill:Ea.string()}),Ca=I("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=mb.safeParse(e.tool_input);if(!t.success)return;let r=/^ripplo:(.+)$/.exec(t.data.skill);if(r==null)return;let n=r[1];n!=null&&A(e.cwd)&&yr(e.cwd,e.session_id,n)});var fb=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,Aa=I("UserPromptSubmit",e=>{A(e.cwd)&&[...e.prompt.matchAll(fb)].map(t=>t[1]).filter(t=>t!=null).forEach(t=>{yr(e.cwd,e.session_id,t)})});Rb();Yt(process.cwd());Ur();var Ia={"coverage-nudge":Zs,"exit-plan-gate":ta,"plan-reminder":ra,"post-edit-flag-stubs":ia,"post-edit-lint":aa,"pre-bash-hooks-pause-gate":ca,"pre-bash-run-gate":ma,"pre-edit-ripplo-skill-gate":ga,"pre-edit-scope-gate":wa,"pre-edit-watch-gate":va,"scope-reminder":Sa,"session-preamble":ka,"stop-enforce":xa,"track-skill-load":Ca,"track-skill-prompt":Aa};async function hb(){await gb(yb(process.argv)).scriptName("ripplo").version("0.6.0").command("watch","Watch for run requests and execute locally",()=>{},()=>Bs()).command("auth <subcommand>","Manage authentication",Pb).command("projects <subcommand>","Inspect Ripplo projects",kb).command("hooks <subcommand>","Pause or resume Ripplo hooks",Sb).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=>Ts({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests (defaults to scope; auto-adds dirty tests)",e=>{let t=[];return e.positional("ids",{array:!0,default:t,describe:"Test ids to run (defaults to dev-session scope)",type:"string"}).option("all",{default:!1,describe:"Run every test in the dev session (expensive)",type:"boolean"})},e=>_s({all:e.all,ids:e.ids})).command("lint [ids..]","Compile and lint tests (all or specific ids)",e=>{let t=[];return e.positional("ids",{array:!0,default:t,describe:"Test ids to lint (all if omitted)",type:"string"}).option("require-implemented",{array:!0,default:t,describe:"Test ids that must not be .notImplemented() \u2014 fails if any still are",type:"string"})},e=>Os({ids:e.ids,requireImplemented:e["require-implemented"]})).command("flake-detect <id>","Run N times to detect flakiness",e=>e.positional("id",{demandOption:!0,type:"string"}).option("runs",{default:10,type:"number"}),e=>ks({id:e.id,runs:e.runs})).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>Us()).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=>Ji({check:e.check})).command("cover","Audit all coverage statements",()=>{},()=>es()).command("doctor","Check project health",()=>{},()=>gs()).command("status","Report unimplemented tests and preconditions",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>Xs({format:e.format})).command("scope <subcommand>","Manage testing scope",vb).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(Ia),demandOption:!0,type:"string"}),e=>wb(e.name)).strict().help().parse()}async function wb(e){let t=Ia[e];t==null&&(process.stderr.write(`Unknown hook: ${e}
|
|
802
802
|
`),process.exit(1));let r=await bb(),n=r.trim()===""?{}:JSON.parse(r),o=await t.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function bb(){return new Promise((e,t)=>{if(process.stdin.isTTY){e("");return}let r=[];process.stdin.on("data",n=>r.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(r).toString("utf8"))}),process.stdin.on("error",t)})}hb().catch(e=>{process.stderr.write(`${e instanceof Error?e.message:String(e)}
|
|
803
803
|
`),process.exit(1)});function vb(e){return e.command("status","Print the current scope",t=>t.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),t=>zs({format:t.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",t=>{let r=[];return t.positional("test-ids",{array:!0,default:r,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},t=>Gs({testIds:t["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",t=>t.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),t=>Ks({id:t.id,testId:t["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",t=>{let r=[];return t.positional("ids",{array:!0,default:r,demandOption:!0,describe:"Scope item ids",type:"string"})},t=>Js({ids:t.ids})).demandCommand(1)}function Sb(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>Ps()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>Rs()).demandCommand(1)}function kb(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>Ls()).demandCommand(1)}function Pb(e){return e.command("login","Authenticate via device flow",()=>{},()=>pi()).command("status","Show authentication status",()=>{},()=>mi()).command("logout","Remove the saved token",()=>{},()=>{fi()}).demandCommand(1)}function Rb(){let e=process.cwd(),t=Pr(e);t!=null&&t!==e&&(process.chdir(t),process.stderr.write(`ripplo: resolved .ripplo/ at ${t}
|
|
804
804
|
`))}export{hb as main};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripplo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI for Ripplo — AI-powered end-to-end testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://ripplo.ai",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"eslint": "catalog:",
|
|
50
50
|
"tsup": "^8.5.1",
|
|
51
51
|
"@ripplo/eslint-config": "0.0.0",
|
|
52
|
-
"@ripplo/graphql": "^0.0.0",
|
|
53
52
|
"@ripplo/runtime": "^0.0.0",
|
|
53
|
+
"@ripplo/testing": "^0.6.0",
|
|
54
54
|
"@ripplo/spec": "^0.0.0",
|
|
55
|
-
"@ripplo/
|
|
55
|
+
"@ripplo/graphql": "^0.0.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"dev": "tsx watch src/index.ts",
|