@wrongstack/tools 0.285.0 → 0.286.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/browser/artifacts.d.ts +13 -0
- package/dist/browser/artifacts.d.ts.map +1 -0
- package/dist/browser/index.d.ts +7 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +1264 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manager.d.ts +57 -0
- package/dist/browser/manager.d.ts.map +1 -0
- package/dist/browser/network-guard-proxy.d.ts +23 -0
- package/dist/browser/network-guard-proxy.d.ts.map +1 -0
- package/dist/browser/security.d.ts +29 -0
- package/dist/browser/security.d.ts.map +1 -0
- package/dist/browser/tools.d.ts +62 -0
- package/dist/browser/tools.d.ts.map +1 -0
- package/dist/browser/types.d.ts +60 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/builtin.d.ts +4 -3
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +2000 -342
- package/dist/builtin.js.map +4 -4
- package/dist/e2e.d.ts +56 -0
- package/dist/e2e.d.ts.map +1 -0
- package/dist/exec.js +1 -1
- package/dist/exec.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2048 -357
- package/dist/index.js.map +4 -4
- package/dist/pack.js +2017 -343
- package/dist/pack.js.map +4 -4
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +8 -2
- package/dist/search.js.map +2 -2
- package/dist/tool-icon-map.d.ts +2 -1
- package/dist/tool-icon-map.d.ts.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -123,7 +123,7 @@ function createOutputSpool(opts) {
|
|
|
123
123
|
let filePath = null;
|
|
124
124
|
let failed = false;
|
|
125
125
|
let finalized = false;
|
|
126
|
-
const
|
|
126
|
+
const open3 = () => {
|
|
127
127
|
if (stream || failed) return;
|
|
128
128
|
try {
|
|
129
129
|
const dir = toolOutputDir();
|
|
@@ -156,7 +156,7 @@ function createOutputSpool(opts) {
|
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
head += text;
|
|
159
|
-
|
|
159
|
+
open3();
|
|
160
160
|
head = "";
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
@@ -910,8 +910,8 @@ async function* spawnStream(opts) {
|
|
|
910
910
|
try {
|
|
911
911
|
for (; ; ) {
|
|
912
912
|
while (queue.length === 0) {
|
|
913
|
-
await new Promise((
|
|
914
|
-
waiter =
|
|
913
|
+
await new Promise((resolve15) => {
|
|
914
|
+
waiter = resolve15;
|
|
915
915
|
});
|
|
916
916
|
}
|
|
917
917
|
const chunk = queue.shift();
|
|
@@ -985,14 +985,14 @@ function sha256hex(content) {
|
|
|
985
985
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
986
986
|
}
|
|
987
987
|
async function detectPackageManager(cwd) {
|
|
988
|
-
const { stat:
|
|
988
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
989
989
|
try {
|
|
990
|
-
await
|
|
990
|
+
await stat18(`${cwd}/pnpm-lock.yaml`);
|
|
991
991
|
return "pnpm";
|
|
992
992
|
} catch {
|
|
993
993
|
}
|
|
994
994
|
try {
|
|
995
|
-
await
|
|
995
|
+
await stat18(`${cwd}/yarn.lock`);
|
|
996
996
|
return "yarn";
|
|
997
997
|
} catch {
|
|
998
998
|
}
|
|
@@ -2561,8 +2561,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
|
|
|
2561
2561
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2562
2562
|
}
|
|
2563
2563
|
}
|
|
2564
|
-
function collectFileEvidence(directory, fullPath,
|
|
2565
|
-
const lower =
|
|
2564
|
+
function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
|
|
2565
|
+
const lower = basename8.toLowerCase();
|
|
2566
2566
|
const extension = path4.extname(lower);
|
|
2567
2567
|
for (const profile of profiles) {
|
|
2568
2568
|
const detector = profile.detectors.find(
|
|
@@ -2573,7 +2573,7 @@ function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
|
|
|
2573
2573
|
candidate.evidence.push({
|
|
2574
2574
|
kind: detector.kind,
|
|
2575
2575
|
path: fullPath,
|
|
2576
|
-
value:
|
|
2576
|
+
value: basename8,
|
|
2577
2577
|
weight: detector.weight
|
|
2578
2578
|
});
|
|
2579
2579
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2701,8 +2701,8 @@ function normalizeLimits(input) {
|
|
|
2701
2701
|
async function canonicalDirectory(input) {
|
|
2702
2702
|
const resolved = path4.resolve(input);
|
|
2703
2703
|
const real = await fs2.realpath(resolved);
|
|
2704
|
-
const
|
|
2705
|
-
if (!
|
|
2704
|
+
const stat18 = await fs2.stat(real);
|
|
2705
|
+
if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2706
2706
|
return real;
|
|
2707
2707
|
}
|
|
2708
2708
|
async function canonicalInside(input, root, label) {
|
|
@@ -2722,12 +2722,12 @@ function resolveFrom(cwd, input) {
|
|
|
2722
2722
|
return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
|
|
2723
2723
|
}
|
|
2724
2724
|
function isInside(candidate, root) {
|
|
2725
|
-
const
|
|
2726
|
-
return
|
|
2725
|
+
const relative13 = path4.relative(root, candidate);
|
|
2726
|
+
return relative13 === "" || !relative13.startsWith("..") && !path4.isAbsolute(relative13);
|
|
2727
2727
|
}
|
|
2728
2728
|
function pathDepth(candidate, root) {
|
|
2729
|
-
const
|
|
2730
|
-
return
|
|
2729
|
+
const relative13 = path4.relative(root, candidate);
|
|
2730
|
+
return relative13 === "" ? 0 : relative13.split(path4.sep).length;
|
|
2731
2731
|
}
|
|
2732
2732
|
function dedupeEvidence(items) {
|
|
2733
2733
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3427,8 +3427,8 @@ function compareCandidates(a, b) {
|
|
|
3427
3427
|
return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
|
|
3428
3428
|
}
|
|
3429
3429
|
function workspaceDepth(workspaceRoot, projectRoot) {
|
|
3430
|
-
const
|
|
3431
|
-
return
|
|
3430
|
+
const relative13 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3431
|
+
return relative13 === "" ? 0 : relative13.split(path6.sep).length;
|
|
3432
3432
|
}
|
|
3433
3433
|
function validateOperationOptions(operation, packages) {
|
|
3434
3434
|
if (!operation.startsWith("package-") || !packages) return;
|
|
@@ -3458,8 +3458,8 @@ async function canonicalTarget(cwd, target, projectRoot) {
|
|
|
3458
3458
|
return real;
|
|
3459
3459
|
}
|
|
3460
3460
|
function isInside2(candidate, root) {
|
|
3461
|
-
const
|
|
3462
|
-
return
|
|
3461
|
+
const relative13 = path6.relative(path6.resolve(root), path6.resolve(candidate));
|
|
3462
|
+
return relative13 === "" || !relative13.startsWith("..") && !path6.isAbsolute(relative13);
|
|
3463
3463
|
}
|
|
3464
3464
|
var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
|
|
3465
3465
|
var init_plan = __esm({
|
|
@@ -3590,8 +3590,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3590
3590
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3591
3591
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3592
3592
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3593
|
-
const
|
|
3594
|
-
if (
|
|
3593
|
+
const stat18 = await fs4.stat(safeTarget);
|
|
3594
|
+
if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3595
3595
|
return unavailableResult(
|
|
3596
3596
|
options,
|
|
3597
3597
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -3638,14 +3638,14 @@ async function validatePlanRealpaths(plan, projectRoot) {
|
|
|
3638
3638
|
return void 0;
|
|
3639
3639
|
}
|
|
3640
3640
|
function isRealInside(candidate, root) {
|
|
3641
|
-
const
|
|
3642
|
-
return
|
|
3641
|
+
const relative13 = path7.relative(root, candidate);
|
|
3642
|
+
return relative13 === "" || !relative13.startsWith("..") && !path7.isAbsolute(relative13);
|
|
3643
3643
|
}
|
|
3644
3644
|
async function assertContainedFile(candidate, projectRoot) {
|
|
3645
3645
|
const realRoot = await fs4.realpath(projectRoot);
|
|
3646
3646
|
const realTarget = await fs4.realpath(candidate);
|
|
3647
|
-
const
|
|
3648
|
-
if (
|
|
3647
|
+
const relative13 = path7.relative(realRoot, realTarget);
|
|
3648
|
+
if (relative13.startsWith("..") || path7.isAbsolute(relative13)) {
|
|
3649
3649
|
throw new Error(`Internal syntax target resolves outside project root: ${candidate}`);
|
|
3650
3650
|
}
|
|
3651
3651
|
return realTarget;
|
|
@@ -3878,8 +3878,8 @@ async function snapshotPaths(paths) {
|
|
|
3878
3878
|
const existing = [];
|
|
3879
3879
|
for (const candidate of paths) {
|
|
3880
3880
|
try {
|
|
3881
|
-
const
|
|
3882
|
-
if (!
|
|
3881
|
+
const stat18 = await fs4.stat(candidate);
|
|
3882
|
+
if (!stat18.isFile()) continue;
|
|
3883
3883
|
existing.push(candidate);
|
|
3884
3884
|
} catch {
|
|
3885
3885
|
}
|
|
@@ -3890,15 +3890,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
|
|
|
3890
3890
|
const beforeSet = new Set(before);
|
|
3891
3891
|
const afterSet = new Set(after);
|
|
3892
3892
|
const changed = /* @__PURE__ */ new Set();
|
|
3893
|
-
for (const
|
|
3894
|
-
if (!beforeSet.has(
|
|
3893
|
+
for (const path33 of after) {
|
|
3894
|
+
if (!beforeSet.has(path33)) changed.add(path33);
|
|
3895
3895
|
}
|
|
3896
|
-
for (const
|
|
3897
|
-
if (!afterSet.has(
|
|
3896
|
+
for (const path33 of before) {
|
|
3897
|
+
if (!afterSet.has(path33)) changed.add(path33);
|
|
3898
3898
|
}
|
|
3899
3899
|
if (beforeSizes && afterSizes) {
|
|
3900
|
-
for (const
|
|
3901
|
-
if (beforeSizes.get(
|
|
3900
|
+
for (const path33 of after) {
|
|
3901
|
+
if (beforeSizes.get(path33) !== afterSizes.get(path33)) changed.add(path33);
|
|
3902
3902
|
}
|
|
3903
3903
|
}
|
|
3904
3904
|
return [...changed].sort();
|
|
@@ -3907,8 +3907,8 @@ async function snapshotSizes(paths) {
|
|
|
3907
3907
|
const sizes = /* @__PURE__ */ new Map();
|
|
3908
3908
|
for (const candidate of paths) {
|
|
3909
3909
|
try {
|
|
3910
|
-
const
|
|
3911
|
-
if (
|
|
3910
|
+
const stat18 = await fs4.stat(candidate);
|
|
3911
|
+
if (stat18.isFile()) sizes.set(candidate, stat18.size);
|
|
3912
3912
|
} catch {
|
|
3913
3913
|
}
|
|
3914
3914
|
}
|
|
@@ -5161,8 +5161,8 @@ var auditTool = {
|
|
|
5161
5161
|
yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
|
|
5162
5162
|
}
|
|
5163
5163
|
};
|
|
5164
|
-
function parseAuditOutput(
|
|
5165
|
-
if (!
|
|
5164
|
+
function parseAuditOutput(json2, exitCode) {
|
|
5165
|
+
if (!json2) {
|
|
5166
5166
|
return {
|
|
5167
5167
|
exit_code: exitCode,
|
|
5168
5168
|
vulnerabilities: [],
|
|
@@ -5173,7 +5173,7 @@ function parseAuditOutput(json, exitCode) {
|
|
|
5173
5173
|
};
|
|
5174
5174
|
}
|
|
5175
5175
|
try {
|
|
5176
|
-
const data = JSON.parse(
|
|
5176
|
+
const data = JSON.parse(json2);
|
|
5177
5177
|
const advisories = [];
|
|
5178
5178
|
const ads = data.advisories ?? {};
|
|
5179
5179
|
for (const id of Object.keys(ads)) {
|
|
@@ -5192,8 +5192,8 @@ function parseAuditOutput(json, exitCode) {
|
|
|
5192
5192
|
vulnerabilities: advisories,
|
|
5193
5193
|
total,
|
|
5194
5194
|
summary,
|
|
5195
|
-
output:
|
|
5196
|
-
truncated:
|
|
5195
|
+
output: json2,
|
|
5196
|
+
truncated: json2.length > 1e5
|
|
5197
5197
|
};
|
|
5198
5198
|
} catch {
|
|
5199
5199
|
return {
|
|
@@ -5201,7 +5201,7 @@ function parseAuditOutput(json, exitCode) {
|
|
|
5201
5201
|
vulnerabilities: [],
|
|
5202
5202
|
total: 0,
|
|
5203
5203
|
summary: "Could not parse audit output",
|
|
5204
|
-
output:
|
|
5204
|
+
output: json2,
|
|
5205
5205
|
truncated: false
|
|
5206
5206
|
};
|
|
5207
5207
|
}
|
|
@@ -6235,10 +6235,10 @@ var bashTool = {
|
|
|
6235
6235
|
queue.push(c);
|
|
6236
6236
|
}
|
|
6237
6237
|
};
|
|
6238
|
-
const next = () => new Promise((
|
|
6238
|
+
const next = () => new Promise((resolve15) => {
|
|
6239
6239
|
const c = queue.shift();
|
|
6240
|
-
if (c)
|
|
6241
|
-
else resolveNext =
|
|
6240
|
+
if (c) resolve15(c);
|
|
6241
|
+
else resolveNext = resolve15;
|
|
6242
6242
|
});
|
|
6243
6243
|
let lastFlush = Date.now();
|
|
6244
6244
|
const flush = () => {
|
|
@@ -6478,6 +6478,1241 @@ async function executeSingle(call, ctx, opts) {
|
|
|
6478
6478
|
}
|
|
6479
6479
|
}
|
|
6480
6480
|
|
|
6481
|
+
// src/browser/artifacts.ts
|
|
6482
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6483
|
+
import { createReadStream } from "node:fs";
|
|
6484
|
+
import * as fs7 from "node:fs/promises";
|
|
6485
|
+
import * as path10 from "node:path";
|
|
6486
|
+
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
6487
|
+
var BrowserArtifactStore = class {
|
|
6488
|
+
constructor(root) {
|
|
6489
|
+
this.root = root;
|
|
6490
|
+
}
|
|
6491
|
+
root;
|
|
6492
|
+
async write(sessionId, kind, extension, mimeType, content) {
|
|
6493
|
+
const id = ulid();
|
|
6494
|
+
const dir = path10.join(this.root, safeSegment(sessionId));
|
|
6495
|
+
const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
|
|
6496
|
+
await atomicWrite(target, content, { mode: 384 });
|
|
6497
|
+
return this.record(id, sessionId, kind, target, mimeType);
|
|
6498
|
+
}
|
|
6499
|
+
async record(id, sessionId, kind, target, mimeType) {
|
|
6500
|
+
await fs7.chmod(target, 384).catch(() => void 0);
|
|
6501
|
+
const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
6502
|
+
const artifact = {
|
|
6503
|
+
id,
|
|
6504
|
+
kind,
|
|
6505
|
+
sensitivity: "sensitive",
|
|
6506
|
+
path: target,
|
|
6507
|
+
mimeType,
|
|
6508
|
+
sizeBytes: stat18.size,
|
|
6509
|
+
sha256,
|
|
6510
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6511
|
+
};
|
|
6512
|
+
const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
|
|
6513
|
+
try {
|
|
6514
|
+
await atomicWrite(
|
|
6515
|
+
metadataPath,
|
|
6516
|
+
`${JSON.stringify({
|
|
6517
|
+
id: artifact.id,
|
|
6518
|
+
sessionId: safeSegment(sessionId),
|
|
6519
|
+
kind: artifact.kind,
|
|
6520
|
+
sensitivity: artifact.sensitivity,
|
|
6521
|
+
mimeType: artifact.mimeType,
|
|
6522
|
+
sizeBytes: artifact.sizeBytes,
|
|
6523
|
+
sha256: artifact.sha256,
|
|
6524
|
+
createdAt: artifact.createdAt
|
|
6525
|
+
})}
|
|
6526
|
+
`,
|
|
6527
|
+
{ mode: 384 }
|
|
6528
|
+
);
|
|
6529
|
+
} catch (error) {
|
|
6530
|
+
await fs7.rm(target, { force: true }).catch(() => void 0);
|
|
6531
|
+
throw error;
|
|
6532
|
+
}
|
|
6533
|
+
return artifact;
|
|
6534
|
+
}
|
|
6535
|
+
pathFor(sessionId, extension) {
|
|
6536
|
+
const id = ulid();
|
|
6537
|
+
return {
|
|
6538
|
+
id,
|
|
6539
|
+
path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
|
|
6540
|
+
};
|
|
6541
|
+
}
|
|
6542
|
+
async describe(id, kind, target, mimeType) {
|
|
6543
|
+
return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
|
|
6544
|
+
}
|
|
6545
|
+
};
|
|
6546
|
+
async function hashFile(target) {
|
|
6547
|
+
const hash = createHash3("sha256");
|
|
6548
|
+
await new Promise((resolve15, reject) => {
|
|
6549
|
+
const stream = createReadStream(target);
|
|
6550
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
6551
|
+
stream.once("error", reject);
|
|
6552
|
+
stream.once("end", resolve15);
|
|
6553
|
+
});
|
|
6554
|
+
return hash.digest("hex");
|
|
6555
|
+
}
|
|
6556
|
+
function safeSegment(value) {
|
|
6557
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6560
|
+
// src/browser/manager.ts
|
|
6561
|
+
import * as fs8 from "node:fs/promises";
|
|
6562
|
+
import * as path11 from "node:path";
|
|
6563
|
+
import { ulid as ulid2 } from "@wrongstack/core/utils";
|
|
6564
|
+
|
|
6565
|
+
// src/browser/network-guard-proxy.ts
|
|
6566
|
+
import * as http from "node:http";
|
|
6567
|
+
import * as net2 from "node:net";
|
|
6568
|
+
|
|
6569
|
+
// src/browser/security.ts
|
|
6570
|
+
import * as dns from "node:dns/promises";
|
|
6571
|
+
import * as net from "node:net";
|
|
6572
|
+
import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
6573
|
+
var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
|
|
6574
|
+
async function assertBrowserUrlAllowed(rawUrl, opts) {
|
|
6575
|
+
const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
|
|
6576
|
+
if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6577
|
+
if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
|
|
6578
|
+
await assertNotPrivateHost(url.hostname);
|
|
6579
|
+
}
|
|
6580
|
+
return url;
|
|
6581
|
+
}
|
|
6582
|
+
async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
|
|
6583
|
+
const url = parseBrowserUrl(rawUrl, true);
|
|
6584
|
+
const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
|
|
6585
|
+
const hostname4 = unbracket(url.hostname);
|
|
6586
|
+
const literalFamily = net.isIP(hostname4);
|
|
6587
|
+
if (literalFamily === 4 || literalFamily === 6) {
|
|
6588
|
+
if (!allowPrivate && isPrivateAddress(hostname4, literalFamily)) {
|
|
6589
|
+
throw new Error(`browser: blocked private/loopback address "${hostname4}"`);
|
|
6590
|
+
}
|
|
6591
|
+
return { url, address: hostname4, family: literalFamily };
|
|
6592
|
+
}
|
|
6593
|
+
if ((hostname4 === "localhost" || hostname4.endsWith(".localhost")) && !allowPrivate) {
|
|
6594
|
+
throw new Error("browser: blocked localhost target");
|
|
6595
|
+
}
|
|
6596
|
+
const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
|
|
6597
|
+
let records;
|
|
6598
|
+
try {
|
|
6599
|
+
records = await lookup3(hostname4);
|
|
6600
|
+
} catch (error) {
|
|
6601
|
+
throw new Error(
|
|
6602
|
+
`browser: DNS resolution failed for "${hostname4}": ${error instanceof Error ? error.message : String(error)}`
|
|
6603
|
+
);
|
|
6604
|
+
}
|
|
6605
|
+
if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname4}"`);
|
|
6606
|
+
for (const record of records) {
|
|
6607
|
+
if (record.family !== 4 && record.family !== 6) {
|
|
6608
|
+
throw new Error(`browser: DNS returned an unsupported address family for "${hostname4}"`);
|
|
6609
|
+
}
|
|
6610
|
+
if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
|
|
6611
|
+
throw new Error(`browser: resolved to private address ${record.address}`);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
const selected = records[0];
|
|
6615
|
+
return { url, address: selected.address, family: selected.family };
|
|
6616
|
+
}
|
|
6617
|
+
function parsePrivateOriginAllowlist(raw) {
|
|
6618
|
+
if (!raw?.trim()) return [];
|
|
6619
|
+
const origins = /* @__PURE__ */ new Set();
|
|
6620
|
+
for (const entry of raw.split(",")) {
|
|
6621
|
+
const candidate = entry.trim();
|
|
6622
|
+
if (!candidate) continue;
|
|
6623
|
+
const url = parseBrowserUrl(candidate, true);
|
|
6624
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
6625
|
+
throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
|
|
6626
|
+
}
|
|
6627
|
+
origins.add(url.origin);
|
|
6628
|
+
}
|
|
6629
|
+
return [...origins];
|
|
6630
|
+
}
|
|
6631
|
+
function parseBrowserUrl(rawUrl, navigation) {
|
|
6632
|
+
let url;
|
|
6633
|
+
try {
|
|
6634
|
+
url = new URL(rawUrl);
|
|
6635
|
+
} catch {
|
|
6636
|
+
throw new Error("browser: url must be an absolute URL");
|
|
6637
|
+
}
|
|
6638
|
+
if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6639
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
6640
|
+
throw new Error(`browser: unsupported protocol "${url.protocol}"`);
|
|
6641
|
+
}
|
|
6642
|
+
if (url.username || url.password) {
|
|
6643
|
+
throw new Error("browser: credentials in URLs are not allowed");
|
|
6644
|
+
}
|
|
6645
|
+
return url;
|
|
6646
|
+
}
|
|
6647
|
+
function isAllowedPrivateOrigin(url, origins) {
|
|
6648
|
+
return origins?.includes(url.origin) ?? false;
|
|
6649
|
+
}
|
|
6650
|
+
function isPrivateAddress(address, family) {
|
|
6651
|
+
return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
|
|
6652
|
+
}
|
|
6653
|
+
function unbracket(hostname4) {
|
|
6654
|
+
return hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
|
|
6655
|
+
}
|
|
6656
|
+
function safeBrowserUrl(rawUrl) {
|
|
6657
|
+
try {
|
|
6658
|
+
const url = new URL(rawUrl);
|
|
6659
|
+
url.username = "";
|
|
6660
|
+
url.password = "";
|
|
6661
|
+
url.search = "";
|
|
6662
|
+
url.hash = "";
|
|
6663
|
+
return url.toString();
|
|
6664
|
+
} catch {
|
|
6665
|
+
return "about:blank";
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
function redactBrowserText(text) {
|
|
6669
|
+
return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
|
|
6670
|
+
/\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
|
|
6671
|
+
"$1=[REDACTED]"
|
|
6672
|
+
);
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
// src/browser/network-guard-proxy.ts
|
|
6676
|
+
var BrowserNetworkGuardProxy = class {
|
|
6677
|
+
constructor(options = {}) {
|
|
6678
|
+
this.options = options;
|
|
6679
|
+
this.server = http.createServer((request2, response) => {
|
|
6680
|
+
void this.forwardHttp(request2, response);
|
|
6681
|
+
});
|
|
6682
|
+
this.server.on("connect", (request2, socket, head) => {
|
|
6683
|
+
void this.forwardConnect(request2, socket, head);
|
|
6684
|
+
});
|
|
6685
|
+
this.server.on("upgrade", (request2, socket, head) => {
|
|
6686
|
+
void this.forwardUpgrade(request2, socket, head);
|
|
6687
|
+
});
|
|
6688
|
+
this.server.on("connection", (socket) => {
|
|
6689
|
+
this.sockets.add(socket);
|
|
6690
|
+
socket.once("close", () => this.sockets.delete(socket));
|
|
6691
|
+
});
|
|
6692
|
+
this.server.on("clientError", (_error, socket) => {
|
|
6693
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
6694
|
+
});
|
|
6695
|
+
}
|
|
6696
|
+
options;
|
|
6697
|
+
server;
|
|
6698
|
+
sockets = /* @__PURE__ */ new Set();
|
|
6699
|
+
startPromise;
|
|
6700
|
+
url;
|
|
6701
|
+
async start() {
|
|
6702
|
+
if (this.url) return this.url;
|
|
6703
|
+
if (this.startPromise) return this.startPromise;
|
|
6704
|
+
this.startPromise = new Promise((resolve15, reject) => {
|
|
6705
|
+
const onError = (error) => {
|
|
6706
|
+
this.server.off("listening", onListening);
|
|
6707
|
+
reject(error);
|
|
6708
|
+
};
|
|
6709
|
+
const onListening = () => {
|
|
6710
|
+
this.server.off("error", onError);
|
|
6711
|
+
const address = this.server.address();
|
|
6712
|
+
if (!address || typeof address === "string") {
|
|
6713
|
+
reject(new Error("browser: network guard proxy failed to bind"));
|
|
6714
|
+
return;
|
|
6715
|
+
}
|
|
6716
|
+
this.url = `http://127.0.0.1:${address.port}`;
|
|
6717
|
+
resolve15(this.url);
|
|
6718
|
+
};
|
|
6719
|
+
this.server.once("error", onError);
|
|
6720
|
+
this.server.once("listening", onListening);
|
|
6721
|
+
this.server.listen(0, "127.0.0.1");
|
|
6722
|
+
}).finally(() => {
|
|
6723
|
+
this.startPromise = void 0;
|
|
6724
|
+
});
|
|
6725
|
+
return this.startPromise;
|
|
6726
|
+
}
|
|
6727
|
+
async close() {
|
|
6728
|
+
this.url = void 0;
|
|
6729
|
+
for (const socket of this.sockets) socket.destroy();
|
|
6730
|
+
this.sockets.clear();
|
|
6731
|
+
if (!this.server.listening) return;
|
|
6732
|
+
await new Promise((resolve15) => this.server.close(() => resolve15()));
|
|
6733
|
+
}
|
|
6734
|
+
async forwardHttp(request2, response) {
|
|
6735
|
+
try {
|
|
6736
|
+
const target = await this.resolve(request2.url ?? "");
|
|
6737
|
+
if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
|
|
6738
|
+
const headers = sanitizeHeaders(request2.headers);
|
|
6739
|
+
headers.host = target.url.host;
|
|
6740
|
+
const upstream = http.request({
|
|
6741
|
+
host: target.address,
|
|
6742
|
+
family: target.family,
|
|
6743
|
+
port: Number(target.url.port || 80),
|
|
6744
|
+
method: request2.method,
|
|
6745
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6746
|
+
headers,
|
|
6747
|
+
agent: false
|
|
6748
|
+
});
|
|
6749
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
|
|
6750
|
+
upstream.destroy(new Error("browser: upstream connection timed out"));
|
|
6751
|
+
});
|
|
6752
|
+
upstream.on("response", (upstreamResponse) => {
|
|
6753
|
+
response.writeHead(
|
|
6754
|
+
upstreamResponse.statusCode ?? 502,
|
|
6755
|
+
sanitizeHeaders(upstreamResponse.headers)
|
|
6756
|
+
);
|
|
6757
|
+
upstreamResponse.pipe(response);
|
|
6758
|
+
});
|
|
6759
|
+
upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
|
|
6760
|
+
request2.on("aborted", () => upstream.destroy());
|
|
6761
|
+
request2.pipe(upstream);
|
|
6762
|
+
} catch {
|
|
6763
|
+
writeProxyError(response, 403, "Blocked by browser network policy");
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
async forwardConnect(request2, client, head) {
|
|
6767
|
+
try {
|
|
6768
|
+
const authority = request2.url ?? "";
|
|
6769
|
+
const target = await this.resolve(`https://${authority}`);
|
|
6770
|
+
const upstream = net2.connect({
|
|
6771
|
+
host: target.address,
|
|
6772
|
+
family: target.family,
|
|
6773
|
+
port: Number(target.url.port || 443)
|
|
6774
|
+
});
|
|
6775
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
|
|
6776
|
+
upstream.once("connect", () => {
|
|
6777
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
6778
|
+
if (head.length > 0) upstream.write(head);
|
|
6779
|
+
upstream.pipe(client);
|
|
6780
|
+
client.pipe(upstream);
|
|
6781
|
+
});
|
|
6782
|
+
upstream.once("error", () => {
|
|
6783
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6784
|
+
});
|
|
6785
|
+
client.once("close", () => upstream.destroy());
|
|
6786
|
+
} catch {
|
|
6787
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
async forwardUpgrade(request2, client, head) {
|
|
6791
|
+
let upstream;
|
|
6792
|
+
try {
|
|
6793
|
+
const rawUrl = request2.url ?? "";
|
|
6794
|
+
const websocketUrl = new URL(rawUrl);
|
|
6795
|
+
if (websocketUrl.protocol !== "ws:") {
|
|
6796
|
+
throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
|
|
6797
|
+
}
|
|
6798
|
+
websocketUrl.protocol = "http:";
|
|
6799
|
+
const target = await this.resolve(websocketUrl.toString());
|
|
6800
|
+
const headers = sanitizeHeaders(request2.headers, true);
|
|
6801
|
+
headers.host = target.url.host;
|
|
6802
|
+
upstream = http.request({
|
|
6803
|
+
host: target.address,
|
|
6804
|
+
family: target.family,
|
|
6805
|
+
port: Number(target.url.port || 80),
|
|
6806
|
+
method: request2.method,
|
|
6807
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6808
|
+
headers,
|
|
6809
|
+
agent: false
|
|
6810
|
+
});
|
|
6811
|
+
upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
6812
|
+
writeRawResponseHead(client, response);
|
|
6813
|
+
if (head.length > 0) upstreamSocket.write(head);
|
|
6814
|
+
if (upstreamHead.length > 0) client.write(upstreamHead);
|
|
6815
|
+
upstreamSocket.pipe(client);
|
|
6816
|
+
client.pipe(upstreamSocket);
|
|
6817
|
+
client.once("close", () => upstreamSocket.destroy());
|
|
6818
|
+
});
|
|
6819
|
+
upstream.once("response", (response) => {
|
|
6820
|
+
writeRawResponseHead(client, response);
|
|
6821
|
+
response.pipe(client);
|
|
6822
|
+
});
|
|
6823
|
+
upstream.once("error", () => {
|
|
6824
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6825
|
+
});
|
|
6826
|
+
client.once("close", () => upstream?.destroy());
|
|
6827
|
+
upstream.end();
|
|
6828
|
+
} catch {
|
|
6829
|
+
upstream?.destroy();
|
|
6830
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
resolve(rawUrl) {
|
|
6834
|
+
return resolvePinnedBrowserTarget(rawUrl, {
|
|
6835
|
+
allowPrivateHosts: this.options.allowPrivateHosts,
|
|
6836
|
+
allowedPrivateOrigins: this.options.allowedPrivateOrigins,
|
|
6837
|
+
lookup: this.options.lookup
|
|
6838
|
+
});
|
|
6839
|
+
}
|
|
6840
|
+
};
|
|
6841
|
+
function sanitizeHeaders(headers, preserveUpgrade = false) {
|
|
6842
|
+
const sanitized = { ...headers };
|
|
6843
|
+
delete sanitized["proxy-authorization"];
|
|
6844
|
+
delete sanitized["proxy-connection"];
|
|
6845
|
+
if (!preserveUpgrade) delete sanitized.connection;
|
|
6846
|
+
return sanitized;
|
|
6847
|
+
}
|
|
6848
|
+
function writeRawResponseHead(client, response) {
|
|
6849
|
+
const status = response.statusCode ?? 502;
|
|
6850
|
+
const statusText = response.statusMessage ?? "Bad Gateway";
|
|
6851
|
+
const lines = [`HTTP/1.1 ${status} ${statusText}`];
|
|
6852
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) {
|
|
6853
|
+
lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
|
|
6854
|
+
}
|
|
6855
|
+
client.write(`${lines.join("\r\n")}\r
|
|
6856
|
+
\r
|
|
6857
|
+
`);
|
|
6858
|
+
}
|
|
6859
|
+
function writeProxyError(response, status, message) {
|
|
6860
|
+
if (response.headersSent || response.destroyed) return;
|
|
6861
|
+
response.writeHead(status, { "content-type": "text/plain", connection: "close" });
|
|
6862
|
+
response.end(message);
|
|
6863
|
+
}
|
|
6864
|
+
|
|
6865
|
+
// src/browser/manager.ts
|
|
6866
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
|
|
6867
|
+
var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
|
|
6868
|
+
var DEFAULT_MAX_ENTRIES = 100;
|
|
6869
|
+
var BrowserSessionManager = class {
|
|
6870
|
+
constructor(options, launcher = defaultLauncher) {
|
|
6871
|
+
this.launcher = launcher;
|
|
6872
|
+
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
6873
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
6874
|
+
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
6875
|
+
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
6876
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6877
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins
|
|
6878
|
+
});
|
|
6879
|
+
this.headless = options.headless ?? true;
|
|
6880
|
+
this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
6881
|
+
this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
|
|
6882
|
+
this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6883
|
+
this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6884
|
+
}
|
|
6885
|
+
launcher;
|
|
6886
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6887
|
+
artifacts;
|
|
6888
|
+
allowPrivateHosts;
|
|
6889
|
+
allowedPrivateOrigins;
|
|
6890
|
+
networkProxy;
|
|
6891
|
+
headless;
|
|
6892
|
+
operationTimeoutMs;
|
|
6893
|
+
maxSnapshotChars;
|
|
6894
|
+
maxConsoleEntries;
|
|
6895
|
+
maxNetworkEntries;
|
|
6896
|
+
browser;
|
|
6897
|
+
launching;
|
|
6898
|
+
async open(ownerId, input, signal) {
|
|
6899
|
+
signal.throwIfAborted();
|
|
6900
|
+
if (input.url) {
|
|
6901
|
+
await assertBrowserUrlAllowed(input.url, {
|
|
6902
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6903
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6904
|
+
navigation: true
|
|
6905
|
+
});
|
|
6906
|
+
}
|
|
6907
|
+
const browser = await this.ensureBrowser(signal);
|
|
6908
|
+
const proxyServer = await this.networkProxy.start();
|
|
6909
|
+
const context = await abortable(
|
|
6910
|
+
signal,
|
|
6911
|
+
async () => {
|
|
6912
|
+
const created = await browser.newContext({
|
|
6913
|
+
acceptDownloads: false,
|
|
6914
|
+
ignoreHTTPSErrors: false,
|
|
6915
|
+
serviceWorkers: "block",
|
|
6916
|
+
proxy: { server: proxyServer },
|
|
6917
|
+
viewport: input.viewport ?? { width: 1440, height: 900 }
|
|
6918
|
+
});
|
|
6919
|
+
if (signal.aborted) {
|
|
6920
|
+
await created.close().catch(() => void 0);
|
|
6921
|
+
signal.throwIfAborted();
|
|
6922
|
+
}
|
|
6923
|
+
return created;
|
|
6924
|
+
},
|
|
6925
|
+
() => void 0
|
|
6926
|
+
);
|
|
6927
|
+
const session = {
|
|
6928
|
+
id: ulid2(),
|
|
6929
|
+
ownerId,
|
|
6930
|
+
context,
|
|
6931
|
+
page: await context.newPage(),
|
|
6932
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6933
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6934
|
+
tracing: input.trace ?? true,
|
|
6935
|
+
console: [],
|
|
6936
|
+
network: []
|
|
6937
|
+
};
|
|
6938
|
+
this.sessions.set(session.id, session);
|
|
6939
|
+
this.attachEvidenceCollectors(session);
|
|
6940
|
+
await context.route("**/*", async (route) => {
|
|
6941
|
+
try {
|
|
6942
|
+
await assertBrowserUrlAllowed(route.request().url(), {
|
|
6943
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6944
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6945
|
+
navigation: false
|
|
6946
|
+
});
|
|
6947
|
+
await route.continue();
|
|
6948
|
+
} catch {
|
|
6949
|
+
this.pushNetwork(session, {
|
|
6950
|
+
method: route.request().method(),
|
|
6951
|
+
url: safeBrowserUrl(route.request().url()),
|
|
6952
|
+
failed: true,
|
|
6953
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6954
|
+
});
|
|
6955
|
+
await route.abort("blockedbyclient");
|
|
6956
|
+
}
|
|
6957
|
+
});
|
|
6958
|
+
try {
|
|
6959
|
+
if (session.tracing) {
|
|
6960
|
+
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
6961
|
+
}
|
|
6962
|
+
if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
|
|
6963
|
+
return await this.summary(session);
|
|
6964
|
+
} catch (err) {
|
|
6965
|
+
await context.close().catch(() => void 0);
|
|
6966
|
+
this.sessions.delete(session.id);
|
|
6967
|
+
await this.closeBrowserIfIdle();
|
|
6968
|
+
throw err;
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
async list(ownerId) {
|
|
6972
|
+
const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
|
|
6973
|
+
return Promise.all(owned.map((session) => this.summary(session)));
|
|
6974
|
+
}
|
|
6975
|
+
async navigate(id, ownerId, rawUrl, signal) {
|
|
6976
|
+
await assertBrowserUrlAllowed(rawUrl, {
|
|
6977
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6978
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6979
|
+
navigation: true
|
|
6980
|
+
});
|
|
6981
|
+
const session = this.requireOwned(id, ownerId);
|
|
6982
|
+
await this.runPageOperation(
|
|
6983
|
+
session,
|
|
6984
|
+
signal,
|
|
6985
|
+
() => session.page.goto(rawUrl, {
|
|
6986
|
+
waitUntil: "domcontentloaded",
|
|
6987
|
+
timeout: this.operationTimeoutMs
|
|
6988
|
+
})
|
|
6989
|
+
);
|
|
6990
|
+
return this.summary(session);
|
|
6991
|
+
}
|
|
6992
|
+
async snapshot(id, ownerId, signal) {
|
|
6993
|
+
const session = this.requireOwned(id, ownerId);
|
|
6994
|
+
const raw = await this.runPageOperation(session, signal, async () => {
|
|
6995
|
+
try {
|
|
6996
|
+
return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
|
|
6997
|
+
} catch {
|
|
6998
|
+
return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
|
|
6999
|
+
}
|
|
7000
|
+
});
|
|
7001
|
+
const aria = redactBrowserText(raw);
|
|
7002
|
+
const truncated = aria.length > this.maxSnapshotChars;
|
|
7003
|
+
return {
|
|
7004
|
+
session: await this.summary(session),
|
|
7005
|
+
aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
|
|
7006
|
+
\u2026[snapshot truncated]` : aria,
|
|
7007
|
+
console: session.console.map((entry) => ({ ...entry })),
|
|
7008
|
+
network: session.network.map((entry) => ({ ...entry })),
|
|
7009
|
+
truncated
|
|
7010
|
+
};
|
|
7011
|
+
}
|
|
7012
|
+
async screenshot(id, ownerId, input, signal) {
|
|
7013
|
+
const session = this.requireOwned(id, ownerId);
|
|
7014
|
+
const bytes = await this.runPageOperation(
|
|
7015
|
+
session,
|
|
7016
|
+
signal,
|
|
7017
|
+
() => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
|
|
7018
|
+
);
|
|
7019
|
+
signal.throwIfAborted();
|
|
7020
|
+
return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
|
|
7021
|
+
}
|
|
7022
|
+
async click(id, ownerId, selector, signal) {
|
|
7023
|
+
const session = this.requireOwned(id, ownerId);
|
|
7024
|
+
await this.runPageOperation(
|
|
7025
|
+
session,
|
|
7026
|
+
signal,
|
|
7027
|
+
() => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
|
|
7028
|
+
);
|
|
7029
|
+
}
|
|
7030
|
+
async type(id, ownerId, selector, text, signal) {
|
|
7031
|
+
const session = this.requireOwned(id, ownerId);
|
|
7032
|
+
await this.runPageOperation(
|
|
7033
|
+
session,
|
|
7034
|
+
signal,
|
|
7035
|
+
() => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
|
|
7036
|
+
);
|
|
7037
|
+
}
|
|
7038
|
+
async select(id, ownerId, selector, value, signal) {
|
|
7039
|
+
const session = this.requireOwned(id, ownerId);
|
|
7040
|
+
await this.runPageOperation(
|
|
7041
|
+
session,
|
|
7042
|
+
signal,
|
|
7043
|
+
() => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7046
|
+
async press(id, ownerId, key, signal) {
|
|
7047
|
+
const session = this.requireOwned(id, ownerId);
|
|
7048
|
+
await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
|
|
7049
|
+
}
|
|
7050
|
+
async hover(id, ownerId, selector, signal) {
|
|
7051
|
+
const session = this.requireOwned(id, ownerId);
|
|
7052
|
+
await this.runPageOperation(
|
|
7053
|
+
session,
|
|
7054
|
+
signal,
|
|
7055
|
+
() => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
|
|
7056
|
+
);
|
|
7057
|
+
}
|
|
7058
|
+
async wait(id, ownerId, input, signal) {
|
|
7059
|
+
const session = this.requireOwned(id, ownerId);
|
|
7060
|
+
const timeout = Math.min(
|
|
7061
|
+
Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
|
|
7062
|
+
this.operationTimeoutMs
|
|
7063
|
+
);
|
|
7064
|
+
await this.runPageOperation(
|
|
7065
|
+
session,
|
|
7066
|
+
signal,
|
|
7067
|
+
() => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
|
|
7068
|
+
);
|
|
7069
|
+
}
|
|
7070
|
+
async drag(id, ownerId, from, to, signal) {
|
|
7071
|
+
const session = this.requireOwned(id, ownerId);
|
|
7072
|
+
await this.runPageOperation(
|
|
7073
|
+
session,
|
|
7074
|
+
signal,
|
|
7075
|
+
() => session.page.locator(from).dragTo(session.page.locator(to), {
|
|
7076
|
+
timeout: this.operationTimeoutMs
|
|
7077
|
+
})
|
|
7078
|
+
);
|
|
7079
|
+
}
|
|
7080
|
+
async upload(id, ownerId, selector, files, projectRoot, signal) {
|
|
7081
|
+
const session = this.requireOwned(id, ownerId);
|
|
7082
|
+
const root = path11.resolve(projectRoot);
|
|
7083
|
+
const realRoot = await fs8.realpath(root);
|
|
7084
|
+
const resolved = [];
|
|
7085
|
+
for (const file of files) {
|
|
7086
|
+
const absolute = path11.resolve(root, file);
|
|
7087
|
+
const relative13 = path11.relative(root, absolute);
|
|
7088
|
+
if (relative13.startsWith("..") || path11.isAbsolute(relative13)) {
|
|
7089
|
+
throw new Error("browser: upload files must stay inside the project root");
|
|
7090
|
+
}
|
|
7091
|
+
const realFile = await fs8.realpath(absolute);
|
|
7092
|
+
const realRelative = path11.relative(realRoot, realFile);
|
|
7093
|
+
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
7094
|
+
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
7095
|
+
}
|
|
7096
|
+
const stat18 = await fs8.stat(realFile);
|
|
7097
|
+
if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
7098
|
+
resolved.push(realFile);
|
|
7099
|
+
}
|
|
7100
|
+
await this.runPageOperation(
|
|
7101
|
+
session,
|
|
7102
|
+
signal,
|
|
7103
|
+
() => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
|
|
7104
|
+
);
|
|
7105
|
+
}
|
|
7106
|
+
async evaluate(id, ownerId, expression, signal) {
|
|
7107
|
+
const session = this.requireOwned(id, ownerId);
|
|
7108
|
+
const result = await this.runPageOperation(
|
|
7109
|
+
session,
|
|
7110
|
+
signal,
|
|
7111
|
+
() => session.page.evaluate(expression)
|
|
7112
|
+
);
|
|
7113
|
+
const serialized = JSON.stringify(result);
|
|
7114
|
+
if (serialized && serialized.length > this.maxSnapshotChars) {
|
|
7115
|
+
throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
|
|
7116
|
+
}
|
|
7117
|
+
return result;
|
|
7118
|
+
}
|
|
7119
|
+
async close(id, ownerId) {
|
|
7120
|
+
const session = this.requireOwned(id, ownerId);
|
|
7121
|
+
let trace;
|
|
7122
|
+
try {
|
|
7123
|
+
if (session.tracing) {
|
|
7124
|
+
const target = this.artifacts.pathFor(session.id, "zip");
|
|
7125
|
+
await fs8.mkdir(path11.dirname(target.path), { recursive: true });
|
|
7126
|
+
await session.context.tracing.stop({ path: target.path });
|
|
7127
|
+
trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
|
|
7128
|
+
}
|
|
7129
|
+
} finally {
|
|
7130
|
+
await session.context.close().catch(() => void 0);
|
|
7131
|
+
this.sessions.delete(id);
|
|
7132
|
+
await this.closeBrowserIfIdle();
|
|
7133
|
+
}
|
|
7134
|
+
return trace ? { trace } : {};
|
|
7135
|
+
}
|
|
7136
|
+
async closeOwner(ownerId) {
|
|
7137
|
+
const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
|
|
7138
|
+
for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
|
|
7139
|
+
}
|
|
7140
|
+
async dispose() {
|
|
7141
|
+
for (const session of [...this.sessions.values()]) {
|
|
7142
|
+
await session.context.close().catch(() => void 0);
|
|
7143
|
+
}
|
|
7144
|
+
this.sessions.clear();
|
|
7145
|
+
const browser = this.browser;
|
|
7146
|
+
this.browser = void 0;
|
|
7147
|
+
this.launching = void 0;
|
|
7148
|
+
await browser?.close().catch(() => void 0);
|
|
7149
|
+
await this.networkProxy.close();
|
|
7150
|
+
}
|
|
7151
|
+
async ensureBrowser(signal) {
|
|
7152
|
+
if (this.browser?.isConnected()) return this.browser;
|
|
7153
|
+
if (!this.launching) {
|
|
7154
|
+
this.launching = this.launcher(this.headless).then((browser) => {
|
|
7155
|
+
this.browser = browser;
|
|
7156
|
+
browser.on("disconnected", () => {
|
|
7157
|
+
if (this.browser === browser) this.browser = void 0;
|
|
7158
|
+
for (const [id, session] of this.sessions) {
|
|
7159
|
+
if (session.context.browser() === browser) this.sessions.delete(id);
|
|
7160
|
+
}
|
|
7161
|
+
});
|
|
7162
|
+
return browser;
|
|
7163
|
+
});
|
|
7164
|
+
}
|
|
7165
|
+
try {
|
|
7166
|
+
return await abortable(
|
|
7167
|
+
signal,
|
|
7168
|
+
() => this.launching,
|
|
7169
|
+
async () => {
|
|
7170
|
+
const browser = await this.launching?.catch(() => void 0);
|
|
7171
|
+
await browser?.close().catch(() => void 0);
|
|
7172
|
+
}
|
|
7173
|
+
);
|
|
7174
|
+
} finally {
|
|
7175
|
+
this.launching = void 0;
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
7178
|
+
requireOwned(id, ownerId) {
|
|
7179
|
+
const session = this.sessions.get(id);
|
|
7180
|
+
if (!session) throw new Error(`browser: unknown session "${id}"`);
|
|
7181
|
+
if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
|
|
7182
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7183
|
+
return session;
|
|
7184
|
+
}
|
|
7185
|
+
async summary(session) {
|
|
7186
|
+
return {
|
|
7187
|
+
id: session.id,
|
|
7188
|
+
ownerId: session.ownerId,
|
|
7189
|
+
url: safeBrowserUrl(session.page.url()),
|
|
7190
|
+
title: redactBrowserText(await session.page.title().catch(() => "")),
|
|
7191
|
+
createdAt: session.createdAt,
|
|
7192
|
+
lastUsedAt: session.lastUsedAt,
|
|
7193
|
+
tracing: session.tracing
|
|
7194
|
+
};
|
|
7195
|
+
}
|
|
7196
|
+
attachEvidenceCollectors(session) {
|
|
7197
|
+
session.page.on("console", (message) => {
|
|
7198
|
+
pushBounded(
|
|
7199
|
+
session.console,
|
|
7200
|
+
{
|
|
7201
|
+
level: message.type(),
|
|
7202
|
+
text: redactBrowserText(message.text()).slice(0, 4096),
|
|
7203
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7204
|
+
},
|
|
7205
|
+
this.maxConsoleEntries
|
|
7206
|
+
);
|
|
7207
|
+
});
|
|
7208
|
+
session.page.on("response", (response) => {
|
|
7209
|
+
this.pushNetwork(session, {
|
|
7210
|
+
method: response.request().method(),
|
|
7211
|
+
url: safeBrowserUrl(response.url()),
|
|
7212
|
+
status: response.status(),
|
|
7213
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7214
|
+
});
|
|
7215
|
+
});
|
|
7216
|
+
session.page.on("requestfailed", (request2) => {
|
|
7217
|
+
this.pushNetwork(session, {
|
|
7218
|
+
method: request2.method(),
|
|
7219
|
+
url: safeBrowserUrl(request2.url()),
|
|
7220
|
+
failed: true,
|
|
7221
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7222
|
+
});
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
pushNetwork(session, entry) {
|
|
7226
|
+
pushBounded(session.network, entry, this.maxNetworkEntries);
|
|
7227
|
+
}
|
|
7228
|
+
async runPageOperation(session, signal, operation) {
|
|
7229
|
+
signal.throwIfAborted();
|
|
7230
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7231
|
+
return abortable(signal, operation, async () => {
|
|
7232
|
+
await session.context.close().catch(() => void 0);
|
|
7233
|
+
this.sessions.delete(session.id);
|
|
7234
|
+
await this.closeBrowserIfIdle();
|
|
7235
|
+
});
|
|
7236
|
+
}
|
|
7237
|
+
async closeBrowserIfIdle() {
|
|
7238
|
+
if (this.sessions.size > 0 || !this.browser) return;
|
|
7239
|
+
const browser = this.browser;
|
|
7240
|
+
this.browser = void 0;
|
|
7241
|
+
await browser.close().catch(() => void 0);
|
|
7242
|
+
}
|
|
7243
|
+
};
|
|
7244
|
+
async function defaultLauncher(headless) {
|
|
7245
|
+
try {
|
|
7246
|
+
const { chromium } = await import("@playwright/test");
|
|
7247
|
+
return await chromium.launch({ headless });
|
|
7248
|
+
} catch (err) {
|
|
7249
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
7250
|
+
throw new Error(
|
|
7251
|
+
`browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
|
|
7252
|
+
);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
async function browserInstallationDiagnostics() {
|
|
7256
|
+
try {
|
|
7257
|
+
const { chromium } = await import("@playwright/test");
|
|
7258
|
+
const executablePath = chromium.executablePath();
|
|
7259
|
+
await fs8.access(executablePath);
|
|
7260
|
+
return { available: true, executablePath, message: "Playwright Chromium is available." };
|
|
7261
|
+
} catch {
|
|
7262
|
+
return {
|
|
7263
|
+
available: false,
|
|
7264
|
+
message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
|
|
7265
|
+
};
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
function pushBounded(target, value, limit) {
|
|
7269
|
+
target.push(value);
|
|
7270
|
+
if (target.length > limit) target.splice(0, target.length - limit);
|
|
7271
|
+
}
|
|
7272
|
+
async function abortable(signal, operation, onAbort) {
|
|
7273
|
+
signal.throwIfAborted();
|
|
7274
|
+
return new Promise((resolve15, reject) => {
|
|
7275
|
+
let settled = false;
|
|
7276
|
+
let aborting = false;
|
|
7277
|
+
const finish = (fn) => {
|
|
7278
|
+
if (settled) return;
|
|
7279
|
+
settled = true;
|
|
7280
|
+
signal.removeEventListener("abort", abort);
|
|
7281
|
+
fn();
|
|
7282
|
+
};
|
|
7283
|
+
const abort = () => {
|
|
7284
|
+
aborting = true;
|
|
7285
|
+
void Promise.resolve(onAbort()).finally(() => {
|
|
7286
|
+
finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
|
|
7287
|
+
});
|
|
7288
|
+
};
|
|
7289
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
7290
|
+
operation().then(
|
|
7291
|
+
(value) => {
|
|
7292
|
+
if (!aborting) finish(() => resolve15(value));
|
|
7293
|
+
},
|
|
7294
|
+
(err) => {
|
|
7295
|
+
if (!aborting) finish(() => reject(err));
|
|
7296
|
+
}
|
|
7297
|
+
);
|
|
7298
|
+
});
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
// src/browser/tools.ts
|
|
7302
|
+
import * as path12 from "node:path";
|
|
7303
|
+
import { ToolCapabilities } from "@wrongstack/core";
|
|
7304
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
7305
|
+
var managers = /* @__PURE__ */ new Map();
|
|
7306
|
+
var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
|
|
7307
|
+
function managerFor(ctx) {
|
|
7308
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
7309
|
+
let manager = managers.get(root);
|
|
7310
|
+
if (!manager) {
|
|
7311
|
+
const wpaths = resolveWstackPaths({ projectRoot: root });
|
|
7312
|
+
manager = new BrowserSessionManager({
|
|
7313
|
+
artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
|
|
7314
|
+
allowedPrivateOrigins: parsePrivateOriginAllowlist(
|
|
7315
|
+
process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
|
|
7316
|
+
)
|
|
7317
|
+
});
|
|
7318
|
+
managers.set(root, manager);
|
|
7319
|
+
}
|
|
7320
|
+
const runSignal = ctx.signal;
|
|
7321
|
+
if (cleanupSignalByContext.get(ctx) !== runSignal) {
|
|
7322
|
+
cleanupSignalByContext.set(ctx, runSignal);
|
|
7323
|
+
const managerForRun = manager;
|
|
7324
|
+
const ownerForRun = owner(ctx);
|
|
7325
|
+
ctx.registerAbortHook?.(async () => {
|
|
7326
|
+
if (cleanupSignalByContext.get(ctx) === runSignal) {
|
|
7327
|
+
cleanupSignalByContext.delete(ctx);
|
|
7328
|
+
}
|
|
7329
|
+
await managerForRun.closeOwner(ownerForRun);
|
|
7330
|
+
});
|
|
7331
|
+
}
|
|
7332
|
+
return manager;
|
|
7333
|
+
}
|
|
7334
|
+
function owner(ctx) {
|
|
7335
|
+
return ctx.agentId || "leader";
|
|
7336
|
+
}
|
|
7337
|
+
function json(value) {
|
|
7338
|
+
return JSON.stringify(value, null, 2);
|
|
7339
|
+
}
|
|
7340
|
+
var sessionIdSchema = {
|
|
7341
|
+
type: "string",
|
|
7342
|
+
minLength: 1,
|
|
7343
|
+
description: "Browser session id returned by browser_open."
|
|
7344
|
+
};
|
|
7345
|
+
var browserOpenTool = {
|
|
7346
|
+
name: "browser_open",
|
|
7347
|
+
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
|
|
7348
|
+
usageHint: "browser_open({ url?, width?, height?, trace? })",
|
|
7349
|
+
permission: "confirm",
|
|
7350
|
+
mutating: true,
|
|
7351
|
+
riskTier: "standard",
|
|
7352
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7353
|
+
subjectKey: "url",
|
|
7354
|
+
inputSchema: {
|
|
7355
|
+
type: "object",
|
|
7356
|
+
properties: {
|
|
7357
|
+
url: { type: "string", description: "Optional absolute http(s) URL." },
|
|
7358
|
+
width: { type: "integer", minimum: 320, maximum: 3840 },
|
|
7359
|
+
height: { type: "integer", minimum: 240, maximum: 2160 },
|
|
7360
|
+
trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
|
|
7361
|
+
}
|
|
7362
|
+
},
|
|
7363
|
+
async execute(input, ctx, opts) {
|
|
7364
|
+
const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
|
|
7365
|
+
return json(
|
|
7366
|
+
await managerFor(ctx).open(
|
|
7367
|
+
owner(ctx),
|
|
7368
|
+
{ url: input.url, viewport, trace: input.trace },
|
|
7369
|
+
opts.signal
|
|
7370
|
+
)
|
|
7371
|
+
);
|
|
7372
|
+
}
|
|
7373
|
+
};
|
|
7374
|
+
var browserListTool = {
|
|
7375
|
+
name: "browser_list",
|
|
7376
|
+
description: "List browser sessions owned by the current agent without exposing other agents sessions.",
|
|
7377
|
+
permission: "auto",
|
|
7378
|
+
mutating: false,
|
|
7379
|
+
riskTier: "safe",
|
|
7380
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7381
|
+
inputSchema: { type: "object", properties: {} },
|
|
7382
|
+
async execute(_input, ctx) {
|
|
7383
|
+
return json(await managerFor(ctx).list(owner(ctx)));
|
|
7384
|
+
}
|
|
7385
|
+
};
|
|
7386
|
+
var browserStatusTool = {
|
|
7387
|
+
name: "browser_status",
|
|
7388
|
+
description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
|
|
7389
|
+
permission: "auto",
|
|
7390
|
+
mutating: false,
|
|
7391
|
+
riskTier: "safe",
|
|
7392
|
+
capabilities: [ToolCapabilities.TOOL_META],
|
|
7393
|
+
inputSchema: { type: "object", properties: {} },
|
|
7394
|
+
async execute() {
|
|
7395
|
+
return json(await browserInstallationDiagnostics());
|
|
7396
|
+
}
|
|
7397
|
+
};
|
|
7398
|
+
var browserNavigateTool = {
|
|
7399
|
+
name: "browser_navigate",
|
|
7400
|
+
description: "Navigate an owned browser session to an approved http(s) URL.",
|
|
7401
|
+
usageHint: "browser_navigate({ sessionId, url })",
|
|
7402
|
+
permission: "confirm",
|
|
7403
|
+
mutating: true,
|
|
7404
|
+
riskTier: "standard",
|
|
7405
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7406
|
+
subjectKey: "url",
|
|
7407
|
+
inputSchema: {
|
|
7408
|
+
type: "object",
|
|
7409
|
+
properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
|
|
7410
|
+
required: ["sessionId", "url"]
|
|
7411
|
+
},
|
|
7412
|
+
async execute(input, ctx, opts) {
|
|
7413
|
+
return json(
|
|
7414
|
+
await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
|
|
7415
|
+
);
|
|
7416
|
+
}
|
|
7417
|
+
};
|
|
7418
|
+
var browserSnapshotTool = {
|
|
7419
|
+
name: "browser_snapshot",
|
|
7420
|
+
description: "Return bounded accessibility state plus redacted console and network summaries.",
|
|
7421
|
+
usageHint: "browser_snapshot({ sessionId })",
|
|
7422
|
+
permission: "auto",
|
|
7423
|
+
mutating: false,
|
|
7424
|
+
riskTier: "safe",
|
|
7425
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7426
|
+
inputSchema: {
|
|
7427
|
+
type: "object",
|
|
7428
|
+
properties: { sessionId: sessionIdSchema },
|
|
7429
|
+
required: ["sessionId"]
|
|
7430
|
+
},
|
|
7431
|
+
async execute(input, ctx, opts) {
|
|
7432
|
+
return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
|
|
7433
|
+
}
|
|
7434
|
+
};
|
|
7435
|
+
var browserScreenshotTool = {
|
|
7436
|
+
name: "browser_screenshot",
|
|
7437
|
+
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
7438
|
+
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
7439
|
+
permission: "confirm",
|
|
7440
|
+
mutating: true,
|
|
7441
|
+
riskTier: "safe",
|
|
7442
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7443
|
+
inputSchema: {
|
|
7444
|
+
type: "object",
|
|
7445
|
+
properties: {
|
|
7446
|
+
sessionId: sessionIdSchema,
|
|
7447
|
+
fullPage: { type: "boolean" },
|
|
7448
|
+
selector: { type: "string", minLength: 1 }
|
|
7449
|
+
},
|
|
7450
|
+
required: ["sessionId"]
|
|
7451
|
+
},
|
|
7452
|
+
async execute(input, ctx, opts) {
|
|
7453
|
+
return json(
|
|
7454
|
+
await managerFor(ctx).screenshot(
|
|
7455
|
+
input.sessionId,
|
|
7456
|
+
owner(ctx),
|
|
7457
|
+
{ fullPage: input.fullPage, selector: input.selector },
|
|
7458
|
+
opts.signal
|
|
7459
|
+
)
|
|
7460
|
+
);
|
|
7461
|
+
}
|
|
7462
|
+
};
|
|
7463
|
+
function selectorTool(name, description, run) {
|
|
7464
|
+
return {
|
|
7465
|
+
name,
|
|
7466
|
+
description,
|
|
7467
|
+
usageHint: `${name}({ sessionId, selector })`,
|
|
7468
|
+
permission: "confirm",
|
|
7469
|
+
mutating: true,
|
|
7470
|
+
riskTier: "standard",
|
|
7471
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7472
|
+
inputSchema: {
|
|
7473
|
+
type: "object",
|
|
7474
|
+
properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
|
|
7475
|
+
required: ["sessionId", "selector"]
|
|
7476
|
+
},
|
|
7477
|
+
async execute(input, ctx, opts) {
|
|
7478
|
+
await run(managerFor(ctx), input, owner(ctx), opts.signal);
|
|
7479
|
+
return "ok";
|
|
7480
|
+
}
|
|
7481
|
+
};
|
|
7482
|
+
}
|
|
7483
|
+
var browserClickTool = selectorTool(
|
|
7484
|
+
"browser_click",
|
|
7485
|
+
"Click an element in an owned browser session.",
|
|
7486
|
+
(manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
|
|
7487
|
+
);
|
|
7488
|
+
var browserHoverTool = selectorTool(
|
|
7489
|
+
"browser_hover",
|
|
7490
|
+
"Hover an element in an owned browser session.",
|
|
7491
|
+
(manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
|
|
7492
|
+
);
|
|
7493
|
+
var browserTypeTool = {
|
|
7494
|
+
name: "browser_type",
|
|
7495
|
+
description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
|
|
7496
|
+
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
7497
|
+
permission: "confirm",
|
|
7498
|
+
mutating: true,
|
|
7499
|
+
riskTier: "standard",
|
|
7500
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7501
|
+
inputSchema: {
|
|
7502
|
+
type: "object",
|
|
7503
|
+
properties: {
|
|
7504
|
+
sessionId: sessionIdSchema,
|
|
7505
|
+
selector: { type: "string", minLength: 1 },
|
|
7506
|
+
text: { type: "string" },
|
|
7507
|
+
secretEnv: {
|
|
7508
|
+
type: "string",
|
|
7509
|
+
pattern: "^[A-Z_][A-Z0-9_]*$",
|
|
7510
|
+
description: "Host environment variable resolved only at execution time."
|
|
7511
|
+
}
|
|
7512
|
+
},
|
|
7513
|
+
required: ["sessionId", "selector"]
|
|
7514
|
+
},
|
|
7515
|
+
validate(input) {
|
|
7516
|
+
return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
|
|
7517
|
+
},
|
|
7518
|
+
async execute(input, ctx, opts) {
|
|
7519
|
+
const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
|
|
7520
|
+
if (value === void 0) {
|
|
7521
|
+
throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
|
|
7522
|
+
}
|
|
7523
|
+
await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
|
|
7524
|
+
return "ok";
|
|
7525
|
+
}
|
|
7526
|
+
};
|
|
7527
|
+
var browserSelectTool = {
|
|
7528
|
+
name: "browser_select",
|
|
7529
|
+
description: "Select an option in an owned browser session.",
|
|
7530
|
+
usageHint: "browser_select({ sessionId, selector, value })",
|
|
7531
|
+
permission: "confirm",
|
|
7532
|
+
mutating: true,
|
|
7533
|
+
riskTier: "standard",
|
|
7534
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7535
|
+
inputSchema: {
|
|
7536
|
+
type: "object",
|
|
7537
|
+
properties: {
|
|
7538
|
+
sessionId: sessionIdSchema,
|
|
7539
|
+
selector: { type: "string", minLength: 1 },
|
|
7540
|
+
value: { type: "string" }
|
|
7541
|
+
},
|
|
7542
|
+
required: ["sessionId", "selector", "value"]
|
|
7543
|
+
},
|
|
7544
|
+
async execute(input, ctx, opts) {
|
|
7545
|
+
await managerFor(ctx).select(
|
|
7546
|
+
input.sessionId,
|
|
7547
|
+
owner(ctx),
|
|
7548
|
+
input.selector,
|
|
7549
|
+
input.value,
|
|
7550
|
+
opts.signal
|
|
7551
|
+
);
|
|
7552
|
+
return "ok";
|
|
7553
|
+
}
|
|
7554
|
+
};
|
|
7555
|
+
var browserPressTool = {
|
|
7556
|
+
name: "browser_press",
|
|
7557
|
+
description: "Press a keyboard key in an owned browser session.",
|
|
7558
|
+
usageHint: "browser_press({ sessionId, key })",
|
|
7559
|
+
permission: "confirm",
|
|
7560
|
+
mutating: true,
|
|
7561
|
+
riskTier: "standard",
|
|
7562
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7563
|
+
inputSchema: {
|
|
7564
|
+
type: "object",
|
|
7565
|
+
properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
|
|
7566
|
+
required: ["sessionId", "key"]
|
|
7567
|
+
},
|
|
7568
|
+
async execute(input, ctx, opts) {
|
|
7569
|
+
await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
|
|
7570
|
+
return "ok";
|
|
7571
|
+
}
|
|
7572
|
+
};
|
|
7573
|
+
var browserDragTool = {
|
|
7574
|
+
name: "browser_drag",
|
|
7575
|
+
description: "Drag one element to another in an owned browser session.",
|
|
7576
|
+
usageHint: "browser_drag({ sessionId, from, to })",
|
|
7577
|
+
permission: "confirm",
|
|
7578
|
+
mutating: true,
|
|
7579
|
+
riskTier: "standard",
|
|
7580
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7581
|
+
inputSchema: {
|
|
7582
|
+
type: "object",
|
|
7583
|
+
properties: {
|
|
7584
|
+
sessionId: sessionIdSchema,
|
|
7585
|
+
from: { type: "string", minLength: 1 },
|
|
7586
|
+
to: { type: "string", minLength: 1 }
|
|
7587
|
+
},
|
|
7588
|
+
required: ["sessionId", "from", "to"]
|
|
7589
|
+
},
|
|
7590
|
+
async execute(input, ctx, opts) {
|
|
7591
|
+
await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
|
|
7592
|
+
return "ok";
|
|
7593
|
+
}
|
|
7594
|
+
};
|
|
7595
|
+
var browserWaitTool = {
|
|
7596
|
+
name: "browser_wait",
|
|
7597
|
+
description: "Wait for an element or a bounded duration in an owned browser session.",
|
|
7598
|
+
usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
|
|
7599
|
+
permission: "auto",
|
|
7600
|
+
mutating: false,
|
|
7601
|
+
riskTier: "safe",
|
|
7602
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7603
|
+
inputSchema: {
|
|
7604
|
+
type: "object",
|
|
7605
|
+
properties: {
|
|
7606
|
+
sessionId: sessionIdSchema,
|
|
7607
|
+
selector: { type: "string", minLength: 1 },
|
|
7608
|
+
timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
|
|
7609
|
+
},
|
|
7610
|
+
required: ["sessionId"]
|
|
7611
|
+
},
|
|
7612
|
+
async execute(input, ctx, opts) {
|
|
7613
|
+
await managerFor(ctx).wait(
|
|
7614
|
+
input.sessionId,
|
|
7615
|
+
owner(ctx),
|
|
7616
|
+
{ selector: input.selector, timeoutMs: input.timeoutMs },
|
|
7617
|
+
opts.signal
|
|
7618
|
+
);
|
|
7619
|
+
return "ok";
|
|
7620
|
+
}
|
|
7621
|
+
};
|
|
7622
|
+
var browserEvaluateTool = {
|
|
7623
|
+
name: "browser_evaluate",
|
|
7624
|
+
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
7625
|
+
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
7626
|
+
permission: "confirm",
|
|
7627
|
+
mutating: true,
|
|
7628
|
+
riskTier: "destructive",
|
|
7629
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
7630
|
+
inputSchema: {
|
|
7631
|
+
type: "object",
|
|
7632
|
+
properties: {
|
|
7633
|
+
sessionId: sessionIdSchema,
|
|
7634
|
+
expression: { type: "string", minLength: 1, maxLength: 16384 }
|
|
7635
|
+
},
|
|
7636
|
+
required: ["sessionId", "expression"]
|
|
7637
|
+
},
|
|
7638
|
+
async execute(input, ctx, opts) {
|
|
7639
|
+
return json(
|
|
7640
|
+
await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
|
|
7641
|
+
);
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7644
|
+
var browserUploadTool = {
|
|
7645
|
+
name: "browser_upload",
|
|
7646
|
+
description: "Upload project-local files through a file input in an owned browser session.",
|
|
7647
|
+
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
7648
|
+
permission: "confirm",
|
|
7649
|
+
mutating: true,
|
|
7650
|
+
riskTier: "destructive",
|
|
7651
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
7652
|
+
inputSchema: {
|
|
7653
|
+
type: "object",
|
|
7654
|
+
properties: {
|
|
7655
|
+
sessionId: sessionIdSchema,
|
|
7656
|
+
selector: { type: "string", minLength: 1 },
|
|
7657
|
+
files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
|
|
7658
|
+
},
|
|
7659
|
+
required: ["sessionId", "selector", "files"]
|
|
7660
|
+
},
|
|
7661
|
+
async execute(input, ctx, opts) {
|
|
7662
|
+
await managerFor(ctx).upload(
|
|
7663
|
+
input.sessionId,
|
|
7664
|
+
owner(ctx),
|
|
7665
|
+
input.selector,
|
|
7666
|
+
input.files,
|
|
7667
|
+
ctx.projectRoot,
|
|
7668
|
+
opts.signal
|
|
7669
|
+
);
|
|
7670
|
+
return "ok";
|
|
7671
|
+
}
|
|
7672
|
+
};
|
|
7673
|
+
var browserCloseTool = {
|
|
7674
|
+
name: "browser_close",
|
|
7675
|
+
description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
|
|
7676
|
+
usageHint: "browser_close({ sessionId })",
|
|
7677
|
+
permission: "auto",
|
|
7678
|
+
mutating: false,
|
|
7679
|
+
riskTier: "safe",
|
|
7680
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7681
|
+
inputSchema: {
|
|
7682
|
+
type: "object",
|
|
7683
|
+
properties: { sessionId: sessionIdSchema },
|
|
7684
|
+
required: ["sessionId"]
|
|
7685
|
+
},
|
|
7686
|
+
async execute(input, ctx) {
|
|
7687
|
+
return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
|
|
7688
|
+
}
|
|
7689
|
+
};
|
|
7690
|
+
var browserTools = [
|
|
7691
|
+
browserOpenTool,
|
|
7692
|
+
browserStatusTool,
|
|
7693
|
+
browserListTool,
|
|
7694
|
+
browserNavigateTool,
|
|
7695
|
+
browserSnapshotTool,
|
|
7696
|
+
browserScreenshotTool,
|
|
7697
|
+
browserClickTool,
|
|
7698
|
+
browserTypeTool,
|
|
7699
|
+
browserSelectTool,
|
|
7700
|
+
browserPressTool,
|
|
7701
|
+
browserHoverTool,
|
|
7702
|
+
browserDragTool,
|
|
7703
|
+
browserWaitTool,
|
|
7704
|
+
browserEvaluateTool,
|
|
7705
|
+
browserUploadTool,
|
|
7706
|
+
browserCloseTool
|
|
7707
|
+
];
|
|
7708
|
+
for (const tool of browserTools) tool.icon = "web";
|
|
7709
|
+
for (const tool of browserTools) tool.timeoutMs ??= 6e4;
|
|
7710
|
+
async function shutdownBrowserTools() {
|
|
7711
|
+
const active = [...managers.values()];
|
|
7712
|
+
managers.clear();
|
|
7713
|
+
await Promise.all(active.map((manager) => manager.dispose()));
|
|
7714
|
+
}
|
|
7715
|
+
|
|
6481
7716
|
// src/codebase-index/writer.ts
|
|
6482
7717
|
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
6483
7718
|
|
|
@@ -6567,9 +7802,9 @@ function resetIndexCircuitBreaker() {
|
|
|
6567
7802
|
// src/codebase-index/writer.ts
|
|
6568
7803
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
6569
7804
|
import { createRequire } from "node:module";
|
|
6570
|
-
import * as
|
|
6571
|
-
import * as
|
|
6572
|
-
import { resolveWstackPaths } from "@wrongstack/core";
|
|
7805
|
+
import * as fs9 from "node:fs";
|
|
7806
|
+
import * as path13 from "node:path";
|
|
7807
|
+
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
|
|
6573
7808
|
|
|
6574
7809
|
// src/codebase-index/schema.ts
|
|
6575
7810
|
var SCHEMA_VERSION = 2;
|
|
@@ -6698,7 +7933,7 @@ var Bm25Index = class {
|
|
|
6698
7933
|
// src/codebase-index/writer.ts
|
|
6699
7934
|
var DB_FILE = "index.db";
|
|
6700
7935
|
function resolveIndexDir(projectRoot, override) {
|
|
6701
|
-
return override ??
|
|
7936
|
+
return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
|
|
6702
7937
|
}
|
|
6703
7938
|
function codebaseIndexDirOverride(ctx) {
|
|
6704
7939
|
const v = ctx.meta?.["codebaseIndexDir"];
|
|
@@ -6813,9 +8048,9 @@ var IndexStore = class {
|
|
|
6813
8048
|
}
|
|
6814
8049
|
constructor(projectRoot, opts = {}) {
|
|
6815
8050
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
6816
|
-
|
|
8051
|
+
fs9.mkdirSync(this.indexDir, { recursive: true });
|
|
6817
8052
|
const Database = loadDatabaseSync();
|
|
6818
|
-
this.db = new Database(
|
|
8053
|
+
this.db = new Database(path13.join(this.indexDir, DB_FILE));
|
|
6819
8054
|
try {
|
|
6820
8055
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
6821
8056
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -7394,9 +8629,9 @@ var IndexStore = class {
|
|
|
7394
8629
|
}));
|
|
7395
8630
|
}
|
|
7396
8631
|
sizeBytes() {
|
|
7397
|
-
const dbPath =
|
|
8632
|
+
const dbPath = path13.join(this.indexDir, DB_FILE);
|
|
7398
8633
|
try {
|
|
7399
|
-
return
|
|
8634
|
+
return fs9.statSync(dbPath).size;
|
|
7400
8635
|
} catch {
|
|
7401
8636
|
return 0;
|
|
7402
8637
|
}
|
|
@@ -7411,14 +8646,14 @@ var IndexStore = class {
|
|
|
7411
8646
|
};
|
|
7412
8647
|
|
|
7413
8648
|
// src/codebase-index/background-indexer.ts
|
|
7414
|
-
import * as
|
|
8649
|
+
import * as fs15 from "node:fs";
|
|
7415
8650
|
import { fileURLToPath } from "node:url";
|
|
7416
8651
|
import { Worker } from "node:worker_threads";
|
|
7417
8652
|
|
|
7418
8653
|
// src/codebase-index/indexer.ts
|
|
7419
8654
|
import { expectDefined as expectDefined6 } from "@wrongstack/core";
|
|
7420
|
-
import * as
|
|
7421
|
-
import * as
|
|
8655
|
+
import * as fs14 from "node:fs/promises";
|
|
8656
|
+
import * as path19 from "node:path";
|
|
7422
8657
|
import { compileGlob as compileGlob2 } from "@wrongstack/core";
|
|
7423
8658
|
|
|
7424
8659
|
// src/codebase-index/ts-parser.ts
|
|
@@ -7611,8 +8846,8 @@ function detectLang(file) {
|
|
|
7611
8846
|
// src/codebase-index/go-parser.ts
|
|
7612
8847
|
import { spawn as spawn4 } from "node:child_process";
|
|
7613
8848
|
import * as os5 from "node:os";
|
|
7614
|
-
import * as
|
|
7615
|
-
import * as
|
|
8849
|
+
import * as path14 from "node:path";
|
|
8850
|
+
import * as fs10 from "node:fs/promises";
|
|
7616
8851
|
async function parseSymbols2(opts) {
|
|
7617
8852
|
const { file, content, lang } = opts;
|
|
7618
8853
|
try {
|
|
@@ -7913,10 +9148,10 @@ func formatType(t ast.Expr) string {
|
|
|
7913
9148
|
}
|
|
7914
9149
|
`;
|
|
7915
9150
|
async function syncGoParse(filePath, content, lang) {
|
|
7916
|
-
const tmpDir = await
|
|
9151
|
+
const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
|
|
7917
9152
|
try {
|
|
7918
|
-
const scriptPath =
|
|
7919
|
-
await
|
|
9153
|
+
const scriptPath = path14.join(tmpDir, "parse.go");
|
|
9154
|
+
await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
7920
9155
|
const proc = spawn4("go", ["run", scriptPath], {
|
|
7921
9156
|
stdio: ["pipe", "pipe", "pipe"],
|
|
7922
9157
|
windowsHide: true
|
|
@@ -7928,8 +9163,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
7928
9163
|
proc.stdin?.write(content);
|
|
7929
9164
|
proc.stdin?.end();
|
|
7930
9165
|
const { code } = await Promise.race([
|
|
7931
|
-
new Promise((
|
|
7932
|
-
proc.on("close", (c) =>
|
|
9166
|
+
new Promise((resolve15) => {
|
|
9167
|
+
proc.on("close", (c) => resolve15({ code: c }));
|
|
7933
9168
|
}),
|
|
7934
9169
|
new Promise(
|
|
7935
9170
|
(_, reject) => setTimeout(() => {
|
|
@@ -7959,15 +9194,15 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
7959
9194
|
} catch {
|
|
7960
9195
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7961
9196
|
} finally {
|
|
7962
|
-
await
|
|
9197
|
+
await fs10.rm(tmpDir, { recursive: true, force: true });
|
|
7963
9198
|
}
|
|
7964
9199
|
}
|
|
7965
9200
|
|
|
7966
9201
|
// src/codebase-index/py-parser.ts
|
|
7967
9202
|
import { spawn as spawn5 } from "node:child_process";
|
|
7968
|
-
import * as
|
|
9203
|
+
import * as fs11 from "node:fs/promises";
|
|
7969
9204
|
import * as os6 from "node:os";
|
|
7970
|
-
import * as
|
|
9205
|
+
import * as path15 from "node:path";
|
|
7971
9206
|
async function parseSymbols3(opts) {
|
|
7972
9207
|
const { file, content, lang } = opts;
|
|
7973
9208
|
try {
|
|
@@ -8183,10 +9418,10 @@ var _cachedScriptPath = null;
|
|
|
8183
9418
|
async function syncPyParse(filePath, content, lang) {
|
|
8184
9419
|
try {
|
|
8185
9420
|
if (!_cachedScriptPath) {
|
|
8186
|
-
const tmpDir =
|
|
8187
|
-
await
|
|
8188
|
-
_cachedScriptPath =
|
|
8189
|
-
await
|
|
9421
|
+
const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
|
|
9422
|
+
await fs11.mkdir(tmpDir, { recursive: true });
|
|
9423
|
+
_cachedScriptPath = path15.join(tmpDir, "parse.py");
|
|
9424
|
+
await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
8190
9425
|
}
|
|
8191
9426
|
const proc = spawn5("python", [_cachedScriptPath, filePath], {
|
|
8192
9427
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -8199,8 +9434,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
8199
9434
|
stdout += chunk.toString();
|
|
8200
9435
|
});
|
|
8201
9436
|
const { code } = await Promise.race([
|
|
8202
|
-
new Promise((
|
|
8203
|
-
proc.on("close", (c) =>
|
|
9437
|
+
new Promise((resolve15) => {
|
|
9438
|
+
proc.on("close", (c) => resolve15({ code: c }));
|
|
8204
9439
|
}),
|
|
8205
9440
|
new Promise(
|
|
8206
9441
|
(_, reject) => setTimeout(() => {
|
|
@@ -8235,8 +9470,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
8235
9470
|
// src/codebase-index/rs-parser.ts
|
|
8236
9471
|
import { expectDefined as expectDefined3 } from "@wrongstack/core";
|
|
8237
9472
|
import { execFileSync, spawn as spawn6 } from "node:child_process";
|
|
8238
|
-
import * as
|
|
8239
|
-
import * as
|
|
9473
|
+
import * as fs12 from "node:fs/promises";
|
|
9474
|
+
import * as path16 from "node:path";
|
|
8240
9475
|
async function parseSymbols4(opts) {
|
|
8241
9476
|
const { file, content, lang } = opts;
|
|
8242
9477
|
const nativeAvailable = checkNativeParser();
|
|
@@ -8249,7 +9484,7 @@ async function parseSymbols4(opts) {
|
|
|
8249
9484
|
function checkNativeParser() {
|
|
8250
9485
|
try {
|
|
8251
9486
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
8252
|
-
const toolsDir =
|
|
9487
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
8253
9488
|
try {
|
|
8254
9489
|
execFileSync(
|
|
8255
9490
|
"cargo",
|
|
@@ -8259,7 +9494,7 @@ function checkNativeParser() {
|
|
|
8259
9494
|
"--format-version",
|
|
8260
9495
|
"1",
|
|
8261
9496
|
"--manifest-path",
|
|
8262
|
-
|
|
9497
|
+
path16.join(toolsDir, "Cargo.toml")
|
|
8263
9498
|
],
|
|
8264
9499
|
{ stdio: "pipe", windowsHide: true }
|
|
8265
9500
|
);
|
|
@@ -8273,13 +9508,13 @@ function checkNativeParser() {
|
|
|
8273
9508
|
}
|
|
8274
9509
|
async function tryNativeParse(file, content) {
|
|
8275
9510
|
try {
|
|
8276
|
-
const toolsDir =
|
|
8277
|
-
const crateDir =
|
|
8278
|
-
const tmpFile =
|
|
8279
|
-
await
|
|
9511
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
9512
|
+
const crateDir = path16.join(toolsDir, "syn-parser");
|
|
9513
|
+
const tmpFile = path16.join(crateDir, "src", "input.rs");
|
|
9514
|
+
await fs12.writeFile(tmpFile, content, "utf8");
|
|
8280
9515
|
const proc = spawn6(
|
|
8281
9516
|
"cargo",
|
|
8282
|
-
["run", "--manifest-path",
|
|
9517
|
+
["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
|
|
8283
9518
|
{
|
|
8284
9519
|
cwd: process.cwd(),
|
|
8285
9520
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -8291,8 +9526,8 @@ async function tryNativeParse(file, content) {
|
|
|
8291
9526
|
stdout += chunk.toString();
|
|
8292
9527
|
});
|
|
8293
9528
|
const { code } = await Promise.race([
|
|
8294
|
-
new Promise((
|
|
8295
|
-
proc.on("close", (c) =>
|
|
9529
|
+
new Promise((resolve15) => {
|
|
9530
|
+
proc.on("close", (c) => resolve15({ code: c }));
|
|
8296
9531
|
}),
|
|
8297
9532
|
new Promise(
|
|
8298
9533
|
(_, reject) => setTimeout(() => {
|
|
@@ -8383,7 +9618,7 @@ function regexParse(opts) {
|
|
|
8383
9618
|
|
|
8384
9619
|
// src/codebase-index/json-parser.ts
|
|
8385
9620
|
import { expectDefined as expectDefined4 } from "@wrongstack/core";
|
|
8386
|
-
import * as
|
|
9621
|
+
import * as path17 from "node:path";
|
|
8387
9622
|
function parseSymbols5(opts) {
|
|
8388
9623
|
const { file, content, lang } = opts;
|
|
8389
9624
|
try {
|
|
@@ -8395,9 +9630,9 @@ function parseSymbols5(opts) {
|
|
|
8395
9630
|
function regexParse2(opts) {
|
|
8396
9631
|
const { file, content, lang } = opts;
|
|
8397
9632
|
const symbols = [];
|
|
8398
|
-
const
|
|
8399
|
-
const isPackageJson =
|
|
8400
|
-
const isTsconfig =
|
|
9633
|
+
const basename8 = path17.basename(file).toLowerCase();
|
|
9634
|
+
const isPackageJson = basename8 === "package.json";
|
|
9635
|
+
const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
|
|
8401
9636
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
8402
9637
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
8403
9638
|
const lines = content.split("\n");
|
|
@@ -8421,11 +9656,11 @@ function regexParse2(opts) {
|
|
|
8421
9656
|
const line = lineFromOffset(offset);
|
|
8422
9657
|
symbols.push(
|
|
8423
9658
|
makeSymbol({
|
|
8424
|
-
name:
|
|
9659
|
+
name: path17.basename(file),
|
|
8425
9660
|
kind: "object",
|
|
8426
9661
|
line,
|
|
8427
9662
|
col: 0,
|
|
8428
|
-
signature: `"${
|
|
9663
|
+
signature: `"${path17.basename(file)}" = { ... }`,
|
|
8429
9664
|
file,
|
|
8430
9665
|
lang
|
|
8431
9666
|
})
|
|
@@ -8736,8 +9971,8 @@ function makeSymbol2(opts) {
|
|
|
8736
9971
|
}
|
|
8737
9972
|
|
|
8738
9973
|
// src/codebase-index/gitignore.ts
|
|
8739
|
-
import * as
|
|
8740
|
-
import * as
|
|
9974
|
+
import * as fs13 from "node:fs/promises";
|
|
9975
|
+
import * as path18 from "node:path";
|
|
8741
9976
|
import { compileGlob } from "@wrongstack/core";
|
|
8742
9977
|
function globBody(glob) {
|
|
8743
9978
|
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -8783,7 +10018,7 @@ function compileGitignore(lines) {
|
|
|
8783
10018
|
async function loadGitignoreMatcher(projectRoot) {
|
|
8784
10019
|
let lines = [];
|
|
8785
10020
|
try {
|
|
8786
|
-
const raw = await
|
|
10021
|
+
const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
|
|
8787
10022
|
lines = raw.split("\n");
|
|
8788
10023
|
} catch {
|
|
8789
10024
|
}
|
|
@@ -8794,7 +10029,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
8794
10029
|
var YIELD_EVERY_N = 50;
|
|
8795
10030
|
var PARALLEL_BATCH = 20;
|
|
8796
10031
|
function yieldEventLoop() {
|
|
8797
|
-
return new Promise((
|
|
10032
|
+
return new Promise((resolve15) => setImmediate(resolve15));
|
|
8798
10033
|
}
|
|
8799
10034
|
function throwIfAborted(signal) {
|
|
8800
10035
|
if (!signal?.aborted) return;
|
|
@@ -8841,21 +10076,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
8841
10076
|
}
|
|
8842
10077
|
let entries;
|
|
8843
10078
|
try {
|
|
8844
|
-
entries = await
|
|
10079
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
8845
10080
|
} catch {
|
|
8846
10081
|
return;
|
|
8847
10082
|
}
|
|
8848
10083
|
dirCount++;
|
|
8849
10084
|
for (const e of entries) {
|
|
8850
10085
|
if (ignoreSet.has(e.name)) continue;
|
|
8851
|
-
const full =
|
|
8852
|
-
const rel =
|
|
10086
|
+
const full = path19.join(dir, e.name);
|
|
10087
|
+
const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
|
|
8853
10088
|
if (e.isDirectory()) {
|
|
8854
10089
|
if (isGitIgnored(rel, true)) continue;
|
|
8855
10090
|
await walk2(full);
|
|
8856
10091
|
} else if (e.isFile()) {
|
|
8857
10092
|
if (isGitIgnored(rel, false)) continue;
|
|
8858
|
-
const ext =
|
|
10093
|
+
const ext = path19.extname(e.name);
|
|
8859
10094
|
for (const { ext: extName, pat } of globs) {
|
|
8860
10095
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
8861
10096
|
results.push(full);
|
|
@@ -8911,7 +10146,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8911
10146
|
let files;
|
|
8912
10147
|
let discoveredFiles = null;
|
|
8913
10148
|
if (opts.files && opts.files.length > 0) {
|
|
8914
|
-
files = opts.files.map((f) =>
|
|
10149
|
+
files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
8915
10150
|
} else {
|
|
8916
10151
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
8917
10152
|
discoveredFiles = new Set(files);
|
|
@@ -8939,34 +10174,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8939
10174
|
const statOpts = signal ? { signal } : {};
|
|
8940
10175
|
const statReadParse = await Promise.allSettled(
|
|
8941
10176
|
batchFiles.map(async (file) => {
|
|
8942
|
-
let
|
|
10177
|
+
let stat18;
|
|
8943
10178
|
try {
|
|
8944
|
-
|
|
10179
|
+
stat18 = await fs14.stat(file, statOpts);
|
|
8945
10180
|
} catch (e) {
|
|
8946
10181
|
if (isAbortError(e)) throw e;
|
|
8947
10182
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8948
10183
|
}
|
|
8949
|
-
if (!
|
|
10184
|
+
if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
|
|
8950
10185
|
const lang = detectLang(file);
|
|
8951
|
-
if (!lang) return { file, stat:
|
|
10186
|
+
if (!lang) return { file, stat: stat18, lang: "", parsed: null };
|
|
8952
10187
|
const meta = existingMeta.get(file);
|
|
8953
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
8954
|
-
return { file, stat:
|
|
10188
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
|
|
10189
|
+
return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
|
|
8955
10190
|
}
|
|
8956
10191
|
let content;
|
|
8957
10192
|
try {
|
|
8958
|
-
content = await
|
|
10193
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
8959
10194
|
} catch (e) {
|
|
8960
10195
|
if (isAbortError(e)) throw e;
|
|
8961
|
-
return { file, stat:
|
|
10196
|
+
return { file, stat: stat18, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8962
10197
|
}
|
|
8963
10198
|
let parsed;
|
|
8964
10199
|
try {
|
|
8965
10200
|
parsed = await parseFile(file, content, lang);
|
|
8966
10201
|
} catch (e) {
|
|
8967
|
-
return { file, stat:
|
|
10202
|
+
return { file, stat: stat18, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8968
10203
|
}
|
|
8969
|
-
return { file, stat:
|
|
10204
|
+
return { file, stat: stat18, lang, parsed, content };
|
|
8970
10205
|
})
|
|
8971
10206
|
);
|
|
8972
10207
|
const batchEntries = [];
|
|
@@ -8986,7 +10221,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8986
10221
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
8987
10222
|
continue;
|
|
8988
10223
|
}
|
|
8989
|
-
const { stat:
|
|
10224
|
+
const { stat: stat18, lang, parsed } = result;
|
|
8990
10225
|
if (result.skippedMeta) {
|
|
8991
10226
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
8992
10227
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -8998,7 +10233,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8998
10233
|
store.upsertFile({
|
|
8999
10234
|
file,
|
|
9000
10235
|
lang,
|
|
9001
|
-
mtimeMs: Math.floor(
|
|
10236
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
9002
10237
|
symbolCount: 0,
|
|
9003
10238
|
lastIndexed: Date.now()
|
|
9004
10239
|
});
|
|
@@ -9010,7 +10245,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
9010
10245
|
store.upsertFile({
|
|
9011
10246
|
file,
|
|
9012
10247
|
lang,
|
|
9013
|
-
mtimeMs: Math.floor(
|
|
10248
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
9014
10249
|
symbolCount: 0,
|
|
9015
10250
|
lastIndexed: Date.now()
|
|
9016
10251
|
});
|
|
@@ -9026,7 +10261,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
9026
10261
|
lang,
|
|
9027
10262
|
symbols: parsed.symbols,
|
|
9028
10263
|
refs,
|
|
9029
|
-
mtimeMs: Math.floor(
|
|
10264
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
9030
10265
|
symbolCount: parsed.symbols.length
|
|
9031
10266
|
});
|
|
9032
10267
|
deleteForFiles.push(file);
|
|
@@ -9224,7 +10459,7 @@ function resolveWorkerUrl() {
|
|
|
9224
10459
|
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
9225
10460
|
try {
|
|
9226
10461
|
const url = new URL(rel, import.meta.url);
|
|
9227
|
-
if (url.protocol === "file:" &&
|
|
10462
|
+
if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
|
|
9228
10463
|
} catch {
|
|
9229
10464
|
}
|
|
9230
10465
|
}
|
|
@@ -9287,7 +10522,7 @@ function shutdownCodebaseIndexHost() {
|
|
|
9287
10522
|
function callIndexOp(op, args, opts) {
|
|
9288
10523
|
const w = ensureWorker();
|
|
9289
10524
|
if (!w) return callInline(op, args, opts);
|
|
9290
|
-
return new Promise((
|
|
10525
|
+
return new Promise((resolve15, reject) => {
|
|
9291
10526
|
const id = nextRpcId++;
|
|
9292
10527
|
const timer = setTimeout(() => {
|
|
9293
10528
|
pending.delete(id);
|
|
@@ -9310,7 +10545,7 @@ function callIndexOp(op, args, opts) {
|
|
|
9310
10545
|
pending.set(id, {
|
|
9311
10546
|
resolve: (v) => {
|
|
9312
10547
|
cleanup();
|
|
9313
|
-
|
|
10548
|
+
resolve15(v);
|
|
9314
10549
|
},
|
|
9315
10550
|
reject: (e) => {
|
|
9316
10551
|
cleanup();
|
|
@@ -9693,8 +10928,8 @@ var codebaseStatsTool = {
|
|
|
9693
10928
|
};
|
|
9694
10929
|
|
|
9695
10930
|
// src/design.ts
|
|
9696
|
-
import * as
|
|
9697
|
-
import * as
|
|
10931
|
+
import * as fs16 from "node:fs/promises";
|
|
10932
|
+
import * as path20 from "node:path";
|
|
9698
10933
|
import {
|
|
9699
10934
|
applyTokenOverrides,
|
|
9700
10935
|
getDesignKitLoader,
|
|
@@ -9708,17 +10943,17 @@ import {
|
|
|
9708
10943
|
setDesignOverrides
|
|
9709
10944
|
} from "@wrongstack/core";
|
|
9710
10945
|
async function resolveReal(p) {
|
|
9711
|
-
const resolved =
|
|
10946
|
+
const resolved = path20.resolve(p);
|
|
9712
10947
|
let probe = resolved;
|
|
9713
10948
|
const missing = [];
|
|
9714
10949
|
for (; ; ) {
|
|
9715
10950
|
try {
|
|
9716
|
-
return
|
|
10951
|
+
return path20.resolve(await fs16.realpath(probe), ...missing);
|
|
9717
10952
|
} catch (err) {
|
|
9718
10953
|
if (err.code === "ENOENT") {
|
|
9719
|
-
const parent =
|
|
10954
|
+
const parent = path20.dirname(probe);
|
|
9720
10955
|
if (parent === probe) return resolved;
|
|
9721
|
-
missing.unshift(
|
|
10956
|
+
missing.unshift(path20.basename(probe));
|
|
9722
10957
|
probe = parent;
|
|
9723
10958
|
continue;
|
|
9724
10959
|
}
|
|
@@ -9884,18 +11119,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
9884
11119
|
outPath: input.out
|
|
9885
11120
|
});
|
|
9886
11121
|
const root = await resolveReal(ctx.projectRoot);
|
|
9887
|
-
const absResolved =
|
|
9888
|
-
const absParent = await resolveReal(
|
|
9889
|
-
const abs =
|
|
9890
|
-
const rel =
|
|
9891
|
-
if (rel.startsWith("..") ||
|
|
11122
|
+
const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
|
|
11123
|
+
const absParent = await resolveReal(path20.dirname(absResolved));
|
|
11124
|
+
const abs = path20.join(absParent, path20.basename(absResolved));
|
|
11125
|
+
const rel = path20.relative(root, abs);
|
|
11126
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
9892
11127
|
throw new Error(
|
|
9893
11128
|
`design: materialize path "${result.path}" would escape the project root`
|
|
9894
11129
|
);
|
|
9895
11130
|
}
|
|
9896
11131
|
let exists = false;
|
|
9897
11132
|
try {
|
|
9898
|
-
await
|
|
11133
|
+
await fs16.access(abs);
|
|
9899
11134
|
exists = true;
|
|
9900
11135
|
} catch {
|
|
9901
11136
|
}
|
|
@@ -9912,8 +11147,8 @@ ${result.content}
|
|
|
9912
11147
|
\`\`\``
|
|
9913
11148
|
};
|
|
9914
11149
|
}
|
|
9915
|
-
await
|
|
9916
|
-
await
|
|
11150
|
+
await fs16.mkdir(path20.dirname(abs), { recursive: true });
|
|
11151
|
+
await fs16.writeFile(abs, result.content);
|
|
9917
11152
|
return {
|
|
9918
11153
|
action,
|
|
9919
11154
|
kit: active.kit,
|
|
@@ -9963,8 +11198,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
|
|
|
9963
11198
|
init_util();
|
|
9964
11199
|
import { spawn as spawn7 } from "node:child_process";
|
|
9965
11200
|
import { statSync as statSync2 } from "node:fs";
|
|
9966
|
-
import * as
|
|
9967
|
-
import * as
|
|
11201
|
+
import * as fs17 from "node:fs/promises";
|
|
11202
|
+
import * as path21 from "node:path";
|
|
9968
11203
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
|
|
9969
11204
|
var diffTool = {
|
|
9970
11205
|
name: "diff",
|
|
@@ -10048,18 +11283,18 @@ function findGitDir(cwd) {
|
|
|
10048
11283
|
let dir = cwd;
|
|
10049
11284
|
for (let i = 0; i < 20; i++) {
|
|
10050
11285
|
try {
|
|
10051
|
-
const
|
|
10052
|
-
if (
|
|
11286
|
+
const stat18 = statSync2(path21.join(dir, ".git"));
|
|
11287
|
+
if (stat18.isDirectory()) return dir;
|
|
10053
11288
|
} catch {
|
|
10054
11289
|
}
|
|
10055
|
-
const parent =
|
|
11290
|
+
const parent = path21.dirname(dir);
|
|
10056
11291
|
if (parent === dir) break;
|
|
10057
11292
|
dir = parent;
|
|
10058
11293
|
}
|
|
10059
11294
|
return null;
|
|
10060
11295
|
}
|
|
10061
11296
|
function runGit(args, cwd, signal) {
|
|
10062
|
-
return new Promise((
|
|
11297
|
+
return new Promise((resolve15) => {
|
|
10063
11298
|
let stdout = "";
|
|
10064
11299
|
let stderr = "";
|
|
10065
11300
|
const child = spawn7("git", args, {
|
|
@@ -10075,8 +11310,8 @@ function runGit(args, cwd, signal) {
|
|
|
10075
11310
|
child.stderr?.on("data", (c) => {
|
|
10076
11311
|
stderr += c.toString();
|
|
10077
11312
|
});
|
|
10078
|
-
child.on("close", (code) =>
|
|
10079
|
-
child.on("error", (e) =>
|
|
11313
|
+
child.on("close", (code) => resolve15({ stdout, stderr, exitCode: code ?? 0 }));
|
|
11314
|
+
child.on("error", (e) => resolve15({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
10080
11315
|
});
|
|
10081
11316
|
}
|
|
10082
11317
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -10093,9 +11328,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
10093
11328
|
const results = [];
|
|
10094
11329
|
for (const file of files) {
|
|
10095
11330
|
const absPath = safeResolve(file, ctx);
|
|
10096
|
-
const
|
|
10097
|
-
if (!
|
|
10098
|
-
const content = await
|
|
11331
|
+
const stat18 = await fs17.stat(absPath).catch(() => null);
|
|
11332
|
+
if (!stat18?.isFile()) continue;
|
|
11333
|
+
const content = await fs17.readFile(absPath, "utf8");
|
|
10099
11334
|
const lines = content.split(/\r?\n/);
|
|
10100
11335
|
results.push(formatWithLineNumbers(file, lines));
|
|
10101
11336
|
}
|
|
@@ -10115,7 +11350,7 @@ ${numbered}`;
|
|
|
10115
11350
|
|
|
10116
11351
|
// src/document.ts
|
|
10117
11352
|
init_util();
|
|
10118
|
-
import * as
|
|
11353
|
+
import * as fs18 from "node:fs/promises";
|
|
10119
11354
|
var documentTool = {
|
|
10120
11355
|
name: "document",
|
|
10121
11356
|
category: "Project",
|
|
@@ -10159,7 +11394,7 @@ var documentTool = {
|
|
|
10159
11394
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
10160
11395
|
for (const absPath of fileList) {
|
|
10161
11396
|
try {
|
|
10162
|
-
const content = await
|
|
11397
|
+
const content = await fs18.readFile(absPath, "utf8");
|
|
10163
11398
|
filesProcessed++;
|
|
10164
11399
|
const processed = processFile(
|
|
10165
11400
|
content,
|
|
@@ -10195,8 +11430,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
10195
11430
|
for (const f of files) {
|
|
10196
11431
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
10197
11432
|
try {
|
|
10198
|
-
const
|
|
10199
|
-
if (
|
|
11433
|
+
const stat18 = await fs18.stat(absPath);
|
|
11434
|
+
if (stat18.isFile()) resolved.push(absPath);
|
|
10200
11435
|
} catch {
|
|
10201
11436
|
}
|
|
10202
11437
|
}
|
|
@@ -10263,10 +11498,426 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
10263
11498
|
return results;
|
|
10264
11499
|
}
|
|
10265
11500
|
|
|
11501
|
+
// src/e2e.ts
|
|
11502
|
+
init_util();
|
|
11503
|
+
import { open, readdir as readdir5 } from "node:fs/promises";
|
|
11504
|
+
import * as path22 from "node:path";
|
|
11505
|
+
async function readBoundedText(filePath, maxBytes) {
|
|
11506
|
+
let handle;
|
|
11507
|
+
try {
|
|
11508
|
+
handle = await open(filePath, "r");
|
|
11509
|
+
const info = await handle.stat();
|
|
11510
|
+
if (!info.isFile() || info.size > maxBytes) return void 0;
|
|
11511
|
+
const buffer = Buffer.alloc(info.size);
|
|
11512
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
11513
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
11514
|
+
} catch {
|
|
11515
|
+
return void 0;
|
|
11516
|
+
} finally {
|
|
11517
|
+
await handle?.close().catch(() => void 0);
|
|
11518
|
+
}
|
|
11519
|
+
}
|
|
11520
|
+
var CONFIG_NAMES = /* @__PURE__ */ new Map([
|
|
11521
|
+
["playwright.config.ts", "playwright"],
|
|
11522
|
+
["playwright.config.js", "playwright"],
|
|
11523
|
+
["playwright.config.mts", "playwright"],
|
|
11524
|
+
["playwright.config.mjs", "playwright"],
|
|
11525
|
+
["playwright.config.cts", "playwright"],
|
|
11526
|
+
["playwright.config.cjs", "playwright"],
|
|
11527
|
+
["cypress.config.ts", "cypress"],
|
|
11528
|
+
["cypress.config.js", "cypress"],
|
|
11529
|
+
["cypress.config.mts", "cypress"],
|
|
11530
|
+
["cypress.config.mjs", "cypress"],
|
|
11531
|
+
["cypress.config.cts", "cypress"],
|
|
11532
|
+
["cypress.config.cjs", "cypress"],
|
|
11533
|
+
["cypress.json", "cypress"]
|
|
11534
|
+
]);
|
|
11535
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
11536
|
+
".git",
|
|
11537
|
+
".hg",
|
|
11538
|
+
".svn",
|
|
11539
|
+
".wrongstack",
|
|
11540
|
+
"node_modules",
|
|
11541
|
+
"dist",
|
|
11542
|
+
"build",
|
|
11543
|
+
"coverage",
|
|
11544
|
+
"coverage-html",
|
|
11545
|
+
"playwright-report",
|
|
11546
|
+
"test-results",
|
|
11547
|
+
".next"
|
|
11548
|
+
]);
|
|
11549
|
+
var MAX_SCAN_DIRECTORIES = 4e3;
|
|
11550
|
+
var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
11551
|
+
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
11552
|
+
var MAX_SPEC_SAMPLES = 20;
|
|
11553
|
+
function relativePath(root, target) {
|
|
11554
|
+
const value = path22.relative(root, target) || ".";
|
|
11555
|
+
return value.split(path22.sep).join("/");
|
|
11556
|
+
}
|
|
11557
|
+
function escapeRegExp(value) {
|
|
11558
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11559
|
+
}
|
|
11560
|
+
function staticString(source, property) {
|
|
11561
|
+
const key = escapeRegExp(property);
|
|
11562
|
+
const direct = source.match(
|
|
11563
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11564
|
+
);
|
|
11565
|
+
if (direct?.[2] && !direct[2].includes("${")) return direct[2];
|
|
11566
|
+
const sameLineFallback = source.match(
|
|
11567
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11568
|
+
);
|
|
11569
|
+
return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
|
|
11570
|
+
}
|
|
11571
|
+
function staticNumber(source, property) {
|
|
11572
|
+
const key = escapeRegExp(property);
|
|
11573
|
+
const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
|
|
11574
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
|
|
11575
|
+
}
|
|
11576
|
+
function serverHints(framework, source) {
|
|
11577
|
+
if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
|
|
11578
|
+
const command = staticString(source, "command");
|
|
11579
|
+
const url = staticString(source, "url");
|
|
11580
|
+
const port = staticNumber(source, "port");
|
|
11581
|
+
return [
|
|
11582
|
+
{
|
|
11583
|
+
kind: "managed",
|
|
11584
|
+
...command ? { command } : {},
|
|
11585
|
+
...url ? { url } : {},
|
|
11586
|
+
...port ? { port } : {},
|
|
11587
|
+
source: "static-config",
|
|
11588
|
+
incomplete: !command || !url && !port
|
|
11589
|
+
}
|
|
11590
|
+
];
|
|
11591
|
+
}
|
|
11592
|
+
const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
|
|
11593
|
+
return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
|
|
11594
|
+
}
|
|
11595
|
+
function configuredSpecPattern(framework, source) {
|
|
11596
|
+
if (framework === "playwright") {
|
|
11597
|
+
const testDir = staticString(source, "testDir") ?? "./tests";
|
|
11598
|
+
return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
|
|
11599
|
+
}
|
|
11600
|
+
return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
|
|
11601
|
+
}
|
|
11602
|
+
async function scanWorkspace(root, maxDepth, signal) {
|
|
11603
|
+
const result = {
|
|
11604
|
+
configs: [],
|
|
11605
|
+
packageFiles: [],
|
|
11606
|
+
scannedDirectories: 0,
|
|
11607
|
+
truncated: false
|
|
11608
|
+
};
|
|
11609
|
+
const queue = [{ directory: root, depth: 0 }];
|
|
11610
|
+
while (queue.length > 0) {
|
|
11611
|
+
signal.throwIfAborted();
|
|
11612
|
+
const current = queue.shift();
|
|
11613
|
+
if (!current) break;
|
|
11614
|
+
result.scannedDirectories += 1;
|
|
11615
|
+
if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
|
|
11616
|
+
result.truncated = true;
|
|
11617
|
+
break;
|
|
11618
|
+
}
|
|
11619
|
+
let entries;
|
|
11620
|
+
try {
|
|
11621
|
+
entries = await readdir5(current.directory, { withFileTypes: true });
|
|
11622
|
+
} catch {
|
|
11623
|
+
continue;
|
|
11624
|
+
}
|
|
11625
|
+
for (const entry of entries) {
|
|
11626
|
+
signal.throwIfAborted();
|
|
11627
|
+
const absolutePath = path22.join(current.directory, entry.name);
|
|
11628
|
+
if (entry.isFile()) {
|
|
11629
|
+
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
11630
|
+
const framework = CONFIG_NAMES.get(entry.name);
|
|
11631
|
+
if (framework) result.configs.push({ framework, absolutePath });
|
|
11632
|
+
} else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
|
|
11633
|
+
queue.push({ directory: absolutePath, depth: current.depth + 1 });
|
|
11634
|
+
}
|
|
11635
|
+
}
|
|
11636
|
+
}
|
|
11637
|
+
return result;
|
|
11638
|
+
}
|
|
11639
|
+
async function readPackageInfo(packagePath) {
|
|
11640
|
+
try {
|
|
11641
|
+
const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
|
|
11642
|
+
if (raw === void 0) return void 0;
|
|
11643
|
+
const parsed = JSON.parse(raw);
|
|
11644
|
+
const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
|
|
11645
|
+
Object.entries(parsed.scripts).filter(
|
|
11646
|
+
(entry) => typeof entry[1] === "string"
|
|
11647
|
+
)
|
|
11648
|
+
) : {};
|
|
11649
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
11650
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
11651
|
+
const value = parsed[field];
|
|
11652
|
+
if (!value || typeof value !== "object") continue;
|
|
11653
|
+
for (const name of Object.keys(value)) dependencies.add(name);
|
|
11654
|
+
}
|
|
11655
|
+
const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
|
|
11656
|
+
const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
|
|
11657
|
+
return { path: packagePath, scripts, dependencies, packageManager };
|
|
11658
|
+
} catch {
|
|
11659
|
+
return void 0;
|
|
11660
|
+
}
|
|
11661
|
+
}
|
|
11662
|
+
function frameworkFromPackage(info) {
|
|
11663
|
+
const found = /* @__PURE__ */ new Set();
|
|
11664
|
+
if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
|
|
11665
|
+
found.add("playwright");
|
|
11666
|
+
}
|
|
11667
|
+
if (info.dependencies.has("cypress")) found.add("cypress");
|
|
11668
|
+
for (const command of Object.values(info.scripts)) {
|
|
11669
|
+
if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
|
|
11670
|
+
if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
|
|
11671
|
+
}
|
|
11672
|
+
return [...found];
|
|
11673
|
+
}
|
|
11674
|
+
async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
11675
|
+
if (declared) return declared;
|
|
11676
|
+
let directory = projectRoot;
|
|
11677
|
+
while (true) {
|
|
11678
|
+
const names = /* @__PURE__ */ new Set();
|
|
11679
|
+
try {
|
|
11680
|
+
for (const entry of await readdir5(directory)) names.add(entry);
|
|
11681
|
+
} catch {
|
|
11682
|
+
}
|
|
11683
|
+
if (names.has("pnpm-lock.yaml")) return "pnpm";
|
|
11684
|
+
if (names.has("yarn.lock")) return "yarn";
|
|
11685
|
+
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
11686
|
+
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
11687
|
+
if (directory === scanRoot) break;
|
|
11688
|
+
const parent = path22.dirname(directory);
|
|
11689
|
+
const relativeParent = path22.relative(scanRoot, parent);
|
|
11690
|
+
if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
|
|
11691
|
+
break;
|
|
11692
|
+
}
|
|
11693
|
+
directory = parent;
|
|
11694
|
+
}
|
|
11695
|
+
return "npm";
|
|
11696
|
+
}
|
|
11697
|
+
function matchingScripts(info, framework) {
|
|
11698
|
+
if (!info) return [];
|
|
11699
|
+
const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
|
|
11700
|
+
return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
|
|
11701
|
+
const preferred = ["test:e2e", "e2e", "e2e:test"];
|
|
11702
|
+
const ai = preferred.indexOf(a);
|
|
11703
|
+
const bi = preferred.indexOf(b);
|
|
11704
|
+
if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
11705
|
+
return a.localeCompare(b);
|
|
11706
|
+
});
|
|
11707
|
+
}
|
|
11708
|
+
function executionPlan(framework, manager, root, configPath, scripts) {
|
|
11709
|
+
const script = scripts[0];
|
|
11710
|
+
if (script) {
|
|
11711
|
+
return {
|
|
11712
|
+
command: manager,
|
|
11713
|
+
args: ["run", script],
|
|
11714
|
+
cwd: root,
|
|
11715
|
+
source: "package-script",
|
|
11716
|
+
script
|
|
11717
|
+
};
|
|
11718
|
+
}
|
|
11719
|
+
const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
|
|
11720
|
+
if (configPath) {
|
|
11721
|
+
runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
|
|
11722
|
+
}
|
|
11723
|
+
return {
|
|
11724
|
+
command: manager === "bun" ? "bunx" : manager,
|
|
11725
|
+
args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
|
|
11726
|
+
cwd: root,
|
|
11727
|
+
source: "framework-default"
|
|
11728
|
+
};
|
|
11729
|
+
}
|
|
11730
|
+
function isSpec(framework, filename) {
|
|
11731
|
+
if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11732
|
+
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11733
|
+
}
|
|
11734
|
+
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
11735
|
+
const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
|
|
11736
|
+
const samples = [];
|
|
11737
|
+
let count = 0;
|
|
11738
|
+
let scanned = 0;
|
|
11739
|
+
const queue = [...roots];
|
|
11740
|
+
while (queue.length > 0) {
|
|
11741
|
+
signal.throwIfAborted();
|
|
11742
|
+
const directory = queue.shift();
|
|
11743
|
+
if (!directory) break;
|
|
11744
|
+
scanned += 1;
|
|
11745
|
+
if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
|
|
11746
|
+
let entries;
|
|
11747
|
+
try {
|
|
11748
|
+
entries = await readdir5(directory, { withFileTypes: true });
|
|
11749
|
+
} catch {
|
|
11750
|
+
continue;
|
|
11751
|
+
}
|
|
11752
|
+
for (const entry of entries) {
|
|
11753
|
+
signal.throwIfAborted();
|
|
11754
|
+
const target = path22.join(directory, entry.name);
|
|
11755
|
+
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
11756
|
+
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
11757
|
+
count += 1;
|
|
11758
|
+
if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
|
|
11759
|
+
}
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
return { count, samples, truncated: count > samples.length };
|
|
11763
|
+
}
|
|
11764
|
+
async function readConfig(configPath) {
|
|
11765
|
+
if (!configPath) return "";
|
|
11766
|
+
return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
|
|
11767
|
+
}
|
|
11768
|
+
function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
11769
|
+
let directory = projectRoot;
|
|
11770
|
+
while (true) {
|
|
11771
|
+
const found = packagesByDirectory.get(directory);
|
|
11772
|
+
if (found) return found;
|
|
11773
|
+
if (directory === scanRoot) return void 0;
|
|
11774
|
+
const parent = path22.dirname(directory);
|
|
11775
|
+
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
11776
|
+
directory = parent;
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
async function discoverE2EProjects(root, options) {
|
|
11780
|
+
const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
|
|
11781
|
+
const scan = await scanWorkspace(root, maxDepth, options.signal);
|
|
11782
|
+
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
11783
|
+
(info) => Boolean(info)
|
|
11784
|
+
);
|
|
11785
|
+
const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
|
|
11786
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
11787
|
+
for (const config of scan.configs) {
|
|
11788
|
+
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
11789
|
+
continue;
|
|
11790
|
+
}
|
|
11791
|
+
const projectRoot = path22.dirname(config.absolutePath);
|
|
11792
|
+
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
11793
|
+
framework: config.framework,
|
|
11794
|
+
root: projectRoot,
|
|
11795
|
+
configPath: config.absolutePath
|
|
11796
|
+
});
|
|
11797
|
+
}
|
|
11798
|
+
for (const info of packages) {
|
|
11799
|
+
const projectRoot = path22.dirname(info.path);
|
|
11800
|
+
for (const framework of frameworkFromPackage(info)) {
|
|
11801
|
+
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
11802
|
+
continue;
|
|
11803
|
+
const key = `${framework}:${projectRoot}`;
|
|
11804
|
+
if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11807
|
+
const projects = [];
|
|
11808
|
+
for (const candidate of candidates.values()) {
|
|
11809
|
+
options.signal.throwIfAborted();
|
|
11810
|
+
const info = nearestPackage(candidate.root, packagesByDirectory, root);
|
|
11811
|
+
const source = await readConfig(candidate.configPath);
|
|
11812
|
+
const scripts = matchingScripts(info, candidate.framework);
|
|
11813
|
+
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
11814
|
+
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
11815
|
+
const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
|
|
11816
|
+
const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
|
|
11817
|
+
const unsafeTestDirectory = Boolean(
|
|
11818
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
|
|
11819
|
+
);
|
|
11820
|
+
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
11821
|
+
const warnings = [];
|
|
11822
|
+
if (!candidate.configPath)
|
|
11823
|
+
warnings.push("Framework dependency or script found without a config file.");
|
|
11824
|
+
if (candidate.configPath && !source) {
|
|
11825
|
+
warnings.push("Config could not be read or exceeded the static inspection limit.");
|
|
11826
|
+
}
|
|
11827
|
+
if (unsafeTestDirectory) {
|
|
11828
|
+
warnings.push(
|
|
11829
|
+
"Configured testDir resolves outside the requested discovery root and was not scanned."
|
|
11830
|
+
);
|
|
11831
|
+
}
|
|
11832
|
+
const hints = serverHints(candidate.framework, source);
|
|
11833
|
+
if (hints.some((hint) => hint.incomplete)) {
|
|
11834
|
+
warnings.push("Configured server contains dynamic values; only static hints are shown.");
|
|
11835
|
+
}
|
|
11836
|
+
projects.push({
|
|
11837
|
+
framework: candidate.framework,
|
|
11838
|
+
root: relativePath(root, candidate.root),
|
|
11839
|
+
...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
|
|
11840
|
+
...info ? { packagePath: relativePath(root, info.path) } : {},
|
|
11841
|
+
packageManager: manager,
|
|
11842
|
+
evidence: [
|
|
11843
|
+
...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
|
|
11844
|
+
...info?.dependencies.has(
|
|
11845
|
+
candidate.framework === "playwright" ? "@playwright/test" : "cypress"
|
|
11846
|
+
) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
|
|
11847
|
+
...scripts.map((script) => `script:${script}`)
|
|
11848
|
+
],
|
|
11849
|
+
scripts,
|
|
11850
|
+
serverHints: hints,
|
|
11851
|
+
specPattern: configuredSpecPattern(candidate.framework, source),
|
|
11852
|
+
specCount: specs.count,
|
|
11853
|
+
specSamples: specs.samples,
|
|
11854
|
+
specsTruncated: specs.truncated,
|
|
11855
|
+
execution: executionPlan(
|
|
11856
|
+
candidate.framework,
|
|
11857
|
+
manager,
|
|
11858
|
+
relativePath(root, candidate.root),
|
|
11859
|
+
candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
|
|
11860
|
+
scripts
|
|
11861
|
+
),
|
|
11862
|
+
warnings
|
|
11863
|
+
});
|
|
11864
|
+
}
|
|
11865
|
+
projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
|
|
11866
|
+
return {
|
|
11867
|
+
root,
|
|
11868
|
+
projects,
|
|
11869
|
+
scannedDirectories: scan.scannedDirectories,
|
|
11870
|
+
scanTruncated: scan.truncated,
|
|
11871
|
+
warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
|
|
11872
|
+
};
|
|
11873
|
+
}
|
|
11874
|
+
var e2ePlanTool = {
|
|
11875
|
+
name: "e2e_plan",
|
|
11876
|
+
category: "Code Quality",
|
|
11877
|
+
description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
|
|
11878
|
+
usageHint: "Use before running browser E2E tests. The result identifies authoritative configs, package scripts, static server hints, specs, and exact argv. This tool never imports project config or executes commands.",
|
|
11879
|
+
permission: "auto",
|
|
11880
|
+
mutating: false,
|
|
11881
|
+
riskTier: "safe",
|
|
11882
|
+
capabilities: ["fs.read"],
|
|
11883
|
+
icon: "test",
|
|
11884
|
+
timeoutMs: 3e4,
|
|
11885
|
+
inputSchema: {
|
|
11886
|
+
type: "object",
|
|
11887
|
+
properties: {
|
|
11888
|
+
cwd: { type: "string", description: "Directory inside the project to inspect." },
|
|
11889
|
+
framework: {
|
|
11890
|
+
type: "string",
|
|
11891
|
+
enum: ["all", "playwright", "cypress"],
|
|
11892
|
+
description: "Optional framework filter; defaults to all."
|
|
11893
|
+
},
|
|
11894
|
+
maxDepth: {
|
|
11895
|
+
type: "integer",
|
|
11896
|
+
minimum: 0,
|
|
11897
|
+
maximum: 8,
|
|
11898
|
+
description: "Maximum workspace discovery depth; defaults to 5."
|
|
11899
|
+
},
|
|
11900
|
+
includeSpecs: {
|
|
11901
|
+
type: "boolean",
|
|
11902
|
+
description: "Count and sample specs; defaults to true."
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
},
|
|
11906
|
+
async execute(input, ctx, opts) {
|
|
11907
|
+
const root = await safeResolveReal(input.cwd ?? ".", ctx);
|
|
11908
|
+
return discoverE2EProjects(root, {
|
|
11909
|
+
framework: input.framework,
|
|
11910
|
+
maxDepth: input.maxDepth,
|
|
11911
|
+
includeSpecs: input.includeSpecs,
|
|
11912
|
+
signal: opts.signal
|
|
11913
|
+
});
|
|
11914
|
+
}
|
|
11915
|
+
};
|
|
11916
|
+
|
|
10266
11917
|
// src/edit.ts
|
|
10267
|
-
import * as
|
|
11918
|
+
import * as fs19 from "node:fs/promises";
|
|
10268
11919
|
import {
|
|
10269
|
-
atomicWrite,
|
|
11920
|
+
atomicWrite as atomicWrite2,
|
|
10270
11921
|
detectNewlineStyle,
|
|
10271
11922
|
normalizeToLf,
|
|
10272
11923
|
ToolValidationError,
|
|
@@ -10479,7 +12130,7 @@ function prefixSimilarity(a, b) {
|
|
|
10479
12130
|
}
|
|
10480
12131
|
|
|
10481
12132
|
// src/_syntax-check.ts
|
|
10482
|
-
import * as
|
|
12133
|
+
import * as path23 from "node:path";
|
|
10483
12134
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
10484
12135
|
var MAX_CHECK_CHARS = 15e5;
|
|
10485
12136
|
var MAX_ERRORS = 5;
|
|
@@ -10503,15 +12154,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
10503
12154
|
return { errors, preExisting };
|
|
10504
12155
|
}
|
|
10505
12156
|
function isJsoncFile(filePath) {
|
|
10506
|
-
const base =
|
|
12157
|
+
const base = path23.basename(filePath).toLowerCase();
|
|
10507
12158
|
if (base.endsWith(".jsonc")) return true;
|
|
10508
12159
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
10509
|
-
const dir =
|
|
12160
|
+
const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
|
|
10510
12161
|
return dir === ".vscode";
|
|
10511
12162
|
}
|
|
10512
12163
|
async function parseErrors(filePath, content) {
|
|
10513
12164
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
10514
|
-
const ext =
|
|
12165
|
+
const ext = path23.extname(filePath).toLowerCase();
|
|
10515
12166
|
if (ext === ".json" || ext === ".jsonc") {
|
|
10516
12167
|
try {
|
|
10517
12168
|
JSON.parse(content);
|
|
@@ -10538,7 +12189,7 @@ async function parseErrors(filePath, content) {
|
|
|
10538
12189
|
ts2.ScriptKind.JSX
|
|
10539
12190
|
);
|
|
10540
12191
|
const sourceFile = ts2.createSourceFile(
|
|
10541
|
-
|
|
12192
|
+
path23.basename(filePath),
|
|
10542
12193
|
content,
|
|
10543
12194
|
ts2.ScriptTarget.Latest,
|
|
10544
12195
|
/* setParentNodes */
|
|
@@ -10612,7 +12263,7 @@ var editTool = {
|
|
|
10612
12263
|
});
|
|
10613
12264
|
}
|
|
10614
12265
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10615
|
-
const
|
|
12266
|
+
const stat18 = await fs19.stat(absPath).catch((err) => {
|
|
10616
12267
|
if (err.code === "ENOENT") {
|
|
10617
12268
|
throw new ToolValidationError({
|
|
10618
12269
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -10622,15 +12273,15 @@ var editTool = {
|
|
|
10622
12273
|
}
|
|
10623
12274
|
throw err;
|
|
10624
12275
|
});
|
|
10625
|
-
if (!
|
|
12276
|
+
if (!stat18.isFile()) {
|
|
10626
12277
|
throw new ToolValidationError({
|
|
10627
12278
|
message: `edit: "${input.path}" is not a regular file`,
|
|
10628
12279
|
field: "path"
|
|
10629
12280
|
});
|
|
10630
12281
|
}
|
|
10631
12282
|
const autoRead = !ctx.hasRead(absPath);
|
|
10632
|
-
const original = await
|
|
10633
|
-
const updated = await
|
|
12283
|
+
const original = await fs19.readFile(absPath, "utf8");
|
|
12284
|
+
const updated = await fs19.stat(absPath);
|
|
10634
12285
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
10635
12286
|
const originalHash = sha256hex(original);
|
|
10636
12287
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
@@ -10652,7 +12303,7 @@ var editTool = {
|
|
|
10652
12303
|
});
|
|
10653
12304
|
}
|
|
10654
12305
|
}
|
|
10655
|
-
if (autoRead && updated.mtimeMs >
|
|
12306
|
+
if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
|
|
10656
12307
|
throw new ToolValidationError({
|
|
10657
12308
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
10658
12309
|
field: "path",
|
|
@@ -10734,8 +12385,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
10734
12385
|
}
|
|
10735
12386
|
const newFile = toStyle(newFileLf, style);
|
|
10736
12387
|
opts?.signal?.throwIfAborted();
|
|
10737
|
-
await
|
|
10738
|
-
const written = await
|
|
12388
|
+
await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
|
|
12389
|
+
const written = await fs19.stat(absPath);
|
|
10739
12390
|
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
10740
12391
|
ctx.session.recordFileChange({
|
|
10741
12392
|
path: absPath,
|
|
@@ -11441,7 +13092,7 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
11441
13092
|
// and glob patterns that could expand to dangerous targets.
|
|
11442
13093
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
11443
13094
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
11444
|
-
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
13095
|
+
rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
11445
13096
|
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
11446
13097
|
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
11447
13098
|
npm: [],
|
|
@@ -11609,7 +13260,7 @@ var execTool = {
|
|
|
11609
13260
|
}
|
|
11610
13261
|
};
|
|
11611
13262
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
11612
|
-
return new Promise((
|
|
13263
|
+
return new Promise((resolve15) => {
|
|
11613
13264
|
let stdout = "";
|
|
11614
13265
|
let stderr = "";
|
|
11615
13266
|
let killed = false;
|
|
@@ -11617,7 +13268,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
11617
13268
|
const finish = (result) => {
|
|
11618
13269
|
if (resolvedOnce.value) return;
|
|
11619
13270
|
resolvedOnce.value = true;
|
|
11620
|
-
|
|
13271
|
+
resolve15(result);
|
|
11621
13272
|
};
|
|
11622
13273
|
const startedAt = Date.now();
|
|
11623
13274
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
@@ -11726,13 +13377,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
|
|
|
11726
13377
|
import TurndownService from "turndown";
|
|
11727
13378
|
|
|
11728
13379
|
// src/_fetch-guard.ts
|
|
11729
|
-
import * as
|
|
11730
|
-
import * as
|
|
13380
|
+
import * as dns2 from "node:dns/promises";
|
|
13381
|
+
import * as net3 from "node:net";
|
|
11731
13382
|
import {
|
|
11732
13383
|
FetchError,
|
|
11733
13384
|
ToolValidationError as ToolValidationError2,
|
|
11734
|
-
isPrivateIPv4,
|
|
11735
|
-
isPrivateIPv6
|
|
13385
|
+
isPrivateIPv4 as isPrivateIPv42,
|
|
13386
|
+
isPrivateIPv6 as isPrivateIPv62
|
|
11736
13387
|
} from "@wrongstack/core";
|
|
11737
13388
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
11738
13389
|
var nativeGlobalFetch = globalThis.fetch;
|
|
@@ -11743,13 +13394,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
11743
13394
|
);
|
|
11744
13395
|
}
|
|
11745
13396
|
function guardedLookup(hostname4, options, callback) {
|
|
11746
|
-
|
|
13397
|
+
dns2.lookup(hostname4, { all: true }).then((records) => {
|
|
11747
13398
|
const family = options?.family;
|
|
11748
13399
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
11749
13400
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
11750
13401
|
if (!ALLOW_PRIVATE) {
|
|
11751
13402
|
for (const r of list) {
|
|
11752
|
-
const bad = r.family === 4 ?
|
|
13403
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11753
13404
|
if (bad) {
|
|
11754
13405
|
callback(
|
|
11755
13406
|
Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
|
|
@@ -11854,16 +13505,16 @@ async function assertNotPrivate(hostname4) {
|
|
|
11854
13505
|
field: "url"
|
|
11855
13506
|
});
|
|
11856
13507
|
}
|
|
11857
|
-
const ipVersion =
|
|
13508
|
+
const ipVersion = net3.isIP(host);
|
|
11858
13509
|
if (ipVersion === 4) {
|
|
11859
|
-
if (
|
|
13510
|
+
if (isPrivateIPv42(host)) {
|
|
11860
13511
|
throw new ToolValidationError2({
|
|
11861
13512
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11862
13513
|
field: "url"
|
|
11863
13514
|
});
|
|
11864
13515
|
}
|
|
11865
13516
|
} else if (ipVersion === 6) {
|
|
11866
|
-
if (
|
|
13517
|
+
if (isPrivateIPv62(host)) {
|
|
11867
13518
|
throw new ToolValidationError2({
|
|
11868
13519
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11869
13520
|
field: "url"
|
|
@@ -11871,9 +13522,9 @@ async function assertNotPrivate(hostname4) {
|
|
|
11871
13522
|
}
|
|
11872
13523
|
} else {
|
|
11873
13524
|
try {
|
|
11874
|
-
const records = await
|
|
13525
|
+
const records = await dns2.lookup(host, { all: true });
|
|
11875
13526
|
for (const r of records) {
|
|
11876
|
-
const bad = r.family === 4 ?
|
|
13527
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11877
13528
|
if (bad) {
|
|
11878
13529
|
throw new ToolValidationError2({
|
|
11879
13530
|
message: `fetch: resolved to private address ${r.address}`,
|
|
@@ -12212,13 +13863,13 @@ var formatTool = {
|
|
|
12212
13863
|
}
|
|
12213
13864
|
};
|
|
12214
13865
|
async function detectFixer(cwd) {
|
|
12215
|
-
const { stat:
|
|
13866
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
12216
13867
|
try {
|
|
12217
|
-
await
|
|
13868
|
+
await stat18(`${cwd}/biome.json`);
|
|
12218
13869
|
return "biome";
|
|
12219
13870
|
} catch {
|
|
12220
13871
|
try {
|
|
12221
|
-
await
|
|
13872
|
+
await stat18(`${cwd}/.prettierrc`);
|
|
12222
13873
|
return "prettier";
|
|
12223
13874
|
} catch {
|
|
12224
13875
|
return "biome";
|
|
@@ -12230,7 +13881,7 @@ async function detectFixer(cwd) {
|
|
|
12230
13881
|
init_util();
|
|
12231
13882
|
import { spawn as spawn9 } from "node:child_process";
|
|
12232
13883
|
import { statSync as statSync3 } from "node:fs";
|
|
12233
|
-
import { dirname as
|
|
13884
|
+
import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
|
|
12234
13885
|
import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
|
|
12235
13886
|
var TIMEOUT_MS2 = 3e4;
|
|
12236
13887
|
var MAX_OUTPUT3 = 1e5;
|
|
@@ -12373,9 +14024,9 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
12373
14024
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
12374
14025
|
}
|
|
12375
14026
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
12376
|
-
const root =
|
|
12377
|
-
const abs =
|
|
12378
|
-
if (abs !== root && !abs.startsWith(root +
|
|
14027
|
+
const root = resolve11(projectRoot);
|
|
14028
|
+
const abs = resolve11(root, input.worktreePath);
|
|
14029
|
+
if (abs !== root && !abs.startsWith(root + sep4)) {
|
|
12379
14030
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
12380
14031
|
}
|
|
12381
14032
|
}
|
|
@@ -12386,12 +14037,12 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
12386
14037
|
let dir = cwd;
|
|
12387
14038
|
for (let i = 0; i < 20; i++) {
|
|
12388
14039
|
try {
|
|
12389
|
-
const
|
|
12390
|
-
if (
|
|
14040
|
+
const stat18 = statSync3(`${dir}/.git`);
|
|
14041
|
+
if (stat18.isDirectory() || stat18.isFile()) return dir;
|
|
12391
14042
|
} catch {
|
|
12392
14043
|
}
|
|
12393
14044
|
if (dir === root) break;
|
|
12394
|
-
const parent =
|
|
14045
|
+
const parent = dirname12(dir);
|
|
12395
14046
|
if (parent === dir) break;
|
|
12396
14047
|
dir = parent;
|
|
12397
14048
|
}
|
|
@@ -12468,7 +14119,7 @@ function buildArgs(input) {
|
|
|
12468
14119
|
}
|
|
12469
14120
|
}
|
|
12470
14121
|
function runGit2(args, cwd, signal) {
|
|
12471
|
-
return new Promise((
|
|
14122
|
+
return new Promise((resolve15) => {
|
|
12472
14123
|
let stdout = "";
|
|
12473
14124
|
let stderr = "";
|
|
12474
14125
|
const child = spawn9("git", args, {
|
|
@@ -12489,7 +14140,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12489
14140
|
}
|
|
12490
14141
|
});
|
|
12491
14142
|
child.on("error", (err) => {
|
|
12492
|
-
|
|
14143
|
+
resolve15({
|
|
12493
14144
|
command: args[0],
|
|
12494
14145
|
stdout: normalizeCommandOutput(stdout),
|
|
12495
14146
|
stderr: err.message,
|
|
@@ -12498,7 +14149,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12498
14149
|
});
|
|
12499
14150
|
});
|
|
12500
14151
|
child.on("close", (code) => {
|
|
12501
|
-
|
|
14152
|
+
resolve15({
|
|
12502
14153
|
command: args[0],
|
|
12503
14154
|
stdout: normalizeCommandOutput(stdout),
|
|
12504
14155
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -12510,8 +14161,8 @@ function runGit2(args, cwd, signal) {
|
|
|
12510
14161
|
}
|
|
12511
14162
|
|
|
12512
14163
|
// src/glob.ts
|
|
12513
|
-
import * as
|
|
12514
|
-
import * as
|
|
14164
|
+
import * as fs20 from "node:fs/promises";
|
|
14165
|
+
import * as path24 from "node:path";
|
|
12515
14166
|
import { compileGlob as compileGlob3 } from "@wrongstack/core";
|
|
12516
14167
|
|
|
12517
14168
|
// src/_concurrency.ts
|
|
@@ -12583,7 +14234,7 @@ var globTool = {
|
|
|
12583
14234
|
return;
|
|
12584
14235
|
}
|
|
12585
14236
|
try {
|
|
12586
|
-
const st = await
|
|
14237
|
+
const st = await fs20.stat(full);
|
|
12587
14238
|
if (truncated || results.length >= limit) {
|
|
12588
14239
|
truncated = true;
|
|
12589
14240
|
return;
|
|
@@ -12604,7 +14255,7 @@ var globTool = {
|
|
|
12604
14255
|
}
|
|
12605
14256
|
let entries;
|
|
12606
14257
|
try {
|
|
12607
|
-
entries = await
|
|
14258
|
+
entries = await fs20.readdir(dir, { withFileTypes: true });
|
|
12608
14259
|
} catch {
|
|
12609
14260
|
return;
|
|
12610
14261
|
}
|
|
@@ -12615,7 +14266,7 @@ var globTool = {
|
|
|
12615
14266
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
12616
14267
|
if (ignored.includes(name)) continue;
|
|
12617
14268
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
12618
|
-
const full =
|
|
14269
|
+
const full = path24.join(dir, name);
|
|
12619
14270
|
if (e.isDirectory()) {
|
|
12620
14271
|
subdirs.push({ full, rel });
|
|
12621
14272
|
} else if (e.isFile()) {
|
|
@@ -12628,13 +14279,13 @@ var globTool = {
|
|
|
12628
14279
|
}
|
|
12629
14280
|
} else if (e.isSymbolicLink()) {
|
|
12630
14281
|
try {
|
|
12631
|
-
const st = await
|
|
14282
|
+
const st = await fs20.stat(full);
|
|
12632
14283
|
if (st.isDirectory()) {
|
|
12633
|
-
const real = await
|
|
14284
|
+
const real = await fs20.realpath(full);
|
|
12634
14285
|
await assertRealInsideRoot(real, ctx);
|
|
12635
14286
|
subdirs.push({ full, rel });
|
|
12636
14287
|
} else if (st.isFile()) {
|
|
12637
|
-
const real = await
|
|
14288
|
+
const real = await fs20.realpath(full);
|
|
12638
14289
|
await assertRealInsideRoot(real, ctx);
|
|
12639
14290
|
re.lastIndex = 0;
|
|
12640
14291
|
const relMatch = re.test(rel);
|
|
@@ -12659,7 +14310,7 @@ var globTool = {
|
|
|
12659
14310
|
};
|
|
12660
14311
|
async function readGitignore(dir) {
|
|
12661
14312
|
try {
|
|
12662
|
-
const raw = await
|
|
14313
|
+
const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
|
|
12663
14314
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
12664
14315
|
} catch {
|
|
12665
14316
|
return [];
|
|
@@ -12669,8 +14320,8 @@ async function readGitignore(dir) {
|
|
|
12669
14320
|
// src/grep.ts
|
|
12670
14321
|
import { expectDefined as expectDefined7 } from "@wrongstack/core";
|
|
12671
14322
|
import { spawn as spawn10 } from "node:child_process";
|
|
12672
|
-
import * as
|
|
12673
|
-
import * as
|
|
14323
|
+
import * as fs21 from "node:fs/promises";
|
|
14324
|
+
import * as path25 from "node:path";
|
|
12674
14325
|
import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
|
|
12675
14326
|
|
|
12676
14327
|
// src/_regex.ts
|
|
@@ -12815,13 +14466,13 @@ var grepTool = {
|
|
|
12815
14466
|
}
|
|
12816
14467
|
};
|
|
12817
14468
|
async function detectRg(signal) {
|
|
12818
|
-
return new Promise((
|
|
14469
|
+
return new Promise((resolve15) => {
|
|
12819
14470
|
try {
|
|
12820
14471
|
const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
|
|
12821
|
-
p.on("error", () =>
|
|
12822
|
-
p.on("close", (code) =>
|
|
14472
|
+
p.on("error", () => resolve15(false));
|
|
14473
|
+
p.on("close", (code) => resolve15(code === 0));
|
|
12823
14474
|
} catch {
|
|
12824
|
-
|
|
14475
|
+
resolve15(false);
|
|
12825
14476
|
}
|
|
12826
14477
|
});
|
|
12827
14478
|
}
|
|
@@ -12972,9 +14623,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12972
14623
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
12973
14624
|
if (globRe) globRe.lastIndex = 0;
|
|
12974
14625
|
try {
|
|
12975
|
-
const
|
|
12976
|
-
if (!
|
|
12977
|
-
const file = await
|
|
14626
|
+
const stat18 = await fs21.stat(full);
|
|
14627
|
+
if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
|
|
14628
|
+
const file = await fs21.open(full, "r");
|
|
12978
14629
|
try {
|
|
12979
14630
|
let bytesReadTotal = 0;
|
|
12980
14631
|
let lineNumber = 0;
|
|
@@ -13053,7 +14704,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
13053
14704
|
if (stopped || signal.aborted) return;
|
|
13054
14705
|
let entries;
|
|
13055
14706
|
try {
|
|
13056
|
-
entries = await
|
|
14707
|
+
entries = await fs21.readdir(dir, { withFileTypes: true });
|
|
13057
14708
|
} catch {
|
|
13058
14709
|
return;
|
|
13059
14710
|
}
|
|
@@ -13063,7 +14714,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
13063
14714
|
if (stopped) return;
|
|
13064
14715
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
13065
14716
|
if (e.isSymbolicLink()) continue;
|
|
13066
|
-
const full =
|
|
14717
|
+
const full = path25.join(dir, e.name);
|
|
13067
14718
|
if (e.isDirectory()) {
|
|
13068
14719
|
subdirs.push(full);
|
|
13069
14720
|
} else if (e.isFile()) {
|
|
@@ -13086,7 +14737,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
13086
14737
|
init_spawn_stream();
|
|
13087
14738
|
init_util();
|
|
13088
14739
|
init_legacy_bridge();
|
|
13089
|
-
import { join as
|
|
14740
|
+
import { join as join21 } from "node:path";
|
|
13090
14741
|
import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
|
|
13091
14742
|
var installTool = {
|
|
13092
14743
|
name: "install",
|
|
@@ -13263,16 +14914,16 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
13263
14914
|
case "pnpm":
|
|
13264
14915
|
case "yarn":
|
|
13265
14916
|
case "npm":
|
|
13266
|
-
return
|
|
14917
|
+
return join21(cwd, "package.json");
|
|
13267
14918
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
13268
14919
|
default:
|
|
13269
|
-
return
|
|
14920
|
+
return join21(cwd, "package.json");
|
|
13270
14921
|
}
|
|
13271
14922
|
}
|
|
13272
14923
|
|
|
13273
14924
|
// src/json.ts
|
|
13274
14925
|
init_util();
|
|
13275
|
-
import * as
|
|
14926
|
+
import * as fs22 from "node:fs/promises";
|
|
13276
14927
|
import { deepMerge as deepMergeCore } from "@wrongstack/core";
|
|
13277
14928
|
var jsonTool = {
|
|
13278
14929
|
name: "json",
|
|
@@ -13347,7 +14998,7 @@ async function executeParse(input, ctx) {
|
|
|
13347
14998
|
let raw;
|
|
13348
14999
|
if (input.file) {
|
|
13349
15000
|
try {
|
|
13350
|
-
raw = await
|
|
15001
|
+
raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13351
15002
|
} catch {
|
|
13352
15003
|
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
13353
15004
|
}
|
|
@@ -13405,7 +15056,7 @@ async function executeQuery(input, ctx) {
|
|
|
13405
15056
|
let parsed;
|
|
13406
15057
|
if (input.file) {
|
|
13407
15058
|
try {
|
|
13408
|
-
const raw = await
|
|
15059
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13409
15060
|
parsed = JSON.parse(raw);
|
|
13410
15061
|
} catch {
|
|
13411
15062
|
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
@@ -13447,7 +15098,7 @@ async function executeValidate(input, ctx) {
|
|
|
13447
15098
|
let parsed;
|
|
13448
15099
|
if (input.file) {
|
|
13449
15100
|
try {
|
|
13450
|
-
const raw = await
|
|
15101
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13451
15102
|
parsed = JSON.parse(raw);
|
|
13452
15103
|
} catch {
|
|
13453
15104
|
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
@@ -13489,7 +15140,7 @@ async function executeTransform(input, ctx) {
|
|
|
13489
15140
|
let parsed;
|
|
13490
15141
|
if (input.file) {
|
|
13491
15142
|
try {
|
|
13492
|
-
const raw = await
|
|
15143
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13493
15144
|
parsed = JSON.parse(raw);
|
|
13494
15145
|
} catch {
|
|
13495
15146
|
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
@@ -13651,56 +15302,56 @@ function jmespathSearch(data, query) {
|
|
|
13651
15302
|
}
|
|
13652
15303
|
function validateJsonSchema(data, schema) {
|
|
13653
15304
|
const errors = [];
|
|
13654
|
-
function check(value, s,
|
|
15305
|
+
function check(value, s, path33) {
|
|
13655
15306
|
if (s["type"]) {
|
|
13656
15307
|
const expectedType = s["type"];
|
|
13657
15308
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
13658
15309
|
if (expectedType === "integer") {
|
|
13659
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
15310
|
+
if (!Number.isInteger(value)) errors.push(`${path33}: expected integer, got ${actualType}`);
|
|
13660
15311
|
} else if (expectedType !== actualType) {
|
|
13661
|
-
errors.push(`${
|
|
15312
|
+
errors.push(`${path33}: expected ${expectedType}, got ${actualType}`);
|
|
13662
15313
|
}
|
|
13663
15314
|
}
|
|
13664
15315
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
13665
15316
|
try {
|
|
13666
15317
|
new URL(value);
|
|
13667
15318
|
} catch {
|
|
13668
|
-
errors.push(`${
|
|
15319
|
+
errors.push(`${path33}: not a valid URI`);
|
|
13669
15320
|
}
|
|
13670
15321
|
}
|
|
13671
15322
|
if (typeof value === "string" && s["pattern"]) {
|
|
13672
15323
|
const re = new RegExp(s["pattern"]);
|
|
13673
|
-
if (!re.test(value)) errors.push(`${
|
|
15324
|
+
if (!re.test(value)) errors.push(`${path33}: does not match pattern ${s["pattern"]}`);
|
|
13674
15325
|
}
|
|
13675
15326
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
13676
|
-
errors.push(`${
|
|
15327
|
+
errors.push(`${path33}: string too short (min ${s["minLength"]})`);
|
|
13677
15328
|
}
|
|
13678
15329
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
13679
|
-
errors.push(`${
|
|
15330
|
+
errors.push(`${path33}: string too long (max ${s["maxLength"]})`);
|
|
13680
15331
|
}
|
|
13681
15332
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
13682
|
-
errors.push(`${
|
|
15333
|
+
errors.push(`${path33}: below minimum ${s["minimum"]}`);
|
|
13683
15334
|
}
|
|
13684
15335
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
13685
|
-
errors.push(`${
|
|
15336
|
+
errors.push(`${path33}: above maximum ${s["maximum"]}`);
|
|
13686
15337
|
}
|
|
13687
15338
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
13688
15339
|
for (let i = 0; i < value.length; i++) {
|
|
13689
|
-
check(value[i], s["items"], `${
|
|
15340
|
+
check(value[i], s["items"], `${path33}[${i}]`);
|
|
13690
15341
|
}
|
|
13691
15342
|
}
|
|
13692
15343
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
13693
15344
|
const props = s["properties"];
|
|
13694
15345
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
13695
|
-
check(value[k], propSchema, `${
|
|
15346
|
+
check(value[k], propSchema, `${path33}.${k}`);
|
|
13696
15347
|
}
|
|
13697
15348
|
}
|
|
13698
15349
|
}
|
|
13699
15350
|
check(data, schema, "$");
|
|
13700
15351
|
return { valid: errors.length === 0, errors };
|
|
13701
15352
|
}
|
|
13702
|
-
function simpleQuery(data,
|
|
13703
|
-
const parts =
|
|
15353
|
+
function simpleQuery(data, path33) {
|
|
15354
|
+
const parts = path33.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
13704
15355
|
let current = data;
|
|
13705
15356
|
for (const part of parts) {
|
|
13706
15357
|
if (current === null || current === void 0) return void 0;
|
|
@@ -14704,11 +16355,11 @@ var lintTool = {
|
|
|
14704
16355
|
}
|
|
14705
16356
|
};
|
|
14706
16357
|
async function detectLinter(cwd) {
|
|
14707
|
-
const { stat:
|
|
16358
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
14708
16359
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
14709
16360
|
for (const f of checks) {
|
|
14710
16361
|
try {
|
|
14711
|
-
await
|
|
16362
|
+
await stat18(`${cwd}/${f}`);
|
|
14712
16363
|
if (f.includes("biome")) return "biome";
|
|
14713
16364
|
if (f.includes("eslint")) return "eslint";
|
|
14714
16365
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -14808,7 +16459,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14808
16459
|
};
|
|
14809
16460
|
}
|
|
14810
16461
|
args.push("--timestamps", service);
|
|
14811
|
-
return new Promise((
|
|
16462
|
+
return new Promise((resolve15) => {
|
|
14812
16463
|
let stdout = "";
|
|
14813
16464
|
let stderr = "";
|
|
14814
16465
|
const MAX = 2e5;
|
|
@@ -14824,7 +16475,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14824
16475
|
if (settled) return;
|
|
14825
16476
|
settled = true;
|
|
14826
16477
|
clearTimeout(timer);
|
|
14827
|
-
|
|
16478
|
+
resolve15(result);
|
|
14828
16479
|
};
|
|
14829
16480
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
14830
16481
|
const timer = setTimeout(() => {
|
|
@@ -14859,16 +16510,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14859
16510
|
}
|
|
14860
16511
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
14861
16512
|
var MAX_TAIL_LINES = 1e5;
|
|
14862
|
-
async function fileLogs(
|
|
16513
|
+
async function fileLogs(path33, lines, filterRe, stream) {
|
|
14863
16514
|
const { createInterface } = await import("node:readline");
|
|
14864
|
-
const { createReadStream } = await import("node:fs");
|
|
16515
|
+
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
14865
16516
|
const entries = [];
|
|
14866
16517
|
const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
|
|
14867
16518
|
const window = new Array(effLines);
|
|
14868
16519
|
let writeIdx = 0;
|
|
14869
16520
|
let totalLines = 0;
|
|
14870
16521
|
const rl = createInterface({
|
|
14871
|
-
input:
|
|
16522
|
+
input: createReadStream2(path33),
|
|
14872
16523
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
14873
16524
|
});
|
|
14874
16525
|
for await (const line of rl) {
|
|
@@ -14889,7 +16540,7 @@ async function fileLogs(path29, lines, filterRe, stream) {
|
|
|
14889
16540
|
if (parsed) entries.push(parsed);
|
|
14890
16541
|
}
|
|
14891
16542
|
return {
|
|
14892
|
-
source:
|
|
16543
|
+
source: path33,
|
|
14893
16544
|
entries,
|
|
14894
16545
|
total: entries.length,
|
|
14895
16546
|
truncated: totalLines > effLines,
|
|
@@ -15043,7 +16694,7 @@ var outdatedTool = {
|
|
|
15043
16694
|
}
|
|
15044
16695
|
};
|
|
15045
16696
|
function runOutdated(manager, args, cwd, signal) {
|
|
15046
|
-
return new Promise((
|
|
16697
|
+
return new Promise((resolve15) => {
|
|
15047
16698
|
let stdout = "";
|
|
15048
16699
|
let stderr = "";
|
|
15049
16700
|
const MAX = 1e5;
|
|
@@ -15068,10 +16719,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
15068
16719
|
});
|
|
15069
16720
|
child.on("close", (code) => {
|
|
15070
16721
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
15071
|
-
|
|
16722
|
+
resolve15(result);
|
|
15072
16723
|
});
|
|
15073
16724
|
child.on("error", (e) => {
|
|
15074
|
-
|
|
16725
|
+
resolve15({
|
|
15075
16726
|
exit_code: 1,
|
|
15076
16727
|
packages: [],
|
|
15077
16728
|
total: 0,
|
|
@@ -15081,9 +16732,9 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
15081
16732
|
});
|
|
15082
16733
|
});
|
|
15083
16734
|
}
|
|
15084
|
-
function parseOutdatedOutput(
|
|
16735
|
+
function parseOutdatedOutput(json2, exitCode) {
|
|
15085
16736
|
const packages = [];
|
|
15086
|
-
if (!
|
|
16737
|
+
if (!json2) {
|
|
15087
16738
|
return {
|
|
15088
16739
|
exit_code: exitCode,
|
|
15089
16740
|
packages: [],
|
|
@@ -15093,7 +16744,7 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
15093
16744
|
};
|
|
15094
16745
|
}
|
|
15095
16746
|
try {
|
|
15096
|
-
const data = JSON.parse(
|
|
16747
|
+
const data = JSON.parse(json2);
|
|
15097
16748
|
for (const name of Object.keys(data)) {
|
|
15098
16749
|
const info = data[name];
|
|
15099
16750
|
packages.push({
|
|
@@ -15111,17 +16762,17 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
15111
16762
|
exit_code: exitCode,
|
|
15112
16763
|
packages,
|
|
15113
16764
|
total: packages.length,
|
|
15114
|
-
output:
|
|
15115
|
-
truncated:
|
|
16765
|
+
output: json2,
|
|
16766
|
+
truncated: json2.length >= 1e5
|
|
15116
16767
|
};
|
|
15117
16768
|
}
|
|
15118
16769
|
|
|
15119
16770
|
// src/patch.ts
|
|
15120
16771
|
init_util();
|
|
15121
16772
|
import { spawn as spawn13 } from "node:child_process";
|
|
15122
|
-
import * as
|
|
16773
|
+
import * as fs23 from "node:fs/promises";
|
|
15123
16774
|
import * as os7 from "node:os";
|
|
15124
|
-
import * as
|
|
16775
|
+
import * as path26 from "node:path";
|
|
15125
16776
|
import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
|
|
15126
16777
|
var patchTool = {
|
|
15127
16778
|
name: "patch",
|
|
@@ -15157,9 +16808,9 @@ var patchTool = {
|
|
|
15157
16808
|
for (const t of targets) {
|
|
15158
16809
|
const stripped = stripPathComponents(t, strip);
|
|
15159
16810
|
if (!stripped) continue;
|
|
15160
|
-
const candidate =
|
|
15161
|
-
const rel =
|
|
15162
|
-
if (rel.startsWith("..") ||
|
|
16811
|
+
const candidate = path26.resolve(dir, stripped);
|
|
16812
|
+
const rel = path26.relative(ctx.projectRoot, candidate);
|
|
16813
|
+
if (rel.startsWith("..") || path26.isAbsolute(rel)) {
|
|
15163
16814
|
return {
|
|
15164
16815
|
applied: 0,
|
|
15165
16816
|
rejected: 1,
|
|
@@ -15176,12 +16827,12 @@ var patchTool = {
|
|
|
15176
16827
|
beforeContents.set(target, await readTextForTracking(target));
|
|
15177
16828
|
}
|
|
15178
16829
|
}
|
|
15179
|
-
const tmpDir = await
|
|
16830
|
+
const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
|
|
15180
16831
|
try {
|
|
15181
|
-
await
|
|
16832
|
+
await fs23.chmod(tmpDir, 448).catch(() => {
|
|
15182
16833
|
});
|
|
15183
|
-
const patchFile =
|
|
15184
|
-
await
|
|
16834
|
+
const patchFile = path26.join(tmpDir, "in.diff");
|
|
16835
|
+
await fs23.writeFile(patchFile, input.patch, { mode: 384 });
|
|
15185
16836
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
15186
16837
|
const result = await runPatch(args, dir, opts.signal);
|
|
15187
16838
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -15199,8 +16850,8 @@ var patchTool = {
|
|
|
15199
16850
|
const before = beforeContents.get(target) ?? null;
|
|
15200
16851
|
const after = await readTextForTracking(target);
|
|
15201
16852
|
if (after === null || after === before) continue;
|
|
15202
|
-
const
|
|
15203
|
-
if (
|
|
16853
|
+
const stat18 = await fs23.stat(target).catch(() => null);
|
|
16854
|
+
if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
|
|
15204
16855
|
ctx.session?.recordFileChange?.({
|
|
15205
16856
|
path: target,
|
|
15206
16857
|
action: before === null ? "created" : "modified",
|
|
@@ -15217,7 +16868,7 @@ var patchTool = {
|
|
|
15217
16868
|
message: result.stdout || "patch applied"
|
|
15218
16869
|
};
|
|
15219
16870
|
} finally {
|
|
15220
|
-
await
|
|
16871
|
+
await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
15221
16872
|
});
|
|
15222
16873
|
}
|
|
15223
16874
|
}
|
|
@@ -15225,9 +16876,9 @@ var patchTool = {
|
|
|
15225
16876
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
15226
16877
|
async function readTextForTracking(absPath) {
|
|
15227
16878
|
try {
|
|
15228
|
-
const
|
|
15229
|
-
if (!
|
|
15230
|
-
const buf = await
|
|
16879
|
+
const stat18 = await fs23.stat(absPath);
|
|
16880
|
+
if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
|
|
16881
|
+
const buf = await fs23.readFile(absPath);
|
|
15231
16882
|
if (buf.includes(0)) return null;
|
|
15232
16883
|
return buf.toString("utf8");
|
|
15233
16884
|
} catch {
|
|
@@ -15252,7 +16903,7 @@ function stripPathComponents(p, strip) {
|
|
|
15252
16903
|
return parts.slice(strip).join("/");
|
|
15253
16904
|
}
|
|
15254
16905
|
function runPatch(args, cwd, signal) {
|
|
15255
|
-
return new Promise((
|
|
16906
|
+
return new Promise((resolve15) => {
|
|
15256
16907
|
let stdout = "";
|
|
15257
16908
|
let stderr = "";
|
|
15258
16909
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -15263,8 +16914,8 @@ function runPatch(args, cwd, signal) {
|
|
|
15263
16914
|
child.stderr?.on("data", (c) => {
|
|
15264
16915
|
stderr += c.toString();
|
|
15265
16916
|
});
|
|
15266
|
-
child.on("close", (code) =>
|
|
15267
|
-
child.on("error", (e) =>
|
|
16917
|
+
child.on("close", (code) => resolve15({ exitCode: code ?? 1, stdout, stderr }));
|
|
16918
|
+
child.on("error", (e) => resolve15({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
15268
16919
|
});
|
|
15269
16920
|
}
|
|
15270
16921
|
function extractPatchedFiles(output) {
|
|
@@ -15541,13 +17192,13 @@ ${formatTaskList(taskFile.tasks)}`
|
|
|
15541
17192
|
}
|
|
15542
17193
|
};
|
|
15543
17194
|
function mkResult(plan, ok, message, todos) {
|
|
15544
|
-
const
|
|
17195
|
+
const open3 = plan.items.filter((i) => i.status !== "done").length;
|
|
15545
17196
|
const result = {
|
|
15546
17197
|
ok,
|
|
15547
17198
|
message,
|
|
15548
17199
|
plan: formatPlan(plan),
|
|
15549
17200
|
count: plan.items.length,
|
|
15550
|
-
open:
|
|
17201
|
+
open: open3
|
|
15551
17202
|
};
|
|
15552
17203
|
if (todos !== void 0) result.todos = todos;
|
|
15553
17204
|
return result;
|
|
@@ -15555,7 +17206,7 @@ function mkResult(plan, ok, message, todos) {
|
|
|
15555
17206
|
|
|
15556
17207
|
// src/read.ts
|
|
15557
17208
|
init_util();
|
|
15558
|
-
import * as
|
|
17209
|
+
import * as fs24 from "node:fs/promises";
|
|
15559
17210
|
import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
|
|
15560
17211
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
15561
17212
|
var readTool = {
|
|
@@ -15604,9 +17255,9 @@ var readTool = {
|
|
|
15604
17255
|
});
|
|
15605
17256
|
}
|
|
15606
17257
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15607
|
-
let
|
|
17258
|
+
let stat18;
|
|
15608
17259
|
try {
|
|
15609
|
-
|
|
17260
|
+
stat18 = await fs24.stat(absPath);
|
|
15610
17261
|
} catch (err) {
|
|
15611
17262
|
const code = err.code;
|
|
15612
17263
|
if (code === "ENOENT") {
|
|
@@ -15625,7 +17276,7 @@ var readTool = {
|
|
|
15625
17276
|
cause: err
|
|
15626
17277
|
});
|
|
15627
17278
|
}
|
|
15628
|
-
if (!
|
|
17279
|
+
if (!stat18.isFile()) {
|
|
15629
17280
|
throw new FsError({
|
|
15630
17281
|
message: `read: "${input.path}" is not a regular file`,
|
|
15631
17282
|
code: "FS_READ_FAILED",
|
|
@@ -15633,22 +17284,22 @@ var readTool = {
|
|
|
15633
17284
|
context: { reason: "not-a-regular-file" }
|
|
15634
17285
|
});
|
|
15635
17286
|
}
|
|
15636
|
-
if (
|
|
17287
|
+
if (stat18.size > MAX_BYTES2) {
|
|
15637
17288
|
throw new FsError({
|
|
15638
|
-
message: `read: file too large (${
|
|
17289
|
+
message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
|
|
15639
17290
|
code: "FS_READ_FAILED",
|
|
15640
17291
|
path: absPath,
|
|
15641
|
-
context: { size:
|
|
17292
|
+
context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
15642
17293
|
});
|
|
15643
17294
|
}
|
|
15644
17295
|
const offset = Math.max(1, input.offset ?? 1);
|
|
15645
17296
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
15646
17297
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
15647
17298
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
15648
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
15649
|
-
ctx.recordRead(absPath,
|
|
17299
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
|
|
17300
|
+
ctx.recordRead(absPath, stat18.mtimeMs);
|
|
15650
17301
|
return {
|
|
15651
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
17302
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
15652
17303
|
total_lines: prior.totalLines,
|
|
15653
17304
|
encoding: "utf8",
|
|
15654
17305
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -15656,7 +17307,7 @@ var readTool = {
|
|
|
15656
17307
|
note: "Repeated read suppressed to save tokens."
|
|
15657
17308
|
};
|
|
15658
17309
|
}
|
|
15659
|
-
const buf = await
|
|
17310
|
+
const buf = await fs24.readFile(absPath);
|
|
15660
17311
|
if (isBinaryBuffer(buf)) {
|
|
15661
17312
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
15662
17313
|
}
|
|
@@ -15665,10 +17316,10 @@ var readTool = {
|
|
|
15665
17316
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
15666
17317
|
const total = allLines.length;
|
|
15667
17318
|
if (input.mode === "summary") {
|
|
15668
|
-
ctx.recordRead(absPath,
|
|
15669
|
-
rememberReadRange(ctx, absPath,
|
|
17319
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
17320
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
|
|
15670
17321
|
return {
|
|
15671
|
-
text: summarizeFile(input.path,
|
|
17322
|
+
text: summarizeFile(input.path, stat18.size, allLines),
|
|
15672
17323
|
total_lines: total,
|
|
15673
17324
|
encoding: "utf8",
|
|
15674
17325
|
truncated: total > 200,
|
|
@@ -15676,13 +17327,13 @@ var readTool = {
|
|
|
15676
17327
|
};
|
|
15677
17328
|
}
|
|
15678
17329
|
if (limit === 0) {
|
|
15679
|
-
ctx.recordRead(absPath,
|
|
15680
|
-
rememberReadRange(ctx, absPath,
|
|
17330
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
17331
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
|
|
15681
17332
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
15682
17333
|
}
|
|
15683
17334
|
if (offset > total) {
|
|
15684
|
-
ctx.recordRead(absPath,
|
|
15685
|
-
rememberReadRange(ctx, absPath,
|
|
17335
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
17336
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
|
|
15686
17337
|
return {
|
|
15687
17338
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
15688
17339
|
total_lines: total,
|
|
@@ -15694,8 +17345,8 @@ var readTool = {
|
|
|
15694
17345
|
const truncated = offset - 1 + slice.length < total;
|
|
15695
17346
|
const width = String(offset + slice.length - 1).length;
|
|
15696
17347
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
15697
|
-
ctx.recordRead(absPath,
|
|
15698
|
-
rememberReadRange(ctx, absPath,
|
|
17348
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
17349
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
|
|
15699
17350
|
return {
|
|
15700
17351
|
text: numbered,
|
|
15701
17352
|
total_lines: total,
|
|
@@ -15764,10 +17415,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
15764
17415
|
// src/replace.ts
|
|
15765
17416
|
import { expectDefined as expectDefined8 } from "@wrongstack/core";
|
|
15766
17417
|
import { spawn as spawn14 } from "node:child_process";
|
|
15767
|
-
import * as
|
|
15768
|
-
import * as
|
|
17418
|
+
import * as fs25 from "node:fs/promises";
|
|
17419
|
+
import * as path27 from "node:path";
|
|
15769
17420
|
import {
|
|
15770
|
-
atomicWrite as
|
|
17421
|
+
atomicWrite as atomicWrite3,
|
|
15771
17422
|
buildChildEnv as buildChildEnv9,
|
|
15772
17423
|
compileGlob as compileGlob5,
|
|
15773
17424
|
detectNewlineStyle as detectNewlineStyle2,
|
|
@@ -15838,11 +17489,11 @@ var replaceTool = {
|
|
|
15838
17489
|
const dryRun = input.dry_run ?? true;
|
|
15839
17490
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
15840
17491
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
15841
|
-
const realRoot = await
|
|
17492
|
+
const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
15842
17493
|
const results = [];
|
|
15843
17494
|
let totalReplacements = 0;
|
|
15844
17495
|
for (const absPath of fileList) {
|
|
15845
|
-
const lstat2 = await
|
|
17496
|
+
const lstat2 = await fs25.lstat(absPath).catch((err) => {
|
|
15846
17497
|
if (err.code === "ENOENT") return null;
|
|
15847
17498
|
throw err;
|
|
15848
17499
|
});
|
|
@@ -15850,17 +17501,17 @@ var replaceTool = {
|
|
|
15850
17501
|
if (lstat2.isSymbolicLink()) continue;
|
|
15851
17502
|
let realPath;
|
|
15852
17503
|
try {
|
|
15853
|
-
realPath = await
|
|
17504
|
+
realPath = await fs25.realpath(absPath);
|
|
15854
17505
|
} catch {
|
|
15855
17506
|
continue;
|
|
15856
17507
|
}
|
|
15857
|
-
const rel =
|
|
15858
|
-
if (rel.startsWith("..") ||
|
|
15859
|
-
const
|
|
15860
|
-
if (!
|
|
17508
|
+
const rel = path27.relative(realRoot, realPath);
|
|
17509
|
+
if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
|
|
17510
|
+
const stat18 = await fs25.stat(realPath).catch(() => null);
|
|
17511
|
+
if (!stat18?.isFile()) continue;
|
|
15861
17512
|
let content;
|
|
15862
17513
|
try {
|
|
15863
|
-
const buf = await
|
|
17514
|
+
const buf = await fs25.readFile(realPath);
|
|
15864
17515
|
if (isBinaryBuffer(buf)) continue;
|
|
15865
17516
|
content = buf.toString("utf8");
|
|
15866
17517
|
} catch {
|
|
@@ -15882,8 +17533,8 @@ var replaceTool = {
|
|
|
15882
17533
|
totalReplacements += count;
|
|
15883
17534
|
if (!dryRun) {
|
|
15884
17535
|
const newContent = toStyle2(newContentLf, style);
|
|
15885
|
-
await
|
|
15886
|
-
const written = await
|
|
17536
|
+
await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
|
|
17537
|
+
const written = await fs25.stat(realPath).catch(() => null);
|
|
15887
17538
|
if (written) {
|
|
15888
17539
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
15889
17540
|
}
|
|
@@ -15922,8 +17573,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
15922
17573
|
const resolved = [];
|
|
15923
17574
|
for (const p of parts) {
|
|
15924
17575
|
const absPath = safeResolve(p, ctx);
|
|
15925
|
-
const
|
|
15926
|
-
if (
|
|
17576
|
+
const stat18 = await fs25.stat(absPath).catch(() => null);
|
|
17577
|
+
if (stat18?.isFile()) {
|
|
15927
17578
|
resolved.push(absPath);
|
|
15928
17579
|
}
|
|
15929
17580
|
}
|
|
@@ -15941,13 +17592,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
15941
17592
|
return await globNative(pattern, base, extraGlob);
|
|
15942
17593
|
}
|
|
15943
17594
|
function checkRg() {
|
|
15944
|
-
return new Promise((
|
|
17595
|
+
return new Promise((resolve15) => {
|
|
15945
17596
|
try {
|
|
15946
17597
|
const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
|
|
15947
|
-
p.on("error", () =>
|
|
15948
|
-
p.on("close", (code) =>
|
|
17598
|
+
p.on("error", () => resolve15(false));
|
|
17599
|
+
p.on("close", (code) => resolve15(code === 0));
|
|
15949
17600
|
} catch {
|
|
15950
|
-
|
|
17601
|
+
resolve15(false);
|
|
15951
17602
|
}
|
|
15952
17603
|
});
|
|
15953
17604
|
}
|
|
@@ -15964,10 +17615,10 @@ function spawnRgFind(pattern, base) {
|
|
|
15964
17615
|
buf += chunk.toString();
|
|
15965
17616
|
});
|
|
15966
17617
|
return {
|
|
15967
|
-
promise: new Promise((
|
|
17618
|
+
promise: new Promise((resolve15, reject) => {
|
|
15968
17619
|
child.on("error", reject);
|
|
15969
17620
|
child.on("close", () => {
|
|
15970
|
-
|
|
17621
|
+
resolve15(buf.split("\n").filter(Boolean));
|
|
15971
17622
|
});
|
|
15972
17623
|
})
|
|
15973
17624
|
};
|
|
@@ -15978,16 +17629,16 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
15978
17629
|
const walk2 = async (dir) => {
|
|
15979
17630
|
let entries;
|
|
15980
17631
|
try {
|
|
15981
|
-
entries = await
|
|
17632
|
+
entries = await fs25.readdir(dir, { withFileTypes: true });
|
|
15982
17633
|
} catch {
|
|
15983
17634
|
return;
|
|
15984
17635
|
}
|
|
15985
17636
|
for (const e of entries) {
|
|
15986
17637
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
15987
|
-
const full =
|
|
17638
|
+
const full = path27.join(dir, e.name);
|
|
15988
17639
|
try {
|
|
15989
|
-
const
|
|
15990
|
-
if (
|
|
17640
|
+
const stat18 = await fs25.lstat(full);
|
|
17641
|
+
if (stat18.isSymbolicLink()) continue;
|
|
15991
17642
|
} catch {
|
|
15992
17643
|
continue;
|
|
15993
17644
|
}
|
|
@@ -16010,9 +17661,9 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
16010
17661
|
|
|
16011
17662
|
// src/scaffold.ts
|
|
16012
17663
|
init_util();
|
|
16013
|
-
import * as
|
|
16014
|
-
import * as
|
|
16015
|
-
import { atomicWrite as
|
|
17664
|
+
import * as fs26 from "node:fs/promises";
|
|
17665
|
+
import * as path28 from "node:path";
|
|
17666
|
+
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
16016
17667
|
var BUILT_IN_TEMPLATES = {
|
|
16017
17668
|
"npm-package": {
|
|
16018
17669
|
description: "Basic npm package with ESM",
|
|
@@ -16161,17 +17812,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
16161
17812
|
let filesCreated = 0;
|
|
16162
17813
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
16163
17814
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
16164
|
-
const joinedPath =
|
|
16165
|
-
const root =
|
|
16166
|
-
const target =
|
|
16167
|
-
const rel =
|
|
16168
|
-
if (rel.startsWith("..") ||
|
|
17815
|
+
const joinedPath = path28.join(cwd, resolvedPath);
|
|
17816
|
+
const root = path28.resolve(ctx.projectRoot);
|
|
17817
|
+
const target = path28.resolve(joinedPath);
|
|
17818
|
+
const rel = path28.relative(root, target);
|
|
17819
|
+
if (rel.startsWith("..") || path28.isAbsolute(rel)) {
|
|
16169
17820
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
16170
17821
|
}
|
|
16171
17822
|
const fullPath = target;
|
|
16172
17823
|
if (!dryRun) {
|
|
16173
|
-
await
|
|
16174
|
-
await
|
|
17824
|
+
await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
|
|
17825
|
+
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
16175
17826
|
}
|
|
16176
17827
|
files.push(resolvedPath);
|
|
16177
17828
|
filesCreated++;
|
|
@@ -16205,6 +17856,7 @@ var DEFAULT_NUM = 10;
|
|
|
16205
17856
|
var MAX_RESULTS = 50;
|
|
16206
17857
|
var TIMEOUT_MS3 = 15e3;
|
|
16207
17858
|
var CACHE_TTL_MS = 3e5;
|
|
17859
|
+
var CACHE_MAX_ENTRIES = 200;
|
|
16208
17860
|
var cache = /* @__PURE__ */ new Map();
|
|
16209
17861
|
var searchTool = {
|
|
16210
17862
|
name: "search",
|
|
@@ -16326,7 +17978,7 @@ var searchTool = {
|
|
|
16326
17978
|
}
|
|
16327
17979
|
const finalResults = ranked.slice(0, num);
|
|
16328
17980
|
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
16329
|
-
|
|
17981
|
+
pruneCacheEntries();
|
|
16330
17982
|
yield {
|
|
16331
17983
|
type: "partial_output",
|
|
16332
17984
|
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
@@ -16348,11 +18000,16 @@ var searchTool = {
|
|
|
16348
18000
|
};
|
|
16349
18001
|
}
|
|
16350
18002
|
};
|
|
16351
|
-
function
|
|
18003
|
+
function pruneCacheEntries() {
|
|
16352
18004
|
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
16353
18005
|
for (const [key, entry] of cache.entries()) {
|
|
16354
18006
|
if (entry.timestamp < cutoff) cache.delete(key);
|
|
16355
18007
|
}
|
|
18008
|
+
while (cache.size > CACHE_MAX_ENTRIES) {
|
|
18009
|
+
const oldestKey = cache.keys().next().value;
|
|
18010
|
+
if (oldestKey === void 0) break;
|
|
18011
|
+
cache.delete(oldestKey);
|
|
18012
|
+
}
|
|
16356
18013
|
}
|
|
16357
18014
|
function rankSearchResults(results, query) {
|
|
16358
18015
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
@@ -16581,7 +18238,7 @@ function decodeHtmlEntities(text) {
|
|
|
16581
18238
|
}
|
|
16582
18239
|
|
|
16583
18240
|
// src/set-working-dir.ts
|
|
16584
|
-
import * as
|
|
18241
|
+
import * as fs27 from "node:fs/promises";
|
|
16585
18242
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
16586
18243
|
var setWorkingDirTool = {
|
|
16587
18244
|
name: "set_working_dir",
|
|
@@ -16620,7 +18277,7 @@ var setWorkingDirTool = {
|
|
|
16620
18277
|
};
|
|
16621
18278
|
}
|
|
16622
18279
|
try {
|
|
16623
|
-
await
|
|
18280
|
+
await fs27.access(resolved);
|
|
16624
18281
|
} catch {
|
|
16625
18282
|
try {
|
|
16626
18283
|
ctx.setWorkingDir(previous);
|
|
@@ -17020,7 +18677,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
17020
18677
|
init_spawn_stream();
|
|
17021
18678
|
init_util();
|
|
17022
18679
|
init_legacy_bridge();
|
|
17023
|
-
import * as
|
|
18680
|
+
import * as path29 from "node:path";
|
|
17024
18681
|
var testTool = {
|
|
17025
18682
|
name: "test",
|
|
17026
18683
|
category: "Code Quality",
|
|
@@ -17123,11 +18780,11 @@ var testTool = {
|
|
|
17123
18780
|
}
|
|
17124
18781
|
};
|
|
17125
18782
|
async function detectRunner(cwd) {
|
|
17126
|
-
const { stat:
|
|
18783
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
17127
18784
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
17128
18785
|
for (const f of candidates) {
|
|
17129
18786
|
try {
|
|
17130
|
-
await
|
|
18787
|
+
await stat18(path29.join(cwd, f));
|
|
17131
18788
|
if (f.includes("vitest")) return "vitest";
|
|
17132
18789
|
if (f.includes("jest")) return "jest";
|
|
17133
18790
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -17593,8 +19250,8 @@ var toolUseTool = {
|
|
|
17593
19250
|
// src/tree.ts
|
|
17594
19251
|
init_util();
|
|
17595
19252
|
import { expectDefined as expectDefined10 } from "@wrongstack/core";
|
|
17596
|
-
import * as
|
|
17597
|
-
import * as
|
|
19253
|
+
import * as fs28 from "node:fs/promises";
|
|
19254
|
+
import * as path30 from "node:path";
|
|
17598
19255
|
var DEFAULT_IGNORE5 = [
|
|
17599
19256
|
"node_modules",
|
|
17600
19257
|
".git",
|
|
@@ -17735,7 +19392,7 @@ var treeTool = {
|
|
|
17735
19392
|
}
|
|
17736
19393
|
};
|
|
17737
19394
|
async function walkDir(dir, depth, opts) {
|
|
17738
|
-
const entries = await
|
|
19395
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
17739
19396
|
const filtered = entries.filter((e) => {
|
|
17740
19397
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
17741
19398
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -17765,7 +19422,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17765
19422
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
17766
19423
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
17767
19424
|
const childPrefix = opts.prefix + connector;
|
|
17768
|
-
await walkDir(
|
|
19425
|
+
await walkDir(path30.join(dir, entry.name), depth + 1, {
|
|
17769
19426
|
...opts,
|
|
17770
19427
|
prefix: childPrefix,
|
|
17771
19428
|
isLast
|
|
@@ -17778,7 +19435,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17778
19435
|
init_spawn_stream();
|
|
17779
19436
|
init_util();
|
|
17780
19437
|
init_legacy_bridge();
|
|
17781
|
-
import * as
|
|
19438
|
+
import * as path31 from "node:path";
|
|
17782
19439
|
var typecheckTool = {
|
|
17783
19440
|
name: "typecheck",
|
|
17784
19441
|
category: "Code Quality",
|
|
@@ -17877,12 +19534,12 @@ var typecheckTool = {
|
|
|
17877
19534
|
}
|
|
17878
19535
|
};
|
|
17879
19536
|
async function findTsConfig(cwd) {
|
|
17880
|
-
const { stat:
|
|
19537
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
17881
19538
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
17882
19539
|
for (const f of candidates) {
|
|
17883
19540
|
try {
|
|
17884
|
-
const s = await
|
|
17885
|
-
if (s.isFile()) return
|
|
19541
|
+
const s = await stat18(path31.join(cwd, f));
|
|
19542
|
+
if (s.isFile()) return path31.join(cwd, f);
|
|
17886
19543
|
} catch {
|
|
17887
19544
|
}
|
|
17888
19545
|
}
|
|
@@ -17890,8 +19547,8 @@ async function findTsConfig(cwd) {
|
|
|
17890
19547
|
}
|
|
17891
19548
|
|
|
17892
19549
|
// src/write.ts
|
|
17893
|
-
import * as
|
|
17894
|
-
import { atomicWrite as
|
|
19550
|
+
import * as fs29 from "node:fs/promises";
|
|
19551
|
+
import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
|
|
17895
19552
|
init_util();
|
|
17896
19553
|
var writeTool = {
|
|
17897
19554
|
name: "write",
|
|
@@ -17955,14 +19612,14 @@ async function prepareWrite(input, ctx) {
|
|
|
17955
19612
|
let existed = false;
|
|
17956
19613
|
let prev = "";
|
|
17957
19614
|
try {
|
|
17958
|
-
const
|
|
17959
|
-
existed =
|
|
19615
|
+
const stat18 = await fs29.stat(absPath);
|
|
19616
|
+
existed = stat18.isFile();
|
|
17960
19617
|
if (existed) {
|
|
17961
19618
|
if (!ctx.hasRead(absPath)) {
|
|
17962
|
-
prev = await
|
|
17963
|
-
ctx.recordRead(absPath,
|
|
19619
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
19620
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
|
|
17964
19621
|
} else {
|
|
17965
|
-
prev = await
|
|
19622
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
17966
19623
|
}
|
|
17967
19624
|
}
|
|
17968
19625
|
} catch (err) {
|
|
@@ -17974,11 +19631,11 @@ async function prepareWrite(input, ctx) {
|
|
|
17974
19631
|
}
|
|
17975
19632
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
17976
19633
|
signal?.throwIfAborted();
|
|
17977
|
-
await
|
|
19634
|
+
await atomicWrite5(prepared.absPath, input.content);
|
|
17978
19635
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
17979
19636
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
17980
|
-
const
|
|
17981
|
-
ctx.recordRead(prepared.absPath,
|
|
19637
|
+
const stat18 = await fs29.stat(prepared.absPath);
|
|
19638
|
+
ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
|
|
17982
19639
|
ctx.session.recordFileChange({
|
|
17983
19640
|
path: prepared.absPath,
|
|
17984
19641
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -18003,6 +19660,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
18003
19660
|
|
|
18004
19661
|
// src/builtin.ts
|
|
18005
19662
|
var OPTIONAL_TOOLS = [
|
|
19663
|
+
...browserTools,
|
|
19664
|
+
e2ePlanTool,
|
|
18006
19665
|
installTool,
|
|
18007
19666
|
auditTool,
|
|
18008
19667
|
outdatedTool,
|
|
@@ -18066,6 +19725,8 @@ var TIER3_TOOLS = [
|
|
|
18066
19725
|
setWorkingDirTool
|
|
18067
19726
|
];
|
|
18068
19727
|
var builtinTools = [
|
|
19728
|
+
...browserTools,
|
|
19729
|
+
e2ePlanTool,
|
|
18069
19730
|
readTool,
|
|
18070
19731
|
writeTool,
|
|
18071
19732
|
editTool,
|
|
@@ -18409,7 +20070,7 @@ ${mode.description}`
|
|
|
18409
20070
|
// src/pack.ts
|
|
18410
20071
|
var builtinToolsPack = {
|
|
18411
20072
|
name: "builtin-tools",
|
|
18412
|
-
description: "The complete set of built-in tools that ship with WrongStack. Covers filesystem
|
|
20073
|
+
description: "The complete set of built-in tools that ship with WrongStack. Covers filesystem, execution, first-party browser automation, read-only E2E planning, networking, code quality, planning, and meta tools.",
|
|
18413
20074
|
tools: builtinTools
|
|
18414
20075
|
};
|
|
18415
20076
|
|
|
@@ -18985,8 +20646,8 @@ function createGlobalPsSlashCommand() {
|
|
|
18985
20646
|
}
|
|
18986
20647
|
|
|
18987
20648
|
// src/skill.ts
|
|
18988
|
-
import * as
|
|
18989
|
-
import * as
|
|
20649
|
+
import * as fs30 from "node:fs/promises";
|
|
20650
|
+
import * as path32 from "node:path";
|
|
18990
20651
|
import {
|
|
18991
20652
|
SKILL_LIMITS,
|
|
18992
20653
|
stripFrontmatter,
|
|
@@ -19033,7 +20694,7 @@ function makeSkillTool(skillLoader) {
|
|
|
19033
20694
|
field: "name"
|
|
19034
20695
|
});
|
|
19035
20696
|
}
|
|
19036
|
-
const dir =
|
|
20697
|
+
const dir = path32.dirname(manifest.path);
|
|
19037
20698
|
let loadedResource;
|
|
19038
20699
|
if (input.resource?.trim()) {
|
|
19039
20700
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -19083,15 +20744,15 @@ ${listing}`;
|
|
|
19083
20744
|
}
|
|
19084
20745
|
async function loadResource(skillDir, rel) {
|
|
19085
20746
|
const norm = rel.replace(/\\/g, "/");
|
|
19086
|
-
if (
|
|
20747
|
+
if (path32.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
19087
20748
|
throw new ToolValidationError10({
|
|
19088
20749
|
message: `skill: invalid resource path "${rel}"`,
|
|
19089
20750
|
field: "resource"
|
|
19090
20751
|
});
|
|
19091
20752
|
}
|
|
19092
|
-
const absPath =
|
|
19093
|
-
const root =
|
|
19094
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
20753
|
+
const absPath = path32.resolve(skillDir, rel);
|
|
20754
|
+
const root = path32.resolve(skillDir);
|
|
20755
|
+
if (absPath !== root && !absPath.startsWith(root + path32.sep)) {
|
|
19095
20756
|
throw new ToolValidationError10({
|
|
19096
20757
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
19097
20758
|
field: "resource"
|
|
@@ -19099,7 +20760,7 @@ async function loadResource(skillDir, rel) {
|
|
|
19099
20760
|
}
|
|
19100
20761
|
let buf;
|
|
19101
20762
|
try {
|
|
19102
|
-
buf = await
|
|
20763
|
+
buf = await fs30.readFile(absPath);
|
|
19103
20764
|
} catch {
|
|
19104
20765
|
throw new ToolValidationError10({
|
|
19105
20766
|
message: `skill: resource "${rel}" not readable`,
|
|
@@ -19126,17 +20787,17 @@ async function walk(root, dir, out) {
|
|
|
19126
20787
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
19127
20788
|
let entries;
|
|
19128
20789
|
try {
|
|
19129
|
-
entries = await
|
|
20790
|
+
entries = await fs30.readdir(dir, { withFileTypes: true });
|
|
19130
20791
|
} catch {
|
|
19131
20792
|
return;
|
|
19132
20793
|
}
|
|
19133
20794
|
for (const e of entries) {
|
|
19134
20795
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
19135
|
-
const fullPath =
|
|
20796
|
+
const fullPath = path32.join(dir, e.name);
|
|
19136
20797
|
let isDir = e.isDirectory();
|
|
19137
20798
|
if (e.isSymbolicLink()) {
|
|
19138
20799
|
try {
|
|
19139
|
-
isDir = (await
|
|
20800
|
+
isDir = (await fs30.stat(fullPath)).isDirectory();
|
|
19140
20801
|
} catch {
|
|
19141
20802
|
continue;
|
|
19142
20803
|
}
|
|
@@ -19147,9 +20808,9 @@ async function walk(root, dir, out) {
|
|
|
19147
20808
|
} else if (e.isFile()) {
|
|
19148
20809
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
19149
20810
|
try {
|
|
19150
|
-
const
|
|
19151
|
-
const rel =
|
|
19152
|
-
out.push({ path: rel, bytes:
|
|
20811
|
+
const stat18 = await fs30.stat(fullPath);
|
|
20812
|
+
const rel = path32.relative(root, fullPath).split(path32.sep).join("/");
|
|
20813
|
+
out.push({ path: rel, bytes: stat18.size });
|
|
19153
20814
|
} catch {
|
|
19154
20815
|
}
|
|
19155
20816
|
}
|
|
@@ -19198,6 +20859,7 @@ var TOOL_ICON_MAP = {
|
|
|
19198
20859
|
// Testing
|
|
19199
20860
|
test: "test",
|
|
19200
20861
|
tests: "test",
|
|
20862
|
+
e2e_plan: "test",
|
|
19201
20863
|
// Package management
|
|
19202
20864
|
install: "package",
|
|
19203
20865
|
uninstall: "package",
|
|
@@ -19318,6 +20980,9 @@ var TOOL_ICON_CONFIG = {
|
|
|
19318
20980
|
var FALLBACK_ICON = "fallback";
|
|
19319
20981
|
export {
|
|
19320
20982
|
ADDITIONAL_LANGUAGE_PROFILES,
|
|
20983
|
+
BrowserArtifactStore,
|
|
20984
|
+
BrowserNetworkGuardProxy,
|
|
20985
|
+
BrowserSessionManager,
|
|
19321
20986
|
CircuitBreaker,
|
|
19322
20987
|
CircuitOpenError,
|
|
19323
20988
|
FALLBACK_ICON,
|
|
@@ -19332,9 +20997,28 @@ export {
|
|
|
19332
20997
|
TOOL_ICON_CONFIG,
|
|
19333
20998
|
TOOL_ICON_MAP,
|
|
19334
20999
|
_resetProcessRegistry,
|
|
21000
|
+
assertBrowserUrlAllowed,
|
|
19335
21001
|
auditTool,
|
|
19336
21002
|
bashTool,
|
|
19337
21003
|
batchToolUseTool,
|
|
21004
|
+
browserClickTool,
|
|
21005
|
+
browserCloseTool,
|
|
21006
|
+
browserDragTool,
|
|
21007
|
+
browserEvaluateTool,
|
|
21008
|
+
browserHoverTool,
|
|
21009
|
+
browserInstallationDiagnostics,
|
|
21010
|
+
browserListTool,
|
|
21011
|
+
browserNavigateTool,
|
|
21012
|
+
browserOpenTool,
|
|
21013
|
+
browserPressTool,
|
|
21014
|
+
browserScreenshotTool,
|
|
21015
|
+
browserSelectTool,
|
|
21016
|
+
browserSnapshotTool,
|
|
21017
|
+
browserStatusTool,
|
|
21018
|
+
browserTools,
|
|
21019
|
+
browserTypeTool,
|
|
21020
|
+
browserUploadTool,
|
|
21021
|
+
browserWaitTool,
|
|
19338
21022
|
builtinTools,
|
|
19339
21023
|
builtinToolsPack,
|
|
19340
21024
|
cancelPendingReindexes,
|
|
@@ -19351,7 +21035,9 @@ export {
|
|
|
19351
21035
|
detectLanguageWorkspaces,
|
|
19352
21036
|
diagnosticsForInternalSyntax,
|
|
19353
21037
|
diffTool,
|
|
21038
|
+
discoverE2EProjects,
|
|
19354
21039
|
documentTool,
|
|
21040
|
+
e2ePlanTool,
|
|
19355
21041
|
editTool,
|
|
19356
21042
|
enqueueReindex,
|
|
19357
21043
|
ensureSessionShell,
|
|
@@ -19396,10 +21082,12 @@ export {
|
|
|
19396
21082
|
outdatedTool,
|
|
19397
21083
|
parseLanguageDiagnostics,
|
|
19398
21084
|
parsePackageReports,
|
|
21085
|
+
parsePrivateOriginAllowlist,
|
|
19399
21086
|
patchTool,
|
|
19400
21087
|
planLanguageOperation,
|
|
19401
21088
|
planTool,
|
|
19402
21089
|
readTool,
|
|
21090
|
+
redactBrowserText,
|
|
19403
21091
|
relatedMemoryTool,
|
|
19404
21092
|
rememberTool,
|
|
19405
21093
|
replaceTool,
|
|
@@ -19407,12 +21095,15 @@ export {
|
|
|
19407
21095
|
resetExecPolicy,
|
|
19408
21096
|
resetIndexCircuitBreaker,
|
|
19409
21097
|
resetPersistentProcessRegistry,
|
|
21098
|
+
resolvePinnedBrowserTarget,
|
|
19410
21099
|
resolveSessionShell,
|
|
19411
21100
|
runStartupIndex,
|
|
21101
|
+
safeBrowserUrl,
|
|
19412
21102
|
scaffoldTool,
|
|
19413
21103
|
searchCodebaseIndex,
|
|
19414
21104
|
searchMemoryTool,
|
|
19415
21105
|
searchTool,
|
|
21106
|
+
shutdownBrowserTools,
|
|
19416
21107
|
shutdownCodebaseIndexHost,
|
|
19417
21108
|
startProcessGuardian,
|
|
19418
21109
|
stopProcessGuardian,
|