harnery 0.37.0 → 0.38.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 (184) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +7 -2
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +61 -1
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +48 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +349 -21
  17. package/dist/commands/fetch.js +1 -0
  18. package/dist/commands/qa-record.d.ts +144 -0
  19. package/dist/commands/qa-record.d.ts.map +1 -0
  20. package/dist/commands/qa-record.js +0 -0
  21. package/dist/commands/qa-run.d.ts +7 -3
  22. package/dist/commands/qa-run.d.ts.map +1 -1
  23. package/dist/commands/qa-run.js +268 -13
  24. package/dist/commands/qa-status.d.ts +71 -0
  25. package/dist/commands/qa-status.d.ts.map +1 -0
  26. package/dist/commands/qa-status.js +490 -0
  27. package/dist/commands/qa-verify.d.ts +40 -0
  28. package/dist/commands/qa-verify.d.ts.map +1 -0
  29. package/dist/commands/qa-verify.js +180 -0
  30. package/dist/commands/review-pack.d.ts +4 -0
  31. package/dist/commands/review-pack.d.ts.map +1 -0
  32. package/dist/commands/review-pack.js +1001 -0
  33. package/dist/core/agents/qa-signal.d.ts +111 -0
  34. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  35. package/dist/core/agents/qa-signal.js +231 -0
  36. package/dist/core/agents/session-name-display.d.ts +20 -5
  37. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  38. package/dist/core/agents/session-name-display.js +67 -7
  39. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  40. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  41. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  42. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  43. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  44. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  45. package/dist/core/agents/state/live-coordination-view.js +1 -0
  46. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  47. package/dist/core/artifacts/constants.d.ts +1 -1
  48. package/dist/core/artifacts/constants.js +1 -1
  49. package/dist/core/artifacts/index.d.ts +57 -6
  50. package/dist/core/artifacts/index.d.ts.map +1 -1
  51. package/dist/core/artifacts/index.js +265 -10
  52. package/dist/core/config.d.ts +8 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +16 -0
  55. package/dist/core/diagnostics/bundle.d.ts +16 -0
  56. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  57. package/dist/core/diagnostics/bundle.js +101 -7
  58. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  59. package/dist/core/events/v3/bootstrap.js +10 -0
  60. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  61. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  62. package/dist/core/events/v3/coordination-view.js +69 -8
  63. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  64. package/dist/core/events/v3/producers/intake.js +9 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts +23 -0
  66. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  67. package/dist/core/events/v3/producers/recorder.js +264 -18
  68. package/dist/core/hooks/cli.js +120 -27
  69. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  70. package/dist/core/hooks/resolve/transcript.js +10 -3
  71. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  72. package/dist/core/hooks/session-name-presence.js +4 -1
  73. package/dist/core/qa-artifacts.d.ts +20 -0
  74. package/dist/core/qa-artifacts.d.ts.map +1 -0
  75. package/dist/core/qa-artifacts.js +110 -0
  76. package/dist/core/resources/contract.d.ts +6 -0
  77. package/dist/core/resources/contract.d.ts.map +1 -1
  78. package/dist/core/resources/sampler.d.ts +7 -0
  79. package/dist/core/resources/sampler.d.ts.map +1 -1
  80. package/dist/core/resources/sampler.js +106 -5
  81. package/dist/lib/admission.d.ts +71 -0
  82. package/dist/lib/admission.d.ts.map +1 -0
  83. package/dist/lib/admission.js +264 -0
  84. package/dist/lib/agent-browser/client.d.ts +1 -1
  85. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  86. package/dist/lib/agent-browser/client.js +1 -5
  87. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  88. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  89. package/dist/lib/browser/capture-fidelity.js +84 -0
  90. package/dist/lib/browser/client.d.ts +41 -1
  91. package/dist/lib/browser/client.d.ts.map +1 -1
  92. package/dist/lib/browser/client.js +167 -9
  93. package/dist/lib/browser/critique.d.ts +38 -1
  94. package/dist/lib/browser/critique.d.ts.map +1 -1
  95. package/dist/lib/browser/critique.js +34 -6
  96. package/dist/lib/browser/index.d.ts +4 -2
  97. package/dist/lib/browser/index.d.ts.map +1 -1
  98. package/dist/lib/browser/index.js +2 -0
  99. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  100. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  101. package/dist/lib/browser/page-review-judge.js +270 -0
  102. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  103. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  104. package/dist/lib/browser/page-review-pack.js +1751 -0
  105. package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
  106. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  107. package/dist/lib/browser/qa-run-contracts.js +136 -1
  108. package/dist/lib/browser/qa-run.d.ts +100 -10
  109. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  110. package/dist/lib/browser/qa-run.js +768 -169
  111. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  112. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  113. package/dist/lib/browser/request-diagnostics.js +18 -0
  114. package/dist/lib/browser/tiling.d.ts +19 -0
  115. package/dist/lib/browser/tiling.d.ts.map +1 -1
  116. package/dist/lib/browser/tiling.js +28 -0
  117. package/dist/lib/cookies/client.d.ts +9 -0
  118. package/dist/lib/cookies/client.d.ts.map +1 -1
  119. package/dist/lib/cookies/client.js +197 -44
  120. package/dist/lib/cookies/extra.d.ts +18 -0
  121. package/dist/lib/cookies/extra.d.ts.map +1 -0
  122. package/dist/lib/cookies/extra.js +14 -0
  123. package/dist/lib/cookies/index.d.ts +2 -1
  124. package/dist/lib/cookies/index.d.ts.map +1 -1
  125. package/dist/lib/cookies/index.js +2 -1
  126. package/dist/lib/durable-job.d.ts +124 -0
  127. package/dist/lib/durable-job.d.ts.map +1 -0
  128. package/dist/lib/durable-job.js +296 -0
  129. package/dist/lib/http/client.d.ts +7 -1
  130. package/dist/lib/http/client.d.ts.map +1 -1
  131. package/dist/lib/http/client.js +2 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +5 -2
  134. package/package.json +8 -2
  135. package/src/commander.ts +50 -2
  136. package/src/commands/admission.ts +699 -0
  137. package/src/commands/agents.ts +87 -1
  138. package/src/commands/artifacts.ts +97 -21
  139. package/src/commands/browse-ai.ts +10 -5
  140. package/src/commands/browse.ts +481 -20
  141. package/src/commands/fetch.ts +1 -0
  142. package/src/commands/qa-record.ts +682 -0
  143. package/src/commands/qa-run.ts +335 -16
  144. package/src/commands/qa-status.ts +608 -0
  145. package/src/commands/qa-verify.ts +238 -0
  146. package/src/commands/review-pack.ts +1281 -0
  147. package/src/core/agents/qa-signal.ts +261 -0
  148. package/src/core/agents/session-name-display.ts +78 -7
  149. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  150. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  151. package/src/core/agents/state/live-coordination-view.ts +1 -0
  152. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  153. package/src/core/artifacts/constants.ts +1 -1
  154. package/src/core/artifacts/index.ts +370 -21
  155. package/src/core/config.ts +23 -0
  156. package/src/core/diagnostics/bundle.ts +119 -11
  157. package/src/core/events/v3/bootstrap.ts +10 -0
  158. package/src/core/events/v3/coordination-view.ts +100 -11
  159. package/src/core/events/v3/producers/intake.ts +9 -2
  160. package/src/core/events/v3/producers/recorder.ts +312 -18
  161. package/src/core/hooks/cli.ts +140 -32
  162. package/src/core/hooks/resolve/transcript.ts +10 -3
  163. package/src/core/hooks/session-name-presence.ts +6 -1
  164. package/src/core/qa-artifacts.ts +126 -0
  165. package/src/core/resources/contract.ts +7 -0
  166. package/src/core/resources/sampler.ts +138 -6
  167. package/src/lib/admission.ts +347 -0
  168. package/src/lib/agent-browser/client.ts +2 -10
  169. package/src/lib/browser/capture-fidelity.ts +98 -0
  170. package/src/lib/browser/client.ts +206 -10
  171. package/src/lib/browser/critique.ts +62 -7
  172. package/src/lib/browser/index.ts +36 -0
  173. package/src/lib/browser/page-review-judge.ts +360 -0
  174. package/src/lib/browser/page-review-pack.ts +2384 -0
  175. package/src/lib/browser/qa-run-contracts.ts +366 -3
  176. package/src/lib/browser/qa-run.ts +868 -190
  177. package/src/lib/browser/request-diagnostics.ts +27 -0
  178. package/src/lib/browser/tiling.ts +32 -0
  179. package/src/lib/cookies/client.ts +228 -42
  180. package/src/lib/cookies/extra.ts +28 -0
  181. package/src/lib/cookies/index.ts +2 -0
  182. package/src/lib/durable-job.ts +407 -0
  183. package/src/lib/http/client.ts +13 -1
  184. package/src/lib/instructions/templates.ts +5 -2
@@ -0,0 +1,27 @@
1
+ export interface FailedRequestCandidate {
2
+ url: string;
3
+ method: string;
4
+ failure: string;
5
+ resourceType: string;
6
+ }
7
+
8
+ /**
9
+ * Next.js may start a speculative React Server Component fetch and cancel it
10
+ * once the destination is no longer needed. Chromium reports that deliberate
11
+ * cancellation as net::ERR_ABORTED even though the rendered page is healthy.
12
+ */
13
+ export function isBenignNextRscAbort(request: FailedRequestCandidate): boolean {
14
+ if (
15
+ request.failure !== "net::ERR_ABORTED" ||
16
+ request.method !== "GET" ||
17
+ request.resourceType !== "fetch"
18
+ ) {
19
+ return false;
20
+ }
21
+
22
+ try {
23
+ return new URL(request.url).searchParams.has("_rsc");
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
@@ -162,3 +162,35 @@ export function bandOversizedRect(
162
162
  height: r.height,
163
163
  }));
164
164
  }
165
+
166
+ /**
167
+ * Indices of bands past the kept prefix (`allBands[0..keptCount)`) that
168
+ * intersect any of `rects`. A tile cap keeps the top of a tall page and drops
169
+ * the rest; a deterministic gate that already found a defect below the cap
170
+ * still deserves a tile, so the caller cuts these bands as well and labels
171
+ * them as hit bands. Pure: the result is deduplicated, ascending, and holds
172
+ * at most `maxExtra` entries. Rects and bands share one pixel space.
173
+ */
174
+ export function bandsCoveringRects(
175
+ allBands: ReadonlyArray<{ x: number; y: number; width: number; height: number }>,
176
+ keptCount: number,
177
+ rects: ReadonlyArray<{ x: number; y: number; width: number; height: number }>,
178
+ maxExtra = 50,
179
+ ): number[] {
180
+ const out: number[] = [];
181
+ if (rects.length === 0 || maxExtra <= 0) return out;
182
+ for (let index = Math.max(0, keptCount); index < allBands.length; index++) {
183
+ if (out.length >= maxExtra) break;
184
+ const band = allBands[index];
185
+ if (!band) continue;
186
+ const hit = rects.some((rect) => {
187
+ const x1 = rect.x + Math.max(rect.width, 1);
188
+ const y1 = rect.y + Math.max(rect.height, 1);
189
+ return (
190
+ band.x < x1 && band.x + band.width > rect.x && band.y < y1 && band.y + band.height > rect.y
191
+ );
192
+ });
193
+ if (hit) out.push(index);
194
+ }
195
+ return out;
196
+ }
@@ -1,5 +1,16 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { basename, dirname, join } from "node:path";
3
14
 
4
15
  /**
5
16
  * Shared cookie-store library.
@@ -58,6 +69,160 @@ export interface InfoResult {
58
69
  exportedFrom?: string;
59
70
  }
60
71
 
72
+ export class CookieStoreParseError extends Error {
73
+ readonly path: string;
74
+
75
+ constructor(path: string, cause: unknown) {
76
+ const detail = cause instanceof Error ? cause.message : String(cause);
77
+ super(
78
+ `Cookie-store parse failed at "${path}" after a stable read: ${detail}. ` +
79
+ "The original cookie-store file was left unchanged.",
80
+ { cause },
81
+ );
82
+ this.name = "CookieStoreParseError";
83
+ this.path = path;
84
+ }
85
+ }
86
+
87
+ const LOCK_WAIT_MS = 10_000;
88
+ const LOCK_POLL_MS = 10;
89
+ const STABLE_READ_ATTEMPTS = 5;
90
+ const STABLE_READ_POLL_MS = 10;
91
+ const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
92
+
93
+ function sleepSync(ms: number): void {
94
+ Atomics.wait(sleepBuffer, 0, 0, ms);
95
+ }
96
+
97
+ function isErrorCode(error: unknown, code: string): boolean {
98
+ return (error as NodeJS.ErrnoException).code === code;
99
+ }
100
+
101
+ interface FileSnapshot {
102
+ dev: number;
103
+ ino: number;
104
+ size: number;
105
+ mtimeMs: number;
106
+ ctimeMs: number;
107
+ }
108
+
109
+ function snapshot(path: string): FileSnapshot {
110
+ const stat = statSync(path);
111
+ return {
112
+ dev: stat.dev,
113
+ ino: stat.ino,
114
+ size: stat.size,
115
+ mtimeMs: stat.mtimeMs,
116
+ ctimeMs: stat.ctimeMs,
117
+ };
118
+ }
119
+
120
+ function sameSnapshot(left: FileSnapshot, right: FileSnapshot): boolean {
121
+ return (
122
+ left.dev === right.dev &&
123
+ left.ino === right.ino &&
124
+ left.size === right.size &&
125
+ left.mtimeMs === right.mtimeMs &&
126
+ left.ctimeMs === right.ctimeMs
127
+ );
128
+ }
129
+
130
+ function readStableFile(path: string): string | undefined {
131
+ for (let attempt = 1; attempt <= STABLE_READ_ATTEMPTS; attempt++) {
132
+ let before: FileSnapshot;
133
+ try {
134
+ before = snapshot(path);
135
+ } catch (error) {
136
+ if (isErrorCode(error, "ENOENT")) return undefined;
137
+ throw error;
138
+ }
139
+
140
+ try {
141
+ const raw = readFileSync(path, "utf8");
142
+ const after = snapshot(path);
143
+ if (sameSnapshot(before, after)) return raw;
144
+ } catch (error) {
145
+ if (!isErrorCode(error, "ENOENT")) throw error;
146
+ }
147
+
148
+ if (attempt < STABLE_READ_ATTEMPTS) sleepSync(STABLE_READ_POLL_MS);
149
+ }
150
+
151
+ throw new Error(
152
+ `Cookie-store read failed at "${path}": the file did not remain stable across ` +
153
+ `${STABLE_READ_ATTEMPTS} attempts. The original cookie-store file was left unchanged.`,
154
+ );
155
+ }
156
+
157
+ function atomicWrite(path: string, contents: string): void {
158
+ const dir = dirname(path);
159
+ mkdirSync(dir, { recursive: true });
160
+ const temporary = join(dir, `.${basename(path)}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`);
161
+ try {
162
+ writeFileSync(temporary, contents, { mode: 0o600 });
163
+ renameSync(temporary, path);
164
+ } finally {
165
+ try {
166
+ unlinkSync(temporary);
167
+ } catch {
168
+ // A successful rename already removed the temporary path. A failed
169
+ // cleanup must not hide the original write error.
170
+ }
171
+ }
172
+ }
173
+
174
+ function withStoreLock<T>(path: string, operation: () => T): T {
175
+ mkdirSync(dirname(path), { recursive: true });
176
+ const lockPath = `${path}.lock`;
177
+ const deadline = Date.now() + LOCK_WAIT_MS;
178
+ let lockFd: number | undefined;
179
+
180
+ while (lockFd === undefined) {
181
+ try {
182
+ lockFd = openSync(lockPath, "wx", 0o600);
183
+ try {
184
+ writeFileSync(
185
+ lockFd,
186
+ `${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() })}\n`,
187
+ );
188
+ } catch (error) {
189
+ closeSync(lockFd);
190
+ lockFd = undefined;
191
+ try {
192
+ unlinkSync(lockPath);
193
+ } catch {
194
+ // Preserve the lock-write error.
195
+ }
196
+ throw error;
197
+ }
198
+ } catch (error) {
199
+ if (!isErrorCode(error, "EEXIST")) {
200
+ if (lockFd !== undefined) closeSync(lockFd);
201
+ throw error;
202
+ }
203
+ if (Date.now() >= deadline) {
204
+ throw new Error(
205
+ `Cookie-store update timed out waiting for lock "${lockPath}" after ${LOCK_WAIT_MS}ms. ` +
206
+ "The cookie-store file was left unchanged.",
207
+ );
208
+ }
209
+ sleepSync(LOCK_POLL_MS);
210
+ }
211
+ }
212
+
213
+ closeSync(lockFd);
214
+ try {
215
+ return operation();
216
+ } finally {
217
+ try {
218
+ unlinkSync(lockPath);
219
+ } catch {
220
+ // The update has already committed. Do not replace its result with a
221
+ // cleanup error; a later writer will report the exact lock path.
222
+ }
223
+ }
224
+ }
225
+
61
226
  export class CookieJar {
62
227
  readonly path: string;
63
228
  private readonly source: string;
@@ -72,11 +237,14 @@ export class CookieJar {
72
237
  }
73
238
 
74
239
  load(): CookieStore {
75
- if (!existsSync(this.path)) {
76
- return { cookies: [], origins: [] };
240
+ const raw = readStableFile(this.path);
241
+ if (raw === undefined) return { cookies: [], origins: [] };
242
+ let parsed: CookieStore;
243
+ try {
244
+ parsed = JSON.parse(raw) as CookieStore;
245
+ } catch (error) {
246
+ throw new CookieStoreParseError(this.path, error);
77
247
  }
78
- const raw = readFileSync(this.path, "utf-8");
79
- const parsed = JSON.parse(raw);
80
248
  return {
81
249
  cookies: parsed.cookies ?? [],
82
250
  origins: parsed.origins ?? [],
@@ -90,13 +258,29 @@ export class CookieJar {
90
258
  * (constructor-provided source). Creates the parent directory if needed.
91
259
  */
92
260
  save(store: CookieStore): void {
93
- const stamped: CookieStore = {
261
+ withStoreLock(this.path, () => this.saveUnlocked(store));
262
+ }
263
+
264
+ private saveUnlocked(store: CookieStore): CookieStore {
265
+ const stamped = this.stamp(store);
266
+ atomicWrite(this.path, `${JSON.stringify(stamped, null, 2)}\n`);
267
+ return stamped;
268
+ }
269
+
270
+ private stamp(store: CookieStore): CookieStore {
271
+ return {
94
272
  ...store,
95
273
  exportedAt: new Date().toISOString(),
96
274
  exportedFrom: this.source,
97
275
  };
98
- mkdirSync(dirname(this.path), { recursive: true });
99
- writeFileSync(this.path, `${JSON.stringify(stamped, null, 2)}\n`);
276
+ }
277
+
278
+ private update<T>(operation: (store: CookieStore) => { store: CookieStore; result: T }): T {
279
+ return withStoreLock(this.path, () => {
280
+ const updated = operation(this.load());
281
+ this.saveUnlocked(updated.store);
282
+ return updated.result;
283
+ });
100
284
  }
101
285
 
102
286
  /**
@@ -118,10 +302,18 @@ export class CookieJar {
118
302
  * Returns the saved store.
119
303
  */
120
304
  set(cookie: Cookie): CookieStore {
121
- const store = this.load();
122
- const merged = mergeCookies(store, [cookie]);
123
- this.save(merged);
124
- return merged;
305
+ return this.merge([cookie]);
306
+ }
307
+
308
+ /** Merge cookies and origins in one interprocess-coordinated update. */
309
+ merge(cookies: Cookie[], origins: OriginEntry[] = []): CookieStore {
310
+ return this.update((store) => {
311
+ const merged = mergeCookies(store, cookies);
312
+ const byOrigin = new Map(store.origins.map((origin) => [origin.origin, origin]));
313
+ for (const origin of origins) byOrigin.set(origin.origin, origin);
314
+ merged.origins = [...byOrigin.values()];
315
+ return { store: merged, result: merged };
316
+ });
125
317
  }
126
318
 
127
319
  /**
@@ -133,24 +325,27 @@ export class CookieJar {
133
325
  if (!opts.domain && !opts.all) {
134
326
  throw new Error("Specify { domain } or { all: true }");
135
327
  }
136
- const store = this.load();
137
- const before = store.cookies.length;
138
- if (opts.all) {
139
- store.cookies = [];
140
- store.origins = [];
141
- } else if (opts.domain) {
142
- const dom = opts.domain;
143
- store.cookies = store.cookies.filter((c) => !domainMatches(c.domain, dom));
144
- store.origins = store.origins.filter((o) => {
145
- try {
146
- return !domainMatches(dom, new URL(o.origin).hostname);
147
- } catch {
148
- return true;
149
- }
150
- });
151
- }
152
- this.save(store);
153
- return { before, after: store.cookies.length };
328
+ return this.update((store) => {
329
+ const before = store.cookies.length;
330
+ if (opts.all) {
331
+ store.cookies = [];
332
+ store.origins = [];
333
+ } else if (opts.domain) {
334
+ const dom = opts.domain;
335
+ store.cookies = store.cookies.filter((c) => !domainMatches(c.domain, dom));
336
+ store.origins = store.origins.filter((o) => {
337
+ try {
338
+ return !domainMatches(dom, new URL(o.origin).hostname);
339
+ } catch {
340
+ return true;
341
+ }
342
+ });
343
+ }
344
+ return {
345
+ store,
346
+ result: { before, after: store.cookies.length },
347
+ };
348
+ });
154
349
  }
155
350
 
156
351
  /**
@@ -188,15 +383,7 @@ export class CookieJar {
188
383
  const store: CookieStore = { cookies, origins: incoming.origins ?? [] };
189
384
  this.save(store);
190
385
  } else {
191
- const store = this.load();
192
- const merged = mergeCookies(store, cookies);
193
- if (incoming.origins) {
194
- const seen = new Set(store.origins.map((o) => o.origin));
195
- for (const o of incoming.origins) {
196
- if (!seen.has(o.origin)) merged.origins.push(o);
197
- }
198
- }
199
- this.save(merged);
386
+ this.merge(cookies, incoming.origins ?? []);
200
387
  }
201
388
  return { count: cookies.length };
202
389
  }
@@ -209,8 +396,7 @@ export class CookieJar {
209
396
  exportedAt: new Date().toISOString(),
210
397
  exportedFrom: `${this.source}-export`,
211
398
  };
212
- mkdirSync(dirname(filePath), { recursive: true });
213
- writeFileSync(filePath, `${JSON.stringify(stamped, null, 2)}\n`);
399
+ atomicWrite(filePath, `${JSON.stringify(stamped, null, 2)}\n`);
214
400
  return { count: stamped.cookies.length };
215
401
  }
216
402
 
@@ -0,0 +1,28 @@
1
+ import type { Cookie, CookieJar } from "./client.ts";
2
+
3
+ /**
4
+ * Host callback that can mint or refresh cookies for one request URL.
5
+ * Must stay synchronous: `fetch` and `browse` attach cookies before they
6
+ * leave this process, the same constraint as `extraHeaders`.
7
+ *
8
+ * Return cookies to add; do not mutate the jar. The caller persists them
9
+ * with `jar.set()` so `--no-cookies` (no jar) skips the callback entirely.
10
+ */
11
+ export type ExtraCookies = (url: string, jar: CookieJar) => Cookie[];
12
+
13
+ /**
14
+ * Run a host `extraCookies` callback and persist whatever it returns.
15
+ * No-op when the callback is omitted. Order matters: this must run
16
+ * before `jar.header()` or Playwright `addCookies`, because an already
17
+ * populated `Cookie` header cannot pick up extra cookies later.
18
+ */
19
+ export function applyExtraCookies(
20
+ url: string,
21
+ jar: CookieJar,
22
+ extraCookies?: ExtraCookies,
23
+ ): Cookie[] {
24
+ if (!extraCookies) return [];
25
+ const cookies = extraCookies(url, jar);
26
+ for (const cookie of cookies) jar.set(cookie);
27
+ return cookies;
28
+ }
@@ -3,6 +3,7 @@ export {
3
3
  CookieJar,
4
4
  type CookieJarOptions,
5
5
  type CookieStore,
6
+ CookieStoreParseError,
6
7
  domainMatches,
7
8
  type InfoResult,
8
9
  isExpired,
@@ -11,3 +12,4 @@ export {
11
12
  type OriginEntry,
12
13
  pathMatches,
13
14
  } from "./client.js";
15
+ export { applyExtraCookies, type ExtraCookies } from "./extra.js";