Skip to content

Commit 64fcfec

Browse files
authored
Merge branch 'main' into copilot/make-applycontext-type-available
2 parents 5b462b2 + 873ffa8 commit 64fcfec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3674
-390
lines changed

.changeset/add-internal-jsdocs.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Add JSDoc comments throughout the codebase, covering all major classes, functions, and interfaces in `src/server/`, `src/typescript-generator/`, `src/repl/`, and `src/util/`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Make the hexagon in the REPL prompt the same shade of blue as the logo (#0071b5).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Moved `openapi-example.yaml` from the repository root into `test/fixtures/openapi-example.yaml` and expanded it with many OpenAPI edge cases: CRUD operations on `/users` and `/users/{userId}`, polymorphic events via `oneOf`/`allOf`/`discriminator`, nullable fields, enum types, integer formats, file upload via `multipart/form-data`, cookie parameters, deprecated endpoints, multiple response content types, a no-body `204` health-check endpoint, and free-form `additionalProperties` objects.

.changeset/refine-readme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Replaced the five-minute walkthrough README with a concise, confident two-paragraph introduction. Added ten README variants under `docs/readme-variants/` for the team to review and choose from.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Rename the `.apply` REPL command to `.scenario`. Update all references in code, tests, and documentation.

README.md

Lines changed: 4 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -4,174 +4,19 @@
44

55
<br>
66

7-
**Your backend isn't ready. Your frontend can't wait.**
8-
9-
**Counterfact turns your OpenAPI spec into a live, stateful API you can program in TypeScript.**
10-
11-
<br>
12-
137
![MIT License](https://img.shields.io/badge/license-MIT-blue) [![TypeScript](./typescript-badge.png)](https://github.com/ellerbrock/typescript-badges/) [![Coverage Status](https://coveralls.io/repos/github/pmcelhaney/counterfact/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact)
148

159
</div>
1610

17-
This is a five-minute walkthrough. By the end, you’ll have a **stateful, type-safe, hot-reloading API simulator** running locally—and you’ll understand why it’s different from traditional mock servers.
18-
19-
Built for frontend developers, test engineers, and AI agents that need a predictable API to work against.
11+
You've used mock servers. You know where they stop being useful: static responses, no shared state, no way to inject a failure mid-run, no control without restarting. Counterfact picks up where they leave off.
2012

21-
22-
23-
## Minute 1 — Start the server
13+
Point it at an OpenAPI spec and it generates TypeScript handlers for every endpoint—type-safe, hot-reloading, sharing state across routes. A built-in REPL gives you a live control surface: seed data, trigger error conditions, proxy individual routes to a real backend, all on a running server. Whether you're a frontend developer waiting on a backend, a test engineer who needs clean reproducible state, or an AI agent that needs a stable API to work against, Counterfact is the simulator that doesn't plateau.
2414

2515
```sh
2616
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
2717
```
2818

29-
> **Requires Node ≥ 22.0.0**
30-
31-
That’s it.
32-
33-
Counterfact reads your spec, generates a TypeScript handler for every endpoint, and starts a server at `http://localhost:3100`.
34-
35-
Open `http://localhost:3100/counterfact/swagger/`.
36-
37-
Every endpoint is already live, returning random, schema-valid responses. No code written yet.
38-
39-
40-
41-
## Minute 2 — Make a route return real data
42-
43-
Open the generated file for `GET /pet/{petId}`:
44-
45-
```ts
46-
import type { HTTP_GET } from "../../types/paths/pet/{petId}.types.js";
47-
48-
export const GET: HTTP_GET = ($) => $.response[200].random();
49-
```
50-
51-
Replace `.random()` with your own logic:
52-
53-
```ts
54-
export const GET: HTTP_GET = ($) => {
55-
if ($.path.petId === 99) {
56-
return $.response[404].text("Pet not found");
57-
}
58-
return $.response[200].json({
59-
id: $.path.petId,
60-
name: "Fluffy",
61-
status: "available",
62-
photoUrls: []
63-
});
64-
};
65-
```
66-
67-
Save the file. The server reloads instantly—no restart, no lost state.
68-
69-
TypeScript enforces the contract. If your response doesn’t match the spec, you’ll know before you make the request.
70-
71-
## Minute 3 — Add state that survives across requests
72-
73-
Real APIs have memory. Yours should too.
74-
75-
Create `api/routes/_.context.ts`:
76-
77-
```ts
78-
import type { Pet } from "../types/components/pet.types.js";
79-
80-
export class Context {
81-
private pets = new Map<number, Pet>();
82-
private nextId = 1;
83-
84-
add(data: Omit<Pet, "id">): Pet {
85-
const pet = { ...data, id: this.nextId++ };
86-
this.pets.set(pet.id, pet);
87-
return pet;
88-
}
89-
90-
get(id: number): Pet | undefined { return this.pets.get(id); }
91-
list(): Pet[] { return [...this.pets.values()]; }
92-
remove(id: number): void { this.pets.delete(id); }
93-
}
94-
```
95-
96-
Use it in your routes:
97-
98-
```ts
99-
export const GET: HTTP_GET = ($) => $.response[200].json($.context.list());
100-
export const POST: HTTP_POST = ($) => $.response[200].json($.context.add($.body));
101-
```
102-
103-
Now your API behaves like a real system:
104-
- POST creates data
105-
- GET returns it
106-
- DELETE removes it
107-
108-
State survives hot reloads. Restarting resets everything—perfect for clean test runs.
109-
110-
111-
112-
## Minute 4 — Control the system at runtime (REPL)
113-
114-
This is where Counterfact becomes more than a mock.
115-
116-
The built-in REPL lets you inspect and control the system while it’s running.
117-
118-
Seed data:
119-
120-
```
121-
⬣> context.add({ name: "Fluffy", status: "available", photoUrls: [] })
122-
⬣> context.add({ name: "Rex", status: "pending", photoUrls: [] })
123-
```
124-
125-
Make requests:
126-
127-
```
128-
⬣> client.get("/pet/1")
129-
```
130-
131-
Simulate failures instantly:
132-
133-
```
134-
⬣> context.rateLimitExceeded = true
135-
⬣> client.get("/pet/1")
136-
{ status: 429, body: "Too Many Requests" }
137-
```
138-
139-
No HTTP scripts. No restarts. Just direct control.
140-
141-
142-
143-
## Minute 5 — Proxy to the real backend
144-
145-
When parts of your backend are ready, forward them through.
146-
147-
Everything else stays simulated.
148-
149-
```sh
150-
npx counterfact@latest openapi.yaml api --proxy-url https://api.example.com
151-
```
152-
153-
Toggle paths live:
154-
155-
```
156-
⬣> .proxy on /payments
157-
⬣> .proxy on /auth
158-
⬣> .proxy off
159-
```
160-
161-
162-
163-
## What you just built
164-
165-
In five minutes, you turned a static spec into a working system:
166-
167-
- **Schema-valid responses** from the moment it starts
168-
- **Type-safe handlers** generated from your spec
169-
- **Shared state** across all routes
170-
- **Hot reloading** without losing that state
171-
- A **live control surface (REPL)** for runtime behavior
172-
- **Selective proxying** to real services
173-
174-
19+
> Requires Node ≥ 22.0.0
17520
17621
## Go deeper
17722

@@ -189,4 +34,4 @@ In five minutes, you turned a static spec into a working system:
18934

19035
[Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md)
19136

192-
</div>
37+
</div>

docs/adr/001-apply-command-with-function-injection.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ADR 001: .apply Command Design — Minimalist Function Injection
1+
# ADR 001: .scenario Command Design — Minimalist Function Injection
22

33
## Status
44

@@ -8,7 +8,7 @@ Accepted
88

99
Counterfact's REPL lets developers interact with the running mock server from the terminal. A common need is to transition the server into a specific state (e.g. "all pets sold", "service unavailable") in a reproducible, shareable way. Today, operators must manually call REPL commands one by one; there is no mechanism to save and replay a named scenario.
1010

11-
The `.apply` command is proposed to address this: given a path argument, it loads and executes a user-authored script that mutates REPL context and routes, then reports what changed.
11+
The `.scenario` command is proposed to address this: given a path argument, it loads and executes a user-authored script that mutates REPL context and routes, then reports what changed.
1212

1313
Three designs were proposed as working documents in `.github/issue-proposals/`:
1414

@@ -27,7 +27,7 @@ Three designs were proposed as working documents in `.github/issue-proposals/`:
2727

2828
**Solution 1 (Minimalist Function Injection) is selected.**
2929

30-
An apply script is a TypeScript file with one or more named function exports. When `.apply <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `ApplyContext` (`$`) object:
30+
A scenario script is a TypeScript file with one or more named function exports. When `.scenario <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `ApplyContext` (`$`) object:
3131

3232
```ts
3333
// repl/sold-pets.ts
@@ -55,7 +55,7 @@ Scripts export named functions that receive `$: ApplyContext`. Counterfact resol
5555

5656
### Solution 2: Scenario Class with Lifecycle Hooks
5757

58-
Scripts export a named class that implements a `Scenario` interface with `setup()` and optional `teardown()` methods. Counterfact instantiates the class, calls `setup()`, and tracks applied instances in a map for later `.unapply`. A static `dependencies` array enables ordered composition.
58+
Scripts export a named class that implements a `Scenario` interface with `setup()` and optional `teardown()` methods. Counterfact instantiates the class, calls `setup()`, and tracks applied instances in a map for later `.unscenario`. A static `dependencies` array enables ordered composition.
5959

6060
**Why not chosen:** Class syntax and lifecycle coupling add complexity that is not justified until the need for teardown and dependency ordering is proven in practice. These concerns can be layered on top of Solution 1 once the basic command exists.
6161

@@ -81,7 +81,7 @@ Identical surface syntax to Solution 1, but Counterfact wraps `context` and `rou
8181

8282
### Risks and downsides
8383

84-
- Without lifecycle hooks, accumulated state across many `.apply` calls may be difficult to reason about.
84+
- Without lifecycle hooks, accumulated state across many `.scenario` calls may be difficult to reason about.
8585
- If teardown proves to be a common need, adding it later will require extending the API in a backward-compatible way.
8686
- Proxy-based auto-diffing (Solution 3) remains attractive for DX; deferring it means script authors will need to be disciplined about documenting context changes in the short term.
8787

docs/features/repl.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,21 @@ await req.send()
5656

5757
See the [Route Builder guide](./route-builder.md) for full documentation.
5858

59-
## Scenario scripts with `.apply`
59+
## Scenario scripts with `.scenario`
6060

61-
For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.apply`:
61+
For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.scenario`:
6262

6363
```
64-
⬣> .apply soldPets
64+
⬣> .scenario soldPets
6565
```
6666

67-
**Path resolution:** the argument to `.apply` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `<basePath>/scenarios/` (with `index.ts` as the default file).
67+
**Path resolution:** the argument to `.scenario` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `<basePath>/scenarios/` (with `index.ts` as the default file).
6868

6969
| Command | File | Function |
7070
|---|---|---|
71-
| `.apply foo` | `scenarios/index.ts` | `foo` |
72-
| `.apply foo/bar` | `scenarios/foo.ts` | `bar` |
73-
| `.apply foo/bar/baz` | `scenarios/foo/bar.ts` | `baz` |
71+
| `.scenario soldPets` | `scenarios/index.ts` | `soldPets` |
72+
| `.scenario pets/resetAll` | `scenarios/pets.ts` | `resetAll` |
73+
| `.scenario pets/orders/pending` | `scenarios/pets/orders.ts` | `pending` |
7474

7575
A scenario function receives a single argument with `{ context, loadContext, routes, route }`:
7676

docs/readme-variants/variant-1.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<div align="center" markdown="1">
2+
3+
<img src="../../counterfact.svg" alt="Counterfact" border=0>
4+
5+
<br>
6+
7+
![MIT License](https://img.shields.io/badge/license-MIT-blue) [![TypeScript](../../typescript-badge.png)](https://github.com/ellerbrock/typescript-badges/) [![Coverage Status](https://coveralls.io/repos/github/pmcelhaney/counterfact/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact)
8+
9+
</div>
10+
11+
Counterfact turns an OpenAPI spec into a live, stateful API server you can program in TypeScript. Point it at a spec and every endpoint is immediately live—returning schema-valid responses, sharing state across routes, and hot-reloading when you edit a handler.
12+
13+
That's where traditional mock servers stop. Counterfact keeps going. A built-in REPL lets you inspect and manipulate the running server: seed data, trigger failure modes, flip a proxy on or off—without restarting. It's a direct control surface for a running API. It works for frontend developers who can't wait on a backend, for test engineers who need reproducible states, and for AI coding agents that need a programmable, predictable sandbox to iterate in.
14+
15+
```sh
16+
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
17+
```
18+
19+
> Requires Node ≥ 22.0.0
20+
21+
## Go deeper
22+
23+
| | |
24+
|---|---|
25+
| [Getting started](../getting-started.md) | Detailed walkthrough with state, REPL, and proxy |
26+
| [Usage](../usage.md) | Feature index: routes, context, REPL, proxy, middleware, and more |
27+
| [Patterns](../patterns/index.md) | Failures, latency, AI sandboxes, integration tests |
28+
| [Reference](../reference.md) | `$` API, CLI flags, architecture |
29+
| [How it compares](../comparison.md) | json-server, WireMock, Prism, Microcks, MSW |
30+
| [FAQ](../faq.md) | State, types, regeneration |
31+
| [Petstore example](https://github.com/counterfact/example-petstore) | Full working example |
32+
33+
<div align="center" markdown="1">
34+
35+
[Changelog](../../CHANGELOG.md) · [Contributing](../../CONTRIBUTING.md)
36+
37+
</div>

docs/readme-variants/variant-10.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<div align="center" markdown="1">
2+
3+
<img src="../../counterfact.svg" alt="Counterfact" border=0>
4+
5+
<br>
6+
7+
![MIT License](https://img.shields.io/badge/license-MIT-blue) [![TypeScript](../../typescript-badge.png)](https://github.com/ellerbrock/typescript-badges/) [![Coverage Status](https://coveralls.io/repos/github/pmcelhaney/counterfact/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact)
8+
9+
</div>
10+
11+
The shift toward AI-assisted development changes what a mock server needs to be. Agents iterate fast, depend on state carrying over between requests, and need to simulate failures and edge cases on demand. Static mocks—fixed responses, no memory, no runtime control—aren't enough anymore.
12+
13+
Counterfact is a programmable API simulator built for this moment. It reads an OpenAPI spec and starts a stateful TypeScript server in one command. A built-in REPL lets you—or an agent—control the running system: seed data, trigger failures, proxy individual routes to a real backend. The handlers it generates are type-safe, hot-reloading, and share state across routes. It works equally well for human developers who can't wait on a backend and for AI agents that need a reliable sandbox to reason about.
14+
15+
```sh
16+
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
17+
```
18+
19+
> Requires Node ≥ 22.0.0
20+
21+
## Go deeper
22+
23+
| | |
24+
|---|---|
25+
| [Getting started](../getting-started.md) | Detailed walkthrough with state, REPL, and proxy |
26+
| [Usage](../usage.md) | Feature index: routes, context, REPL, proxy, middleware, and more |
27+
| [Patterns](../patterns/index.md) | Failures, latency, AI sandboxes, integration tests |
28+
| [Reference](../reference.md) | `$` API, CLI flags, architecture |
29+
| [How it compares](../comparison.md) | json-server, WireMock, Prism, Microcks, MSW |
30+
| [FAQ](../faq.md) | State, types, regeneration |
31+
| [Petstore example](https://github.com/counterfact/example-petstore) | Full working example |
32+
33+
<div align="center" markdown="1">
34+
35+
[Changelog](../../CHANGELOG.md) · [Contributing](../../CONTRIBUTING.md)
36+
37+
</div>

0 commit comments

Comments
 (0)