versioncam 0.5.2 → 0.7.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +95 -2
  3. package/dist/cli/commands/check.js +25 -0
  4. package/dist/cli/commands/check.js.map +1 -1
  5. package/dist/cli/commands/doctor.js +54 -1
  6. package/dist/cli/commands/doctor.js.map +1 -1
  7. package/dist/cli/commands/install.d.ts +40 -3
  8. package/dist/cli/commands/install.js +66 -11
  9. package/dist/cli/commands/install.js.map +1 -1
  10. package/dist/cli/commands/record.d.ts +6 -0
  11. package/dist/cli/commands/record.js +24 -3
  12. package/dist/cli/commands/record.js.map +1 -1
  13. package/dist/cli/commands/stability.js +10 -3
  14. package/dist/cli/commands/stability.js.map +1 -1
  15. package/dist/cli/usage.d.ts +1 -1
  16. package/dist/cli/usage.js +2 -0
  17. package/dist/cli/usage.js.map +1 -1
  18. package/dist/config.d.ts +37 -1
  19. package/dist/config.js +115 -3
  20. package/dist/config.js.map +1 -1
  21. package/dist/core/cursor.d.ts +22 -0
  22. package/dist/core/cursor.js +6 -0
  23. package/dist/core/cursor.js.map +1 -1
  24. package/dist/core/motion-defaults.d.ts +8 -0
  25. package/dist/core/motion-defaults.js +8 -0
  26. package/dist/core/motion-defaults.js.map +1 -1
  27. package/dist/core/timeline.d.ts +24 -0
  28. package/dist/core/timeline.js.map +1 -1
  29. package/dist/driver/clip.d.ts +6 -0
  30. package/dist/driver/clip.js +33 -2
  31. package/dist/driver/clip.js.map +1 -1
  32. package/dist/driver/file-window.d.ts +66 -0
  33. package/dist/driver/file-window.js +245 -0
  34. package/dist/driver/file-window.js.map +1 -0
  35. package/dist/driver/page-hooks.d.ts +23 -1
  36. package/dist/driver/page-hooks.js +36 -1
  37. package/dist/driver/page-hooks.js.map +1 -1
  38. package/dist/driver/replay.d.ts +263 -0
  39. package/dist/driver/replay.js +558 -0
  40. package/dist/driver/replay.js.map +1 -0
  41. package/dist/driver/session.d.ts +70 -4
  42. package/dist/driver/session.js +267 -29
  43. package/dist/driver/session.js.map +1 -1
  44. package/dist/driver/settle.js +6 -0
  45. package/dist/driver/settle.js.map +1 -1
  46. package/dist/index.d.ts +4 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/page/assets/index-CnRDAiUz.js +2 -0
  49. package/dist/page/index.html +1 -1
  50. package/dist/publish/protocol.d.ts +12 -1
  51. package/dist/publish/protocol.js.map +1 -1
  52. package/dsl.md +68 -1
  53. package/package.json +1 -1
  54. package/plugin/.claude-plugin/plugin.json +1 -1
  55. package/plugin/skills/versioncam/authoring.md +6 -1
  56. package/plugin/skills/versioncam/onboarding.md +9 -3
  57. package/dist/page/assets/index-BtUbUEdN.js +0 -2
@@ -0,0 +1,263 @@
1
+ import type { BrowserContext } from "playwright";
2
+ /**
3
+ * Record a clip's responses once, and serve them on every run after.
4
+ *
5
+ * Some of what a clip films is not the app's to decide: a map's tiles, a
6
+ * model's answer, a job's status, a count that grows. Each comes from a
7
+ * server, at the server's speed and in its words of the day, so two
8
+ * recordings of one clip differ wherever one of them reaches the page, and
9
+ * the clip can never be re-rendered the same. The fix every app reached for
10
+ * was a mock written by hand, which is right once and wrong the day the
11
+ * endpoint changes. A replay is the server's own answer, written down the
12
+ * first time the clip runs and kept beside the clip as an input: the
13
+ * requests the config names are answered from it, and a request it does not
14
+ * hold fails the clip, with its URL, instead of quietly going live.
15
+ *
16
+ * The file is a HAR (one per clip, `<dir>/<clip>.har`), so a browser's
17
+ * developer tools can open it. Text bodies are written into it, where a
18
+ * diff can read them; binary bodies (a tile, an image) go beside it, under
19
+ * `bodies/`, named by their hash, so clips that load the same tiles store
20
+ * them once.
21
+ */
22
+ export type ReplayMode = "auto" | "record" | "replay" | "live";
23
+ /** A glob, in Playwright's syntax, or a regular expression, over the whole URL. */
24
+ export type ReplayPattern = string | RegExp;
25
+ export type ReplayConfig = {
26
+ /** The requests to record and replay, by URL. */
27
+ record: ReplayPattern[];
28
+ /**
29
+ * `"auto"`, the default, replays a clip whose file exists and records one
30
+ * whose file does not. `"record"` always records, `"replay"` fails a clip
31
+ * without a file, and `"live"` leaves every request to the network.
32
+ */
33
+ mode?: ReplayMode;
34
+ /** Where the files go. Default `replay/` beside the config. Commit it. */
35
+ dir?: string;
36
+ /**
37
+ * Responses the page reads over time, an event stream or lines of JSON,
38
+ * delivered one event or one line every `chunkMs` of the page's clock, so
39
+ * the same piece lands on the same frame every run.
40
+ */
41
+ pace?: {
42
+ url: ReplayPattern;
43
+ chunkMs: number;
44
+ }[];
45
+ /** Query parameters left out of the file and of the match, by name. */
46
+ redact?: string[];
47
+ /**
48
+ * What a request the replay does not hold does: `"fail"`, the default,
49
+ * fails the clip and names it; `"live"` sends it to the network and warns.
50
+ */
51
+ onMissing?: "fail" | "live";
52
+ };
53
+ export type ResolvedReplay = {
54
+ record: RegExp[];
55
+ mode: ReplayMode;
56
+ dir: string;
57
+ pace: {
58
+ url: RegExp;
59
+ chunkMs: number;
60
+ }[];
61
+ redact: string[];
62
+ onMissing: "fail" | "live";
63
+ };
64
+ /** How old a replay may be before `check` says so. */
65
+ export declare const STALE_DAYS = 90;
66
+ /**
67
+ * A glob in Playwright's syntax as a regular expression, so the recorder and
68
+ * the script it puts in the page match a URL the same way: `**` crosses `/`,
69
+ * `*` does not, `{a,b}` is either, and every other character is itself, `?`
70
+ * included. Anchored at both ends, as Playwright anchors a glob.
71
+ */
72
+ export declare function globToRegExp(glob: string): RegExp;
73
+ export declare function patternOf(pattern: ReplayPattern): RegExp;
74
+ /**
75
+ * The URL a request is known by in the file: without its fragment and
76
+ * without the query parameters `redact` names, which are gone from the file
77
+ * and so cannot be part of the match.
78
+ */
79
+ export declare function urlKey(url: string, redact: readonly string[]): string;
80
+ type Header = {
81
+ name: string;
82
+ value: string;
83
+ };
84
+ type HarContent = {
85
+ size: number;
86
+ mimeType: string;
87
+ text?: string;
88
+ encoding?: "base64";
89
+ /** A binary body, under the replay's `bodies/`, by its SHA-256. */
90
+ _file?: string;
91
+ };
92
+ export type HarEntry = {
93
+ startedDateTime: string;
94
+ time: number;
95
+ request: {
96
+ method: string;
97
+ url: string;
98
+ httpVersion: string;
99
+ headers: Header[];
100
+ queryString: Header[];
101
+ cookies: [];
102
+ headersSize: -1;
103
+ bodySize: number;
104
+ postData?: {
105
+ mimeType: string;
106
+ text: string;
107
+ };
108
+ };
109
+ response: {
110
+ status: number;
111
+ statusText: string;
112
+ httpVersion: string;
113
+ headers: Header[];
114
+ cookies: [];
115
+ content: HarContent;
116
+ redirectURL: string;
117
+ headersSize: -1;
118
+ bodySize: number;
119
+ };
120
+ cache: Record<string, never>;
121
+ timings: {
122
+ send: number;
123
+ wait: number;
124
+ receive: number;
125
+ };
126
+ };
127
+ export type Har = {
128
+ log: {
129
+ version: "1.2";
130
+ creator: {
131
+ name: string;
132
+ version: string;
133
+ };
134
+ comment: string;
135
+ _versioncam: {
136
+ clip: string;
137
+ recorded: string;
138
+ recorder: string;
139
+ };
140
+ entries: HarEntry[];
141
+ };
142
+ };
143
+ export declare function sha256(bytes: Uint8Array): string;
144
+ /**
145
+ * One recorded exchange, as the file keeps it: the secrets and the redacted
146
+ * parameters gone, a text body in the entry, a binary one handed to `store`
147
+ * and named here by its hash.
148
+ */
149
+ export declare function entryOf(exchange: {
150
+ method: string;
151
+ url: string;
152
+ requestHeaders: Record<string, string>;
153
+ postData: string | null;
154
+ status: number;
155
+ statusText: string;
156
+ responseHeaders: Record<string, string>;
157
+ body: Uint8Array;
158
+ }, options: {
159
+ redact: readonly string[];
160
+ recorded: string;
161
+ store: (hash: string, bytes: Uint8Array) => void;
162
+ }): HarEntry;
163
+ /**
164
+ * The entries of a replay, by what a request is known by, each list in the
165
+ * order the requests were made, and served in that order: the third poll of
166
+ * a job's status gets the third answer the job gave, and every poll after
167
+ * the last gets the last. Among answers to one URL, one recorded for the
168
+ * same request body is taken first, for an app that posts several queries
169
+ * to one endpoint in an order that can vary.
170
+ */
171
+ export declare class ReplayBook {
172
+ private readonly byKey;
173
+ private readonly used;
174
+ private readonly redact;
175
+ constructor(entries: readonly HarEntry[], redact: readonly string[]);
176
+ answer(method: string, url: string, postData: string | null): HarEntry | null;
177
+ }
178
+ /** The file a clip's replay is in. */
179
+ export declare function replayFile(dir: string, clipId: string): string;
180
+ export declare function readReplay(file: string): Har;
181
+ /** What `doctor` and `check` say about a clip's replay. */
182
+ export declare function describeReplay(dir: string, clipId: string, now?: Date): {
183
+ file: string;
184
+ exists: false;
185
+ } | {
186
+ file: string;
187
+ exists: true;
188
+ requests: number;
189
+ recorded: string;
190
+ ageDays: number;
191
+ recorder: string;
192
+ };
193
+ /** Which mode a clip runs in, once the file's presence is known. */
194
+ export declare function effectiveMode(mode: ReplayMode, fileExists: boolean): "record" | "replay" | "live";
195
+ /**
196
+ * A clip's replay while it runs: the routes it installs, what it recorded,
197
+ * what it could not answer, and how many answers are still on their way.
198
+ */
199
+ export declare class ClipReplay {
200
+ readonly mode: "record" | "replay" | "live";
201
+ readonly file: string;
202
+ /** Requests the replay matched and did not hold, as "METHOD url". */
203
+ readonly misses: string[];
204
+ /** Requests sent live because the replay did not hold them (`onMissing: "live"`). */
205
+ readonly wentLive: string[];
206
+ private readonly recorded;
207
+ private readonly bodies;
208
+ private readonly book;
209
+ private readonly when;
210
+ private inFlight;
211
+ private waiters;
212
+ private readonly config;
213
+ private readonly clipId;
214
+ private readonly recorder;
215
+ constructor(config: ResolvedReplay, clipId: string, recorder: string, override?: ReplayMode);
216
+ /** The page-side script that paces streamed bodies, or null when nothing is paced. */
217
+ pacingScript(): string | null;
218
+ /** Install the routes. Before the page is opened, so its first request is caught. */
219
+ install(context: BrowserContext): Promise<void>;
220
+ /**
221
+ * Resolves once every request the replay has taken is answered and its body
222
+ * is in the page. The driver waits on it after each tick, so a request made
223
+ * in a frame is answered within that frame on every run, however long the
224
+ * network or the disk took: a tile arrives on the frame it was asked for.
225
+ */
226
+ idle(timeoutMs?: number): Promise<void>;
227
+ private settled;
228
+ private handle;
229
+ private record;
230
+ private serve;
231
+ /**
232
+ * What stops the clip: a request the replay was asked for and did not
233
+ * hold. Null when there is none.
234
+ */
235
+ failure(): string | null;
236
+ /**
237
+ * Write what was recorded. Only for a clip that passed: a replay of half a
238
+ * clip would serve a later run a different app than the one it shows.
239
+ */
240
+ write(): void;
241
+ }
242
+ /**
243
+ * Deliver a paced response a piece at a time on the page's own clock.
244
+ *
245
+ * The recorder cannot stream a response into the page: Playwright fulfils a
246
+ * request whole. So the page does it. For a URL the config paces, `fetch`
247
+ * hands back a response whose body is a stream that gives out one event of
248
+ * an event stream, or one line of anything else, every `chunkMs`, on the
249
+ * page's `setTimeout`, which the recorder's clock advances a frame at a
250
+ * time; and `EventSource` dispatches one event every `chunkMs` the same
251
+ * way. The same piece therefore lands on the same frame every run, at any
252
+ * frame rate. `window.__recorderReplay.pending` counts the bodies still
253
+ * arriving, and a settle waits for it to reach none.
254
+ *
255
+ * An `EventSource` whose stream has ended reports an error and stays
256
+ * closed, instead of reconnecting as a live one would: a replay would only
257
+ * answer it with the same stream again.
258
+ */
259
+ export declare function pacedReplayScript(pace: readonly {
260
+ url: RegExp;
261
+ chunkMs: number;
262
+ }[]): string;
263
+ export {};