@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/pack.js
CHANGED
|
@@ -56,7 +56,7 @@ function createOutputSpool(opts) {
|
|
|
56
56
|
let filePath = null;
|
|
57
57
|
let failed = false;
|
|
58
58
|
let finalized = false;
|
|
59
|
-
const
|
|
59
|
+
const open3 = () => {
|
|
60
60
|
if (stream || failed) return;
|
|
61
61
|
try {
|
|
62
62
|
const dir = toolOutputDir();
|
|
@@ -89,7 +89,7 @@ function createOutputSpool(opts) {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
head += text;
|
|
92
|
-
|
|
92
|
+
open3();
|
|
93
93
|
head = "";
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
@@ -907,8 +907,8 @@ async function* spawnStream(opts) {
|
|
|
907
907
|
try {
|
|
908
908
|
for (; ; ) {
|
|
909
909
|
while (queue.length === 0) {
|
|
910
|
-
await new Promise((
|
|
911
|
-
waiter =
|
|
910
|
+
await new Promise((resolve14) => {
|
|
911
|
+
waiter = resolve14;
|
|
912
912
|
});
|
|
913
913
|
}
|
|
914
914
|
const chunk = queue.shift();
|
|
@@ -982,14 +982,14 @@ function sha256hex(content) {
|
|
|
982
982
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
983
983
|
}
|
|
984
984
|
async function detectPackageManager(cwd) {
|
|
985
|
-
const { stat:
|
|
985
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
986
986
|
try {
|
|
987
|
-
await
|
|
987
|
+
await stat17(`${cwd}/pnpm-lock.yaml`);
|
|
988
988
|
return "pnpm";
|
|
989
989
|
} catch {
|
|
990
990
|
}
|
|
991
991
|
try {
|
|
992
|
-
await
|
|
992
|
+
await stat17(`${cwd}/yarn.lock`);
|
|
993
993
|
return "yarn";
|
|
994
994
|
} catch {
|
|
995
995
|
}
|
|
@@ -2558,8 +2558,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
|
|
|
2558
2558
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2559
2559
|
}
|
|
2560
2560
|
}
|
|
2561
|
-
function collectFileEvidence(directory, fullPath,
|
|
2562
|
-
const lower =
|
|
2561
|
+
function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
|
|
2562
|
+
const lower = basename8.toLowerCase();
|
|
2563
2563
|
const extension = path4.extname(lower);
|
|
2564
2564
|
for (const profile of profiles) {
|
|
2565
2565
|
const detector = profile.detectors.find(
|
|
@@ -2570,7 +2570,7 @@ function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
|
|
|
2570
2570
|
candidate.evidence.push({
|
|
2571
2571
|
kind: detector.kind,
|
|
2572
2572
|
path: fullPath,
|
|
2573
|
-
value:
|
|
2573
|
+
value: basename8,
|
|
2574
2574
|
weight: detector.weight
|
|
2575
2575
|
});
|
|
2576
2576
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2698,8 +2698,8 @@ function normalizeLimits(input) {
|
|
|
2698
2698
|
async function canonicalDirectory(input) {
|
|
2699
2699
|
const resolved = path4.resolve(input);
|
|
2700
2700
|
const real = await fs2.realpath(resolved);
|
|
2701
|
-
const
|
|
2702
|
-
if (!
|
|
2701
|
+
const stat17 = await fs2.stat(real);
|
|
2702
|
+
if (!stat17.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2703
2703
|
return real;
|
|
2704
2704
|
}
|
|
2705
2705
|
async function canonicalInside(input, root, label) {
|
|
@@ -2719,12 +2719,12 @@ function resolveFrom(cwd, input) {
|
|
|
2719
2719
|
return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
|
|
2720
2720
|
}
|
|
2721
2721
|
function isInside(candidate, root) {
|
|
2722
|
-
const
|
|
2723
|
-
return
|
|
2722
|
+
const relative12 = path4.relative(root, candidate);
|
|
2723
|
+
return relative12 === "" || !relative12.startsWith("..") && !path4.isAbsolute(relative12);
|
|
2724
2724
|
}
|
|
2725
2725
|
function pathDepth(candidate, root) {
|
|
2726
|
-
const
|
|
2727
|
-
return
|
|
2726
|
+
const relative12 = path4.relative(root, candidate);
|
|
2727
|
+
return relative12 === "" ? 0 : relative12.split(path4.sep).length;
|
|
2728
2728
|
}
|
|
2729
2729
|
function dedupeEvidence(items) {
|
|
2730
2730
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3424,8 +3424,8 @@ function compareCandidates(a, b) {
|
|
|
3424
3424
|
return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
|
|
3425
3425
|
}
|
|
3426
3426
|
function workspaceDepth(workspaceRoot, projectRoot) {
|
|
3427
|
-
const
|
|
3428
|
-
return
|
|
3427
|
+
const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3428
|
+
return relative12 === "" ? 0 : relative12.split(path6.sep).length;
|
|
3429
3429
|
}
|
|
3430
3430
|
function validateOperationOptions(operation, packages) {
|
|
3431
3431
|
if (!operation.startsWith("package-") || !packages) return;
|
|
@@ -3455,8 +3455,8 @@ async function canonicalTarget(cwd, target, projectRoot) {
|
|
|
3455
3455
|
return real;
|
|
3456
3456
|
}
|
|
3457
3457
|
function isInside2(candidate, root) {
|
|
3458
|
-
const
|
|
3459
|
-
return
|
|
3458
|
+
const relative12 = path6.relative(path6.resolve(root), path6.resolve(candidate));
|
|
3459
|
+
return relative12 === "" || !relative12.startsWith("..") && !path6.isAbsolute(relative12);
|
|
3460
3460
|
}
|
|
3461
3461
|
var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
|
|
3462
3462
|
var init_plan = __esm({
|
|
@@ -3587,8 +3587,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3587
3587
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3588
3588
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3589
3589
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3590
|
-
const
|
|
3591
|
-
if (
|
|
3590
|
+
const stat17 = await fs4.stat(safeTarget);
|
|
3591
|
+
if (stat17.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3592
3592
|
return unavailableResult(
|
|
3593
3593
|
options,
|
|
3594
3594
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -3635,14 +3635,14 @@ async function validatePlanRealpaths(plan, projectRoot) {
|
|
|
3635
3635
|
return void 0;
|
|
3636
3636
|
}
|
|
3637
3637
|
function isRealInside(candidate, root) {
|
|
3638
|
-
const
|
|
3639
|
-
return
|
|
3638
|
+
const relative12 = path7.relative(root, candidate);
|
|
3639
|
+
return relative12 === "" || !relative12.startsWith("..") && !path7.isAbsolute(relative12);
|
|
3640
3640
|
}
|
|
3641
3641
|
async function assertContainedFile(candidate, projectRoot) {
|
|
3642
3642
|
const realRoot = await fs4.realpath(projectRoot);
|
|
3643
3643
|
const realTarget = await fs4.realpath(candidate);
|
|
3644
|
-
const
|
|
3645
|
-
if (
|
|
3644
|
+
const relative12 = path7.relative(realRoot, realTarget);
|
|
3645
|
+
if (relative12.startsWith("..") || path7.isAbsolute(relative12)) {
|
|
3646
3646
|
throw new Error(`Internal syntax target resolves outside project root: ${candidate}`);
|
|
3647
3647
|
}
|
|
3648
3648
|
return realTarget;
|
|
@@ -3875,8 +3875,8 @@ async function snapshotPaths(paths) {
|
|
|
3875
3875
|
const existing = [];
|
|
3876
3876
|
for (const candidate of paths) {
|
|
3877
3877
|
try {
|
|
3878
|
-
const
|
|
3879
|
-
if (!
|
|
3878
|
+
const stat17 = await fs4.stat(candidate);
|
|
3879
|
+
if (!stat17.isFile()) continue;
|
|
3880
3880
|
existing.push(candidate);
|
|
3881
3881
|
} catch {
|
|
3882
3882
|
}
|
|
@@ -3887,15 +3887,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
|
|
|
3887
3887
|
const beforeSet = new Set(before);
|
|
3888
3888
|
const afterSet = new Set(after);
|
|
3889
3889
|
const changed = /* @__PURE__ */ new Set();
|
|
3890
|
-
for (const
|
|
3891
|
-
if (!beforeSet.has(
|
|
3890
|
+
for (const path32 of after) {
|
|
3891
|
+
if (!beforeSet.has(path32)) changed.add(path32);
|
|
3892
3892
|
}
|
|
3893
|
-
for (const
|
|
3894
|
-
if (!afterSet.has(
|
|
3893
|
+
for (const path32 of before) {
|
|
3894
|
+
if (!afterSet.has(path32)) changed.add(path32);
|
|
3895
3895
|
}
|
|
3896
3896
|
if (beforeSizes && afterSizes) {
|
|
3897
|
-
for (const
|
|
3898
|
-
if (beforeSizes.get(
|
|
3897
|
+
for (const path32 of after) {
|
|
3898
|
+
if (beforeSizes.get(path32) !== afterSizes.get(path32)) changed.add(path32);
|
|
3899
3899
|
}
|
|
3900
3900
|
}
|
|
3901
3901
|
return [...changed].sort();
|
|
@@ -3904,8 +3904,8 @@ async function snapshotSizes(paths) {
|
|
|
3904
3904
|
const sizes = /* @__PURE__ */ new Map();
|
|
3905
3905
|
for (const candidate of paths) {
|
|
3906
3906
|
try {
|
|
3907
|
-
const
|
|
3908
|
-
if (
|
|
3907
|
+
const stat17 = await fs4.stat(candidate);
|
|
3908
|
+
if (stat17.isFile()) sizes.set(candidate, stat17.size);
|
|
3909
3909
|
} catch {
|
|
3910
3910
|
}
|
|
3911
3911
|
}
|
|
@@ -4848,8 +4848,8 @@ var auditTool = {
|
|
|
4848
4848
|
yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
|
|
4849
4849
|
}
|
|
4850
4850
|
};
|
|
4851
|
-
function parseAuditOutput(
|
|
4852
|
-
if (!
|
|
4851
|
+
function parseAuditOutput(json2, exitCode) {
|
|
4852
|
+
if (!json2) {
|
|
4853
4853
|
return {
|
|
4854
4854
|
exit_code: exitCode,
|
|
4855
4855
|
vulnerabilities: [],
|
|
@@ -4860,7 +4860,7 @@ function parseAuditOutput(json, exitCode) {
|
|
|
4860
4860
|
};
|
|
4861
4861
|
}
|
|
4862
4862
|
try {
|
|
4863
|
-
const data = JSON.parse(
|
|
4863
|
+
const data = JSON.parse(json2);
|
|
4864
4864
|
const advisories = [];
|
|
4865
4865
|
const ads = data.advisories ?? {};
|
|
4866
4866
|
for (const id of Object.keys(ads)) {
|
|
@@ -4879,8 +4879,8 @@ function parseAuditOutput(json, exitCode) {
|
|
|
4879
4879
|
vulnerabilities: advisories,
|
|
4880
4880
|
total,
|
|
4881
4881
|
summary,
|
|
4882
|
-
output:
|
|
4883
|
-
truncated:
|
|
4882
|
+
output: json2,
|
|
4883
|
+
truncated: json2.length > 1e5
|
|
4884
4884
|
};
|
|
4885
4885
|
} catch {
|
|
4886
4886
|
return {
|
|
@@ -4888,7 +4888,7 @@ function parseAuditOutput(json, exitCode) {
|
|
|
4888
4888
|
vulnerabilities: [],
|
|
4889
4889
|
total: 0,
|
|
4890
4890
|
summary: "Could not parse audit output",
|
|
4891
|
-
output:
|
|
4891
|
+
output: json2,
|
|
4892
4892
|
truncated: false
|
|
4893
4893
|
};
|
|
4894
4894
|
}
|
|
@@ -5916,10 +5916,10 @@ var bashTool = {
|
|
|
5916
5916
|
queue.push(c);
|
|
5917
5917
|
}
|
|
5918
5918
|
};
|
|
5919
|
-
const next = () => new Promise((
|
|
5919
|
+
const next = () => new Promise((resolve14) => {
|
|
5920
5920
|
const c = queue.shift();
|
|
5921
|
-
if (c)
|
|
5922
|
-
else resolveNext =
|
|
5921
|
+
if (c) resolve14(c);
|
|
5922
|
+
else resolveNext = resolve14;
|
|
5923
5923
|
});
|
|
5924
5924
|
let lastFlush = Date.now();
|
|
5925
5925
|
const flush = () => {
|
|
@@ -6159,6 +6159,1238 @@ async function executeSingle(call, ctx, opts) {
|
|
|
6159
6159
|
}
|
|
6160
6160
|
}
|
|
6161
6161
|
|
|
6162
|
+
// src/browser/tools.ts
|
|
6163
|
+
import * as path12 from "node:path";
|
|
6164
|
+
import { ToolCapabilities } from "@wrongstack/core";
|
|
6165
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
6166
|
+
|
|
6167
|
+
// src/browser/manager.ts
|
|
6168
|
+
import * as fs8 from "node:fs/promises";
|
|
6169
|
+
import * as path11 from "node:path";
|
|
6170
|
+
import { ulid as ulid2 } from "@wrongstack/core/utils";
|
|
6171
|
+
|
|
6172
|
+
// src/browser/artifacts.ts
|
|
6173
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6174
|
+
import { createReadStream } from "node:fs";
|
|
6175
|
+
import * as fs7 from "node:fs/promises";
|
|
6176
|
+
import * as path10 from "node:path";
|
|
6177
|
+
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
6178
|
+
var BrowserArtifactStore = class {
|
|
6179
|
+
constructor(root) {
|
|
6180
|
+
this.root = root;
|
|
6181
|
+
}
|
|
6182
|
+
root;
|
|
6183
|
+
async write(sessionId, kind, extension, mimeType, content) {
|
|
6184
|
+
const id = ulid();
|
|
6185
|
+
const dir = path10.join(this.root, safeSegment(sessionId));
|
|
6186
|
+
const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
|
|
6187
|
+
await atomicWrite(target, content, { mode: 384 });
|
|
6188
|
+
return this.record(id, sessionId, kind, target, mimeType);
|
|
6189
|
+
}
|
|
6190
|
+
async record(id, sessionId, kind, target, mimeType) {
|
|
6191
|
+
await fs7.chmod(target, 384).catch(() => void 0);
|
|
6192
|
+
const [stat17, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
6193
|
+
const artifact = {
|
|
6194
|
+
id,
|
|
6195
|
+
kind,
|
|
6196
|
+
sensitivity: "sensitive",
|
|
6197
|
+
path: target,
|
|
6198
|
+
mimeType,
|
|
6199
|
+
sizeBytes: stat17.size,
|
|
6200
|
+
sha256,
|
|
6201
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6202
|
+
};
|
|
6203
|
+
const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
|
|
6204
|
+
try {
|
|
6205
|
+
await atomicWrite(
|
|
6206
|
+
metadataPath,
|
|
6207
|
+
`${JSON.stringify({
|
|
6208
|
+
id: artifact.id,
|
|
6209
|
+
sessionId: safeSegment(sessionId),
|
|
6210
|
+
kind: artifact.kind,
|
|
6211
|
+
sensitivity: artifact.sensitivity,
|
|
6212
|
+
mimeType: artifact.mimeType,
|
|
6213
|
+
sizeBytes: artifact.sizeBytes,
|
|
6214
|
+
sha256: artifact.sha256,
|
|
6215
|
+
createdAt: artifact.createdAt
|
|
6216
|
+
})}
|
|
6217
|
+
`,
|
|
6218
|
+
{ mode: 384 }
|
|
6219
|
+
);
|
|
6220
|
+
} catch (error) {
|
|
6221
|
+
await fs7.rm(target, { force: true }).catch(() => void 0);
|
|
6222
|
+
throw error;
|
|
6223
|
+
}
|
|
6224
|
+
return artifact;
|
|
6225
|
+
}
|
|
6226
|
+
pathFor(sessionId, extension) {
|
|
6227
|
+
const id = ulid();
|
|
6228
|
+
return {
|
|
6229
|
+
id,
|
|
6230
|
+
path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
|
|
6231
|
+
};
|
|
6232
|
+
}
|
|
6233
|
+
async describe(id, kind, target, mimeType) {
|
|
6234
|
+
return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
|
|
6235
|
+
}
|
|
6236
|
+
};
|
|
6237
|
+
async function hashFile(target) {
|
|
6238
|
+
const hash = createHash3("sha256");
|
|
6239
|
+
await new Promise((resolve14, reject) => {
|
|
6240
|
+
const stream = createReadStream(target);
|
|
6241
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
6242
|
+
stream.once("error", reject);
|
|
6243
|
+
stream.once("end", resolve14);
|
|
6244
|
+
});
|
|
6245
|
+
return hash.digest("hex");
|
|
6246
|
+
}
|
|
6247
|
+
function safeSegment(value) {
|
|
6248
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
// src/browser/network-guard-proxy.ts
|
|
6252
|
+
import * as http from "node:http";
|
|
6253
|
+
import * as net2 from "node:net";
|
|
6254
|
+
|
|
6255
|
+
// src/browser/security.ts
|
|
6256
|
+
import * as dns from "node:dns/promises";
|
|
6257
|
+
import * as net from "node:net";
|
|
6258
|
+
import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
6259
|
+
var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
|
|
6260
|
+
async function assertBrowserUrlAllowed(rawUrl, opts) {
|
|
6261
|
+
const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
|
|
6262
|
+
if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6263
|
+
if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
|
|
6264
|
+
await assertNotPrivateHost(url.hostname);
|
|
6265
|
+
}
|
|
6266
|
+
return url;
|
|
6267
|
+
}
|
|
6268
|
+
async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
|
|
6269
|
+
const url = parseBrowserUrl(rawUrl, true);
|
|
6270
|
+
const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
|
|
6271
|
+
const hostname2 = unbracket(url.hostname);
|
|
6272
|
+
const literalFamily = net.isIP(hostname2);
|
|
6273
|
+
if (literalFamily === 4 || literalFamily === 6) {
|
|
6274
|
+
if (!allowPrivate && isPrivateAddress(hostname2, literalFamily)) {
|
|
6275
|
+
throw new Error(`browser: blocked private/loopback address "${hostname2}"`);
|
|
6276
|
+
}
|
|
6277
|
+
return { url, address: hostname2, family: literalFamily };
|
|
6278
|
+
}
|
|
6279
|
+
if ((hostname2 === "localhost" || hostname2.endsWith(".localhost")) && !allowPrivate) {
|
|
6280
|
+
throw new Error("browser: blocked localhost target");
|
|
6281
|
+
}
|
|
6282
|
+
const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
|
|
6283
|
+
let records;
|
|
6284
|
+
try {
|
|
6285
|
+
records = await lookup3(hostname2);
|
|
6286
|
+
} catch (error) {
|
|
6287
|
+
throw new Error(
|
|
6288
|
+
`browser: DNS resolution failed for "${hostname2}": ${error instanceof Error ? error.message : String(error)}`
|
|
6289
|
+
);
|
|
6290
|
+
}
|
|
6291
|
+
if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname2}"`);
|
|
6292
|
+
for (const record of records) {
|
|
6293
|
+
if (record.family !== 4 && record.family !== 6) {
|
|
6294
|
+
throw new Error(`browser: DNS returned an unsupported address family for "${hostname2}"`);
|
|
6295
|
+
}
|
|
6296
|
+
if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
|
|
6297
|
+
throw new Error(`browser: resolved to private address ${record.address}`);
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
const selected = records[0];
|
|
6301
|
+
return { url, address: selected.address, family: selected.family };
|
|
6302
|
+
}
|
|
6303
|
+
function parsePrivateOriginAllowlist(raw) {
|
|
6304
|
+
if (!raw?.trim()) return [];
|
|
6305
|
+
const origins = /* @__PURE__ */ new Set();
|
|
6306
|
+
for (const entry of raw.split(",")) {
|
|
6307
|
+
const candidate = entry.trim();
|
|
6308
|
+
if (!candidate) continue;
|
|
6309
|
+
const url = parseBrowserUrl(candidate, true);
|
|
6310
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
6311
|
+
throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
|
|
6312
|
+
}
|
|
6313
|
+
origins.add(url.origin);
|
|
6314
|
+
}
|
|
6315
|
+
return [...origins];
|
|
6316
|
+
}
|
|
6317
|
+
function parseBrowserUrl(rawUrl, navigation) {
|
|
6318
|
+
let url;
|
|
6319
|
+
try {
|
|
6320
|
+
url = new URL(rawUrl);
|
|
6321
|
+
} catch {
|
|
6322
|
+
throw new Error("browser: url must be an absolute URL");
|
|
6323
|
+
}
|
|
6324
|
+
if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6325
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
6326
|
+
throw new Error(`browser: unsupported protocol "${url.protocol}"`);
|
|
6327
|
+
}
|
|
6328
|
+
if (url.username || url.password) {
|
|
6329
|
+
throw new Error("browser: credentials in URLs are not allowed");
|
|
6330
|
+
}
|
|
6331
|
+
return url;
|
|
6332
|
+
}
|
|
6333
|
+
function isAllowedPrivateOrigin(url, origins) {
|
|
6334
|
+
return origins?.includes(url.origin) ?? false;
|
|
6335
|
+
}
|
|
6336
|
+
function isPrivateAddress(address, family) {
|
|
6337
|
+
return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
|
|
6338
|
+
}
|
|
6339
|
+
function unbracket(hostname2) {
|
|
6340
|
+
return hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
6341
|
+
}
|
|
6342
|
+
function safeBrowserUrl(rawUrl) {
|
|
6343
|
+
try {
|
|
6344
|
+
const url = new URL(rawUrl);
|
|
6345
|
+
url.username = "";
|
|
6346
|
+
url.password = "";
|
|
6347
|
+
url.search = "";
|
|
6348
|
+
url.hash = "";
|
|
6349
|
+
return url.toString();
|
|
6350
|
+
} catch {
|
|
6351
|
+
return "about:blank";
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
function redactBrowserText(text) {
|
|
6355
|
+
return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
|
|
6356
|
+
/\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
|
|
6357
|
+
"$1=[REDACTED]"
|
|
6358
|
+
);
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
// src/browser/network-guard-proxy.ts
|
|
6362
|
+
var BrowserNetworkGuardProxy = class {
|
|
6363
|
+
constructor(options = {}) {
|
|
6364
|
+
this.options = options;
|
|
6365
|
+
this.server = http.createServer((request2, response) => {
|
|
6366
|
+
void this.forwardHttp(request2, response);
|
|
6367
|
+
});
|
|
6368
|
+
this.server.on("connect", (request2, socket, head) => {
|
|
6369
|
+
void this.forwardConnect(request2, socket, head);
|
|
6370
|
+
});
|
|
6371
|
+
this.server.on("upgrade", (request2, socket, head) => {
|
|
6372
|
+
void this.forwardUpgrade(request2, socket, head);
|
|
6373
|
+
});
|
|
6374
|
+
this.server.on("connection", (socket) => {
|
|
6375
|
+
this.sockets.add(socket);
|
|
6376
|
+
socket.once("close", () => this.sockets.delete(socket));
|
|
6377
|
+
});
|
|
6378
|
+
this.server.on("clientError", (_error, socket) => {
|
|
6379
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
6380
|
+
});
|
|
6381
|
+
}
|
|
6382
|
+
options;
|
|
6383
|
+
server;
|
|
6384
|
+
sockets = /* @__PURE__ */ new Set();
|
|
6385
|
+
startPromise;
|
|
6386
|
+
url;
|
|
6387
|
+
async start() {
|
|
6388
|
+
if (this.url) return this.url;
|
|
6389
|
+
if (this.startPromise) return this.startPromise;
|
|
6390
|
+
this.startPromise = new Promise((resolve14, reject) => {
|
|
6391
|
+
const onError = (error) => {
|
|
6392
|
+
this.server.off("listening", onListening);
|
|
6393
|
+
reject(error);
|
|
6394
|
+
};
|
|
6395
|
+
const onListening = () => {
|
|
6396
|
+
this.server.off("error", onError);
|
|
6397
|
+
const address = this.server.address();
|
|
6398
|
+
if (!address || typeof address === "string") {
|
|
6399
|
+
reject(new Error("browser: network guard proxy failed to bind"));
|
|
6400
|
+
return;
|
|
6401
|
+
}
|
|
6402
|
+
this.url = `http://127.0.0.1:${address.port}`;
|
|
6403
|
+
resolve14(this.url);
|
|
6404
|
+
};
|
|
6405
|
+
this.server.once("error", onError);
|
|
6406
|
+
this.server.once("listening", onListening);
|
|
6407
|
+
this.server.listen(0, "127.0.0.1");
|
|
6408
|
+
}).finally(() => {
|
|
6409
|
+
this.startPromise = void 0;
|
|
6410
|
+
});
|
|
6411
|
+
return this.startPromise;
|
|
6412
|
+
}
|
|
6413
|
+
async close() {
|
|
6414
|
+
this.url = void 0;
|
|
6415
|
+
for (const socket of this.sockets) socket.destroy();
|
|
6416
|
+
this.sockets.clear();
|
|
6417
|
+
if (!this.server.listening) return;
|
|
6418
|
+
await new Promise((resolve14) => this.server.close(() => resolve14()));
|
|
6419
|
+
}
|
|
6420
|
+
async forwardHttp(request2, response) {
|
|
6421
|
+
try {
|
|
6422
|
+
const target = await this.resolve(request2.url ?? "");
|
|
6423
|
+
if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
|
|
6424
|
+
const headers = sanitizeHeaders(request2.headers);
|
|
6425
|
+
headers.host = target.url.host;
|
|
6426
|
+
const upstream = http.request({
|
|
6427
|
+
host: target.address,
|
|
6428
|
+
family: target.family,
|
|
6429
|
+
port: Number(target.url.port || 80),
|
|
6430
|
+
method: request2.method,
|
|
6431
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6432
|
+
headers,
|
|
6433
|
+
agent: false
|
|
6434
|
+
});
|
|
6435
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
|
|
6436
|
+
upstream.destroy(new Error("browser: upstream connection timed out"));
|
|
6437
|
+
});
|
|
6438
|
+
upstream.on("response", (upstreamResponse) => {
|
|
6439
|
+
response.writeHead(
|
|
6440
|
+
upstreamResponse.statusCode ?? 502,
|
|
6441
|
+
sanitizeHeaders(upstreamResponse.headers)
|
|
6442
|
+
);
|
|
6443
|
+
upstreamResponse.pipe(response);
|
|
6444
|
+
});
|
|
6445
|
+
upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
|
|
6446
|
+
request2.on("aborted", () => upstream.destroy());
|
|
6447
|
+
request2.pipe(upstream);
|
|
6448
|
+
} catch {
|
|
6449
|
+
writeProxyError(response, 403, "Blocked by browser network policy");
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
async forwardConnect(request2, client, head) {
|
|
6453
|
+
try {
|
|
6454
|
+
const authority = request2.url ?? "";
|
|
6455
|
+
const target = await this.resolve(`https://${authority}`);
|
|
6456
|
+
const upstream = net2.connect({
|
|
6457
|
+
host: target.address,
|
|
6458
|
+
family: target.family,
|
|
6459
|
+
port: Number(target.url.port || 443)
|
|
6460
|
+
});
|
|
6461
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
|
|
6462
|
+
upstream.once("connect", () => {
|
|
6463
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
6464
|
+
if (head.length > 0) upstream.write(head);
|
|
6465
|
+
upstream.pipe(client);
|
|
6466
|
+
client.pipe(upstream);
|
|
6467
|
+
});
|
|
6468
|
+
upstream.once("error", () => {
|
|
6469
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6470
|
+
});
|
|
6471
|
+
client.once("close", () => upstream.destroy());
|
|
6472
|
+
} catch {
|
|
6473
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
async forwardUpgrade(request2, client, head) {
|
|
6477
|
+
let upstream;
|
|
6478
|
+
try {
|
|
6479
|
+
const rawUrl = request2.url ?? "";
|
|
6480
|
+
const websocketUrl = new URL(rawUrl);
|
|
6481
|
+
if (websocketUrl.protocol !== "ws:") {
|
|
6482
|
+
throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
|
|
6483
|
+
}
|
|
6484
|
+
websocketUrl.protocol = "http:";
|
|
6485
|
+
const target = await this.resolve(websocketUrl.toString());
|
|
6486
|
+
const headers = sanitizeHeaders(request2.headers, true);
|
|
6487
|
+
headers.host = target.url.host;
|
|
6488
|
+
upstream = http.request({
|
|
6489
|
+
host: target.address,
|
|
6490
|
+
family: target.family,
|
|
6491
|
+
port: Number(target.url.port || 80),
|
|
6492
|
+
method: request2.method,
|
|
6493
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6494
|
+
headers,
|
|
6495
|
+
agent: false
|
|
6496
|
+
});
|
|
6497
|
+
upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
6498
|
+
writeRawResponseHead(client, response);
|
|
6499
|
+
if (head.length > 0) upstreamSocket.write(head);
|
|
6500
|
+
if (upstreamHead.length > 0) client.write(upstreamHead);
|
|
6501
|
+
upstreamSocket.pipe(client);
|
|
6502
|
+
client.pipe(upstreamSocket);
|
|
6503
|
+
client.once("close", () => upstreamSocket.destroy());
|
|
6504
|
+
});
|
|
6505
|
+
upstream.once("response", (response) => {
|
|
6506
|
+
writeRawResponseHead(client, response);
|
|
6507
|
+
response.pipe(client);
|
|
6508
|
+
});
|
|
6509
|
+
upstream.once("error", () => {
|
|
6510
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6511
|
+
});
|
|
6512
|
+
client.once("close", () => upstream?.destroy());
|
|
6513
|
+
upstream.end();
|
|
6514
|
+
} catch {
|
|
6515
|
+
upstream?.destroy();
|
|
6516
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
resolve(rawUrl) {
|
|
6520
|
+
return resolvePinnedBrowserTarget(rawUrl, {
|
|
6521
|
+
allowPrivateHosts: this.options.allowPrivateHosts,
|
|
6522
|
+
allowedPrivateOrigins: this.options.allowedPrivateOrigins,
|
|
6523
|
+
lookup: this.options.lookup
|
|
6524
|
+
});
|
|
6525
|
+
}
|
|
6526
|
+
};
|
|
6527
|
+
function sanitizeHeaders(headers, preserveUpgrade = false) {
|
|
6528
|
+
const sanitized = { ...headers };
|
|
6529
|
+
delete sanitized["proxy-authorization"];
|
|
6530
|
+
delete sanitized["proxy-connection"];
|
|
6531
|
+
if (!preserveUpgrade) delete sanitized.connection;
|
|
6532
|
+
return sanitized;
|
|
6533
|
+
}
|
|
6534
|
+
function writeRawResponseHead(client, response) {
|
|
6535
|
+
const status = response.statusCode ?? 502;
|
|
6536
|
+
const statusText = response.statusMessage ?? "Bad Gateway";
|
|
6537
|
+
const lines = [`HTTP/1.1 ${status} ${statusText}`];
|
|
6538
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) {
|
|
6539
|
+
lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
|
|
6540
|
+
}
|
|
6541
|
+
client.write(`${lines.join("\r\n")}\r
|
|
6542
|
+
\r
|
|
6543
|
+
`);
|
|
6544
|
+
}
|
|
6545
|
+
function writeProxyError(response, status, message) {
|
|
6546
|
+
if (response.headersSent || response.destroyed) return;
|
|
6547
|
+
response.writeHead(status, { "content-type": "text/plain", connection: "close" });
|
|
6548
|
+
response.end(message);
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6551
|
+
// src/browser/manager.ts
|
|
6552
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
|
|
6553
|
+
var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
|
|
6554
|
+
var DEFAULT_MAX_ENTRIES = 100;
|
|
6555
|
+
var BrowserSessionManager = class {
|
|
6556
|
+
constructor(options, launcher = defaultLauncher) {
|
|
6557
|
+
this.launcher = launcher;
|
|
6558
|
+
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
6559
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
6560
|
+
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
6561
|
+
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
6562
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6563
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins
|
|
6564
|
+
});
|
|
6565
|
+
this.headless = options.headless ?? true;
|
|
6566
|
+
this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
6567
|
+
this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
|
|
6568
|
+
this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6569
|
+
this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6570
|
+
}
|
|
6571
|
+
launcher;
|
|
6572
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6573
|
+
artifacts;
|
|
6574
|
+
allowPrivateHosts;
|
|
6575
|
+
allowedPrivateOrigins;
|
|
6576
|
+
networkProxy;
|
|
6577
|
+
headless;
|
|
6578
|
+
operationTimeoutMs;
|
|
6579
|
+
maxSnapshotChars;
|
|
6580
|
+
maxConsoleEntries;
|
|
6581
|
+
maxNetworkEntries;
|
|
6582
|
+
browser;
|
|
6583
|
+
launching;
|
|
6584
|
+
async open(ownerId, input, signal) {
|
|
6585
|
+
signal.throwIfAborted();
|
|
6586
|
+
if (input.url) {
|
|
6587
|
+
await assertBrowserUrlAllowed(input.url, {
|
|
6588
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6589
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6590
|
+
navigation: true
|
|
6591
|
+
});
|
|
6592
|
+
}
|
|
6593
|
+
const browser = await this.ensureBrowser(signal);
|
|
6594
|
+
const proxyServer = await this.networkProxy.start();
|
|
6595
|
+
const context = await abortable(
|
|
6596
|
+
signal,
|
|
6597
|
+
async () => {
|
|
6598
|
+
const created = await browser.newContext({
|
|
6599
|
+
acceptDownloads: false,
|
|
6600
|
+
ignoreHTTPSErrors: false,
|
|
6601
|
+
serviceWorkers: "block",
|
|
6602
|
+
proxy: { server: proxyServer },
|
|
6603
|
+
viewport: input.viewport ?? { width: 1440, height: 900 }
|
|
6604
|
+
});
|
|
6605
|
+
if (signal.aborted) {
|
|
6606
|
+
await created.close().catch(() => void 0);
|
|
6607
|
+
signal.throwIfAborted();
|
|
6608
|
+
}
|
|
6609
|
+
return created;
|
|
6610
|
+
},
|
|
6611
|
+
() => void 0
|
|
6612
|
+
);
|
|
6613
|
+
const session = {
|
|
6614
|
+
id: ulid2(),
|
|
6615
|
+
ownerId,
|
|
6616
|
+
context,
|
|
6617
|
+
page: await context.newPage(),
|
|
6618
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6619
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6620
|
+
tracing: input.trace ?? true,
|
|
6621
|
+
console: [],
|
|
6622
|
+
network: []
|
|
6623
|
+
};
|
|
6624
|
+
this.sessions.set(session.id, session);
|
|
6625
|
+
this.attachEvidenceCollectors(session);
|
|
6626
|
+
await context.route("**/*", async (route) => {
|
|
6627
|
+
try {
|
|
6628
|
+
await assertBrowserUrlAllowed(route.request().url(), {
|
|
6629
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6630
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6631
|
+
navigation: false
|
|
6632
|
+
});
|
|
6633
|
+
await route.continue();
|
|
6634
|
+
} catch {
|
|
6635
|
+
this.pushNetwork(session, {
|
|
6636
|
+
method: route.request().method(),
|
|
6637
|
+
url: safeBrowserUrl(route.request().url()),
|
|
6638
|
+
failed: true,
|
|
6639
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6640
|
+
});
|
|
6641
|
+
await route.abort("blockedbyclient");
|
|
6642
|
+
}
|
|
6643
|
+
});
|
|
6644
|
+
try {
|
|
6645
|
+
if (session.tracing) {
|
|
6646
|
+
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
6647
|
+
}
|
|
6648
|
+
if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
|
|
6649
|
+
return await this.summary(session);
|
|
6650
|
+
} catch (err) {
|
|
6651
|
+
await context.close().catch(() => void 0);
|
|
6652
|
+
this.sessions.delete(session.id);
|
|
6653
|
+
await this.closeBrowserIfIdle();
|
|
6654
|
+
throw err;
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
async list(ownerId) {
|
|
6658
|
+
const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
|
|
6659
|
+
return Promise.all(owned.map((session) => this.summary(session)));
|
|
6660
|
+
}
|
|
6661
|
+
async navigate(id, ownerId, rawUrl, signal) {
|
|
6662
|
+
await assertBrowserUrlAllowed(rawUrl, {
|
|
6663
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6664
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6665
|
+
navigation: true
|
|
6666
|
+
});
|
|
6667
|
+
const session = this.requireOwned(id, ownerId);
|
|
6668
|
+
await this.runPageOperation(
|
|
6669
|
+
session,
|
|
6670
|
+
signal,
|
|
6671
|
+
() => session.page.goto(rawUrl, {
|
|
6672
|
+
waitUntil: "domcontentloaded",
|
|
6673
|
+
timeout: this.operationTimeoutMs
|
|
6674
|
+
})
|
|
6675
|
+
);
|
|
6676
|
+
return this.summary(session);
|
|
6677
|
+
}
|
|
6678
|
+
async snapshot(id, ownerId, signal) {
|
|
6679
|
+
const session = this.requireOwned(id, ownerId);
|
|
6680
|
+
const raw = await this.runPageOperation(session, signal, async () => {
|
|
6681
|
+
try {
|
|
6682
|
+
return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
|
|
6683
|
+
} catch {
|
|
6684
|
+
return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
const aria = redactBrowserText(raw);
|
|
6688
|
+
const truncated = aria.length > this.maxSnapshotChars;
|
|
6689
|
+
return {
|
|
6690
|
+
session: await this.summary(session),
|
|
6691
|
+
aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
|
|
6692
|
+
\u2026[snapshot truncated]` : aria,
|
|
6693
|
+
console: session.console.map((entry) => ({ ...entry })),
|
|
6694
|
+
network: session.network.map((entry) => ({ ...entry })),
|
|
6695
|
+
truncated
|
|
6696
|
+
};
|
|
6697
|
+
}
|
|
6698
|
+
async screenshot(id, ownerId, input, signal) {
|
|
6699
|
+
const session = this.requireOwned(id, ownerId);
|
|
6700
|
+
const bytes = await this.runPageOperation(
|
|
6701
|
+
session,
|
|
6702
|
+
signal,
|
|
6703
|
+
() => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
|
|
6704
|
+
);
|
|
6705
|
+
signal.throwIfAborted();
|
|
6706
|
+
return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
|
|
6707
|
+
}
|
|
6708
|
+
async click(id, ownerId, selector, signal) {
|
|
6709
|
+
const session = this.requireOwned(id, ownerId);
|
|
6710
|
+
await this.runPageOperation(
|
|
6711
|
+
session,
|
|
6712
|
+
signal,
|
|
6713
|
+
() => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
|
|
6714
|
+
);
|
|
6715
|
+
}
|
|
6716
|
+
async type(id, ownerId, selector, text, signal) {
|
|
6717
|
+
const session = this.requireOwned(id, ownerId);
|
|
6718
|
+
await this.runPageOperation(
|
|
6719
|
+
session,
|
|
6720
|
+
signal,
|
|
6721
|
+
() => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
6724
|
+
async select(id, ownerId, selector, value, signal) {
|
|
6725
|
+
const session = this.requireOwned(id, ownerId);
|
|
6726
|
+
await this.runPageOperation(
|
|
6727
|
+
session,
|
|
6728
|
+
signal,
|
|
6729
|
+
() => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
|
|
6730
|
+
);
|
|
6731
|
+
}
|
|
6732
|
+
async press(id, ownerId, key, signal) {
|
|
6733
|
+
const session = this.requireOwned(id, ownerId);
|
|
6734
|
+
await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
|
|
6735
|
+
}
|
|
6736
|
+
async hover(id, ownerId, selector, signal) {
|
|
6737
|
+
const session = this.requireOwned(id, ownerId);
|
|
6738
|
+
await this.runPageOperation(
|
|
6739
|
+
session,
|
|
6740
|
+
signal,
|
|
6741
|
+
() => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
|
|
6742
|
+
);
|
|
6743
|
+
}
|
|
6744
|
+
async wait(id, ownerId, input, signal) {
|
|
6745
|
+
const session = this.requireOwned(id, ownerId);
|
|
6746
|
+
const timeout = Math.min(
|
|
6747
|
+
Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
|
|
6748
|
+
this.operationTimeoutMs
|
|
6749
|
+
);
|
|
6750
|
+
await this.runPageOperation(
|
|
6751
|
+
session,
|
|
6752
|
+
signal,
|
|
6753
|
+
() => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
|
|
6754
|
+
);
|
|
6755
|
+
}
|
|
6756
|
+
async drag(id, ownerId, from, to, signal) {
|
|
6757
|
+
const session = this.requireOwned(id, ownerId);
|
|
6758
|
+
await this.runPageOperation(
|
|
6759
|
+
session,
|
|
6760
|
+
signal,
|
|
6761
|
+
() => session.page.locator(from).dragTo(session.page.locator(to), {
|
|
6762
|
+
timeout: this.operationTimeoutMs
|
|
6763
|
+
})
|
|
6764
|
+
);
|
|
6765
|
+
}
|
|
6766
|
+
async upload(id, ownerId, selector, files, projectRoot, signal) {
|
|
6767
|
+
const session = this.requireOwned(id, ownerId);
|
|
6768
|
+
const root = path11.resolve(projectRoot);
|
|
6769
|
+
const realRoot = await fs8.realpath(root);
|
|
6770
|
+
const resolved = [];
|
|
6771
|
+
for (const file of files) {
|
|
6772
|
+
const absolute = path11.resolve(root, file);
|
|
6773
|
+
const relative12 = path11.relative(root, absolute);
|
|
6774
|
+
if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
|
|
6775
|
+
throw new Error("browser: upload files must stay inside the project root");
|
|
6776
|
+
}
|
|
6777
|
+
const realFile = await fs8.realpath(absolute);
|
|
6778
|
+
const realRelative = path11.relative(realRoot, realFile);
|
|
6779
|
+
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
6780
|
+
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
6781
|
+
}
|
|
6782
|
+
const stat17 = await fs8.stat(realFile);
|
|
6783
|
+
if (!stat17.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
6784
|
+
resolved.push(realFile);
|
|
6785
|
+
}
|
|
6786
|
+
await this.runPageOperation(
|
|
6787
|
+
session,
|
|
6788
|
+
signal,
|
|
6789
|
+
() => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
|
|
6790
|
+
);
|
|
6791
|
+
}
|
|
6792
|
+
async evaluate(id, ownerId, expression, signal) {
|
|
6793
|
+
const session = this.requireOwned(id, ownerId);
|
|
6794
|
+
const result = await this.runPageOperation(
|
|
6795
|
+
session,
|
|
6796
|
+
signal,
|
|
6797
|
+
() => session.page.evaluate(expression)
|
|
6798
|
+
);
|
|
6799
|
+
const serialized = JSON.stringify(result);
|
|
6800
|
+
if (serialized && serialized.length > this.maxSnapshotChars) {
|
|
6801
|
+
throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
|
|
6802
|
+
}
|
|
6803
|
+
return result;
|
|
6804
|
+
}
|
|
6805
|
+
async close(id, ownerId) {
|
|
6806
|
+
const session = this.requireOwned(id, ownerId);
|
|
6807
|
+
let trace;
|
|
6808
|
+
try {
|
|
6809
|
+
if (session.tracing) {
|
|
6810
|
+
const target = this.artifacts.pathFor(session.id, "zip");
|
|
6811
|
+
await fs8.mkdir(path11.dirname(target.path), { recursive: true });
|
|
6812
|
+
await session.context.tracing.stop({ path: target.path });
|
|
6813
|
+
trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
|
|
6814
|
+
}
|
|
6815
|
+
} finally {
|
|
6816
|
+
await session.context.close().catch(() => void 0);
|
|
6817
|
+
this.sessions.delete(id);
|
|
6818
|
+
await this.closeBrowserIfIdle();
|
|
6819
|
+
}
|
|
6820
|
+
return trace ? { trace } : {};
|
|
6821
|
+
}
|
|
6822
|
+
async closeOwner(ownerId) {
|
|
6823
|
+
const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
|
|
6824
|
+
for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
|
|
6825
|
+
}
|
|
6826
|
+
async dispose() {
|
|
6827
|
+
for (const session of [...this.sessions.values()]) {
|
|
6828
|
+
await session.context.close().catch(() => void 0);
|
|
6829
|
+
}
|
|
6830
|
+
this.sessions.clear();
|
|
6831
|
+
const browser = this.browser;
|
|
6832
|
+
this.browser = void 0;
|
|
6833
|
+
this.launching = void 0;
|
|
6834
|
+
await browser?.close().catch(() => void 0);
|
|
6835
|
+
await this.networkProxy.close();
|
|
6836
|
+
}
|
|
6837
|
+
async ensureBrowser(signal) {
|
|
6838
|
+
if (this.browser?.isConnected()) return this.browser;
|
|
6839
|
+
if (!this.launching) {
|
|
6840
|
+
this.launching = this.launcher(this.headless).then((browser) => {
|
|
6841
|
+
this.browser = browser;
|
|
6842
|
+
browser.on("disconnected", () => {
|
|
6843
|
+
if (this.browser === browser) this.browser = void 0;
|
|
6844
|
+
for (const [id, session] of this.sessions) {
|
|
6845
|
+
if (session.context.browser() === browser) this.sessions.delete(id);
|
|
6846
|
+
}
|
|
6847
|
+
});
|
|
6848
|
+
return browser;
|
|
6849
|
+
});
|
|
6850
|
+
}
|
|
6851
|
+
try {
|
|
6852
|
+
return await abortable(
|
|
6853
|
+
signal,
|
|
6854
|
+
() => this.launching,
|
|
6855
|
+
async () => {
|
|
6856
|
+
const browser = await this.launching?.catch(() => void 0);
|
|
6857
|
+
await browser?.close().catch(() => void 0);
|
|
6858
|
+
}
|
|
6859
|
+
);
|
|
6860
|
+
} finally {
|
|
6861
|
+
this.launching = void 0;
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
requireOwned(id, ownerId) {
|
|
6865
|
+
const session = this.sessions.get(id);
|
|
6866
|
+
if (!session) throw new Error(`browser: unknown session "${id}"`);
|
|
6867
|
+
if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
|
|
6868
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6869
|
+
return session;
|
|
6870
|
+
}
|
|
6871
|
+
async summary(session) {
|
|
6872
|
+
return {
|
|
6873
|
+
id: session.id,
|
|
6874
|
+
ownerId: session.ownerId,
|
|
6875
|
+
url: safeBrowserUrl(session.page.url()),
|
|
6876
|
+
title: redactBrowserText(await session.page.title().catch(() => "")),
|
|
6877
|
+
createdAt: session.createdAt,
|
|
6878
|
+
lastUsedAt: session.lastUsedAt,
|
|
6879
|
+
tracing: session.tracing
|
|
6880
|
+
};
|
|
6881
|
+
}
|
|
6882
|
+
attachEvidenceCollectors(session) {
|
|
6883
|
+
session.page.on("console", (message) => {
|
|
6884
|
+
pushBounded(
|
|
6885
|
+
session.console,
|
|
6886
|
+
{
|
|
6887
|
+
level: message.type(),
|
|
6888
|
+
text: redactBrowserText(message.text()).slice(0, 4096),
|
|
6889
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6890
|
+
},
|
|
6891
|
+
this.maxConsoleEntries
|
|
6892
|
+
);
|
|
6893
|
+
});
|
|
6894
|
+
session.page.on("response", (response) => {
|
|
6895
|
+
this.pushNetwork(session, {
|
|
6896
|
+
method: response.request().method(),
|
|
6897
|
+
url: safeBrowserUrl(response.url()),
|
|
6898
|
+
status: response.status(),
|
|
6899
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6900
|
+
});
|
|
6901
|
+
});
|
|
6902
|
+
session.page.on("requestfailed", (request2) => {
|
|
6903
|
+
this.pushNetwork(session, {
|
|
6904
|
+
method: request2.method(),
|
|
6905
|
+
url: safeBrowserUrl(request2.url()),
|
|
6906
|
+
failed: true,
|
|
6907
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6908
|
+
});
|
|
6909
|
+
});
|
|
6910
|
+
}
|
|
6911
|
+
pushNetwork(session, entry) {
|
|
6912
|
+
pushBounded(session.network, entry, this.maxNetworkEntries);
|
|
6913
|
+
}
|
|
6914
|
+
async runPageOperation(session, signal, operation) {
|
|
6915
|
+
signal.throwIfAborted();
|
|
6916
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6917
|
+
return abortable(signal, operation, async () => {
|
|
6918
|
+
await session.context.close().catch(() => void 0);
|
|
6919
|
+
this.sessions.delete(session.id);
|
|
6920
|
+
await this.closeBrowserIfIdle();
|
|
6921
|
+
});
|
|
6922
|
+
}
|
|
6923
|
+
async closeBrowserIfIdle() {
|
|
6924
|
+
if (this.sessions.size > 0 || !this.browser) return;
|
|
6925
|
+
const browser = this.browser;
|
|
6926
|
+
this.browser = void 0;
|
|
6927
|
+
await browser.close().catch(() => void 0);
|
|
6928
|
+
}
|
|
6929
|
+
};
|
|
6930
|
+
async function defaultLauncher(headless) {
|
|
6931
|
+
try {
|
|
6932
|
+
const { chromium } = await import("@playwright/test");
|
|
6933
|
+
return await chromium.launch({ headless });
|
|
6934
|
+
} catch (err) {
|
|
6935
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
6936
|
+
throw new Error(
|
|
6937
|
+
`browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
|
|
6938
|
+
);
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
async function browserInstallationDiagnostics() {
|
|
6942
|
+
try {
|
|
6943
|
+
const { chromium } = await import("@playwright/test");
|
|
6944
|
+
const executablePath = chromium.executablePath();
|
|
6945
|
+
await fs8.access(executablePath);
|
|
6946
|
+
return { available: true, executablePath, message: "Playwright Chromium is available." };
|
|
6947
|
+
} catch {
|
|
6948
|
+
return {
|
|
6949
|
+
available: false,
|
|
6950
|
+
message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
function pushBounded(target, value, limit) {
|
|
6955
|
+
target.push(value);
|
|
6956
|
+
if (target.length > limit) target.splice(0, target.length - limit);
|
|
6957
|
+
}
|
|
6958
|
+
async function abortable(signal, operation, onAbort) {
|
|
6959
|
+
signal.throwIfAborted();
|
|
6960
|
+
return new Promise((resolve14, reject) => {
|
|
6961
|
+
let settled = false;
|
|
6962
|
+
let aborting = false;
|
|
6963
|
+
const finish = (fn) => {
|
|
6964
|
+
if (settled) return;
|
|
6965
|
+
settled = true;
|
|
6966
|
+
signal.removeEventListener("abort", abort);
|
|
6967
|
+
fn();
|
|
6968
|
+
};
|
|
6969
|
+
const abort = () => {
|
|
6970
|
+
aborting = true;
|
|
6971
|
+
void Promise.resolve(onAbort()).finally(() => {
|
|
6972
|
+
finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
|
|
6973
|
+
});
|
|
6974
|
+
};
|
|
6975
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6976
|
+
operation().then(
|
|
6977
|
+
(value) => {
|
|
6978
|
+
if (!aborting) finish(() => resolve14(value));
|
|
6979
|
+
},
|
|
6980
|
+
(err) => {
|
|
6981
|
+
if (!aborting) finish(() => reject(err));
|
|
6982
|
+
}
|
|
6983
|
+
);
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6987
|
+
// src/browser/tools.ts
|
|
6988
|
+
var managers = /* @__PURE__ */ new Map();
|
|
6989
|
+
var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
|
|
6990
|
+
function managerFor(ctx) {
|
|
6991
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
6992
|
+
let manager = managers.get(root);
|
|
6993
|
+
if (!manager) {
|
|
6994
|
+
const wpaths = resolveWstackPaths({ projectRoot: root });
|
|
6995
|
+
manager = new BrowserSessionManager({
|
|
6996
|
+
artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
|
|
6997
|
+
allowedPrivateOrigins: parsePrivateOriginAllowlist(
|
|
6998
|
+
process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
|
|
6999
|
+
)
|
|
7000
|
+
});
|
|
7001
|
+
managers.set(root, manager);
|
|
7002
|
+
}
|
|
7003
|
+
const runSignal = ctx.signal;
|
|
7004
|
+
if (cleanupSignalByContext.get(ctx) !== runSignal) {
|
|
7005
|
+
cleanupSignalByContext.set(ctx, runSignal);
|
|
7006
|
+
const managerForRun = manager;
|
|
7007
|
+
const ownerForRun = owner(ctx);
|
|
7008
|
+
ctx.registerAbortHook?.(async () => {
|
|
7009
|
+
if (cleanupSignalByContext.get(ctx) === runSignal) {
|
|
7010
|
+
cleanupSignalByContext.delete(ctx);
|
|
7011
|
+
}
|
|
7012
|
+
await managerForRun.closeOwner(ownerForRun);
|
|
7013
|
+
});
|
|
7014
|
+
}
|
|
7015
|
+
return manager;
|
|
7016
|
+
}
|
|
7017
|
+
function owner(ctx) {
|
|
7018
|
+
return ctx.agentId || "leader";
|
|
7019
|
+
}
|
|
7020
|
+
function json(value) {
|
|
7021
|
+
return JSON.stringify(value, null, 2);
|
|
7022
|
+
}
|
|
7023
|
+
var sessionIdSchema = {
|
|
7024
|
+
type: "string",
|
|
7025
|
+
minLength: 1,
|
|
7026
|
+
description: "Browser session id returned by browser_open."
|
|
7027
|
+
};
|
|
7028
|
+
var browserOpenTool = {
|
|
7029
|
+
name: "browser_open",
|
|
7030
|
+
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
|
|
7031
|
+
usageHint: "browser_open({ url?, width?, height?, trace? })",
|
|
7032
|
+
permission: "confirm",
|
|
7033
|
+
mutating: true,
|
|
7034
|
+
riskTier: "standard",
|
|
7035
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7036
|
+
subjectKey: "url",
|
|
7037
|
+
inputSchema: {
|
|
7038
|
+
type: "object",
|
|
7039
|
+
properties: {
|
|
7040
|
+
url: { type: "string", description: "Optional absolute http(s) URL." },
|
|
7041
|
+
width: { type: "integer", minimum: 320, maximum: 3840 },
|
|
7042
|
+
height: { type: "integer", minimum: 240, maximum: 2160 },
|
|
7043
|
+
trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
|
|
7044
|
+
}
|
|
7045
|
+
},
|
|
7046
|
+
async execute(input, ctx, opts) {
|
|
7047
|
+
const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
|
|
7048
|
+
return json(
|
|
7049
|
+
await managerFor(ctx).open(
|
|
7050
|
+
owner(ctx),
|
|
7051
|
+
{ url: input.url, viewport, trace: input.trace },
|
|
7052
|
+
opts.signal
|
|
7053
|
+
)
|
|
7054
|
+
);
|
|
7055
|
+
}
|
|
7056
|
+
};
|
|
7057
|
+
var browserListTool = {
|
|
7058
|
+
name: "browser_list",
|
|
7059
|
+
description: "List browser sessions owned by the current agent without exposing other agents sessions.",
|
|
7060
|
+
permission: "auto",
|
|
7061
|
+
mutating: false,
|
|
7062
|
+
riskTier: "safe",
|
|
7063
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7064
|
+
inputSchema: { type: "object", properties: {} },
|
|
7065
|
+
async execute(_input, ctx) {
|
|
7066
|
+
return json(await managerFor(ctx).list(owner(ctx)));
|
|
7067
|
+
}
|
|
7068
|
+
};
|
|
7069
|
+
var browserStatusTool = {
|
|
7070
|
+
name: "browser_status",
|
|
7071
|
+
description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
|
|
7072
|
+
permission: "auto",
|
|
7073
|
+
mutating: false,
|
|
7074
|
+
riskTier: "safe",
|
|
7075
|
+
capabilities: [ToolCapabilities.TOOL_META],
|
|
7076
|
+
inputSchema: { type: "object", properties: {} },
|
|
7077
|
+
async execute() {
|
|
7078
|
+
return json(await browserInstallationDiagnostics());
|
|
7079
|
+
}
|
|
7080
|
+
};
|
|
7081
|
+
var browserNavigateTool = {
|
|
7082
|
+
name: "browser_navigate",
|
|
7083
|
+
description: "Navigate an owned browser session to an approved http(s) URL.",
|
|
7084
|
+
usageHint: "browser_navigate({ sessionId, url })",
|
|
7085
|
+
permission: "confirm",
|
|
7086
|
+
mutating: true,
|
|
7087
|
+
riskTier: "standard",
|
|
7088
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7089
|
+
subjectKey: "url",
|
|
7090
|
+
inputSchema: {
|
|
7091
|
+
type: "object",
|
|
7092
|
+
properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
|
|
7093
|
+
required: ["sessionId", "url"]
|
|
7094
|
+
},
|
|
7095
|
+
async execute(input, ctx, opts) {
|
|
7096
|
+
return json(
|
|
7097
|
+
await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
|
|
7098
|
+
);
|
|
7099
|
+
}
|
|
7100
|
+
};
|
|
7101
|
+
var browserSnapshotTool = {
|
|
7102
|
+
name: "browser_snapshot",
|
|
7103
|
+
description: "Return bounded accessibility state plus redacted console and network summaries.",
|
|
7104
|
+
usageHint: "browser_snapshot({ sessionId })",
|
|
7105
|
+
permission: "auto",
|
|
7106
|
+
mutating: false,
|
|
7107
|
+
riskTier: "safe",
|
|
7108
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7109
|
+
inputSchema: {
|
|
7110
|
+
type: "object",
|
|
7111
|
+
properties: { sessionId: sessionIdSchema },
|
|
7112
|
+
required: ["sessionId"]
|
|
7113
|
+
},
|
|
7114
|
+
async execute(input, ctx, opts) {
|
|
7115
|
+
return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
|
|
7116
|
+
}
|
|
7117
|
+
};
|
|
7118
|
+
var browserScreenshotTool = {
|
|
7119
|
+
name: "browser_screenshot",
|
|
7120
|
+
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
7121
|
+
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
7122
|
+
permission: "confirm",
|
|
7123
|
+
mutating: true,
|
|
7124
|
+
riskTier: "safe",
|
|
7125
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7126
|
+
inputSchema: {
|
|
7127
|
+
type: "object",
|
|
7128
|
+
properties: {
|
|
7129
|
+
sessionId: sessionIdSchema,
|
|
7130
|
+
fullPage: { type: "boolean" },
|
|
7131
|
+
selector: { type: "string", minLength: 1 }
|
|
7132
|
+
},
|
|
7133
|
+
required: ["sessionId"]
|
|
7134
|
+
},
|
|
7135
|
+
async execute(input, ctx, opts) {
|
|
7136
|
+
return json(
|
|
7137
|
+
await managerFor(ctx).screenshot(
|
|
7138
|
+
input.sessionId,
|
|
7139
|
+
owner(ctx),
|
|
7140
|
+
{ fullPage: input.fullPage, selector: input.selector },
|
|
7141
|
+
opts.signal
|
|
7142
|
+
)
|
|
7143
|
+
);
|
|
7144
|
+
}
|
|
7145
|
+
};
|
|
7146
|
+
function selectorTool(name, description, run) {
|
|
7147
|
+
return {
|
|
7148
|
+
name,
|
|
7149
|
+
description,
|
|
7150
|
+
usageHint: `${name}({ sessionId, selector })`,
|
|
7151
|
+
permission: "confirm",
|
|
7152
|
+
mutating: true,
|
|
7153
|
+
riskTier: "standard",
|
|
7154
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7155
|
+
inputSchema: {
|
|
7156
|
+
type: "object",
|
|
7157
|
+
properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
|
|
7158
|
+
required: ["sessionId", "selector"]
|
|
7159
|
+
},
|
|
7160
|
+
async execute(input, ctx, opts) {
|
|
7161
|
+
await run(managerFor(ctx), input, owner(ctx), opts.signal);
|
|
7162
|
+
return "ok";
|
|
7163
|
+
}
|
|
7164
|
+
};
|
|
7165
|
+
}
|
|
7166
|
+
var browserClickTool = selectorTool(
|
|
7167
|
+
"browser_click",
|
|
7168
|
+
"Click an element in an owned browser session.",
|
|
7169
|
+
(manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
|
|
7170
|
+
);
|
|
7171
|
+
var browserHoverTool = selectorTool(
|
|
7172
|
+
"browser_hover",
|
|
7173
|
+
"Hover an element in an owned browser session.",
|
|
7174
|
+
(manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
|
|
7175
|
+
);
|
|
7176
|
+
var browserTypeTool = {
|
|
7177
|
+
name: "browser_type",
|
|
7178
|
+
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.",
|
|
7179
|
+
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
7180
|
+
permission: "confirm",
|
|
7181
|
+
mutating: true,
|
|
7182
|
+
riskTier: "standard",
|
|
7183
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7184
|
+
inputSchema: {
|
|
7185
|
+
type: "object",
|
|
7186
|
+
properties: {
|
|
7187
|
+
sessionId: sessionIdSchema,
|
|
7188
|
+
selector: { type: "string", minLength: 1 },
|
|
7189
|
+
text: { type: "string" },
|
|
7190
|
+
secretEnv: {
|
|
7191
|
+
type: "string",
|
|
7192
|
+
pattern: "^[A-Z_][A-Z0-9_]*$",
|
|
7193
|
+
description: "Host environment variable resolved only at execution time."
|
|
7194
|
+
}
|
|
7195
|
+
},
|
|
7196
|
+
required: ["sessionId", "selector"]
|
|
7197
|
+
},
|
|
7198
|
+
validate(input) {
|
|
7199
|
+
return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
|
|
7200
|
+
},
|
|
7201
|
+
async execute(input, ctx, opts) {
|
|
7202
|
+
const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
|
|
7203
|
+
if (value === void 0) {
|
|
7204
|
+
throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
|
|
7205
|
+
}
|
|
7206
|
+
await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
|
|
7207
|
+
return "ok";
|
|
7208
|
+
}
|
|
7209
|
+
};
|
|
7210
|
+
var browserSelectTool = {
|
|
7211
|
+
name: "browser_select",
|
|
7212
|
+
description: "Select an option in an owned browser session.",
|
|
7213
|
+
usageHint: "browser_select({ sessionId, selector, value })",
|
|
7214
|
+
permission: "confirm",
|
|
7215
|
+
mutating: true,
|
|
7216
|
+
riskTier: "standard",
|
|
7217
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7218
|
+
inputSchema: {
|
|
7219
|
+
type: "object",
|
|
7220
|
+
properties: {
|
|
7221
|
+
sessionId: sessionIdSchema,
|
|
7222
|
+
selector: { type: "string", minLength: 1 },
|
|
7223
|
+
value: { type: "string" }
|
|
7224
|
+
},
|
|
7225
|
+
required: ["sessionId", "selector", "value"]
|
|
7226
|
+
},
|
|
7227
|
+
async execute(input, ctx, opts) {
|
|
7228
|
+
await managerFor(ctx).select(
|
|
7229
|
+
input.sessionId,
|
|
7230
|
+
owner(ctx),
|
|
7231
|
+
input.selector,
|
|
7232
|
+
input.value,
|
|
7233
|
+
opts.signal
|
|
7234
|
+
);
|
|
7235
|
+
return "ok";
|
|
7236
|
+
}
|
|
7237
|
+
};
|
|
7238
|
+
var browserPressTool = {
|
|
7239
|
+
name: "browser_press",
|
|
7240
|
+
description: "Press a keyboard key in an owned browser session.",
|
|
7241
|
+
usageHint: "browser_press({ sessionId, key })",
|
|
7242
|
+
permission: "confirm",
|
|
7243
|
+
mutating: true,
|
|
7244
|
+
riskTier: "standard",
|
|
7245
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7246
|
+
inputSchema: {
|
|
7247
|
+
type: "object",
|
|
7248
|
+
properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
|
|
7249
|
+
required: ["sessionId", "key"]
|
|
7250
|
+
},
|
|
7251
|
+
async execute(input, ctx, opts) {
|
|
7252
|
+
await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
|
|
7253
|
+
return "ok";
|
|
7254
|
+
}
|
|
7255
|
+
};
|
|
7256
|
+
var browserDragTool = {
|
|
7257
|
+
name: "browser_drag",
|
|
7258
|
+
description: "Drag one element to another in an owned browser session.",
|
|
7259
|
+
usageHint: "browser_drag({ sessionId, from, to })",
|
|
7260
|
+
permission: "confirm",
|
|
7261
|
+
mutating: true,
|
|
7262
|
+
riskTier: "standard",
|
|
7263
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7264
|
+
inputSchema: {
|
|
7265
|
+
type: "object",
|
|
7266
|
+
properties: {
|
|
7267
|
+
sessionId: sessionIdSchema,
|
|
7268
|
+
from: { type: "string", minLength: 1 },
|
|
7269
|
+
to: { type: "string", minLength: 1 }
|
|
7270
|
+
},
|
|
7271
|
+
required: ["sessionId", "from", "to"]
|
|
7272
|
+
},
|
|
7273
|
+
async execute(input, ctx, opts) {
|
|
7274
|
+
await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
|
|
7275
|
+
return "ok";
|
|
7276
|
+
}
|
|
7277
|
+
};
|
|
7278
|
+
var browserWaitTool = {
|
|
7279
|
+
name: "browser_wait",
|
|
7280
|
+
description: "Wait for an element or a bounded duration in an owned browser session.",
|
|
7281
|
+
usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
|
|
7282
|
+
permission: "auto",
|
|
7283
|
+
mutating: false,
|
|
7284
|
+
riskTier: "safe",
|
|
7285
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7286
|
+
inputSchema: {
|
|
7287
|
+
type: "object",
|
|
7288
|
+
properties: {
|
|
7289
|
+
sessionId: sessionIdSchema,
|
|
7290
|
+
selector: { type: "string", minLength: 1 },
|
|
7291
|
+
timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
|
|
7292
|
+
},
|
|
7293
|
+
required: ["sessionId"]
|
|
7294
|
+
},
|
|
7295
|
+
async execute(input, ctx, opts) {
|
|
7296
|
+
await managerFor(ctx).wait(
|
|
7297
|
+
input.sessionId,
|
|
7298
|
+
owner(ctx),
|
|
7299
|
+
{ selector: input.selector, timeoutMs: input.timeoutMs },
|
|
7300
|
+
opts.signal
|
|
7301
|
+
);
|
|
7302
|
+
return "ok";
|
|
7303
|
+
}
|
|
7304
|
+
};
|
|
7305
|
+
var browserEvaluateTool = {
|
|
7306
|
+
name: "browser_evaluate",
|
|
7307
|
+
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
7308
|
+
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
7309
|
+
permission: "confirm",
|
|
7310
|
+
mutating: true,
|
|
7311
|
+
riskTier: "destructive",
|
|
7312
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
7313
|
+
inputSchema: {
|
|
7314
|
+
type: "object",
|
|
7315
|
+
properties: {
|
|
7316
|
+
sessionId: sessionIdSchema,
|
|
7317
|
+
expression: { type: "string", minLength: 1, maxLength: 16384 }
|
|
7318
|
+
},
|
|
7319
|
+
required: ["sessionId", "expression"]
|
|
7320
|
+
},
|
|
7321
|
+
async execute(input, ctx, opts) {
|
|
7322
|
+
return json(
|
|
7323
|
+
await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
|
|
7324
|
+
);
|
|
7325
|
+
}
|
|
7326
|
+
};
|
|
7327
|
+
var browserUploadTool = {
|
|
7328
|
+
name: "browser_upload",
|
|
7329
|
+
description: "Upload project-local files through a file input in an owned browser session.",
|
|
7330
|
+
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
7331
|
+
permission: "confirm",
|
|
7332
|
+
mutating: true,
|
|
7333
|
+
riskTier: "destructive",
|
|
7334
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
7335
|
+
inputSchema: {
|
|
7336
|
+
type: "object",
|
|
7337
|
+
properties: {
|
|
7338
|
+
sessionId: sessionIdSchema,
|
|
7339
|
+
selector: { type: "string", minLength: 1 },
|
|
7340
|
+
files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
|
|
7341
|
+
},
|
|
7342
|
+
required: ["sessionId", "selector", "files"]
|
|
7343
|
+
},
|
|
7344
|
+
async execute(input, ctx, opts) {
|
|
7345
|
+
await managerFor(ctx).upload(
|
|
7346
|
+
input.sessionId,
|
|
7347
|
+
owner(ctx),
|
|
7348
|
+
input.selector,
|
|
7349
|
+
input.files,
|
|
7350
|
+
ctx.projectRoot,
|
|
7351
|
+
opts.signal
|
|
7352
|
+
);
|
|
7353
|
+
return "ok";
|
|
7354
|
+
}
|
|
7355
|
+
};
|
|
7356
|
+
var browserCloseTool = {
|
|
7357
|
+
name: "browser_close",
|
|
7358
|
+
description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
|
|
7359
|
+
usageHint: "browser_close({ sessionId })",
|
|
7360
|
+
permission: "auto",
|
|
7361
|
+
mutating: false,
|
|
7362
|
+
riskTier: "safe",
|
|
7363
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7364
|
+
inputSchema: {
|
|
7365
|
+
type: "object",
|
|
7366
|
+
properties: { sessionId: sessionIdSchema },
|
|
7367
|
+
required: ["sessionId"]
|
|
7368
|
+
},
|
|
7369
|
+
async execute(input, ctx) {
|
|
7370
|
+
return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
|
|
7371
|
+
}
|
|
7372
|
+
};
|
|
7373
|
+
var browserTools = [
|
|
7374
|
+
browserOpenTool,
|
|
7375
|
+
browserStatusTool,
|
|
7376
|
+
browserListTool,
|
|
7377
|
+
browserNavigateTool,
|
|
7378
|
+
browserSnapshotTool,
|
|
7379
|
+
browserScreenshotTool,
|
|
7380
|
+
browserClickTool,
|
|
7381
|
+
browserTypeTool,
|
|
7382
|
+
browserSelectTool,
|
|
7383
|
+
browserPressTool,
|
|
7384
|
+
browserHoverTool,
|
|
7385
|
+
browserDragTool,
|
|
7386
|
+
browserWaitTool,
|
|
7387
|
+
browserEvaluateTool,
|
|
7388
|
+
browserUploadTool,
|
|
7389
|
+
browserCloseTool
|
|
7390
|
+
];
|
|
7391
|
+
for (const tool of browserTools) tool.icon = "web";
|
|
7392
|
+
for (const tool of browserTools) tool.timeoutMs ??= 6e4;
|
|
7393
|
+
|
|
6162
7394
|
// src/codebase-index/writer.ts
|
|
6163
7395
|
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
6164
7396
|
|
|
@@ -6245,9 +7477,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
|
6245
7477
|
// src/codebase-index/writer.ts
|
|
6246
7478
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
6247
7479
|
import { createRequire } from "node:module";
|
|
6248
|
-
import * as
|
|
6249
|
-
import * as
|
|
6250
|
-
import { resolveWstackPaths } from "@wrongstack/core";
|
|
7480
|
+
import * as fs9 from "node:fs";
|
|
7481
|
+
import * as path13 from "node:path";
|
|
7482
|
+
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
|
|
6251
7483
|
|
|
6252
7484
|
// src/codebase-index/schema.ts
|
|
6253
7485
|
var SCHEMA_VERSION = 2;
|
|
@@ -6376,7 +7608,7 @@ var Bm25Index = class {
|
|
|
6376
7608
|
// src/codebase-index/writer.ts
|
|
6377
7609
|
var DB_FILE = "index.db";
|
|
6378
7610
|
function resolveIndexDir(projectRoot, override) {
|
|
6379
|
-
return override ??
|
|
7611
|
+
return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
|
|
6380
7612
|
}
|
|
6381
7613
|
function codebaseIndexDirOverride(ctx) {
|
|
6382
7614
|
const v = ctx.meta?.["codebaseIndexDir"];
|
|
@@ -6491,9 +7723,9 @@ var IndexStore = class {
|
|
|
6491
7723
|
}
|
|
6492
7724
|
constructor(projectRoot, opts = {}) {
|
|
6493
7725
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
6494
|
-
|
|
7726
|
+
fs9.mkdirSync(this.indexDir, { recursive: true });
|
|
6495
7727
|
const Database = loadDatabaseSync();
|
|
6496
|
-
this.db = new Database(
|
|
7728
|
+
this.db = new Database(path13.join(this.indexDir, DB_FILE));
|
|
6497
7729
|
try {
|
|
6498
7730
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
6499
7731
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -7072,9 +8304,9 @@ var IndexStore = class {
|
|
|
7072
8304
|
}));
|
|
7073
8305
|
}
|
|
7074
8306
|
sizeBytes() {
|
|
7075
|
-
const dbPath =
|
|
8307
|
+
const dbPath = path13.join(this.indexDir, DB_FILE);
|
|
7076
8308
|
try {
|
|
7077
|
-
return
|
|
8309
|
+
return fs9.statSync(dbPath).size;
|
|
7078
8310
|
} catch {
|
|
7079
8311
|
return 0;
|
|
7080
8312
|
}
|
|
@@ -7089,14 +8321,14 @@ var IndexStore = class {
|
|
|
7089
8321
|
};
|
|
7090
8322
|
|
|
7091
8323
|
// src/codebase-index/background-indexer.ts
|
|
7092
|
-
import * as
|
|
8324
|
+
import * as fs15 from "node:fs";
|
|
7093
8325
|
import { fileURLToPath } from "node:url";
|
|
7094
8326
|
import { Worker } from "node:worker_threads";
|
|
7095
8327
|
|
|
7096
8328
|
// src/codebase-index/indexer.ts
|
|
7097
8329
|
import { expectDefined as expectDefined6 } from "@wrongstack/core";
|
|
7098
|
-
import * as
|
|
7099
|
-
import * as
|
|
8330
|
+
import * as fs14 from "node:fs/promises";
|
|
8331
|
+
import * as path19 from "node:path";
|
|
7100
8332
|
import { compileGlob as compileGlob2 } from "@wrongstack/core";
|
|
7101
8333
|
|
|
7102
8334
|
// src/codebase-index/ts-parser.ts
|
|
@@ -7289,8 +8521,8 @@ function detectLang(file) {
|
|
|
7289
8521
|
// src/codebase-index/go-parser.ts
|
|
7290
8522
|
import { spawn as spawn4 } from "node:child_process";
|
|
7291
8523
|
import * as os5 from "node:os";
|
|
7292
|
-
import * as
|
|
7293
|
-
import * as
|
|
8524
|
+
import * as path14 from "node:path";
|
|
8525
|
+
import * as fs10 from "node:fs/promises";
|
|
7294
8526
|
async function parseSymbols2(opts) {
|
|
7295
8527
|
const { file, content, lang } = opts;
|
|
7296
8528
|
try {
|
|
@@ -7591,10 +8823,10 @@ func formatType(t ast.Expr) string {
|
|
|
7591
8823
|
}
|
|
7592
8824
|
`;
|
|
7593
8825
|
async function syncGoParse(filePath, content, lang) {
|
|
7594
|
-
const tmpDir = await
|
|
8826
|
+
const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
|
|
7595
8827
|
try {
|
|
7596
|
-
const scriptPath =
|
|
7597
|
-
await
|
|
8828
|
+
const scriptPath = path14.join(tmpDir, "parse.go");
|
|
8829
|
+
await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
7598
8830
|
const proc = spawn4("go", ["run", scriptPath], {
|
|
7599
8831
|
stdio: ["pipe", "pipe", "pipe"],
|
|
7600
8832
|
windowsHide: true
|
|
@@ -7606,8 +8838,8 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
7606
8838
|
proc.stdin?.write(content);
|
|
7607
8839
|
proc.stdin?.end();
|
|
7608
8840
|
const { code } = await Promise.race([
|
|
7609
|
-
new Promise((
|
|
7610
|
-
proc.on("close", (c) =>
|
|
8841
|
+
new Promise((resolve14) => {
|
|
8842
|
+
proc.on("close", (c) => resolve14({ code: c }));
|
|
7611
8843
|
}),
|
|
7612
8844
|
new Promise(
|
|
7613
8845
|
(_, reject) => setTimeout(() => {
|
|
@@ -7637,15 +8869,15 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
7637
8869
|
} catch {
|
|
7638
8870
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7639
8871
|
} finally {
|
|
7640
|
-
await
|
|
8872
|
+
await fs10.rm(tmpDir, { recursive: true, force: true });
|
|
7641
8873
|
}
|
|
7642
8874
|
}
|
|
7643
8875
|
|
|
7644
8876
|
// src/codebase-index/py-parser.ts
|
|
7645
8877
|
import { spawn as spawn5 } from "node:child_process";
|
|
7646
|
-
import * as
|
|
8878
|
+
import * as fs11 from "node:fs/promises";
|
|
7647
8879
|
import * as os6 from "node:os";
|
|
7648
|
-
import * as
|
|
8880
|
+
import * as path15 from "node:path";
|
|
7649
8881
|
async function parseSymbols3(opts) {
|
|
7650
8882
|
const { file, content, lang } = opts;
|
|
7651
8883
|
try {
|
|
@@ -7861,10 +9093,10 @@ var _cachedScriptPath = null;
|
|
|
7861
9093
|
async function syncPyParse(filePath, content, lang) {
|
|
7862
9094
|
try {
|
|
7863
9095
|
if (!_cachedScriptPath) {
|
|
7864
|
-
const tmpDir =
|
|
7865
|
-
await
|
|
7866
|
-
_cachedScriptPath =
|
|
7867
|
-
await
|
|
9096
|
+
const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
|
|
9097
|
+
await fs11.mkdir(tmpDir, { recursive: true });
|
|
9098
|
+
_cachedScriptPath = path15.join(tmpDir, "parse.py");
|
|
9099
|
+
await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
7868
9100
|
}
|
|
7869
9101
|
const proc = spawn5("python", [_cachedScriptPath, filePath], {
|
|
7870
9102
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7877,8 +9109,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
7877
9109
|
stdout += chunk.toString();
|
|
7878
9110
|
});
|
|
7879
9111
|
const { code } = await Promise.race([
|
|
7880
|
-
new Promise((
|
|
7881
|
-
proc.on("close", (c) =>
|
|
9112
|
+
new Promise((resolve14) => {
|
|
9113
|
+
proc.on("close", (c) => resolve14({ code: c }));
|
|
7882
9114
|
}),
|
|
7883
9115
|
new Promise(
|
|
7884
9116
|
(_, reject) => setTimeout(() => {
|
|
@@ -7913,8 +9145,8 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
7913
9145
|
// src/codebase-index/rs-parser.ts
|
|
7914
9146
|
import { expectDefined as expectDefined3 } from "@wrongstack/core";
|
|
7915
9147
|
import { execFileSync, spawn as spawn6 } from "node:child_process";
|
|
7916
|
-
import * as
|
|
7917
|
-
import * as
|
|
9148
|
+
import * as fs12 from "node:fs/promises";
|
|
9149
|
+
import * as path16 from "node:path";
|
|
7918
9150
|
async function parseSymbols4(opts) {
|
|
7919
9151
|
const { file, content, lang } = opts;
|
|
7920
9152
|
const nativeAvailable = checkNativeParser();
|
|
@@ -7927,7 +9159,7 @@ async function parseSymbols4(opts) {
|
|
|
7927
9159
|
function checkNativeParser() {
|
|
7928
9160
|
try {
|
|
7929
9161
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
7930
|
-
const toolsDir =
|
|
9162
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
7931
9163
|
try {
|
|
7932
9164
|
execFileSync(
|
|
7933
9165
|
"cargo",
|
|
@@ -7937,7 +9169,7 @@ function checkNativeParser() {
|
|
|
7937
9169
|
"--format-version",
|
|
7938
9170
|
"1",
|
|
7939
9171
|
"--manifest-path",
|
|
7940
|
-
|
|
9172
|
+
path16.join(toolsDir, "Cargo.toml")
|
|
7941
9173
|
],
|
|
7942
9174
|
{ stdio: "pipe", windowsHide: true }
|
|
7943
9175
|
);
|
|
@@ -7951,13 +9183,13 @@ function checkNativeParser() {
|
|
|
7951
9183
|
}
|
|
7952
9184
|
async function tryNativeParse(file, content) {
|
|
7953
9185
|
try {
|
|
7954
|
-
const toolsDir =
|
|
7955
|
-
const crateDir =
|
|
7956
|
-
const tmpFile =
|
|
7957
|
-
await
|
|
9186
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
9187
|
+
const crateDir = path16.join(toolsDir, "syn-parser");
|
|
9188
|
+
const tmpFile = path16.join(crateDir, "src", "input.rs");
|
|
9189
|
+
await fs12.writeFile(tmpFile, content, "utf8");
|
|
7958
9190
|
const proc = spawn6(
|
|
7959
9191
|
"cargo",
|
|
7960
|
-
["run", "--manifest-path",
|
|
9192
|
+
["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
|
|
7961
9193
|
{
|
|
7962
9194
|
cwd: process.cwd(),
|
|
7963
9195
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -7969,8 +9201,8 @@ async function tryNativeParse(file, content) {
|
|
|
7969
9201
|
stdout += chunk.toString();
|
|
7970
9202
|
});
|
|
7971
9203
|
const { code } = await Promise.race([
|
|
7972
|
-
new Promise((
|
|
7973
|
-
proc.on("close", (c) =>
|
|
9204
|
+
new Promise((resolve14) => {
|
|
9205
|
+
proc.on("close", (c) => resolve14({ code: c }));
|
|
7974
9206
|
}),
|
|
7975
9207
|
new Promise(
|
|
7976
9208
|
(_, reject) => setTimeout(() => {
|
|
@@ -8061,7 +9293,7 @@ function regexParse(opts) {
|
|
|
8061
9293
|
|
|
8062
9294
|
// src/codebase-index/json-parser.ts
|
|
8063
9295
|
import { expectDefined as expectDefined4 } from "@wrongstack/core";
|
|
8064
|
-
import * as
|
|
9296
|
+
import * as path17 from "node:path";
|
|
8065
9297
|
function parseSymbols5(opts) {
|
|
8066
9298
|
const { file, content, lang } = opts;
|
|
8067
9299
|
try {
|
|
@@ -8073,9 +9305,9 @@ function parseSymbols5(opts) {
|
|
|
8073
9305
|
function regexParse2(opts) {
|
|
8074
9306
|
const { file, content, lang } = opts;
|
|
8075
9307
|
const symbols = [];
|
|
8076
|
-
const
|
|
8077
|
-
const isPackageJson =
|
|
8078
|
-
const isTsconfig =
|
|
9308
|
+
const basename8 = path17.basename(file).toLowerCase();
|
|
9309
|
+
const isPackageJson = basename8 === "package.json";
|
|
9310
|
+
const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
|
|
8079
9311
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
8080
9312
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
8081
9313
|
const lines = content.split("\n");
|
|
@@ -8099,11 +9331,11 @@ function regexParse2(opts) {
|
|
|
8099
9331
|
const line = lineFromOffset(offset);
|
|
8100
9332
|
symbols.push(
|
|
8101
9333
|
makeSymbol({
|
|
8102
|
-
name:
|
|
9334
|
+
name: path17.basename(file),
|
|
8103
9335
|
kind: "object",
|
|
8104
9336
|
line,
|
|
8105
9337
|
col: 0,
|
|
8106
|
-
signature: `"${
|
|
9338
|
+
signature: `"${path17.basename(file)}" = { ... }`,
|
|
8107
9339
|
file,
|
|
8108
9340
|
lang
|
|
8109
9341
|
})
|
|
@@ -8414,8 +9646,8 @@ function makeSymbol2(opts) {
|
|
|
8414
9646
|
}
|
|
8415
9647
|
|
|
8416
9648
|
// src/codebase-index/gitignore.ts
|
|
8417
|
-
import * as
|
|
8418
|
-
import * as
|
|
9649
|
+
import * as fs13 from "node:fs/promises";
|
|
9650
|
+
import * as path18 from "node:path";
|
|
8419
9651
|
import { compileGlob } from "@wrongstack/core";
|
|
8420
9652
|
function globBody(glob) {
|
|
8421
9653
|
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -8461,7 +9693,7 @@ function compileGitignore(lines) {
|
|
|
8461
9693
|
async function loadGitignoreMatcher(projectRoot) {
|
|
8462
9694
|
let lines = [];
|
|
8463
9695
|
try {
|
|
8464
|
-
const raw = await
|
|
9696
|
+
const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
|
|
8465
9697
|
lines = raw.split("\n");
|
|
8466
9698
|
} catch {
|
|
8467
9699
|
}
|
|
@@ -8472,7 +9704,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
8472
9704
|
var YIELD_EVERY_N = 50;
|
|
8473
9705
|
var PARALLEL_BATCH = 20;
|
|
8474
9706
|
function yieldEventLoop() {
|
|
8475
|
-
return new Promise((
|
|
9707
|
+
return new Promise((resolve14) => setImmediate(resolve14));
|
|
8476
9708
|
}
|
|
8477
9709
|
function throwIfAborted(signal) {
|
|
8478
9710
|
if (!signal?.aborted) return;
|
|
@@ -8519,21 +9751,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
8519
9751
|
}
|
|
8520
9752
|
let entries;
|
|
8521
9753
|
try {
|
|
8522
|
-
entries = await
|
|
9754
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
8523
9755
|
} catch {
|
|
8524
9756
|
return;
|
|
8525
9757
|
}
|
|
8526
9758
|
dirCount++;
|
|
8527
9759
|
for (const e of entries) {
|
|
8528
9760
|
if (ignoreSet.has(e.name)) continue;
|
|
8529
|
-
const full =
|
|
8530
|
-
const rel =
|
|
9761
|
+
const full = path19.join(dir, e.name);
|
|
9762
|
+
const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
|
|
8531
9763
|
if (e.isDirectory()) {
|
|
8532
9764
|
if (isGitIgnored(rel, true)) continue;
|
|
8533
9765
|
await walk(full);
|
|
8534
9766
|
} else if (e.isFile()) {
|
|
8535
9767
|
if (isGitIgnored(rel, false)) continue;
|
|
8536
|
-
const ext =
|
|
9768
|
+
const ext = path19.extname(e.name);
|
|
8537
9769
|
for (const { ext: extName, pat } of globs) {
|
|
8538
9770
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
8539
9771
|
results.push(full);
|
|
@@ -8589,7 +9821,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8589
9821
|
let files;
|
|
8590
9822
|
let discoveredFiles = null;
|
|
8591
9823
|
if (opts.files && opts.files.length > 0) {
|
|
8592
|
-
files = opts.files.map((f) =>
|
|
9824
|
+
files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
8593
9825
|
} else {
|
|
8594
9826
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
8595
9827
|
discoveredFiles = new Set(files);
|
|
@@ -8617,34 +9849,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8617
9849
|
const statOpts = signal ? { signal } : {};
|
|
8618
9850
|
const statReadParse = await Promise.allSettled(
|
|
8619
9851
|
batchFiles.map(async (file) => {
|
|
8620
|
-
let
|
|
9852
|
+
let stat17;
|
|
8621
9853
|
try {
|
|
8622
|
-
|
|
9854
|
+
stat17 = await fs14.stat(file, statOpts);
|
|
8623
9855
|
} catch (e) {
|
|
8624
9856
|
if (isAbortError(e)) throw e;
|
|
8625
9857
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8626
9858
|
}
|
|
8627
|
-
if (!
|
|
9859
|
+
if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
|
|
8628
9860
|
const lang = detectLang(file);
|
|
8629
|
-
if (!lang) return { file, stat:
|
|
9861
|
+
if (!lang) return { file, stat: stat17, lang: "", parsed: null };
|
|
8630
9862
|
const meta = existingMeta.get(file);
|
|
8631
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
8632
|
-
return { file, stat:
|
|
9863
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
|
|
9864
|
+
return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
|
|
8633
9865
|
}
|
|
8634
9866
|
let content;
|
|
8635
9867
|
try {
|
|
8636
|
-
content = await
|
|
9868
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
8637
9869
|
} catch (e) {
|
|
8638
9870
|
if (isAbortError(e)) throw e;
|
|
8639
|
-
return { file, stat:
|
|
9871
|
+
return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8640
9872
|
}
|
|
8641
9873
|
let parsed;
|
|
8642
9874
|
try {
|
|
8643
9875
|
parsed = await parseFile(file, content, lang);
|
|
8644
9876
|
} catch (e) {
|
|
8645
|
-
return { file, stat:
|
|
9877
|
+
return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8646
9878
|
}
|
|
8647
|
-
return { file, stat:
|
|
9879
|
+
return { file, stat: stat17, lang, parsed, content };
|
|
8648
9880
|
})
|
|
8649
9881
|
);
|
|
8650
9882
|
const batchEntries = [];
|
|
@@ -8664,7 +9896,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8664
9896
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
8665
9897
|
continue;
|
|
8666
9898
|
}
|
|
8667
|
-
const { stat:
|
|
9899
|
+
const { stat: stat17, lang, parsed } = result;
|
|
8668
9900
|
if (result.skippedMeta) {
|
|
8669
9901
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
8670
9902
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -8676,7 +9908,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8676
9908
|
store.upsertFile({
|
|
8677
9909
|
file,
|
|
8678
9910
|
lang,
|
|
8679
|
-
mtimeMs: Math.floor(
|
|
9911
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8680
9912
|
symbolCount: 0,
|
|
8681
9913
|
lastIndexed: Date.now()
|
|
8682
9914
|
});
|
|
@@ -8688,7 +9920,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8688
9920
|
store.upsertFile({
|
|
8689
9921
|
file,
|
|
8690
9922
|
lang,
|
|
8691
|
-
mtimeMs: Math.floor(
|
|
9923
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8692
9924
|
symbolCount: 0,
|
|
8693
9925
|
lastIndexed: Date.now()
|
|
8694
9926
|
});
|
|
@@ -8704,7 +9936,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8704
9936
|
lang,
|
|
8705
9937
|
symbols: parsed.symbols,
|
|
8706
9938
|
refs,
|
|
8707
|
-
mtimeMs: Math.floor(
|
|
9939
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8708
9940
|
symbolCount: parsed.symbols.length
|
|
8709
9941
|
});
|
|
8710
9942
|
deleteForFiles.push(file);
|
|
@@ -8892,7 +10124,7 @@ function resolveWorkerUrl() {
|
|
|
8892
10124
|
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8893
10125
|
try {
|
|
8894
10126
|
const url = new URL(rel, import.meta.url);
|
|
8895
|
-
if (url.protocol === "file:" &&
|
|
10127
|
+
if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
|
|
8896
10128
|
} catch {
|
|
8897
10129
|
}
|
|
8898
10130
|
}
|
|
@@ -8950,7 +10182,7 @@ function terminateWorker(reason) {
|
|
|
8950
10182
|
function callIndexOp(op, args, opts) {
|
|
8951
10183
|
const w = ensureWorker();
|
|
8952
10184
|
if (!w) return callInline(op, args, opts);
|
|
8953
|
-
return new Promise((
|
|
10185
|
+
return new Promise((resolve14, reject) => {
|
|
8954
10186
|
const id = nextRpcId++;
|
|
8955
10187
|
const timer = setTimeout(() => {
|
|
8956
10188
|
pending.delete(id);
|
|
@@ -8973,7 +10205,7 @@ function callIndexOp(op, args, opts) {
|
|
|
8973
10205
|
pending.set(id, {
|
|
8974
10206
|
resolve: (v) => {
|
|
8975
10207
|
cleanup();
|
|
8976
|
-
|
|
10208
|
+
resolve14(v);
|
|
8977
10209
|
},
|
|
8978
10210
|
reject: (e) => {
|
|
8979
10211
|
cleanup();
|
|
@@ -9312,8 +10544,8 @@ var codebaseStatsTool = {
|
|
|
9312
10544
|
};
|
|
9313
10545
|
|
|
9314
10546
|
// src/design.ts
|
|
9315
|
-
import * as
|
|
9316
|
-
import * as
|
|
10547
|
+
import * as fs16 from "node:fs/promises";
|
|
10548
|
+
import * as path20 from "node:path";
|
|
9317
10549
|
import {
|
|
9318
10550
|
applyTokenOverrides,
|
|
9319
10551
|
getDesignKitLoader,
|
|
@@ -9327,17 +10559,17 @@ import {
|
|
|
9327
10559
|
setDesignOverrides
|
|
9328
10560
|
} from "@wrongstack/core";
|
|
9329
10561
|
async function resolveReal(p) {
|
|
9330
|
-
const resolved =
|
|
10562
|
+
const resolved = path20.resolve(p);
|
|
9331
10563
|
let probe = resolved;
|
|
9332
10564
|
const missing = [];
|
|
9333
10565
|
for (; ; ) {
|
|
9334
10566
|
try {
|
|
9335
|
-
return
|
|
10567
|
+
return path20.resolve(await fs16.realpath(probe), ...missing);
|
|
9336
10568
|
} catch (err) {
|
|
9337
10569
|
if (err.code === "ENOENT") {
|
|
9338
|
-
const parent =
|
|
10570
|
+
const parent = path20.dirname(probe);
|
|
9339
10571
|
if (parent === probe) return resolved;
|
|
9340
|
-
missing.unshift(
|
|
10572
|
+
missing.unshift(path20.basename(probe));
|
|
9341
10573
|
probe = parent;
|
|
9342
10574
|
continue;
|
|
9343
10575
|
}
|
|
@@ -9503,18 +10735,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
9503
10735
|
outPath: input.out
|
|
9504
10736
|
});
|
|
9505
10737
|
const root = await resolveReal(ctx.projectRoot);
|
|
9506
|
-
const absResolved =
|
|
9507
|
-
const absParent = await resolveReal(
|
|
9508
|
-
const abs =
|
|
9509
|
-
const rel =
|
|
9510
|
-
if (rel.startsWith("..") ||
|
|
10738
|
+
const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
|
|
10739
|
+
const absParent = await resolveReal(path20.dirname(absResolved));
|
|
10740
|
+
const abs = path20.join(absParent, path20.basename(absResolved));
|
|
10741
|
+
const rel = path20.relative(root, abs);
|
|
10742
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
9511
10743
|
throw new Error(
|
|
9512
10744
|
`design: materialize path "${result.path}" would escape the project root`
|
|
9513
10745
|
);
|
|
9514
10746
|
}
|
|
9515
10747
|
let exists = false;
|
|
9516
10748
|
try {
|
|
9517
|
-
await
|
|
10749
|
+
await fs16.access(abs);
|
|
9518
10750
|
exists = true;
|
|
9519
10751
|
} catch {
|
|
9520
10752
|
}
|
|
@@ -9531,8 +10763,8 @@ ${result.content}
|
|
|
9531
10763
|
\`\`\``
|
|
9532
10764
|
};
|
|
9533
10765
|
}
|
|
9534
|
-
await
|
|
9535
|
-
await
|
|
10766
|
+
await fs16.mkdir(path20.dirname(abs), { recursive: true });
|
|
10767
|
+
await fs16.writeFile(abs, result.content);
|
|
9536
10768
|
return {
|
|
9537
10769
|
action,
|
|
9538
10770
|
kit: active.kit,
|
|
@@ -9582,8 +10814,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
|
|
|
9582
10814
|
init_util();
|
|
9583
10815
|
import { spawn as spawn7 } from "node:child_process";
|
|
9584
10816
|
import { statSync as statSync2 } from "node:fs";
|
|
9585
|
-
import * as
|
|
9586
|
-
import * as
|
|
10817
|
+
import * as fs17 from "node:fs/promises";
|
|
10818
|
+
import * as path21 from "node:path";
|
|
9587
10819
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
|
|
9588
10820
|
var diffTool = {
|
|
9589
10821
|
name: "diff",
|
|
@@ -9667,18 +10899,18 @@ function findGitDir(cwd) {
|
|
|
9667
10899
|
let dir = cwd;
|
|
9668
10900
|
for (let i = 0; i < 20; i++) {
|
|
9669
10901
|
try {
|
|
9670
|
-
const
|
|
9671
|
-
if (
|
|
10902
|
+
const stat17 = statSync2(path21.join(dir, ".git"));
|
|
10903
|
+
if (stat17.isDirectory()) return dir;
|
|
9672
10904
|
} catch {
|
|
9673
10905
|
}
|
|
9674
|
-
const parent =
|
|
10906
|
+
const parent = path21.dirname(dir);
|
|
9675
10907
|
if (parent === dir) break;
|
|
9676
10908
|
dir = parent;
|
|
9677
10909
|
}
|
|
9678
10910
|
return null;
|
|
9679
10911
|
}
|
|
9680
10912
|
function runGit(args, cwd, signal) {
|
|
9681
|
-
return new Promise((
|
|
10913
|
+
return new Promise((resolve14) => {
|
|
9682
10914
|
let stdout = "";
|
|
9683
10915
|
let stderr = "";
|
|
9684
10916
|
const child = spawn7("git", args, {
|
|
@@ -9694,8 +10926,8 @@ function runGit(args, cwd, signal) {
|
|
|
9694
10926
|
child.stderr?.on("data", (c) => {
|
|
9695
10927
|
stderr += c.toString();
|
|
9696
10928
|
});
|
|
9697
|
-
child.on("close", (code) =>
|
|
9698
|
-
child.on("error", (e) =>
|
|
10929
|
+
child.on("close", (code) => resolve14({ stdout, stderr, exitCode: code ?? 0 }));
|
|
10930
|
+
child.on("error", (e) => resolve14({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
9699
10931
|
});
|
|
9700
10932
|
}
|
|
9701
10933
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -9712,9 +10944,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
9712
10944
|
const results = [];
|
|
9713
10945
|
for (const file of files) {
|
|
9714
10946
|
const absPath = safeResolve(file, ctx);
|
|
9715
|
-
const
|
|
9716
|
-
if (!
|
|
9717
|
-
const content = await
|
|
10947
|
+
const stat17 = await fs17.stat(absPath).catch(() => null);
|
|
10948
|
+
if (!stat17?.isFile()) continue;
|
|
10949
|
+
const content = await fs17.readFile(absPath, "utf8");
|
|
9718
10950
|
const lines = content.split(/\r?\n/);
|
|
9719
10951
|
results.push(formatWithLineNumbers(file, lines));
|
|
9720
10952
|
}
|
|
@@ -9734,7 +10966,7 @@ ${numbered}`;
|
|
|
9734
10966
|
|
|
9735
10967
|
// src/document.ts
|
|
9736
10968
|
init_util();
|
|
9737
|
-
import * as
|
|
10969
|
+
import * as fs18 from "node:fs/promises";
|
|
9738
10970
|
var documentTool = {
|
|
9739
10971
|
name: "document",
|
|
9740
10972
|
category: "Project",
|
|
@@ -9778,7 +11010,7 @@ var documentTool = {
|
|
|
9778
11010
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
9779
11011
|
for (const absPath of fileList) {
|
|
9780
11012
|
try {
|
|
9781
|
-
const content = await
|
|
11013
|
+
const content = await fs18.readFile(absPath, "utf8");
|
|
9782
11014
|
filesProcessed++;
|
|
9783
11015
|
const processed = processFile(
|
|
9784
11016
|
content,
|
|
@@ -9814,8 +11046,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
9814
11046
|
for (const f of files) {
|
|
9815
11047
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
9816
11048
|
try {
|
|
9817
|
-
const
|
|
9818
|
-
if (
|
|
11049
|
+
const stat17 = await fs18.stat(absPath);
|
|
11050
|
+
if (stat17.isFile()) resolved.push(absPath);
|
|
9819
11051
|
} catch {
|
|
9820
11052
|
}
|
|
9821
11053
|
}
|
|
@@ -9882,10 +11114,426 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
9882
11114
|
return results;
|
|
9883
11115
|
}
|
|
9884
11116
|
|
|
11117
|
+
// src/e2e.ts
|
|
11118
|
+
init_util();
|
|
11119
|
+
import { open, readdir as readdir5 } from "node:fs/promises";
|
|
11120
|
+
import * as path22 from "node:path";
|
|
11121
|
+
async function readBoundedText(filePath, maxBytes) {
|
|
11122
|
+
let handle;
|
|
11123
|
+
try {
|
|
11124
|
+
handle = await open(filePath, "r");
|
|
11125
|
+
const info = await handle.stat();
|
|
11126
|
+
if (!info.isFile() || info.size > maxBytes) return void 0;
|
|
11127
|
+
const buffer = Buffer.alloc(info.size);
|
|
11128
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
11129
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
11130
|
+
} catch {
|
|
11131
|
+
return void 0;
|
|
11132
|
+
} finally {
|
|
11133
|
+
await handle?.close().catch(() => void 0);
|
|
11134
|
+
}
|
|
11135
|
+
}
|
|
11136
|
+
var CONFIG_NAMES = /* @__PURE__ */ new Map([
|
|
11137
|
+
["playwright.config.ts", "playwright"],
|
|
11138
|
+
["playwright.config.js", "playwright"],
|
|
11139
|
+
["playwright.config.mts", "playwright"],
|
|
11140
|
+
["playwright.config.mjs", "playwright"],
|
|
11141
|
+
["playwright.config.cts", "playwright"],
|
|
11142
|
+
["playwright.config.cjs", "playwright"],
|
|
11143
|
+
["cypress.config.ts", "cypress"],
|
|
11144
|
+
["cypress.config.js", "cypress"],
|
|
11145
|
+
["cypress.config.mts", "cypress"],
|
|
11146
|
+
["cypress.config.mjs", "cypress"],
|
|
11147
|
+
["cypress.config.cts", "cypress"],
|
|
11148
|
+
["cypress.config.cjs", "cypress"],
|
|
11149
|
+
["cypress.json", "cypress"]
|
|
11150
|
+
]);
|
|
11151
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
11152
|
+
".git",
|
|
11153
|
+
".hg",
|
|
11154
|
+
".svn",
|
|
11155
|
+
".wrongstack",
|
|
11156
|
+
"node_modules",
|
|
11157
|
+
"dist",
|
|
11158
|
+
"build",
|
|
11159
|
+
"coverage",
|
|
11160
|
+
"coverage-html",
|
|
11161
|
+
"playwright-report",
|
|
11162
|
+
"test-results",
|
|
11163
|
+
".next"
|
|
11164
|
+
]);
|
|
11165
|
+
var MAX_SCAN_DIRECTORIES = 4e3;
|
|
11166
|
+
var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
11167
|
+
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
11168
|
+
var MAX_SPEC_SAMPLES = 20;
|
|
11169
|
+
function relativePath(root, target) {
|
|
11170
|
+
const value = path22.relative(root, target) || ".";
|
|
11171
|
+
return value.split(path22.sep).join("/");
|
|
11172
|
+
}
|
|
11173
|
+
function escapeRegExp(value) {
|
|
11174
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11175
|
+
}
|
|
11176
|
+
function staticString(source, property) {
|
|
11177
|
+
const key = escapeRegExp(property);
|
|
11178
|
+
const direct = source.match(
|
|
11179
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11180
|
+
);
|
|
11181
|
+
if (direct?.[2] && !direct[2].includes("${")) return direct[2];
|
|
11182
|
+
const sameLineFallback = source.match(
|
|
11183
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11184
|
+
);
|
|
11185
|
+
return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
|
|
11186
|
+
}
|
|
11187
|
+
function staticNumber(source, property) {
|
|
11188
|
+
const key = escapeRegExp(property);
|
|
11189
|
+
const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
|
|
11190
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
|
|
11191
|
+
}
|
|
11192
|
+
function serverHints(framework, source) {
|
|
11193
|
+
if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
|
|
11194
|
+
const command = staticString(source, "command");
|
|
11195
|
+
const url = staticString(source, "url");
|
|
11196
|
+
const port = staticNumber(source, "port");
|
|
11197
|
+
return [
|
|
11198
|
+
{
|
|
11199
|
+
kind: "managed",
|
|
11200
|
+
...command ? { command } : {},
|
|
11201
|
+
...url ? { url } : {},
|
|
11202
|
+
...port ? { port } : {},
|
|
11203
|
+
source: "static-config",
|
|
11204
|
+
incomplete: !command || !url && !port
|
|
11205
|
+
}
|
|
11206
|
+
];
|
|
11207
|
+
}
|
|
11208
|
+
const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
|
|
11209
|
+
return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
|
|
11210
|
+
}
|
|
11211
|
+
function configuredSpecPattern(framework, source) {
|
|
11212
|
+
if (framework === "playwright") {
|
|
11213
|
+
const testDir = staticString(source, "testDir") ?? "./tests";
|
|
11214
|
+
return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
|
|
11215
|
+
}
|
|
11216
|
+
return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
|
|
11217
|
+
}
|
|
11218
|
+
async function scanWorkspace(root, maxDepth, signal) {
|
|
11219
|
+
const result = {
|
|
11220
|
+
configs: [],
|
|
11221
|
+
packageFiles: [],
|
|
11222
|
+
scannedDirectories: 0,
|
|
11223
|
+
truncated: false
|
|
11224
|
+
};
|
|
11225
|
+
const queue = [{ directory: root, depth: 0 }];
|
|
11226
|
+
while (queue.length > 0) {
|
|
11227
|
+
signal.throwIfAborted();
|
|
11228
|
+
const current = queue.shift();
|
|
11229
|
+
if (!current) break;
|
|
11230
|
+
result.scannedDirectories += 1;
|
|
11231
|
+
if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
|
|
11232
|
+
result.truncated = true;
|
|
11233
|
+
break;
|
|
11234
|
+
}
|
|
11235
|
+
let entries;
|
|
11236
|
+
try {
|
|
11237
|
+
entries = await readdir5(current.directory, { withFileTypes: true });
|
|
11238
|
+
} catch {
|
|
11239
|
+
continue;
|
|
11240
|
+
}
|
|
11241
|
+
for (const entry of entries) {
|
|
11242
|
+
signal.throwIfAborted();
|
|
11243
|
+
const absolutePath = path22.join(current.directory, entry.name);
|
|
11244
|
+
if (entry.isFile()) {
|
|
11245
|
+
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
11246
|
+
const framework = CONFIG_NAMES.get(entry.name);
|
|
11247
|
+
if (framework) result.configs.push({ framework, absolutePath });
|
|
11248
|
+
} else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
|
|
11249
|
+
queue.push({ directory: absolutePath, depth: current.depth + 1 });
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
return result;
|
|
11254
|
+
}
|
|
11255
|
+
async function readPackageInfo(packagePath) {
|
|
11256
|
+
try {
|
|
11257
|
+
const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
|
|
11258
|
+
if (raw === void 0) return void 0;
|
|
11259
|
+
const parsed = JSON.parse(raw);
|
|
11260
|
+
const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
|
|
11261
|
+
Object.entries(parsed.scripts).filter(
|
|
11262
|
+
(entry) => typeof entry[1] === "string"
|
|
11263
|
+
)
|
|
11264
|
+
) : {};
|
|
11265
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
11266
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
11267
|
+
const value = parsed[field];
|
|
11268
|
+
if (!value || typeof value !== "object") continue;
|
|
11269
|
+
for (const name of Object.keys(value)) dependencies.add(name);
|
|
11270
|
+
}
|
|
11271
|
+
const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
|
|
11272
|
+
const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
|
|
11273
|
+
return { path: packagePath, scripts, dependencies, packageManager };
|
|
11274
|
+
} catch {
|
|
11275
|
+
return void 0;
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11278
|
+
function frameworkFromPackage(info) {
|
|
11279
|
+
const found = /* @__PURE__ */ new Set();
|
|
11280
|
+
if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
|
|
11281
|
+
found.add("playwright");
|
|
11282
|
+
}
|
|
11283
|
+
if (info.dependencies.has("cypress")) found.add("cypress");
|
|
11284
|
+
for (const command of Object.values(info.scripts)) {
|
|
11285
|
+
if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
|
|
11286
|
+
if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
|
|
11287
|
+
}
|
|
11288
|
+
return [...found];
|
|
11289
|
+
}
|
|
11290
|
+
async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
11291
|
+
if (declared) return declared;
|
|
11292
|
+
let directory = projectRoot;
|
|
11293
|
+
while (true) {
|
|
11294
|
+
const names = /* @__PURE__ */ new Set();
|
|
11295
|
+
try {
|
|
11296
|
+
for (const entry of await readdir5(directory)) names.add(entry);
|
|
11297
|
+
} catch {
|
|
11298
|
+
}
|
|
11299
|
+
if (names.has("pnpm-lock.yaml")) return "pnpm";
|
|
11300
|
+
if (names.has("yarn.lock")) return "yarn";
|
|
11301
|
+
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
11302
|
+
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
11303
|
+
if (directory === scanRoot) break;
|
|
11304
|
+
const parent = path22.dirname(directory);
|
|
11305
|
+
const relativeParent = path22.relative(scanRoot, parent);
|
|
11306
|
+
if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
|
|
11307
|
+
break;
|
|
11308
|
+
}
|
|
11309
|
+
directory = parent;
|
|
11310
|
+
}
|
|
11311
|
+
return "npm";
|
|
11312
|
+
}
|
|
11313
|
+
function matchingScripts(info, framework) {
|
|
11314
|
+
if (!info) return [];
|
|
11315
|
+
const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
|
|
11316
|
+
return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
|
|
11317
|
+
const preferred = ["test:e2e", "e2e", "e2e:test"];
|
|
11318
|
+
const ai = preferred.indexOf(a);
|
|
11319
|
+
const bi = preferred.indexOf(b);
|
|
11320
|
+
if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
11321
|
+
return a.localeCompare(b);
|
|
11322
|
+
});
|
|
11323
|
+
}
|
|
11324
|
+
function executionPlan(framework, manager, root, configPath, scripts) {
|
|
11325
|
+
const script = scripts[0];
|
|
11326
|
+
if (script) {
|
|
11327
|
+
return {
|
|
11328
|
+
command: manager,
|
|
11329
|
+
args: ["run", script],
|
|
11330
|
+
cwd: root,
|
|
11331
|
+
source: "package-script",
|
|
11332
|
+
script
|
|
11333
|
+
};
|
|
11334
|
+
}
|
|
11335
|
+
const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
|
|
11336
|
+
if (configPath) {
|
|
11337
|
+
runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
|
|
11338
|
+
}
|
|
11339
|
+
return {
|
|
11340
|
+
command: manager === "bun" ? "bunx" : manager,
|
|
11341
|
+
args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
|
|
11342
|
+
cwd: root,
|
|
11343
|
+
source: "framework-default"
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
function isSpec(framework, filename) {
|
|
11347
|
+
if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11348
|
+
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11349
|
+
}
|
|
11350
|
+
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
11351
|
+
const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
|
|
11352
|
+
const samples = [];
|
|
11353
|
+
let count = 0;
|
|
11354
|
+
let scanned = 0;
|
|
11355
|
+
const queue = [...roots];
|
|
11356
|
+
while (queue.length > 0) {
|
|
11357
|
+
signal.throwIfAborted();
|
|
11358
|
+
const directory = queue.shift();
|
|
11359
|
+
if (!directory) break;
|
|
11360
|
+
scanned += 1;
|
|
11361
|
+
if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
|
|
11362
|
+
let entries;
|
|
11363
|
+
try {
|
|
11364
|
+
entries = await readdir5(directory, { withFileTypes: true });
|
|
11365
|
+
} catch {
|
|
11366
|
+
continue;
|
|
11367
|
+
}
|
|
11368
|
+
for (const entry of entries) {
|
|
11369
|
+
signal.throwIfAborted();
|
|
11370
|
+
const target = path22.join(directory, entry.name);
|
|
11371
|
+
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
11372
|
+
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
11373
|
+
count += 1;
|
|
11374
|
+
if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
}
|
|
11378
|
+
return { count, samples, truncated: count > samples.length };
|
|
11379
|
+
}
|
|
11380
|
+
async function readConfig(configPath) {
|
|
11381
|
+
if (!configPath) return "";
|
|
11382
|
+
return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
|
|
11383
|
+
}
|
|
11384
|
+
function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
11385
|
+
let directory = projectRoot;
|
|
11386
|
+
while (true) {
|
|
11387
|
+
const found = packagesByDirectory.get(directory);
|
|
11388
|
+
if (found) return found;
|
|
11389
|
+
if (directory === scanRoot) return void 0;
|
|
11390
|
+
const parent = path22.dirname(directory);
|
|
11391
|
+
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
11392
|
+
directory = parent;
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
async function discoverE2EProjects(root, options) {
|
|
11396
|
+
const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
|
|
11397
|
+
const scan = await scanWorkspace(root, maxDepth, options.signal);
|
|
11398
|
+
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
11399
|
+
(info) => Boolean(info)
|
|
11400
|
+
);
|
|
11401
|
+
const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
|
|
11402
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
11403
|
+
for (const config of scan.configs) {
|
|
11404
|
+
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
11405
|
+
continue;
|
|
11406
|
+
}
|
|
11407
|
+
const projectRoot = path22.dirname(config.absolutePath);
|
|
11408
|
+
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
11409
|
+
framework: config.framework,
|
|
11410
|
+
root: projectRoot,
|
|
11411
|
+
configPath: config.absolutePath
|
|
11412
|
+
});
|
|
11413
|
+
}
|
|
11414
|
+
for (const info of packages) {
|
|
11415
|
+
const projectRoot = path22.dirname(info.path);
|
|
11416
|
+
for (const framework of frameworkFromPackage(info)) {
|
|
11417
|
+
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
11418
|
+
continue;
|
|
11419
|
+
const key = `${framework}:${projectRoot}`;
|
|
11420
|
+
if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
const projects = [];
|
|
11424
|
+
for (const candidate of candidates.values()) {
|
|
11425
|
+
options.signal.throwIfAborted();
|
|
11426
|
+
const info = nearestPackage(candidate.root, packagesByDirectory, root);
|
|
11427
|
+
const source = await readConfig(candidate.configPath);
|
|
11428
|
+
const scripts = matchingScripts(info, candidate.framework);
|
|
11429
|
+
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
11430
|
+
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
11431
|
+
const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
|
|
11432
|
+
const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
|
|
11433
|
+
const unsafeTestDirectory = Boolean(
|
|
11434
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
|
|
11435
|
+
);
|
|
11436
|
+
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
11437
|
+
const warnings = [];
|
|
11438
|
+
if (!candidate.configPath)
|
|
11439
|
+
warnings.push("Framework dependency or script found without a config file.");
|
|
11440
|
+
if (candidate.configPath && !source) {
|
|
11441
|
+
warnings.push("Config could not be read or exceeded the static inspection limit.");
|
|
11442
|
+
}
|
|
11443
|
+
if (unsafeTestDirectory) {
|
|
11444
|
+
warnings.push(
|
|
11445
|
+
"Configured testDir resolves outside the requested discovery root and was not scanned."
|
|
11446
|
+
);
|
|
11447
|
+
}
|
|
11448
|
+
const hints = serverHints(candidate.framework, source);
|
|
11449
|
+
if (hints.some((hint) => hint.incomplete)) {
|
|
11450
|
+
warnings.push("Configured server contains dynamic values; only static hints are shown.");
|
|
11451
|
+
}
|
|
11452
|
+
projects.push({
|
|
11453
|
+
framework: candidate.framework,
|
|
11454
|
+
root: relativePath(root, candidate.root),
|
|
11455
|
+
...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
|
|
11456
|
+
...info ? { packagePath: relativePath(root, info.path) } : {},
|
|
11457
|
+
packageManager: manager,
|
|
11458
|
+
evidence: [
|
|
11459
|
+
...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
|
|
11460
|
+
...info?.dependencies.has(
|
|
11461
|
+
candidate.framework === "playwright" ? "@playwright/test" : "cypress"
|
|
11462
|
+
) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
|
|
11463
|
+
...scripts.map((script) => `script:${script}`)
|
|
11464
|
+
],
|
|
11465
|
+
scripts,
|
|
11466
|
+
serverHints: hints,
|
|
11467
|
+
specPattern: configuredSpecPattern(candidate.framework, source),
|
|
11468
|
+
specCount: specs.count,
|
|
11469
|
+
specSamples: specs.samples,
|
|
11470
|
+
specsTruncated: specs.truncated,
|
|
11471
|
+
execution: executionPlan(
|
|
11472
|
+
candidate.framework,
|
|
11473
|
+
manager,
|
|
11474
|
+
relativePath(root, candidate.root),
|
|
11475
|
+
candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
|
|
11476
|
+
scripts
|
|
11477
|
+
),
|
|
11478
|
+
warnings
|
|
11479
|
+
});
|
|
11480
|
+
}
|
|
11481
|
+
projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
|
|
11482
|
+
return {
|
|
11483
|
+
root,
|
|
11484
|
+
projects,
|
|
11485
|
+
scannedDirectories: scan.scannedDirectories,
|
|
11486
|
+
scanTruncated: scan.truncated,
|
|
11487
|
+
warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
|
|
11488
|
+
};
|
|
11489
|
+
}
|
|
11490
|
+
var e2ePlanTool = {
|
|
11491
|
+
name: "e2e_plan",
|
|
11492
|
+
category: "Code Quality",
|
|
11493
|
+
description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
|
|
11494
|
+
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.",
|
|
11495
|
+
permission: "auto",
|
|
11496
|
+
mutating: false,
|
|
11497
|
+
riskTier: "safe",
|
|
11498
|
+
capabilities: ["fs.read"],
|
|
11499
|
+
icon: "test",
|
|
11500
|
+
timeoutMs: 3e4,
|
|
11501
|
+
inputSchema: {
|
|
11502
|
+
type: "object",
|
|
11503
|
+
properties: {
|
|
11504
|
+
cwd: { type: "string", description: "Directory inside the project to inspect." },
|
|
11505
|
+
framework: {
|
|
11506
|
+
type: "string",
|
|
11507
|
+
enum: ["all", "playwright", "cypress"],
|
|
11508
|
+
description: "Optional framework filter; defaults to all."
|
|
11509
|
+
},
|
|
11510
|
+
maxDepth: {
|
|
11511
|
+
type: "integer",
|
|
11512
|
+
minimum: 0,
|
|
11513
|
+
maximum: 8,
|
|
11514
|
+
description: "Maximum workspace discovery depth; defaults to 5."
|
|
11515
|
+
},
|
|
11516
|
+
includeSpecs: {
|
|
11517
|
+
type: "boolean",
|
|
11518
|
+
description: "Count and sample specs; defaults to true."
|
|
11519
|
+
}
|
|
11520
|
+
}
|
|
11521
|
+
},
|
|
11522
|
+
async execute(input, ctx, opts) {
|
|
11523
|
+
const root = await safeResolveReal(input.cwd ?? ".", ctx);
|
|
11524
|
+
return discoverE2EProjects(root, {
|
|
11525
|
+
framework: input.framework,
|
|
11526
|
+
maxDepth: input.maxDepth,
|
|
11527
|
+
includeSpecs: input.includeSpecs,
|
|
11528
|
+
signal: opts.signal
|
|
11529
|
+
});
|
|
11530
|
+
}
|
|
11531
|
+
};
|
|
11532
|
+
|
|
9885
11533
|
// src/edit.ts
|
|
9886
|
-
import * as
|
|
11534
|
+
import * as fs19 from "node:fs/promises";
|
|
9887
11535
|
import {
|
|
9888
|
-
atomicWrite,
|
|
11536
|
+
atomicWrite as atomicWrite2,
|
|
9889
11537
|
detectNewlineStyle,
|
|
9890
11538
|
normalizeToLf,
|
|
9891
11539
|
ToolValidationError,
|
|
@@ -10098,7 +11746,7 @@ function prefixSimilarity(a, b) {
|
|
|
10098
11746
|
}
|
|
10099
11747
|
|
|
10100
11748
|
// src/_syntax-check.ts
|
|
10101
|
-
import * as
|
|
11749
|
+
import * as path23 from "node:path";
|
|
10102
11750
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
10103
11751
|
var MAX_CHECK_CHARS = 15e5;
|
|
10104
11752
|
var MAX_ERRORS = 5;
|
|
@@ -10122,15 +11770,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
10122
11770
|
return { errors, preExisting };
|
|
10123
11771
|
}
|
|
10124
11772
|
function isJsoncFile(filePath) {
|
|
10125
|
-
const base =
|
|
11773
|
+
const base = path23.basename(filePath).toLowerCase();
|
|
10126
11774
|
if (base.endsWith(".jsonc")) return true;
|
|
10127
11775
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
10128
|
-
const dir =
|
|
11776
|
+
const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
|
|
10129
11777
|
return dir === ".vscode";
|
|
10130
11778
|
}
|
|
10131
11779
|
async function parseErrors(filePath, content) {
|
|
10132
11780
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
10133
|
-
const ext =
|
|
11781
|
+
const ext = path23.extname(filePath).toLowerCase();
|
|
10134
11782
|
if (ext === ".json" || ext === ".jsonc") {
|
|
10135
11783
|
try {
|
|
10136
11784
|
JSON.parse(content);
|
|
@@ -10157,7 +11805,7 @@ async function parseErrors(filePath, content) {
|
|
|
10157
11805
|
ts2.ScriptKind.JSX
|
|
10158
11806
|
);
|
|
10159
11807
|
const sourceFile = ts2.createSourceFile(
|
|
10160
|
-
|
|
11808
|
+
path23.basename(filePath),
|
|
10161
11809
|
content,
|
|
10162
11810
|
ts2.ScriptTarget.Latest,
|
|
10163
11811
|
/* setParentNodes */
|
|
@@ -10231,7 +11879,7 @@ var editTool = {
|
|
|
10231
11879
|
});
|
|
10232
11880
|
}
|
|
10233
11881
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10234
|
-
const
|
|
11882
|
+
const stat17 = await fs19.stat(absPath).catch((err) => {
|
|
10235
11883
|
if (err.code === "ENOENT") {
|
|
10236
11884
|
throw new ToolValidationError({
|
|
10237
11885
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -10241,15 +11889,15 @@ var editTool = {
|
|
|
10241
11889
|
}
|
|
10242
11890
|
throw err;
|
|
10243
11891
|
});
|
|
10244
|
-
if (!
|
|
11892
|
+
if (!stat17.isFile()) {
|
|
10245
11893
|
throw new ToolValidationError({
|
|
10246
11894
|
message: `edit: "${input.path}" is not a regular file`,
|
|
10247
11895
|
field: "path"
|
|
10248
11896
|
});
|
|
10249
11897
|
}
|
|
10250
11898
|
const autoRead = !ctx.hasRead(absPath);
|
|
10251
|
-
const original = await
|
|
10252
|
-
const updated = await
|
|
11899
|
+
const original = await fs19.readFile(absPath, "utf8");
|
|
11900
|
+
const updated = await fs19.stat(absPath);
|
|
10253
11901
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
10254
11902
|
const originalHash = sha256hex(original);
|
|
10255
11903
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
@@ -10271,7 +11919,7 @@ var editTool = {
|
|
|
10271
11919
|
});
|
|
10272
11920
|
}
|
|
10273
11921
|
}
|
|
10274
|
-
if (autoRead && updated.mtimeMs >
|
|
11922
|
+
if (autoRead && updated.mtimeMs > stat17.mtimeMs + mtimeTolerance) {
|
|
10275
11923
|
throw new ToolValidationError({
|
|
10276
11924
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
10277
11925
|
field: "path",
|
|
@@ -10353,8 +12001,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
10353
12001
|
}
|
|
10354
12002
|
const newFile = toStyle(newFileLf, style);
|
|
10355
12003
|
opts?.signal?.throwIfAborted();
|
|
10356
|
-
await
|
|
10357
|
-
const written = await
|
|
12004
|
+
await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
|
|
12005
|
+
const written = await fs19.stat(absPath);
|
|
10358
12006
|
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
10359
12007
|
ctx.session.recordFileChange({
|
|
10360
12008
|
path: absPath,
|
|
@@ -11314,7 +12962,7 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
11314
12962
|
// and glob patterns that could expand to dangerous targets.
|
|
11315
12963
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
11316
12964
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
11317
|
-
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
12965
|
+
rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
11318
12966
|
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
11319
12967
|
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
11320
12968
|
npm: [],
|
|
@@ -11482,7 +13130,7 @@ var execTool = {
|
|
|
11482
13130
|
}
|
|
11483
13131
|
};
|
|
11484
13132
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
11485
|
-
return new Promise((
|
|
13133
|
+
return new Promise((resolve14) => {
|
|
11486
13134
|
let stdout = "";
|
|
11487
13135
|
let stderr = "";
|
|
11488
13136
|
let killed = false;
|
|
@@ -11490,7 +13138,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
11490
13138
|
const finish = (result) => {
|
|
11491
13139
|
if (resolvedOnce.value) return;
|
|
11492
13140
|
resolvedOnce.value = true;
|
|
11493
|
-
|
|
13141
|
+
resolve14(result);
|
|
11494
13142
|
};
|
|
11495
13143
|
const startedAt = Date.now();
|
|
11496
13144
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
@@ -11599,13 +13247,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
|
|
|
11599
13247
|
import TurndownService from "turndown";
|
|
11600
13248
|
|
|
11601
13249
|
// src/_fetch-guard.ts
|
|
11602
|
-
import * as
|
|
11603
|
-
import * as
|
|
13250
|
+
import * as dns2 from "node:dns/promises";
|
|
13251
|
+
import * as net3 from "node:net";
|
|
11604
13252
|
import {
|
|
11605
13253
|
FetchError,
|
|
11606
13254
|
ToolValidationError as ToolValidationError2,
|
|
11607
|
-
isPrivateIPv4,
|
|
11608
|
-
isPrivateIPv6
|
|
13255
|
+
isPrivateIPv4 as isPrivateIPv42,
|
|
13256
|
+
isPrivateIPv6 as isPrivateIPv62
|
|
11609
13257
|
} from "@wrongstack/core";
|
|
11610
13258
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
11611
13259
|
var nativeGlobalFetch = globalThis.fetch;
|
|
@@ -11616,13 +13264,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
11616
13264
|
);
|
|
11617
13265
|
}
|
|
11618
13266
|
function guardedLookup(hostname2, options, callback) {
|
|
11619
|
-
|
|
13267
|
+
dns2.lookup(hostname2, { all: true }).then((records) => {
|
|
11620
13268
|
const family = options?.family;
|
|
11621
13269
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
11622
13270
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
11623
13271
|
if (!ALLOW_PRIVATE) {
|
|
11624
13272
|
for (const r of list) {
|
|
11625
|
-
const bad = r.family === 4 ?
|
|
13273
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11626
13274
|
if (bad) {
|
|
11627
13275
|
callback(
|
|
11628
13276
|
Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
|
|
@@ -11727,16 +13375,16 @@ async function assertNotPrivate(hostname2) {
|
|
|
11727
13375
|
field: "url"
|
|
11728
13376
|
});
|
|
11729
13377
|
}
|
|
11730
|
-
const ipVersion =
|
|
13378
|
+
const ipVersion = net3.isIP(host);
|
|
11731
13379
|
if (ipVersion === 4) {
|
|
11732
|
-
if (
|
|
13380
|
+
if (isPrivateIPv42(host)) {
|
|
11733
13381
|
throw new ToolValidationError2({
|
|
11734
13382
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11735
13383
|
field: "url"
|
|
11736
13384
|
});
|
|
11737
13385
|
}
|
|
11738
13386
|
} else if (ipVersion === 6) {
|
|
11739
|
-
if (
|
|
13387
|
+
if (isPrivateIPv62(host)) {
|
|
11740
13388
|
throw new ToolValidationError2({
|
|
11741
13389
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11742
13390
|
field: "url"
|
|
@@ -11744,9 +13392,9 @@ async function assertNotPrivate(hostname2) {
|
|
|
11744
13392
|
}
|
|
11745
13393
|
} else {
|
|
11746
13394
|
try {
|
|
11747
|
-
const records = await
|
|
13395
|
+
const records = await dns2.lookup(host, { all: true });
|
|
11748
13396
|
for (const r of records) {
|
|
11749
|
-
const bad = r.family === 4 ?
|
|
13397
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11750
13398
|
if (bad) {
|
|
11751
13399
|
throw new ToolValidationError2({
|
|
11752
13400
|
message: `fetch: resolved to private address ${r.address}`,
|
|
@@ -12085,13 +13733,13 @@ var formatTool = {
|
|
|
12085
13733
|
}
|
|
12086
13734
|
};
|
|
12087
13735
|
async function detectFixer(cwd) {
|
|
12088
|
-
const { stat:
|
|
13736
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
12089
13737
|
try {
|
|
12090
|
-
await
|
|
13738
|
+
await stat17(`${cwd}/biome.json`);
|
|
12091
13739
|
return "biome";
|
|
12092
13740
|
} catch {
|
|
12093
13741
|
try {
|
|
12094
|
-
await
|
|
13742
|
+
await stat17(`${cwd}/.prettierrc`);
|
|
12095
13743
|
return "prettier";
|
|
12096
13744
|
} catch {
|
|
12097
13745
|
return "biome";
|
|
@@ -12103,7 +13751,7 @@ async function detectFixer(cwd) {
|
|
|
12103
13751
|
init_util();
|
|
12104
13752
|
import { spawn as spawn9 } from "node:child_process";
|
|
12105
13753
|
import { statSync as statSync3 } from "node:fs";
|
|
12106
|
-
import { dirname as
|
|
13754
|
+
import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
|
|
12107
13755
|
import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
|
|
12108
13756
|
var TIMEOUT_MS2 = 3e4;
|
|
12109
13757
|
var MAX_OUTPUT3 = 1e5;
|
|
@@ -12246,9 +13894,9 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
12246
13894
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
12247
13895
|
}
|
|
12248
13896
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
12249
|
-
const root =
|
|
12250
|
-
const abs =
|
|
12251
|
-
if (abs !== root && !abs.startsWith(root +
|
|
13897
|
+
const root = resolve11(projectRoot);
|
|
13898
|
+
const abs = resolve11(root, input.worktreePath);
|
|
13899
|
+
if (abs !== root && !abs.startsWith(root + sep4)) {
|
|
12252
13900
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
12253
13901
|
}
|
|
12254
13902
|
}
|
|
@@ -12259,12 +13907,12 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
12259
13907
|
let dir = cwd;
|
|
12260
13908
|
for (let i = 0; i < 20; i++) {
|
|
12261
13909
|
try {
|
|
12262
|
-
const
|
|
12263
|
-
if (
|
|
13910
|
+
const stat17 = statSync3(`${dir}/.git`);
|
|
13911
|
+
if (stat17.isDirectory() || stat17.isFile()) return dir;
|
|
12264
13912
|
} catch {
|
|
12265
13913
|
}
|
|
12266
13914
|
if (dir === root) break;
|
|
12267
|
-
const parent =
|
|
13915
|
+
const parent = dirname12(dir);
|
|
12268
13916
|
if (parent === dir) break;
|
|
12269
13917
|
dir = parent;
|
|
12270
13918
|
}
|
|
@@ -12341,7 +13989,7 @@ function buildArgs(input) {
|
|
|
12341
13989
|
}
|
|
12342
13990
|
}
|
|
12343
13991
|
function runGit2(args, cwd, signal) {
|
|
12344
|
-
return new Promise((
|
|
13992
|
+
return new Promise((resolve14) => {
|
|
12345
13993
|
let stdout = "";
|
|
12346
13994
|
let stderr = "";
|
|
12347
13995
|
const child = spawn9("git", args, {
|
|
@@ -12362,7 +14010,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12362
14010
|
}
|
|
12363
14011
|
});
|
|
12364
14012
|
child.on("error", (err) => {
|
|
12365
|
-
|
|
14013
|
+
resolve14({
|
|
12366
14014
|
command: args[0],
|
|
12367
14015
|
stdout: normalizeCommandOutput(stdout),
|
|
12368
14016
|
stderr: err.message,
|
|
@@ -12371,7 +14019,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12371
14019
|
});
|
|
12372
14020
|
});
|
|
12373
14021
|
child.on("close", (code) => {
|
|
12374
|
-
|
|
14022
|
+
resolve14({
|
|
12375
14023
|
command: args[0],
|
|
12376
14024
|
stdout: normalizeCommandOutput(stdout),
|
|
12377
14025
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -12383,8 +14031,8 @@ function runGit2(args, cwd, signal) {
|
|
|
12383
14031
|
}
|
|
12384
14032
|
|
|
12385
14033
|
// src/glob.ts
|
|
12386
|
-
import * as
|
|
12387
|
-
import * as
|
|
14034
|
+
import * as fs20 from "node:fs/promises";
|
|
14035
|
+
import * as path24 from "node:path";
|
|
12388
14036
|
import { compileGlob as compileGlob3 } from "@wrongstack/core";
|
|
12389
14037
|
|
|
12390
14038
|
// src/_concurrency.ts
|
|
@@ -12456,7 +14104,7 @@ var globTool = {
|
|
|
12456
14104
|
return;
|
|
12457
14105
|
}
|
|
12458
14106
|
try {
|
|
12459
|
-
const st = await
|
|
14107
|
+
const st = await fs20.stat(full);
|
|
12460
14108
|
if (truncated || results.length >= limit) {
|
|
12461
14109
|
truncated = true;
|
|
12462
14110
|
return;
|
|
@@ -12477,7 +14125,7 @@ var globTool = {
|
|
|
12477
14125
|
}
|
|
12478
14126
|
let entries;
|
|
12479
14127
|
try {
|
|
12480
|
-
entries = await
|
|
14128
|
+
entries = await fs20.readdir(dir, { withFileTypes: true });
|
|
12481
14129
|
} catch {
|
|
12482
14130
|
return;
|
|
12483
14131
|
}
|
|
@@ -12488,7 +14136,7 @@ var globTool = {
|
|
|
12488
14136
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
12489
14137
|
if (ignored.includes(name)) continue;
|
|
12490
14138
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
12491
|
-
const full =
|
|
14139
|
+
const full = path24.join(dir, name);
|
|
12492
14140
|
if (e.isDirectory()) {
|
|
12493
14141
|
subdirs.push({ full, rel });
|
|
12494
14142
|
} else if (e.isFile()) {
|
|
@@ -12501,13 +14149,13 @@ var globTool = {
|
|
|
12501
14149
|
}
|
|
12502
14150
|
} else if (e.isSymbolicLink()) {
|
|
12503
14151
|
try {
|
|
12504
|
-
const st = await
|
|
14152
|
+
const st = await fs20.stat(full);
|
|
12505
14153
|
if (st.isDirectory()) {
|
|
12506
|
-
const real = await
|
|
14154
|
+
const real = await fs20.realpath(full);
|
|
12507
14155
|
await assertRealInsideRoot(real, ctx);
|
|
12508
14156
|
subdirs.push({ full, rel });
|
|
12509
14157
|
} else if (st.isFile()) {
|
|
12510
|
-
const real = await
|
|
14158
|
+
const real = await fs20.realpath(full);
|
|
12511
14159
|
await assertRealInsideRoot(real, ctx);
|
|
12512
14160
|
re.lastIndex = 0;
|
|
12513
14161
|
const relMatch = re.test(rel);
|
|
@@ -12532,7 +14180,7 @@ var globTool = {
|
|
|
12532
14180
|
};
|
|
12533
14181
|
async function readGitignore(dir) {
|
|
12534
14182
|
try {
|
|
12535
|
-
const raw = await
|
|
14183
|
+
const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
|
|
12536
14184
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
12537
14185
|
} catch {
|
|
12538
14186
|
return [];
|
|
@@ -12542,8 +14190,8 @@ async function readGitignore(dir) {
|
|
|
12542
14190
|
// src/grep.ts
|
|
12543
14191
|
import { expectDefined as expectDefined7 } from "@wrongstack/core";
|
|
12544
14192
|
import { spawn as spawn10 } from "node:child_process";
|
|
12545
|
-
import * as
|
|
12546
|
-
import * as
|
|
14193
|
+
import * as fs21 from "node:fs/promises";
|
|
14194
|
+
import * as path25 from "node:path";
|
|
12547
14195
|
import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
|
|
12548
14196
|
|
|
12549
14197
|
// src/_regex.ts
|
|
@@ -12688,13 +14336,13 @@ var grepTool = {
|
|
|
12688
14336
|
}
|
|
12689
14337
|
};
|
|
12690
14338
|
async function detectRg(signal) {
|
|
12691
|
-
return new Promise((
|
|
14339
|
+
return new Promise((resolve14) => {
|
|
12692
14340
|
try {
|
|
12693
14341
|
const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
|
|
12694
|
-
p.on("error", () =>
|
|
12695
|
-
p.on("close", (code) =>
|
|
14342
|
+
p.on("error", () => resolve14(false));
|
|
14343
|
+
p.on("close", (code) => resolve14(code === 0));
|
|
12696
14344
|
} catch {
|
|
12697
|
-
|
|
14345
|
+
resolve14(false);
|
|
12698
14346
|
}
|
|
12699
14347
|
});
|
|
12700
14348
|
}
|
|
@@ -12845,9 +14493,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12845
14493
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
12846
14494
|
if (globRe) globRe.lastIndex = 0;
|
|
12847
14495
|
try {
|
|
12848
|
-
const
|
|
12849
|
-
if (!
|
|
12850
|
-
const file = await
|
|
14496
|
+
const stat17 = await fs21.stat(full);
|
|
14497
|
+
if (!stat17.isFile() || stat17.size > maxBytes || stopped || signal.aborted) return;
|
|
14498
|
+
const file = await fs21.open(full, "r");
|
|
12851
14499
|
try {
|
|
12852
14500
|
let bytesReadTotal = 0;
|
|
12853
14501
|
let lineNumber = 0;
|
|
@@ -12926,7 +14574,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12926
14574
|
if (stopped || signal.aborted) return;
|
|
12927
14575
|
let entries;
|
|
12928
14576
|
try {
|
|
12929
|
-
entries = await
|
|
14577
|
+
entries = await fs21.readdir(dir, { withFileTypes: true });
|
|
12930
14578
|
} catch {
|
|
12931
14579
|
return;
|
|
12932
14580
|
}
|
|
@@ -12936,7 +14584,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12936
14584
|
if (stopped) return;
|
|
12937
14585
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
12938
14586
|
if (e.isSymbolicLink()) continue;
|
|
12939
|
-
const full =
|
|
14587
|
+
const full = path25.join(dir, e.name);
|
|
12940
14588
|
if (e.isDirectory()) {
|
|
12941
14589
|
subdirs.push(full);
|
|
12942
14590
|
} else if (e.isFile()) {
|
|
@@ -12959,7 +14607,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12959
14607
|
init_spawn_stream();
|
|
12960
14608
|
init_util();
|
|
12961
14609
|
init_legacy_bridge();
|
|
12962
|
-
import { join as
|
|
14610
|
+
import { join as join21 } from "node:path";
|
|
12963
14611
|
import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
|
|
12964
14612
|
var installTool = {
|
|
12965
14613
|
name: "install",
|
|
@@ -13136,16 +14784,16 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
13136
14784
|
case "pnpm":
|
|
13137
14785
|
case "yarn":
|
|
13138
14786
|
case "npm":
|
|
13139
|
-
return
|
|
14787
|
+
return join21(cwd, "package.json");
|
|
13140
14788
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
13141
14789
|
default:
|
|
13142
|
-
return
|
|
14790
|
+
return join21(cwd, "package.json");
|
|
13143
14791
|
}
|
|
13144
14792
|
}
|
|
13145
14793
|
|
|
13146
14794
|
// src/json.ts
|
|
13147
14795
|
init_util();
|
|
13148
|
-
import * as
|
|
14796
|
+
import * as fs22 from "node:fs/promises";
|
|
13149
14797
|
import { deepMerge as deepMergeCore } from "@wrongstack/core";
|
|
13150
14798
|
var jsonTool = {
|
|
13151
14799
|
name: "json",
|
|
@@ -13220,7 +14868,7 @@ async function executeParse(input, ctx) {
|
|
|
13220
14868
|
let raw;
|
|
13221
14869
|
if (input.file) {
|
|
13222
14870
|
try {
|
|
13223
|
-
raw = await
|
|
14871
|
+
raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13224
14872
|
} catch {
|
|
13225
14873
|
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
13226
14874
|
}
|
|
@@ -13278,7 +14926,7 @@ async function executeQuery(input, ctx) {
|
|
|
13278
14926
|
let parsed;
|
|
13279
14927
|
if (input.file) {
|
|
13280
14928
|
try {
|
|
13281
|
-
const raw = await
|
|
14929
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13282
14930
|
parsed = JSON.parse(raw);
|
|
13283
14931
|
} catch {
|
|
13284
14932
|
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
@@ -13320,7 +14968,7 @@ async function executeValidate(input, ctx) {
|
|
|
13320
14968
|
let parsed;
|
|
13321
14969
|
if (input.file) {
|
|
13322
14970
|
try {
|
|
13323
|
-
const raw = await
|
|
14971
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13324
14972
|
parsed = JSON.parse(raw);
|
|
13325
14973
|
} catch {
|
|
13326
14974
|
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
@@ -13362,7 +15010,7 @@ async function executeTransform(input, ctx) {
|
|
|
13362
15010
|
let parsed;
|
|
13363
15011
|
if (input.file) {
|
|
13364
15012
|
try {
|
|
13365
|
-
const raw = await
|
|
15013
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13366
15014
|
parsed = JSON.parse(raw);
|
|
13367
15015
|
} catch {
|
|
13368
15016
|
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
@@ -13524,56 +15172,56 @@ function jmespathSearch(data, query) {
|
|
|
13524
15172
|
}
|
|
13525
15173
|
function validateJsonSchema(data, schema) {
|
|
13526
15174
|
const errors = [];
|
|
13527
|
-
function check(value, s,
|
|
15175
|
+
function check(value, s, path32) {
|
|
13528
15176
|
if (s["type"]) {
|
|
13529
15177
|
const expectedType = s["type"];
|
|
13530
15178
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
13531
15179
|
if (expectedType === "integer") {
|
|
13532
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
15180
|
+
if (!Number.isInteger(value)) errors.push(`${path32}: expected integer, got ${actualType}`);
|
|
13533
15181
|
} else if (expectedType !== actualType) {
|
|
13534
|
-
errors.push(`${
|
|
15182
|
+
errors.push(`${path32}: expected ${expectedType}, got ${actualType}`);
|
|
13535
15183
|
}
|
|
13536
15184
|
}
|
|
13537
15185
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
13538
15186
|
try {
|
|
13539
15187
|
new URL(value);
|
|
13540
15188
|
} catch {
|
|
13541
|
-
errors.push(`${
|
|
15189
|
+
errors.push(`${path32}: not a valid URI`);
|
|
13542
15190
|
}
|
|
13543
15191
|
}
|
|
13544
15192
|
if (typeof value === "string" && s["pattern"]) {
|
|
13545
15193
|
const re = new RegExp(s["pattern"]);
|
|
13546
|
-
if (!re.test(value)) errors.push(`${
|
|
15194
|
+
if (!re.test(value)) errors.push(`${path32}: does not match pattern ${s["pattern"]}`);
|
|
13547
15195
|
}
|
|
13548
15196
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
13549
|
-
errors.push(`${
|
|
15197
|
+
errors.push(`${path32}: string too short (min ${s["minLength"]})`);
|
|
13550
15198
|
}
|
|
13551
15199
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
13552
|
-
errors.push(`${
|
|
15200
|
+
errors.push(`${path32}: string too long (max ${s["maxLength"]})`);
|
|
13553
15201
|
}
|
|
13554
15202
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
13555
|
-
errors.push(`${
|
|
15203
|
+
errors.push(`${path32}: below minimum ${s["minimum"]}`);
|
|
13556
15204
|
}
|
|
13557
15205
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
13558
|
-
errors.push(`${
|
|
15206
|
+
errors.push(`${path32}: above maximum ${s["maximum"]}`);
|
|
13559
15207
|
}
|
|
13560
15208
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
13561
15209
|
for (let i = 0; i < value.length; i++) {
|
|
13562
|
-
check(value[i], s["items"], `${
|
|
15210
|
+
check(value[i], s["items"], `${path32}[${i}]`);
|
|
13563
15211
|
}
|
|
13564
15212
|
}
|
|
13565
15213
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
13566
15214
|
const props = s["properties"];
|
|
13567
15215
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
13568
|
-
check(value[k], propSchema, `${
|
|
15216
|
+
check(value[k], propSchema, `${path32}.${k}`);
|
|
13569
15217
|
}
|
|
13570
15218
|
}
|
|
13571
15219
|
}
|
|
13572
15220
|
check(data, schema, "$");
|
|
13573
15221
|
return { valid: errors.length === 0, errors };
|
|
13574
15222
|
}
|
|
13575
|
-
function simpleQuery(data,
|
|
13576
|
-
const parts =
|
|
15223
|
+
function simpleQuery(data, path32) {
|
|
15224
|
+
const parts = path32.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
13577
15225
|
let current = data;
|
|
13578
15226
|
for (const part of parts) {
|
|
13579
15227
|
if (current === null || current === void 0) return void 0;
|
|
@@ -14577,11 +16225,11 @@ var lintTool = {
|
|
|
14577
16225
|
}
|
|
14578
16226
|
};
|
|
14579
16227
|
async function detectLinter(cwd) {
|
|
14580
|
-
const { stat:
|
|
16228
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
14581
16229
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
14582
16230
|
for (const f of checks) {
|
|
14583
16231
|
try {
|
|
14584
|
-
await
|
|
16232
|
+
await stat17(`${cwd}/${f}`);
|
|
14585
16233
|
if (f.includes("biome")) return "biome";
|
|
14586
16234
|
if (f.includes("eslint")) return "eslint";
|
|
14587
16235
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -14681,7 +16329,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14681
16329
|
};
|
|
14682
16330
|
}
|
|
14683
16331
|
args.push("--timestamps", service);
|
|
14684
|
-
return new Promise((
|
|
16332
|
+
return new Promise((resolve14) => {
|
|
14685
16333
|
let stdout = "";
|
|
14686
16334
|
let stderr = "";
|
|
14687
16335
|
const MAX = 2e5;
|
|
@@ -14697,7 +16345,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14697
16345
|
if (settled) return;
|
|
14698
16346
|
settled = true;
|
|
14699
16347
|
clearTimeout(timer);
|
|
14700
|
-
|
|
16348
|
+
resolve14(result);
|
|
14701
16349
|
};
|
|
14702
16350
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
14703
16351
|
const timer = setTimeout(() => {
|
|
@@ -14732,16 +16380,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14732
16380
|
}
|
|
14733
16381
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
14734
16382
|
var MAX_TAIL_LINES = 1e5;
|
|
14735
|
-
async function fileLogs(
|
|
16383
|
+
async function fileLogs(path32, lines, filterRe, stream) {
|
|
14736
16384
|
const { createInterface } = await import("node:readline");
|
|
14737
|
-
const { createReadStream } = await import("node:fs");
|
|
16385
|
+
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
14738
16386
|
const entries = [];
|
|
14739
16387
|
const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
|
|
14740
16388
|
const window = new Array(effLines);
|
|
14741
16389
|
let writeIdx = 0;
|
|
14742
16390
|
let totalLines = 0;
|
|
14743
16391
|
const rl = createInterface({
|
|
14744
|
-
input:
|
|
16392
|
+
input: createReadStream2(path32),
|
|
14745
16393
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
14746
16394
|
});
|
|
14747
16395
|
for await (const line of rl) {
|
|
@@ -14762,7 +16410,7 @@ async function fileLogs(path28, lines, filterRe, stream) {
|
|
|
14762
16410
|
if (parsed) entries.push(parsed);
|
|
14763
16411
|
}
|
|
14764
16412
|
return {
|
|
14765
|
-
source:
|
|
16413
|
+
source: path32,
|
|
14766
16414
|
entries,
|
|
14767
16415
|
total: entries.length,
|
|
14768
16416
|
truncated: totalLines > effLines,
|
|
@@ -14916,7 +16564,7 @@ var outdatedTool = {
|
|
|
14916
16564
|
}
|
|
14917
16565
|
};
|
|
14918
16566
|
function runOutdated(manager, args, cwd, signal) {
|
|
14919
|
-
return new Promise((
|
|
16567
|
+
return new Promise((resolve14) => {
|
|
14920
16568
|
let stdout = "";
|
|
14921
16569
|
let stderr = "";
|
|
14922
16570
|
const MAX = 1e5;
|
|
@@ -14941,10 +16589,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
14941
16589
|
});
|
|
14942
16590
|
child.on("close", (code) => {
|
|
14943
16591
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
14944
|
-
|
|
16592
|
+
resolve14(result);
|
|
14945
16593
|
});
|
|
14946
16594
|
child.on("error", (e) => {
|
|
14947
|
-
|
|
16595
|
+
resolve14({
|
|
14948
16596
|
exit_code: 1,
|
|
14949
16597
|
packages: [],
|
|
14950
16598
|
total: 0,
|
|
@@ -14954,9 +16602,9 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
14954
16602
|
});
|
|
14955
16603
|
});
|
|
14956
16604
|
}
|
|
14957
|
-
function parseOutdatedOutput(
|
|
16605
|
+
function parseOutdatedOutput(json2, exitCode) {
|
|
14958
16606
|
const packages = [];
|
|
14959
|
-
if (!
|
|
16607
|
+
if (!json2) {
|
|
14960
16608
|
return {
|
|
14961
16609
|
exit_code: exitCode,
|
|
14962
16610
|
packages: [],
|
|
@@ -14966,7 +16614,7 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
14966
16614
|
};
|
|
14967
16615
|
}
|
|
14968
16616
|
try {
|
|
14969
|
-
const data = JSON.parse(
|
|
16617
|
+
const data = JSON.parse(json2);
|
|
14970
16618
|
for (const name of Object.keys(data)) {
|
|
14971
16619
|
const info = data[name];
|
|
14972
16620
|
packages.push({
|
|
@@ -14984,17 +16632,17 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
14984
16632
|
exit_code: exitCode,
|
|
14985
16633
|
packages,
|
|
14986
16634
|
total: packages.length,
|
|
14987
|
-
output:
|
|
14988
|
-
truncated:
|
|
16635
|
+
output: json2,
|
|
16636
|
+
truncated: json2.length >= 1e5
|
|
14989
16637
|
};
|
|
14990
16638
|
}
|
|
14991
16639
|
|
|
14992
16640
|
// src/patch.ts
|
|
14993
16641
|
init_util();
|
|
14994
16642
|
import { spawn as spawn13 } from "node:child_process";
|
|
14995
|
-
import * as
|
|
16643
|
+
import * as fs23 from "node:fs/promises";
|
|
14996
16644
|
import * as os7 from "node:os";
|
|
14997
|
-
import * as
|
|
16645
|
+
import * as path26 from "node:path";
|
|
14998
16646
|
import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
|
|
14999
16647
|
var patchTool = {
|
|
15000
16648
|
name: "patch",
|
|
@@ -15030,9 +16678,9 @@ var patchTool = {
|
|
|
15030
16678
|
for (const t of targets) {
|
|
15031
16679
|
const stripped = stripPathComponents(t, strip);
|
|
15032
16680
|
if (!stripped) continue;
|
|
15033
|
-
const candidate =
|
|
15034
|
-
const rel =
|
|
15035
|
-
if (rel.startsWith("..") ||
|
|
16681
|
+
const candidate = path26.resolve(dir, stripped);
|
|
16682
|
+
const rel = path26.relative(ctx.projectRoot, candidate);
|
|
16683
|
+
if (rel.startsWith("..") || path26.isAbsolute(rel)) {
|
|
15036
16684
|
return {
|
|
15037
16685
|
applied: 0,
|
|
15038
16686
|
rejected: 1,
|
|
@@ -15049,12 +16697,12 @@ var patchTool = {
|
|
|
15049
16697
|
beforeContents.set(target, await readTextForTracking(target));
|
|
15050
16698
|
}
|
|
15051
16699
|
}
|
|
15052
|
-
const tmpDir = await
|
|
16700
|
+
const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
|
|
15053
16701
|
try {
|
|
15054
|
-
await
|
|
16702
|
+
await fs23.chmod(tmpDir, 448).catch(() => {
|
|
15055
16703
|
});
|
|
15056
|
-
const patchFile =
|
|
15057
|
-
await
|
|
16704
|
+
const patchFile = path26.join(tmpDir, "in.diff");
|
|
16705
|
+
await fs23.writeFile(patchFile, input.patch, { mode: 384 });
|
|
15058
16706
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
15059
16707
|
const result = await runPatch(args, dir, opts.signal);
|
|
15060
16708
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -15072,8 +16720,8 @@ var patchTool = {
|
|
|
15072
16720
|
const before = beforeContents.get(target) ?? null;
|
|
15073
16721
|
const after = await readTextForTracking(target);
|
|
15074
16722
|
if (after === null || after === before) continue;
|
|
15075
|
-
const
|
|
15076
|
-
if (
|
|
16723
|
+
const stat17 = await fs23.stat(target).catch(() => null);
|
|
16724
|
+
if (stat17) ctx.recordRead?.(target, stat17.mtimeMs, "write", sha256hex(after));
|
|
15077
16725
|
ctx.session?.recordFileChange?.({
|
|
15078
16726
|
path: target,
|
|
15079
16727
|
action: before === null ? "created" : "modified",
|
|
@@ -15090,7 +16738,7 @@ var patchTool = {
|
|
|
15090
16738
|
message: result.stdout || "patch applied"
|
|
15091
16739
|
};
|
|
15092
16740
|
} finally {
|
|
15093
|
-
await
|
|
16741
|
+
await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
15094
16742
|
});
|
|
15095
16743
|
}
|
|
15096
16744
|
}
|
|
@@ -15098,9 +16746,9 @@ var patchTool = {
|
|
|
15098
16746
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
15099
16747
|
async function readTextForTracking(absPath) {
|
|
15100
16748
|
try {
|
|
15101
|
-
const
|
|
15102
|
-
if (!
|
|
15103
|
-
const buf = await
|
|
16749
|
+
const stat17 = await fs23.stat(absPath);
|
|
16750
|
+
if (!stat17.isFile() || stat17.size > MAX_TRACKING_BYTES) return null;
|
|
16751
|
+
const buf = await fs23.readFile(absPath);
|
|
15104
16752
|
if (buf.includes(0)) return null;
|
|
15105
16753
|
return buf.toString("utf8");
|
|
15106
16754
|
} catch {
|
|
@@ -15125,7 +16773,7 @@ function stripPathComponents(p, strip) {
|
|
|
15125
16773
|
return parts.slice(strip).join("/");
|
|
15126
16774
|
}
|
|
15127
16775
|
function runPatch(args, cwd, signal) {
|
|
15128
|
-
return new Promise((
|
|
16776
|
+
return new Promise((resolve14) => {
|
|
15129
16777
|
let stdout = "";
|
|
15130
16778
|
let stderr = "";
|
|
15131
16779
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -15136,8 +16784,8 @@ function runPatch(args, cwd, signal) {
|
|
|
15136
16784
|
child.stderr?.on("data", (c) => {
|
|
15137
16785
|
stderr += c.toString();
|
|
15138
16786
|
});
|
|
15139
|
-
child.on("close", (code) =>
|
|
15140
|
-
child.on("error", (e) =>
|
|
16787
|
+
child.on("close", (code) => resolve14({ exitCode: code ?? 1, stdout, stderr }));
|
|
16788
|
+
child.on("error", (e) => resolve14({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
15141
16789
|
});
|
|
15142
16790
|
}
|
|
15143
16791
|
function extractPatchedFiles(output) {
|
|
@@ -15414,13 +17062,13 @@ ${formatTaskList(taskFile.tasks)}`
|
|
|
15414
17062
|
}
|
|
15415
17063
|
};
|
|
15416
17064
|
function mkResult(plan, ok, message, todos) {
|
|
15417
|
-
const
|
|
17065
|
+
const open3 = plan.items.filter((i) => i.status !== "done").length;
|
|
15418
17066
|
const result = {
|
|
15419
17067
|
ok,
|
|
15420
17068
|
message,
|
|
15421
17069
|
plan: formatPlan(plan),
|
|
15422
17070
|
count: plan.items.length,
|
|
15423
|
-
open:
|
|
17071
|
+
open: open3
|
|
15424
17072
|
};
|
|
15425
17073
|
if (todos !== void 0) result.todos = todos;
|
|
15426
17074
|
return result;
|
|
@@ -15428,7 +17076,7 @@ function mkResult(plan, ok, message, todos) {
|
|
|
15428
17076
|
|
|
15429
17077
|
// src/read.ts
|
|
15430
17078
|
init_util();
|
|
15431
|
-
import * as
|
|
17079
|
+
import * as fs24 from "node:fs/promises";
|
|
15432
17080
|
import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
|
|
15433
17081
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
15434
17082
|
var readTool = {
|
|
@@ -15477,9 +17125,9 @@ var readTool = {
|
|
|
15477
17125
|
});
|
|
15478
17126
|
}
|
|
15479
17127
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15480
|
-
let
|
|
17128
|
+
let stat17;
|
|
15481
17129
|
try {
|
|
15482
|
-
|
|
17130
|
+
stat17 = await fs24.stat(absPath);
|
|
15483
17131
|
} catch (err) {
|
|
15484
17132
|
const code = err.code;
|
|
15485
17133
|
if (code === "ENOENT") {
|
|
@@ -15498,7 +17146,7 @@ var readTool = {
|
|
|
15498
17146
|
cause: err
|
|
15499
17147
|
});
|
|
15500
17148
|
}
|
|
15501
|
-
if (!
|
|
17149
|
+
if (!stat17.isFile()) {
|
|
15502
17150
|
throw new FsError({
|
|
15503
17151
|
message: `read: "${input.path}" is not a regular file`,
|
|
15504
17152
|
code: "FS_READ_FAILED",
|
|
@@ -15506,22 +17154,22 @@ var readTool = {
|
|
|
15506
17154
|
context: { reason: "not-a-regular-file" }
|
|
15507
17155
|
});
|
|
15508
17156
|
}
|
|
15509
|
-
if (
|
|
17157
|
+
if (stat17.size > MAX_BYTES2) {
|
|
15510
17158
|
throw new FsError({
|
|
15511
|
-
message: `read: file too large (${
|
|
17159
|
+
message: `read: file too large (${stat17.size} bytes, limit ${MAX_BYTES2})`,
|
|
15512
17160
|
code: "FS_READ_FAILED",
|
|
15513
17161
|
path: absPath,
|
|
15514
|
-
context: { size:
|
|
17162
|
+
context: { size: stat17.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
15515
17163
|
});
|
|
15516
17164
|
}
|
|
15517
17165
|
const offset = Math.max(1, input.offset ?? 1);
|
|
15518
17166
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
15519
17167
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
15520
17168
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
15521
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
15522
|
-
ctx.recordRead(absPath,
|
|
17169
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
|
|
17170
|
+
ctx.recordRead(absPath, stat17.mtimeMs);
|
|
15523
17171
|
return {
|
|
15524
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
17172
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat17.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
15525
17173
|
total_lines: prior.totalLines,
|
|
15526
17174
|
encoding: "utf8",
|
|
15527
17175
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -15529,7 +17177,7 @@ var readTool = {
|
|
|
15529
17177
|
note: "Repeated read suppressed to save tokens."
|
|
15530
17178
|
};
|
|
15531
17179
|
}
|
|
15532
|
-
const buf = await
|
|
17180
|
+
const buf = await fs24.readFile(absPath);
|
|
15533
17181
|
if (isBinaryBuffer(buf)) {
|
|
15534
17182
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
15535
17183
|
}
|
|
@@ -15538,10 +17186,10 @@ var readTool = {
|
|
|
15538
17186
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
15539
17187
|
const total = allLines.length;
|
|
15540
17188
|
if (input.mode === "summary") {
|
|
15541
|
-
ctx.recordRead(absPath,
|
|
15542
|
-
rememberReadRange(ctx, absPath,
|
|
17189
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17190
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
|
|
15543
17191
|
return {
|
|
15544
|
-
text: summarizeFile(input.path,
|
|
17192
|
+
text: summarizeFile(input.path, stat17.size, allLines),
|
|
15545
17193
|
total_lines: total,
|
|
15546
17194
|
encoding: "utf8",
|
|
15547
17195
|
truncated: total > 200,
|
|
@@ -15549,13 +17197,13 @@ var readTool = {
|
|
|
15549
17197
|
};
|
|
15550
17198
|
}
|
|
15551
17199
|
if (limit === 0) {
|
|
15552
|
-
ctx.recordRead(absPath,
|
|
15553
|
-
rememberReadRange(ctx, absPath,
|
|
17200
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17201
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
|
|
15554
17202
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
15555
17203
|
}
|
|
15556
17204
|
if (offset > total) {
|
|
15557
|
-
ctx.recordRead(absPath,
|
|
15558
|
-
rememberReadRange(ctx, absPath,
|
|
17205
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17206
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
|
|
15559
17207
|
return {
|
|
15560
17208
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
15561
17209
|
total_lines: total,
|
|
@@ -15567,8 +17215,8 @@ var readTool = {
|
|
|
15567
17215
|
const truncated = offset - 1 + slice.length < total;
|
|
15568
17216
|
const width = String(offset + slice.length - 1).length;
|
|
15569
17217
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
15570
|
-
ctx.recordRead(absPath,
|
|
15571
|
-
rememberReadRange(ctx, absPath,
|
|
17218
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17219
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
|
|
15572
17220
|
return {
|
|
15573
17221
|
text: numbered,
|
|
15574
17222
|
total_lines: total,
|
|
@@ -15637,10 +17285,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
15637
17285
|
// src/replace.ts
|
|
15638
17286
|
import { expectDefined as expectDefined8 } from "@wrongstack/core";
|
|
15639
17287
|
import { spawn as spawn14 } from "node:child_process";
|
|
15640
|
-
import * as
|
|
15641
|
-
import * as
|
|
17288
|
+
import * as fs25 from "node:fs/promises";
|
|
17289
|
+
import * as path27 from "node:path";
|
|
15642
17290
|
import {
|
|
15643
|
-
atomicWrite as
|
|
17291
|
+
atomicWrite as atomicWrite3,
|
|
15644
17292
|
buildChildEnv as buildChildEnv9,
|
|
15645
17293
|
compileGlob as compileGlob5,
|
|
15646
17294
|
detectNewlineStyle as detectNewlineStyle2,
|
|
@@ -15711,11 +17359,11 @@ var replaceTool = {
|
|
|
15711
17359
|
const dryRun = input.dry_run ?? true;
|
|
15712
17360
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
15713
17361
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
15714
|
-
const realRoot = await
|
|
17362
|
+
const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
15715
17363
|
const results = [];
|
|
15716
17364
|
let totalReplacements = 0;
|
|
15717
17365
|
for (const absPath of fileList) {
|
|
15718
|
-
const lstat2 = await
|
|
17366
|
+
const lstat2 = await fs25.lstat(absPath).catch((err) => {
|
|
15719
17367
|
if (err.code === "ENOENT") return null;
|
|
15720
17368
|
throw err;
|
|
15721
17369
|
});
|
|
@@ -15723,17 +17371,17 @@ var replaceTool = {
|
|
|
15723
17371
|
if (lstat2.isSymbolicLink()) continue;
|
|
15724
17372
|
let realPath;
|
|
15725
17373
|
try {
|
|
15726
|
-
realPath = await
|
|
17374
|
+
realPath = await fs25.realpath(absPath);
|
|
15727
17375
|
} catch {
|
|
15728
17376
|
continue;
|
|
15729
17377
|
}
|
|
15730
|
-
const rel =
|
|
15731
|
-
if (rel.startsWith("..") ||
|
|
15732
|
-
const
|
|
15733
|
-
if (!
|
|
17378
|
+
const rel = path27.relative(realRoot, realPath);
|
|
17379
|
+
if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
|
|
17380
|
+
const stat17 = await fs25.stat(realPath).catch(() => null);
|
|
17381
|
+
if (!stat17?.isFile()) continue;
|
|
15734
17382
|
let content;
|
|
15735
17383
|
try {
|
|
15736
|
-
const buf = await
|
|
17384
|
+
const buf = await fs25.readFile(realPath);
|
|
15737
17385
|
if (isBinaryBuffer(buf)) continue;
|
|
15738
17386
|
content = buf.toString("utf8");
|
|
15739
17387
|
} catch {
|
|
@@ -15755,8 +17403,8 @@ var replaceTool = {
|
|
|
15755
17403
|
totalReplacements += count;
|
|
15756
17404
|
if (!dryRun) {
|
|
15757
17405
|
const newContent = toStyle2(newContentLf, style);
|
|
15758
|
-
await
|
|
15759
|
-
const written = await
|
|
17406
|
+
await atomicWrite3(realPath, newContent, { mode: stat17.mode & 511 });
|
|
17407
|
+
const written = await fs25.stat(realPath).catch(() => null);
|
|
15760
17408
|
if (written) {
|
|
15761
17409
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
15762
17410
|
}
|
|
@@ -15795,8 +17443,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
15795
17443
|
const resolved = [];
|
|
15796
17444
|
for (const p of parts) {
|
|
15797
17445
|
const absPath = safeResolve(p, ctx);
|
|
15798
|
-
const
|
|
15799
|
-
if (
|
|
17446
|
+
const stat17 = await fs25.stat(absPath).catch(() => null);
|
|
17447
|
+
if (stat17?.isFile()) {
|
|
15800
17448
|
resolved.push(absPath);
|
|
15801
17449
|
}
|
|
15802
17450
|
}
|
|
@@ -15814,13 +17462,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
15814
17462
|
return await globNative(pattern, base, extraGlob);
|
|
15815
17463
|
}
|
|
15816
17464
|
function checkRg() {
|
|
15817
|
-
return new Promise((
|
|
17465
|
+
return new Promise((resolve14) => {
|
|
15818
17466
|
try {
|
|
15819
17467
|
const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
|
|
15820
|
-
p.on("error", () =>
|
|
15821
|
-
p.on("close", (code) =>
|
|
17468
|
+
p.on("error", () => resolve14(false));
|
|
17469
|
+
p.on("close", (code) => resolve14(code === 0));
|
|
15822
17470
|
} catch {
|
|
15823
|
-
|
|
17471
|
+
resolve14(false);
|
|
15824
17472
|
}
|
|
15825
17473
|
});
|
|
15826
17474
|
}
|
|
@@ -15837,10 +17485,10 @@ function spawnRgFind(pattern, base) {
|
|
|
15837
17485
|
buf += chunk.toString();
|
|
15838
17486
|
});
|
|
15839
17487
|
return {
|
|
15840
|
-
promise: new Promise((
|
|
17488
|
+
promise: new Promise((resolve14, reject) => {
|
|
15841
17489
|
child.on("error", reject);
|
|
15842
17490
|
child.on("close", () => {
|
|
15843
|
-
|
|
17491
|
+
resolve14(buf.split("\n").filter(Boolean));
|
|
15844
17492
|
});
|
|
15845
17493
|
})
|
|
15846
17494
|
};
|
|
@@ -15851,16 +17499,16 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
15851
17499
|
const walk = async (dir) => {
|
|
15852
17500
|
let entries;
|
|
15853
17501
|
try {
|
|
15854
|
-
entries = await
|
|
17502
|
+
entries = await fs25.readdir(dir, { withFileTypes: true });
|
|
15855
17503
|
} catch {
|
|
15856
17504
|
return;
|
|
15857
17505
|
}
|
|
15858
17506
|
for (const e of entries) {
|
|
15859
17507
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
15860
|
-
const full =
|
|
17508
|
+
const full = path27.join(dir, e.name);
|
|
15861
17509
|
try {
|
|
15862
|
-
const
|
|
15863
|
-
if (
|
|
17510
|
+
const stat17 = await fs25.lstat(full);
|
|
17511
|
+
if (stat17.isSymbolicLink()) continue;
|
|
15864
17512
|
} catch {
|
|
15865
17513
|
continue;
|
|
15866
17514
|
}
|
|
@@ -15883,9 +17531,9 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
15883
17531
|
|
|
15884
17532
|
// src/scaffold.ts
|
|
15885
17533
|
init_util();
|
|
15886
|
-
import * as
|
|
15887
|
-
import * as
|
|
15888
|
-
import { atomicWrite as
|
|
17534
|
+
import * as fs26 from "node:fs/promises";
|
|
17535
|
+
import * as path28 from "node:path";
|
|
17536
|
+
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
15889
17537
|
var BUILT_IN_TEMPLATES = {
|
|
15890
17538
|
"npm-package": {
|
|
15891
17539
|
description: "Basic npm package with ESM",
|
|
@@ -16034,17 +17682,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
16034
17682
|
let filesCreated = 0;
|
|
16035
17683
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
16036
17684
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
16037
|
-
const joinedPath =
|
|
16038
|
-
const root =
|
|
16039
|
-
const target =
|
|
16040
|
-
const rel =
|
|
16041
|
-
if (rel.startsWith("..") ||
|
|
17685
|
+
const joinedPath = path28.join(cwd, resolvedPath);
|
|
17686
|
+
const root = path28.resolve(ctx.projectRoot);
|
|
17687
|
+
const target = path28.resolve(joinedPath);
|
|
17688
|
+
const rel = path28.relative(root, target);
|
|
17689
|
+
if (rel.startsWith("..") || path28.isAbsolute(rel)) {
|
|
16042
17690
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
16043
17691
|
}
|
|
16044
17692
|
const fullPath = target;
|
|
16045
17693
|
if (!dryRun) {
|
|
16046
|
-
await
|
|
16047
|
-
await
|
|
17694
|
+
await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
|
|
17695
|
+
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
16048
17696
|
}
|
|
16049
17697
|
files.push(resolvedPath);
|
|
16050
17698
|
filesCreated++;
|
|
@@ -16078,6 +17726,7 @@ var DEFAULT_NUM = 10;
|
|
|
16078
17726
|
var MAX_RESULTS = 50;
|
|
16079
17727
|
var TIMEOUT_MS3 = 15e3;
|
|
16080
17728
|
var CACHE_TTL_MS = 3e5;
|
|
17729
|
+
var CACHE_MAX_ENTRIES = 200;
|
|
16081
17730
|
var cache = /* @__PURE__ */ new Map();
|
|
16082
17731
|
var searchTool = {
|
|
16083
17732
|
name: "search",
|
|
@@ -16199,7 +17848,7 @@ var searchTool = {
|
|
|
16199
17848
|
}
|
|
16200
17849
|
const finalResults = ranked.slice(0, num);
|
|
16201
17850
|
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
16202
|
-
|
|
17851
|
+
pruneCacheEntries();
|
|
16203
17852
|
yield {
|
|
16204
17853
|
type: "partial_output",
|
|
16205
17854
|
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
@@ -16221,11 +17870,16 @@ var searchTool = {
|
|
|
16221
17870
|
};
|
|
16222
17871
|
}
|
|
16223
17872
|
};
|
|
16224
|
-
function
|
|
17873
|
+
function pruneCacheEntries() {
|
|
16225
17874
|
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
16226
17875
|
for (const [key, entry] of cache.entries()) {
|
|
16227
17876
|
if (entry.timestamp < cutoff) cache.delete(key);
|
|
16228
17877
|
}
|
|
17878
|
+
while (cache.size > CACHE_MAX_ENTRIES) {
|
|
17879
|
+
const oldestKey = cache.keys().next().value;
|
|
17880
|
+
if (oldestKey === void 0) break;
|
|
17881
|
+
cache.delete(oldestKey);
|
|
17882
|
+
}
|
|
16229
17883
|
}
|
|
16230
17884
|
function rankSearchResults(results, query) {
|
|
16231
17885
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
@@ -16454,7 +18108,7 @@ function decodeHtmlEntities(text) {
|
|
|
16454
18108
|
}
|
|
16455
18109
|
|
|
16456
18110
|
// src/set-working-dir.ts
|
|
16457
|
-
import * as
|
|
18111
|
+
import * as fs27 from "node:fs/promises";
|
|
16458
18112
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
16459
18113
|
var setWorkingDirTool = {
|
|
16460
18114
|
name: "set_working_dir",
|
|
@@ -16493,7 +18147,7 @@ var setWorkingDirTool = {
|
|
|
16493
18147
|
};
|
|
16494
18148
|
}
|
|
16495
18149
|
try {
|
|
16496
|
-
await
|
|
18150
|
+
await fs27.access(resolved);
|
|
16497
18151
|
} catch {
|
|
16498
18152
|
try {
|
|
16499
18153
|
ctx.setWorkingDir(previous);
|
|
@@ -16893,7 +18547,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
16893
18547
|
init_spawn_stream();
|
|
16894
18548
|
init_util();
|
|
16895
18549
|
init_legacy_bridge();
|
|
16896
|
-
import * as
|
|
18550
|
+
import * as path29 from "node:path";
|
|
16897
18551
|
var testTool = {
|
|
16898
18552
|
name: "test",
|
|
16899
18553
|
category: "Code Quality",
|
|
@@ -16996,11 +18650,11 @@ var testTool = {
|
|
|
16996
18650
|
}
|
|
16997
18651
|
};
|
|
16998
18652
|
async function detectRunner(cwd) {
|
|
16999
|
-
const { stat:
|
|
18653
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
17000
18654
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
17001
18655
|
for (const f of candidates) {
|
|
17002
18656
|
try {
|
|
17003
|
-
await
|
|
18657
|
+
await stat17(path29.join(cwd, f));
|
|
17004
18658
|
if (f.includes("vitest")) return "vitest";
|
|
17005
18659
|
if (f.includes("jest")) return "jest";
|
|
17006
18660
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -17466,8 +19120,8 @@ var toolUseTool = {
|
|
|
17466
19120
|
// src/tree.ts
|
|
17467
19121
|
init_util();
|
|
17468
19122
|
import { expectDefined as expectDefined10 } from "@wrongstack/core";
|
|
17469
|
-
import * as
|
|
17470
|
-
import * as
|
|
19123
|
+
import * as fs28 from "node:fs/promises";
|
|
19124
|
+
import * as path30 from "node:path";
|
|
17471
19125
|
var DEFAULT_IGNORE5 = [
|
|
17472
19126
|
"node_modules",
|
|
17473
19127
|
".git",
|
|
@@ -17608,7 +19262,7 @@ var treeTool = {
|
|
|
17608
19262
|
}
|
|
17609
19263
|
};
|
|
17610
19264
|
async function walkDir(dir, depth, opts) {
|
|
17611
|
-
const entries = await
|
|
19265
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
17612
19266
|
const filtered = entries.filter((e) => {
|
|
17613
19267
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
17614
19268
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -17638,7 +19292,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17638
19292
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
17639
19293
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
17640
19294
|
const childPrefix = opts.prefix + connector;
|
|
17641
|
-
await walkDir(
|
|
19295
|
+
await walkDir(path30.join(dir, entry.name), depth + 1, {
|
|
17642
19296
|
...opts,
|
|
17643
19297
|
prefix: childPrefix,
|
|
17644
19298
|
isLast
|
|
@@ -17651,7 +19305,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17651
19305
|
init_spawn_stream();
|
|
17652
19306
|
init_util();
|
|
17653
19307
|
init_legacy_bridge();
|
|
17654
|
-
import * as
|
|
19308
|
+
import * as path31 from "node:path";
|
|
17655
19309
|
var typecheckTool = {
|
|
17656
19310
|
name: "typecheck",
|
|
17657
19311
|
category: "Code Quality",
|
|
@@ -17750,12 +19404,12 @@ var typecheckTool = {
|
|
|
17750
19404
|
}
|
|
17751
19405
|
};
|
|
17752
19406
|
async function findTsConfig(cwd) {
|
|
17753
|
-
const { stat:
|
|
19407
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
17754
19408
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
17755
19409
|
for (const f of candidates) {
|
|
17756
19410
|
try {
|
|
17757
|
-
const s = await
|
|
17758
|
-
if (s.isFile()) return
|
|
19411
|
+
const s = await stat17(path31.join(cwd, f));
|
|
19412
|
+
if (s.isFile()) return path31.join(cwd, f);
|
|
17759
19413
|
} catch {
|
|
17760
19414
|
}
|
|
17761
19415
|
}
|
|
@@ -17763,8 +19417,8 @@ async function findTsConfig(cwd) {
|
|
|
17763
19417
|
}
|
|
17764
19418
|
|
|
17765
19419
|
// src/write.ts
|
|
17766
|
-
import * as
|
|
17767
|
-
import { atomicWrite as
|
|
19420
|
+
import * as fs29 from "node:fs/promises";
|
|
19421
|
+
import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
|
|
17768
19422
|
init_util();
|
|
17769
19423
|
var writeTool = {
|
|
17770
19424
|
name: "write",
|
|
@@ -17828,14 +19482,14 @@ async function prepareWrite(input, ctx) {
|
|
|
17828
19482
|
let existed = false;
|
|
17829
19483
|
let prev = "";
|
|
17830
19484
|
try {
|
|
17831
|
-
const
|
|
17832
|
-
existed =
|
|
19485
|
+
const stat17 = await fs29.stat(absPath);
|
|
19486
|
+
existed = stat17.isFile();
|
|
17833
19487
|
if (existed) {
|
|
17834
19488
|
if (!ctx.hasRead(absPath)) {
|
|
17835
|
-
prev = await
|
|
17836
|
-
ctx.recordRead(absPath,
|
|
19489
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
19490
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "write", sha256hex(prev));
|
|
17837
19491
|
} else {
|
|
17838
|
-
prev = await
|
|
19492
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
17839
19493
|
}
|
|
17840
19494
|
}
|
|
17841
19495
|
} catch (err) {
|
|
@@ -17847,11 +19501,11 @@ async function prepareWrite(input, ctx) {
|
|
|
17847
19501
|
}
|
|
17848
19502
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
17849
19503
|
signal?.throwIfAborted();
|
|
17850
|
-
await
|
|
19504
|
+
await atomicWrite5(prepared.absPath, input.content);
|
|
17851
19505
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
17852
19506
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
17853
|
-
const
|
|
17854
|
-
ctx.recordRead(prepared.absPath,
|
|
19507
|
+
const stat17 = await fs29.stat(prepared.absPath);
|
|
19508
|
+
ctx.recordRead(prepared.absPath, stat17.mtimeMs, "write", sha256hex(input.content));
|
|
17855
19509
|
ctx.session.recordFileChange({
|
|
17856
19510
|
path: prepared.absPath,
|
|
17857
19511
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -17875,7 +19529,27 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
17875
19529
|
}
|
|
17876
19530
|
|
|
17877
19531
|
// src/builtin.ts
|
|
19532
|
+
var OPTIONAL_TOOLS = [
|
|
19533
|
+
...browserTools,
|
|
19534
|
+
e2ePlanTool,
|
|
19535
|
+
installTool,
|
|
19536
|
+
auditTool,
|
|
19537
|
+
outdatedTool,
|
|
19538
|
+
logsTool,
|
|
19539
|
+
documentTool,
|
|
19540
|
+
scaffoldTool,
|
|
19541
|
+
toolSearchTool,
|
|
19542
|
+
toolUseTool,
|
|
19543
|
+
batchToolUseTool,
|
|
19544
|
+
toolHelpTool,
|
|
19545
|
+
codebaseIndexTool,
|
|
19546
|
+
codebaseSearchTool,
|
|
19547
|
+
codebaseStatsTool,
|
|
19548
|
+
setWorkingDirTool
|
|
19549
|
+
];
|
|
17878
19550
|
var builtinTools = [
|
|
19551
|
+
...browserTools,
|
|
19552
|
+
e2ePlanTool,
|
|
17879
19553
|
readTool,
|
|
17880
19554
|
writeTool,
|
|
17881
19555
|
editTool,
|
|
@@ -17922,7 +19596,7 @@ var builtinTools = [
|
|
|
17922
19596
|
// src/pack.ts
|
|
17923
19597
|
var builtinToolsPack = {
|
|
17924
19598
|
name: "builtin-tools",
|
|
17925
|
-
description: "The complete set of built-in tools that ship with WrongStack. Covers filesystem
|
|
19599
|
+
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.",
|
|
17926
19600
|
tools: builtinTools
|
|
17927
19601
|
};
|
|
17928
19602
|
export {
|