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.
@@ -6,8 +6,8 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/agent/local-agent.ts
9
- import { existsSync as existsSync8, readFileSync as readFileSync12, watch } from "fs";
10
- import path20 from "path";
9
+ import { existsSync as existsSync9, readFileSync as readFileSync12, watch } from "fs";
10
+ import path21 from "path";
11
11
  import { fileURLToPath } from "url";
12
12
 
13
13
  // ../agent-core/src/path-policy.ts
@@ -925,10 +925,10 @@ function mergeDefs(...defs) {
925
925
  function cloneDef(schema) {
926
926
  return mergeDefs(schema._zod.def);
927
927
  }
928
- function getElementAtPath(obj, path21) {
929
- if (!path21)
928
+ function getElementAtPath(obj, path22) {
929
+ if (!path22)
930
930
  return obj;
931
- return path21.reduce((acc, key) => acc?.[key], obj);
931
+ return path22.reduce((acc, key) => acc?.[key], obj);
932
932
  }
933
933
  function promiseAllObject(promisesObj) {
934
934
  const keys = Object.keys(promisesObj);
@@ -1268,11 +1268,11 @@ function explicitlyAborted(x, startIndex = 0) {
1268
1268
  }
1269
1269
  return false;
1270
1270
  }
1271
- function prefixIssues(path21, issues) {
1271
+ function prefixIssues(path22, issues) {
1272
1272
  return issues.map((iss) => {
1273
1273
  var _a3;
1274
1274
  (_a3 = iss).path ?? (_a3.path = []);
1275
- iss.path.unshift(path21);
1275
+ iss.path.unshift(path22);
1276
1276
  return iss;
1277
1277
  });
1278
1278
  }
@@ -1734,16 +1734,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
1734
1734
  }
1735
1735
  function formatError(error62, mapper = (issue2) => issue2.message) {
1736
1736
  const fieldErrors = { _errors: [] };
1737
- const processError = (error63, path21 = []) => {
1737
+ const processError = (error63, path22 = []) => {
1738
1738
  for (const issue2 of error63.issues) {
1739
1739
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1740
- issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
1740
+ issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
1741
1741
  } else if (issue2.code === "invalid_key") {
1742
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1742
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1743
1743
  } else if (issue2.code === "invalid_element") {
1744
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1744
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1745
1745
  } else {
1746
- const fullpath = [...path21, ...issue2.path];
1746
+ const fullpath = [...path22, ...issue2.path];
1747
1747
  if (fullpath.length === 0) {
1748
1748
  fieldErrors._errors.push(mapper(issue2));
1749
1749
  } else {
@@ -1782,17 +1782,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
1782
1782
  }
1783
1783
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
1784
1784
  const result = { errors: [] };
1785
- const processError = (error63, path21 = []) => {
1785
+ const processError = (error63, path22 = []) => {
1786
1786
  var _a3;
1787
1787
  for (const issue2 of error63.issues) {
1788
1788
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1789
- issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
1789
+ issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
1790
1790
  } else if (issue2.code === "invalid_key") {
1791
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1791
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1792
1792
  } else if (issue2.code === "invalid_element") {
1793
- processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
1793
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
1794
1794
  } else {
1795
- const fullpath = [...path21, ...issue2.path];
1795
+ const fullpath = [...path22, ...issue2.path];
1796
1796
  if (fullpath.length === 0) {
1797
1797
  result.errors.push(mapper(issue2));
1798
1798
  continue;
@@ -1831,8 +1831,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
1831
1831
  }
1832
1832
  function toDotPath(_path) {
1833
1833
  const segs = [];
1834
- const path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1835
- for (const seg of path21) {
1834
+ const path22 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1835
+ for (const seg of path22) {
1836
1836
  if (typeof seg === "number")
1837
1837
  segs.push(`[${seg}]`);
1838
1838
  else if (typeof seg === "symbol")
@@ -18970,13 +18970,13 @@ function resolveRef(ref, ctx) {
18970
18970
  if (!ref.startsWith("#")) {
18971
18971
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
18972
18972
  }
18973
- const path21 = ref.slice(1).split("/").filter(Boolean);
18974
- if (path21.length === 0) {
18973
+ const path22 = ref.slice(1).split("/").filter(Boolean);
18974
+ if (path22.length === 0) {
18975
18975
  return ctx.rootSchema;
18976
18976
  }
18977
18977
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18978
- if (path21[0] === defsKey) {
18979
- const key = path21[1] === void 0 ? void 0 : decodeJSONPointerSegment(path21[1]);
18978
+ if (path22[0] === defsKey) {
18979
+ const key = path22[1] === void 0 ? void 0 : decodeJSONPointerSegment(path22[1]);
18980
18980
  if (!key || !ctx.defs[key]) {
18981
18981
  throw new Error(`Reference not found: ${ref}`);
18982
18982
  }
@@ -20280,6 +20280,14 @@ var agentEventSchema = external_exports.object({
20280
20280
  event: external_exports.string(),
20281
20281
  data: external_exports.unknown().optional()
20282
20282
  });
20283
+ var changedFileSchema = external_exports.object({
20284
+ path: external_exports.string(),
20285
+ additions: external_exports.number().int(),
20286
+ deletions: external_exports.number().int(),
20287
+ explanation: external_exports.string().optional(),
20288
+ /** Agent engine: whether the file was created, modified or deleted. */
20289
+ kind: external_exports.enum(["create", "modify", "delete"]).optional()
20290
+ });
20283
20291
  var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
20284
20292
  external_exports.object({ type: external_exports.literal("planning"), changeId: external_exports.uuid() }),
20285
20293
  external_exports.object({
@@ -20287,6 +20295,8 @@ var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
20287
20295
  changeId: external_exports.uuid(),
20288
20296
  plan: changePlanSchema,
20289
20297
  model: external_exports.string(),
20298
+ /** Models tried first that couldn't run (A7, A11): the plan says it ran on another. */
20299
+ fellBackFrom: external_exports.array(external_exports.string()).optional(),
20290
20300
  /** Credits the change has cost so far (B1). */
20291
20301
  cost: external_exports.number().nonnegative().optional()
20292
20302
  }),
@@ -20306,22 +20316,23 @@ var agentChangeEventSchema = external_exports.discriminatedUnion("type", [
20306
20316
  external_exports.object({
20307
20317
  type: external_exports.literal("checking"),
20308
20318
  changeId: external_exports.uuid(),
20309
- checks: external_exports.array(external_exports.object({ kind: checkKindSchema, status: checkStatusSchema }))
20319
+ checks: external_exports.array(
20320
+ external_exports.object({
20321
+ kind: checkKindSchema,
20322
+ status: checkStatusSchema,
20323
+ /** How long a finished check took (L16). */
20324
+ durationMs: external_exports.number().int().nonnegative().optional()
20325
+ })
20326
+ ),
20327
+ /** The change as written, shown while its checks run (L17, ADR 0042). */
20328
+ files: external_exports.array(changedFileSchema).optional(),
20329
+ diff: external_exports.string().optional()
20310
20330
  }),
20311
20331
  external_exports.object({
20312
20332
  type: external_exports.literal("result"),
20313
20333
  changeId: external_exports.uuid(),
20314
20334
  state: changeStateSchema,
20315
- files: external_exports.array(
20316
- external_exports.object({
20317
- path: external_exports.string(),
20318
- additions: external_exports.number().int(),
20319
- deletions: external_exports.number().int(),
20320
- explanation: external_exports.string().optional(),
20321
- /** Agent engine: whether the file was created, modified or deleted. */
20322
- kind: external_exports.enum(["create", "modify", "delete"]).optional()
20323
- })
20324
- ),
20335
+ files: external_exports.array(changedFileSchema),
20325
20336
  diff: external_exports.string(),
20326
20337
  checks: external_exports.array(checkResultSchema),
20327
20338
  /** Credits the change has cost so far (B1). */
@@ -21778,6 +21789,71 @@ ${summary}` : summary
21778
21789
  // ../agent-core/src/checks/browser.ts
21779
21790
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
21780
21791
  import path11 from "path";
21792
+ var LOC_ATTRIBUTE = "data-revyse-loc";
21793
+ var FRAME_PADDING = 24;
21794
+ var FRAME_MIN_WIDTH = 480;
21795
+ var FRAME_MAX_RATIO = 2;
21796
+ function frameAround(box, viewport) {
21797
+ const width = Math.min(Math.max(box.width + 2 * FRAME_PADDING, FRAME_MIN_WIDTH), viewport.width);
21798
+ const height = Math.min(
21799
+ Math.max(box.height + 2 * FRAME_PADDING, width / FRAME_MAX_RATIO),
21800
+ viewport.height
21801
+ );
21802
+ const place = (start, size, frame, limit) => Math.round(Math.min(Math.max(start + size / 2 - frame / 2, 0), limit - frame));
21803
+ return {
21804
+ x: place(box.x, box.width, width, viewport.width),
21805
+ y: place(box.y, box.height, height, viewport.height),
21806
+ width: Math.round(width),
21807
+ height: Math.round(height)
21808
+ };
21809
+ }
21810
+ function nearestLoc(locs, source) {
21811
+ const distance = (loc) => {
21812
+ const [line, column] = loc.split(":").slice(-2).map(Number);
21813
+ if (!Number.isFinite(line) || source.line === void 0) return Infinity;
21814
+ return Math.abs(line - source.line) * 1e4 + Math.abs((column ?? 0) - (source.column ?? 0));
21815
+ };
21816
+ let best = 0;
21817
+ for (let i = 1; i < locs.length; i++) if (distance(locs[i]) < distance(locs[best])) best = i;
21818
+ return best;
21819
+ }
21820
+ async function findSelected(page, selector, source) {
21821
+ const bySelector = page.locator(selector).first();
21822
+ const selectorFound = await page.locator(selector).count().catch(() => 0) > 0;
21823
+ if (source?.line) {
21824
+ const exact = source.column === void 0 ? `${source.file}:${source.line}:` : `${source.file}:${source.line}:${source.column}`;
21825
+ const isExact = (loc) => source.column === void 0 ? !!loc?.startsWith(exact) : loc === exact;
21826
+ if (selectorFound) {
21827
+ const loc = await bySelector.evaluate((el, attr) => el.closest(`[${attr}]`)?.getAttribute(attr) ?? null, LOC_ATTRIBUTE).catch(() => null);
21828
+ if (isExact(loc)) return bySelector;
21829
+ }
21830
+ const op = source.column === void 0 ? "^=" : "=";
21831
+ const byLoc = page.locator(`[${LOC_ATTRIBUTE}${op}${JSON.stringify(exact)}]`);
21832
+ if (await byLoc.count().catch(() => 0) > 0) return byLoc.first();
21833
+ }
21834
+ if (selectorFound) return bySelector;
21835
+ if (!source) return null;
21836
+ const fromFile = page.locator(`[${LOC_ATTRIBUTE}^=${JSON.stringify(`${source.file}:`)}]`);
21837
+ const locs = await fromFile.evaluateAll((els, attr) => els.map((el) => el.getAttribute(attr) ?? ""), LOC_ATTRIBUTE).catch(() => []);
21838
+ return locs.length > 0 ? fromFile.nth(nearestLoc(locs, source)) : null;
21839
+ }
21840
+ async function frameSelected(page, target) {
21841
+ await target.scrollIntoViewIfNeeded({ timeout: 2e3 }).catch(() => {
21842
+ });
21843
+ await page.evaluate(
21844
+ () => Promise.race([
21845
+ Promise.all(
21846
+ Array.from(document.images).filter((img) => !img.complete).map((img) => new Promise((done) => img.addEventListener("loadend", done)))
21847
+ ),
21848
+ new Promise((done) => setTimeout(done, 2e3))
21849
+ ])
21850
+ ).catch(() => {
21851
+ });
21852
+ const box = await target.boundingBox().catch(() => null);
21853
+ const viewport = page.viewportSize();
21854
+ if (!box || !viewport || box.width <= 0 || box.height <= 0) return null;
21855
+ return frameAround(box, viewport);
21856
+ }
21781
21857
  var BrowserUnavailableError = class extends Error {
21782
21858
  };
21783
21859
  var MAX_ERRORS = 10;
@@ -21805,7 +21881,7 @@ async function createPlaywrightDriver(env = process.env) {
21805
21881
  if (!browser) throw new BrowserUnavailableError(`No Chromium could be started. ${NOT_RUN_HINT}`);
21806
21882
  const b = browser;
21807
21883
  return {
21808
- async capture({ url: url2, selector, sourceFile, timeoutMs }) {
21884
+ async capture({ url: url2, selector, source, timeoutMs }) {
21809
21885
  const context = await b.newContext({ viewport: { width: 1280, height: 800 } });
21810
21886
  const page = await context.newPage();
21811
21887
  const errors = [];
@@ -21825,9 +21901,15 @@ async function createPlaywrightDriver(env = process.env) {
21825
21901
  let found = null;
21826
21902
  if (await page.locator(selector).count().catch(() => 0) > 0)
21827
21903
  found = "selector";
21828
- else if (sourceFile && await page.locator(`[data-revyse-loc^="${sourceFile}:"]`).count() > 0)
21904
+ else if (source && await page.locator(`[${LOC_ATTRIBUTE}^=${JSON.stringify(`${source.file}:`)}]`).count() > 0)
21829
21905
  found = "source";
21830
- const screenshot = await page.screenshot({ type: "png" });
21906
+ const target = await findSelected(page, selector, source);
21907
+ const clip = target ? await frameSelected(page, target) : null;
21908
+ const screenshot = await page.screenshot({
21909
+ type: "png",
21910
+ animations: "disabled",
21911
+ ...clip ? { clip } : {}
21912
+ });
21831
21913
  return { status: res?.status() ?? null, found, screenshot, errors };
21832
21914
  } finally {
21833
21915
  await context.close();
@@ -21942,7 +22024,7 @@ function createAgentApi(opts) {
21942
22024
  const f = opts.fetch ?? fetch;
21943
22025
  const base = opts.apiUrl.replace(/\/+$/, "");
21944
22026
  const auth = { authorization: `Bearer ${opts.token}` };
21945
- const call = (path21, body, signal) => f(`${base}${path21}`, {
22027
+ const call = (path22, body, signal) => f(`${base}${path22}`, {
21946
22028
  method: "POST",
21947
22029
  headers: { "content-type": "application/json", ...auth },
21948
22030
  body: JSON.stringify(body),
@@ -21958,18 +22040,18 @@ function createAgentApi(opts) {
21958
22040
  }
21959
22041
  return {
21960
22042
  baseUrl: base,
21961
- async get(path21) {
21962
- const res = await f(`${base}${path21}`, { headers: auth });
22043
+ async get(path22) {
22044
+ const res = await f(`${base}${path22}`, { headers: auth });
21963
22045
  if (!res.ok) throw await failure2(res);
21964
22046
  return await res.json();
21965
22047
  },
21966
- async post(path21, body) {
21967
- const res = await call(path21, body);
22048
+ async post(path22, body) {
22049
+ const res = await call(path22, body);
21968
22050
  if (!res.ok) throw await failure2(res);
21969
22051
  return await res.json();
21970
22052
  },
21971
- async *stream(path21, body, signal) {
21972
- const res = await call(path21, body, signal);
22053
+ async *stream(path22, body, signal) {
22054
+ const res = await call(path22, body, signal);
21973
22055
  if (!res.ok) throw await failure2(res);
21974
22056
  if (!res.body) return;
21975
22057
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
@@ -22002,11 +22084,51 @@ function parseBlock(block) {
22002
22084
 
22003
22085
  // ../agent-core/src/change/runner.ts
22004
22086
  import { readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
22005
- import path14 from "path";
22087
+ import path15 from "path";
22088
+
22089
+ // ../agent-core/src/patch/originals.ts
22090
+ import { existsSync as existsSync4 } from "fs";
22091
+ import path12 from "path";
22092
+ async function withOriginals(opts) {
22093
+ const io = opts.io ?? nodeIO;
22094
+ const dir = changeDir(opts.projectRoot, opts.changeId);
22095
+ const manifest = JSON.parse(io.readFile(path12.join(dir, "manifest.json")));
22096
+ const files = manifest.files.map((f) => ({
22097
+ ...f,
22098
+ real: resolveProjectPath(opts.projectRoot, f.path),
22099
+ pre: f.preHash === null ? null : io.readFile(path12.join(dir, "pre", f.path)),
22100
+ post: f.postHash === null ? null : io.readFile(path12.join(dir, "post", f.path))
22101
+ }));
22102
+ const hashOf = (real) => existsSync4(real) ? hashContent(io.readFile(real)) : null;
22103
+ const put = (real, content) => {
22104
+ if (content === null) io.unlink(real);
22105
+ else {
22106
+ io.mkdir(path12.dirname(real));
22107
+ io.writeFile(real, content);
22108
+ }
22109
+ };
22110
+ const changed = files.filter((f) => hashOf(f.real) !== f.postHash).map((f) => f.path);
22111
+ if (changed.length > 0) return { status: "changed", paths: changed };
22112
+ for (const f of files) put(f.real, f.pre);
22113
+ const putBack = () => {
22114
+ const kept2 = files.filter((f) => hashOf(f.real) !== f.preHash).map((f) => f.path);
22115
+ for (const f of files) if (!kept2.includes(f.path)) put(f.real, f.post);
22116
+ return kept2;
22117
+ };
22118
+ let value;
22119
+ try {
22120
+ value = await opts.work();
22121
+ } catch (err) {
22122
+ putBack();
22123
+ throw err;
22124
+ }
22125
+ const kept = putBack();
22126
+ return kept.length > 0 ? { status: "kept-edit", value, paths: kept } : { status: "done", value };
22127
+ }
22006
22128
 
22007
22129
  // ../agent-core/src/tools/file-host.ts
22008
22130
  import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
22009
- import path12 from "path";
22131
+ import path13 from "path";
22010
22132
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([
22011
22133
  "node_modules",
22012
22134
  ".git",
@@ -22022,7 +22144,7 @@ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
22022
22144
  ]);
22023
22145
  var MAX_HOST_FILE_BYTES = 1e6;
22024
22146
  var MAX_LISTED = 2e4;
22025
- var toPosix2 = (p) => p.split(path12.sep).join("/");
22147
+ var toPosix2 = (p) => p.split(path13.sep).join("/");
22026
22148
  function localFileHost(projectRoot) {
22027
22149
  return {
22028
22150
  async read(relativePath) {
@@ -22050,12 +22172,12 @@ function localFileHost(projectRoot) {
22050
22172
  if (out.length >= MAX_LISTED) return;
22051
22173
  let entries;
22052
22174
  try {
22053
- entries = readdirSync3(path12.join(projectRoot, dir), { withFileTypes: true });
22175
+ entries = readdirSync3(path13.join(projectRoot, dir), { withFileTypes: true });
22054
22176
  } catch {
22055
22177
  return;
22056
22178
  }
22057
22179
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
22058
- const rel = toPosix2(path12.join(dir, e.name));
22180
+ const rel = toPosix2(path13.join(dir, e.name));
22059
22181
  if (e.isDirectory()) {
22060
22182
  if (!SKIPPED_DIRS.has(e.name) && !isSensitivePath(rel)) walk(rel);
22061
22183
  } else if (e.isFile() && !isSensitivePath(rel)) {
@@ -22071,7 +22193,7 @@ function localFileHost(projectRoot) {
22071
22193
  }
22072
22194
 
22073
22195
  // ../agent-core/src/tools/integrations.ts
22074
- import path13 from "path";
22196
+ import path14 from "path";
22075
22197
  var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
22076
22198
  var ENV_TEMPLATE = /^\.env\.(example|sample|template)$/;
22077
22199
  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;
@@ -22094,7 +22216,7 @@ async function projectSignals(read, paths) {
22094
22216
  for (const m of text.matchAll(ENV_IN_CODE)) env.add(m[1] ?? m[2] ?? m[3]);
22095
22217
  for (const m of text.matchAll(IMPORT)) imports.add(packageOf(m[1] ?? m[2]));
22096
22218
  }
22097
- for (const p of paths.filter((p2) => ENV_TEMPLATE.test(path13.posix.basename(p2)))) {
22219
+ for (const p of paths.filter((p2) => ENV_TEMPLATE.test(path14.posix.basename(p2)))) {
22098
22220
  for (const line of (await read(p) ?? "").split("\n")) {
22099
22221
  const name = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/)?.[1];
22100
22222
  if (name) env.add(name);
@@ -22154,27 +22276,27 @@ var StagedFiles = class {
22154
22276
  /** Paths written by the package manager (I7), not by the model: the write rules don't apply. */
22155
22277
  fromPackageManager = /* @__PURE__ */ new Set();
22156
22278
  /** Current content as the agent sees it: staged if written, else the project's. */
22157
- async read(path21) {
22158
- const s = this.staged.get(path21);
22279
+ async read(path22) {
22280
+ const s = this.staged.get(path22);
22159
22281
  if (s) return s.content;
22160
- const content = await this.host.read(path21);
22161
- if (!this.seen.has(path21)) this.seen.set(path21, content === null ? null : hashContent(content));
22282
+ const content = await this.host.read(path22);
22283
+ if (!this.seen.has(path22)) this.seen.set(path22, content === null ? null : hashContent(content));
22162
22284
  return content;
22163
22285
  }
22164
22286
  /** Like `read`, without recording a snapshot: for searching, which only looks. */
22165
- async peek(path21) {
22166
- const s = this.staged.get(path21);
22167
- return s ? s.content : this.host.read(path21);
22287
+ async peek(path22) {
22288
+ const s = this.staged.get(path22);
22289
+ return s ? s.content : this.host.read(path22);
22168
22290
  }
22169
22291
  /** Writes `content` (or deletes, with null) in the staging layer. */
22170
- async write(path21, content) {
22171
- const existing = this.staged.get(path21);
22292
+ async write(path22, content) {
22293
+ const existing = this.staged.get(path22);
22172
22294
  if (existing) {
22173
22295
  existing.content = content;
22174
22296
  return;
22175
22297
  }
22176
- const before = await this.read(path21);
22177
- this.staged.set(path21, {
22298
+ const before = await this.read(path22);
22299
+ this.staged.set(path22, {
22178
22300
  content,
22179
22301
  existed: before !== null,
22180
22302
  baseHash: before === null ? null : hashContent(before)
@@ -22185,9 +22307,9 @@ var StagedFiles = class {
22185
22307
  * a supported SDK). The patch engine applies it with the rest, so it lands, is checked and is
22186
22308
  * reverted together; the write rules that keep the model out of these files don't apply.
22187
22309
  */
22188
- async writeFromPackageManager(path21, content) {
22189
- await this.write(path21, content);
22190
- this.fromPackageManager.add(path21);
22310
+ async writeFromPackageManager(path22, content) {
22311
+ await this.write(path22, content);
22312
+ this.fromPackageManager.add(path22);
22191
22313
  }
22192
22314
  /** The paths the package manager wrote. */
22193
22315
  packageManagerPaths() {
@@ -22205,18 +22327,18 @@ var StagedFiles = class {
22205
22327
  /** What the apply phase changed, net: a file created then deleted is no change at all. */
22206
22328
  changes() {
22207
22329
  const out = [];
22208
- for (const [path21, s] of [...this.staged].sort(([a], [b]) => a.localeCompare(b))) {
22330
+ for (const [path22, s] of [...this.staged].sort(([a], [b]) => a.localeCompare(b))) {
22209
22331
  if (!s.existed && s.content === null) continue;
22210
- if (!s.existed) out.push({ path: path21, kind: "create", content: s.content });
22211
- else if (s.content === null) out.push({ path: path21, kind: "delete", baseHash: s.baseHash });
22332
+ if (!s.existed) out.push({ path: path22, kind: "create", content: s.content });
22333
+ else if (s.content === null) out.push({ path: path22, kind: "delete", baseHash: s.baseHash });
22212
22334
  else if (hashContent(s.content) !== s.baseHash)
22213
- out.push({ path: path21, kind: "modify", content: s.content, baseHash: s.baseHash });
22335
+ out.push({ path: path22, kind: "modify", content: s.content, baseHash: s.baseHash });
22214
22336
  }
22215
22337
  return out;
22216
22338
  }
22217
22339
  /** Every file the agent read or wrote, with its hash when first seen (null: didn't exist). */
22218
22340
  snapshots() {
22219
- return [...this.seen].map(([path21, hash2]) => ({ path: path21, hash: hash2 })).sort((a, b) => a.path.localeCompare(b.path));
22341
+ return [...this.seen].map(([path22, hash2]) => ({ path: path22, hash: hash2 })).sort((a, b) => a.path.localeCompare(b.path));
22220
22342
  }
22221
22343
  };
22222
22344
 
@@ -22253,7 +22375,7 @@ var ok = (callId, output2) => ({
22253
22375
  output: output2,
22254
22376
  isError: false
22255
22377
  });
22256
- var secretFile = (path21) => `Can't change ${path21}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
22378
+ var secretFile = (path22) => `Can't change ${path22}: it looks like it contains a secret. Leave it, and name it in notDone so the developer moves the secret to an environment variable.`;
22257
22379
  var fail = (callId, output2) => ({
22258
22380
  callId,
22259
22381
  output: output2,
@@ -22296,12 +22418,12 @@ function createToolRuntime(opts) {
22296
22418
  const found = [];
22297
22419
  let withheld = 0;
22298
22420
  let stopped = false;
22299
- for (const path21 of await visible(input2.path, input2.glob)) {
22421
+ for (const path22 of await visible(input2.path, input2.glob)) {
22300
22422
  if (found.length >= max || Date.now() - started > budget) {
22301
22423
  stopped = true;
22302
22424
  break;
22303
22425
  }
22304
- const content = await staging.peek(path21);
22426
+ const content = await staging.peek(path22);
22305
22427
  if (content === null) continue;
22306
22428
  if (containsSecret(content)) {
22307
22429
  withheld++;
@@ -22313,7 +22435,7 @@ function createToolRuntime(opts) {
22313
22435
  if (line.length > MAX_SEARCH_LINE || !matches(line)) continue;
22314
22436
  const text = line.trim();
22315
22437
  found.push(
22316
- `${path21}:${i + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
22438
+ `${path22}:${i + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
22317
22439
  );
22318
22440
  }
22319
22441
  }
@@ -22521,11 +22643,13 @@ function createChangeRunner(opts) {
22521
22643
  function run(session, work) {
22522
22644
  if (session.busy) throw new Busy("Revyse is still working on this change");
22523
22645
  session.busy = true;
22524
- session.abort = new AbortController();
22525
- const signal = session.abort.signal;
22646
+ const controller = new AbortController();
22647
+ session.abort = controller;
22648
+ const signal = controller.signal;
22526
22649
  void work(signal).catch((err) => {
22527
22650
  if (!signal.aborted) report(session.id, err);
22528
22651
  }).finally(() => {
22652
+ if (session.abort !== controller) return;
22529
22653
  session.busy = false;
22530
22654
  session.abort = void 0;
22531
22655
  });
@@ -22553,9 +22677,9 @@ function createChangeRunner(opts) {
22553
22677
  const stored = [];
22554
22678
  for (const key of localKeys) {
22555
22679
  try {
22556
- const bytes2 = readFileSync6(path14.join(opts.projectRoot, key));
22680
+ const bytes2 = readFileSync6(path15.join(opts.projectRoot, key));
22557
22681
  const { artifact, upload } = await opts.api.post(`/api/changes/${changeId}/artifacts`, {
22558
- name: path14.basename(key),
22682
+ name: path15.basename(key),
22559
22683
  contentType: "image/png",
22560
22684
  size: bytes2.length
22561
22685
  });
@@ -22600,7 +22724,7 @@ function createChangeRunner(opts) {
22600
22724
  const before = await driver.capture({
22601
22725
  url: url2,
22602
22726
  selector: session.element.selector,
22603
- sourceFile: session.element.source?.file,
22727
+ source: session.element.source,
22604
22728
  timeoutMs: 3e4
22605
22729
  }).catch((err) => {
22606
22730
  log(`browser check: before-capture failed: ${err.message}`);
@@ -22633,7 +22757,7 @@ function createChangeRunner(opts) {
22633
22757
  `/api/changes/${session.id}/plan`,
22634
22758
  {
22635
22759
  element: session.element,
22636
- files: gathered.files.map(({ path: path21, content, hash: hash2 }) => ({ path: path21, content, hash: hash2 })),
22760
+ files: gathered.files.map(({ path: path22, content, hash: hash2 }) => ({ path: path22, content, hash: hash2 })),
22637
22761
  modelTier: session.tier,
22638
22762
  model: session.model,
22639
22763
  ...extra
@@ -22645,16 +22769,24 @@ function createChangeRunner(opts) {
22645
22769
  const {
22646
22770
  plan: result,
22647
22771
  model,
22648
- cost
22772
+ cost,
22773
+ fellBackFrom
22649
22774
  } = ev.data;
22650
- onPlan(session, result, model, cost);
22775
+ onPlan(session, result, model, cost, void 0, fellBackFrom);
22651
22776
  }
22652
22777
  }
22653
- function onPlan(session, result, model, cost, connectPath) {
22778
+ function onPlan(session, result, model, cost, connectPath, fellBackFrom) {
22654
22779
  session.plan = result;
22655
22780
  if (cost !== void 0) session.cost = cost;
22656
22781
  if (result.decision === "proceed")
22657
- opts.emit({ type: "plan", changeId: session.id, plan: result, model, cost: session.cost });
22782
+ opts.emit({
22783
+ type: "plan",
22784
+ changeId: session.id,
22785
+ plan: result,
22786
+ model,
22787
+ ...fellBackFrom?.length ? { fellBackFrom } : {},
22788
+ cost: session.cost
22789
+ });
22658
22790
  else if (result.decision === "clarify")
22659
22791
  opts.emit({
22660
22792
  type: "clarification",
@@ -22684,19 +22816,23 @@ function createChangeRunner(opts) {
22684
22816
  }
22685
22817
  async function apply(session, signal) {
22686
22818
  const browser = prepareBrowser(session);
22687
- const baseline = hasLocalChecks() ? runChecks(session.contextPaths).catch((err) => {
22688
- log(`baseline checks failed to run: ${err.message}`);
22689
- return [];
22690
- }) : Promise.resolve([]);
22691
22819
  try {
22692
- await applyWithBrowser(session, signal, browser, baseline);
22820
+ await applyWithBrowser(session, signal, browser);
22693
22821
  } finally {
22694
22822
  void browser.then((b) => b.driver?.close()).catch(() => {
22695
22823
  });
22696
22824
  }
22697
22825
  }
22698
- const hasLocalChecks = () => ["typecheck", "lint", "test"].some((k) => opts.config.scripts[k]);
22699
- async function applyWithBrowser(session, signal, browserPrep, baselinePrep) {
22826
+ async function beforePage(prep) {
22827
+ const late = /* @__PURE__ */ Symbol("late");
22828
+ const ready = await Promise.race([
22829
+ prep,
22830
+ new Promise((r) => setTimeout(() => r(late), opts.beforeWaitMs ?? 3e3))
22831
+ ]);
22832
+ if (ready !== late) return ready;
22833
+ return { before: null, reason: "The page took too long to load before the change." };
22834
+ }
22835
+ async function applyWithBrowser(session, signal, browserPrep) {
22700
22836
  const { files, missing } = readFiles(session.contextPaths);
22701
22837
  const stale = [
22702
22838
  ...missing,
@@ -22713,10 +22849,16 @@ function createChangeRunner(opts) {
22713
22849
  return conflict(session.id, "changed_since_plan", stale);
22714
22850
  }
22715
22851
  session.stage = "patching";
22852
+ const planned = new Set(session.plan?.steps.flatMap((s) => s.files) ?? []);
22853
+ const toPatch = planned.size > 0 ? files.filter((f) => planned.has(f.path)) : files;
22716
22854
  let patch;
22717
- for await (const ev of opts.api.stream(`/api/changes/${session.id}/patch`, { files }, signal)) {
22855
+ for await (const ev of opts.api.stream(
22856
+ `/api/changes/${session.id}/patch`,
22857
+ { files: toPatch },
22858
+ signal
22859
+ )) {
22718
22860
  if (ev.event === "progress" && ev.data.stage === "patching")
22719
- opts.emit({ type: "applying", changeId: session.id, files: files.length });
22861
+ opts.emit({ type: "applying", changeId: session.id, files: toPatch.length });
22720
22862
  if (ev.event === "error") return emitStreamError(session, ev.data);
22721
22863
  if (ev.event === "result") {
22722
22864
  const data = ev.data;
@@ -22725,13 +22867,13 @@ function createChangeRunner(opts) {
22725
22867
  }
22726
22868
  }
22727
22869
  if (!patch) return;
22728
- const [browser, baseline] = await Promise.all([browserPrep, baselinePrep]);
22870
+ const browser = await beforePage(browserPrep);
22729
22871
  session.stage = "applying";
22730
- const snapshots = files.map(({ path: path21, hash: hash2 }) => ({ path: path21, hash: hash2 }));
22872
+ const snapshots = files.map(({ path: path22, hash: hash2 }) => ({ path: path22, hash: hash2 }));
22731
22873
  const applied = applyPatch({
22732
22874
  projectRoot: opts.projectRoot,
22733
22875
  changeId: session.id,
22734
- patch: { files: patch.files.map(({ path: path21, edits }) => ({ path: path21, edits })) },
22876
+ patch: { files: patch.files.map(({ path: path22, edits }) => ({ path: path22, edits })) },
22735
22877
  snapshots
22736
22878
  });
22737
22879
  if (applied.status === "conflict") {
@@ -22754,23 +22896,56 @@ function createChangeRunner(opts) {
22754
22896
  }
22755
22897
  session.applied = true;
22756
22898
  const explanations = new Map(patch.files.map((f) => [f.path, f.explanation]));
22757
- await checkAndReport(session, applied, browser, baseline, { explanations });
22899
+ await checkAndReport(session, applied, browser, signal, { explanations });
22758
22900
  }
22759
- async function checkAndReport(session, applied, browser, baseline, extras = {}) {
22901
+ async function checkAndReport(session, applied, browser, signal, extras = {}) {
22760
22902
  session.stage = "checking";
22761
- const configured = [
22762
- ...["typecheck", "lint", "test", "build"].filter((k) => opts.config.scripts[k]),
22763
- ...browser.driver ? ["browser"] : []
22764
- ];
22765
- opts.emit({
22766
- type: "checking",
22767
- changeId: session.id,
22768
- checks: configured.map((kind) => ({ kind, status: "running" }))
22769
- });
22903
+ const ALL = ["typecheck", "lint", "test", "build"];
22904
+ const kinds = ALL.filter((k) => opts.config.scripts[k]);
22905
+ const files = applied.files.map((f) => ({
22906
+ path: f.path,
22907
+ additions: f.additions,
22908
+ deletions: f.deletions,
22909
+ explanation: extras.explanations?.get(f.path),
22910
+ ...f.kind ? { kind: f.kind } : {}
22911
+ }));
22912
+ const progress = new Map([
22913
+ ...kinds.map((k) => [k, { status: "running" }]),
22914
+ ...browser.driver ? [["browser", { status: "running" }]] : []
22915
+ ]);
22916
+ const report2 = () => {
22917
+ if (signal.aborted) return;
22918
+ opts.emit({
22919
+ type: "checking",
22920
+ changeId: session.id,
22921
+ checks: [...progress].map(([kind, p]) => ({
22922
+ kind,
22923
+ status: p.status,
22924
+ ...p.ms !== void 0 ? { durationMs: p.ms } : {}
22925
+ })),
22926
+ files,
22927
+ diff: applied.diff
22928
+ });
22929
+ };
22930
+ const finished = (c) => {
22931
+ progress.set(c.kind, { status: c.status, ms: c.durationMs });
22932
+ report2();
22933
+ return c;
22934
+ };
22935
+ report2();
22770
22936
  const changed = applied.files.filter((f) => f.kind !== "delete").map((f) => f.path);
22771
22937
  const startedBrowser = performance.now();
22772
22938
  const [local, browserRaw] = await Promise.all([
22773
- runChecks(changed),
22939
+ Promise.all(
22940
+ ALL.map(async (kind) => {
22941
+ const [r] = await runChecks(changed, [kind]);
22942
+ if (!r) {
22943
+ if (progress.delete(kind)) report2();
22944
+ return null;
22945
+ }
22946
+ return progress.has(kind) ? finished(r) : r;
22947
+ })
22948
+ ).then((results) => results.filter((r) => r !== null)),
22774
22949
  browser.driver && browser.url ? runBrowserCheck({
22775
22950
  projectRoot: opts.projectRoot,
22776
22951
  changeId: session.id,
@@ -22782,26 +22957,45 @@ function createChangeRunner(opts) {
22782
22957
  return browser.driver.capture({
22783
22958
  url: browser.url,
22784
22959
  selector: session.element.selector,
22785
- sourceFile: session.element.source?.file,
22960
+ source: session.element.source,
22786
22961
  timeoutMs: 6e4
22787
22962
  });
22788
22963
  },
22789
22964
  durationMs: () => performance.now() - startedBrowser
22790
- }) : Promise.resolve(browserNotRun(browser.reason ?? "The browser check didn't run."))
22965
+ }).then((r) => finished(redactCheck(r))) : Promise.resolve(browserNotRun(browser.reason ?? "The browser check didn't run."))
22791
22966
  ]);
22792
- const browserResult = redactCheck(browserRaw);
22967
+ if (signal.aborted) return;
22968
+ const browserResult = browserRaw;
22793
22969
  const localKeys = browserResult.evidence?.artifactKeys ?? [];
22794
22970
  if (localKeys.length > 0) {
22795
22971
  const stored = await uploadArtifacts(session.id, localKeys);
22796
22972
  browserResult.evidence.artifactKeys = [...localKeys, ...stored];
22797
22973
  }
22798
- const checks = [
22799
- ...compareWithBaseline(
22800
- local.filter((c) => c.kind !== "browser"),
22801
- baseline
22802
- ),
22803
- browserResult
22804
- ];
22974
+ const failed = local.filter((c) => c.status === "failed").map((c) => c.kind);
22975
+ let baseline = [];
22976
+ if (failed.length > 0) {
22977
+ const before = await withOriginals({
22978
+ projectRoot: opts.projectRoot,
22979
+ changeId: session.id,
22980
+ work: () => runChecks(changed, failed)
22981
+ }).catch((err) => {
22982
+ log(`couldn't re-run the failed checks on the original files: ${err.message}`);
22983
+ return null;
22984
+ });
22985
+ if (signal.aborted) return;
22986
+ if (before?.status === "kept-edit") {
22987
+ await transition(
22988
+ session.id,
22989
+ "failed",
22990
+ `Files changed while Revyse was checking: ${before.paths.join(", ")}`,
22991
+ "conflict"
22992
+ ).catch(() => {
22993
+ });
22994
+ return conflict(session.id, "changed_while_applying", before.paths);
22995
+ }
22996
+ if (before?.status === "done") baseline = before.value;
22997
+ }
22998
+ const checks = [...compareWithBaseline(local, baseline), browserResult];
22805
22999
  session.stage = "reporting";
22806
23000
  const { change } = await opts.api.post(
22807
23001
  `/api/changes/${session.id}/result`,
@@ -22823,13 +23017,7 @@ function createChangeRunner(opts) {
22823
23017
  type: "result",
22824
23018
  changeId: session.id,
22825
23019
  state: change.state,
22826
- files: applied.files.map((f) => ({
22827
- path: f.path,
22828
- additions: f.additions,
22829
- deletions: f.deletions,
22830
- explanation: extras.explanations?.get(f.path),
22831
- ...f.kind ? { kind: f.kind } : {}
22832
- })),
23020
+ files,
22833
23021
  diff: applied.diff,
22834
23022
  checks,
22835
23023
  cost: session.cost,
@@ -22955,21 +23143,19 @@ function createChangeRunner(opts) {
22955
23143
  const planned = new Set(session.plan?.steps.flatMap((s) => s.files) ?? []);
22956
23144
  opts.emit({ type: "applying", changeId: session.id, files: planned.size });
22957
23145
  const browserPrep = prepareBrowser(session);
22958
- const baselinePrep = hasLocalChecks() ? runChecks([]).catch((err) => {
22959
- log(`baseline checks failed to run: ${err.message}`);
22960
- return [];
22961
- }) : Promise.resolve([]);
22962
23146
  try {
22963
- await agentApplyWith(session, signal, browserPrep, baselinePrep);
23147
+ await agentApplyWith(session, signal, browserPrep);
22964
23148
  } finally {
22965
23149
  void browserPrep.then((b) => b.driver?.close()).catch(() => {
22966
23150
  });
22967
23151
  }
22968
23152
  }
22969
- async function agentApplyWith(session, signal, browserPrep, baselinePrep) {
23153
+ async function agentApplyWith(session, signal, browserPrep) {
23154
+ let ready;
23155
+ const browserReady = () => ready ??= beforePage(browserPrep);
22970
23156
  const stop = {};
22971
23157
  const flush = async () => {
22972
- await Promise.all([browserPrep, baselinePrep]);
23158
+ await browserReady();
22973
23159
  if (session.applied) {
22974
23160
  const undone = revertChange({ projectRoot: opts.projectRoot, changeId: session.id });
22975
23161
  if (undone.status === "conflict")
@@ -23033,21 +23219,20 @@ ${detail}` : "."}`,
23033
23219
  });
23034
23220
  }
23035
23221
  if (final.status !== "applied") return landingFailed(session, final);
23036
- const [browser, baseline] = await Promise.all([browserPrep, baselinePrep]);
23037
- await checkAndReport(session, final, browser, baseline, { summary: done.summary });
23222
+ await checkAndReport(session, final, await browserReady(), signal, { summary: done.summary });
23038
23223
  }
23039
23224
  async function addDependency(rt, pkg, version2) {
23040
23225
  const pm = opts.config.packageManager;
23041
23226
  const files = ["package.json", LOCKFILE[pm]];
23042
23227
  const disk = (rel) => {
23043
23228
  try {
23044
- return readFileSync6(path14.join(opts.projectRoot, rel), "utf8");
23229
+ return readFileSync6(path15.join(opts.projectRoot, rel), "utf8");
23045
23230
  } catch {
23046
23231
  return null;
23047
23232
  }
23048
23233
  };
23049
23234
  const put = (rel, content) => {
23050
- const file2 = path14.join(opts.projectRoot, rel);
23235
+ const file2 = path15.join(opts.projectRoot, rel);
23051
23236
  if (content === null) rmSync2(file2, { force: true });
23052
23237
  else writeFileSync3(file2, content);
23053
23238
  };
@@ -23259,6 +23444,11 @@ ${r.output.slice(-3e3)}`,
23259
23444
  },
23260
23445
  revert(changeId) {
23261
23446
  const s = sessionOf(changeId);
23447
+ if (s.busy && s.stage === "checking") {
23448
+ s.abort?.abort();
23449
+ s.abort = void 0;
23450
+ s.busy = false;
23451
+ }
23262
23452
  run(s, async () => {
23263
23453
  s.stage = "reverting";
23264
23454
  const files = restore(s);
@@ -23310,32 +23500,32 @@ ${r.output.slice(-3e3)}`,
23310
23500
  }
23311
23501
 
23312
23502
  // src/credentials.ts
23313
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
23503
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
23314
23504
  import { homedir } from "os";
23315
- import path15 from "path";
23505
+ import path16 from "path";
23316
23506
  function credentialsPath(env = process.env) {
23317
- const base = env.XDG_CONFIG_HOME || path15.join(homedir(), ".config");
23318
- return path15.join(base, "revyse", "credentials.json");
23507
+ const base = env.XDG_CONFIG_HOME || path16.join(homedir(), ".config");
23508
+ return path16.join(base, "revyse", "credentials.json");
23319
23509
  }
23320
23510
  function readAll(file2) {
23321
- if (!existsSync4(file2)) return {};
23511
+ if (!existsSync5(file2)) return {};
23322
23512
  return JSON.parse(readFileSync7(file2, "utf8"));
23323
23513
  }
23324
23514
  function writeAll(file2, data) {
23325
- mkdirSync3(path15.dirname(file2), { recursive: true, mode: 448 });
23515
+ mkdirSync3(path16.dirname(file2), { recursive: true, mode: 448 });
23326
23516
  writeFileSync4(file2, `${JSON.stringify(data, null, 2)}
23327
23517
  `, { mode: 384 });
23328
23518
  chmodSync(file2, 384);
23329
23519
  }
23330
23520
  function loadCredential(projectRoot, file2 = credentialsPath()) {
23331
- return readAll(file2)[path15.resolve(projectRoot)] ?? null;
23521
+ return readAll(file2)[path16.resolve(projectRoot)] ?? null;
23332
23522
  }
23333
23523
  function saveCredential(projectRoot, cred, file2 = credentialsPath()) {
23334
- writeAll(file2, { ...readAll(file2), [path15.resolve(projectRoot)]: cred });
23524
+ writeAll(file2, { ...readAll(file2), [path16.resolve(projectRoot)]: cred });
23335
23525
  }
23336
23526
  function deleteCredential(projectRoot, file2 = credentialsPath()) {
23337
23527
  const all = readAll(file2);
23338
- delete all[path15.resolve(projectRoot)];
23528
+ delete all[path16.resolve(projectRoot)];
23339
23529
  writeAll(file2, all);
23340
23530
  }
23341
23531
 
@@ -23374,13 +23564,13 @@ Full policy: ${DATA_HANDLING_URL}
23374
23564
  `;
23375
23565
 
23376
23566
  // src/init-flow.ts
23377
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
23378
- import path18 from "path";
23567
+ import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
23568
+ import path19 from "path";
23379
23569
  import { createTwoFilesPatch as createTwoFilesPatch4 } from "diff";
23380
23570
 
23381
23571
  // src/detect.ts
23382
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
23383
- import path16 from "path";
23572
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
23573
+ import path17 from "path";
23384
23574
  var LOCKFILES2 = [
23385
23575
  ["pnpm-lock.yaml", "pnpm"],
23386
23576
  ["yarn.lock", "yarn"],
@@ -23390,8 +23580,8 @@ var LOCKFILES2 = [
23390
23580
  ];
23391
23581
  var SOURCE_DIR_CANDIDATES = ["src", "app", "pages", "components", "lib"];
23392
23582
  function detectProject(root) {
23393
- const pkgPath = path16.join(root, "package.json");
23394
- if (!existsSync5(pkgPath)) {
23583
+ const pkgPath = path17.join(root, "package.json");
23584
+ if (!existsSync6(pkgPath)) {
23395
23585
  throw new Error(`No package.json found in ${root}. Run revyse init from your project root.`);
23396
23586
  }
23397
23587
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
@@ -23403,13 +23593,13 @@ function detectProject(root) {
23403
23593
  } else if (framework !== "nextjs") {
23404
23594
  warnings.push(`${framework} support is experimental; Next.js is the supported target.`);
23405
23595
  }
23406
- const managers = LOCKFILES2.filter(([file2]) => existsSync5(path16.join(root, file2)));
23596
+ const managers = LOCKFILES2.filter(([file2]) => existsSync6(path17.join(root, file2)));
23407
23597
  const packageManager = managers[0]?.[1] ?? "npm";
23408
23598
  if (managers.length === 0) warnings.push("No lockfile found; defaulting to npm.");
23409
23599
  if (new Set(managers.map(([, m]) => m)).size > 1) {
23410
23600
  warnings.push(`Multiple lockfiles found; using ${packageManager}.`);
23411
23601
  }
23412
- const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync5(path16.join(root, d)));
23602
+ const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync6(path17.join(root, d)));
23413
23603
  for (const d of aliasedDirs(root)) if (!sourceDirs.includes(d)) sourceDirs.push(d);
23414
23604
  if (sourceDirs.length === 0) {
23415
23605
  warnings.push("No source directories found; edit sourceDirs in revyse.config.json.");
@@ -23427,14 +23617,16 @@ function detectProject(root) {
23427
23617
  dev: pick2("dev"),
23428
23618
  typecheck: pick2("typecheck", "type-check", "tsc"),
23429
23619
  lint: pick2("lint"),
23430
- test: pick2("test"),
23620
+ // Tests are opt-in (L19, ADR 0042): a suite can take minutes, and a local change should
23621
+ // take seconds. The build stays: it only ever runs in cloud workspaces.
23431
23622
  build: pick2("build")
23432
23623
  },
23433
23624
  exclude: [],
23434
23625
  agentPort: 4545,
23435
23626
  appOrigins: devOrigins(scripts.dev)
23436
23627
  },
23437
- warnings
23628
+ warnings,
23629
+ optional: { test: pick2("test") }
23438
23630
  };
23439
23631
  }
23440
23632
  function detectFramework(deps) {
@@ -23482,13 +23674,13 @@ function hasSource(dir, depth = 3) {
23482
23674
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
23483
23675
  if (entry.isFile() && SOURCE_FILE.test(entry.name)) return true;
23484
23676
  if (entry.isDirectory() && depth > 1 && !NOT_SOURCE.has(entry.name)) {
23485
- if (hasSource(path16.join(dir, entry.name), depth - 1)) return true;
23677
+ if (hasSource(path17.join(dir, entry.name), depth - 1)) return true;
23486
23678
  }
23487
23679
  }
23488
23680
  return false;
23489
23681
  }
23490
23682
  function aliasedDirs(root) {
23491
- const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path16.join(root, f)).find((f) => existsSync5(f));
23683
+ const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path17.join(root, f)).find((f) => existsSync6(f));
23492
23684
  if (!file2) return [];
23493
23685
  let compilerOptions;
23494
23686
  try {
@@ -23500,15 +23692,15 @@ function aliasedDirs(root) {
23500
23692
  const baseUrl = compilerOptions?.baseUrl ?? ".";
23501
23693
  const dirs = /* @__PURE__ */ new Set();
23502
23694
  for (const target of Object.values(paths).flat()) {
23503
- const rel = path16.relative(root, path16.join(root, baseUrl, target)).split(path16.sep).join("/");
23695
+ const rel = path17.relative(root, path17.join(root, baseUrl, target)).split(path17.sep).join("/");
23504
23696
  const top = rel.split("/")[0];
23505
23697
  if (rel.startsWith("..") || NOT_SOURCE.has(top)) continue;
23506
23698
  if (top === "*" || top === "") {
23507
23699
  for (const entry of readdirSync4(root, { withFileTypes: true }))
23508
23700
  if (entry.isDirectory() && !NOT_SOURCE.has(entry.name) && !entry.name.startsWith(".")) {
23509
- if (hasSource(path16.join(root, entry.name))) dirs.add(entry.name);
23701
+ if (hasSource(path17.join(root, entry.name))) dirs.add(entry.name);
23510
23702
  }
23511
- } else if (existsSync5(path16.join(root, top)) && statSync5(path16.join(root, top)).isDirectory()) {
23703
+ } else if (existsSync6(path17.join(root, top)) && statSync5(path17.join(root, top)).isDirectory()) {
23512
23704
  dirs.add(top);
23513
23705
  }
23514
23706
  }
@@ -23516,8 +23708,8 @@ function aliasedDirs(root) {
23516
23708
  }
23517
23709
 
23518
23710
  // src/next-config.ts
23519
- import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
23520
- import path17 from "path";
23711
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
23712
+ import path18 from "path";
23521
23713
  import { parse as parse3 } from "@babel/parser";
23522
23714
  import MagicString from "magic-string";
23523
23715
  var NEXT_CONFIG_FILES = [
@@ -23528,7 +23720,7 @@ var NEXT_CONFIG_FILES = [
23528
23720
  ];
23529
23721
  function findNextConfig(root) {
23530
23722
  for (const name of NEXT_CONFIG_FILES) {
23531
- if (existsSync6(path17.join(root, name))) return name;
23723
+ if (existsSync7(path18.join(root, name))) return name;
23532
23724
  }
23533
23725
  return null;
23534
23726
  }
@@ -23583,7 +23775,7 @@ ${importLine}`);
23583
23775
  return { status: "unsupported", reason: "it has no `export default` or `module.exports`" };
23584
23776
  }
23585
23777
  function readNextConfig(root, file2) {
23586
- return readFileSync9(path17.join(root, file2), "utf8");
23778
+ return readFileSync9(path18.join(root, file2), "utf8");
23587
23779
  }
23588
23780
 
23589
23781
  // src/version.ts
@@ -23610,9 +23802,9 @@ async function runInit(opts, io) {
23610
23802
  const ask = (q) => opts.yes ? Promise.resolve(true) : io.confirm(q);
23611
23803
  let configured;
23612
23804
  let status;
23613
- if (existsSync7(path18.join(root, CONFIG_FILE)) && !opts.force) {
23805
+ if (existsSync8(path19.join(root, CONFIG_FILE)) && !opts.force) {
23614
23806
  const config3 = projectConfigSchema.parse(
23615
- JSON.parse(readFileSync10(path18.join(root, CONFIG_FILE), "utf8"))
23807
+ JSON.parse(readFileSync10(path19.join(root, CONFIG_FILE), "utf8"))
23616
23808
  );
23617
23809
  io.log(`${CONFIG_FILE} is already here; checking the rest of the setup.`);
23618
23810
  if (!config3.dataDisclosure || config3.dataDisclosure.version < DISCLOSURE_VERSION)
@@ -23649,7 +23841,7 @@ Next: ${steps.join(", then ")}. Then start your app as usual.`
23649
23841
  async function configure(opts, io, ask) {
23650
23842
  const { root } = opts;
23651
23843
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
23652
- const { config: config2, warnings } = detectProject(root);
23844
+ const { config: config2, warnings, optional: optional2 } = detectProject(root);
23653
23845
  io.log(`Detected ${config2.framework} (${config2.packageManager})`);
23654
23846
  io.log(`Source directories: ${config2.sourceDirs.join(", ")}`);
23655
23847
  for (const w of warnings) io.warn(`warning: ${w}`);
@@ -23693,27 +23885,68 @@ Revyse needs this change to ${file2} (it only has an effect in \`next dev\`):
23693
23885
  }
23694
23886
  }
23695
23887
  }
23696
- writeFileSync5(path18.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
23888
+ writeFileSync5(path19.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
23697
23889
  `);
23698
- if (nextConfigEdit) writeFileSync5(path18.join(root, nextConfigEdit.file), nextConfigEdit.code);
23890
+ if (nextConfigEdit) writeFileSync5(path19.join(root, nextConfigEdit.file), nextConfigEdit.code);
23699
23891
  if (ensureGitignored(root))
23700
23892
  io.log(`Added ${GITIGNORE_ENTRY} to .gitignore (local change history).`);
23701
23893
  io.log(`
23702
23894
  Wrote ${CONFIG_FILE}.`);
23895
+ const checks = [
23896
+ ...config2.scripts.typecheck ? ["typecheck"] : [],
23897
+ ...config2.scripts.lint ? ["lint"] : [],
23898
+ ...config2.framework === "nextjs" ? ["the page in a browser"] : []
23899
+ ];
23900
+ if (checks.length > 0)
23901
+ io.log(
23902
+ `After each change, Revyse checks ${checks.join(", ").replace(/, ([^,]*)$/, " and $1")}.`
23903
+ );
23904
+ if (optional2.test)
23905
+ io.log(
23906
+ `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}.`
23907
+ );
23703
23908
  return { config: accepted, nextConfigEdited: Boolean(nextConfigEdit) };
23704
23909
  }
23910
+ function olderThan(a, b) {
23911
+ const parse4 = (v) => /^(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number);
23912
+ const [x, y] = [parse4(a), parse4(b)];
23913
+ if (!x || !y) return false;
23914
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] < y[i];
23915
+ return false;
23916
+ }
23917
+ function installedVersion(root, name) {
23918
+ try {
23919
+ const pkg = JSON.parse(
23920
+ readFileSync10(path19.join(root, "node_modules", name, "package.json"), "utf8")
23921
+ );
23922
+ return pkg.version ?? null;
23923
+ } catch {
23924
+ return null;
23925
+ }
23926
+ }
23705
23927
  async function installPackages(root, config2, setup, io, ask) {
23706
- const missing = PACKAGES.filter((name) => !devDependencies(root)[name]);
23707
- if (missing.length === 0) return true;
23708
- const specs = missing.map((name) => `${name}@${VERSION}`);
23928
+ const listed2 = devDependencies(root);
23929
+ const todo = PACKAGES.flatMap((name) => {
23930
+ const installed = installedVersion(root, name);
23931
+ if (!listed2[name] || !installed) return [{ name, from: null }];
23932
+ return olderThan(installed, VERSION) ? [{ name, from: installed }] : [];
23933
+ });
23934
+ if (todo.length === 0) return true;
23935
+ const specs = todo.map(({ name }) => `${name}@${VERSION}`);
23709
23936
  const command = installCommand(config2.packageManager, specs).join(" ");
23710
- if (!await ask(`Install ${missing.join(" and ")} as dev dependencies (${command})?`)) {
23711
- io.log(`Skipped. Install them yourself: ${command}`);
23937
+ const installs = todo.filter((t) => !t.from).map((t) => t.name);
23938
+ const updates = todo.filter((t) => t.from).map((t) => `${t.name} from ${t.from} to ${VERSION}`);
23939
+ const what = [
23940
+ ...installs.length ? [`install ${installs.join(" and ")} as dev dependencies`] : [],
23941
+ ...updates.length ? [`update ${updates.join(" and ")}`] : []
23942
+ ].join(", and ");
23943
+ if (!await ask(`${what[0].toUpperCase()}${what.slice(1)} (${command})?`)) {
23944
+ io.log(`Skipped. Run it yourself: ${command}`);
23712
23945
  return false;
23713
23946
  }
23714
23947
  try {
23715
23948
  await setup.install(root, config2.packageManager, specs);
23716
- io.log(`Installed ${missing.join(" and ")}.`);
23949
+ io.log(`Revyse's packages are at ${VERSION}.`);
23717
23950
  return true;
23718
23951
  } catch (err) {
23719
23952
  io.warn(`Couldn't install them (${err.message}). Run: ${command}`);
@@ -23745,11 +23978,11 @@ async function connectProject(root, config2, setup, io, ask) {
23745
23978
  }
23746
23979
  function projectName(root) {
23747
23980
  const name = readPackageJson(root)?.name?.replace(/^@[^/]+\//, "").trim();
23748
- return (name || path18.basename(root)).slice(0, 100);
23981
+ return (name || path19.basename(root)).slice(0, 100);
23749
23982
  }
23750
23983
  function ensureGitignored(root) {
23751
- const file2 = path18.join(root, ".gitignore");
23752
- const current = existsSync7(file2) ? readFileSync10(file2, "utf8") : "";
23984
+ const file2 = path19.join(root, ".gitignore");
23985
+ const current = existsSync8(file2) ? readFileSync10(file2, "utf8") : "";
23753
23986
  const lines = current.split(/\r?\n/).map((l) => l.trim());
23754
23987
  if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".revyse")) return false;
23755
23988
  const separator = current === "" ? "" : current.endsWith("\n") ? "\n" : "\n\n";
@@ -23760,7 +23993,7 @@ ${GITIGNORE_ENTRY}
23760
23993
  }
23761
23994
  function readPackageJson(root) {
23762
23995
  try {
23763
- return JSON.parse(readFileSync10(path18.join(root, "package.json"), "utf8"));
23996
+ return JSON.parse(readFileSync10(path19.join(root, "package.json"), "utf8"));
23764
23997
  } catch {
23765
23998
  return null;
23766
23999
  }
@@ -23805,7 +24038,7 @@ function createRequestHandler(currentRunner) {
23805
24038
  import { randomBytes, timingSafeEqual } from "crypto";
23806
24039
  import { readFileSync as readFileSync11 } from "fs";
23807
24040
  import { createServer } from "http";
23808
- import path19 from "path";
24041
+ import path20 from "path";
23809
24042
  import { WebSocketServer } from "ws";
23810
24043
  var ARTIFACT_PATH = /^\/artifacts\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/(before|after)\.png$/;
23811
24044
  var safeEqual = (a, b) => {
@@ -23857,7 +24090,7 @@ ${opts.overlayBundle()}`
23857
24090
  res.writeHead(403).end();
23858
24091
  return;
23859
24092
  }
23860
- const file2 = path19.join(
24093
+ const file2 = path20.join(
23861
24094
  opts.project.root,
23862
24095
  ".revyse",
23863
24096
  "changes",
@@ -23984,7 +24217,7 @@ ${opts.overlayBundle()}`
23984
24217
 
23985
24218
  // src/agent/local-agent.ts
23986
24219
  function loadConfig(file2) {
23987
- if (!existsSync8(file2)) throw new Error(`No ${CONFIG_FILE} found. Run \`revyse init\` first.`);
24220
+ if (!existsSync9(file2)) throw new Error(`No ${CONFIG_FILE} found. Run \`revyse init\` first.`);
23988
24221
  const config2 = projectConfigSchema.parse(JSON.parse(readFileSync12(file2, "utf8")));
23989
24222
  if (!config2.dataDisclosure || config2.dataDisclosure.version < DISCLOSURE_VERSION) {
23990
24223
  throw new Error("Review and accept the data disclosure first: run `revyse init --force`.");
@@ -23992,7 +24225,7 @@ function loadConfig(file2) {
23992
24225
  return config2;
23993
24226
  }
23994
24227
  function readOverlayBundle() {
23995
- const file2 = path20.join(path20.dirname(fileURLToPath(import.meta.url)), "overlay.js");
24228
+ const file2 = path21.join(path21.dirname(fileURLToPath(import.meta.url)), "overlay.js");
23996
24229
  return readFileSync12(file2, "utf8");
23997
24230
  }
23998
24231
  async function identifyAgent(port) {
@@ -24009,7 +24242,7 @@ async function identifyAgent(port) {
24009
24242
  }
24010
24243
  async function startLocalAgent(opts) {
24011
24244
  const { root, log } = opts;
24012
- const configFile = path20.join(root, CONFIG_FILE);
24245
+ const configFile = path21.join(root, CONFIG_FILE);
24013
24246
  let config2 = loadConfig(configFile);
24014
24247
  const port = opts.port ?? config2.agentPort;
24015
24248
  const sink = {};