revyse 0.1.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/README.md +7 -6
- package/dist/agent.js +11 -0
- package/dist/chunk-FVZWKW32.js +24339 -0
- package/dist/headless.js +706 -210
- package/dist/index.js +212 -24043
- package/dist/overlay.js +130 -61
- package/package.json +7 -3
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(
|
|
3120
|
-
if (
|
|
3121
|
-
this._executableDir =
|
|
3119
|
+
executableDir(path31) {
|
|
3120
|
+
if (path31 === void 0) return this._executableDir;
|
|
3121
|
+
this._executableDir = path31;
|
|
3122
3122
|
return this;
|
|
3123
3123
|
}
|
|
3124
3124
|
/**
|
|
@@ -3374,8 +3374,8 @@ function useColor() {
|
|
|
3374
3374
|
var program = new Command();
|
|
3375
3375
|
|
|
3376
3376
|
// src/commands/change.ts
|
|
3377
|
-
import { readFileSync as
|
|
3378
|
-
import
|
|
3377
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
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,
|
|
4296
|
-
if (!
|
|
4295
|
+
function getElementAtPath(obj, path31) {
|
|
4296
|
+
if (!path31)
|
|
4297
4297
|
return obj;
|
|
4298
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 }, [...
|
|
5107
|
+
issue2.errors.map((issues) => processError({ issues }, [...path31, ...issue2.path]));
|
|
5108
5108
|
} else if (issue2.code === "invalid_key") {
|
|
5109
|
-
processError({ issues: issue2.issues }, [...
|
|
5109
|
+
processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
|
|
5110
5110
|
} else if (issue2.code === "invalid_element") {
|
|
5111
|
-
processError({ issues: issue2.issues }, [...
|
|
5111
|
+
processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
|
|
5112
5112
|
} else {
|
|
5113
|
-
const fullpath = [...
|
|
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,
|
|
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 }, [...
|
|
5156
|
+
issue2.errors.map((issues) => processError({ issues }, [...path31, ...issue2.path]));
|
|
5157
5157
|
} else if (issue2.code === "invalid_key") {
|
|
5158
|
-
processError({ issues: issue2.issues }, [...
|
|
5158
|
+
processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
|
|
5159
5159
|
} else if (issue2.code === "invalid_element") {
|
|
5160
|
-
processError({ issues: issue2.issues }, [...
|
|
5160
|
+
processError({ issues: issue2.issues }, [...path31, ...issue2.path]);
|
|
5161
5161
|
} else {
|
|
5162
|
-
const fullpath = [...
|
|
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
|
|
5202
|
-
for (const seg of
|
|
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")
|
|
@@ -6301,9 +6301,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
|
|
|
6301
6301
|
function stripTabAndNewline(value) {
|
|
6302
6302
|
return value.replace(asciiTabOrNewline, "");
|
|
6303
6303
|
}
|
|
6304
|
-
function urlHostnameOk(url2,
|
|
6305
|
-
|
|
6306
|
-
return
|
|
6304
|
+
function urlHostnameOk(url2, hostname4) {
|
|
6305
|
+
hostname4.lastIndex = 0;
|
|
6306
|
+
return hostname4.test(url2.hostname);
|
|
6307
6307
|
}
|
|
6308
6308
|
function urlProtocolOk(url2, protocol) {
|
|
6309
6309
|
protocol.lastIndex = 0;
|
|
@@ -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
|
|
22341
|
-
if (
|
|
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 (
|
|
22346
|
-
const key =
|
|
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(
|
|
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(
|
|
24645
|
-
const last =
|
|
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:
|
|
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:
|
|
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
|
|
30459
|
-
if (writtenPaths.has(
|
|
30469
|
+
const path31 = type === "fileNumber" ? stdioItem.path : getFileTarget(stdioItem);
|
|
30470
|
+
if (writtenPaths.has(path31)) {
|
|
30460
30471
|
continue;
|
|
30461
30472
|
}
|
|
30462
|
-
writtenPaths.add(
|
|
30463
|
-
if (append || outputFiles.has(
|
|
30464
|
-
appendFileSync(
|
|
30473
|
+
writtenPaths.add(path31);
|
|
30474
|
+
if (append || outputFiles.has(path31)) {
|
|
30475
|
+
appendFileSync(path31, contents);
|
|
30465
30476
|
} else {
|
|
30466
|
-
writeFileSync2(
|
|
30477
|
+
writeFileSync2(path31, contents);
|
|
30467
30478
|
}
|
|
30468
|
-
outputFiles.add(
|
|
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,
|
|
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 (
|
|
33573
|
+
else if (source && await page.locator(`[${LOC_ATTRIBUTE}^=${JSON.stringify(`${source.file}:`)}]`).count() > 0)
|
|
33498
33574
|
found = "source";
|
|
33499
|
-
const
|
|
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 = (
|
|
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(
|
|
33631
|
-
const res = await f(`${base}${
|
|
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(
|
|
33636
|
-
const res = await call(
|
|
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(
|
|
33641
|
-
const res = await call(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
33827
|
-
const s = this.staged.get(
|
|
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(
|
|
33830
|
-
if (!this.seen.has(
|
|
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(
|
|
33835
|
-
const s = this.staged.get(
|
|
33836
|
-
return s ? s.content : this.host.read(
|
|
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(
|
|
33840
|
-
const existing = this.staged.get(
|
|
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(
|
|
33846
|
-
this.staged.set(
|
|
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(
|
|
33858
|
-
await this.write(
|
|
33859
|
-
this.fromPackageManager.add(
|
|
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 [
|
|
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:
|
|
33880
|
-
else if (s.content === null) out.push({ path:
|
|
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:
|
|
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(([
|
|
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 = (
|
|
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
|
|
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(
|
|
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
|
-
`${
|
|
34107
|
+
`${path31}:${i2 + 1}: ${text.length > MAX_LINE ? `${text.slice(0, MAX_LINE)}\u2026` : text}`
|
|
33986
34108
|
);
|
|
33987
34109
|
}
|
|
33988
34110
|
}
|
|
@@ -34183,25 +34305,27 @@ function createChangeRunner(opts) {
|
|
|
34183
34305
|
log(`change ${changeId ?? "(new)"} failed: ${err.message}`);
|
|
34184
34306
|
fail2(session, changeId, {
|
|
34185
34307
|
code: "internal",
|
|
34186
|
-
message: "Something went wrong in the Revyse agent. Check
|
|
34308
|
+
message: "Something went wrong in the Revyse agent. Check your dev server's output.",
|
|
34187
34309
|
retryable: true
|
|
34188
34310
|
});
|
|
34189
34311
|
}
|
|
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
|
-
|
|
34194
|
-
|
|
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
|
});
|
|
34201
34325
|
}
|
|
34202
34326
|
function sessionOf(changeId) {
|
|
34203
34327
|
const s = sessions.get(changeId);
|
|
34204
|
-
if (!s) throw new Error("This change isn't open in this
|
|
34328
|
+
if (!s) throw new Error("This change isn't open in this session of your dev server");
|
|
34205
34329
|
return s;
|
|
34206
34330
|
}
|
|
34207
34331
|
function readFiles(paths) {
|
|
@@ -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(
|
|
34349
|
+
const bytes2 = readFileSync8(path25.join(opts.projectRoot, key));
|
|
34226
34350
|
const { artifact, upload } = await opts.api.post(`/api/changes/${changeId}/artifacts`, {
|
|
34227
|
-
name:
|
|
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
|
-
|
|
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:
|
|
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({
|
|
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
|
|
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
|
-
|
|
34368
|
-
|
|
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(
|
|
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:
|
|
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
|
|
34539
|
+
const browser = await beforePage(browserPrep);
|
|
34398
34540
|
session.stage = "applying";
|
|
34399
|
-
const snapshots = files.map(({ path:
|
|
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:
|
|
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,
|
|
34568
|
+
await checkAndReport(session, applied, browser, signal, { explanations });
|
|
34427
34569
|
}
|
|
34428
|
-
async function checkAndReport(session, applied, browser,
|
|
34570
|
+
async function checkAndReport(session, applied, browser, signal, extras = {}) {
|
|
34429
34571
|
session.stage = "checking";
|
|
34430
|
-
const
|
|
34431
|
-
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34468
|
-
|
|
34469
|
-
|
|
34470
|
-
|
|
34471
|
-
|
|
34472
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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);
|
|
@@ -34978,13 +35168,113 @@ ${r.output.slice(-3e3)}`,
|
|
|
34978
35168
|
};
|
|
34979
35169
|
}
|
|
34980
35170
|
|
|
35171
|
+
// src/api-client.ts
|
|
35172
|
+
var DEFAULT_API_URL = "https://revyse.dev";
|
|
35173
|
+
function resolveApiUrl(flag, env = process.env) {
|
|
35174
|
+
return (flag || env.REVYSE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
35175
|
+
}
|
|
35176
|
+
var ApiError = class extends Error {
|
|
35177
|
+
constructor(status, code2, message) {
|
|
35178
|
+
super(message);
|
|
35179
|
+
this.status = status;
|
|
35180
|
+
this.code = code2;
|
|
35181
|
+
this.name = "ApiError";
|
|
35182
|
+
}
|
|
35183
|
+
status;
|
|
35184
|
+
code;
|
|
35185
|
+
};
|
|
35186
|
+
function createApiClient({ apiUrl, token, fetch: f = fetch }) {
|
|
35187
|
+
async function request(method, path31, body) {
|
|
35188
|
+
const res = await f(`${apiUrl}${path31}`, {
|
|
35189
|
+
method,
|
|
35190
|
+
headers: {
|
|
35191
|
+
"content-type": "application/json",
|
|
35192
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
35193
|
+
},
|
|
35194
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
35195
|
+
});
|
|
35196
|
+
const data = await res.json().catch(() => ({}));
|
|
35197
|
+
if (!res.ok) {
|
|
35198
|
+
const code2 = data.error?.code ?? "error";
|
|
35199
|
+
throw new ApiError(res.status, code2, data.error?.message ?? `Request failed (${res.status})`);
|
|
35200
|
+
}
|
|
35201
|
+
return data;
|
|
35202
|
+
}
|
|
35203
|
+
return {
|
|
35204
|
+
get: (path31) => request("GET", path31),
|
|
35205
|
+
post: (path31, body) => request("POST", path31, body ?? {})
|
|
35206
|
+
};
|
|
35207
|
+
}
|
|
35208
|
+
|
|
35209
|
+
// src/connect.ts
|
|
35210
|
+
import { spawn as spawn2 } from "child_process";
|
|
35211
|
+
import { hostname as hostname3 } from "os";
|
|
35212
|
+
|
|
35213
|
+
// src/credentials.ts
|
|
35214
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
35215
|
+
import { homedir } from "os";
|
|
35216
|
+
import path26 from "path";
|
|
35217
|
+
function credentialsPath(env = process.env) {
|
|
35218
|
+
const base = env.XDG_CONFIG_HOME || path26.join(homedir(), ".config");
|
|
35219
|
+
return path26.join(base, "revyse", "credentials.json");
|
|
35220
|
+
}
|
|
35221
|
+
function readAll(file2) {
|
|
35222
|
+
if (!existsSync5(file2)) return {};
|
|
35223
|
+
return JSON.parse(readFileSync9(file2, "utf8"));
|
|
35224
|
+
}
|
|
35225
|
+
function writeAll(file2, data) {
|
|
35226
|
+
mkdirSync3(path26.dirname(file2), { recursive: true, mode: 448 });
|
|
35227
|
+
writeFileSync5(file2, `${JSON.stringify(data, null, 2)}
|
|
35228
|
+
`, { mode: 384 });
|
|
35229
|
+
chmodSync(file2, 384);
|
|
35230
|
+
}
|
|
35231
|
+
function loadCredential(projectRoot, file2 = credentialsPath()) {
|
|
35232
|
+
return readAll(file2)[path26.resolve(projectRoot)] ?? null;
|
|
35233
|
+
}
|
|
35234
|
+
function saveCredential(projectRoot, cred, file2 = credentialsPath()) {
|
|
35235
|
+
writeAll(file2, { ...readAll(file2), [path26.resolve(projectRoot)]: cred });
|
|
35236
|
+
}
|
|
35237
|
+
|
|
35238
|
+
// src/device-login.ts
|
|
35239
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35240
|
+
async function deviceLogin({
|
|
35241
|
+
client,
|
|
35242
|
+
clientName,
|
|
35243
|
+
onCode,
|
|
35244
|
+
sleep = defaultSleep
|
|
35245
|
+
}) {
|
|
35246
|
+
const start = await client.post("/api/cli/device", { clientName });
|
|
35247
|
+
onCode(start);
|
|
35248
|
+
let interval = start.interval;
|
|
35249
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
35250
|
+
while (Date.now() < deadline) {
|
|
35251
|
+
await sleep(interval * 1e3);
|
|
35252
|
+
try {
|
|
35253
|
+
return await client.post("/api/cli/token", { deviceCode: start.deviceCode });
|
|
35254
|
+
} catch (err) {
|
|
35255
|
+
if (!(err instanceof ApiError)) throw err;
|
|
35256
|
+
if (err.code === "authorization_pending") continue;
|
|
35257
|
+
if (err.code === "slow_down") {
|
|
35258
|
+
interval += 5;
|
|
35259
|
+
continue;
|
|
35260
|
+
}
|
|
35261
|
+
if (err.code === "access_denied") {
|
|
35262
|
+
throw new Error("Login was denied in the browser.", { cause: err });
|
|
35263
|
+
}
|
|
35264
|
+
if (err.code === "expired_token") break;
|
|
35265
|
+
throw err;
|
|
35266
|
+
}
|
|
35267
|
+
}
|
|
35268
|
+
throw new Error("Login code expired. Run `revyse login` again.");
|
|
35269
|
+
}
|
|
35270
|
+
|
|
34981
35271
|
// src/init-flow.ts
|
|
34982
|
-
import { existsSync as
|
|
34983
|
-
import
|
|
35272
|
+
import { existsSync as existsSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
|
|
35273
|
+
import path29 from "path";
|
|
34984
35274
|
|
|
34985
35275
|
// src/detect.ts
|
|
34986
|
-
import { existsSync as
|
|
34987
|
-
import
|
|
35276
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
|
|
35277
|
+
import path27 from "path";
|
|
34988
35278
|
var LOCKFILES2 = [
|
|
34989
35279
|
["pnpm-lock.yaml", "pnpm"],
|
|
34990
35280
|
["yarn.lock", "yarn"],
|
|
@@ -34994,11 +35284,11 @@ var LOCKFILES2 = [
|
|
|
34994
35284
|
];
|
|
34995
35285
|
var SOURCE_DIR_CANDIDATES = ["src", "app", "pages", "components", "lib"];
|
|
34996
35286
|
function detectProject(root) {
|
|
34997
|
-
const pkgPath =
|
|
34998
|
-
if (!
|
|
35287
|
+
const pkgPath = path27.join(root, "package.json");
|
|
35288
|
+
if (!existsSync6(pkgPath)) {
|
|
34999
35289
|
throw new Error(`No package.json found in ${root}. Run revyse init from your project root.`);
|
|
35000
35290
|
}
|
|
35001
|
-
const pkg = JSON.parse(
|
|
35291
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
35002
35292
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35003
35293
|
const warnings = [];
|
|
35004
35294
|
const framework = detectFramework(deps);
|
|
@@ -35007,13 +35297,13 @@ function detectProject(root) {
|
|
|
35007
35297
|
} else if (framework !== "nextjs") {
|
|
35008
35298
|
warnings.push(`${framework} support is experimental; Next.js is the supported target.`);
|
|
35009
35299
|
}
|
|
35010
|
-
const managers = LOCKFILES2.filter(([file2]) =>
|
|
35300
|
+
const managers = LOCKFILES2.filter(([file2]) => existsSync6(path27.join(root, file2)));
|
|
35011
35301
|
const packageManager = managers[0]?.[1] ?? "npm";
|
|
35012
35302
|
if (managers.length === 0) warnings.push("No lockfile found; defaulting to npm.");
|
|
35013
35303
|
if (new Set(managers.map(([, m]) => m)).size > 1) {
|
|
35014
35304
|
warnings.push(`Multiple lockfiles found; using ${packageManager}.`);
|
|
35015
35305
|
}
|
|
35016
|
-
const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) =>
|
|
35306
|
+
const sourceDirs = SOURCE_DIR_CANDIDATES.filter((d) => existsSync6(path27.join(root, d)));
|
|
35017
35307
|
for (const d of aliasedDirs(root)) if (!sourceDirs.includes(d)) sourceDirs.push(d);
|
|
35018
35308
|
if (sourceDirs.length === 0) {
|
|
35019
35309
|
warnings.push("No source directories found; edit sourceDirs in revyse.config.json.");
|
|
@@ -35031,14 +35321,16 @@ function detectProject(root) {
|
|
|
35031
35321
|
dev: pick2("dev"),
|
|
35032
35322
|
typecheck: pick2("typecheck", "type-check", "tsc"),
|
|
35033
35323
|
lint: pick2("lint"),
|
|
35034
|
-
|
|
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.
|
|
35035
35326
|
build: pick2("build")
|
|
35036
35327
|
},
|
|
35037
35328
|
exclude: [],
|
|
35038
35329
|
agentPort: 4545,
|
|
35039
35330
|
appOrigins: devOrigins(scripts.dev)
|
|
35040
35331
|
},
|
|
35041
|
-
warnings
|
|
35332
|
+
warnings,
|
|
35333
|
+
optional: { test: pick2("test") }
|
|
35042
35334
|
};
|
|
35043
35335
|
}
|
|
35044
35336
|
function detectFramework(deps) {
|
|
@@ -35086,17 +35378,17 @@ function hasSource(dir, depth = 3) {
|
|
|
35086
35378
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
35087
35379
|
if (entry.isFile() && SOURCE_FILE.test(entry.name)) return true;
|
|
35088
35380
|
if (entry.isDirectory() && depth > 1 && !NOT_SOURCE.has(entry.name)) {
|
|
35089
|
-
if (hasSource(
|
|
35381
|
+
if (hasSource(path27.join(dir, entry.name), depth - 1)) return true;
|
|
35090
35382
|
}
|
|
35091
35383
|
}
|
|
35092
35384
|
return false;
|
|
35093
35385
|
}
|
|
35094
35386
|
function aliasedDirs(root) {
|
|
35095
|
-
const file2 = ["tsconfig.json", "jsconfig.json"].map((f) =>
|
|
35387
|
+
const file2 = ["tsconfig.json", "jsconfig.json"].map((f) => path27.join(root, f)).find((f) => existsSync6(f));
|
|
35096
35388
|
if (!file2) return [];
|
|
35097
35389
|
let compilerOptions;
|
|
35098
35390
|
try {
|
|
35099
|
-
compilerOptions = parseJsonc(
|
|
35391
|
+
compilerOptions = parseJsonc(readFileSync10(file2, "utf8")).compilerOptions;
|
|
35100
35392
|
} catch {
|
|
35101
35393
|
return [];
|
|
35102
35394
|
}
|
|
@@ -35104,15 +35396,15 @@ function aliasedDirs(root) {
|
|
|
35104
35396
|
const baseUrl = compilerOptions?.baseUrl ?? ".";
|
|
35105
35397
|
const dirs = /* @__PURE__ */ new Set();
|
|
35106
35398
|
for (const target of Object.values(paths).flat()) {
|
|
35107
|
-
const rel =
|
|
35399
|
+
const rel = path27.relative(root, path27.join(root, baseUrl, target)).split(path27.sep).join("/");
|
|
35108
35400
|
const top = rel.split("/")[0];
|
|
35109
35401
|
if (rel.startsWith("..") || NOT_SOURCE.has(top)) continue;
|
|
35110
35402
|
if (top === "*" || top === "") {
|
|
35111
35403
|
for (const entry of readdirSync4(root, { withFileTypes: true }))
|
|
35112
35404
|
if (entry.isDirectory() && !NOT_SOURCE.has(entry.name) && !entry.name.startsWith(".")) {
|
|
35113
|
-
if (hasSource(
|
|
35405
|
+
if (hasSource(path27.join(root, entry.name))) dirs.add(entry.name);
|
|
35114
35406
|
}
|
|
35115
|
-
} else if (
|
|
35407
|
+
} else if (existsSync6(path27.join(root, top)) && statSync6(path27.join(root, top)).isDirectory()) {
|
|
35116
35408
|
dirs.add(top);
|
|
35117
35409
|
}
|
|
35118
35410
|
}
|
|
@@ -35154,8 +35446,8 @@ Full policy: ${DATA_HANDLING_URL}
|
|
|
35154
35446
|
`;
|
|
35155
35447
|
|
|
35156
35448
|
// src/next-config.ts
|
|
35157
|
-
import { existsSync as
|
|
35158
|
-
import
|
|
35449
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
|
|
35450
|
+
import path28 from "path";
|
|
35159
35451
|
|
|
35160
35452
|
// ../../node_modules/.pnpm/@babel+parser@8.0.7/node_modules/@babel/parser/lib/index.js
|
|
35161
35453
|
var Position = class {
|
|
@@ -50913,7 +51205,7 @@ var NEXT_CONFIG_FILES = [
|
|
|
50913
51205
|
];
|
|
50914
51206
|
function findNextConfig(root) {
|
|
50915
51207
|
for (const name of NEXT_CONFIG_FILES) {
|
|
50916
|
-
if (
|
|
51208
|
+
if (existsSync7(path28.join(root, name))) return name;
|
|
50917
51209
|
}
|
|
50918
51210
|
return null;
|
|
50919
51211
|
}
|
|
@@ -50968,28 +51260,80 @@ ${importLine}`);
|
|
|
50968
51260
|
return { status: "unsupported", reason: "it has no `export default` or `module.exports`" };
|
|
50969
51261
|
}
|
|
50970
51262
|
function readNextConfig(root, file2) {
|
|
50971
|
-
return
|
|
51263
|
+
return readFileSync11(path28.join(root, file2), "utf8");
|
|
50972
51264
|
}
|
|
50973
51265
|
|
|
51266
|
+
// src/version.ts
|
|
51267
|
+
import { createRequire } from "module";
|
|
51268
|
+
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
51269
|
+
|
|
50974
51270
|
// src/init-flow.ts
|
|
50975
51271
|
var CONFIG_FILE = "revyse.config.json";
|
|
50976
51272
|
var GITIGNORE_ENTRY = ".revyse/";
|
|
51273
|
+
var PACKAGES = ["revyse", "@revyse/next"];
|
|
51274
|
+
var INSTALL = {
|
|
51275
|
+
npm: ["npm", "install", "--save-dev", "--no-audit", "--no-fund"],
|
|
51276
|
+
pnpm: ["pnpm", "add", "--save-dev"],
|
|
51277
|
+
yarn: ["yarn", "add", "--dev"],
|
|
51278
|
+
bun: ["bun", "add", "--dev"]
|
|
51279
|
+
};
|
|
51280
|
+
var installCommand = (pm, specs) => [
|
|
51281
|
+
...INSTALL[pm],
|
|
51282
|
+
...specs
|
|
51283
|
+
];
|
|
51284
|
+
var runScript = (pm, script) => pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
50977
51285
|
async function runInit(opts, io) {
|
|
50978
51286
|
const { root } = opts;
|
|
50979
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
50980
51287
|
const ask = (q) => opts.yes ? Promise.resolve(true) : io.confirm(q);
|
|
50981
|
-
|
|
50982
|
-
|
|
50983
|
-
|
|
51288
|
+
let configured;
|
|
51289
|
+
let status;
|
|
51290
|
+
if (existsSync8(path29.join(root, CONFIG_FILE)) && !opts.force) {
|
|
51291
|
+
const config3 = projectConfigSchema.parse(
|
|
51292
|
+
JSON.parse(readFileSync12(path29.join(root, CONFIG_FILE), "utf8"))
|
|
51293
|
+
);
|
|
51294
|
+
io.log(`${CONFIG_FILE} is already here; checking the rest of the setup.`);
|
|
51295
|
+
if (!config3.dataDisclosure || config3.dataDisclosure.version < DISCLOSURE_VERSION)
|
|
51296
|
+
io.warn(
|
|
51297
|
+
"How Revyse handles your code has changed: run `npx revyse init --force` to review it."
|
|
51298
|
+
);
|
|
51299
|
+
configured = { config: config3, nextConfigEdited: false };
|
|
51300
|
+
status = "exists";
|
|
51301
|
+
} else {
|
|
51302
|
+
configured = await configure(opts, io, ask);
|
|
51303
|
+
if (!configured) return { status: "declined" };
|
|
51304
|
+
status = "initialized";
|
|
51305
|
+
}
|
|
51306
|
+
const { config: config2 } = configured;
|
|
51307
|
+
const nextjs = config2.framework === "nextjs";
|
|
51308
|
+
let installed = !nextjs;
|
|
51309
|
+
if (nextjs && opts.setup) installed = await installPackages(root, config2, opts.setup, io, ask);
|
|
51310
|
+
let connection = null;
|
|
51311
|
+
if (opts.setup && opts.connect !== false)
|
|
51312
|
+
connection = await connectProject(root, config2, opts.setup, io, ask);
|
|
51313
|
+
if (nextjs) {
|
|
51314
|
+
const steps = [
|
|
51315
|
+
...installed ? [] : [`install ${PACKAGES.join(" and ")} as dev dependencies`],
|
|
51316
|
+
...connection ? [] : ["connect it to Revyse with `npx revyse login`"]
|
|
51317
|
+
];
|
|
51318
|
+
io.log(
|
|
51319
|
+
steps.length === 0 ? `
|
|
51320
|
+
Done. Start your app as usual (\`${runScript(config2.packageManager, config2.scripts.dev ?? "dev")}\`) and click the Revyse button.` : `
|
|
51321
|
+
Next: ${steps.join(", then ")}. Then start your app as usual.`
|
|
51322
|
+
);
|
|
50984
51323
|
}
|
|
50985
|
-
|
|
51324
|
+
return { status, ...configured, connection };
|
|
51325
|
+
}
|
|
51326
|
+
async function configure(opts, io, ask) {
|
|
51327
|
+
const { root } = opts;
|
|
51328
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
51329
|
+
const { config: config2, warnings, optional: optional2 } = detectProject(root);
|
|
50986
51330
|
io.log(`Detected ${config2.framework} (${config2.packageManager})`);
|
|
50987
51331
|
io.log(`Source directories: ${config2.sourceDirs.join(", ")}`);
|
|
50988
51332
|
for (const w of warnings) io.warn(`warning: ${w}`);
|
|
50989
51333
|
io.log(DISCLOSURE_TEXT);
|
|
50990
51334
|
if (!await ask("Do you accept how Revyse handles your code?")) {
|
|
50991
51335
|
io.log("Nothing was changed. Run `revyse init` again if you change your mind.");
|
|
50992
|
-
return
|
|
51336
|
+
return null;
|
|
50993
51337
|
}
|
|
50994
51338
|
const accepted = {
|
|
50995
51339
|
...config2,
|
|
@@ -51022,46 +51366,187 @@ Revyse needs this change to ${file2} (it only has an effect in \`next dev\`):
|
|
|
51022
51366
|
);
|
|
51023
51367
|
if (await ask(`Apply this change to ${file2}?`))
|
|
51024
51368
|
nextConfigEdit = { file: file2, code: result.code };
|
|
51025
|
-
else
|
|
51026
|
-
io.log(
|
|
51027
|
-
`Skipped. Wrap the export of ${file2} with withRevyse() yourself before \`revyse dev\`.`
|
|
51028
|
-
);
|
|
51369
|
+
else io.log(`Skipped. Wrap the export of ${file2} with withRevyse() yourself.`);
|
|
51029
51370
|
}
|
|
51030
51371
|
}
|
|
51031
51372
|
}
|
|
51032
|
-
|
|
51373
|
+
writeFileSync6(path29.join(root, CONFIG_FILE), `${JSON.stringify(accepted, null, 2)}
|
|
51033
51374
|
`);
|
|
51034
|
-
if (nextConfigEdit)
|
|
51375
|
+
if (nextConfigEdit) writeFileSync6(path29.join(root, nextConfigEdit.file), nextConfigEdit.code);
|
|
51035
51376
|
if (ensureGitignored(root))
|
|
51036
51377
|
io.log(`Added ${GITIGNORE_ENTRY} to .gitignore (local change history).`);
|
|
51037
51378
|
io.log(`
|
|
51038
51379
|
Wrote ${CONFIG_FILE}.`);
|
|
51039
|
-
|
|
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)
|
|
51040
51386
|
io.log(
|
|
51041
|
-
|
|
51387
|
+
`After each change, Revyse checks ${checks.join(", ").replace(/, ([^,]*)$/, " and $1")}.`
|
|
51042
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
|
+
);
|
|
51393
|
+
return { config: accepted, nextConfigEdited: Boolean(nextConfigEdit) };
|
|
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;
|
|
51043
51410
|
}
|
|
51044
|
-
|
|
51411
|
+
}
|
|
51412
|
+
async function installPackages(root, config2, setup2, io, ask) {
|
|
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}`);
|
|
51421
|
+
const command = installCommand(config2.packageManager, specs).join(" ");
|
|
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}`);
|
|
51430
|
+
return false;
|
|
51431
|
+
}
|
|
51432
|
+
try {
|
|
51433
|
+
await setup2.install(root, config2.packageManager, specs);
|
|
51434
|
+
io.log(`Revyse's packages are at ${VERSION}.`);
|
|
51435
|
+
return true;
|
|
51436
|
+
} catch (err) {
|
|
51437
|
+
io.warn(`Couldn't install them (${err.message}). Run: ${command}`);
|
|
51438
|
+
return false;
|
|
51439
|
+
}
|
|
51440
|
+
}
|
|
51441
|
+
async function connectProject(root, config2, setup2, io, ask) {
|
|
51442
|
+
const existing = await setup2.connection(root).catch(() => null);
|
|
51443
|
+
if (existing) {
|
|
51444
|
+
io.log(`Connected to Revyse: project "${existing.project}" (${existing.email}).`);
|
|
51445
|
+
return existing;
|
|
51446
|
+
}
|
|
51447
|
+
if (!await ask(
|
|
51448
|
+
"Connect this app to Revyse? Your browser opens to sign in and create the project."
|
|
51449
|
+
))
|
|
51450
|
+
return null;
|
|
51451
|
+
try {
|
|
51452
|
+
const connection = await setup2.connect(root, {
|
|
51453
|
+
name: projectName(root),
|
|
51454
|
+
framework: config2.framework,
|
|
51455
|
+
packageManager: config2.packageManager
|
|
51456
|
+
});
|
|
51457
|
+
io.log(`Connected to Revyse: project "${connection.project}" (${connection.email}).`);
|
|
51458
|
+
return connection;
|
|
51459
|
+
} catch (err) {
|
|
51460
|
+
io.warn(`Couldn't connect to Revyse (${err.message}).`);
|
|
51461
|
+
return null;
|
|
51462
|
+
}
|
|
51463
|
+
}
|
|
51464
|
+
function projectName(root) {
|
|
51465
|
+
const name = readPackageJson(root)?.name?.replace(/^@[^/]+\//, "").trim();
|
|
51466
|
+
return (name || path29.basename(root)).slice(0, 100);
|
|
51045
51467
|
}
|
|
51046
51468
|
function ensureGitignored(root) {
|
|
51047
|
-
const file2 =
|
|
51048
|
-
const current =
|
|
51469
|
+
const file2 = path29.join(root, ".gitignore");
|
|
51470
|
+
const current = existsSync8(file2) ? readFileSync12(file2, "utf8") : "";
|
|
51049
51471
|
const lines = current.split(/\r?\n/).map((l) => l.trim());
|
|
51050
51472
|
if (lines.includes(GITIGNORE_ENTRY) || lines.includes(".revyse")) return false;
|
|
51051
51473
|
const separator = current === "" ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
51052
|
-
|
|
51474
|
+
writeFileSync6(file2, `${current}${separator}# Revyse local state
|
|
51053
51475
|
${GITIGNORE_ENTRY}
|
|
51054
51476
|
`);
|
|
51055
51477
|
return true;
|
|
51056
51478
|
}
|
|
51057
|
-
function
|
|
51479
|
+
function readPackageJson(root) {
|
|
51058
51480
|
try {
|
|
51059
|
-
|
|
51060
|
-
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
51481
|
+
return JSON.parse(readFileSync12(path29.join(root, "package.json"), "utf8"));
|
|
51061
51482
|
} catch {
|
|
51062
|
-
return
|
|
51483
|
+
return null;
|
|
51484
|
+
}
|
|
51485
|
+
}
|
|
51486
|
+
var devDependencies = (root) => readPackageJson(root)?.devDependencies ?? {};
|
|
51487
|
+
|
|
51488
|
+
// src/connect.ts
|
|
51489
|
+
function openBrowser(url2) {
|
|
51490
|
+
if (process.env.BROWSER === "none") return;
|
|
51491
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2.replace(/&/g, "^&")]] : ["xdg-open", [url2]];
|
|
51492
|
+
try {
|
|
51493
|
+
const child = spawn2(command, args, { stdio: "ignore", detached: true });
|
|
51494
|
+
child.on("error", () => {
|
|
51495
|
+
});
|
|
51496
|
+
child.unref();
|
|
51497
|
+
} catch {
|
|
51498
|
+
}
|
|
51499
|
+
}
|
|
51500
|
+
function activationUrl(verificationUriComplete, suggestion) {
|
|
51501
|
+
if (!suggestion) return verificationUriComplete;
|
|
51502
|
+
const url2 = new URL(verificationUriComplete);
|
|
51503
|
+
url2.searchParams.set("name", suggestion.name);
|
|
51504
|
+
url2.searchParams.set("framework", suggestion.framework);
|
|
51505
|
+
url2.searchParams.set("pm", suggestion.packageManager);
|
|
51506
|
+
return url2.toString();
|
|
51507
|
+
}
|
|
51508
|
+
async function connectInBrowser(opts) {
|
|
51509
|
+
const { token, projectId } = await deviceLogin({
|
|
51510
|
+
client: createApiClient({ apiUrl: opts.apiUrl }),
|
|
51511
|
+
clientName: `revyse-cli on ${hostname3()}`,
|
|
51512
|
+
onCode: ({ userCode, verificationUriComplete }) => {
|
|
51513
|
+
const url2 = activationUrl(verificationUriComplete, opts.suggestion);
|
|
51514
|
+
opts.log(`
|
|
51515
|
+
Your browser is opening to connect this app to Revyse. If it doesn't, go to:
|
|
51516
|
+
`);
|
|
51517
|
+
opts.log(` ${url2}
|
|
51518
|
+
`);
|
|
51519
|
+
opts.log(`and check the code matches: ${userCode}
|
|
51520
|
+
`);
|
|
51521
|
+
opts.log("Waiting for you to approve it\u2026");
|
|
51522
|
+
(opts.open ?? openBrowser)(url2);
|
|
51523
|
+
}
|
|
51524
|
+
});
|
|
51525
|
+
saveCredential(opts.root, { apiUrl: opts.apiUrl, token, projectId });
|
|
51526
|
+
const me = await createApiClient({ apiUrl: opts.apiUrl, token }).get("/api/cli/whoami");
|
|
51527
|
+
return { email: me.user.email, project: me.project.name };
|
|
51528
|
+
}
|
|
51529
|
+
async function currentConnection(root) {
|
|
51530
|
+
const cred = loadCredential(root);
|
|
51531
|
+
if (!cred) return null;
|
|
51532
|
+
try {
|
|
51533
|
+
const me = await createApiClient(cred).get("/api/cli/whoami");
|
|
51534
|
+
return { email: me.user.email, project: me.project.name };
|
|
51535
|
+
} catch (err) {
|
|
51536
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 404)) return null;
|
|
51537
|
+
throw err;
|
|
51063
51538
|
}
|
|
51064
51539
|
}
|
|
51540
|
+
function initSetup(apiUrl, log) {
|
|
51541
|
+
return {
|
|
51542
|
+
install: async (root, pm, specs) => {
|
|
51543
|
+
const [command, ...args] = installCommand(pm, specs);
|
|
51544
|
+
await execa(command, args, { cwd: root, stdio: "inherit" });
|
|
51545
|
+
},
|
|
51546
|
+
connection: currentConnection,
|
|
51547
|
+
connect: (root, suggestion) => connectInBrowser({ root, apiUrl, suggestion, log })
|
|
51548
|
+
};
|
|
51549
|
+
}
|
|
51065
51550
|
|
|
51066
51551
|
// src/prompt.ts
|
|
51067
51552
|
import { createInterface } from "readline";
|
|
@@ -51092,18 +51577,29 @@ function createPrompter(input2 = process.stdin, output2 = process.stdout) {
|
|
|
51092
51577
|
}
|
|
51093
51578
|
|
|
51094
51579
|
// src/commands/init.ts
|
|
51095
|
-
var initCommand = new Command("init").description(
|
|
51096
|
-
|
|
51097
|
-
|
|
51098
|
-
|
|
51099
|
-
|
|
51100
|
-
|
|
51101
|
-
|
|
51102
|
-
|
|
51103
|
-
|
|
51104
|
-
|
|
51580
|
+
var initCommand = new Command("init").description(
|
|
51581
|
+
"Set Revyse up in this app: the data disclosure, next.config, the packages, and the project on Revyse"
|
|
51582
|
+
).option("--force", "re-run the disclosure and next.config steps when revyse.config.json exists").option("-y, --yes", "accept the disclosure and proposed edits without prompting").option("--no-connect", "set up without connecting to Revyse (CI, offline)").option("--api-url <url>", "Revyse API URL (default: $REVYSE_API_URL or https://revyse.dev)").action(
|
|
51583
|
+
async (options) => {
|
|
51584
|
+
const prompter = createPrompter();
|
|
51585
|
+
const log = (m) => console.log(m);
|
|
51586
|
+
try {
|
|
51587
|
+
const outcome = await runInit(
|
|
51588
|
+
{
|
|
51589
|
+
root: process.cwd(),
|
|
51590
|
+
force: options.force,
|
|
51591
|
+
yes: options.yes,
|
|
51592
|
+
connect: options.connect,
|
|
51593
|
+
setup: initSetup(resolveApiUrl(options.apiUrl), log)
|
|
51594
|
+
},
|
|
51595
|
+
{ log, warn: (m) => console.warn(m), confirm: prompter.confirm }
|
|
51596
|
+
);
|
|
51597
|
+
if (outcome.status === "declined") process.exitCode = 1;
|
|
51598
|
+
} finally {
|
|
51599
|
+
prompter.close();
|
|
51600
|
+
}
|
|
51105
51601
|
}
|
|
51106
|
-
|
|
51602
|
+
);
|
|
51107
51603
|
|
|
51108
51604
|
// src/commands/change.ts
|
|
51109
51605
|
var changeCommand = new Command("change").description("Headless change operations");
|
|
@@ -51117,7 +51613,7 @@ var common2 = (cmd) => cmd.requiredOption("--api-url <url>", "Revyse API URL").r
|
|
|
51117
51613
|
function setup(opts) {
|
|
51118
51614
|
const root = process.cwd();
|
|
51119
51615
|
const config2 = projectConfigSchema.parse(
|
|
51120
|
-
JSON.parse(
|
|
51616
|
+
JSON.parse(readFileSync13(path30.join(root, CONFIG_FILE), "utf8"))
|
|
51121
51617
|
);
|
|
51122
51618
|
const token = opts.token ?? process.env.REVYSE_TOKEN;
|
|
51123
51619
|
if (!token) throw new Error("A project token is required (--token or REVYSE_TOKEN)");
|