revyse 0.2.0 → 0.2.1

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/headless.js CHANGED
@@ -3116,9 +3116,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3116
3116
  * @param {string} [path]
3117
3117
  * @return {(string|null|Command)}
3118
3118
  */
3119
- executableDir(path30) {
3120
- if (path30 === void 0) return this._executableDir;
3121
- this._executableDir = path30;
3119
+ executableDir(path31) {
3120
+ if (path31 === void 0) return this._executableDir;
3121
+ this._executableDir = path31;
3122
3122
  return this;
3123
3123
  }
3124
3124
  /**
@@ -3375,7 +3375,7 @@ var program = new Command();
3375
3375
 
3376
3376
  // src/commands/change.ts
3377
3377
  import { readFileSync as readFileSync13 } from "fs";
3378
- import path29 from "path";
3378
+ import path30 from "path";
3379
3379
 
3380
3380
  // ../agent-core/src/path-policy.ts
3381
3381
  import { realpathSync } from "fs";
@@ -4292,10 +4292,10 @@ function mergeDefs(...defs) {
4292
4292
  function cloneDef(schema) {
4293
4293
  return mergeDefs(schema._zod.def);
4294
4294
  }
4295
- function getElementAtPath(obj, path30) {
4296
- if (!path30)
4295
+ function getElementAtPath(obj, path31) {
4296
+ if (!path31)
4297
4297
  return obj;
4298
- return path30.reduce((acc, key) => acc?.[key], obj);
4298
+ return path31.reduce((acc, key) => acc?.[key], obj);
4299
4299
  }
4300
4300
  function promiseAllObject(promisesObj) {
4301
4301
  const keys = Object.keys(promisesObj);
@@ -4635,11 +4635,11 @@ function explicitlyAborted(x, startIndex = 0) {
4635
4635
  }
4636
4636
  return false;
4637
4637
  }
4638
- function prefixIssues(path30, issues) {
4638
+ function prefixIssues(path31, issues) {
4639
4639
  return issues.map((iss) => {
4640
4640
  var _a3;
4641
4641
  (_a3 = iss).path ?? (_a3.path = []);
4642
- iss.path.unshift(path30);
4642
+ iss.path.unshift(path31);
4643
4643
  return iss;
4644
4644
  });
4645
4645
  }
@@ -5101,16 +5101,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
5101
5101
  }
5102
5102
  function formatError(error62, mapper = (issue2) => issue2.message) {
5103
5103
  const fieldErrors = { _errors: [] };
5104
- const processError = (error63, path30 = []) => {
5104
+ const processError = (error63, path31 = []) => {
5105
5105
  for (const issue2 of error63.issues) {
5106
5106
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5107
- issue2.errors.map((issues) => processError({ issues }, [...path30, ...issue2.path]));
5107
+ issue2.errors.map((issues) => processError({ issues }, [...path31, ...issue2.path]));
5108
5108
  } else if (issue2.code === "invalid_key") {
5109
- processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5109
+ processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
5110
5110
  } else if (issue2.code === "invalid_element") {
5111
- processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5111
+ processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
5112
5112
  } else {
5113
- const fullpath = [...path30, ...issue2.path];
5113
+ const fullpath = [...path31, ...issue2.path];
5114
5114
  if (fullpath.length === 0) {
5115
5115
  fieldErrors._errors.push(mapper(issue2));
5116
5116
  } else {
@@ -5149,17 +5149,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
5149
5149
  }
5150
5150
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
5151
5151
  const result = { errors: [] };
5152
- const processError = (error63, path30 = []) => {
5152
+ const processError = (error63, path31 = []) => {
5153
5153
  var _a3;
5154
5154
  for (const issue2 of error63.issues) {
5155
5155
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5156
- issue2.errors.map((issues) => processError({ issues }, [...path30, ...issue2.path]));
5156
+ issue2.errors.map((issues) => processError({ issues }, [...path31, ...issue2.path]));
5157
5157
  } else if (issue2.code === "invalid_key") {
5158
- processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5158
+ processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
5159
5159
  } else if (issue2.code === "invalid_element") {
5160
- processError({ issues: issue2.issues }, [...path30, ...issue2.path]);
5160
+ processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
5161
5161
  } else {
5162
- const fullpath = [...path30, ...issue2.path];
5162
+ const fullpath = [...path31, ...issue2.path];
5163
5163
  if (fullpath.length === 0) {
5164
5164
  result.errors.push(mapper(issue2));
5165
5165
  continue;
@@ -5198,8 +5198,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
5198
5198
  }
5199
5199
  function toDotPath(_path) {
5200
5200
  const segs = [];
5201
- const path30 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5202
- for (const seg of path30) {
5201
+ const path31 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5202
+ for (const seg of path31) {
5203
5203
  if (typeof seg === "number")
5204
5204
  segs.push(`[${seg}]`);
5205
5205
  else if (typeof seg === "symbol")
@@ -22337,13 +22337,13 @@ function resolveRef(ref, ctx) {
22337
22337
  if (!ref.startsWith("#")) {
22338
22338
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
22339
22339
  }
22340
- const path30 = ref.slice(1).split("/").filter(Boolean);
22341
- if (path30.length === 0) {
22340
+ const path31 = ref.slice(1).split("/").filter(Boolean);
22341
+ if (path31.length === 0) {
22342
22342
  return ctx.rootSchema;
22343
22343
  }
22344
22344
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
22345
- if (path30[0] === defsKey) {
22346
- const key = path30[1] === void 0 ? void 0 : decodeJSONPointerSegment(path30[1]);
22345
+ if (path31[0] === defsKey) {
22346
+ const key = path31[1] === void 0 ? void 0 : decodeJSONPointerSegment(path31[1]);
22347
22347
  if (!key || !ctx.defs[key]) {
22348
22348
  throw new Error(`Reference not found: ${ref}`);
22349
22349
  }
@@ -23647,6 +23647,14 @@ var agentEventSchema = external_exports.object({
23647
23647
  event: external_exports.string(),
23648
23648
  data: external_exports.unknown().optional()
23649
23649
  });
23650
+ var changedFileSchema = external_exports.object({
23651
+ path: external_exports.string(),
23652
+ additions: external_exports.number().int(),
23653
+ deletions: external_exports.number().int(),
23654
+ explanation: external_exports.string().optional(),
23655
+ /** Agent engine: whether the file was created, modified or deleted. */
23656
+ kind: external_exports.enum(["create", "modify", "delete"]).optional()
23657
+ });
23650
23658
  var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
23651
23659
  external_exports.object({ type: external_exports.literal("planning"), changeId: external_exports.uuid() }),
23652
23660
  external_exports.object({
@@ -23654,6 +23662,8 @@ var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
23654
23662
  changeId: external_exports.uuid(),
23655
23663
  plan: changePlanSchema,
23656
23664
  model: external_exports.string(),
23665
+ /** Models tried first that couldn't run (A7, A11): the plan says it ran on another. */
23666
+ fellBackFrom: external_exports.array(external_exports.string()).optional(),
23657
23667
  /** Credits the change has cost so far (B1). */
23658
23668
  cost: external_exports.number().nonnegative().optional()
23659
23669
  }),
@@ -23673,22 +23683,23 @@ var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
23673
23683
  external_exports.object({
23674
23684
  type: external_exports.literal("checking"),
23675
23685
  changeId: external_exports.uuid(),
23676
- checks: external_exports.array(external_exports.object({ kind: checkKindSchema, status: checkStatusSchema }))
23686
+ checks: external_exports.array(
23687
+ external_exports.object({
23688
+ kind: checkKindSchema,
23689
+ status: checkStatusSchema,
23690
+ /** How long a finished check took (L16). */
23691
+ durationMs: external_exports.number().int().nonnegative().optional()
23692
+ })
23693
+ ),
23694
+ /** The change as written, shown while its checks run (L17, ADR 0042). */
23695
+ files: external_exports.array(changedFileSchema).optional(),
23696
+ diff: external_exports.string().optional()
23677
23697
  }),
23678
23698
  external_exports.object({
23679
23699
  type: external_exports.literal("result"),
23680
23700
  changeId: external_exports.uuid(),
23681
23701
  state: changeStateSchema,
23682
- files: external_exports.array(
23683
- external_exports.object({
23684
- path: external_exports.string(),
23685
- additions: external_exports.number().int(),
23686
- deletions: external_exports.number().int(),
23687
- explanation: external_exports.string().optional(),
23688
- /** Agent engine: whether the file was created, modified or deleted. */
23689
- kind: external_exports.enum(["create", "modify", "delete"]).optional()
23690
- })
23691
- ),
23702
+ files: external_exports.array(changedFileSchema),
23692
23703
  diff: external_exports.string(),
23693
23704
  checks: external_exports.array(checkResultSchema),
23694
23705
  /** Credits the change has cost so far (B1). */
@@ -24641,16 +24652,16 @@ var Diff = class {
24641
24652
  }
24642
24653
  }
24643
24654
  }
24644
- addToPath(path30, added, removed, oldPosInc, options) {
24645
- const last = path30.lastComponent;
24655
+ addToPath(path31, added, removed, oldPosInc, options) {
24656
+ const last = path31.lastComponent;
24646
24657
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
24647
24658
  return {
24648
- oldPos: path30.oldPos + oldPosInc,
24659
+ oldPos: path31.oldPos + oldPosInc,
24649
24660
  lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
24650
24661
  };
24651
24662
  } else {
24652
24663
  return {
24653
- oldPos: path30.oldPos + oldPosInc,
24664
+ oldPos: path31.oldPos + oldPosInc,
24654
24665
  lastComponent: { count: 1, added, removed, previousComponent: last }
24655
24666
  };
24656
24667
  }
@@ -30455,17 +30466,17 @@ var writeToFiles = (chunks, stdioItems, outputFiles) => {
30455
30466
  const writtenPaths = /* @__PURE__ */ new Set();
30456
30467
  for (const stdioItem of fileItems) {
30457
30468
  const { type, append } = stdioItem;
30458
- const path30 = type === "fileNumber" ? stdioItem.path : getFileTarget(stdioItem);
30459
- if (writtenPaths.has(path30)) {
30469
+ const path31 = type === "fileNumber" ? stdioItem.path : getFileTarget(stdioItem);
30470
+ if (writtenPaths.has(path31)) {
30460
30471
  continue;
30461
30472
  }
30462
- writtenPaths.add(path30);
30463
- if (append || outputFiles.has(path30)) {
30464
- appendFileSync(path30, contents);
30473
+ writtenPaths.add(path31);
30474
+ if (append || outputFiles.has(path31)) {
30475
+ appendFileSync(path31, contents);
30465
30476
  } else {
30466
- writeFileSync2(path30, contents);
30477
+ writeFileSync2(path31, contents);
30467
30478
  }
30468
- outputFiles.add(path30);
30479
+ outputFiles.add(path31);
30469
30480
  }
30470
30481
  };
30471
30482
 
@@ -33447,6 +33458,71 @@ ${summary}` : summary
33447
33458
  // ../agent-core/src/checks/browser.ts
33448
33459
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
33449
33460
  import path21 from "path";
33461
+ var LOC_ATTRIBUTE = "data-revyse-loc";
33462
+ var FRAME_PADDING = 24;
33463
+ var FRAME_MIN_WIDTH = 480;
33464
+ var FRAME_MAX_RATIO = 2;
33465
+ function frameAround(box, viewport) {
33466
+ const width = Math.min(Math.max(box.width + 2 * FRAME_PADDING, FRAME_MIN_WIDTH), viewport.width);
33467
+ const height = Math.min(
33468
+ Math.max(box.height + 2 * FRAME_PADDING, width / FRAME_MAX_RATIO),
33469
+ viewport.height
33470
+ );
33471
+ const place = (start, size, frame, limit) => Math.round(Math.min(Math.max(start + size / 2 - frame / 2, 0), limit - frame));
33472
+ return {
33473
+ x: place(box.x, box.width, width, viewport.width),
33474
+ y: place(box.y, box.height, height, viewport.height),
33475
+ width: Math.round(width),
33476
+ height: Math.round(height)
33477
+ };
33478
+ }
33479
+ function nearestLoc(locs, source) {
33480
+ const distance = (loc) => {
33481
+ const [line, column] = loc.split(":").slice(-2).map(Number);
33482
+ if (!Number.isFinite(line) || source.line === void 0) return Infinity;
33483
+ return Math.abs(line - source.line) * 1e4 + Math.abs((column ?? 0) - (source.column ?? 0));
33484
+ };
33485
+ let best = 0;
33486
+ for (let i2 = 1; i2 < locs.length; i2++) if (distance(locs[i2]) < distance(locs[best])) best = i2;
33487
+ return best;
33488
+ }
33489
+ async function findSelected(page, selector, source) {
33490
+ const bySelector = page.locator(selector).first();
33491
+ const selectorFound = await page.locator(selector).count().catch(() => 0) > 0;
33492
+ if (source?.line) {
33493
+ const exact = source.column === void 0 ? `${source.file}:${source.line}:` : `${source.file}:${source.line}:${source.column}`;
33494
+ const isExact = (loc) => source.column === void 0 ? !!loc?.startsWith(exact) : loc === exact;
33495
+ if (selectorFound) {
33496
+ const loc = await bySelector.evaluate((el, attr) => el.closest(`[${attr}]`)?.getAttribute(attr) ?? null, LOC_ATTRIBUTE).catch(() => null);
33497
+ if (isExact(loc)) return bySelector;
33498
+ }
33499
+ const op = source.column === void 0 ? "^=" : "=";
33500
+ const byLoc = page.locator(`[${LOC_ATTRIBUTE}${op}${JSON.stringify(exact)}]`);
33501
+ if (await byLoc.count().catch(() => 0) > 0) return byLoc.first();
33502
+ }
33503
+ if (selectorFound) return bySelector;
33504
+ if (!source) return null;
33505
+ const fromFile = page.locator(`[${LOC_ATTRIBUTE}^=${JSON.stringify(`${source.file}:`)}]`);
33506
+ const locs = await fromFile.evaluateAll((els, attr) => els.map((el) => el.getAttribute(attr) ?? ""), LOC_ATTRIBUTE).catch(() => []);
33507
+ return locs.length > 0 ? fromFile.nth(nearestLoc(locs, source)) : null;
33508
+ }
33509
+ async function frameSelected(page, target) {
33510
+ await target.scrollIntoViewIfNeeded({ timeout: 2e3 }).catch(() => {
33511
+ });
33512
+ await page.evaluate(
33513
+ () => Promise.race([
33514
+ Promise.all(
33515
+ Array.from(document.images).filter((img) => !img.complete).map((img) => new Promise((done) => img.addEventListener("loadend", done)))
33516
+ ),
33517
+ new Promise((done) => setTimeout(done, 2e3))
33518
+ ])
33519
+ ).catch(() => {
33520
+ });
33521
+ const box = await target.boundingBox().catch(() => null);
33522
+ const viewport = page.viewportSize();
33523
+ if (!box || !viewport || box.width <= 0 || box.height <= 0) return null;
33524
+ return frameAround(box, viewport);
33525
+ }
33450
33526
  var BrowserUnavailableError = class extends Error {
33451
33527
  };
33452
33528
  var MAX_ERRORS = 10;
@@ -33474,7 +33550,7 @@ async function createPlaywrightDriver(env = process.env) {
33474
33550
  if (!browser) throw new BrowserUnavailableError(`No Chromium could be started. ${NOT_RUN_HINT}`);
33475
33551
  const b = browser;
33476
33552
  return {
33477
- async capture({ url: url2, selector, sourceFile, timeoutMs }) {
33553
+ async capture({ url: url2, selector, source, timeoutMs }) {
33478
33554
  const context = await b.newContext({ viewport: { width: 1280, height: 800 } });
33479
33555
  const page = await context.newPage();
33480
33556
  const errors = [];
@@ -33494,9 +33570,15 @@ async function createPlaywrightDriver(env = process.env) {
33494
33570
  let found = null;
33495
33571
  if (await page.locator(selector).count().catch(() => 0) > 0)
33496
33572
  found = "selector";
33497
- else if (sourceFile && await page.locator(`[data-revyse-loc^="${sourceFile}:"]`).count() > 0)
33573
+ else if (source && await page.locator(`[${LOC_ATTRIBUTE}^=${JSON.stringify(`${source.file}:`)}]`).count() > 0)
33498
33574
  found = "source";
33499
- const screenshot = await page.screenshot({ type: "png" });
33575
+ const target = await findSelected(page, selector, source);
33576
+ const clip = target ? await frameSelected(page, target) : null;
33577
+ const screenshot = await page.screenshot({
33578
+ type: "png",
33579
+ animations: "disabled",
33580
+ ...clip ? { clip } : {}
33581
+ });
33500
33582
  return { status: res?.status() ?? null, found, screenshot, errors };
33501
33583
  } finally {
33502
33584
  await context.close();
@@ -33611,7 +33693,7 @@ function createAgentApi(opts) {
33611
33693
  const f = opts.fetch ?? fetch;
33612
33694
  const base = opts.apiUrl.replace(/\/+$/, "");
33613
33695
  const auth = { authorization: `Bearer ${opts.token}` };
33614
- const call = (path30, body, signal) => f(`${base}${path30}`, {
33696
+ const call = (path31, body, signal) => f(`${base}${path31}`, {
33615
33697
  method: "POST",
33616
33698
  headers: { "content-type": "application/json", ...auth },
33617
33699
  body: JSON.stringify(body),
@@ -33627,18 +33709,18 @@ function createAgentApi(opts) {
33627
33709
  }
33628
33710
  return {
33629
33711
  baseUrl: base,
33630
- async get(path30) {
33631
- const res = await f(`${base}${path30}`, { headers: auth });
33712
+ async get(path31) {
33713
+ const res = await f(`${base}${path31}`, { headers: auth });
33632
33714
  if (!res.ok) throw await failure2(res);
33633
33715
  return await res.json();
33634
33716
  },
33635
- async post(path30, body) {
33636
- const res = await call(path30, body);
33717
+ async post(path31, body) {
33718
+ const res = await call(path31, body);
33637
33719
  if (!res.ok) throw await failure2(res);
33638
33720
  return await res.json();
33639
33721
  },
33640
- async *stream(path30, body, signal) {
33641
- const res = await call(path30, body, signal);
33722
+ async *stream(path31, body, signal) {
33723
+ const res = await call(path31, body, signal);
33642
33724
  if (!res.ok) throw await failure2(res);
33643
33725
  if (!res.body) return;
33644
33726
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
@@ -33671,11 +33753,51 @@ function parseBlock(block) {
33671
33753
 
33672
33754
  // ../agent-core/src/change/runner.ts
33673
33755
  import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
33674
- import path24 from "path";
33756
+ import path25 from "path";
33757
+
33758
+ // ../agent-core/src/patch/originals.ts
33759
+ import { existsSync as existsSync4 } from "fs";
33760
+ import path22 from "path";
33761
+ async function withOriginals(opts) {
33762
+ const io = opts.io ?? nodeIO;
33763
+ const dir = changeDir(opts.projectRoot, opts.changeId);
33764
+ const manifest = JSON.parse(io.readFile(path22.join(dir, "manifest.json")));
33765
+ const files = manifest.files.map((f) => ({
33766
+ ...f,
33767
+ real: resolveProjectPath(opts.projectRoot, f.path),
33768
+ pre: f.preHash === null ? null : io.readFile(path22.join(dir, "pre", f.path)),
33769
+ post: f.postHash === null ? null : io.readFile(path22.join(dir, "post", f.path))
33770
+ }));
33771
+ const hashOf = (real) => existsSync4(real) ? hashContent(io.readFile(real)) : null;
33772
+ const put = (real, content) => {
33773
+ if (content === null) io.unlink(real);
33774
+ else {
33775
+ io.mkdir(path22.dirname(real));
33776
+ io.writeFile(real, content);
33777
+ }
33778
+ };
33779
+ const changed = files.filter((f) => hashOf(f.real) !== f.postHash).map((f) => f.path);
33780
+ if (changed.length > 0) return { status: "changed", paths: changed };
33781
+ for (const f of files) put(f.real, f.pre);
33782
+ const putBack = () => {
33783
+ const kept2 = files.filter((f) => hashOf(f.real) !== f.preHash).map((f) => f.path);
33784
+ for (const f of files) if (!kept2.includes(f.path)) put(f.real, f.post);
33785
+ return kept2;
33786
+ };
33787
+ let value;
33788
+ try {
33789
+ value = await opts.work();
33790
+ } catch (err) {
33791
+ putBack();
33792
+ throw err;
33793
+ }
33794
+ const kept = putBack();
33795
+ return kept.length > 0 ? { status: "kept-edit", value, paths: kept } : { status: "done", value };
33796
+ }
33675
33797
 
33676
33798
  // ../agent-core/src/tools/file-host.ts
33677
33799
  import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
33678
- import path22 from "path";
33800
+ import path23 from "path";
33679
33801
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([
33680
33802
  "node_modules",
33681
33803
  ".git",
@@ -33691,7 +33813,7 @@ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
33691
33813
  ]);
33692
33814
  var MAX_HOST_FILE_BYTES = 1e6;
33693
33815
  var MAX_LISTED = 2e4;
33694
- var toPosix2 = (p) => p.split(path22.sep).join("/");
33816
+ var toPosix2 = (p) => p.split(path23.sep).join("/");
33695
33817
  function localFileHost(projectRoot) {
33696
33818
  return {
33697
33819
  async read(relativePath) {
@@ -33719,12 +33841,12 @@ function localFileHost(projectRoot) {
33719
33841
  if (out.length >= MAX_LISTED) return;
33720
33842
  let entries;
33721
33843
  try {
33722
- entries = readdirSync3(path22.join(projectRoot, dir), { withFileTypes: true });
33844
+ entries = readdirSync3(path23.join(projectRoot, dir), { withFileTypes: true });
33723
33845
  } catch {
33724
33846
  return;
33725
33847
  }
33726
33848
  for (const e of entries.sort((a2, b) => a2.name.localeCompare(b.name))) {
33727
- const rel = toPosix2(path22.join(dir, e.name));
33849
+ const rel = toPosix2(path23.join(dir, e.name));
33728
33850
  if (e.isDirectory()) {
33729
33851
  if (!SKIPPED_DIRS.has(e.name) && !isSensitivePath(rel)) walk(rel);
33730
33852
  } else if (e.isFile() && !isSensitivePath(rel)) {
@@ -33740,7 +33862,7 @@ function localFileHost(projectRoot) {
33740
33862
  }
33741
33863
 
33742
33864
  // ../agent-core/src/tools/integrations.ts
33743
- import path23 from "path";
33865
+ import path24 from "path";
33744
33866
  var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
33745
33867
  var ENV_TEMPLATE = /^\.env\.(example|sample|template)$/;
33746
33868
  var ENV_IN_CODE = /process\.env\.([A-Z][A-Z0-9_]*)|process\.env\[\s*["']([A-Z][A-Z0-9_]*)["']\s*\]|import\.meta\.env\.([A-Z][A-Z0-9_]*)/g;
@@ -33763,7 +33885,7 @@ async function projectSignals(read, paths) {
33763
33885
  for (const m of text.matchAll(ENV_IN_CODE)) env.add(m[1] ?? m[2] ?? m[3]);
33764
33886
  for (const m of text.matchAll(IMPORT)) imports.add(packageOf(m[1] ?? m[2]));
33765
33887
  }
33766
- for (const p of paths.filter((p2) => ENV_TEMPLATE.test(path23.posix.basename(p2)))) {
33888
+ for (const p of paths.filter((p2) => ENV_TEMPLATE.test(path24.posix.basename(p2)))) {
33767
33889
  for (const line of (await read(p) ?? "").split("\n")) {
33768
33890
  const name = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/)?.[1];
33769
33891
  if (name) env.add(name);
@@ -33823,27 +33945,27 @@ var StagedFiles = class {
33823
33945
  /** Paths written by the package manager (I7), not by the model: the write rules don't apply. */
33824
33946
  fromPackageManager = /* @__PURE__ */ new Set();
33825
33947
  /** Current content as the agent sees it: staged if written, else the project's. */
33826
- async read(path30) {
33827
- const s = this.staged.get(path30);
33948
+ async read(path31) {
33949
+ const s = this.staged.get(path31);
33828
33950
  if (s) return s.content;
33829
- const content = await this.host.read(path30);
33830
- if (!this.seen.has(path30)) this.seen.set(path30, content === null ? null : hashContent(content));
33951
+ const content = await this.host.read(path31);
33952
+ if (!this.seen.has(path31)) this.seen.set(path31, content === null ? null : hashContent(content));
33831
33953
  return content;
33832
33954
  }
33833
33955
  /** Like `read`, without recording a snapshot: for searching, which only looks. */
33834
- async peek(path30) {
33835
- const s = this.staged.get(path30);
33836
- return s ? s.content : this.host.read(path30);
33956
+ async peek(path31) {
33957
+ const s = this.staged.get(path31);
33958
+ return s ? s.content : this.host.read(path31);
33837
33959
  }
33838
33960
  /** Writes `content` (or deletes, with null) in the staging layer. */
33839
- async write(path30, content) {
33840
- const existing = this.staged.get(path30);
33961
+ async write(path31, content) {
33962
+ const existing = this.staged.get(path31);
33841
33963
  if (existing) {
33842
33964
  existing.content = content;
33843
33965
  return;
33844
33966
  }
33845
- const before = await this.read(path30);
33846
- this.staged.set(path30, {
33967
+ const before = await this.read(path31);
33968
+ this.staged.set(path31, {
33847
33969
  content,
33848
33970
  existed: before !== null,
33849
33971
  baseHash: before === null ? null : hashContent(before)
@@ -33854,9 +33976,9 @@ var StagedFiles = class {
33854
33976
  * a supported SDK). The patch engine applies it with the rest, so it lands, is checked and is
33855
33977
  * reverted together; the write rules that keep the model out of these files don't apply.
33856
33978
  */
33857
- async writeFromPackageManager(path30, content) {
33858
- await this.write(path30, content);
33859
- this.fromPackageManager.add(path30);
33979
+ async writeFromPackageManager(path31, content) {
33980
+ await this.write(path31, content);
33981
+ this.fromPackageManager.add(path31);
33860
33982
  }
33861
33983
  /** The paths the package manager wrote. */
33862
33984
  packageManagerPaths() {
@@ -33874,18 +33996,18 @@ var StagedFiles = class {
33874
33996
  /** What the apply phase changed, net: a file created then deleted is no change at all. */
33875
33997
  changes() {
33876
33998
  const out = [];
33877
- for (const [path30, s] of [...this.staged].sort(([a2], [b]) => a2.localeCompare(b))) {
33999
+ for (const [path31, s] of [...this.staged].sort(([a2], [b]) => a2.localeCompare(b))) {
33878
34000
  if (!s.existed && s.content === null) continue;
33879
- if (!s.existed) out.push({ path: path30, kind: "create", content: s.content });
33880
- else if (s.content === null) out.push({ path: path30, kind: "delete", baseHash: s.baseHash });
34001
+ if (!s.existed) out.push({ path: path31, kind: "create", content: s.content });
34002
+ else if (s.content === null) out.push({ path: path31, kind: "delete", baseHash: s.baseHash });
33881
34003
  else if (hashContent(s.content) !== s.baseHash)
33882
- out.push({ path: path30, kind: "modify", content: s.content, baseHash: s.baseHash });
34004
+ out.push({ path: path31, kind: "modify", content: s.content, baseHash: s.baseHash });
33883
34005
  }
33884
34006
  return out;
33885
34007
  }
33886
34008
  /** Every file the agent read or wrote, with its hash when first seen (null: didn't exist). */
33887
34009
  snapshots() {
33888
- return [...this.seen].map(([path30, hash2]) => ({ path: path30, hash: hash2 })).sort((a2, b) => a2.path.localeCompare(b.path));
34010
+ return [...this.seen].map(([path31, hash2]) => ({ path: path31, hash: hash2 })).sort((a2, b) => a2.path.localeCompare(b.path));
33889
34011
  }
33890
34012
  };
33891
34013
 
@@ -33922,7 +34044,7 @@ var ok = (callId, output2) => ({
33922
34044
  output: output2,
33923
34045
  isError: false
33924
34046
  });
33925
- var secretFile = (path30) => `Can't change ${path30}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
34047
+ var secretFile = (path31) => `Can't change ${path31}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
33926
34048
  var fail = (callId, output2) => ({
33927
34049
  callId,
33928
34050
  output: output2,
@@ -33965,12 +34087,12 @@ function createToolRuntime(opts) {
33965
34087
  const found = [];
33966
34088
  let withheld = 0;
33967
34089
  let stopped = false;
33968
- for (const path30 of await visible(input2.path, input2.glob)) {
34090
+ for (const path31 of await visible(input2.path, input2.glob)) {
33969
34091
  if (found.length >= max || Date.now() - started > budget) {
33970
34092
  stopped = true;
33971
34093
  break;
33972
34094
  }
33973
- const content = await staging.peek(path30);
34095
+ const content = await staging.peek(path31);
33974
34096
  if (content === null) continue;
33975
34097
  if (containsSecret(content)) {
33976
34098
  withheld++;
@@ -33982,7 +34104,7 @@ function createToolRuntime(opts) {
33982
34104
  if (line.length > MAX_SEARCH_LINE || !matches(line)) continue;
33983
34105
  const text = line.trim();
33984
34106
  found.push(
33985
- `${path30}:${i2 + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
34107
+ `${path31}:${i2 + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
33986
34108
  );
33987
34109
  }
33988
34110
  }
@@ -34190,11 +34312,13 @@ function createChangeRunner(opts) {
34190
34312
  function run(session, work) {
34191
34313
  if (session.busy) throw new Busy("Revyse is still working on this change");
34192
34314
  session.busy = true;
34193
- session.abort = new AbortController();
34194
- const signal = session.abort.signal;
34315
+ const controller = new AbortController();
34316
+ session.abort = controller;
34317
+ const signal = controller.signal;
34195
34318
  void work(signal).catch((err) => {
34196
34319
  if (!signal.aborted) report(session.id, err);
34197
34320
  }).finally(() => {
34321
+ if (session.abort !== controller) return;
34198
34322
  session.busy = false;
34199
34323
  session.abort = void 0;
34200
34324
  });
@@ -34222,9 +34346,9 @@ function createChangeRunner(opts) {
34222
34346
  const stored = [];
34223
34347
  for (const key of localKeys) {
34224
34348
  try {
34225
- const bytes2 = readFileSync8(path24.join(opts.projectRoot, key));
34349
+ const bytes2 = readFileSync8(path25.join(opts.projectRoot, key));
34226
34350
  const { artifact, upload } = await opts.api.post(`/api/changes/${changeId}/artifacts`, {
34227
- name: path24.basename(key),
34351
+ name: path25.basename(key),
34228
34352
  contentType: "image/png",
34229
34353
  size: bytes2.length
34230
34354
  });
@@ -34269,7 +34393,7 @@ function createChangeRunner(opts) {
34269
34393
  const before = await driver.capture({
34270
34394
  url: url2,
34271
34395
  selector: session.element.selector,
34272
- sourceFile: session.element.source?.file,
34396
+ source: session.element.source,
34273
34397
  timeoutMs: 3e4
34274
34398
  }).catch((err) => {
34275
34399
  log(`browser check: before-capture failed: ${err.message}`);
@@ -34302,7 +34426,7 @@ function createChangeRunner(opts) {
34302
34426
  `/api/changes/${session.id}/plan`,
34303
34427
  {
34304
34428
  element: session.element,
34305
- files: gathered.files.map(({ path: path30, content, hash: hash2 }) => ({ path: path30, content, hash: hash2 })),
34429
+ files: gathered.files.map(({ path: path31, content, hash: hash2 }) => ({ path: path31, content, hash: hash2 })),
34306
34430
  modelTier: session.tier,
34307
34431
  model: session.model,
34308
34432
  ...extra
@@ -34314,16 +34438,24 @@ function createChangeRunner(opts) {
34314
34438
  const {
34315
34439
  plan: result,
34316
34440
  model,
34317
- cost
34441
+ cost,
34442
+ fellBackFrom
34318
34443
  } = ev.data;
34319
- onPlan(session, result, model, cost);
34444
+ onPlan(session, result, model, cost, void 0, fellBackFrom);
34320
34445
  }
34321
34446
  }
34322
- function onPlan(session, result, model, cost, connectPath) {
34447
+ function onPlan(session, result, model, cost, connectPath, fellBackFrom) {
34323
34448
  session.plan = result;
34324
34449
  if (cost !== void 0) session.cost = cost;
34325
34450
  if (result.decision === "proceed")
34326
- opts.emit({ type: "plan", changeId: session.id, plan: result, model, cost: session.cost });
34451
+ opts.emit({
34452
+ type: "plan",
34453
+ changeId: session.id,
34454
+ plan: result,
34455
+ model,
34456
+ ...fellBackFrom?.length ? { fellBackFrom } : {},
34457
+ cost: session.cost
34458
+ });
34327
34459
  else if (result.decision === "clarify")
34328
34460
  opts.emit({
34329
34461
  type: "clarification",
@@ -34353,19 +34485,23 @@ function createChangeRunner(opts) {
34353
34485
  }
34354
34486
  async function apply(session, signal) {
34355
34487
  const browser = prepareBrowser(session);
34356
- const baseline = hasLocalChecks() ? runChecks(session.contextPaths).catch((err) => {
34357
- log(`baseline checks failed to run: ${err.message}`);
34358
- return [];
34359
- }) : Promise.resolve([]);
34360
34488
  try {
34361
- await applyWithBrowser(session, signal, browser, baseline);
34489
+ await applyWithBrowser(session, signal, browser);
34362
34490
  } finally {
34363
34491
  void browser.then((b) => b.driver?.close()).catch(() => {
34364
34492
  });
34365
34493
  }
34366
34494
  }
34367
- const hasLocalChecks = () => ["typecheck", "lint", "test"].some((k) => opts.config.scripts[k]);
34368
- async function applyWithBrowser(session, signal, browserPrep, baselinePrep) {
34495
+ async function beforePage(prep) {
34496
+ const late = /* @__PURE__ */ Symbol("late");
34497
+ const ready = await Promise.race([
34498
+ prep,
34499
+ new Promise((r) => setTimeout(() => r(late), opts.beforeWaitMs ?? 3e3))
34500
+ ]);
34501
+ if (ready !== late) return ready;
34502
+ return { before: null, reason: "The page took too long to load before the change." };
34503
+ }
34504
+ async function applyWithBrowser(session, signal, browserPrep) {
34369
34505
  const { files, missing } = readFiles(session.contextPaths);
34370
34506
  const stale = [
34371
34507
  ...missing,
@@ -34382,10 +34518,16 @@ function createChangeRunner(opts) {
34382
34518
  return conflict(session.id, "changed_since_plan", stale);
34383
34519
  }
34384
34520
  session.stage = "patching";
34521
+ const planned = new Set(session.plan?.steps.flatMap((s) => s.files) ?? []);
34522
+ const toPatch = planned.size > 0 ? files.filter((f) => planned.has(f.path)) : files;
34385
34523
  let patch;
34386
- for await (const ev of opts.api.stream(`/api/changes/${session.id}/patch`, { files }, signal)) {
34524
+ for await (const ev of opts.api.stream(
34525
+ `/api/changes/${session.id}/patch`,
34526
+ { files: toPatch },
34527
+ signal
34528
+ )) {
34387
34529
  if (ev.event === "progress" && ev.data.stage === "patching")
34388
- opts.emit({ type: "applying", changeId: session.id, files: files.length });
34530
+ opts.emit({ type: "applying", changeId: session.id, files: toPatch.length });
34389
34531
  if (ev.event === "error") return emitStreamError(session, ev.data);
34390
34532
  if (ev.event === "result") {
34391
34533
  const data = ev.data;
@@ -34394,13 +34536,13 @@ function createChangeRunner(opts) {
34394
34536
  }
34395
34537
  }
34396
34538
  if (!patch) return;
34397
- const [browser, baseline] = await Promise.all([browserPrep, baselinePrep]);
34539
+ const browser = await beforePage(browserPrep);
34398
34540
  session.stage = "applying";
34399
- const snapshots = files.map(({ path: path30, hash: hash2 }) => ({ path: path30, hash: hash2 }));
34541
+ const snapshots = files.map(({ path: path31, hash: hash2 }) => ({ path: path31, hash: hash2 }));
34400
34542
  const applied = applyPatch({
34401
34543
  projectRoot: opts.projectRoot,
34402
34544
  changeId: session.id,
34403
- patch: { files: patch.files.map(({ path: path30, edits }) => ({ path: path30, edits })) },
34545
+ patch: { files: patch.files.map(({ path: path31, edits }) => ({ path: path31, edits })) },
34404
34546
  snapshots
34405
34547
  });
34406
34548
  if (applied.status === "conflict") {
@@ -34423,23 +34565,56 @@ function createChangeRunner(opts) {
34423
34565
  }
34424
34566
  session.applied = true;
34425
34567
  const explanations = new Map(patch.files.map((f) => [f.path, f.explanation]));
34426
- await checkAndReport(session, applied, browser, baseline, { explanations });
34568
+ await checkAndReport(session, applied, browser, signal, { explanations });
34427
34569
  }
34428
- async function checkAndReport(session, applied, browser, baseline, extras = {}) {
34570
+ async function checkAndReport(session, applied, browser, signal, extras = {}) {
34429
34571
  session.stage = "checking";
34430
- const configured = [
34431
- ...["typecheck", "lint", "test", "build"].filter((k) => opts.config.scripts[k]),
34432
- ...browser.driver ? ["browser"] : []
34433
- ];
34434
- opts.emit({
34435
- type: "checking",
34436
- changeId: session.id,
34437
- checks: configured.map((kind) => ({ kind, status: "running" }))
34438
- });
34572
+ const ALL = ["typecheck", "lint", "test", "build"];
34573
+ const kinds = ALL.filter((k) => opts.config.scripts[k]);
34574
+ const files = applied.files.map((f) => ({
34575
+ path: f.path,
34576
+ additions: f.additions,
34577
+ deletions: f.deletions,
34578
+ explanation: extras.explanations?.get(f.path),
34579
+ ...f.kind ? { kind: f.kind } : {}
34580
+ }));
34581
+ const progress = new Map([
34582
+ ...kinds.map((k) => [k, { status: "running" }]),
34583
+ ...browser.driver ? [["browser", { status: "running" }]] : []
34584
+ ]);
34585
+ const report2 = () => {
34586
+ if (signal.aborted) return;
34587
+ opts.emit({
34588
+ type: "checking",
34589
+ changeId: session.id,
34590
+ checks: [...progress].map(([kind, p]) => ({
34591
+ kind,
34592
+ status: p.status,
34593
+ ...p.ms !== void 0 ? { durationMs: p.ms } : {}
34594
+ })),
34595
+ files,
34596
+ diff: applied.diff
34597
+ });
34598
+ };
34599
+ const finished7 = (c3) => {
34600
+ progress.set(c3.kind, { status: c3.status, ms: c3.durationMs });
34601
+ report2();
34602
+ return c3;
34603
+ };
34604
+ report2();
34439
34605
  const changed = applied.files.filter((f) => f.kind !== "delete").map((f) => f.path);
34440
34606
  const startedBrowser = performance.now();
34441
34607
  const [local, browserRaw] = await Promise.all([
34442
- runChecks(changed),
34608
+ Promise.all(
34609
+ ALL.map(async (kind) => {
34610
+ const [r] = await runChecks(changed, [kind]);
34611
+ if (!r) {
34612
+ if (progress.delete(kind)) report2();
34613
+ return null;
34614
+ }
34615
+ return progress.has(kind) ? finished7(r) : r;
34616
+ })
34617
+ ).then((results) => results.filter((r) => r !== null)),
34443
34618
  browser.driver && browser.url ? runBrowserCheck({
34444
34619
  projectRoot: opts.projectRoot,
34445
34620
  changeId: session.id,
@@ -34451,26 +34626,45 @@ function createChangeRunner(opts) {
34451
34626
  return browser.driver.capture({
34452
34627
  url: browser.url,
34453
34628
  selector: session.element.selector,
34454
- sourceFile: session.element.source?.file,
34629
+ source: session.element.source,
34455
34630
  timeoutMs: 6e4
34456
34631
  });
34457
34632
  },
34458
34633
  durationMs: () => performance.now() - startedBrowser
34459
- }) : Promise.resolve(browserNotRun(browser.reason ?? "The browser check didn't run."))
34634
+ }).then((r) => finished7(redactCheck(r))) : Promise.resolve(browserNotRun(browser.reason ?? "The browser check didn't run."))
34460
34635
  ]);
34461
- const browserResult = redactCheck(browserRaw);
34636
+ if (signal.aborted) return;
34637
+ const browserResult = browserRaw;
34462
34638
  const localKeys = browserResult.evidence?.artifactKeys ?? [];
34463
34639
  if (localKeys.length > 0) {
34464
34640
  const stored = await uploadArtifacts(session.id, localKeys);
34465
34641
  browserResult.evidence.artifactKeys = [...localKeys, ...stored];
34466
34642
  }
34467
- const checks = [
34468
- ...compareWithBaseline(
34469
- local.filter((c3) => c3.kind !== "browser"),
34470
- baseline
34471
- ),
34472
- browserResult
34473
- ];
34643
+ const failed = local.filter((c3) => c3.status === "failed").map((c3) => c3.kind);
34644
+ let baseline = [];
34645
+ if (failed.length > 0) {
34646
+ const before = await withOriginals({
34647
+ projectRoot: opts.projectRoot,
34648
+ changeId: session.id,
34649
+ work: () => runChecks(changed, failed)
34650
+ }).catch((err) => {
34651
+ log(`couldn't re-run the failed checks on the original files: ${err.message}`);
34652
+ return null;
34653
+ });
34654
+ if (signal.aborted) return;
34655
+ if (before?.status === "kept-edit") {
34656
+ await transition(
34657
+ session.id,
34658
+ "failed",
34659
+ `Files changed while Revyse was checking: ${before.paths.join(", ")}`,
34660
+ "conflict"
34661
+ ).catch(() => {
34662
+ });
34663
+ return conflict(session.id, "changed_while_applying", before.paths);
34664
+ }
34665
+ if (before?.status === "done") baseline = before.value;
34666
+ }
34667
+ const checks = [...compareWithBaseline(local, baseline), browserResult];
34474
34668
  session.stage = "reporting";
34475
34669
  const { change } = await opts.api.post(
34476
34670
  `/api/changes/${session.id}/result`,
@@ -34492,13 +34686,7 @@ function createChangeRunner(opts) {
34492
34686
  type: "result",
34493
34687
  changeId: session.id,
34494
34688
  state: change.state,
34495
- files: applied.files.map((f) => ({
34496
- path: f.path,
34497
- additions: f.additions,
34498
- deletions: f.deletions,
34499
- explanation: extras.explanations?.get(f.path),
34500
- ...f.kind ? { kind: f.kind } : {}
34501
- })),
34689
+ files,
34502
34690
  diff: applied.diff,
34503
34691
  checks,
34504
34692
  cost: session.cost,
@@ -34624,21 +34812,19 @@ function createChangeRunner(opts) {
34624
34812
  const planned = new Set(session.plan?.steps.flatMap((s) => s.files) ?? []);
34625
34813
  opts.emit({ type: "applying", changeId: session.id, files: planned.size });
34626
34814
  const browserPrep = prepareBrowser(session);
34627
- const baselinePrep = hasLocalChecks() ? runChecks([]).catch((err) => {
34628
- log(`baseline checks failed to run: ${err.message}`);
34629
- return [];
34630
- }) : Promise.resolve([]);
34631
34815
  try {
34632
- await agentApplyWith(session, signal, browserPrep, baselinePrep);
34816
+ await agentApplyWith(session, signal, browserPrep);
34633
34817
  } finally {
34634
34818
  void browserPrep.then((b) => b.driver?.close()).catch(() => {
34635
34819
  });
34636
34820
  }
34637
34821
  }
34638
- async function agentApplyWith(session, signal, browserPrep, baselinePrep) {
34822
+ async function agentApplyWith(session, signal, browserPrep) {
34823
+ let ready;
34824
+ const browserReady = () => ready ??= beforePage(browserPrep);
34639
34825
  const stop = {};
34640
34826
  const flush = async () => {
34641
- await Promise.all([browserPrep, baselinePrep]);
34827
+ await browserReady();
34642
34828
  if (session.applied) {
34643
34829
  const undone = revertChange({ projectRoot: opts.projectRoot, changeId: session.id });
34644
34830
  if (undone.status === "conflict")
@@ -34702,21 +34888,20 @@ ${detail}` : "."}`,
34702
34888
  });
34703
34889
  }
34704
34890
  if (final.status !== "applied") return landingFailed(session, final);
34705
- const [browser, baseline] = await Promise.all([browserPrep, baselinePrep]);
34706
- await checkAndReport(session, final, browser, baseline, { summary: done.summary });
34891
+ await checkAndReport(session, final, await browserReady(), signal, { summary: done.summary });
34707
34892
  }
34708
34893
  async function addDependency(rt, pkg, version2) {
34709
34894
  const pm = opts.config.packageManager;
34710
34895
  const files = ["package.json", LOCKFILE[pm]];
34711
34896
  const disk = (rel) => {
34712
34897
  try {
34713
- return readFileSync8(path24.join(opts.projectRoot, rel), "utf8");
34898
+ return readFileSync8(path25.join(opts.projectRoot, rel), "utf8");
34714
34899
  } catch {
34715
34900
  return null;
34716
34901
  }
34717
34902
  };
34718
34903
  const put = (rel, content) => {
34719
- const file2 = path24.join(opts.projectRoot, rel);
34904
+ const file2 = path25.join(opts.projectRoot, rel);
34720
34905
  if (content === null) rmSync2(file2, { force: true });
34721
34906
  else writeFileSync4(file2, content);
34722
34907
  };
@@ -34928,6 +35113,11 @@ ${r.output.slice(-3e3)}`,
34928
35113
  },
34929
35114
  revert(changeId) {
34930
35115
  const s = sessionOf(changeId);
35116
+ if (s.busy && s.stage === "checking") {
35117
+ s.abort?.abort();
35118
+ s.abort = void 0;
35119
+ s.busy = false;
35120
+ }
34931
35121
  run(s, async () => {
34932
35122
  s.stage = "reverting";
34933
35123
  const files = restore(s);
@@ -34994,8 +35184,8 @@ var ApiError = class extends Error {
34994
35184
  code;
34995
35185
  };
34996
35186
  function createApiClient({ apiUrl, token, fetch: f = fetch }) {
34997
- async function request(method, path30, body) {
34998
- const res = await f(`${apiUrl}${path30}`, {
35187
+ async function request(method, path31, body) {
35188
+ const res = await f(`${apiUrl}${path31}`, {
34999
35189
  method,
35000
35190
  headers: {
35001
35191
  "content-type": "application/json",
@@ -35011,8 +35201,8 @@ function createApiClient({ apiUrl, token, fetch: f = fetch }) {
35011
35201
  return data;
35012
35202
  }
35013
35203
  return {
35014
- get: (path30) => request("GET", path30),
35015
- post: (path30, body) => request("POST", path30, body ?? {})
35204
+ get: (path31) => request("GET", path31),
35205
+ post: (path31, body) => request("POST", path31, body ?? {})
35016
35206
  };
35017
35207
  }
35018
35208
 
@@ -35021,28 +35211,28 @@ import { spawn as spawn2 } from "child_process";
35021
35211
  import { hostname as hostname3 } from "os";
35022
35212
 
35023
35213
  // src/credentials.ts
35024
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
35214
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
35025
35215
  import { homedir } from "os";
35026
- import path25 from "path";
35216
+ import path26 from "path";
35027
35217
  function credentialsPath(env = process.env) {
35028
- const base = env.XDG_CONFIG_HOME || path25.join(homedir(), ".config");
35029
- return path25.join(base, "revyse", "credentials.json");
35218
+ const base = env.XDG_CONFIG_HOME || path26.join(homedir(), ".config");
35219
+ return path26.join(base, "revyse", "credentials.json");
35030
35220
  }
35031
35221
  function readAll(file2) {
35032
- if (!existsSync4(file2)) return {};
35222
+ if (!existsSync5(file2)) return {};
35033
35223
  return JSON.parse(readFileSync9(file2, "utf8"));
35034
35224
  }
35035
35225
  function writeAll(file2, data) {
35036
- mkdirSync3(path25.dirname(file2), { recursive: true, mode: 448 });
35226
+ mkdirSync3(path26.dirname(file2), { recursive: true, mode: 448 });
35037
35227
  writeFileSync5(file2, `${JSON.stringify(data, null, 2)}
35038
35228
  `, { mode: 384 });
35039
35229
  chmodSync(file2, 384);
35040
35230
  }
35041
35231
  function loadCredential(projectRoot, file2 = credentialsPath()) {
35042
- return readAll(file2)[path25.resolve(projectRoot)] ?? null;
35232
+ return readAll(file2)[path26.resolve(projectRoot)] ?? null;
35043
35233
  }
35044
35234
  function saveCredential(projectRoot, cred, file2 = credentialsPath()) {
35045
- writeAll(file2, { ...readAll(file2), [path25.resolve(projectRoot)]: cred });
35235
+ writeAll(file2, { ...readAll(file2), [path26.resolve(projectRoot)]: cred });
35046
35236
  }
35047
35237
 
35048
35238
  // src/device-login.ts
@@ -35079,12 +35269,12 @@ async function deviceLogin({
35079
35269
  }
35080
35270
 
35081
35271
  // src/init-flow.ts
35082
- import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35083
- import path28 from "path";
35272
+ import { existsSync as existsSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35273
+ import path29 from "path";
35084
35274
 
35085
35275
  // src/detect.ts
35086
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
35087
- import path26 from "path";
35276
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
35277
+ import path27 from "path";
35088
35278
  var LOCKFILES2 = [
35089
35279
  ["pnpm-lock.yaml", "pnpm"],
35090
35280
  ["yarn.lock", "yarn"],
@@ -35094,8 +35284,8 @@ var LOCKFILES2 = [
35094
35284
  ];
35095
35285
  var SOURCE_DIR_CANDIDATES = ["src", "app", "pages", "components", "lib"];
35096
35286
  function detectProject(root) {
35097
- const pkgPath = path26.join(root, "package.json");
35098
- if (!existsSync5(pkgPath)) {
35287
+ const pkgPath = path27.join(root, "package.json");
35288
+ if (!existsSync6(pkgPath)) {
35099
35289
  throw new Error(`No package.json found in ${root}. Run revyse init from your project root.`);
35100
35290
  }
35101
35291
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
@@ -35107,13 +35297,13 @@ function detectProject(root) {
35107
35297
  } else if (framework !== "nextjs") {
35108
35298
  warnings.push(`${framework} support is experimental; Next.js is the supported target.`);
35109
35299
  }
35110
- const managers = LOCKFILES2.filter(([file2]) => existsSync5(path26.join(root, file2)));
35300
+ const managers = LOCKFILES2.filter(([file2]) => existsSync6(path27.join(root, file2)));
35111
35301
  const packageManager = managers[0]?.[1] ?? "npm";
35112
35302
  if (managers.length === 0) warnings.push("No lockfile found; defaulting to npm.");
35113
35303
  if (new Set(managers.map(([, m]) => m)).size > 1) {
35114
35304
  warnings.push(`Multiple lockfiles found; using ${packageManager}.`);
35115
35305
  }
35116
- const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync5(path26.join(root, d)));
35306
+ const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync6(path27.join(root, d)));
35117
35307
  for (const d of aliasedDirs(root)) if (!sourceDirs.includes(d)) sourceDirs.push(d);
35118
35308
  if (sourceDirs.length === 0) {
35119
35309
  warnings.push("No source directories found; edit sourceDirs in revyse.config.json.");
@@ -35131,14 +35321,16 @@ function detectProject(root) {
35131
35321
  dev: pick2("dev"),
35132
35322
  typecheck: pick2("typecheck", "type-check", "tsc"),
35133
35323
  lint: pick2("lint"),
35134
- test: pick2("test"),
35324
+ // Tests are opt-in (L19, ADR 0042): a suite can take minutes, and a local change should
35325
+ // take seconds. The build stays: it only ever runs in cloud workspaces.
35135
35326
  build: pick2("build")
35136
35327
  },
35137
35328
  exclude: [],
35138
35329
  agentPort: 4545,
35139
35330
  appOrigins: devOrigins(scripts.dev)
35140
35331
  },
35141
- warnings
35332
+ warnings,
35333
+ optional: { test: pick2("test") }
35142
35334
  };
35143
35335
  }
35144
35336
  function detectFramework(deps) {
@@ -35186,13 +35378,13 @@ function hasSource(dir, depth = 3) {
35186
35378
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
35187
35379
  if (entry.isFile() && SOURCE_FILE.test(entry.name)) return true;
35188
35380
  if (entry.isDirectory() && depth > 1 && !NOT_SOURCE.has(entry.name)) {
35189
- if (hasSource(path26.join(dir, entry.name), depth - 1)) return true;
35381
+ if (hasSource(path27.join(dir, entry.name), depth - 1)) return true;
35190
35382
  }
35191
35383
  }
35192
35384
  return false;
35193
35385
  }
35194
35386
  function aliasedDirs(root) {
35195
- const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path26.join(root, f)).find((f) => existsSync5(f));
35387
+ const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path27.join(root, f)).find((f) => existsSync6(f));
35196
35388
  if (!file2) return [];
35197
35389
  let compilerOptions;
35198
35390
  try {
@@ -35204,15 +35396,15 @@ function aliasedDirs(root) {
35204
35396
  const baseUrl = compilerOptions?.baseUrl ?? ".";
35205
35397
  const dirs = /* @__PURE__ */ new Set();
35206
35398
  for (const target of Object.values(paths).flat()) {
35207
- const rel = path26.relative(root, path26.join(root, baseUrl, target)).split(path26.sep).join("/");
35399
+ const rel = path27.relative(root, path27.join(root, baseUrl, target)).split(path27.sep).join("/");
35208
35400
  const top = rel.split("/")[0];
35209
35401
  if (rel.startsWith("..") || NOT_SOURCE.has(top)) continue;
35210
35402
  if (top === "*" || top === "") {
35211
35403
  for (const entry of readdirSync4(root, { withFileTypes: true }))
35212
35404
  if (entry.isDirectory() && !NOT_SOURCE.has(entry.name) && !entry.name.startsWith(".")) {
35213
- if (hasSource(path26.join(root, entry.name))) dirs.add(entry.name);
35405
+ if (hasSource(path27.join(root, entry.name))) dirs.add(entry.name);
35214
35406
  }
35215
- } else if (existsSync5(path26.join(root, top)) && statSync6(path26.join(root, top)).isDirectory()) {
35407
+ } else if (existsSync6(path27.join(root, top)) && statSync6(path27.join(root, top)).isDirectory()) {
35216
35408
  dirs.add(top);
35217
35409
  }
35218
35410
  }
@@ -35254,8 +35446,8 @@ Full policy: ${DATA_HANDLING_URL}
35254
35446
  `;
35255
35447
 
35256
35448
  // src/next-config.ts
35257
- import { existsSync as existsSync6, readFileSync as readFileSync11 } from "fs";
35258
- import path27 from "path";
35449
+ import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
35450
+ import path28 from "path";
35259
35451
 
35260
35452
  // ../../node_modules/.pnpm/@babel+parser@8.0.7/node_modules/@babel/parser/lib/index.js
35261
35453
  var Position = class {
@@ -51013,7 +51205,7 @@ var NEXT_CONFIG_FILES = [
51013
51205
  ];
51014
51206
  function findNextConfig(root) {
51015
51207
  for (const name of NEXT_CONFIG_FILES) {
51016
- if (existsSync6(path27.join(root, name))) return name;
51208
+ if (existsSync7(path28.join(root, name))) return name;
51017
51209
  }
51018
51210
  return null;
51019
51211
  }
@@ -51068,7 +51260,7 @@ ${importLine}`);
51068
51260
  return { status: "unsupported", reason: "it has no `export default` or `module.exports`" };
51069
51261
  }
51070
51262
  function readNextConfig(root, file2) {
51071
- return readFileSync11(path27.join(root, file2), "utf8");
51263
+ return readFileSync11(path28.join(root, file2), "utf8");
51072
51264
  }
51073
51265
 
51074
51266
  // src/version.ts
@@ -51095,9 +51287,9 @@ async function runInit(opts, io) {
51095
51287
  const ask = (q) => opts.yes ? Promise.resolve(true) : io.confirm(q);
51096
51288
  let configured;
51097
51289
  let status;
51098
- if (existsSync7(path28.join(root, CONFIG_FILE)) && !opts.force) {
51290
+ if (existsSync8(path29.join(root, CONFIG_FILE)) && !opts.force) {
51099
51291
  const config3 = projectConfigSchema.parse(
51100
- JSON.parse(readFileSync12(path28.join(root, CONFIG_FILE), "utf8"))
51292
+ JSON.parse(readFileSync12(path29.join(root, CONFIG_FILE), "utf8"))
51101
51293
  );
51102
51294
  io.log(`${CONFIG_FILE} is already here; checking the rest of the setup.`);
51103
51295
  if (!config3.dataDisclosure || config3.dataDisclosure.version < DISCLOSURE_VERSION)
@@ -51134,7 +51326,7 @@ Next: ${steps.join(", then ")}. Then start your app as usual.`
51134
51326
  async function configure(opts, io, ask) {
51135
51327
  const { root } = opts;
51136
51328
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
51137
- const { config: config2, warnings } = detectProject(root);
51329
+ const { config: config2, warnings, optional: optional2 } = detectProject(root);
51138
51330
  io.log(`Detected ${config2.framework} (${config2.packageManager})`);
51139
51331
  io.log(`Source directories: ${config2.sourceDirs.join(", ")}`);
51140
51332
  for (const w of warnings) io.warn(`warning: ${w}`);
@@ -51178,27 +51370,68 @@ Revyse needs this change to ${file2} (it only has an effect in \`next dev\`):
51178
51370
  }
51179
51371
  }
51180
51372
  }
51181
- writeFileSync6(path28.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
51373
+ writeFileSync6(path29.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
51182
51374
  `);
51183
- if (nextConfigEdit) writeFileSync6(path28.join(root, nextConfigEdit.file), nextConfigEdit.code);
51375
+ if (nextConfigEdit) writeFileSync6(path29.join(root, nextConfigEdit.file), nextConfigEdit.code);
51184
51376
  if (ensureGitignored(root))
51185
51377
  io.log(`Added ${GITIGNORE_ENTRY} to .gitignore (local change history).`);
51186
51378
  io.log(`
51187
51379
  Wrote ${CONFIG_FILE}.`);
51380
+ const checks = [
51381
+ ...config2.scripts.typecheck ? ["typecheck"] : [],
51382
+ ...config2.scripts.lint ? ["lint"] : [],
51383
+ ...config2.framework === "nextjs" ? ["the page in a browser"] : []
51384
+ ];
51385
+ if (checks.length > 0)
51386
+ io.log(
51387
+ `After each change, Revyse checks ${checks.join(", ").replace(/, ([^,]*)$/, " and $1")}.`
51388
+ );
51389
+ if (optional2.test)
51390
+ io.log(
51391
+ `Your tests (\`${optional2.test}\`) don't run on each change, so changes stay fast. To run them too, add "test": "${optional2.test}" to scripts in ${CONFIG_FILE}.`
51392
+ );
51188
51393
  return { config: accepted, nextConfigEdited: Boolean(nextConfigEdit) };
51189
51394
  }
51395
+ function olderThan(a2, b) {
51396
+ const parse4 = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number);
51397
+ const [x, y] = [parse4(a2), parse4(b)];
51398
+ if (!x || !y) return false;
51399
+ for (let i2 = 0; i2 < 3; i2++) if (x[i2] !== y[i2]) return x[i2] < y[i2];
51400
+ return false;
51401
+ }
51402
+ function installedVersion(root, name) {
51403
+ try {
51404
+ const pkg = JSON.parse(
51405
+ readFileSync12(path29.join(root, "node_modules", name, "package.json"), "utf8")
51406
+ );
51407
+ return pkg.version ?? null;
51408
+ } catch {
51409
+ return null;
51410
+ }
51411
+ }
51190
51412
  async function installPackages(root, config2, setup2, io, ask) {
51191
- const missing = PACKAGES.filter((name) => !devDependencies(root)[name]);
51192
- if (missing.length === 0) return true;
51193
- const specs = missing.map((name) => `${name}@${VERSION}`);
51413
+ const listed2 = devDependencies(root);
51414
+ const todo = PACKAGES.flatMap((name) => {
51415
+ const installed = installedVersion(root, name);
51416
+ if (!listed2[name] || !installed) return [{ name, from: null }];
51417
+ return olderThan(installed, VERSION) ? [{ name, from: installed }] : [];
51418
+ });
51419
+ if (todo.length === 0) return true;
51420
+ const specs = todo.map(({ name }) => `${name}@${VERSION}`);
51194
51421
  const command = installCommand(config2.packageManager, specs).join(" ");
51195
- if (!await ask(`Install ${missing.join(" and ")} as dev dependencies (${command})?`)) {
51196
- io.log(`Skipped. Install them yourself: ${command}`);
51422
+ const installs = todo.filter((t) => !t.from).map((t) => t.name);
51423
+ const updates = todo.filter((t) => t.from).map((t) => `${t.name} from ${t.from} to ${VERSION}`);
51424
+ const what = [
51425
+ ...installs.length ? [`install ${installs.join(" and ")} as dev dependencies`] : [],
51426
+ ...updates.length ? [`update ${updates.join(" and ")}`] : []
51427
+ ].join(", and ");
51428
+ if (!await ask(`${what[0].toUpperCase()}${what.slice(1)} (${command})?`)) {
51429
+ io.log(`Skipped. Run it yourself: ${command}`);
51197
51430
  return false;
51198
51431
  }
51199
51432
  try {
51200
51433
  await setup2.install(root, config2.packageManager, specs);
51201
- io.log(`Installed ${missing.join(" and ")}.`);
51434
+ io.log(`Revyse's packages are at ${VERSION}.`);
51202
51435
  return true;
51203
51436
  } catch (err) {
51204
51437
  io.warn(`Couldn't install them (${err.message}). Run: ${command}`);
@@ -51230,11 +51463,11 @@ async function connectProject(root, config2, setup2, io, ask) {
51230
51463
  }
51231
51464
  function projectName(root) {
51232
51465
  const name = readPackageJson(root)?.name?.replace(/^@[^/]+\//, "").trim();
51233
- return (name || path28.basename(root)).slice(0, 100);
51466
+ return (name || path29.basename(root)).slice(0, 100);
51234
51467
  }
51235
51468
  function ensureGitignored(root) {
51236
- const file2 = path28.join(root, ".gitignore");
51237
- const current = existsSync7(file2) ? readFileSync12(file2, "utf8") : "";
51469
+ const file2 = path29.join(root, ".gitignore");
51470
+ const current = existsSync8(file2) ? readFileSync12(file2, "utf8") : "";
51238
51471
  const lines = current.split(/\r?\n/).map((l) => l.trim());
51239
51472
  if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".revyse")) return false;
51240
51473
  const separator = current === "" ? "" : current.endsWith("\n") ? "\n" : "\n\n";
@@ -51245,7 +51478,7 @@ ${GITIGNORE_ENTRY}
51245
51478
  }
51246
51479
  function readPackageJson(root) {
51247
51480
  try {
51248
- return JSON.parse(readFileSync12(path28.join(root, "package.json"), "utf8"));
51481
+ return JSON.parse(readFileSync12(path29.join(root, "package.json"), "utf8"));
51249
51482
  } catch {
51250
51483
  return null;
51251
51484
  }
@@ -51380,7 +51613,7 @@ var common2 = (cmd) => cmd.requiredOption("--api-url <url>", "Revyse API URL").r
51380
51613
  function setup(opts) {
51381
51614
  const root = process.cwd();
51382
51615
  const config2 = projectConfigSchema.parse(
51383
- JSON.parse(readFileSync13(path29.join(root, CONFIG_FILE), "utf8"))
51616
+ JSON.parse(readFileSync13(path30.join(root, CONFIG_FILE), "utf8"))
51384
51617
  );
51385
51618
  const token = opts.token ?? process.env.REVYSE_TOKEN;
51386
51619
  if (!token) throw new Error("A project token is required (--token or REVYSE_TOKEN)");