@wrongstack/tools 0.285.0 → 0.287.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 +21 -2
- package/dist/_edit-match.d.ts.map +1 -1
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +26 -8
- package/dist/batch-tool-use.js.map +2 -2
- 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 +2609 -525
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index.js +190 -96
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +188 -94
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/e2e.d.ts +56 -0
- package/dist/e2e.d.ts.map +1 -0
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +36 -12
- package/dist/edit.js.map +3 -3
- package/dist/exec.js +1 -1
- package/dist/exec.js.map +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2895 -511
- package/dist/index.js.map +4 -4
- package/dist/kanban.d.ts +7 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +158 -19
- package/dist/kanban.js.map +4 -4
- package/dist/next-steps.d.ts +6 -2
- package/dist/next-steps.d.ts.map +1 -1
- package/dist/next-steps.js +3 -3
- package/dist/next-steps.js.map +2 -2
- package/dist/pack.js +2625 -525
- package/dist/pack.js.map +4 -4
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +154 -4
- package/dist/plan.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/session-kanban.d.ts +38 -0
- package/dist/session-kanban.d.ts.map +1 -0
- package/dist/session-kanban.js +489 -0
- package/dist/session-kanban.js.map +7 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +163 -4
- package/dist/task.js.map +4 -4
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +155 -7
- package/dist/todo.js.map +4 -4
- package/dist/tool-icon-map.d.ts +2 -1
- package/dist/tool-icon-map.d.ts.map +1 -1
- package/dist/tool-icons.d.ts.map +1 -1
- package/dist/tool-icons.js +7 -1
- package/dist/tool-icons.js.map +2 -2
- package/package.json +12 -3
package/dist/builtin.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 = () => {
|
|
@@ -6054,6 +6054,9 @@ ${hint}` : ""),
|
|
|
6054
6054
|
};
|
|
6055
6055
|
|
|
6056
6056
|
// src/batch-tool-use.ts
|
|
6057
|
+
import {
|
|
6058
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
6059
|
+
} from "@wrongstack/core";
|
|
6057
6060
|
var batchToolUseTool = {
|
|
6058
6061
|
name: "batch_tool_use",
|
|
6059
6062
|
category: "Meta",
|
|
@@ -6090,7 +6093,7 @@ var batchToolUseTool = {
|
|
|
6090
6093
|
},
|
|
6091
6094
|
required: ["calls"]
|
|
6092
6095
|
},
|
|
6093
|
-
async execute(input, ctx,
|
|
6096
|
+
async execute(input, ctx, _opts) {
|
|
6094
6097
|
if (!input?.calls || input.calls.length === 0) {
|
|
6095
6098
|
return {
|
|
6096
6099
|
results: [],
|
|
@@ -6100,18 +6103,33 @@ var batchToolUseTool = {
|
|
|
6100
6103
|
stop_on_error: false
|
|
6101
6104
|
};
|
|
6102
6105
|
}
|
|
6106
|
+
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
6107
|
+
if (typeof governedExecute !== "function") {
|
|
6108
|
+
return {
|
|
6109
|
+
results: input.calls.map((call) => ({
|
|
6110
|
+
tool: call.tool,
|
|
6111
|
+
success: false,
|
|
6112
|
+
error: "governed nested execution is unavailable; call the tool directly",
|
|
6113
|
+
executionMs: 0
|
|
6114
|
+
})),
|
|
6115
|
+
total: input.calls.length,
|
|
6116
|
+
succeeded: 0,
|
|
6117
|
+
failed: input.calls.length,
|
|
6118
|
+
stop_on_error: input.stop_on_error ?? false
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6103
6121
|
const results = [];
|
|
6104
6122
|
let succeeded = 0;
|
|
6105
6123
|
let failed = 0;
|
|
6106
6124
|
if (input.parallel !== false) {
|
|
6107
|
-
const promises = input.calls.map(async (call) => executeSingle(call, ctx,
|
|
6125
|
+
const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
|
|
6108
6126
|
const allResults = await Promise.all(promises);
|
|
6109
6127
|
results.push(...allResults);
|
|
6110
6128
|
succeeded = allResults.filter((r) => r.success).length;
|
|
6111
6129
|
failed = allResults.filter((r) => !r.success).length;
|
|
6112
6130
|
} else {
|
|
6113
6131
|
for (const call of input.calls) {
|
|
6114
|
-
const result = await executeSingle(call, ctx,
|
|
6132
|
+
const result = await executeSingle(call, ctx, governedExecute);
|
|
6115
6133
|
results.push(result);
|
|
6116
6134
|
if (result.success) {
|
|
6117
6135
|
succeeded++;
|
|
@@ -6130,9 +6148,9 @@ var batchToolUseTool = {
|
|
|
6130
6148
|
};
|
|
6131
6149
|
}
|
|
6132
6150
|
};
|
|
6133
|
-
async function executeSingle(call, ctx,
|
|
6151
|
+
async function executeSingle(call, ctx, governedExecute) {
|
|
6134
6152
|
const start = Date.now();
|
|
6135
|
-
const tool = ctx.tools.find((
|
|
6153
|
+
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
6136
6154
|
if (!tool) {
|
|
6137
6155
|
return {
|
|
6138
6156
|
tool: call.tool,
|
|
@@ -6141,23 +6159,1255 @@ async function executeSingle(call, ctx, opts) {
|
|
|
6141
6159
|
executionMs: Date.now() - start
|
|
6142
6160
|
};
|
|
6143
6161
|
}
|
|
6144
|
-
try {
|
|
6145
|
-
const result = await
|
|
6146
|
-
return {
|
|
6147
|
-
tool: call.tool,
|
|
6148
|
-
success:
|
|
6149
|
-
result,
|
|
6150
|
-
executionMs: Date.now() - start
|
|
6151
|
-
};
|
|
6152
|
-
} catch (e) {
|
|
6153
|
-
return {
|
|
6154
|
-
tool: call.tool,
|
|
6155
|
-
success: false,
|
|
6156
|
-
error: e instanceof Error ? e.message : String(e),
|
|
6157
|
-
executionMs: Date.now() - start
|
|
6158
|
-
};
|
|
6162
|
+
try {
|
|
6163
|
+
const result = await governedExecute(call.tool, call.input);
|
|
6164
|
+
return {
|
|
6165
|
+
tool: call.tool,
|
|
6166
|
+
success: result.success,
|
|
6167
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
6168
|
+
executionMs: Date.now() - start
|
|
6169
|
+
};
|
|
6170
|
+
} catch (e) {
|
|
6171
|
+
return {
|
|
6172
|
+
tool: call.tool,
|
|
6173
|
+
success: false,
|
|
6174
|
+
error: e instanceof Error ? e.message : String(e),
|
|
6175
|
+
executionMs: Date.now() - start
|
|
6176
|
+
};
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6180
|
+
// src/browser/tools.ts
|
|
6181
|
+
import * as path12 from "node:path";
|
|
6182
|
+
import { ToolCapabilities } from "@wrongstack/core";
|
|
6183
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
6184
|
+
|
|
6185
|
+
// src/browser/manager.ts
|
|
6186
|
+
import * as fs8 from "node:fs/promises";
|
|
6187
|
+
import * as path11 from "node:path";
|
|
6188
|
+
import { ulid as ulid2 } from "@wrongstack/core/utils";
|
|
6189
|
+
|
|
6190
|
+
// src/browser/artifacts.ts
|
|
6191
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6192
|
+
import { createReadStream } from "node:fs";
|
|
6193
|
+
import * as fs7 from "node:fs/promises";
|
|
6194
|
+
import * as path10 from "node:path";
|
|
6195
|
+
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
6196
|
+
var BrowserArtifactStore = class {
|
|
6197
|
+
constructor(root) {
|
|
6198
|
+
this.root = root;
|
|
6199
|
+
}
|
|
6200
|
+
root;
|
|
6201
|
+
async write(sessionId, kind, extension, mimeType, content) {
|
|
6202
|
+
const id = ulid();
|
|
6203
|
+
const dir = path10.join(this.root, safeSegment(sessionId));
|
|
6204
|
+
const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
|
|
6205
|
+
await atomicWrite(target, content, { mode: 384 });
|
|
6206
|
+
return this.record(id, sessionId, kind, target, mimeType);
|
|
6207
|
+
}
|
|
6208
|
+
async record(id, sessionId, kind, target, mimeType) {
|
|
6209
|
+
await fs7.chmod(target, 384).catch(() => void 0);
|
|
6210
|
+
const [stat17, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
6211
|
+
const artifact = {
|
|
6212
|
+
id,
|
|
6213
|
+
kind,
|
|
6214
|
+
sensitivity: "sensitive",
|
|
6215
|
+
path: target,
|
|
6216
|
+
mimeType,
|
|
6217
|
+
sizeBytes: stat17.size,
|
|
6218
|
+
sha256,
|
|
6219
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6220
|
+
};
|
|
6221
|
+
const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
|
|
6222
|
+
try {
|
|
6223
|
+
await atomicWrite(
|
|
6224
|
+
metadataPath,
|
|
6225
|
+
`${JSON.stringify({
|
|
6226
|
+
id: artifact.id,
|
|
6227
|
+
sessionId: safeSegment(sessionId),
|
|
6228
|
+
kind: artifact.kind,
|
|
6229
|
+
sensitivity: artifact.sensitivity,
|
|
6230
|
+
mimeType: artifact.mimeType,
|
|
6231
|
+
sizeBytes: artifact.sizeBytes,
|
|
6232
|
+
sha256: artifact.sha256,
|
|
6233
|
+
createdAt: artifact.createdAt
|
|
6234
|
+
})}
|
|
6235
|
+
`,
|
|
6236
|
+
{ mode: 384 }
|
|
6237
|
+
);
|
|
6238
|
+
} catch (error) {
|
|
6239
|
+
await fs7.rm(target, { force: true }).catch(() => void 0);
|
|
6240
|
+
throw error;
|
|
6241
|
+
}
|
|
6242
|
+
return artifact;
|
|
6243
|
+
}
|
|
6244
|
+
pathFor(sessionId, extension) {
|
|
6245
|
+
const id = ulid();
|
|
6246
|
+
return {
|
|
6247
|
+
id,
|
|
6248
|
+
path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
|
|
6249
|
+
};
|
|
6250
|
+
}
|
|
6251
|
+
async describe(id, kind, target, mimeType) {
|
|
6252
|
+
return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
|
|
6253
|
+
}
|
|
6254
|
+
};
|
|
6255
|
+
async function hashFile(target) {
|
|
6256
|
+
const hash = createHash3("sha256");
|
|
6257
|
+
await new Promise((resolve14, reject) => {
|
|
6258
|
+
const stream = createReadStream(target);
|
|
6259
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
6260
|
+
stream.once("error", reject);
|
|
6261
|
+
stream.once("end", resolve14);
|
|
6262
|
+
});
|
|
6263
|
+
return hash.digest("hex");
|
|
6264
|
+
}
|
|
6265
|
+
function safeSegment(value) {
|
|
6266
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
6267
|
+
}
|
|
6268
|
+
|
|
6269
|
+
// src/browser/network-guard-proxy.ts
|
|
6270
|
+
import * as http from "node:http";
|
|
6271
|
+
import * as net2 from "node:net";
|
|
6272
|
+
|
|
6273
|
+
// src/browser/security.ts
|
|
6274
|
+
import * as dns from "node:dns/promises";
|
|
6275
|
+
import * as net from "node:net";
|
|
6276
|
+
import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
6277
|
+
var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
|
|
6278
|
+
async function assertBrowserUrlAllowed(rawUrl, opts) {
|
|
6279
|
+
const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
|
|
6280
|
+
if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6281
|
+
if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
|
|
6282
|
+
await assertNotPrivateHost(url.hostname);
|
|
6283
|
+
}
|
|
6284
|
+
return url;
|
|
6285
|
+
}
|
|
6286
|
+
async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
|
|
6287
|
+
const url = parseBrowserUrl(rawUrl, true);
|
|
6288
|
+
const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
|
|
6289
|
+
const hostname2 = unbracket(url.hostname);
|
|
6290
|
+
const literalFamily = net.isIP(hostname2);
|
|
6291
|
+
if (literalFamily === 4 || literalFamily === 6) {
|
|
6292
|
+
if (!allowPrivate && isPrivateAddress(hostname2, literalFamily)) {
|
|
6293
|
+
throw new Error(`browser: blocked private/loopback address "${hostname2}"`);
|
|
6294
|
+
}
|
|
6295
|
+
return { url, address: hostname2, family: literalFamily };
|
|
6296
|
+
}
|
|
6297
|
+
if ((hostname2 === "localhost" || hostname2.endsWith(".localhost")) && !allowPrivate) {
|
|
6298
|
+
throw new Error("browser: blocked localhost target");
|
|
6299
|
+
}
|
|
6300
|
+
const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
|
|
6301
|
+
let records;
|
|
6302
|
+
try {
|
|
6303
|
+
records = await lookup3(hostname2);
|
|
6304
|
+
} catch (error) {
|
|
6305
|
+
throw new Error(
|
|
6306
|
+
`browser: DNS resolution failed for "${hostname2}": ${error instanceof Error ? error.message : String(error)}`
|
|
6307
|
+
);
|
|
6308
|
+
}
|
|
6309
|
+
if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname2}"`);
|
|
6310
|
+
for (const record of records) {
|
|
6311
|
+
if (record.family !== 4 && record.family !== 6) {
|
|
6312
|
+
throw new Error(`browser: DNS returned an unsupported address family for "${hostname2}"`);
|
|
6313
|
+
}
|
|
6314
|
+
if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
|
|
6315
|
+
throw new Error(`browser: resolved to private address ${record.address}`);
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
const selected = records[0];
|
|
6319
|
+
return { url, address: selected.address, family: selected.family };
|
|
6320
|
+
}
|
|
6321
|
+
function parsePrivateOriginAllowlist(raw) {
|
|
6322
|
+
if (!raw?.trim()) return [];
|
|
6323
|
+
const origins = /* @__PURE__ */ new Set();
|
|
6324
|
+
for (const entry of raw.split(",")) {
|
|
6325
|
+
const candidate = entry.trim();
|
|
6326
|
+
if (!candidate) continue;
|
|
6327
|
+
const url = parseBrowserUrl(candidate, true);
|
|
6328
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
6329
|
+
throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
|
|
6330
|
+
}
|
|
6331
|
+
origins.add(url.origin);
|
|
6332
|
+
}
|
|
6333
|
+
return [...origins];
|
|
6334
|
+
}
|
|
6335
|
+
function parseBrowserUrl(rawUrl, navigation) {
|
|
6336
|
+
let url;
|
|
6337
|
+
try {
|
|
6338
|
+
url = new URL(rawUrl);
|
|
6339
|
+
} catch {
|
|
6340
|
+
throw new Error("browser: url must be an absolute URL");
|
|
6341
|
+
}
|
|
6342
|
+
if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
|
|
6343
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
6344
|
+
throw new Error(`browser: unsupported protocol "${url.protocol}"`);
|
|
6345
|
+
}
|
|
6346
|
+
if (url.username || url.password) {
|
|
6347
|
+
throw new Error("browser: credentials in URLs are not allowed");
|
|
6348
|
+
}
|
|
6349
|
+
return url;
|
|
6350
|
+
}
|
|
6351
|
+
function isAllowedPrivateOrigin(url, origins) {
|
|
6352
|
+
return origins?.includes(url.origin) ?? false;
|
|
6353
|
+
}
|
|
6354
|
+
function isPrivateAddress(address, family) {
|
|
6355
|
+
return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
|
|
6356
|
+
}
|
|
6357
|
+
function unbracket(hostname2) {
|
|
6358
|
+
return hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
6359
|
+
}
|
|
6360
|
+
function safeBrowserUrl(rawUrl) {
|
|
6361
|
+
try {
|
|
6362
|
+
const url = new URL(rawUrl);
|
|
6363
|
+
url.username = "";
|
|
6364
|
+
url.password = "";
|
|
6365
|
+
url.search = "";
|
|
6366
|
+
url.hash = "";
|
|
6367
|
+
return url.toString();
|
|
6368
|
+
} catch {
|
|
6369
|
+
return "about:blank";
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
function redactBrowserText(text) {
|
|
6373
|
+
return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
|
|
6374
|
+
/\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
|
|
6375
|
+
"$1=[REDACTED]"
|
|
6376
|
+
);
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6379
|
+
// src/browser/network-guard-proxy.ts
|
|
6380
|
+
var BrowserNetworkGuardProxy = class {
|
|
6381
|
+
constructor(options = {}) {
|
|
6382
|
+
this.options = options;
|
|
6383
|
+
this.server = http.createServer((request2, response) => {
|
|
6384
|
+
void this.forwardHttp(request2, response);
|
|
6385
|
+
});
|
|
6386
|
+
this.server.on("connect", (request2, socket, head) => {
|
|
6387
|
+
void this.forwardConnect(request2, socket, head);
|
|
6388
|
+
});
|
|
6389
|
+
this.server.on("upgrade", (request2, socket, head) => {
|
|
6390
|
+
void this.forwardUpgrade(request2, socket, head);
|
|
6391
|
+
});
|
|
6392
|
+
this.server.on("connection", (socket) => {
|
|
6393
|
+
this.sockets.add(socket);
|
|
6394
|
+
socket.once("close", () => this.sockets.delete(socket));
|
|
6395
|
+
});
|
|
6396
|
+
this.server.on("clientError", (_error, socket) => {
|
|
6397
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6400
|
+
options;
|
|
6401
|
+
server;
|
|
6402
|
+
sockets = /* @__PURE__ */ new Set();
|
|
6403
|
+
startPromise;
|
|
6404
|
+
url;
|
|
6405
|
+
async start() {
|
|
6406
|
+
if (this.url) return this.url;
|
|
6407
|
+
if (this.startPromise) return this.startPromise;
|
|
6408
|
+
this.startPromise = new Promise((resolve14, reject) => {
|
|
6409
|
+
const onError = (error) => {
|
|
6410
|
+
this.server.off("listening", onListening);
|
|
6411
|
+
reject(error);
|
|
6412
|
+
};
|
|
6413
|
+
const onListening = () => {
|
|
6414
|
+
this.server.off("error", onError);
|
|
6415
|
+
const address = this.server.address();
|
|
6416
|
+
if (!address || typeof address === "string") {
|
|
6417
|
+
reject(new Error("browser: network guard proxy failed to bind"));
|
|
6418
|
+
return;
|
|
6419
|
+
}
|
|
6420
|
+
this.url = `http://127.0.0.1:${address.port}`;
|
|
6421
|
+
resolve14(this.url);
|
|
6422
|
+
};
|
|
6423
|
+
this.server.once("error", onError);
|
|
6424
|
+
this.server.once("listening", onListening);
|
|
6425
|
+
this.server.listen(0, "127.0.0.1");
|
|
6426
|
+
}).finally(() => {
|
|
6427
|
+
this.startPromise = void 0;
|
|
6428
|
+
});
|
|
6429
|
+
return this.startPromise;
|
|
6430
|
+
}
|
|
6431
|
+
async close() {
|
|
6432
|
+
this.url = void 0;
|
|
6433
|
+
for (const socket of this.sockets) socket.destroy();
|
|
6434
|
+
this.sockets.clear();
|
|
6435
|
+
if (!this.server.listening) return;
|
|
6436
|
+
await new Promise((resolve14) => this.server.close(() => resolve14()));
|
|
6437
|
+
}
|
|
6438
|
+
async forwardHttp(request2, response) {
|
|
6439
|
+
try {
|
|
6440
|
+
const target = await this.resolve(request2.url ?? "");
|
|
6441
|
+
if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
|
|
6442
|
+
const headers = sanitizeHeaders(request2.headers);
|
|
6443
|
+
headers.host = target.url.host;
|
|
6444
|
+
const upstream = http.request({
|
|
6445
|
+
host: target.address,
|
|
6446
|
+
family: target.family,
|
|
6447
|
+
port: Number(target.url.port || 80),
|
|
6448
|
+
method: request2.method,
|
|
6449
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6450
|
+
headers,
|
|
6451
|
+
agent: false
|
|
6452
|
+
});
|
|
6453
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
|
|
6454
|
+
upstream.destroy(new Error("browser: upstream connection timed out"));
|
|
6455
|
+
});
|
|
6456
|
+
upstream.on("response", (upstreamResponse) => {
|
|
6457
|
+
response.writeHead(
|
|
6458
|
+
upstreamResponse.statusCode ?? 502,
|
|
6459
|
+
sanitizeHeaders(upstreamResponse.headers)
|
|
6460
|
+
);
|
|
6461
|
+
upstreamResponse.pipe(response);
|
|
6462
|
+
});
|
|
6463
|
+
upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
|
|
6464
|
+
request2.on("aborted", () => upstream.destroy());
|
|
6465
|
+
request2.pipe(upstream);
|
|
6466
|
+
} catch {
|
|
6467
|
+
writeProxyError(response, 403, "Blocked by browser network policy");
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
async forwardConnect(request2, client, head) {
|
|
6471
|
+
try {
|
|
6472
|
+
const authority = request2.url ?? "";
|
|
6473
|
+
const target = await this.resolve(`https://${authority}`);
|
|
6474
|
+
const upstream = net2.connect({
|
|
6475
|
+
host: target.address,
|
|
6476
|
+
family: target.family,
|
|
6477
|
+
port: Number(target.url.port || 443)
|
|
6478
|
+
});
|
|
6479
|
+
upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
|
|
6480
|
+
upstream.once("connect", () => {
|
|
6481
|
+
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
6482
|
+
if (head.length > 0) upstream.write(head);
|
|
6483
|
+
upstream.pipe(client);
|
|
6484
|
+
client.pipe(upstream);
|
|
6485
|
+
});
|
|
6486
|
+
upstream.once("error", () => {
|
|
6487
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6488
|
+
});
|
|
6489
|
+
client.once("close", () => upstream.destroy());
|
|
6490
|
+
} catch {
|
|
6491
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6494
|
+
async forwardUpgrade(request2, client, head) {
|
|
6495
|
+
let upstream;
|
|
6496
|
+
try {
|
|
6497
|
+
const rawUrl = request2.url ?? "";
|
|
6498
|
+
const websocketUrl = new URL(rawUrl);
|
|
6499
|
+
if (websocketUrl.protocol !== "ws:") {
|
|
6500
|
+
throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
|
|
6501
|
+
}
|
|
6502
|
+
websocketUrl.protocol = "http:";
|
|
6503
|
+
const target = await this.resolve(websocketUrl.toString());
|
|
6504
|
+
const headers = sanitizeHeaders(request2.headers, true);
|
|
6505
|
+
headers.host = target.url.host;
|
|
6506
|
+
upstream = http.request({
|
|
6507
|
+
host: target.address,
|
|
6508
|
+
family: target.family,
|
|
6509
|
+
port: Number(target.url.port || 80),
|
|
6510
|
+
method: request2.method,
|
|
6511
|
+
path: `${target.url.pathname}${target.url.search}`,
|
|
6512
|
+
headers,
|
|
6513
|
+
agent: false
|
|
6514
|
+
});
|
|
6515
|
+
upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
6516
|
+
writeRawResponseHead(client, response);
|
|
6517
|
+
if (head.length > 0) upstreamSocket.write(head);
|
|
6518
|
+
if (upstreamHead.length > 0) client.write(upstreamHead);
|
|
6519
|
+
upstreamSocket.pipe(client);
|
|
6520
|
+
client.pipe(upstreamSocket);
|
|
6521
|
+
client.once("close", () => upstreamSocket.destroy());
|
|
6522
|
+
});
|
|
6523
|
+
upstream.once("response", (response) => {
|
|
6524
|
+
writeRawResponseHead(client, response);
|
|
6525
|
+
response.pipe(client);
|
|
6526
|
+
});
|
|
6527
|
+
upstream.once("error", () => {
|
|
6528
|
+
client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
6529
|
+
});
|
|
6530
|
+
client.once("close", () => upstream?.destroy());
|
|
6531
|
+
upstream.end();
|
|
6532
|
+
} catch {
|
|
6533
|
+
upstream?.destroy();
|
|
6534
|
+
client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
resolve(rawUrl) {
|
|
6538
|
+
return resolvePinnedBrowserTarget(rawUrl, {
|
|
6539
|
+
allowPrivateHosts: this.options.allowPrivateHosts,
|
|
6540
|
+
allowedPrivateOrigins: this.options.allowedPrivateOrigins,
|
|
6541
|
+
lookup: this.options.lookup
|
|
6542
|
+
});
|
|
6543
|
+
}
|
|
6544
|
+
};
|
|
6545
|
+
function sanitizeHeaders(headers, preserveUpgrade = false) {
|
|
6546
|
+
const sanitized = { ...headers };
|
|
6547
|
+
delete sanitized["proxy-authorization"];
|
|
6548
|
+
delete sanitized["proxy-connection"];
|
|
6549
|
+
if (!preserveUpgrade) delete sanitized.connection;
|
|
6550
|
+
return sanitized;
|
|
6551
|
+
}
|
|
6552
|
+
function writeRawResponseHead(client, response) {
|
|
6553
|
+
const status = response.statusCode ?? 502;
|
|
6554
|
+
const statusText = response.statusMessage ?? "Bad Gateway";
|
|
6555
|
+
const lines = [`HTTP/1.1 ${status} ${statusText}`];
|
|
6556
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) {
|
|
6557
|
+
lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
|
|
6558
|
+
}
|
|
6559
|
+
client.write(`${lines.join("\r\n")}\r
|
|
6560
|
+
\r
|
|
6561
|
+
`);
|
|
6562
|
+
}
|
|
6563
|
+
function writeProxyError(response, status, message) {
|
|
6564
|
+
if (response.headersSent || response.destroyed) return;
|
|
6565
|
+
response.writeHead(status, { "content-type": "text/plain", connection: "close" });
|
|
6566
|
+
response.end(message);
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6569
|
+
// src/browser/manager.ts
|
|
6570
|
+
var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
|
|
6571
|
+
var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
|
|
6572
|
+
var DEFAULT_MAX_ENTRIES = 100;
|
|
6573
|
+
var BrowserSessionManager = class {
|
|
6574
|
+
constructor(options, launcher = defaultLauncher) {
|
|
6575
|
+
this.launcher = launcher;
|
|
6576
|
+
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
6577
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? true;
|
|
6578
|
+
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
6579
|
+
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
6580
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6581
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins
|
|
6582
|
+
});
|
|
6583
|
+
this.headless = options.headless ?? true;
|
|
6584
|
+
this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
6585
|
+
this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
|
|
6586
|
+
this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6587
|
+
this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
|
|
6588
|
+
}
|
|
6589
|
+
launcher;
|
|
6590
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6591
|
+
artifacts;
|
|
6592
|
+
allowPrivateHosts;
|
|
6593
|
+
allowedPrivateOrigins;
|
|
6594
|
+
networkProxy;
|
|
6595
|
+
headless;
|
|
6596
|
+
operationTimeoutMs;
|
|
6597
|
+
maxSnapshotChars;
|
|
6598
|
+
maxConsoleEntries;
|
|
6599
|
+
maxNetworkEntries;
|
|
6600
|
+
browser;
|
|
6601
|
+
launching;
|
|
6602
|
+
async open(ownerId, input, signal) {
|
|
6603
|
+
signal.throwIfAborted();
|
|
6604
|
+
if (input.url) {
|
|
6605
|
+
await assertBrowserUrlAllowed(input.url, {
|
|
6606
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6607
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6608
|
+
navigation: true
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
const browser = await this.ensureBrowser(signal);
|
|
6612
|
+
const proxyServer = await this.networkProxy.start();
|
|
6613
|
+
const context = await abortable(
|
|
6614
|
+
signal,
|
|
6615
|
+
async () => {
|
|
6616
|
+
const created = await browser.newContext({
|
|
6617
|
+
acceptDownloads: false,
|
|
6618
|
+
ignoreHTTPSErrors: false,
|
|
6619
|
+
serviceWorkers: "block",
|
|
6620
|
+
proxy: { server: proxyServer },
|
|
6621
|
+
viewport: input.viewport ?? { width: 1440, height: 900 }
|
|
6622
|
+
});
|
|
6623
|
+
if (signal.aborted) {
|
|
6624
|
+
await created.close().catch(() => void 0);
|
|
6625
|
+
signal.throwIfAborted();
|
|
6626
|
+
}
|
|
6627
|
+
return created;
|
|
6628
|
+
},
|
|
6629
|
+
() => void 0
|
|
6630
|
+
);
|
|
6631
|
+
const session = {
|
|
6632
|
+
id: ulid2(),
|
|
6633
|
+
ownerId,
|
|
6634
|
+
context,
|
|
6635
|
+
page: await context.newPage(),
|
|
6636
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6637
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6638
|
+
tracing: input.trace ?? true,
|
|
6639
|
+
console: [],
|
|
6640
|
+
network: []
|
|
6641
|
+
};
|
|
6642
|
+
this.sessions.set(session.id, session);
|
|
6643
|
+
this.attachEvidenceCollectors(session);
|
|
6644
|
+
await context.route("**/*", async (route) => {
|
|
6645
|
+
try {
|
|
6646
|
+
await assertBrowserUrlAllowed(route.request().url(), {
|
|
6647
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6648
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6649
|
+
navigation: false
|
|
6650
|
+
});
|
|
6651
|
+
await route.continue();
|
|
6652
|
+
} catch {
|
|
6653
|
+
this.pushNetwork(session, {
|
|
6654
|
+
method: route.request().method(),
|
|
6655
|
+
url: safeBrowserUrl(route.request().url()),
|
|
6656
|
+
failed: true,
|
|
6657
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6658
|
+
});
|
|
6659
|
+
await route.abort("blockedbyclient");
|
|
6660
|
+
}
|
|
6661
|
+
});
|
|
6662
|
+
try {
|
|
6663
|
+
if (session.tracing) {
|
|
6664
|
+
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
6665
|
+
}
|
|
6666
|
+
if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
|
|
6667
|
+
return await this.summary(session);
|
|
6668
|
+
} catch (err) {
|
|
6669
|
+
await context.close().catch(() => void 0);
|
|
6670
|
+
this.sessions.delete(session.id);
|
|
6671
|
+
await this.closeBrowserIfIdle();
|
|
6672
|
+
throw err;
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
async list(ownerId) {
|
|
6676
|
+
const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
|
|
6677
|
+
return Promise.all(owned.map((session) => this.summary(session)));
|
|
6678
|
+
}
|
|
6679
|
+
async navigate(id, ownerId, rawUrl, signal) {
|
|
6680
|
+
await assertBrowserUrlAllowed(rawUrl, {
|
|
6681
|
+
allowPrivateHosts: this.allowPrivateHosts,
|
|
6682
|
+
allowedPrivateOrigins: this.allowedPrivateOrigins,
|
|
6683
|
+
navigation: true
|
|
6684
|
+
});
|
|
6685
|
+
const session = this.requireOwned(id, ownerId);
|
|
6686
|
+
await this.runPageOperation(
|
|
6687
|
+
session,
|
|
6688
|
+
signal,
|
|
6689
|
+
() => session.page.goto(rawUrl, {
|
|
6690
|
+
waitUntil: "domcontentloaded",
|
|
6691
|
+
timeout: this.operationTimeoutMs
|
|
6692
|
+
})
|
|
6693
|
+
);
|
|
6694
|
+
return this.summary(session);
|
|
6695
|
+
}
|
|
6696
|
+
async snapshot(id, ownerId, signal) {
|
|
6697
|
+
const session = this.requireOwned(id, ownerId);
|
|
6698
|
+
const raw = await this.runPageOperation(session, signal, async () => {
|
|
6699
|
+
try {
|
|
6700
|
+
return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
|
|
6701
|
+
} catch {
|
|
6702
|
+
return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
6705
|
+
const aria = redactBrowserText(raw);
|
|
6706
|
+
const truncated = aria.length > this.maxSnapshotChars;
|
|
6707
|
+
return {
|
|
6708
|
+
session: await this.summary(session),
|
|
6709
|
+
aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
|
|
6710
|
+
\u2026[snapshot truncated]` : aria,
|
|
6711
|
+
console: session.console.map((entry) => ({ ...entry })),
|
|
6712
|
+
network: session.network.map((entry) => ({ ...entry })),
|
|
6713
|
+
truncated
|
|
6714
|
+
};
|
|
6715
|
+
}
|
|
6716
|
+
async screenshot(id, ownerId, input, signal) {
|
|
6717
|
+
const session = this.requireOwned(id, ownerId);
|
|
6718
|
+
const bytes = await this.runPageOperation(
|
|
6719
|
+
session,
|
|
6720
|
+
signal,
|
|
6721
|
+
() => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
|
|
6722
|
+
);
|
|
6723
|
+
signal.throwIfAborted();
|
|
6724
|
+
return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
|
|
6725
|
+
}
|
|
6726
|
+
async click(id, ownerId, selector, signal) {
|
|
6727
|
+
const session = this.requireOwned(id, ownerId);
|
|
6728
|
+
await this.runPageOperation(
|
|
6729
|
+
session,
|
|
6730
|
+
signal,
|
|
6731
|
+
() => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6734
|
+
async type(id, ownerId, selector, text, signal) {
|
|
6735
|
+
const session = this.requireOwned(id, ownerId);
|
|
6736
|
+
await this.runPageOperation(
|
|
6737
|
+
session,
|
|
6738
|
+
signal,
|
|
6739
|
+
() => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
|
|
6740
|
+
);
|
|
6741
|
+
}
|
|
6742
|
+
async select(id, ownerId, selector, value, signal) {
|
|
6743
|
+
const session = this.requireOwned(id, ownerId);
|
|
6744
|
+
await this.runPageOperation(
|
|
6745
|
+
session,
|
|
6746
|
+
signal,
|
|
6747
|
+
() => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
|
|
6748
|
+
);
|
|
6749
|
+
}
|
|
6750
|
+
async press(id, ownerId, key, signal) {
|
|
6751
|
+
const session = this.requireOwned(id, ownerId);
|
|
6752
|
+
await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
|
|
6753
|
+
}
|
|
6754
|
+
async hover(id, ownerId, selector, signal) {
|
|
6755
|
+
const session = this.requireOwned(id, ownerId);
|
|
6756
|
+
await this.runPageOperation(
|
|
6757
|
+
session,
|
|
6758
|
+
signal,
|
|
6759
|
+
() => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
|
|
6760
|
+
);
|
|
6761
|
+
}
|
|
6762
|
+
async wait(id, ownerId, input, signal) {
|
|
6763
|
+
const session = this.requireOwned(id, ownerId);
|
|
6764
|
+
const timeout = Math.min(
|
|
6765
|
+
Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
|
|
6766
|
+
this.operationTimeoutMs
|
|
6767
|
+
);
|
|
6768
|
+
await this.runPageOperation(
|
|
6769
|
+
session,
|
|
6770
|
+
signal,
|
|
6771
|
+
() => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
|
|
6772
|
+
);
|
|
6773
|
+
}
|
|
6774
|
+
async drag(id, ownerId, from, to, signal) {
|
|
6775
|
+
const session = this.requireOwned(id, ownerId);
|
|
6776
|
+
await this.runPageOperation(
|
|
6777
|
+
session,
|
|
6778
|
+
signal,
|
|
6779
|
+
() => session.page.locator(from).dragTo(session.page.locator(to), {
|
|
6780
|
+
timeout: this.operationTimeoutMs
|
|
6781
|
+
})
|
|
6782
|
+
);
|
|
6783
|
+
}
|
|
6784
|
+
async upload(id, ownerId, selector, files, projectRoot, signal) {
|
|
6785
|
+
const session = this.requireOwned(id, ownerId);
|
|
6786
|
+
const root = path11.resolve(projectRoot);
|
|
6787
|
+
const realRoot = await fs8.realpath(root);
|
|
6788
|
+
const resolved = [];
|
|
6789
|
+
for (const file of files) {
|
|
6790
|
+
const absolute = path11.resolve(root, file);
|
|
6791
|
+
const relative12 = path11.relative(root, absolute);
|
|
6792
|
+
if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
|
|
6793
|
+
throw new Error("browser: upload files must stay inside the project root");
|
|
6794
|
+
}
|
|
6795
|
+
const realFile = await fs8.realpath(absolute);
|
|
6796
|
+
const realRelative = path11.relative(realRoot, realFile);
|
|
6797
|
+
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
6798
|
+
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
6799
|
+
}
|
|
6800
|
+
const stat17 = await fs8.stat(realFile);
|
|
6801
|
+
if (!stat17.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
6802
|
+
resolved.push(realFile);
|
|
6803
|
+
}
|
|
6804
|
+
await this.runPageOperation(
|
|
6805
|
+
session,
|
|
6806
|
+
signal,
|
|
6807
|
+
() => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
|
|
6808
|
+
);
|
|
6809
|
+
}
|
|
6810
|
+
async evaluate(id, ownerId, expression, signal) {
|
|
6811
|
+
const session = this.requireOwned(id, ownerId);
|
|
6812
|
+
const result = await this.runPageOperation(
|
|
6813
|
+
session,
|
|
6814
|
+
signal,
|
|
6815
|
+
() => session.page.evaluate(expression)
|
|
6816
|
+
);
|
|
6817
|
+
const serialized = JSON.stringify(result);
|
|
6818
|
+
if (serialized && serialized.length > this.maxSnapshotChars) {
|
|
6819
|
+
throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
|
|
6820
|
+
}
|
|
6821
|
+
return result;
|
|
6822
|
+
}
|
|
6823
|
+
async close(id, ownerId) {
|
|
6824
|
+
const session = this.requireOwned(id, ownerId);
|
|
6825
|
+
let trace;
|
|
6826
|
+
try {
|
|
6827
|
+
if (session.tracing) {
|
|
6828
|
+
const target = this.artifacts.pathFor(session.id, "zip");
|
|
6829
|
+
await fs8.mkdir(path11.dirname(target.path), { recursive: true });
|
|
6830
|
+
await session.context.tracing.stop({ path: target.path });
|
|
6831
|
+
trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
|
|
6832
|
+
}
|
|
6833
|
+
} finally {
|
|
6834
|
+
await session.context.close().catch(() => void 0);
|
|
6835
|
+
this.sessions.delete(id);
|
|
6836
|
+
await this.closeBrowserIfIdle();
|
|
6837
|
+
}
|
|
6838
|
+
return trace ? { trace } : {};
|
|
6839
|
+
}
|
|
6840
|
+
async closeOwner(ownerId) {
|
|
6841
|
+
const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
|
|
6842
|
+
for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
|
|
6843
|
+
}
|
|
6844
|
+
async dispose() {
|
|
6845
|
+
for (const session of [...this.sessions.values()]) {
|
|
6846
|
+
await session.context.close().catch(() => void 0);
|
|
6847
|
+
}
|
|
6848
|
+
this.sessions.clear();
|
|
6849
|
+
const browser = this.browser;
|
|
6850
|
+
this.browser = void 0;
|
|
6851
|
+
this.launching = void 0;
|
|
6852
|
+
await browser?.close().catch(() => void 0);
|
|
6853
|
+
await this.networkProxy.close();
|
|
6854
|
+
}
|
|
6855
|
+
async ensureBrowser(signal) {
|
|
6856
|
+
if (this.browser?.isConnected()) return this.browser;
|
|
6857
|
+
if (!this.launching) {
|
|
6858
|
+
this.launching = this.launcher(this.headless).then((browser) => {
|
|
6859
|
+
this.browser = browser;
|
|
6860
|
+
browser.on("disconnected", () => {
|
|
6861
|
+
if (this.browser === browser) this.browser = void 0;
|
|
6862
|
+
for (const [id, session] of this.sessions) {
|
|
6863
|
+
if (session.context.browser() === browser) this.sessions.delete(id);
|
|
6864
|
+
}
|
|
6865
|
+
});
|
|
6866
|
+
return browser;
|
|
6867
|
+
});
|
|
6868
|
+
}
|
|
6869
|
+
try {
|
|
6870
|
+
return await abortable(
|
|
6871
|
+
signal,
|
|
6872
|
+
() => this.launching,
|
|
6873
|
+
async () => {
|
|
6874
|
+
const browser = await this.launching?.catch(() => void 0);
|
|
6875
|
+
await browser?.close().catch(() => void 0);
|
|
6876
|
+
}
|
|
6877
|
+
);
|
|
6878
|
+
} finally {
|
|
6879
|
+
this.launching = void 0;
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
requireOwned(id, ownerId) {
|
|
6883
|
+
const session = this.sessions.get(id);
|
|
6884
|
+
if (!session) throw new Error(`browser: unknown session "${id}"`);
|
|
6885
|
+
if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
|
|
6886
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6887
|
+
return session;
|
|
6888
|
+
}
|
|
6889
|
+
async summary(session) {
|
|
6890
|
+
return {
|
|
6891
|
+
id: session.id,
|
|
6892
|
+
ownerId: session.ownerId,
|
|
6893
|
+
url: safeBrowserUrl(session.page.url()),
|
|
6894
|
+
title: redactBrowserText(await session.page.title().catch(() => "")),
|
|
6895
|
+
createdAt: session.createdAt,
|
|
6896
|
+
lastUsedAt: session.lastUsedAt,
|
|
6897
|
+
tracing: session.tracing
|
|
6898
|
+
};
|
|
6899
|
+
}
|
|
6900
|
+
attachEvidenceCollectors(session) {
|
|
6901
|
+
session.page.on("console", (message) => {
|
|
6902
|
+
pushBounded(
|
|
6903
|
+
session.console,
|
|
6904
|
+
{
|
|
6905
|
+
level: message.type(),
|
|
6906
|
+
text: redactBrowserText(message.text()).slice(0, 4096),
|
|
6907
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6908
|
+
},
|
|
6909
|
+
this.maxConsoleEntries
|
|
6910
|
+
);
|
|
6911
|
+
});
|
|
6912
|
+
session.page.on("response", (response) => {
|
|
6913
|
+
this.pushNetwork(session, {
|
|
6914
|
+
method: response.request().method(),
|
|
6915
|
+
url: safeBrowserUrl(response.url()),
|
|
6916
|
+
status: response.status(),
|
|
6917
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6918
|
+
});
|
|
6919
|
+
});
|
|
6920
|
+
session.page.on("requestfailed", (request2) => {
|
|
6921
|
+
this.pushNetwork(session, {
|
|
6922
|
+
method: request2.method(),
|
|
6923
|
+
url: safeBrowserUrl(request2.url()),
|
|
6924
|
+
failed: true,
|
|
6925
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6926
|
+
});
|
|
6927
|
+
});
|
|
6928
|
+
}
|
|
6929
|
+
pushNetwork(session, entry) {
|
|
6930
|
+
pushBounded(session.network, entry, this.maxNetworkEntries);
|
|
6931
|
+
}
|
|
6932
|
+
async runPageOperation(session, signal, operation) {
|
|
6933
|
+
signal.throwIfAborted();
|
|
6934
|
+
session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6935
|
+
return abortable(signal, operation, async () => {
|
|
6936
|
+
await session.context.close().catch(() => void 0);
|
|
6937
|
+
this.sessions.delete(session.id);
|
|
6938
|
+
await this.closeBrowserIfIdle();
|
|
6939
|
+
});
|
|
6940
|
+
}
|
|
6941
|
+
async closeBrowserIfIdle() {
|
|
6942
|
+
if (this.sessions.size > 0 || !this.browser) return;
|
|
6943
|
+
const browser = this.browser;
|
|
6944
|
+
this.browser = void 0;
|
|
6945
|
+
await browser.close().catch(() => void 0);
|
|
6946
|
+
}
|
|
6947
|
+
};
|
|
6948
|
+
async function defaultLauncher(headless) {
|
|
6949
|
+
try {
|
|
6950
|
+
const { chromium } = await import("@playwright/test");
|
|
6951
|
+
return await chromium.launch({ headless });
|
|
6952
|
+
} catch (err) {
|
|
6953
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
6954
|
+
throw new Error(
|
|
6955
|
+
`browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
|
|
6956
|
+
);
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
async function browserInstallationDiagnostics() {
|
|
6960
|
+
try {
|
|
6961
|
+
const { chromium } = await import("@playwright/test");
|
|
6962
|
+
const executablePath = chromium.executablePath();
|
|
6963
|
+
await fs8.access(executablePath);
|
|
6964
|
+
return { available: true, executablePath, message: "Playwright Chromium is available." };
|
|
6965
|
+
} catch {
|
|
6966
|
+
return {
|
|
6967
|
+
available: false,
|
|
6968
|
+
message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
|
|
6969
|
+
};
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
function pushBounded(target, value, limit) {
|
|
6973
|
+
target.push(value);
|
|
6974
|
+
if (target.length > limit) target.splice(0, target.length - limit);
|
|
6975
|
+
}
|
|
6976
|
+
async function abortable(signal, operation, onAbort) {
|
|
6977
|
+
signal.throwIfAborted();
|
|
6978
|
+
return new Promise((resolve14, reject) => {
|
|
6979
|
+
let settled = false;
|
|
6980
|
+
let aborting = false;
|
|
6981
|
+
const finish = (fn) => {
|
|
6982
|
+
if (settled) return;
|
|
6983
|
+
settled = true;
|
|
6984
|
+
signal.removeEventListener("abort", abort);
|
|
6985
|
+
fn();
|
|
6986
|
+
};
|
|
6987
|
+
const abort = () => {
|
|
6988
|
+
aborting = true;
|
|
6989
|
+
void Promise.resolve(onAbort()).finally(() => {
|
|
6990
|
+
finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
|
|
6991
|
+
});
|
|
6992
|
+
};
|
|
6993
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6994
|
+
operation().then(
|
|
6995
|
+
(value) => {
|
|
6996
|
+
if (!aborting) finish(() => resolve14(value));
|
|
6997
|
+
},
|
|
6998
|
+
(err) => {
|
|
6999
|
+
if (!aborting) finish(() => reject(err));
|
|
7000
|
+
}
|
|
7001
|
+
);
|
|
7002
|
+
});
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
// src/browser/tools.ts
|
|
7006
|
+
var managers = /* @__PURE__ */ new Map();
|
|
7007
|
+
var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
|
|
7008
|
+
function managerFor(ctx) {
|
|
7009
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
7010
|
+
let manager = managers.get(root);
|
|
7011
|
+
if (!manager) {
|
|
7012
|
+
const wpaths = resolveWstackPaths({ projectRoot: root });
|
|
7013
|
+
manager = new BrowserSessionManager({
|
|
7014
|
+
artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
|
|
7015
|
+
allowedPrivateOrigins: parsePrivateOriginAllowlist(
|
|
7016
|
+
process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
|
|
7017
|
+
)
|
|
7018
|
+
});
|
|
7019
|
+
managers.set(root, manager);
|
|
7020
|
+
}
|
|
7021
|
+
const runSignal = ctx.signal;
|
|
7022
|
+
if (cleanupSignalByContext.get(ctx) !== runSignal) {
|
|
7023
|
+
cleanupSignalByContext.set(ctx, runSignal);
|
|
7024
|
+
const managerForRun = manager;
|
|
7025
|
+
const ownerForRun = owner(ctx);
|
|
7026
|
+
ctx.registerAbortHook?.(async () => {
|
|
7027
|
+
if (cleanupSignalByContext.get(ctx) === runSignal) {
|
|
7028
|
+
cleanupSignalByContext.delete(ctx);
|
|
7029
|
+
}
|
|
7030
|
+
await managerForRun.closeOwner(ownerForRun);
|
|
7031
|
+
});
|
|
7032
|
+
}
|
|
7033
|
+
return manager;
|
|
7034
|
+
}
|
|
7035
|
+
function owner(ctx) {
|
|
7036
|
+
return ctx.agentId || "leader";
|
|
7037
|
+
}
|
|
7038
|
+
function json(value) {
|
|
7039
|
+
return JSON.stringify(value, null, 2);
|
|
7040
|
+
}
|
|
7041
|
+
var sessionIdSchema = {
|
|
7042
|
+
type: "string",
|
|
7043
|
+
minLength: 1,
|
|
7044
|
+
description: "Browser session id returned by browser_open."
|
|
7045
|
+
};
|
|
7046
|
+
var browserOpenTool = {
|
|
7047
|
+
name: "browser_open",
|
|
7048
|
+
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
|
|
7049
|
+
usageHint: "browser_open({ url?, width?, height?, trace? })",
|
|
7050
|
+
permission: "confirm",
|
|
7051
|
+
mutating: true,
|
|
7052
|
+
riskTier: "standard",
|
|
7053
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7054
|
+
subjectKey: "url",
|
|
7055
|
+
inputSchema: {
|
|
7056
|
+
type: "object",
|
|
7057
|
+
properties: {
|
|
7058
|
+
url: { type: "string", description: "Optional absolute http(s) URL." },
|
|
7059
|
+
width: { type: "integer", minimum: 320, maximum: 3840 },
|
|
7060
|
+
height: { type: "integer", minimum: 240, maximum: 2160 },
|
|
7061
|
+
trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
|
|
7062
|
+
}
|
|
7063
|
+
},
|
|
7064
|
+
async execute(input, ctx, opts) {
|
|
7065
|
+
const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
|
|
7066
|
+
return json(
|
|
7067
|
+
await managerFor(ctx).open(
|
|
7068
|
+
owner(ctx),
|
|
7069
|
+
{ url: input.url, viewport, trace: input.trace },
|
|
7070
|
+
opts.signal
|
|
7071
|
+
)
|
|
7072
|
+
);
|
|
7073
|
+
}
|
|
7074
|
+
};
|
|
7075
|
+
var browserListTool = {
|
|
7076
|
+
name: "browser_list",
|
|
7077
|
+
description: "List browser sessions owned by the current agent without exposing other agents sessions.",
|
|
7078
|
+
permission: "auto",
|
|
7079
|
+
mutating: false,
|
|
7080
|
+
riskTier: "safe",
|
|
7081
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7082
|
+
inputSchema: { type: "object", properties: {} },
|
|
7083
|
+
async execute(_input, ctx) {
|
|
7084
|
+
return json(await managerFor(ctx).list(owner(ctx)));
|
|
7085
|
+
}
|
|
7086
|
+
};
|
|
7087
|
+
var browserStatusTool = {
|
|
7088
|
+
name: "browser_status",
|
|
7089
|
+
description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
|
|
7090
|
+
permission: "auto",
|
|
7091
|
+
mutating: false,
|
|
7092
|
+
riskTier: "safe",
|
|
7093
|
+
capabilities: [ToolCapabilities.TOOL_META],
|
|
7094
|
+
inputSchema: { type: "object", properties: {} },
|
|
7095
|
+
async execute() {
|
|
7096
|
+
return json(await browserInstallationDiagnostics());
|
|
7097
|
+
}
|
|
7098
|
+
};
|
|
7099
|
+
var browserNavigateTool = {
|
|
7100
|
+
name: "browser_navigate",
|
|
7101
|
+
description: "Navigate an owned browser session to an approved http(s) URL.",
|
|
7102
|
+
usageHint: "browser_navigate({ sessionId, url })",
|
|
7103
|
+
permission: "confirm",
|
|
7104
|
+
mutating: true,
|
|
7105
|
+
riskTier: "standard",
|
|
7106
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7107
|
+
subjectKey: "url",
|
|
7108
|
+
inputSchema: {
|
|
7109
|
+
type: "object",
|
|
7110
|
+
properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
|
|
7111
|
+
required: ["sessionId", "url"]
|
|
7112
|
+
},
|
|
7113
|
+
async execute(input, ctx, opts) {
|
|
7114
|
+
return json(
|
|
7115
|
+
await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
7118
|
+
};
|
|
7119
|
+
var browserSnapshotTool = {
|
|
7120
|
+
name: "browser_snapshot",
|
|
7121
|
+
description: "Return bounded accessibility state plus redacted console and network summaries.",
|
|
7122
|
+
usageHint: "browser_snapshot({ sessionId })",
|
|
7123
|
+
permission: "auto",
|
|
7124
|
+
mutating: false,
|
|
7125
|
+
riskTier: "safe",
|
|
7126
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7127
|
+
inputSchema: {
|
|
7128
|
+
type: "object",
|
|
7129
|
+
properties: { sessionId: sessionIdSchema },
|
|
7130
|
+
required: ["sessionId"]
|
|
7131
|
+
},
|
|
7132
|
+
async execute(input, ctx, opts) {
|
|
7133
|
+
return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
|
|
7134
|
+
}
|
|
7135
|
+
};
|
|
7136
|
+
var browserScreenshotTool = {
|
|
7137
|
+
name: "browser_screenshot",
|
|
7138
|
+
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
7139
|
+
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
7140
|
+
permission: "confirm",
|
|
7141
|
+
mutating: true,
|
|
7142
|
+
riskTier: "safe",
|
|
7143
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7144
|
+
inputSchema: {
|
|
7145
|
+
type: "object",
|
|
7146
|
+
properties: {
|
|
7147
|
+
sessionId: sessionIdSchema,
|
|
7148
|
+
fullPage: { type: "boolean" },
|
|
7149
|
+
selector: { type: "string", minLength: 1 }
|
|
7150
|
+
},
|
|
7151
|
+
required: ["sessionId"]
|
|
7152
|
+
},
|
|
7153
|
+
async execute(input, ctx, opts) {
|
|
7154
|
+
return json(
|
|
7155
|
+
await managerFor(ctx).screenshot(
|
|
7156
|
+
input.sessionId,
|
|
7157
|
+
owner(ctx),
|
|
7158
|
+
{ fullPage: input.fullPage, selector: input.selector },
|
|
7159
|
+
opts.signal
|
|
7160
|
+
)
|
|
7161
|
+
);
|
|
7162
|
+
}
|
|
7163
|
+
};
|
|
7164
|
+
function selectorTool(name, description, run) {
|
|
7165
|
+
return {
|
|
7166
|
+
name,
|
|
7167
|
+
description,
|
|
7168
|
+
usageHint: `${name}({ sessionId, selector })`,
|
|
7169
|
+
permission: "confirm",
|
|
7170
|
+
mutating: true,
|
|
7171
|
+
riskTier: "standard",
|
|
7172
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7173
|
+
inputSchema: {
|
|
7174
|
+
type: "object",
|
|
7175
|
+
properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
|
|
7176
|
+
required: ["sessionId", "selector"]
|
|
7177
|
+
},
|
|
7178
|
+
async execute(input, ctx, opts) {
|
|
7179
|
+
await run(managerFor(ctx), input, owner(ctx), opts.signal);
|
|
7180
|
+
return "ok";
|
|
7181
|
+
}
|
|
7182
|
+
};
|
|
7183
|
+
}
|
|
7184
|
+
var browserClickTool = selectorTool(
|
|
7185
|
+
"browser_click",
|
|
7186
|
+
"Click an element in an owned browser session.",
|
|
7187
|
+
(manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
|
|
7188
|
+
);
|
|
7189
|
+
var browserHoverTool = selectorTool(
|
|
7190
|
+
"browser_hover",
|
|
7191
|
+
"Hover an element in an owned browser session.",
|
|
7192
|
+
(manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
|
|
7193
|
+
);
|
|
7194
|
+
var browserTypeTool = {
|
|
7195
|
+
name: "browser_type",
|
|
7196
|
+
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.",
|
|
7197
|
+
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
7198
|
+
permission: "confirm",
|
|
7199
|
+
mutating: true,
|
|
7200
|
+
riskTier: "standard",
|
|
7201
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7202
|
+
inputSchema: {
|
|
7203
|
+
type: "object",
|
|
7204
|
+
properties: {
|
|
7205
|
+
sessionId: sessionIdSchema,
|
|
7206
|
+
selector: { type: "string", minLength: 1 },
|
|
7207
|
+
text: { type: "string" },
|
|
7208
|
+
secretEnv: {
|
|
7209
|
+
type: "string",
|
|
7210
|
+
pattern: "^[A-Z_][A-Z0-9_]*$",
|
|
7211
|
+
description: "Host environment variable resolved only at execution time."
|
|
7212
|
+
}
|
|
7213
|
+
},
|
|
7214
|
+
required: ["sessionId", "selector"]
|
|
7215
|
+
},
|
|
7216
|
+
validate(input) {
|
|
7217
|
+
return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
|
|
7218
|
+
},
|
|
7219
|
+
async execute(input, ctx, opts) {
|
|
7220
|
+
const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
|
|
7221
|
+
if (value === void 0) {
|
|
7222
|
+
throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
|
|
7223
|
+
}
|
|
7224
|
+
await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
|
|
7225
|
+
return "ok";
|
|
7226
|
+
}
|
|
7227
|
+
};
|
|
7228
|
+
var browserSelectTool = {
|
|
7229
|
+
name: "browser_select",
|
|
7230
|
+
description: "Select an option in an owned browser session.",
|
|
7231
|
+
usageHint: "browser_select({ sessionId, selector, value })",
|
|
7232
|
+
permission: "confirm",
|
|
7233
|
+
mutating: true,
|
|
7234
|
+
riskTier: "standard",
|
|
7235
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7236
|
+
inputSchema: {
|
|
7237
|
+
type: "object",
|
|
7238
|
+
properties: {
|
|
7239
|
+
sessionId: sessionIdSchema,
|
|
7240
|
+
selector: { type: "string", minLength: 1 },
|
|
7241
|
+
value: { type: "string" }
|
|
7242
|
+
},
|
|
7243
|
+
required: ["sessionId", "selector", "value"]
|
|
7244
|
+
},
|
|
7245
|
+
async execute(input, ctx, opts) {
|
|
7246
|
+
await managerFor(ctx).select(
|
|
7247
|
+
input.sessionId,
|
|
7248
|
+
owner(ctx),
|
|
7249
|
+
input.selector,
|
|
7250
|
+
input.value,
|
|
7251
|
+
opts.signal
|
|
7252
|
+
);
|
|
7253
|
+
return "ok";
|
|
7254
|
+
}
|
|
7255
|
+
};
|
|
7256
|
+
var browserPressTool = {
|
|
7257
|
+
name: "browser_press",
|
|
7258
|
+
description: "Press a keyboard key in an owned browser session.",
|
|
7259
|
+
usageHint: "browser_press({ sessionId, key })",
|
|
7260
|
+
permission: "confirm",
|
|
7261
|
+
mutating: true,
|
|
7262
|
+
riskTier: "standard",
|
|
7263
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7264
|
+
inputSchema: {
|
|
7265
|
+
type: "object",
|
|
7266
|
+
properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
|
|
7267
|
+
required: ["sessionId", "key"]
|
|
7268
|
+
},
|
|
7269
|
+
async execute(input, ctx, opts) {
|
|
7270
|
+
await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
|
|
7271
|
+
return "ok";
|
|
7272
|
+
}
|
|
7273
|
+
};
|
|
7274
|
+
var browserDragTool = {
|
|
7275
|
+
name: "browser_drag",
|
|
7276
|
+
description: "Drag one element to another in an owned browser session.",
|
|
7277
|
+
usageHint: "browser_drag({ sessionId, from, to })",
|
|
7278
|
+
permission: "confirm",
|
|
7279
|
+
mutating: true,
|
|
7280
|
+
riskTier: "standard",
|
|
7281
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7282
|
+
inputSchema: {
|
|
7283
|
+
type: "object",
|
|
7284
|
+
properties: {
|
|
7285
|
+
sessionId: sessionIdSchema,
|
|
7286
|
+
from: { type: "string", minLength: 1 },
|
|
7287
|
+
to: { type: "string", minLength: 1 }
|
|
7288
|
+
},
|
|
7289
|
+
required: ["sessionId", "from", "to"]
|
|
7290
|
+
},
|
|
7291
|
+
async execute(input, ctx, opts) {
|
|
7292
|
+
await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
|
|
7293
|
+
return "ok";
|
|
7294
|
+
}
|
|
7295
|
+
};
|
|
7296
|
+
var browserWaitTool = {
|
|
7297
|
+
name: "browser_wait",
|
|
7298
|
+
description: "Wait for an element or a bounded duration in an owned browser session.",
|
|
7299
|
+
usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
|
|
7300
|
+
permission: "auto",
|
|
7301
|
+
mutating: false,
|
|
7302
|
+
riskTier: "safe",
|
|
7303
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7304
|
+
inputSchema: {
|
|
7305
|
+
type: "object",
|
|
7306
|
+
properties: {
|
|
7307
|
+
sessionId: sessionIdSchema,
|
|
7308
|
+
selector: { type: "string", minLength: 1 },
|
|
7309
|
+
timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
|
|
7310
|
+
},
|
|
7311
|
+
required: ["sessionId"]
|
|
7312
|
+
},
|
|
7313
|
+
async execute(input, ctx, opts) {
|
|
7314
|
+
await managerFor(ctx).wait(
|
|
7315
|
+
input.sessionId,
|
|
7316
|
+
owner(ctx),
|
|
7317
|
+
{ selector: input.selector, timeoutMs: input.timeoutMs },
|
|
7318
|
+
opts.signal
|
|
7319
|
+
);
|
|
7320
|
+
return "ok";
|
|
7321
|
+
}
|
|
7322
|
+
};
|
|
7323
|
+
var browserEvaluateTool = {
|
|
7324
|
+
name: "browser_evaluate",
|
|
7325
|
+
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
7326
|
+
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
7327
|
+
permission: "confirm",
|
|
7328
|
+
mutating: true,
|
|
7329
|
+
riskTier: "destructive",
|
|
7330
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
7331
|
+
inputSchema: {
|
|
7332
|
+
type: "object",
|
|
7333
|
+
properties: {
|
|
7334
|
+
sessionId: sessionIdSchema,
|
|
7335
|
+
expression: { type: "string", minLength: 1, maxLength: 16384 }
|
|
7336
|
+
},
|
|
7337
|
+
required: ["sessionId", "expression"]
|
|
7338
|
+
},
|
|
7339
|
+
async execute(input, ctx, opts) {
|
|
7340
|
+
return json(
|
|
7341
|
+
await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
|
|
7342
|
+
);
|
|
7343
|
+
}
|
|
7344
|
+
};
|
|
7345
|
+
var browserUploadTool = {
|
|
7346
|
+
name: "browser_upload",
|
|
7347
|
+
description: "Upload project-local files through a file input in an owned browser session.",
|
|
7348
|
+
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
7349
|
+
permission: "confirm",
|
|
7350
|
+
mutating: true,
|
|
7351
|
+
riskTier: "destructive",
|
|
7352
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
7353
|
+
inputSchema: {
|
|
7354
|
+
type: "object",
|
|
7355
|
+
properties: {
|
|
7356
|
+
sessionId: sessionIdSchema,
|
|
7357
|
+
selector: { type: "string", minLength: 1 },
|
|
7358
|
+
files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
|
|
7359
|
+
},
|
|
7360
|
+
required: ["sessionId", "selector", "files"]
|
|
7361
|
+
},
|
|
7362
|
+
async execute(input, ctx, opts) {
|
|
7363
|
+
await managerFor(ctx).upload(
|
|
7364
|
+
input.sessionId,
|
|
7365
|
+
owner(ctx),
|
|
7366
|
+
input.selector,
|
|
7367
|
+
input.files,
|
|
7368
|
+
ctx.projectRoot,
|
|
7369
|
+
opts.signal
|
|
7370
|
+
);
|
|
7371
|
+
return "ok";
|
|
7372
|
+
}
|
|
7373
|
+
};
|
|
7374
|
+
var browserCloseTool = {
|
|
7375
|
+
name: "browser_close",
|
|
7376
|
+
description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
|
|
7377
|
+
usageHint: "browser_close({ sessionId })",
|
|
7378
|
+
permission: "auto",
|
|
7379
|
+
mutating: false,
|
|
7380
|
+
riskTier: "safe",
|
|
7381
|
+
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
7382
|
+
inputSchema: {
|
|
7383
|
+
type: "object",
|
|
7384
|
+
properties: { sessionId: sessionIdSchema },
|
|
7385
|
+
required: ["sessionId"]
|
|
7386
|
+
},
|
|
7387
|
+
async execute(input, ctx) {
|
|
7388
|
+
return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
|
|
6159
7389
|
}
|
|
6160
|
-
}
|
|
7390
|
+
};
|
|
7391
|
+
var browserTools = [
|
|
7392
|
+
browserOpenTool,
|
|
7393
|
+
browserStatusTool,
|
|
7394
|
+
browserListTool,
|
|
7395
|
+
browserNavigateTool,
|
|
7396
|
+
browserSnapshotTool,
|
|
7397
|
+
browserScreenshotTool,
|
|
7398
|
+
browserClickTool,
|
|
7399
|
+
browserTypeTool,
|
|
7400
|
+
browserSelectTool,
|
|
7401
|
+
browserPressTool,
|
|
7402
|
+
browserHoverTool,
|
|
7403
|
+
browserDragTool,
|
|
7404
|
+
browserWaitTool,
|
|
7405
|
+
browserEvaluateTool,
|
|
7406
|
+
browserUploadTool,
|
|
7407
|
+
browserCloseTool
|
|
7408
|
+
];
|
|
7409
|
+
for (const tool of browserTools) tool.icon = "web";
|
|
7410
|
+
for (const tool of browserTools) tool.timeoutMs ??= 6e4;
|
|
6161
7411
|
|
|
6162
7412
|
// src/codebase-index/writer.ts
|
|
6163
7413
|
import { expectDefined as expectDefined2 } from "@wrongstack/core";
|
|
@@ -6245,9 +7495,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
|
|
|
6245
7495
|
// src/codebase-index/writer.ts
|
|
6246
7496
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
6247
7497
|
import { createRequire } from "node:module";
|
|
6248
|
-
import * as
|
|
6249
|
-
import * as
|
|
6250
|
-
import { resolveWstackPaths } from "@wrongstack/core";
|
|
7498
|
+
import * as fs9 from "node:fs";
|
|
7499
|
+
import * as path13 from "node:path";
|
|
7500
|
+
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
|
|
6251
7501
|
|
|
6252
7502
|
// src/codebase-index/schema.ts
|
|
6253
7503
|
var SCHEMA_VERSION = 2;
|
|
@@ -6376,7 +7626,7 @@ var Bm25Index = class {
|
|
|
6376
7626
|
// src/codebase-index/writer.ts
|
|
6377
7627
|
var DB_FILE = "index.db";
|
|
6378
7628
|
function resolveIndexDir(projectRoot, override) {
|
|
6379
|
-
return override ??
|
|
7629
|
+
return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
|
|
6380
7630
|
}
|
|
6381
7631
|
function codebaseIndexDirOverride(ctx) {
|
|
6382
7632
|
const v = ctx.meta?.["codebaseIndexDir"];
|
|
@@ -6491,9 +7741,9 @@ var IndexStore = class {
|
|
|
6491
7741
|
}
|
|
6492
7742
|
constructor(projectRoot, opts = {}) {
|
|
6493
7743
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
6494
|
-
|
|
7744
|
+
fs9.mkdirSync(this.indexDir, { recursive: true });
|
|
6495
7745
|
const Database = loadDatabaseSync();
|
|
6496
|
-
this.db = new Database(
|
|
7746
|
+
this.db = new Database(path13.join(this.indexDir, DB_FILE));
|
|
6497
7747
|
try {
|
|
6498
7748
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
6499
7749
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -7072,9 +8322,9 @@ var IndexStore = class {
|
|
|
7072
8322
|
}));
|
|
7073
8323
|
}
|
|
7074
8324
|
sizeBytes() {
|
|
7075
|
-
const dbPath =
|
|
8325
|
+
const dbPath = path13.join(this.indexDir, DB_FILE);
|
|
7076
8326
|
try {
|
|
7077
|
-
return
|
|
8327
|
+
return fs9.statSync(dbPath).size;
|
|
7078
8328
|
} catch {
|
|
7079
8329
|
return 0;
|
|
7080
8330
|
}
|
|
@@ -7089,14 +8339,14 @@ var IndexStore = class {
|
|
|
7089
8339
|
};
|
|
7090
8340
|
|
|
7091
8341
|
// src/codebase-index/background-indexer.ts
|
|
7092
|
-
import * as
|
|
8342
|
+
import * as fs15 from "node:fs";
|
|
7093
8343
|
import { fileURLToPath } from "node:url";
|
|
7094
8344
|
import { Worker } from "node:worker_threads";
|
|
7095
8345
|
|
|
7096
8346
|
// src/codebase-index/indexer.ts
|
|
7097
8347
|
import { expectDefined as expectDefined6 } from "@wrongstack/core";
|
|
7098
|
-
import * as
|
|
7099
|
-
import * as
|
|
8348
|
+
import * as fs14 from "node:fs/promises";
|
|
8349
|
+
import * as path19 from "node:path";
|
|
7100
8350
|
import { compileGlob as compileGlob2 } from "@wrongstack/core";
|
|
7101
8351
|
|
|
7102
8352
|
// src/codebase-index/ts-parser.ts
|
|
@@ -7287,10 +8537,11 @@ function detectLang(file) {
|
|
|
7287
8537
|
}
|
|
7288
8538
|
|
|
7289
8539
|
// src/codebase-index/go-parser.ts
|
|
8540
|
+
init_win32_resolve();
|
|
7290
8541
|
import { spawn as spawn4 } from "node:child_process";
|
|
7291
8542
|
import * as os5 from "node:os";
|
|
7292
|
-
import * as
|
|
7293
|
-
import * as
|
|
8543
|
+
import * as path14 from "node:path";
|
|
8544
|
+
import * as fs10 from "node:fs/promises";
|
|
7294
8545
|
async function parseSymbols2(opts) {
|
|
7295
8546
|
const { file, content, lang } = opts;
|
|
7296
8547
|
try {
|
|
@@ -7591,31 +8842,46 @@ func formatType(t ast.Expr) string {
|
|
|
7591
8842
|
}
|
|
7592
8843
|
`;
|
|
7593
8844
|
async function syncGoParse(filePath, content, lang) {
|
|
7594
|
-
const tmpDir = await
|
|
8845
|
+
const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
|
|
7595
8846
|
try {
|
|
7596
|
-
const scriptPath =
|
|
7597
|
-
await
|
|
7598
|
-
const
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
8847
|
+
const scriptPath = path14.join(tmpDir, "parse.go");
|
|
8848
|
+
await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
8849
|
+
const goBinary = resolveWin32Command("go");
|
|
8850
|
+
const goResult = await new Promise(
|
|
8851
|
+
(resolve14, reject) => {
|
|
8852
|
+
let settled = false;
|
|
8853
|
+
const proc = spawn4(goBinary, ["run", scriptPath], {
|
|
8854
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8855
|
+
windowsHide: true
|
|
8856
|
+
});
|
|
8857
|
+
proc.on("error", (err) => {
|
|
8858
|
+
if (settled) return;
|
|
8859
|
+
settled = true;
|
|
8860
|
+
reject(err);
|
|
8861
|
+
});
|
|
8862
|
+
let stdout2 = "";
|
|
8863
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8864
|
+
stdout2 += chunk.toString();
|
|
8865
|
+
});
|
|
8866
|
+
proc.stderr?.resume();
|
|
8867
|
+
proc.stdin?.write(content);
|
|
8868
|
+
proc.stdin?.end();
|
|
8869
|
+
const timer = setTimeout(() => {
|
|
8870
|
+
if (settled) return;
|
|
8871
|
+
settled = true;
|
|
7614
8872
|
proc.kill("SIGKILL");
|
|
7615
8873
|
reject(new Error("timeout"));
|
|
7616
|
-
}, 15e3)
|
|
7617
|
-
|
|
7618
|
-
|
|
8874
|
+
}, 15e3);
|
|
8875
|
+
timer.unref?.();
|
|
8876
|
+
proc.on("close", (code2) => {
|
|
8877
|
+
if (settled) return;
|
|
8878
|
+
settled = true;
|
|
8879
|
+
clearTimeout(timer);
|
|
8880
|
+
resolve14({ code: code2, stdout: stdout2 });
|
|
8881
|
+
});
|
|
8882
|
+
}
|
|
8883
|
+
);
|
|
8884
|
+
const { code, stdout } = goResult;
|
|
7619
8885
|
if (code !== 0 || !stdout.trim()) {
|
|
7620
8886
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7621
8887
|
}
|
|
@@ -7637,15 +8903,16 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
7637
8903
|
} catch {
|
|
7638
8904
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7639
8905
|
} finally {
|
|
7640
|
-
await
|
|
8906
|
+
await fs10.rm(tmpDir, { recursive: true, force: true });
|
|
7641
8907
|
}
|
|
7642
8908
|
}
|
|
7643
8909
|
|
|
7644
8910
|
// src/codebase-index/py-parser.ts
|
|
7645
|
-
|
|
7646
|
-
import
|
|
8911
|
+
init_win32_resolve();
|
|
8912
|
+
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
8913
|
+
import * as fs11 from "node:fs/promises";
|
|
7647
8914
|
import * as os6 from "node:os";
|
|
7648
|
-
import * as
|
|
8915
|
+
import * as path15 from "node:path";
|
|
7649
8916
|
async function parseSymbols3(opts) {
|
|
7650
8917
|
const { file, content, lang } = opts;
|
|
7651
8918
|
try {
|
|
@@ -7857,36 +9124,74 @@ visitor.visit(tree)
|
|
|
7857
9124
|
|
|
7858
9125
|
print(json.dumps([s.to_dict() for s in syms]))
|
|
7859
9126
|
`;
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
9127
|
+
function resolvePython() {
|
|
9128
|
+
const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
|
|
9129
|
+
for (const name of candidates) {
|
|
9130
|
+
const resolved = resolveWin32Command(name);
|
|
9131
|
+
const result = spawnSync(resolved, ["--version"], {
|
|
9132
|
+
stdio: "pipe",
|
|
9133
|
+
timeout: 5e3
|
|
9134
|
+
});
|
|
9135
|
+
if (result.error) continue;
|
|
9136
|
+
if (result.status !== 0) continue;
|
|
9137
|
+
return resolved;
|
|
9138
|
+
}
|
|
9139
|
+
return null;
|
|
9140
|
+
}
|
|
9141
|
+
function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
9142
|
+
return new Promise((resolve14, reject) => {
|
|
9143
|
+
let settled = false;
|
|
9144
|
+
const proc = spawn5(pyBinary, [scriptPath, filePath], {
|
|
7870
9145
|
stdio: ["pipe", "pipe", "pipe"],
|
|
7871
9146
|
windowsHide: true
|
|
7872
9147
|
});
|
|
9148
|
+
proc.on("error", (err) => {
|
|
9149
|
+
if (settled) return;
|
|
9150
|
+
settled = true;
|
|
9151
|
+
reject(err);
|
|
9152
|
+
});
|
|
7873
9153
|
proc.stdin?.write(content);
|
|
7874
9154
|
proc.stdin?.end();
|
|
7875
9155
|
let stdout = "";
|
|
7876
9156
|
proc.stdout?.on("data", (chunk) => {
|
|
7877
9157
|
stdout += chunk.toString();
|
|
7878
9158
|
});
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
)
|
|
7889
|
-
|
|
9159
|
+
proc.stderr?.resume();
|
|
9160
|
+
const timer = setTimeout(() => {
|
|
9161
|
+
if (settled) return;
|
|
9162
|
+
settled = true;
|
|
9163
|
+
proc.kill("SIGKILL");
|
|
9164
|
+
reject(new Error("timeout"));
|
|
9165
|
+
}, 15e3);
|
|
9166
|
+
timer.unref?.();
|
|
9167
|
+
proc.on("close", (code) => {
|
|
9168
|
+
if (settled) return;
|
|
9169
|
+
settled = true;
|
|
9170
|
+
clearTimeout(timer);
|
|
9171
|
+
resolve14({ code, stdout });
|
|
9172
|
+
});
|
|
9173
|
+
});
|
|
9174
|
+
}
|
|
9175
|
+
var _cachedScriptPath = null;
|
|
9176
|
+
var _cachedPyBinary = null;
|
|
9177
|
+
async function syncPyParse(filePath, content, lang) {
|
|
9178
|
+
try {
|
|
9179
|
+
if (!_cachedScriptPath) {
|
|
9180
|
+
const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
|
|
9181
|
+
await fs11.mkdir(tmpDir, { recursive: true });
|
|
9182
|
+
_cachedScriptPath = path15.join(tmpDir, "parse.py");
|
|
9183
|
+
await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
9184
|
+
}
|
|
9185
|
+
if (!_cachedPyBinary) {
|
|
9186
|
+
_cachedPyBinary = resolvePython();
|
|
9187
|
+
if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
9188
|
+
}
|
|
9189
|
+
const { code, stdout } = await spawnPyParser(
|
|
9190
|
+
_cachedPyBinary,
|
|
9191
|
+
_cachedScriptPath,
|
|
9192
|
+
filePath,
|
|
9193
|
+
content
|
|
9194
|
+
);
|
|
7890
9195
|
if (code !== 0 || !stdout.trim()) {
|
|
7891
9196
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
7892
9197
|
}
|
|
@@ -7911,10 +9216,11 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
7911
9216
|
}
|
|
7912
9217
|
|
|
7913
9218
|
// src/codebase-index/rs-parser.ts
|
|
9219
|
+
init_win32_resolve();
|
|
7914
9220
|
import { expectDefined as expectDefined3 } from "@wrongstack/core";
|
|
7915
9221
|
import { execFileSync, spawn as spawn6 } from "node:child_process";
|
|
7916
|
-
import * as
|
|
7917
|
-
import * as
|
|
9222
|
+
import * as fs12 from "node:fs/promises";
|
|
9223
|
+
import * as path16 from "node:path";
|
|
7918
9224
|
async function parseSymbols4(opts) {
|
|
7919
9225
|
const { file, content, lang } = opts;
|
|
7920
9226
|
const nativeAvailable = checkNativeParser();
|
|
@@ -7927,7 +9233,7 @@ async function parseSymbols4(opts) {
|
|
|
7927
9233
|
function checkNativeParser() {
|
|
7928
9234
|
try {
|
|
7929
9235
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
7930
|
-
const toolsDir =
|
|
9236
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
7931
9237
|
try {
|
|
7932
9238
|
execFileSync(
|
|
7933
9239
|
"cargo",
|
|
@@ -7937,7 +9243,7 @@ function checkNativeParser() {
|
|
|
7937
9243
|
"--format-version",
|
|
7938
9244
|
"1",
|
|
7939
9245
|
"--manifest-path",
|
|
7940
|
-
|
|
9246
|
+
path16.join(toolsDir, "Cargo.toml")
|
|
7941
9247
|
],
|
|
7942
9248
|
{ stdio: "pipe", windowsHide: true }
|
|
7943
9249
|
);
|
|
@@ -7951,34 +9257,49 @@ function checkNativeParser() {
|
|
|
7951
9257
|
}
|
|
7952
9258
|
async function tryNativeParse(file, content) {
|
|
7953
9259
|
try {
|
|
7954
|
-
const toolsDir =
|
|
7955
|
-
const crateDir =
|
|
7956
|
-
const tmpFile =
|
|
7957
|
-
await
|
|
7958
|
-
const
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
9260
|
+
const toolsDir = path16.join(process.cwd(), "tools");
|
|
9261
|
+
const crateDir = path16.join(toolsDir, "syn-parser");
|
|
9262
|
+
const tmpFile = path16.join(crateDir, "src", "input.rs");
|
|
9263
|
+
await fs12.writeFile(tmpFile, content, "utf8");
|
|
9264
|
+
const cargoBinary = resolveWin32Command("cargo");
|
|
9265
|
+
const result = await new Promise(
|
|
9266
|
+
(resolve14, reject) => {
|
|
9267
|
+
let settled = false;
|
|
9268
|
+
const proc = spawn6(
|
|
9269
|
+
cargoBinary,
|
|
9270
|
+
["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
|
|
9271
|
+
{
|
|
9272
|
+
cwd: process.cwd(),
|
|
9273
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9274
|
+
windowsHide: true
|
|
9275
|
+
}
|
|
9276
|
+
);
|
|
9277
|
+
proc.on("error", (err) => {
|
|
9278
|
+
if (settled) return;
|
|
9279
|
+
settled = true;
|
|
9280
|
+
reject(err);
|
|
9281
|
+
});
|
|
9282
|
+
let stdout2 = "";
|
|
9283
|
+
proc.stdout?.on("data", (chunk) => {
|
|
9284
|
+
stdout2 += chunk.toString();
|
|
9285
|
+
});
|
|
9286
|
+
proc.stderr?.resume();
|
|
9287
|
+
const timer = setTimeout(() => {
|
|
9288
|
+
if (settled) return;
|
|
9289
|
+
settled = true;
|
|
7977
9290
|
proc.kill("SIGKILL");
|
|
7978
9291
|
reject(new Error("timeout"));
|
|
7979
|
-
}, 15e3)
|
|
7980
|
-
|
|
7981
|
-
|
|
9292
|
+
}, 15e3);
|
|
9293
|
+
timer.unref?.();
|
|
9294
|
+
proc.on("close", (c) => {
|
|
9295
|
+
if (settled) return;
|
|
9296
|
+
settled = true;
|
|
9297
|
+
clearTimeout(timer);
|
|
9298
|
+
resolve14({ code: c, stdout: stdout2 });
|
|
9299
|
+
});
|
|
9300
|
+
}
|
|
9301
|
+
);
|
|
9302
|
+
const { code, stdout } = result;
|
|
7982
9303
|
if (code === 0 && stdout.trim()) {
|
|
7983
9304
|
const symbols = JSON.parse(stdout.trim());
|
|
7984
9305
|
return {
|
|
@@ -8061,7 +9382,7 @@ function regexParse(opts) {
|
|
|
8061
9382
|
|
|
8062
9383
|
// src/codebase-index/json-parser.ts
|
|
8063
9384
|
import { expectDefined as expectDefined4 } from "@wrongstack/core";
|
|
8064
|
-
import * as
|
|
9385
|
+
import * as path17 from "node:path";
|
|
8065
9386
|
function parseSymbols5(opts) {
|
|
8066
9387
|
const { file, content, lang } = opts;
|
|
8067
9388
|
try {
|
|
@@ -8073,9 +9394,9 @@ function parseSymbols5(opts) {
|
|
|
8073
9394
|
function regexParse2(opts) {
|
|
8074
9395
|
const { file, content, lang } = opts;
|
|
8075
9396
|
const symbols = [];
|
|
8076
|
-
const
|
|
8077
|
-
const isPackageJson =
|
|
8078
|
-
const isTsconfig =
|
|
9397
|
+
const basename8 = path17.basename(file).toLowerCase();
|
|
9398
|
+
const isPackageJson = basename8 === "package.json";
|
|
9399
|
+
const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
|
|
8079
9400
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
8080
9401
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
8081
9402
|
const lines = content.split("\n");
|
|
@@ -8099,11 +9420,11 @@ function regexParse2(opts) {
|
|
|
8099
9420
|
const line = lineFromOffset(offset);
|
|
8100
9421
|
symbols.push(
|
|
8101
9422
|
makeSymbol({
|
|
8102
|
-
name:
|
|
9423
|
+
name: path17.basename(file),
|
|
8103
9424
|
kind: "object",
|
|
8104
9425
|
line,
|
|
8105
9426
|
col: 0,
|
|
8106
|
-
signature: `"${
|
|
9427
|
+
signature: `"${path17.basename(file)}" = { ... }`,
|
|
8107
9428
|
file,
|
|
8108
9429
|
lang
|
|
8109
9430
|
})
|
|
@@ -8414,8 +9735,8 @@ function makeSymbol2(opts) {
|
|
|
8414
9735
|
}
|
|
8415
9736
|
|
|
8416
9737
|
// src/codebase-index/gitignore.ts
|
|
8417
|
-
import * as
|
|
8418
|
-
import * as
|
|
9738
|
+
import * as fs13 from "node:fs/promises";
|
|
9739
|
+
import * as path18 from "node:path";
|
|
8419
9740
|
import { compileGlob } from "@wrongstack/core";
|
|
8420
9741
|
function globBody(glob) {
|
|
8421
9742
|
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
@@ -8461,7 +9782,7 @@ function compileGitignore(lines) {
|
|
|
8461
9782
|
async function loadGitignoreMatcher(projectRoot) {
|
|
8462
9783
|
let lines = [];
|
|
8463
9784
|
try {
|
|
8464
|
-
const raw = await
|
|
9785
|
+
const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
|
|
8465
9786
|
lines = raw.split("\n");
|
|
8466
9787
|
} catch {
|
|
8467
9788
|
}
|
|
@@ -8472,7 +9793,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
8472
9793
|
var YIELD_EVERY_N = 50;
|
|
8473
9794
|
var PARALLEL_BATCH = 20;
|
|
8474
9795
|
function yieldEventLoop() {
|
|
8475
|
-
return new Promise((
|
|
9796
|
+
return new Promise((resolve14) => setImmediate(resolve14));
|
|
8476
9797
|
}
|
|
8477
9798
|
function throwIfAborted(signal) {
|
|
8478
9799
|
if (!signal?.aborted) return;
|
|
@@ -8519,21 +9840,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
8519
9840
|
}
|
|
8520
9841
|
let entries;
|
|
8521
9842
|
try {
|
|
8522
|
-
entries = await
|
|
9843
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
8523
9844
|
} catch {
|
|
8524
9845
|
return;
|
|
8525
9846
|
}
|
|
8526
9847
|
dirCount++;
|
|
8527
9848
|
for (const e of entries) {
|
|
8528
9849
|
if (ignoreSet.has(e.name)) continue;
|
|
8529
|
-
const full =
|
|
8530
|
-
const rel =
|
|
9850
|
+
const full = path19.join(dir, e.name);
|
|
9851
|
+
const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
|
|
8531
9852
|
if (e.isDirectory()) {
|
|
8532
9853
|
if (isGitIgnored(rel, true)) continue;
|
|
8533
9854
|
await walk(full);
|
|
8534
9855
|
} else if (e.isFile()) {
|
|
8535
9856
|
if (isGitIgnored(rel, false)) continue;
|
|
8536
|
-
const ext =
|
|
9857
|
+
const ext = path19.extname(e.name);
|
|
8537
9858
|
for (const { ext: extName, pat } of globs) {
|
|
8538
9859
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
8539
9860
|
results.push(full);
|
|
@@ -8589,7 +9910,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8589
9910
|
let files;
|
|
8590
9911
|
let discoveredFiles = null;
|
|
8591
9912
|
if (opts.files && opts.files.length > 0) {
|
|
8592
|
-
files = opts.files.map((f) =>
|
|
9913
|
+
files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
8593
9914
|
} else {
|
|
8594
9915
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
8595
9916
|
discoveredFiles = new Set(files);
|
|
@@ -8617,34 +9938,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8617
9938
|
const statOpts = signal ? { signal } : {};
|
|
8618
9939
|
const statReadParse = await Promise.allSettled(
|
|
8619
9940
|
batchFiles.map(async (file) => {
|
|
8620
|
-
let
|
|
9941
|
+
let stat17;
|
|
8621
9942
|
try {
|
|
8622
|
-
|
|
9943
|
+
stat17 = await fs14.stat(file, statOpts);
|
|
8623
9944
|
} catch (e) {
|
|
8624
9945
|
if (isAbortError(e)) throw e;
|
|
8625
9946
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8626
9947
|
}
|
|
8627
|
-
if (!
|
|
9948
|
+
if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
|
|
8628
9949
|
const lang = detectLang(file);
|
|
8629
|
-
if (!lang) return { file, stat:
|
|
9950
|
+
if (!lang) return { file, stat: stat17, lang: "", parsed: null };
|
|
8630
9951
|
const meta = existingMeta.get(file);
|
|
8631
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
8632
|
-
return { file, stat:
|
|
9952
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
|
|
9953
|
+
return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
|
|
8633
9954
|
}
|
|
8634
9955
|
let content;
|
|
8635
9956
|
try {
|
|
8636
|
-
content = await
|
|
9957
|
+
content = await fs14.readFile(file, { encoding: "utf8", signal });
|
|
8637
9958
|
} catch (e) {
|
|
8638
9959
|
if (isAbortError(e)) throw e;
|
|
8639
|
-
return { file, stat:
|
|
9960
|
+
return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8640
9961
|
}
|
|
8641
9962
|
let parsed;
|
|
8642
9963
|
try {
|
|
8643
9964
|
parsed = await parseFile(file, content, lang);
|
|
8644
9965
|
} catch (e) {
|
|
8645
|
-
return { file, stat:
|
|
9966
|
+
return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
8646
9967
|
}
|
|
8647
|
-
return { file, stat:
|
|
9968
|
+
return { file, stat: stat17, lang, parsed, content };
|
|
8648
9969
|
})
|
|
8649
9970
|
);
|
|
8650
9971
|
const batchEntries = [];
|
|
@@ -8664,7 +9985,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8664
9985
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
8665
9986
|
continue;
|
|
8666
9987
|
}
|
|
8667
|
-
const { stat:
|
|
9988
|
+
const { stat: stat17, lang, parsed } = result;
|
|
8668
9989
|
if (result.skippedMeta) {
|
|
8669
9990
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
8670
9991
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -8676,7 +9997,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8676
9997
|
store.upsertFile({
|
|
8677
9998
|
file,
|
|
8678
9999
|
lang,
|
|
8679
|
-
mtimeMs: Math.floor(
|
|
10000
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8680
10001
|
symbolCount: 0,
|
|
8681
10002
|
lastIndexed: Date.now()
|
|
8682
10003
|
});
|
|
@@ -8688,7 +10009,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8688
10009
|
store.upsertFile({
|
|
8689
10010
|
file,
|
|
8690
10011
|
lang,
|
|
8691
|
-
mtimeMs: Math.floor(
|
|
10012
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8692
10013
|
symbolCount: 0,
|
|
8693
10014
|
lastIndexed: Date.now()
|
|
8694
10015
|
});
|
|
@@ -8704,7 +10025,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8704
10025
|
lang,
|
|
8705
10026
|
symbols: parsed.symbols,
|
|
8706
10027
|
refs,
|
|
8707
|
-
mtimeMs: Math.floor(
|
|
10028
|
+
mtimeMs: Math.floor(stat17.mtimeMs),
|
|
8708
10029
|
symbolCount: parsed.symbols.length
|
|
8709
10030
|
});
|
|
8710
10031
|
deleteForFiles.push(file);
|
|
@@ -8892,7 +10213,7 @@ function resolveWorkerUrl() {
|
|
|
8892
10213
|
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8893
10214
|
try {
|
|
8894
10215
|
const url = new URL(rel, import.meta.url);
|
|
8895
|
-
if (url.protocol === "file:" &&
|
|
10216
|
+
if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
|
|
8896
10217
|
} catch {
|
|
8897
10218
|
}
|
|
8898
10219
|
}
|
|
@@ -8950,7 +10271,7 @@ function terminateWorker(reason) {
|
|
|
8950
10271
|
function callIndexOp(op, args, opts) {
|
|
8951
10272
|
const w = ensureWorker();
|
|
8952
10273
|
if (!w) return callInline(op, args, opts);
|
|
8953
|
-
return new Promise((
|
|
10274
|
+
return new Promise((resolve14, reject) => {
|
|
8954
10275
|
const id = nextRpcId++;
|
|
8955
10276
|
const timer = setTimeout(() => {
|
|
8956
10277
|
pending.delete(id);
|
|
@@ -8973,7 +10294,7 @@ function callIndexOp(op, args, opts) {
|
|
|
8973
10294
|
pending.set(id, {
|
|
8974
10295
|
resolve: (v) => {
|
|
8975
10296
|
cleanup();
|
|
8976
|
-
|
|
10297
|
+
resolve14(v);
|
|
8977
10298
|
},
|
|
8978
10299
|
reject: (e) => {
|
|
8979
10300
|
cleanup();
|
|
@@ -9312,8 +10633,8 @@ var codebaseStatsTool = {
|
|
|
9312
10633
|
};
|
|
9313
10634
|
|
|
9314
10635
|
// src/design.ts
|
|
9315
|
-
import * as
|
|
9316
|
-
import * as
|
|
10636
|
+
import * as fs16 from "node:fs/promises";
|
|
10637
|
+
import * as path20 from "node:path";
|
|
9317
10638
|
import {
|
|
9318
10639
|
applyTokenOverrides,
|
|
9319
10640
|
getDesignKitLoader,
|
|
@@ -9327,17 +10648,17 @@ import {
|
|
|
9327
10648
|
setDesignOverrides
|
|
9328
10649
|
} from "@wrongstack/core";
|
|
9329
10650
|
async function resolveReal(p) {
|
|
9330
|
-
const resolved =
|
|
10651
|
+
const resolved = path20.resolve(p);
|
|
9331
10652
|
let probe = resolved;
|
|
9332
10653
|
const missing = [];
|
|
9333
10654
|
for (; ; ) {
|
|
9334
10655
|
try {
|
|
9335
|
-
return
|
|
10656
|
+
return path20.resolve(await fs16.realpath(probe), ...missing);
|
|
9336
10657
|
} catch (err) {
|
|
9337
10658
|
if (err.code === "ENOENT") {
|
|
9338
|
-
const parent =
|
|
10659
|
+
const parent = path20.dirname(probe);
|
|
9339
10660
|
if (parent === probe) return resolved;
|
|
9340
|
-
missing.unshift(
|
|
10661
|
+
missing.unshift(path20.basename(probe));
|
|
9341
10662
|
probe = parent;
|
|
9342
10663
|
continue;
|
|
9343
10664
|
}
|
|
@@ -9503,18 +10824,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
9503
10824
|
outPath: input.out
|
|
9504
10825
|
});
|
|
9505
10826
|
const root = await resolveReal(ctx.projectRoot);
|
|
9506
|
-
const absResolved =
|
|
9507
|
-
const absParent = await resolveReal(
|
|
9508
|
-
const abs =
|
|
9509
|
-
const rel =
|
|
9510
|
-
if (rel.startsWith("..") ||
|
|
10827
|
+
const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
|
|
10828
|
+
const absParent = await resolveReal(path20.dirname(absResolved));
|
|
10829
|
+
const abs = path20.join(absParent, path20.basename(absResolved));
|
|
10830
|
+
const rel = path20.relative(root, abs);
|
|
10831
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
9511
10832
|
throw new Error(
|
|
9512
10833
|
`design: materialize path "${result.path}" would escape the project root`
|
|
9513
10834
|
);
|
|
9514
10835
|
}
|
|
9515
10836
|
let exists = false;
|
|
9516
10837
|
try {
|
|
9517
|
-
await
|
|
10838
|
+
await fs16.access(abs);
|
|
9518
10839
|
exists = true;
|
|
9519
10840
|
} catch {
|
|
9520
10841
|
}
|
|
@@ -9531,8 +10852,8 @@ ${result.content}
|
|
|
9531
10852
|
\`\`\``
|
|
9532
10853
|
};
|
|
9533
10854
|
}
|
|
9534
|
-
await
|
|
9535
|
-
await
|
|
10855
|
+
await fs16.mkdir(path20.dirname(abs), { recursive: true });
|
|
10856
|
+
await fs16.writeFile(abs, result.content);
|
|
9536
10857
|
return {
|
|
9537
10858
|
action,
|
|
9538
10859
|
kit: active.kit,
|
|
@@ -9582,8 +10903,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
|
|
|
9582
10903
|
init_util();
|
|
9583
10904
|
import { spawn as spawn7 } from "node:child_process";
|
|
9584
10905
|
import { statSync as statSync2 } from "node:fs";
|
|
9585
|
-
import * as
|
|
9586
|
-
import * as
|
|
10906
|
+
import * as fs17 from "node:fs/promises";
|
|
10907
|
+
import * as path21 from "node:path";
|
|
9587
10908
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
|
|
9588
10909
|
var diffTool = {
|
|
9589
10910
|
name: "diff",
|
|
@@ -9667,18 +10988,18 @@ function findGitDir(cwd) {
|
|
|
9667
10988
|
let dir = cwd;
|
|
9668
10989
|
for (let i = 0; i < 20; i++) {
|
|
9669
10990
|
try {
|
|
9670
|
-
const
|
|
9671
|
-
if (
|
|
10991
|
+
const stat17 = statSync2(path21.join(dir, ".git"));
|
|
10992
|
+
if (stat17.isDirectory()) return dir;
|
|
9672
10993
|
} catch {
|
|
9673
10994
|
}
|
|
9674
|
-
const parent =
|
|
10995
|
+
const parent = path21.dirname(dir);
|
|
9675
10996
|
if (parent === dir) break;
|
|
9676
10997
|
dir = parent;
|
|
9677
10998
|
}
|
|
9678
10999
|
return null;
|
|
9679
11000
|
}
|
|
9680
11001
|
function runGit(args, cwd, signal) {
|
|
9681
|
-
return new Promise((
|
|
11002
|
+
return new Promise((resolve14) => {
|
|
9682
11003
|
let stdout = "";
|
|
9683
11004
|
let stderr = "";
|
|
9684
11005
|
const child = spawn7("git", args, {
|
|
@@ -9694,8 +11015,8 @@ function runGit(args, cwd, signal) {
|
|
|
9694
11015
|
child.stderr?.on("data", (c) => {
|
|
9695
11016
|
stderr += c.toString();
|
|
9696
11017
|
});
|
|
9697
|
-
child.on("close", (code) =>
|
|
9698
|
-
child.on("error", (e) =>
|
|
11018
|
+
child.on("close", (code) => resolve14({ stdout, stderr, exitCode: code ?? 0 }));
|
|
11019
|
+
child.on("error", (e) => resolve14({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
9699
11020
|
});
|
|
9700
11021
|
}
|
|
9701
11022
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -9712,9 +11033,9 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
9712
11033
|
const results = [];
|
|
9713
11034
|
for (const file of files) {
|
|
9714
11035
|
const absPath = safeResolve(file, ctx);
|
|
9715
|
-
const
|
|
9716
|
-
if (!
|
|
9717
|
-
const content = await
|
|
11036
|
+
const stat17 = await fs17.stat(absPath).catch(() => null);
|
|
11037
|
+
if (!stat17?.isFile()) continue;
|
|
11038
|
+
const content = await fs17.readFile(absPath, "utf8");
|
|
9718
11039
|
const lines = content.split(/\r?\n/);
|
|
9719
11040
|
results.push(formatWithLineNumbers(file, lines));
|
|
9720
11041
|
}
|
|
@@ -9734,7 +11055,7 @@ ${numbered}`;
|
|
|
9734
11055
|
|
|
9735
11056
|
// src/document.ts
|
|
9736
11057
|
init_util();
|
|
9737
|
-
import * as
|
|
11058
|
+
import * as fs18 from "node:fs/promises";
|
|
9738
11059
|
var documentTool = {
|
|
9739
11060
|
name: "document",
|
|
9740
11061
|
category: "Project",
|
|
@@ -9778,7 +11099,7 @@ var documentTool = {
|
|
|
9778
11099
|
const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
9779
11100
|
for (const absPath of fileList) {
|
|
9780
11101
|
try {
|
|
9781
|
-
const content = await
|
|
11102
|
+
const content = await fs18.readFile(absPath, "utf8");
|
|
9782
11103
|
filesProcessed++;
|
|
9783
11104
|
const processed = processFile(
|
|
9784
11105
|
content,
|
|
@@ -9801,91 +11122,507 @@ var documentTool = {
|
|
|
9801
11122
|
}
|
|
9802
11123
|
}
|
|
9803
11124
|
return {
|
|
9804
|
-
files_processed: filesProcessed,
|
|
9805
|
-
items_documented: itemsDocumented,
|
|
9806
|
-
results,
|
|
9807
|
-
style
|
|
11125
|
+
files_processed: filesProcessed,
|
|
11126
|
+
items_documented: itemsDocumented,
|
|
11127
|
+
results,
|
|
11128
|
+
style
|
|
11129
|
+
};
|
|
11130
|
+
}
|
|
11131
|
+
};
|
|
11132
|
+
async function resolveFiles(filesInput, cwd) {
|
|
11133
|
+
const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
|
|
11134
|
+
const resolved = [];
|
|
11135
|
+
for (const f of files) {
|
|
11136
|
+
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
11137
|
+
try {
|
|
11138
|
+
const stat17 = await fs18.stat(absPath);
|
|
11139
|
+
if (stat17.isFile()) resolved.push(absPath);
|
|
11140
|
+
} catch {
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
return resolved;
|
|
11144
|
+
}
|
|
11145
|
+
function processFile(content, absPath, _style, _overwrite, target) {
|
|
11146
|
+
const results = [];
|
|
11147
|
+
const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
11148
|
+
const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
|
|
11149
|
+
const classRegex = /class\s+(\w+)/g;
|
|
11150
|
+
const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
|
|
11151
|
+
const allMatches = [];
|
|
11152
|
+
if (target === "all" || target === "function") {
|
|
11153
|
+
for (const m of content.matchAll(functionRegex)) {
|
|
11154
|
+
if (!m[1]) continue;
|
|
11155
|
+
allMatches.push({
|
|
11156
|
+
name: m[1],
|
|
11157
|
+
sig: m[2] ?? "",
|
|
11158
|
+
type: "function",
|
|
11159
|
+
line: content.slice(0, m.index).split("\n").length
|
|
11160
|
+
});
|
|
11161
|
+
}
|
|
11162
|
+
for (const m of content.matchAll(arrowRegex)) {
|
|
11163
|
+
if (!m[1]) continue;
|
|
11164
|
+
allMatches.push({
|
|
11165
|
+
name: m[1],
|
|
11166
|
+
sig: m[2] ?? "",
|
|
11167
|
+
type: "arrow",
|
|
11168
|
+
line: content.slice(0, m.index).split("\n").length
|
|
11169
|
+
});
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
if (target === "all" || target === "class") {
|
|
11173
|
+
for (const m of content.matchAll(classRegex)) {
|
|
11174
|
+
if (!m[1]) continue;
|
|
11175
|
+
allMatches.push({
|
|
11176
|
+
name: m[1],
|
|
11177
|
+
sig: "",
|
|
11178
|
+
type: "class",
|
|
11179
|
+
line: content.slice(0, m.index).split("\n").length
|
|
11180
|
+
});
|
|
11181
|
+
}
|
|
11182
|
+
}
|
|
11183
|
+
if (target === "all" || target === "type") {
|
|
11184
|
+
for (const m of content.matchAll(typeRegex)) {
|
|
11185
|
+
if (!m[1]) continue;
|
|
11186
|
+
allMatches.push({
|
|
11187
|
+
name: m[1],
|
|
11188
|
+
sig: m[0] ?? "",
|
|
11189
|
+
type: "type",
|
|
11190
|
+
line: content.slice(0, m.index).split("\n").length
|
|
11191
|
+
});
|
|
11192
|
+
}
|
|
11193
|
+
}
|
|
11194
|
+
for (const m of allMatches) {
|
|
11195
|
+
results.push({
|
|
11196
|
+
path: absPath,
|
|
11197
|
+
name: m.name,
|
|
11198
|
+
signature: m.sig,
|
|
11199
|
+
docstring: `/** ${m.name} - documented at line ${m.line} */`,
|
|
11200
|
+
status: "skipped"
|
|
11201
|
+
});
|
|
11202
|
+
}
|
|
11203
|
+
return results;
|
|
11204
|
+
}
|
|
11205
|
+
|
|
11206
|
+
// src/e2e.ts
|
|
11207
|
+
init_util();
|
|
11208
|
+
import { open, readdir as readdir5 } from "node:fs/promises";
|
|
11209
|
+
import * as path22 from "node:path";
|
|
11210
|
+
async function readBoundedText(filePath, maxBytes) {
|
|
11211
|
+
let handle;
|
|
11212
|
+
try {
|
|
11213
|
+
handle = await open(filePath, "r");
|
|
11214
|
+
const info = await handle.stat();
|
|
11215
|
+
if (!info.isFile() || info.size > maxBytes) return void 0;
|
|
11216
|
+
const buffer = Buffer.alloc(info.size);
|
|
11217
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
11218
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
11219
|
+
} catch {
|
|
11220
|
+
return void 0;
|
|
11221
|
+
} finally {
|
|
11222
|
+
await handle?.close().catch(() => void 0);
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
var CONFIG_NAMES = /* @__PURE__ */ new Map([
|
|
11226
|
+
["playwright.config.ts", "playwright"],
|
|
11227
|
+
["playwright.config.js", "playwright"],
|
|
11228
|
+
["playwright.config.mts", "playwright"],
|
|
11229
|
+
["playwright.config.mjs", "playwright"],
|
|
11230
|
+
["playwright.config.cts", "playwright"],
|
|
11231
|
+
["playwright.config.cjs", "playwright"],
|
|
11232
|
+
["cypress.config.ts", "cypress"],
|
|
11233
|
+
["cypress.config.js", "cypress"],
|
|
11234
|
+
["cypress.config.mts", "cypress"],
|
|
11235
|
+
["cypress.config.mjs", "cypress"],
|
|
11236
|
+
["cypress.config.cts", "cypress"],
|
|
11237
|
+
["cypress.config.cjs", "cypress"],
|
|
11238
|
+
["cypress.json", "cypress"]
|
|
11239
|
+
]);
|
|
11240
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
11241
|
+
".git",
|
|
11242
|
+
".hg",
|
|
11243
|
+
".svn",
|
|
11244
|
+
".wrongstack",
|
|
11245
|
+
"node_modules",
|
|
11246
|
+
"dist",
|
|
11247
|
+
"build",
|
|
11248
|
+
"coverage",
|
|
11249
|
+
"coverage-html",
|
|
11250
|
+
"playwright-report",
|
|
11251
|
+
"test-results",
|
|
11252
|
+
".next"
|
|
11253
|
+
]);
|
|
11254
|
+
var MAX_SCAN_DIRECTORIES = 4e3;
|
|
11255
|
+
var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
11256
|
+
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
11257
|
+
var MAX_SPEC_SAMPLES = 20;
|
|
11258
|
+
function relativePath(root, target) {
|
|
11259
|
+
const value = path22.relative(root, target) || ".";
|
|
11260
|
+
return value.split(path22.sep).join("/");
|
|
11261
|
+
}
|
|
11262
|
+
function escapeRegExp(value) {
|
|
11263
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11264
|
+
}
|
|
11265
|
+
function staticString(source, property) {
|
|
11266
|
+
const key = escapeRegExp(property);
|
|
11267
|
+
const direct = source.match(
|
|
11268
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11269
|
+
);
|
|
11270
|
+
if (direct?.[2] && !direct[2].includes("${")) return direct[2];
|
|
11271
|
+
const sameLineFallback = source.match(
|
|
11272
|
+
new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
|
|
11273
|
+
);
|
|
11274
|
+
return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
|
|
11275
|
+
}
|
|
11276
|
+
function staticNumber(source, property) {
|
|
11277
|
+
const key = escapeRegExp(property);
|
|
11278
|
+
const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
|
|
11279
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
|
|
11280
|
+
}
|
|
11281
|
+
function serverHints(framework, source) {
|
|
11282
|
+
if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
|
|
11283
|
+
const command = staticString(source, "command");
|
|
11284
|
+
const url = staticString(source, "url");
|
|
11285
|
+
const port = staticNumber(source, "port");
|
|
11286
|
+
return [
|
|
11287
|
+
{
|
|
11288
|
+
kind: "managed",
|
|
11289
|
+
...command ? { command } : {},
|
|
11290
|
+
...url ? { url } : {},
|
|
11291
|
+
...port ? { port } : {},
|
|
11292
|
+
source: "static-config",
|
|
11293
|
+
incomplete: !command || !url && !port
|
|
11294
|
+
}
|
|
11295
|
+
];
|
|
11296
|
+
}
|
|
11297
|
+
const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
|
|
11298
|
+
return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
|
|
11299
|
+
}
|
|
11300
|
+
function configuredSpecPattern(framework, source) {
|
|
11301
|
+
if (framework === "playwright") {
|
|
11302
|
+
const testDir = staticString(source, "testDir") ?? "./tests";
|
|
11303
|
+
return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
|
|
11304
|
+
}
|
|
11305
|
+
return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
|
|
11306
|
+
}
|
|
11307
|
+
async function scanWorkspace(root, maxDepth, signal) {
|
|
11308
|
+
const result = {
|
|
11309
|
+
configs: [],
|
|
11310
|
+
packageFiles: [],
|
|
11311
|
+
scannedDirectories: 0,
|
|
11312
|
+
truncated: false
|
|
11313
|
+
};
|
|
11314
|
+
const queue = [{ directory: root, depth: 0 }];
|
|
11315
|
+
while (queue.length > 0) {
|
|
11316
|
+
signal.throwIfAborted();
|
|
11317
|
+
const current = queue.shift();
|
|
11318
|
+
if (!current) break;
|
|
11319
|
+
result.scannedDirectories += 1;
|
|
11320
|
+
if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
|
|
11321
|
+
result.truncated = true;
|
|
11322
|
+
break;
|
|
11323
|
+
}
|
|
11324
|
+
let entries;
|
|
11325
|
+
try {
|
|
11326
|
+
entries = await readdir5(current.directory, { withFileTypes: true });
|
|
11327
|
+
} catch {
|
|
11328
|
+
continue;
|
|
11329
|
+
}
|
|
11330
|
+
for (const entry of entries) {
|
|
11331
|
+
signal.throwIfAborted();
|
|
11332
|
+
const absolutePath = path22.join(current.directory, entry.name);
|
|
11333
|
+
if (entry.isFile()) {
|
|
11334
|
+
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
11335
|
+
const framework = CONFIG_NAMES.get(entry.name);
|
|
11336
|
+
if (framework) result.configs.push({ framework, absolutePath });
|
|
11337
|
+
} else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
|
|
11338
|
+
queue.push({ directory: absolutePath, depth: current.depth + 1 });
|
|
11339
|
+
}
|
|
11340
|
+
}
|
|
11341
|
+
}
|
|
11342
|
+
return result;
|
|
11343
|
+
}
|
|
11344
|
+
async function readPackageInfo(packagePath) {
|
|
11345
|
+
try {
|
|
11346
|
+
const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
|
|
11347
|
+
if (raw === void 0) return void 0;
|
|
11348
|
+
const parsed = JSON.parse(raw);
|
|
11349
|
+
const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
|
|
11350
|
+
Object.entries(parsed.scripts).filter(
|
|
11351
|
+
(entry) => typeof entry[1] === "string"
|
|
11352
|
+
)
|
|
11353
|
+
) : {};
|
|
11354
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
11355
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
11356
|
+
const value = parsed[field];
|
|
11357
|
+
if (!value || typeof value !== "object") continue;
|
|
11358
|
+
for (const name of Object.keys(value)) dependencies.add(name);
|
|
11359
|
+
}
|
|
11360
|
+
const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
|
|
11361
|
+
const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
|
|
11362
|
+
return { path: packagePath, scripts, dependencies, packageManager };
|
|
11363
|
+
} catch {
|
|
11364
|
+
return void 0;
|
|
11365
|
+
}
|
|
11366
|
+
}
|
|
11367
|
+
function frameworkFromPackage(info) {
|
|
11368
|
+
const found = /* @__PURE__ */ new Set();
|
|
11369
|
+
if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
|
|
11370
|
+
found.add("playwright");
|
|
11371
|
+
}
|
|
11372
|
+
if (info.dependencies.has("cypress")) found.add("cypress");
|
|
11373
|
+
for (const command of Object.values(info.scripts)) {
|
|
11374
|
+
if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
|
|
11375
|
+
if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
|
|
11376
|
+
}
|
|
11377
|
+
return [...found];
|
|
11378
|
+
}
|
|
11379
|
+
async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
11380
|
+
if (declared) return declared;
|
|
11381
|
+
let directory = projectRoot;
|
|
11382
|
+
while (true) {
|
|
11383
|
+
const names = /* @__PURE__ */ new Set();
|
|
11384
|
+
try {
|
|
11385
|
+
for (const entry of await readdir5(directory)) names.add(entry);
|
|
11386
|
+
} catch {
|
|
11387
|
+
}
|
|
11388
|
+
if (names.has("pnpm-lock.yaml")) return "pnpm";
|
|
11389
|
+
if (names.has("yarn.lock")) return "yarn";
|
|
11390
|
+
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
11391
|
+
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
11392
|
+
if (directory === scanRoot) break;
|
|
11393
|
+
const parent = path22.dirname(directory);
|
|
11394
|
+
const relativeParent = path22.relative(scanRoot, parent);
|
|
11395
|
+
if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
|
|
11396
|
+
break;
|
|
11397
|
+
}
|
|
11398
|
+
directory = parent;
|
|
11399
|
+
}
|
|
11400
|
+
return "npm";
|
|
11401
|
+
}
|
|
11402
|
+
function matchingScripts(info, framework) {
|
|
11403
|
+
if (!info) return [];
|
|
11404
|
+
const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
|
|
11405
|
+
return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
|
|
11406
|
+
const preferred = ["test:e2e", "e2e", "e2e:test"];
|
|
11407
|
+
const ai = preferred.indexOf(a);
|
|
11408
|
+
const bi = preferred.indexOf(b);
|
|
11409
|
+
if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
11410
|
+
return a.localeCompare(b);
|
|
11411
|
+
});
|
|
11412
|
+
}
|
|
11413
|
+
function executionPlan(framework, manager, root, configPath, scripts) {
|
|
11414
|
+
const script = scripts[0];
|
|
11415
|
+
if (script) {
|
|
11416
|
+
return {
|
|
11417
|
+
command: manager,
|
|
11418
|
+
args: ["run", script],
|
|
11419
|
+
cwd: root,
|
|
11420
|
+
source: "package-script",
|
|
11421
|
+
script
|
|
9808
11422
|
};
|
|
9809
11423
|
}
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
11424
|
+
const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
|
|
11425
|
+
if (configPath) {
|
|
11426
|
+
runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
|
|
11427
|
+
}
|
|
11428
|
+
return {
|
|
11429
|
+
command: manager === "bun" ? "bunx" : manager,
|
|
11430
|
+
args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
|
|
11431
|
+
cwd: root,
|
|
11432
|
+
source: "framework-default"
|
|
11433
|
+
};
|
|
11434
|
+
}
|
|
11435
|
+
function isSpec(framework, filename) {
|
|
11436
|
+
if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11437
|
+
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
11438
|
+
}
|
|
11439
|
+
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
11440
|
+
const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
|
|
11441
|
+
const samples = [];
|
|
11442
|
+
let count = 0;
|
|
11443
|
+
let scanned = 0;
|
|
11444
|
+
const queue = [...roots];
|
|
11445
|
+
while (queue.length > 0) {
|
|
11446
|
+
signal.throwIfAborted();
|
|
11447
|
+
const directory = queue.shift();
|
|
11448
|
+
if (!directory) break;
|
|
11449
|
+
scanned += 1;
|
|
11450
|
+
if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
|
|
11451
|
+
let entries;
|
|
9816
11452
|
try {
|
|
9817
|
-
|
|
9818
|
-
if (stat15.isFile()) resolved.push(absPath);
|
|
11453
|
+
entries = await readdir5(directory, { withFileTypes: true });
|
|
9819
11454
|
} catch {
|
|
11455
|
+
continue;
|
|
11456
|
+
}
|
|
11457
|
+
for (const entry of entries) {
|
|
11458
|
+
signal.throwIfAborted();
|
|
11459
|
+
const target = path22.join(directory, entry.name);
|
|
11460
|
+
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
11461
|
+
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
11462
|
+
count += 1;
|
|
11463
|
+
if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
|
|
11464
|
+
}
|
|
9820
11465
|
}
|
|
9821
11466
|
}
|
|
9822
|
-
return
|
|
11467
|
+
return { count, samples, truncated: count > samples.length };
|
|
9823
11468
|
}
|
|
9824
|
-
function
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
type: "function",
|
|
9838
|
-
line: content.slice(0, m.index).split("\n").length
|
|
9839
|
-
});
|
|
9840
|
-
}
|
|
9841
|
-
for (const m of content.matchAll(arrowRegex)) {
|
|
9842
|
-
if (!m[1]) continue;
|
|
9843
|
-
allMatches.push({
|
|
9844
|
-
name: m[1],
|
|
9845
|
-
sig: m[2] ?? "",
|
|
9846
|
-
type: "arrow",
|
|
9847
|
-
line: content.slice(0, m.index).split("\n").length
|
|
9848
|
-
});
|
|
9849
|
-
}
|
|
11469
|
+
async function readConfig(configPath) {
|
|
11470
|
+
if (!configPath) return "";
|
|
11471
|
+
return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
|
|
11472
|
+
}
|
|
11473
|
+
function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
11474
|
+
let directory = projectRoot;
|
|
11475
|
+
while (true) {
|
|
11476
|
+
const found = packagesByDirectory.get(directory);
|
|
11477
|
+
if (found) return found;
|
|
11478
|
+
if (directory === scanRoot) return void 0;
|
|
11479
|
+
const parent = path22.dirname(directory);
|
|
11480
|
+
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
11481
|
+
directory = parent;
|
|
9850
11482
|
}
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
11483
|
+
}
|
|
11484
|
+
async function discoverE2EProjects(root, options) {
|
|
11485
|
+
const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
|
|
11486
|
+
const scan = await scanWorkspace(root, maxDepth, options.signal);
|
|
11487
|
+
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
11488
|
+
(info) => Boolean(info)
|
|
11489
|
+
);
|
|
11490
|
+
const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
|
|
11491
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
11492
|
+
for (const config of scan.configs) {
|
|
11493
|
+
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
11494
|
+
continue;
|
|
9860
11495
|
}
|
|
11496
|
+
const projectRoot = path22.dirname(config.absolutePath);
|
|
11497
|
+
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
11498
|
+
framework: config.framework,
|
|
11499
|
+
root: projectRoot,
|
|
11500
|
+
configPath: config.absolutePath
|
|
11501
|
+
});
|
|
9861
11502
|
}
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
11503
|
+
for (const info of packages) {
|
|
11504
|
+
const projectRoot = path22.dirname(info.path);
|
|
11505
|
+
for (const framework of frameworkFromPackage(info)) {
|
|
11506
|
+
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
11507
|
+
continue;
|
|
11508
|
+
const key = `${framework}:${projectRoot}`;
|
|
11509
|
+
if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
|
|
11510
|
+
}
|
|
11511
|
+
}
|
|
11512
|
+
const projects = [];
|
|
11513
|
+
for (const candidate of candidates.values()) {
|
|
11514
|
+
options.signal.throwIfAborted();
|
|
11515
|
+
const info = nearestPackage(candidate.root, packagesByDirectory, root);
|
|
11516
|
+
const source = await readConfig(candidate.configPath);
|
|
11517
|
+
const scripts = matchingScripts(info, candidate.framework);
|
|
11518
|
+
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
11519
|
+
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
11520
|
+
const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
|
|
11521
|
+
const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
|
|
11522
|
+
const unsafeTestDirectory = Boolean(
|
|
11523
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
|
|
11524
|
+
);
|
|
11525
|
+
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
11526
|
+
const warnings = [];
|
|
11527
|
+
if (!candidate.configPath)
|
|
11528
|
+
warnings.push("Framework dependency or script found without a config file.");
|
|
11529
|
+
if (candidate.configPath && !source) {
|
|
11530
|
+
warnings.push("Config could not be read or exceeded the static inspection limit.");
|
|
11531
|
+
}
|
|
11532
|
+
if (unsafeTestDirectory) {
|
|
11533
|
+
warnings.push(
|
|
11534
|
+
"Configured testDir resolves outside the requested discovery root and was not scanned."
|
|
11535
|
+
);
|
|
9871
11536
|
}
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
11537
|
+
const hints = serverHints(candidate.framework, source);
|
|
11538
|
+
if (hints.some((hint) => hint.incomplete)) {
|
|
11539
|
+
warnings.push("Configured server contains dynamic values; only static hints are shown.");
|
|
11540
|
+
}
|
|
11541
|
+
projects.push({
|
|
11542
|
+
framework: candidate.framework,
|
|
11543
|
+
root: relativePath(root, candidate.root),
|
|
11544
|
+
...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
|
|
11545
|
+
...info ? { packagePath: relativePath(root, info.path) } : {},
|
|
11546
|
+
packageManager: manager,
|
|
11547
|
+
evidence: [
|
|
11548
|
+
...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
|
|
11549
|
+
...info?.dependencies.has(
|
|
11550
|
+
candidate.framework === "playwright" ? "@playwright/test" : "cypress"
|
|
11551
|
+
) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
|
|
11552
|
+
...scripts.map((script) => `script:${script}`)
|
|
11553
|
+
],
|
|
11554
|
+
scripts,
|
|
11555
|
+
serverHints: hints,
|
|
11556
|
+
specPattern: configuredSpecPattern(candidate.framework, source),
|
|
11557
|
+
specCount: specs.count,
|
|
11558
|
+
specSamples: specs.samples,
|
|
11559
|
+
specsTruncated: specs.truncated,
|
|
11560
|
+
execution: executionPlan(
|
|
11561
|
+
candidate.framework,
|
|
11562
|
+
manager,
|
|
11563
|
+
relativePath(root, candidate.root),
|
|
11564
|
+
candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
|
|
11565
|
+
scripts
|
|
11566
|
+
),
|
|
11567
|
+
warnings
|
|
9880
11568
|
});
|
|
9881
11569
|
}
|
|
9882
|
-
|
|
11570
|
+
projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
|
|
11571
|
+
return {
|
|
11572
|
+
root,
|
|
11573
|
+
projects,
|
|
11574
|
+
scannedDirectories: scan.scannedDirectories,
|
|
11575
|
+
scanTruncated: scan.truncated,
|
|
11576
|
+
warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
|
|
11577
|
+
};
|
|
9883
11578
|
}
|
|
11579
|
+
var e2ePlanTool = {
|
|
11580
|
+
name: "e2e_plan",
|
|
11581
|
+
category: "Code Quality",
|
|
11582
|
+
description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
|
|
11583
|
+
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.",
|
|
11584
|
+
permission: "auto",
|
|
11585
|
+
mutating: false,
|
|
11586
|
+
riskTier: "safe",
|
|
11587
|
+
capabilities: ["fs.read"],
|
|
11588
|
+
icon: "test",
|
|
11589
|
+
timeoutMs: 3e4,
|
|
11590
|
+
inputSchema: {
|
|
11591
|
+
type: "object",
|
|
11592
|
+
properties: {
|
|
11593
|
+
cwd: { type: "string", description: "Directory inside the project to inspect." },
|
|
11594
|
+
framework: {
|
|
11595
|
+
type: "string",
|
|
11596
|
+
enum: ["all", "playwright", "cypress"],
|
|
11597
|
+
description: "Optional framework filter; defaults to all."
|
|
11598
|
+
},
|
|
11599
|
+
maxDepth: {
|
|
11600
|
+
type: "integer",
|
|
11601
|
+
minimum: 0,
|
|
11602
|
+
maximum: 8,
|
|
11603
|
+
description: "Maximum workspace discovery depth; defaults to 5."
|
|
11604
|
+
},
|
|
11605
|
+
includeSpecs: {
|
|
11606
|
+
type: "boolean",
|
|
11607
|
+
description: "Count and sample specs; defaults to true."
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
},
|
|
11611
|
+
async execute(input, ctx, opts) {
|
|
11612
|
+
const root = await safeResolveReal(input.cwd ?? ".", ctx);
|
|
11613
|
+
return discoverE2EProjects(root, {
|
|
11614
|
+
framework: input.framework,
|
|
11615
|
+
maxDepth: input.maxDepth,
|
|
11616
|
+
includeSpecs: input.includeSpecs,
|
|
11617
|
+
signal: opts.signal
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
11620
|
+
};
|
|
9884
11621
|
|
|
9885
11622
|
// src/edit.ts
|
|
9886
|
-
import * as
|
|
11623
|
+
import * as fs19 from "node:fs/promises";
|
|
9887
11624
|
import {
|
|
9888
|
-
atomicWrite,
|
|
11625
|
+
atomicWrite as atomicWrite2,
|
|
9889
11626
|
detectNewlineStyle,
|
|
9890
11627
|
normalizeToLf,
|
|
9891
11628
|
ToolValidationError,
|
|
@@ -9922,25 +11659,46 @@ function findLadderMatches(fileLf, oldLf) {
|
|
|
9922
11659
|
const needleLines = oldLf.split("\n");
|
|
9923
11660
|
if (needleLines.length > fileLines.length) return void 0;
|
|
9924
11661
|
const offsets = lineOffsets(fileLines);
|
|
11662
|
+
const fileTrimEnd = fileLines.map((l) => l.trimEnd());
|
|
11663
|
+
const needleTrimEnd = needleLines.map((l) => l.trimEnd());
|
|
9925
11664
|
const trailing = windowScan(
|
|
9926
|
-
|
|
9927
|
-
|
|
11665
|
+
fileTrimEnd,
|
|
11666
|
+
needleTrimEnd,
|
|
9928
11667
|
offsets,
|
|
9929
|
-
|
|
11668
|
+
fileLines,
|
|
11669
|
+
(a, b) => a === b
|
|
9930
11670
|
);
|
|
9931
11671
|
if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
|
|
9932
|
-
const normalizedLen =
|
|
11672
|
+
const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
|
|
9933
11673
|
if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
|
|
9934
|
-
const
|
|
11674
|
+
const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
|
|
11675
|
+
const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
|
|
11676
|
+
const normalized = windowScan(
|
|
11677
|
+
fileTrimmed,
|
|
11678
|
+
needleTrimmed,
|
|
11679
|
+
offsets,
|
|
11680
|
+
fileLines,
|
|
11681
|
+
(a, b) => a === b
|
|
11682
|
+
);
|
|
9935
11683
|
if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
|
|
9936
11684
|
return fuzzyScan(fileLines, needleLines, offsets);
|
|
9937
11685
|
}
|
|
9938
11686
|
function lineAt(text, pos) {
|
|
11687
|
+
if (pos < 512) {
|
|
11688
|
+
let line2 = 1;
|
|
11689
|
+
for (let i = 0; i < pos; i++) {
|
|
11690
|
+
if (text.charCodeAt(i) === 10) line2++;
|
|
11691
|
+
}
|
|
11692
|
+
return line2;
|
|
11693
|
+
}
|
|
9939
11694
|
let line = 1;
|
|
9940
|
-
|
|
9941
|
-
|
|
11695
|
+
let search = 0;
|
|
11696
|
+
while (true) {
|
|
11697
|
+
const idx = text.indexOf("\n", search);
|
|
11698
|
+
if (idx === -1 || idx >= pos) return line;
|
|
11699
|
+
search = idx + 1;
|
|
11700
|
+
line++;
|
|
9942
11701
|
}
|
|
9943
|
-
return line;
|
|
9944
11702
|
}
|
|
9945
11703
|
function lineOffsets(lines) {
|
|
9946
11704
|
const out = new Array(lines.length);
|
|
@@ -9959,19 +11717,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
|
|
|
9959
11717
|
startLine: start + 1
|
|
9960
11718
|
};
|
|
9961
11719
|
}
|
|
9962
|
-
function windowScan(
|
|
11720
|
+
function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
|
|
9963
11721
|
const n = needleLines.length;
|
|
9964
11722
|
const out = [];
|
|
9965
|
-
for (let i = 0; i + n <=
|
|
11723
|
+
for (let i = 0; i + n <= comparisonLines.length; i++) {
|
|
9966
11724
|
let all = true;
|
|
9967
11725
|
for (let j = 0; j < n; j++) {
|
|
9968
|
-
if (!eq(
|
|
11726
|
+
if (!eq(comparisonLines[i + j], needleLines[j])) {
|
|
9969
11727
|
all = false;
|
|
9970
11728
|
break;
|
|
9971
11729
|
}
|
|
9972
11730
|
}
|
|
9973
11731
|
if (all) {
|
|
9974
|
-
out.push(windowToMatch(
|
|
11732
|
+
out.push(windowToMatch(originalLines, offsets, i, n));
|
|
9975
11733
|
i += n - 1;
|
|
9976
11734
|
}
|
|
9977
11735
|
}
|
|
@@ -10098,7 +11856,7 @@ function prefixSimilarity(a, b) {
|
|
|
10098
11856
|
}
|
|
10099
11857
|
|
|
10100
11858
|
// src/_syntax-check.ts
|
|
10101
|
-
import * as
|
|
11859
|
+
import * as path23 from "node:path";
|
|
10102
11860
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
10103
11861
|
var MAX_CHECK_CHARS = 15e5;
|
|
10104
11862
|
var MAX_ERRORS = 5;
|
|
@@ -10122,15 +11880,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
10122
11880
|
return { errors, preExisting };
|
|
10123
11881
|
}
|
|
10124
11882
|
function isJsoncFile(filePath) {
|
|
10125
|
-
const base =
|
|
11883
|
+
const base = path23.basename(filePath).toLowerCase();
|
|
10126
11884
|
if (base.endsWith(".jsonc")) return true;
|
|
10127
11885
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
10128
|
-
const dir =
|
|
11886
|
+
const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
|
|
10129
11887
|
return dir === ".vscode";
|
|
10130
11888
|
}
|
|
10131
11889
|
async function parseErrors(filePath, content) {
|
|
10132
11890
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
10133
|
-
const ext =
|
|
11891
|
+
const ext = path23.extname(filePath).toLowerCase();
|
|
10134
11892
|
if (ext === ".json" || ext === ".jsonc") {
|
|
10135
11893
|
try {
|
|
10136
11894
|
JSON.parse(content);
|
|
@@ -10157,7 +11915,7 @@ async function parseErrors(filePath, content) {
|
|
|
10157
11915
|
ts2.ScriptKind.JSX
|
|
10158
11916
|
);
|
|
10159
11917
|
const sourceFile = ts2.createSourceFile(
|
|
10160
|
-
|
|
11918
|
+
path23.basename(filePath),
|
|
10161
11919
|
content,
|
|
10162
11920
|
ts2.ScriptTarget.Latest,
|
|
10163
11921
|
/* setParentNodes */
|
|
@@ -10231,7 +11989,7 @@ var editTool = {
|
|
|
10231
11989
|
});
|
|
10232
11990
|
}
|
|
10233
11991
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10234
|
-
const
|
|
11992
|
+
const stat17 = await fs19.stat(absPath).catch((err) => {
|
|
10235
11993
|
if (err.code === "ENOENT") {
|
|
10236
11994
|
throw new ToolValidationError({
|
|
10237
11995
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -10241,15 +11999,15 @@ var editTool = {
|
|
|
10241
11999
|
}
|
|
10242
12000
|
throw err;
|
|
10243
12001
|
});
|
|
10244
|
-
if (!
|
|
12002
|
+
if (!stat17.isFile()) {
|
|
10245
12003
|
throw new ToolValidationError({
|
|
10246
12004
|
message: `edit: "${input.path}" is not a regular file`,
|
|
10247
12005
|
field: "path"
|
|
10248
12006
|
});
|
|
10249
12007
|
}
|
|
10250
12008
|
const autoRead = !ctx.hasRead(absPath);
|
|
10251
|
-
const original = await
|
|
10252
|
-
const updated = await
|
|
12009
|
+
const original = await fs19.readFile(absPath, "utf8");
|
|
12010
|
+
const updated = await fs19.stat(absPath);
|
|
10253
12011
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
10254
12012
|
const originalHash = sha256hex(original);
|
|
10255
12013
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
@@ -10271,7 +12029,7 @@ var editTool = {
|
|
|
10271
12029
|
});
|
|
10272
12030
|
}
|
|
10273
12031
|
}
|
|
10274
|
-
if (autoRead && updated.mtimeMs >
|
|
12032
|
+
if (autoRead && updated.mtimeMs > stat17.mtimeMs + mtimeTolerance) {
|
|
10275
12033
|
throw new ToolValidationError({
|
|
10276
12034
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
10277
12035
|
field: "path",
|
|
@@ -10293,8 +12051,10 @@ var editTool = {
|
|
|
10293
12051
|
note: autoReadNote
|
|
10294
12052
|
};
|
|
10295
12053
|
}
|
|
12054
|
+
opts?.signal?.throwIfAborted();
|
|
10296
12055
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
10297
12056
|
if (!ladder) {
|
|
12057
|
+
opts?.signal?.throwIfAborted();
|
|
10298
12058
|
const hint = nearestMatchHint(fileLf, oldLf);
|
|
10299
12059
|
throw new ToolValidationError({
|
|
10300
12060
|
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
@@ -10353,8 +12113,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
10353
12113
|
}
|
|
10354
12114
|
const newFile = toStyle(newFileLf, style);
|
|
10355
12115
|
opts?.signal?.throwIfAborted();
|
|
10356
|
-
await
|
|
10357
|
-
const written = await
|
|
12116
|
+
await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
|
|
12117
|
+
const written = await fs19.stat(absPath);
|
|
10358
12118
|
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
10359
12119
|
ctx.session.recordFileChange({
|
|
10360
12120
|
path: absPath,
|
|
@@ -10362,6 +12122,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
10362
12122
|
before: original,
|
|
10363
12123
|
after: newFile
|
|
10364
12124
|
});
|
|
12125
|
+
opts?.signal?.throwIfAborted();
|
|
10365
12126
|
const diff = unifiedDiff(original, newFile, {
|
|
10366
12127
|
fromFile: input.path,
|
|
10367
12128
|
toFile: input.path
|
|
@@ -11314,7 +13075,7 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
11314
13075
|
// and glob patterns that could expand to dangerous targets.
|
|
11315
13076
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
11316
13077
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
11317
|
-
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
13078
|
+
rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
11318
13079
|
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
11319
13080
|
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
11320
13081
|
npm: [],
|
|
@@ -11482,7 +13243,7 @@ var execTool = {
|
|
|
11482
13243
|
}
|
|
11483
13244
|
};
|
|
11484
13245
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
11485
|
-
return new Promise((
|
|
13246
|
+
return new Promise((resolve14) => {
|
|
11486
13247
|
let stdout = "";
|
|
11487
13248
|
let stderr = "";
|
|
11488
13249
|
let killed = false;
|
|
@@ -11490,7 +13251,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
11490
13251
|
const finish = (result) => {
|
|
11491
13252
|
if (resolvedOnce.value) return;
|
|
11492
13253
|
resolvedOnce.value = true;
|
|
11493
|
-
|
|
13254
|
+
resolve14(result);
|
|
11494
13255
|
};
|
|
11495
13256
|
const startedAt = Date.now();
|
|
11496
13257
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
@@ -11599,13 +13360,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
|
|
|
11599
13360
|
import TurndownService from "turndown";
|
|
11600
13361
|
|
|
11601
13362
|
// src/_fetch-guard.ts
|
|
11602
|
-
import * as
|
|
11603
|
-
import * as
|
|
13363
|
+
import * as dns2 from "node:dns/promises";
|
|
13364
|
+
import * as net3 from "node:net";
|
|
11604
13365
|
import {
|
|
11605
13366
|
FetchError,
|
|
11606
13367
|
ToolValidationError as ToolValidationError2,
|
|
11607
|
-
isPrivateIPv4,
|
|
11608
|
-
isPrivateIPv6
|
|
13368
|
+
isPrivateIPv4 as isPrivateIPv42,
|
|
13369
|
+
isPrivateIPv6 as isPrivateIPv62
|
|
11609
13370
|
} from "@wrongstack/core";
|
|
11610
13371
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
11611
13372
|
var nativeGlobalFetch = globalThis.fetch;
|
|
@@ -11616,13 +13377,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
|
11616
13377
|
);
|
|
11617
13378
|
}
|
|
11618
13379
|
function guardedLookup(hostname2, options, callback) {
|
|
11619
|
-
|
|
13380
|
+
dns2.lookup(hostname2, { all: true }).then((records) => {
|
|
11620
13381
|
const family = options?.family;
|
|
11621
13382
|
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
11622
13383
|
const list = byFamily.length > 0 ? byFamily : records;
|
|
11623
13384
|
if (!ALLOW_PRIVATE) {
|
|
11624
13385
|
for (const r of list) {
|
|
11625
|
-
const bad = r.family === 4 ?
|
|
13386
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11626
13387
|
if (bad) {
|
|
11627
13388
|
callback(
|
|
11628
13389
|
Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
|
|
@@ -11727,16 +13488,16 @@ async function assertNotPrivate(hostname2) {
|
|
|
11727
13488
|
field: "url"
|
|
11728
13489
|
});
|
|
11729
13490
|
}
|
|
11730
|
-
const ipVersion =
|
|
13491
|
+
const ipVersion = net3.isIP(host);
|
|
11731
13492
|
if (ipVersion === 4) {
|
|
11732
|
-
if (
|
|
13493
|
+
if (isPrivateIPv42(host)) {
|
|
11733
13494
|
throw new ToolValidationError2({
|
|
11734
13495
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11735
13496
|
field: "url"
|
|
11736
13497
|
});
|
|
11737
13498
|
}
|
|
11738
13499
|
} else if (ipVersion === 6) {
|
|
11739
|
-
if (
|
|
13500
|
+
if (isPrivateIPv62(host)) {
|
|
11740
13501
|
throw new ToolValidationError2({
|
|
11741
13502
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
11742
13503
|
field: "url"
|
|
@@ -11744,9 +13505,9 @@ async function assertNotPrivate(hostname2) {
|
|
|
11744
13505
|
}
|
|
11745
13506
|
} else {
|
|
11746
13507
|
try {
|
|
11747
|
-
const records = await
|
|
13508
|
+
const records = await dns2.lookup(host, { all: true });
|
|
11748
13509
|
for (const r of records) {
|
|
11749
|
-
const bad = r.family === 4 ?
|
|
13510
|
+
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
11750
13511
|
if (bad) {
|
|
11751
13512
|
throw new ToolValidationError2({
|
|
11752
13513
|
message: `fetch: resolved to private address ${r.address}`,
|
|
@@ -12085,13 +13846,13 @@ var formatTool = {
|
|
|
12085
13846
|
}
|
|
12086
13847
|
};
|
|
12087
13848
|
async function detectFixer(cwd) {
|
|
12088
|
-
const { stat:
|
|
13849
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
12089
13850
|
try {
|
|
12090
|
-
await
|
|
13851
|
+
await stat17(`${cwd}/biome.json`);
|
|
12091
13852
|
return "biome";
|
|
12092
13853
|
} catch {
|
|
12093
13854
|
try {
|
|
12094
|
-
await
|
|
13855
|
+
await stat17(`${cwd}/.prettierrc`);
|
|
12095
13856
|
return "prettier";
|
|
12096
13857
|
} catch {
|
|
12097
13858
|
return "biome";
|
|
@@ -12103,7 +13864,7 @@ async function detectFixer(cwd) {
|
|
|
12103
13864
|
init_util();
|
|
12104
13865
|
import { spawn as spawn9 } from "node:child_process";
|
|
12105
13866
|
import { statSync as statSync3 } from "node:fs";
|
|
12106
|
-
import { dirname as
|
|
13867
|
+
import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
|
|
12107
13868
|
import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
|
|
12108
13869
|
var TIMEOUT_MS2 = 3e4;
|
|
12109
13870
|
var MAX_OUTPUT3 = 1e5;
|
|
@@ -12246,9 +14007,9 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
12246
14007
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
12247
14008
|
}
|
|
12248
14009
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
12249
|
-
const root =
|
|
12250
|
-
const abs =
|
|
12251
|
-
if (abs !== root && !abs.startsWith(root +
|
|
14010
|
+
const root = resolve11(projectRoot);
|
|
14011
|
+
const abs = resolve11(root, input.worktreePath);
|
|
14012
|
+
if (abs !== root && !abs.startsWith(root + sep4)) {
|
|
12252
14013
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
12253
14014
|
}
|
|
12254
14015
|
}
|
|
@@ -12259,12 +14020,12 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
12259
14020
|
let dir = cwd;
|
|
12260
14021
|
for (let i = 0; i < 20; i++) {
|
|
12261
14022
|
try {
|
|
12262
|
-
const
|
|
12263
|
-
if (
|
|
14023
|
+
const stat17 = statSync3(`${dir}/.git`);
|
|
14024
|
+
if (stat17.isDirectory() || stat17.isFile()) return dir;
|
|
12264
14025
|
} catch {
|
|
12265
14026
|
}
|
|
12266
14027
|
if (dir === root) break;
|
|
12267
|
-
const parent =
|
|
14028
|
+
const parent = dirname12(dir);
|
|
12268
14029
|
if (parent === dir) break;
|
|
12269
14030
|
dir = parent;
|
|
12270
14031
|
}
|
|
@@ -12341,7 +14102,7 @@ function buildArgs(input) {
|
|
|
12341
14102
|
}
|
|
12342
14103
|
}
|
|
12343
14104
|
function runGit2(args, cwd, signal) {
|
|
12344
|
-
return new Promise((
|
|
14105
|
+
return new Promise((resolve14) => {
|
|
12345
14106
|
let stdout = "";
|
|
12346
14107
|
let stderr = "";
|
|
12347
14108
|
const child = spawn9("git", args, {
|
|
@@ -12362,7 +14123,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12362
14123
|
}
|
|
12363
14124
|
});
|
|
12364
14125
|
child.on("error", (err) => {
|
|
12365
|
-
|
|
14126
|
+
resolve14({
|
|
12366
14127
|
command: args[0],
|
|
12367
14128
|
stdout: normalizeCommandOutput(stdout),
|
|
12368
14129
|
stderr: err.message,
|
|
@@ -12371,7 +14132,7 @@ function runGit2(args, cwd, signal) {
|
|
|
12371
14132
|
});
|
|
12372
14133
|
});
|
|
12373
14134
|
child.on("close", (code) => {
|
|
12374
|
-
|
|
14135
|
+
resolve14({
|
|
12375
14136
|
command: args[0],
|
|
12376
14137
|
stdout: normalizeCommandOutput(stdout),
|
|
12377
14138
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -12383,8 +14144,8 @@ function runGit2(args, cwd, signal) {
|
|
|
12383
14144
|
}
|
|
12384
14145
|
|
|
12385
14146
|
// src/glob.ts
|
|
12386
|
-
import * as
|
|
12387
|
-
import * as
|
|
14147
|
+
import * as fs20 from "node:fs/promises";
|
|
14148
|
+
import * as path24 from "node:path";
|
|
12388
14149
|
import { compileGlob as compileGlob3 } from "@wrongstack/core";
|
|
12389
14150
|
|
|
12390
14151
|
// src/_concurrency.ts
|
|
@@ -12456,7 +14217,7 @@ var globTool = {
|
|
|
12456
14217
|
return;
|
|
12457
14218
|
}
|
|
12458
14219
|
try {
|
|
12459
|
-
const st = await
|
|
14220
|
+
const st = await fs20.stat(full);
|
|
12460
14221
|
if (truncated || results.length >= limit) {
|
|
12461
14222
|
truncated = true;
|
|
12462
14223
|
return;
|
|
@@ -12477,7 +14238,7 @@ var globTool = {
|
|
|
12477
14238
|
}
|
|
12478
14239
|
let entries;
|
|
12479
14240
|
try {
|
|
12480
|
-
entries = await
|
|
14241
|
+
entries = await fs20.readdir(dir, { withFileTypes: true });
|
|
12481
14242
|
} catch {
|
|
12482
14243
|
return;
|
|
12483
14244
|
}
|
|
@@ -12488,7 +14249,7 @@ var globTool = {
|
|
|
12488
14249
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
12489
14250
|
if (ignored.includes(name)) continue;
|
|
12490
14251
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
12491
|
-
const full =
|
|
14252
|
+
const full = path24.join(dir, name);
|
|
12492
14253
|
if (e.isDirectory()) {
|
|
12493
14254
|
subdirs.push({ full, rel });
|
|
12494
14255
|
} else if (e.isFile()) {
|
|
@@ -12501,13 +14262,13 @@ var globTool = {
|
|
|
12501
14262
|
}
|
|
12502
14263
|
} else if (e.isSymbolicLink()) {
|
|
12503
14264
|
try {
|
|
12504
|
-
const st = await
|
|
14265
|
+
const st = await fs20.stat(full);
|
|
12505
14266
|
if (st.isDirectory()) {
|
|
12506
|
-
const real = await
|
|
14267
|
+
const real = await fs20.realpath(full);
|
|
12507
14268
|
await assertRealInsideRoot(real, ctx);
|
|
12508
14269
|
subdirs.push({ full, rel });
|
|
12509
14270
|
} else if (st.isFile()) {
|
|
12510
|
-
const real = await
|
|
14271
|
+
const real = await fs20.realpath(full);
|
|
12511
14272
|
await assertRealInsideRoot(real, ctx);
|
|
12512
14273
|
re.lastIndex = 0;
|
|
12513
14274
|
const relMatch = re.test(rel);
|
|
@@ -12532,7 +14293,7 @@ var globTool = {
|
|
|
12532
14293
|
};
|
|
12533
14294
|
async function readGitignore(dir) {
|
|
12534
14295
|
try {
|
|
12535
|
-
const raw = await
|
|
14296
|
+
const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
|
|
12536
14297
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
12537
14298
|
} catch {
|
|
12538
14299
|
return [];
|
|
@@ -12542,8 +14303,8 @@ async function readGitignore(dir) {
|
|
|
12542
14303
|
// src/grep.ts
|
|
12543
14304
|
import { expectDefined as expectDefined7 } from "@wrongstack/core";
|
|
12544
14305
|
import { spawn as spawn10 } from "node:child_process";
|
|
12545
|
-
import * as
|
|
12546
|
-
import * as
|
|
14306
|
+
import * as fs21 from "node:fs/promises";
|
|
14307
|
+
import * as path25 from "node:path";
|
|
12547
14308
|
import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
|
|
12548
14309
|
|
|
12549
14310
|
// src/_regex.ts
|
|
@@ -12688,13 +14449,13 @@ var grepTool = {
|
|
|
12688
14449
|
}
|
|
12689
14450
|
};
|
|
12690
14451
|
async function detectRg(signal) {
|
|
12691
|
-
return new Promise((
|
|
14452
|
+
return new Promise((resolve14) => {
|
|
12692
14453
|
try {
|
|
12693
14454
|
const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
|
|
12694
|
-
p.on("error", () =>
|
|
12695
|
-
p.on("close", (code) =>
|
|
14455
|
+
p.on("error", () => resolve14(false));
|
|
14456
|
+
p.on("close", (code) => resolve14(code === 0));
|
|
12696
14457
|
} catch {
|
|
12697
|
-
|
|
14458
|
+
resolve14(false);
|
|
12698
14459
|
}
|
|
12699
14460
|
});
|
|
12700
14461
|
}
|
|
@@ -12845,9 +14606,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12845
14606
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
12846
14607
|
if (globRe) globRe.lastIndex = 0;
|
|
12847
14608
|
try {
|
|
12848
|
-
const
|
|
12849
|
-
if (!
|
|
12850
|
-
const file = await
|
|
14609
|
+
const stat17 = await fs21.stat(full);
|
|
14610
|
+
if (!stat17.isFile() || stat17.size > maxBytes || stopped || signal.aborted) return;
|
|
14611
|
+
const file = await fs21.open(full, "r");
|
|
12851
14612
|
try {
|
|
12852
14613
|
let bytesReadTotal = 0;
|
|
12853
14614
|
let lineNumber = 0;
|
|
@@ -12926,7 +14687,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12926
14687
|
if (stopped || signal.aborted) return;
|
|
12927
14688
|
let entries;
|
|
12928
14689
|
try {
|
|
12929
|
-
entries = await
|
|
14690
|
+
entries = await fs21.readdir(dir, { withFileTypes: true });
|
|
12930
14691
|
} catch {
|
|
12931
14692
|
return;
|
|
12932
14693
|
}
|
|
@@ -12936,7 +14697,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12936
14697
|
if (stopped) return;
|
|
12937
14698
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
12938
14699
|
if (e.isSymbolicLink()) continue;
|
|
12939
|
-
const full =
|
|
14700
|
+
const full = path25.join(dir, e.name);
|
|
12940
14701
|
if (e.isDirectory()) {
|
|
12941
14702
|
subdirs.push(full);
|
|
12942
14703
|
} else if (e.isFile()) {
|
|
@@ -12959,7 +14720,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
12959
14720
|
init_spawn_stream();
|
|
12960
14721
|
init_util();
|
|
12961
14722
|
init_legacy_bridge();
|
|
12962
|
-
import { join as
|
|
14723
|
+
import { join as join21 } from "node:path";
|
|
12963
14724
|
import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
|
|
12964
14725
|
var installTool = {
|
|
12965
14726
|
name: "install",
|
|
@@ -13136,16 +14897,16 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
13136
14897
|
case "pnpm":
|
|
13137
14898
|
case "yarn":
|
|
13138
14899
|
case "npm":
|
|
13139
|
-
return
|
|
14900
|
+
return join21(cwd, "package.json");
|
|
13140
14901
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
13141
14902
|
default:
|
|
13142
|
-
return
|
|
14903
|
+
return join21(cwd, "package.json");
|
|
13143
14904
|
}
|
|
13144
14905
|
}
|
|
13145
14906
|
|
|
13146
14907
|
// src/json.ts
|
|
13147
14908
|
init_util();
|
|
13148
|
-
import * as
|
|
14909
|
+
import * as fs22 from "node:fs/promises";
|
|
13149
14910
|
import { deepMerge as deepMergeCore } from "@wrongstack/core";
|
|
13150
14911
|
var jsonTool = {
|
|
13151
14912
|
name: "json",
|
|
@@ -13220,7 +14981,7 @@ async function executeParse(input, ctx) {
|
|
|
13220
14981
|
let raw;
|
|
13221
14982
|
if (input.file) {
|
|
13222
14983
|
try {
|
|
13223
|
-
raw = await
|
|
14984
|
+
raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13224
14985
|
} catch {
|
|
13225
14986
|
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
13226
14987
|
}
|
|
@@ -13278,7 +15039,7 @@ async function executeQuery(input, ctx) {
|
|
|
13278
15039
|
let parsed;
|
|
13279
15040
|
if (input.file) {
|
|
13280
15041
|
try {
|
|
13281
|
-
const raw = await
|
|
15042
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13282
15043
|
parsed = JSON.parse(raw);
|
|
13283
15044
|
} catch {
|
|
13284
15045
|
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
@@ -13320,7 +15081,7 @@ async function executeValidate(input, ctx) {
|
|
|
13320
15081
|
let parsed;
|
|
13321
15082
|
if (input.file) {
|
|
13322
15083
|
try {
|
|
13323
|
-
const raw = await
|
|
15084
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13324
15085
|
parsed = JSON.parse(raw);
|
|
13325
15086
|
} catch {
|
|
13326
15087
|
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
@@ -13362,7 +15123,7 @@ async function executeTransform(input, ctx) {
|
|
|
13362
15123
|
let parsed;
|
|
13363
15124
|
if (input.file) {
|
|
13364
15125
|
try {
|
|
13365
|
-
const raw = await
|
|
15126
|
+
const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
|
|
13366
15127
|
parsed = JSON.parse(raw);
|
|
13367
15128
|
} catch {
|
|
13368
15129
|
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
@@ -13524,56 +15285,56 @@ function jmespathSearch(data, query) {
|
|
|
13524
15285
|
}
|
|
13525
15286
|
function validateJsonSchema(data, schema) {
|
|
13526
15287
|
const errors = [];
|
|
13527
|
-
function check(value, s,
|
|
15288
|
+
function check(value, s, path32) {
|
|
13528
15289
|
if (s["type"]) {
|
|
13529
15290
|
const expectedType = s["type"];
|
|
13530
15291
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
13531
15292
|
if (expectedType === "integer") {
|
|
13532
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
15293
|
+
if (!Number.isInteger(value)) errors.push(`${path32}: expected integer, got ${actualType}`);
|
|
13533
15294
|
} else if (expectedType !== actualType) {
|
|
13534
|
-
errors.push(`${
|
|
15295
|
+
errors.push(`${path32}: expected ${expectedType}, got ${actualType}`);
|
|
13535
15296
|
}
|
|
13536
15297
|
}
|
|
13537
15298
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
13538
15299
|
try {
|
|
13539
15300
|
new URL(value);
|
|
13540
15301
|
} catch {
|
|
13541
|
-
errors.push(`${
|
|
15302
|
+
errors.push(`${path32}: not a valid URI`);
|
|
13542
15303
|
}
|
|
13543
15304
|
}
|
|
13544
15305
|
if (typeof value === "string" && s["pattern"]) {
|
|
13545
15306
|
const re = new RegExp(s["pattern"]);
|
|
13546
|
-
if (!re.test(value)) errors.push(`${
|
|
15307
|
+
if (!re.test(value)) errors.push(`${path32}: does not match pattern ${s["pattern"]}`);
|
|
13547
15308
|
}
|
|
13548
15309
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
13549
|
-
errors.push(`${
|
|
15310
|
+
errors.push(`${path32}: string too short (min ${s["minLength"]})`);
|
|
13550
15311
|
}
|
|
13551
15312
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
13552
|
-
errors.push(`${
|
|
15313
|
+
errors.push(`${path32}: string too long (max ${s["maxLength"]})`);
|
|
13553
15314
|
}
|
|
13554
15315
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
13555
|
-
errors.push(`${
|
|
15316
|
+
errors.push(`${path32}: below minimum ${s["minimum"]}`);
|
|
13556
15317
|
}
|
|
13557
15318
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
13558
|
-
errors.push(`${
|
|
15319
|
+
errors.push(`${path32}: above maximum ${s["maximum"]}`);
|
|
13559
15320
|
}
|
|
13560
15321
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
13561
15322
|
for (let i = 0; i < value.length; i++) {
|
|
13562
|
-
check(value[i], s["items"], `${
|
|
15323
|
+
check(value[i], s["items"], `${path32}[${i}]`);
|
|
13563
15324
|
}
|
|
13564
15325
|
}
|
|
13565
15326
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
13566
15327
|
const props = s["properties"];
|
|
13567
15328
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
13568
|
-
check(value[k], propSchema, `${
|
|
15329
|
+
check(value[k], propSchema, `${path32}.${k}`);
|
|
13569
15330
|
}
|
|
13570
15331
|
}
|
|
13571
15332
|
}
|
|
13572
15333
|
check(data, schema, "$");
|
|
13573
15334
|
return { valid: errors.length === 0, errors };
|
|
13574
15335
|
}
|
|
13575
|
-
function simpleQuery(data,
|
|
13576
|
-
const parts =
|
|
15336
|
+
function simpleQuery(data, path32) {
|
|
15337
|
+
const parts = path32.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
13577
15338
|
let current = data;
|
|
13578
15339
|
for (const part of parts) {
|
|
13579
15340
|
if (current === null || current === void 0) return void 0;
|
|
@@ -13623,7 +15384,7 @@ function toYaml(data, indent = 0) {
|
|
|
13623
15384
|
}
|
|
13624
15385
|
|
|
13625
15386
|
// src/kanban.ts
|
|
13626
|
-
import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
|
|
15387
|
+
import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
|
|
13627
15388
|
import {
|
|
13628
15389
|
addCheckToTask,
|
|
13629
15390
|
addColumn,
|
|
@@ -13635,18 +15396,19 @@ import {
|
|
|
13635
15396
|
assignTask,
|
|
13636
15397
|
claimReadyTask,
|
|
13637
15398
|
copyTaskToBoard,
|
|
13638
|
-
createBoard,
|
|
15399
|
+
createBoard as createBoard2,
|
|
15400
|
+
createBoardFromTaskGraph,
|
|
13639
15401
|
duplicateBoard,
|
|
13640
15402
|
exportBoardAsMarkdown,
|
|
13641
15403
|
exportBoardToTaskGraph,
|
|
13642
15404
|
generateBoardFromDescription,
|
|
13643
|
-
getBoard,
|
|
15405
|
+
getBoard as getBoard2,
|
|
13644
15406
|
getKanbanOrchestrationSnapshot,
|
|
13645
15407
|
getKanbanQueueHealth,
|
|
13646
15408
|
getTask,
|
|
13647
15409
|
getTaskChain,
|
|
13648
15410
|
heartbeatTaskAssignment,
|
|
13649
|
-
listBoards,
|
|
15411
|
+
listBoards as listBoards2,
|
|
13650
15412
|
listKanbanEvents,
|
|
13651
15413
|
listReadyTasks,
|
|
13652
15414
|
mergeTasks,
|
|
@@ -13654,21 +15416,244 @@ import {
|
|
|
13654
15416
|
parseLinesIntoTasks,
|
|
13655
15417
|
recoverStaleTaskAssignments,
|
|
13656
15418
|
releaseTaskClaim,
|
|
13657
|
-
removeBoard,
|
|
15419
|
+
removeBoard as removeBoard2,
|
|
13658
15420
|
removeColumn,
|
|
13659
15421
|
removeTask,
|
|
13660
15422
|
searchKanban,
|
|
13661
15423
|
setTaskChain,
|
|
13662
15424
|
splitTask,
|
|
13663
|
-
syncBoardFromTaskGraph,
|
|
15425
|
+
syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
|
|
13664
15426
|
transferTaskToBoard,
|
|
13665
|
-
updateBoard,
|
|
15427
|
+
updateBoard as updateBoard2,
|
|
13666
15428
|
updateCheckOnTask,
|
|
13667
15429
|
updateColumn,
|
|
13668
15430
|
updateGoalMetricOnTask,
|
|
13669
15431
|
updateTask,
|
|
13670
15432
|
updateTaskAssignment
|
|
13671
15433
|
} from "@wrongstack/kanban";
|
|
15434
|
+
|
|
15435
|
+
// src/session-kanban.ts
|
|
15436
|
+
import {
|
|
15437
|
+
deserializeTaskGraph,
|
|
15438
|
+
loadPlan,
|
|
15439
|
+
loadTasks,
|
|
15440
|
+
mutatePlan,
|
|
15441
|
+
mutateTasks
|
|
15442
|
+
} from "@wrongstack/core";
|
|
15443
|
+
import {
|
|
15444
|
+
createBoard,
|
|
15445
|
+
getBoard,
|
|
15446
|
+
listBoards,
|
|
15447
|
+
removeBoard,
|
|
15448
|
+
syncBoardFromTaskGraph,
|
|
15449
|
+
updateBoard
|
|
15450
|
+
} from "@wrongstack/kanban";
|
|
15451
|
+
var SESSION_BOARD_TAG = "session-work";
|
|
15452
|
+
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
15453
|
+
var SESSION_KANBAN_COLUMNS = [
|
|
15454
|
+
{ id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
|
|
15455
|
+
{ id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
|
|
15456
|
+
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
15457
|
+
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
15458
|
+
];
|
|
15459
|
+
var boardQueue = /* @__PURE__ */ new Map();
|
|
15460
|
+
var boardEnsures = /* @__PURE__ */ new Map();
|
|
15461
|
+
function boardKey(projectRoot, sessionId) {
|
|
15462
|
+
return `${projectRoot}\0${sessionId}`;
|
|
15463
|
+
}
|
|
15464
|
+
function sessionTag(sessionId) {
|
|
15465
|
+
return `session:${sessionId}`;
|
|
15466
|
+
}
|
|
15467
|
+
function sessionBoardTitle(sessionId) {
|
|
15468
|
+
const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
|
|
15469
|
+
return `Session ${leaf.slice(0, 12)}`;
|
|
15470
|
+
}
|
|
15471
|
+
function sessionBoardTags(sessionId) {
|
|
15472
|
+
return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
|
|
15473
|
+
}
|
|
15474
|
+
function sameColumns(columns) {
|
|
15475
|
+
return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
|
|
15476
|
+
}
|
|
15477
|
+
async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
15478
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
15479
|
+
const key = boardKey(projectRoot, sessionId);
|
|
15480
|
+
const inFlight = boardEnsures.get(key);
|
|
15481
|
+
if (inFlight) return inFlight;
|
|
15482
|
+
const promise = (async () => {
|
|
15483
|
+
const summary = (await listBoards(projectRoot)).find(
|
|
15484
|
+
(board2) => board2.tags?.includes(sessionTag(sessionId))
|
|
15485
|
+
);
|
|
15486
|
+
let board = summary ? await getBoard(projectRoot, summary.id) : null;
|
|
15487
|
+
if (!board) {
|
|
15488
|
+
return createBoard(projectRoot, {
|
|
15489
|
+
title: sessionBoardTitle(sessionId),
|
|
15490
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
15491
|
+
tags: sessionBoardTags(sessionId),
|
|
15492
|
+
columns: SESSION_KANBAN_COLUMNS,
|
|
15493
|
+
generatedBy: `session-kanban:${sessionId}`
|
|
15494
|
+
});
|
|
15495
|
+
}
|
|
15496
|
+
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
15497
|
+
board = await updateBoard(projectRoot, board.id, {
|
|
15498
|
+
title: sessionBoardTitle(sessionId),
|
|
15499
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
15500
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
15501
|
+
columns: SESSION_KANBAN_COLUMNS
|
|
15502
|
+
}) ?? board;
|
|
15503
|
+
}
|
|
15504
|
+
return board;
|
|
15505
|
+
})();
|
|
15506
|
+
boardEnsures.set(key, promise);
|
|
15507
|
+
try {
|
|
15508
|
+
return await promise;
|
|
15509
|
+
} finally {
|
|
15510
|
+
boardEnsures.delete(key);
|
|
15511
|
+
}
|
|
15512
|
+
}
|
|
15513
|
+
function enqueueBoardWork(projectRoot, sessionId, work) {
|
|
15514
|
+
const key = boardKey(projectRoot, sessionId);
|
|
15515
|
+
const previous = boardQueue.get(key) ?? Promise.resolve();
|
|
15516
|
+
const result = previous.catch(() => void 0).then(work);
|
|
15517
|
+
const tail = result.then(
|
|
15518
|
+
() => void 0,
|
|
15519
|
+
() => void 0
|
|
15520
|
+
);
|
|
15521
|
+
boardQueue.set(key, tail);
|
|
15522
|
+
void tail.then(() => {
|
|
15523
|
+
if (boardQueue.get(key) === tail) boardQueue.delete(key);
|
|
15524
|
+
});
|
|
15525
|
+
return result;
|
|
15526
|
+
}
|
|
15527
|
+
async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
15528
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
15529
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
15530
|
+
const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
|
|
15531
|
+
if (!board) return null;
|
|
15532
|
+
const result = await syncBoardFromTaskGraph(
|
|
15533
|
+
projectRoot,
|
|
15534
|
+
board.id,
|
|
15535
|
+
deserializeTaskGraph(graph),
|
|
15536
|
+
{
|
|
15537
|
+
sourceSystem,
|
|
15538
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
15539
|
+
archiveMissingTasks: true,
|
|
15540
|
+
includeCompletedTasks: true
|
|
15541
|
+
}
|
|
15542
|
+
);
|
|
15543
|
+
return result?.board ?? null;
|
|
15544
|
+
});
|
|
15545
|
+
}
|
|
15546
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
15547
|
+
const nodes = todos.map((todo, index) => ({
|
|
15548
|
+
id: todo.id,
|
|
15549
|
+
title: todo.content,
|
|
15550
|
+
description: todo.activeForm ?? "",
|
|
15551
|
+
type: "chore",
|
|
15552
|
+
priority: "medium",
|
|
15553
|
+
status: todo.status,
|
|
15554
|
+
createdAt: index,
|
|
15555
|
+
updatedAt: index
|
|
15556
|
+
}));
|
|
15557
|
+
return {
|
|
15558
|
+
id: `todo:${sessionId}`,
|
|
15559
|
+
specId: `todo:${sessionId}`,
|
|
15560
|
+
title: "Session todos",
|
|
15561
|
+
nodes,
|
|
15562
|
+
edges: [],
|
|
15563
|
+
rootNodes: nodes.map((node) => node.id),
|
|
15564
|
+
createdAt: 0,
|
|
15565
|
+
updatedAt: 0
|
|
15566
|
+
};
|
|
15567
|
+
}
|
|
15568
|
+
function taskFileToSerializedGraph(tasks, sessionId) {
|
|
15569
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
15570
|
+
const nodes = tasks.map((task, index) => ({
|
|
15571
|
+
id: task.id,
|
|
15572
|
+
title: task.title,
|
|
15573
|
+
description: task.description ?? "",
|
|
15574
|
+
type: task.type,
|
|
15575
|
+
priority: task.priority,
|
|
15576
|
+
status: task.status,
|
|
15577
|
+
...task.assignee ? { assignee: task.assignee } : {},
|
|
15578
|
+
...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
|
|
15579
|
+
createdAt: index,
|
|
15580
|
+
updatedAt: index
|
|
15581
|
+
}));
|
|
15582
|
+
const edges = tasks.flatMap(
|
|
15583
|
+
(task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
|
|
15584
|
+
id: `${dependency}->${task.id}`,
|
|
15585
|
+
from: dependency,
|
|
15586
|
+
to: task.id,
|
|
15587
|
+
type: "depends_on"
|
|
15588
|
+
}))
|
|
15589
|
+
);
|
|
15590
|
+
const hasIncoming = new Set(edges.map((edge) => edge.to));
|
|
15591
|
+
const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
|
|
15592
|
+
return {
|
|
15593
|
+
// Keep the historical graph id so existing mirrored task cards are reused.
|
|
15594
|
+
id: `session:${sessionId}`,
|
|
15595
|
+
specId: `session:${sessionId}`,
|
|
15596
|
+
title: "Session tasks",
|
|
15597
|
+
nodes,
|
|
15598
|
+
edges,
|
|
15599
|
+
rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
|
|
15600
|
+
createdAt: 0,
|
|
15601
|
+
updatedAt: 0
|
|
15602
|
+
};
|
|
15603
|
+
}
|
|
15604
|
+
var PLAN_STATUS_TO_TASK = {
|
|
15605
|
+
open: "pending",
|
|
15606
|
+
in_progress: "in_progress",
|
|
15607
|
+
done: "completed"
|
|
15608
|
+
};
|
|
15609
|
+
function planFileToSerializedGraph(items, sessionId) {
|
|
15610
|
+
const nodes = items.map((item, index) => ({
|
|
15611
|
+
id: item.id,
|
|
15612
|
+
title: item.title,
|
|
15613
|
+
description: item.details ?? "",
|
|
15614
|
+
type: "chore",
|
|
15615
|
+
priority: "medium",
|
|
15616
|
+
status: PLAN_STATUS_TO_TASK[item.status],
|
|
15617
|
+
createdAt: index,
|
|
15618
|
+
updatedAt: index
|
|
15619
|
+
}));
|
|
15620
|
+
return {
|
|
15621
|
+
id: `plan:${sessionId}`,
|
|
15622
|
+
specId: `plan:${sessionId}`,
|
|
15623
|
+
title: "Session plan",
|
|
15624
|
+
nodes,
|
|
15625
|
+
edges: [],
|
|
15626
|
+
rootNodes: nodes.map((node) => node.id),
|
|
15627
|
+
createdAt: 0,
|
|
15628
|
+
updatedAt: 0
|
|
15629
|
+
};
|
|
15630
|
+
}
|
|
15631
|
+
function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
15632
|
+
return projectGraph(
|
|
15633
|
+
projectRoot,
|
|
15634
|
+
sessionId,
|
|
15635
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
15636
|
+
"session-todo"
|
|
15637
|
+
);
|
|
15638
|
+
}
|
|
15639
|
+
function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
|
|
15640
|
+
return projectGraph(
|
|
15641
|
+
projectRoot,
|
|
15642
|
+
sessionId,
|
|
15643
|
+
taskFileToSerializedGraph(tasks, sessionId),
|
|
15644
|
+
"session-task"
|
|
15645
|
+
);
|
|
15646
|
+
}
|
|
15647
|
+
function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
15648
|
+
return projectGraph(
|
|
15649
|
+
projectRoot,
|
|
15650
|
+
sessionId,
|
|
15651
|
+
planFileToSerializedGraph(items, sessionId),
|
|
15652
|
+
"session-plan"
|
|
15653
|
+
);
|
|
15654
|
+
}
|
|
15655
|
+
|
|
15656
|
+
// src/kanban.ts
|
|
13672
15657
|
var kanbanTool = {
|
|
13673
15658
|
name: "kanban",
|
|
13674
15659
|
category: "Project",
|
|
@@ -13695,6 +15680,8 @@ var kanbanTool = {
|
|
|
13695
15680
|
"export_markdown",
|
|
13696
15681
|
"export_task_graph",
|
|
13697
15682
|
"sync_task_graph",
|
|
15683
|
+
"create_from_graph",
|
|
15684
|
+
"import_session_tasks",
|
|
13698
15685
|
"search_tasks",
|
|
13699
15686
|
"ready_tasks",
|
|
13700
15687
|
"snapshot",
|
|
@@ -13741,6 +15728,10 @@ var kanbanTool = {
|
|
|
13741
15728
|
tags: { type: "array", items: { type: "string" } },
|
|
13742
15729
|
labels: { type: "array", items: { type: "string" } },
|
|
13743
15730
|
priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
15731
|
+
taskType: {
|
|
15732
|
+
type: "string",
|
|
15733
|
+
enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
|
|
15734
|
+
},
|
|
13744
15735
|
status: {
|
|
13745
15736
|
type: "string",
|
|
13746
15737
|
enum: [
|
|
@@ -13783,8 +15774,21 @@ var kanbanTool = {
|
|
|
13783
15774
|
releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
|
|
13784
15775
|
releaseReason: { type: "string" },
|
|
13785
15776
|
clearAssignee: { type: "boolean" },
|
|
13786
|
-
recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
|
|
15777
|
+
recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
|
|
13787
15778
|
recoveryNow: { type: "string" },
|
|
15779
|
+
recoveryPolicyFailOnCostCeiling: { type: "boolean" },
|
|
15780
|
+
recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
|
|
15781
|
+
recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
|
|
15782
|
+
recoveryPolicyRetryPolicyOverride: {
|
|
15783
|
+
type: "string",
|
|
15784
|
+
enum: ["off", "incremental", "exponential"]
|
|
15785
|
+
},
|
|
15786
|
+
assignee: { type: "string" },
|
|
15787
|
+
costCeilingUsd: { type: "number" },
|
|
15788
|
+
retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
|
|
15789
|
+
lastFailureKind: { type: "string" },
|
|
15790
|
+
dependsOn: { type: "array", items: { type: "string" } },
|
|
15791
|
+
estimatedHours: { type: "number" },
|
|
13788
15792
|
taskGraph: { type: "object" },
|
|
13789
15793
|
graphId: { type: "string" },
|
|
13790
15794
|
specId: { type: "string" },
|
|
@@ -13840,7 +15844,7 @@ var kanbanTool = {
|
|
|
13840
15844
|
try {
|
|
13841
15845
|
switch (input.action) {
|
|
13842
15846
|
case "list_boards": {
|
|
13843
|
-
const boards = await
|
|
15847
|
+
const boards = await listBoards2(projectRoot);
|
|
13844
15848
|
return { ok: true, message: `${boards.length} board(s).`, boards };
|
|
13845
15849
|
}
|
|
13846
15850
|
case "get_board": {
|
|
@@ -13849,7 +15853,7 @@ var kanbanTool = {
|
|
|
13849
15853
|
}
|
|
13850
15854
|
case "create_board": {
|
|
13851
15855
|
if (!input.title) return fail("create_board requires title.");
|
|
13852
|
-
const board = await
|
|
15856
|
+
const board = await createBoard2(projectRoot, {
|
|
13853
15857
|
title: input.title,
|
|
13854
15858
|
...input.description !== void 0 ? { description: input.description } : {},
|
|
13855
15859
|
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
@@ -13859,7 +15863,7 @@ var kanbanTool = {
|
|
|
13859
15863
|
}
|
|
13860
15864
|
case "update_board": {
|
|
13861
15865
|
if (!input.boardId) return fail("update_board requires boardId.");
|
|
13862
|
-
const board = await
|
|
15866
|
+
const board = await updateBoard2(projectRoot, input.boardId, {
|
|
13863
15867
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
13864
15868
|
...input.description !== void 0 ? { description: input.description } : {},
|
|
13865
15869
|
...input.tags !== void 0 ? { tags: input.tags } : {}
|
|
@@ -13879,7 +15883,7 @@ var kanbanTool = {
|
|
|
13879
15883
|
}
|
|
13880
15884
|
case "delete_board": {
|
|
13881
15885
|
if (!input.boardId) return fail("delete_board requires boardId.");
|
|
13882
|
-
const removed = await
|
|
15886
|
+
const removed = await removeBoard2(projectRoot, input.boardId);
|
|
13883
15887
|
return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
|
|
13884
15888
|
}
|
|
13885
15889
|
case "generate_board": {
|
|
@@ -13890,14 +15894,14 @@ var kanbanTool = {
|
|
|
13890
15894
|
...input.context !== void 0 ? { context: input.context } : {},
|
|
13891
15895
|
...input.columns !== void 0 ? { columns: input.columns } : {}
|
|
13892
15896
|
});
|
|
13893
|
-
const board = await
|
|
15897
|
+
const board = await createBoard2(projectRoot, boardInput);
|
|
13894
15898
|
for (const taskInput2 of parseLinesIntoTasks(
|
|
13895
15899
|
input.description,
|
|
13896
15900
|
board.columns[0]?.id ?? "backlog"
|
|
13897
15901
|
)) {
|
|
13898
15902
|
await addTask(projectRoot, board.id, taskInput2);
|
|
13899
15903
|
}
|
|
13900
|
-
return okBoard(await
|
|
15904
|
+
return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
|
|
13901
15905
|
}
|
|
13902
15906
|
case "export_markdown": {
|
|
13903
15907
|
const board = await requireBoard(projectRoot, input.boardId);
|
|
@@ -13930,8 +15934,8 @@ var kanbanTool = {
|
|
|
13930
15934
|
if (!input.boardId || !input.taskGraph) {
|
|
13931
15935
|
return fail("sync_task_graph requires boardId and taskGraph.");
|
|
13932
15936
|
}
|
|
13933
|
-
const graph =
|
|
13934
|
-
const result = await
|
|
15937
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
15938
|
+
const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
|
|
13935
15939
|
...input.title !== void 0 ? { title: input.title } : {},
|
|
13936
15940
|
...input.description !== void 0 ? { description: input.description } : {},
|
|
13937
15941
|
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
@@ -13948,6 +15952,59 @@ var kanbanTool = {
|
|
|
13948
15952
|
board: result.board
|
|
13949
15953
|
} : fail("Board not found.");
|
|
13950
15954
|
}
|
|
15955
|
+
case "create_from_graph": {
|
|
15956
|
+
if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
|
|
15957
|
+
const graph = deserializeTaskGraph2(input.taskGraph);
|
|
15958
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
15959
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
15960
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
15961
|
+
...input.tags !== void 0 ? { tags: input.tags } : {},
|
|
15962
|
+
...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
|
|
15963
|
+
...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
|
|
15964
|
+
...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
|
|
15965
|
+
...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
|
|
15966
|
+
});
|
|
15967
|
+
return {
|
|
15968
|
+
ok: true,
|
|
15969
|
+
message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
|
|
15970
|
+
board
|
|
15971
|
+
};
|
|
15972
|
+
}
|
|
15973
|
+
case "import_session_tasks": {
|
|
15974
|
+
const taskPath = ctx.meta?.["task.path"];
|
|
15975
|
+
if (!taskPath) return fail("No session task file for this session.");
|
|
15976
|
+
const file = await loadTasks2(taskPath);
|
|
15977
|
+
if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
|
|
15978
|
+
const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
|
|
15979
|
+
const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
|
|
15980
|
+
const tags = ["session", `session:${sessionId}`];
|
|
15981
|
+
const existing = (await listBoards2(projectRoot)).find(
|
|
15982
|
+
(b) => b.tags?.includes(`session:${sessionId}`)
|
|
15983
|
+
);
|
|
15984
|
+
if (existing) {
|
|
15985
|
+
const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
|
|
15986
|
+
sourceSystem: "session",
|
|
15987
|
+
tags,
|
|
15988
|
+
archiveMissingTasks: true,
|
|
15989
|
+
includeCompletedTasks: true
|
|
15990
|
+
});
|
|
15991
|
+
return result ? {
|
|
15992
|
+
ok: true,
|
|
15993
|
+
message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
|
|
15994
|
+
board: result.board
|
|
15995
|
+
} : fail("Session board vanished mid-sync.");
|
|
15996
|
+
}
|
|
15997
|
+
const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
|
|
15998
|
+
title: `Session tasks (${sessionId.slice(0, 8)})`,
|
|
15999
|
+
sourceSystem: "session",
|
|
16000
|
+
tags
|
|
16001
|
+
});
|
|
16002
|
+
return {
|
|
16003
|
+
ok: true,
|
|
16004
|
+
message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
|
|
16005
|
+
board
|
|
16006
|
+
};
|
|
16007
|
+
}
|
|
13951
16008
|
case "search_tasks": {
|
|
13952
16009
|
const tasks = await searchKanban(projectRoot, {
|
|
13953
16010
|
query: input.query,
|
|
@@ -14384,7 +16441,7 @@ function okTask(board, task, message) {
|
|
|
14384
16441
|
return { ok: true, message, board, task };
|
|
14385
16442
|
}
|
|
14386
16443
|
async function requireBoard(projectRoot, boardId) {
|
|
14387
|
-
return boardId ?
|
|
16444
|
+
return boardId ? getBoard2(projectRoot, boardId) : null;
|
|
14388
16445
|
}
|
|
14389
16446
|
function taskInput(input) {
|
|
14390
16447
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
@@ -14393,14 +16450,23 @@ function taskInput(input) {
|
|
|
14393
16450
|
columnId: input.columnId,
|
|
14394
16451
|
description: input.description,
|
|
14395
16452
|
priority: input.priority,
|
|
16453
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
14396
16454
|
status: input.status,
|
|
14397
16455
|
labels: input.labels,
|
|
14398
16456
|
...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
|
|
14399
16457
|
...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
|
|
14400
|
-
...input
|
|
16458
|
+
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
16459
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
|
|
14401
16460
|
...assignment ? { assignment } : {}
|
|
14402
16461
|
};
|
|
14403
16462
|
}
|
|
16463
|
+
function mergedDependsOn(input) {
|
|
16464
|
+
const ids = [
|
|
16465
|
+
...input.dependsOn ?? [],
|
|
16466
|
+
...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
|
|
16467
|
+
].filter((id, i, arr) => id && arr.indexOf(id) === i);
|
|
16468
|
+
return ids.length > 0 ? ids : void 0;
|
|
16469
|
+
}
|
|
14404
16470
|
function taskPatch(input) {
|
|
14405
16471
|
return {
|
|
14406
16472
|
title: input.title,
|
|
@@ -14408,10 +16474,12 @@ function taskPatch(input) {
|
|
|
14408
16474
|
columnId: input.columnId,
|
|
14409
16475
|
order: input.order,
|
|
14410
16476
|
priority: input.priority,
|
|
16477
|
+
...input.taskType !== void 0 ? { type: input.taskType } : {},
|
|
14411
16478
|
status: input.status,
|
|
14412
16479
|
labels: input.labels,
|
|
14413
16480
|
assignedAgent: input.agentId,
|
|
14414
|
-
...input
|
|
16481
|
+
...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
|
|
16482
|
+
...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
|
|
14415
16483
|
};
|
|
14416
16484
|
}
|
|
14417
16485
|
function assignmentInput(input) {
|
|
@@ -14577,11 +16645,11 @@ var lintTool = {
|
|
|
14577
16645
|
}
|
|
14578
16646
|
};
|
|
14579
16647
|
async function detectLinter(cwd) {
|
|
14580
|
-
const { stat:
|
|
16648
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
14581
16649
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
14582
16650
|
for (const f of checks) {
|
|
14583
16651
|
try {
|
|
14584
|
-
await
|
|
16652
|
+
await stat17(`${cwd}/${f}`);
|
|
14585
16653
|
if (f.includes("biome")) return "biome";
|
|
14586
16654
|
if (f.includes("eslint")) return "eslint";
|
|
14587
16655
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -14681,7 +16749,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14681
16749
|
};
|
|
14682
16750
|
}
|
|
14683
16751
|
args.push("--timestamps", service);
|
|
14684
|
-
return new Promise((
|
|
16752
|
+
return new Promise((resolve14) => {
|
|
14685
16753
|
let stdout = "";
|
|
14686
16754
|
let stderr = "";
|
|
14687
16755
|
const MAX = 2e5;
|
|
@@ -14697,7 +16765,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14697
16765
|
if (settled) return;
|
|
14698
16766
|
settled = true;
|
|
14699
16767
|
clearTimeout(timer);
|
|
14700
|
-
|
|
16768
|
+
resolve14(result);
|
|
14701
16769
|
};
|
|
14702
16770
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
14703
16771
|
const timer = setTimeout(() => {
|
|
@@ -14732,16 +16800,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
14732
16800
|
}
|
|
14733
16801
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
14734
16802
|
var MAX_TAIL_LINES = 1e5;
|
|
14735
|
-
async function fileLogs(
|
|
16803
|
+
async function fileLogs(path32, lines, filterRe, stream) {
|
|
14736
16804
|
const { createInterface } = await import("node:readline");
|
|
14737
|
-
const { createReadStream } = await import("node:fs");
|
|
16805
|
+
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
14738
16806
|
const entries = [];
|
|
14739
16807
|
const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
|
|
14740
16808
|
const window = new Array(effLines);
|
|
14741
16809
|
let writeIdx = 0;
|
|
14742
16810
|
let totalLines = 0;
|
|
14743
16811
|
const rl = createInterface({
|
|
14744
|
-
input:
|
|
16812
|
+
input: createReadStream2(path32),
|
|
14745
16813
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
14746
16814
|
});
|
|
14747
16815
|
for await (const line of rl) {
|
|
@@ -14762,7 +16830,7 @@ async function fileLogs(path28, lines, filterRe, stream) {
|
|
|
14762
16830
|
if (parsed) entries.push(parsed);
|
|
14763
16831
|
}
|
|
14764
16832
|
return {
|
|
14765
|
-
source:
|
|
16833
|
+
source: path32,
|
|
14766
16834
|
entries,
|
|
14767
16835
|
total: entries.length,
|
|
14768
16836
|
truncated: totalLines > effLines,
|
|
@@ -14916,7 +16984,7 @@ var outdatedTool = {
|
|
|
14916
16984
|
}
|
|
14917
16985
|
};
|
|
14918
16986
|
function runOutdated(manager, args, cwd, signal) {
|
|
14919
|
-
return new Promise((
|
|
16987
|
+
return new Promise((resolve14) => {
|
|
14920
16988
|
let stdout = "";
|
|
14921
16989
|
let stderr = "";
|
|
14922
16990
|
const MAX = 1e5;
|
|
@@ -14941,10 +17009,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
14941
17009
|
});
|
|
14942
17010
|
child.on("close", (code) => {
|
|
14943
17011
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
14944
|
-
|
|
17012
|
+
resolve14(result);
|
|
14945
17013
|
});
|
|
14946
17014
|
child.on("error", (e) => {
|
|
14947
|
-
|
|
17015
|
+
resolve14({
|
|
14948
17016
|
exit_code: 1,
|
|
14949
17017
|
packages: [],
|
|
14950
17018
|
total: 0,
|
|
@@ -14954,9 +17022,9 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
14954
17022
|
});
|
|
14955
17023
|
});
|
|
14956
17024
|
}
|
|
14957
|
-
function parseOutdatedOutput(
|
|
17025
|
+
function parseOutdatedOutput(json2, exitCode) {
|
|
14958
17026
|
const packages = [];
|
|
14959
|
-
if (!
|
|
17027
|
+
if (!json2) {
|
|
14960
17028
|
return {
|
|
14961
17029
|
exit_code: exitCode,
|
|
14962
17030
|
packages: [],
|
|
@@ -14966,7 +17034,7 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
14966
17034
|
};
|
|
14967
17035
|
}
|
|
14968
17036
|
try {
|
|
14969
|
-
const data = JSON.parse(
|
|
17037
|
+
const data = JSON.parse(json2);
|
|
14970
17038
|
for (const name of Object.keys(data)) {
|
|
14971
17039
|
const info = data[name];
|
|
14972
17040
|
packages.push({
|
|
@@ -14984,17 +17052,17 @@ function parseOutdatedOutput(json, exitCode) {
|
|
|
14984
17052
|
exit_code: exitCode,
|
|
14985
17053
|
packages,
|
|
14986
17054
|
total: packages.length,
|
|
14987
|
-
output:
|
|
14988
|
-
truncated:
|
|
17055
|
+
output: json2,
|
|
17056
|
+
truncated: json2.length >= 1e5
|
|
14989
17057
|
};
|
|
14990
17058
|
}
|
|
14991
17059
|
|
|
14992
17060
|
// src/patch.ts
|
|
14993
17061
|
init_util();
|
|
14994
17062
|
import { spawn as spawn13 } from "node:child_process";
|
|
14995
|
-
import * as
|
|
17063
|
+
import * as fs23 from "node:fs/promises";
|
|
14996
17064
|
import * as os7 from "node:os";
|
|
14997
|
-
import * as
|
|
17065
|
+
import * as path26 from "node:path";
|
|
14998
17066
|
import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
|
|
14999
17067
|
var patchTool = {
|
|
15000
17068
|
name: "patch",
|
|
@@ -15030,9 +17098,9 @@ var patchTool = {
|
|
|
15030
17098
|
for (const t of targets) {
|
|
15031
17099
|
const stripped = stripPathComponents(t, strip);
|
|
15032
17100
|
if (!stripped) continue;
|
|
15033
|
-
const candidate =
|
|
15034
|
-
const rel =
|
|
15035
|
-
if (rel.startsWith("..") ||
|
|
17101
|
+
const candidate = path26.resolve(dir, stripped);
|
|
17102
|
+
const rel = path26.relative(ctx.projectRoot, candidate);
|
|
17103
|
+
if (rel.startsWith("..") || path26.isAbsolute(rel)) {
|
|
15036
17104
|
return {
|
|
15037
17105
|
applied: 0,
|
|
15038
17106
|
rejected: 1,
|
|
@@ -15049,12 +17117,12 @@ var patchTool = {
|
|
|
15049
17117
|
beforeContents.set(target, await readTextForTracking(target));
|
|
15050
17118
|
}
|
|
15051
17119
|
}
|
|
15052
|
-
const tmpDir = await
|
|
17120
|
+
const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
|
|
15053
17121
|
try {
|
|
15054
|
-
await
|
|
17122
|
+
await fs23.chmod(tmpDir, 448).catch(() => {
|
|
15055
17123
|
});
|
|
15056
|
-
const patchFile =
|
|
15057
|
-
await
|
|
17124
|
+
const patchFile = path26.join(tmpDir, "in.diff");
|
|
17125
|
+
await fs23.writeFile(patchFile, input.patch, { mode: 384 });
|
|
15058
17126
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
15059
17127
|
const result = await runPatch(args, dir, opts.signal);
|
|
15060
17128
|
if (result.exitCode !== 0 && !dryRun) {
|
|
@@ -15072,8 +17140,8 @@ var patchTool = {
|
|
|
15072
17140
|
const before = beforeContents.get(target) ?? null;
|
|
15073
17141
|
const after = await readTextForTracking(target);
|
|
15074
17142
|
if (after === null || after === before) continue;
|
|
15075
|
-
const
|
|
15076
|
-
if (
|
|
17143
|
+
const stat17 = await fs23.stat(target).catch(() => null);
|
|
17144
|
+
if (stat17) ctx.recordRead?.(target, stat17.mtimeMs, "write", sha256hex(after));
|
|
15077
17145
|
ctx.session?.recordFileChange?.({
|
|
15078
17146
|
path: target,
|
|
15079
17147
|
action: before === null ? "created" : "modified",
|
|
@@ -15090,7 +17158,7 @@ var patchTool = {
|
|
|
15090
17158
|
message: result.stdout || "patch applied"
|
|
15091
17159
|
};
|
|
15092
17160
|
} finally {
|
|
15093
|
-
await
|
|
17161
|
+
await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
15094
17162
|
});
|
|
15095
17163
|
}
|
|
15096
17164
|
}
|
|
@@ -15098,9 +17166,9 @@ var patchTool = {
|
|
|
15098
17166
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
15099
17167
|
async function readTextForTracking(absPath) {
|
|
15100
17168
|
try {
|
|
15101
|
-
const
|
|
15102
|
-
if (!
|
|
15103
|
-
const buf = await
|
|
17169
|
+
const stat17 = await fs23.stat(absPath);
|
|
17170
|
+
if (!stat17.isFile() || stat17.size > MAX_TRACKING_BYTES) return null;
|
|
17171
|
+
const buf = await fs23.readFile(absPath);
|
|
15104
17172
|
if (buf.includes(0)) return null;
|
|
15105
17173
|
return buf.toString("utf8");
|
|
15106
17174
|
} catch {
|
|
@@ -15125,7 +17193,7 @@ function stripPathComponents(p, strip) {
|
|
|
15125
17193
|
return parts.slice(strip).join("/");
|
|
15126
17194
|
}
|
|
15127
17195
|
function runPatch(args, cwd, signal) {
|
|
15128
|
-
return new Promise((
|
|
17196
|
+
return new Promise((resolve14) => {
|
|
15129
17197
|
let stdout = "";
|
|
15130
17198
|
let stderr = "";
|
|
15131
17199
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -15136,8 +17204,8 @@ function runPatch(args, cwd, signal) {
|
|
|
15136
17204
|
child.stderr?.on("data", (c) => {
|
|
15137
17205
|
stderr += c.toString();
|
|
15138
17206
|
});
|
|
15139
|
-
child.on("close", (code) =>
|
|
15140
|
-
child.on("error", (e) =>
|
|
17207
|
+
child.on("close", (code) => resolve14({ exitCode: code ?? 1, stdout, stderr }));
|
|
17208
|
+
child.on("error", (e) => resolve14({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
15141
17209
|
});
|
|
15142
17210
|
}
|
|
15143
17211
|
function extractPatchedFiles(output) {
|
|
@@ -15156,12 +17224,12 @@ import {
|
|
|
15156
17224
|
deriveTodosFromPlanItem,
|
|
15157
17225
|
formatPlan,
|
|
15158
17226
|
getPlanTemplate,
|
|
15159
|
-
mutatePlan,
|
|
17227
|
+
mutatePlan as mutatePlan2,
|
|
15160
17228
|
removePlanItem,
|
|
15161
17229
|
setPlanItemStatus
|
|
15162
17230
|
} from "@wrongstack/core";
|
|
15163
17231
|
import {
|
|
15164
|
-
mutateTasks,
|
|
17232
|
+
mutateTasks as mutateTasks2,
|
|
15165
17233
|
formatTaskList
|
|
15166
17234
|
} from "@wrongstack/core";
|
|
15167
17235
|
import { randomUUID } from "node:crypto";
|
|
@@ -15248,7 +17316,7 @@ var planTool = {
|
|
|
15248
17316
|
let didTaskify = false;
|
|
15249
17317
|
let plan;
|
|
15250
17318
|
try {
|
|
15251
|
-
plan = await
|
|
17319
|
+
plan = await mutatePlan2(planPath, sessionId, async (p) => {
|
|
15252
17320
|
switch (input.action) {
|
|
15253
17321
|
case "show":
|
|
15254
17322
|
break;
|
|
@@ -15374,6 +17442,7 @@ var planTool = {
|
|
|
15374
17442
|
open: 0
|
|
15375
17443
|
};
|
|
15376
17444
|
}
|
|
17445
|
+
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
15377
17446
|
if (early) return early;
|
|
15378
17447
|
if (didTaskify) {
|
|
15379
17448
|
const taskPathRaw = ctx.meta["task.path"];
|
|
@@ -15387,7 +17456,7 @@ var planTool = {
|
|
|
15387
17456
|
}
|
|
15388
17457
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15389
17458
|
try {
|
|
15390
|
-
const taskFile = await
|
|
17459
|
+
const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
|
|
15391
17460
|
f.tasks.push({
|
|
15392
17461
|
id: `task_${randomUUID()}`,
|
|
15393
17462
|
title: taskifyMeta.title,
|
|
@@ -15414,13 +17483,13 @@ ${formatTaskList(taskFile.tasks)}`
|
|
|
15414
17483
|
}
|
|
15415
17484
|
};
|
|
15416
17485
|
function mkResult(plan, ok, message, todos) {
|
|
15417
|
-
const
|
|
17486
|
+
const open3 = plan.items.filter((i) => i.status !== "done").length;
|
|
15418
17487
|
const result = {
|
|
15419
17488
|
ok,
|
|
15420
17489
|
message,
|
|
15421
17490
|
plan: formatPlan(plan),
|
|
15422
17491
|
count: plan.items.length,
|
|
15423
|
-
open:
|
|
17492
|
+
open: open3
|
|
15424
17493
|
};
|
|
15425
17494
|
if (todos !== void 0) result.todos = todos;
|
|
15426
17495
|
return result;
|
|
@@ -15428,7 +17497,7 @@ function mkResult(plan, ok, message, todos) {
|
|
|
15428
17497
|
|
|
15429
17498
|
// src/read.ts
|
|
15430
17499
|
init_util();
|
|
15431
|
-
import * as
|
|
17500
|
+
import * as fs24 from "node:fs/promises";
|
|
15432
17501
|
import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
|
|
15433
17502
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
15434
17503
|
var readTool = {
|
|
@@ -15477,9 +17546,9 @@ var readTool = {
|
|
|
15477
17546
|
});
|
|
15478
17547
|
}
|
|
15479
17548
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15480
|
-
let
|
|
17549
|
+
let stat17;
|
|
15481
17550
|
try {
|
|
15482
|
-
|
|
17551
|
+
stat17 = await fs24.stat(absPath);
|
|
15483
17552
|
} catch (err) {
|
|
15484
17553
|
const code = err.code;
|
|
15485
17554
|
if (code === "ENOENT") {
|
|
@@ -15498,7 +17567,7 @@ var readTool = {
|
|
|
15498
17567
|
cause: err
|
|
15499
17568
|
});
|
|
15500
17569
|
}
|
|
15501
|
-
if (!
|
|
17570
|
+
if (!stat17.isFile()) {
|
|
15502
17571
|
throw new FsError({
|
|
15503
17572
|
message: `read: "${input.path}" is not a regular file`,
|
|
15504
17573
|
code: "FS_READ_FAILED",
|
|
@@ -15506,22 +17575,22 @@ var readTool = {
|
|
|
15506
17575
|
context: { reason: "not-a-regular-file" }
|
|
15507
17576
|
});
|
|
15508
17577
|
}
|
|
15509
|
-
if (
|
|
17578
|
+
if (stat17.size > MAX_BYTES2) {
|
|
15510
17579
|
throw new FsError({
|
|
15511
|
-
message: `read: file too large (${
|
|
17580
|
+
message: `read: file too large (${stat17.size} bytes, limit ${MAX_BYTES2})`,
|
|
15512
17581
|
code: "FS_READ_FAILED",
|
|
15513
17582
|
path: absPath,
|
|
15514
|
-
context: { size:
|
|
17583
|
+
context: { size: stat17.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
15515
17584
|
});
|
|
15516
17585
|
}
|
|
15517
17586
|
const offset = Math.max(1, input.offset ?? 1);
|
|
15518
17587
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
15519
17588
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
15520
17589
|
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,
|
|
17590
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
|
|
17591
|
+
ctx.recordRead(absPath, stat17.mtimeMs);
|
|
15523
17592
|
return {
|
|
15524
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
17593
|
+
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
17594
|
total_lines: prior.totalLines,
|
|
15526
17595
|
encoding: "utf8",
|
|
15527
17596
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -15529,7 +17598,7 @@ var readTool = {
|
|
|
15529
17598
|
note: "Repeated read suppressed to save tokens."
|
|
15530
17599
|
};
|
|
15531
17600
|
}
|
|
15532
|
-
const buf = await
|
|
17601
|
+
const buf = await fs24.readFile(absPath);
|
|
15533
17602
|
if (isBinaryBuffer(buf)) {
|
|
15534
17603
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
15535
17604
|
}
|
|
@@ -15538,10 +17607,10 @@ var readTool = {
|
|
|
15538
17607
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
15539
17608
|
const total = allLines.length;
|
|
15540
17609
|
if (input.mode === "summary") {
|
|
15541
|
-
ctx.recordRead(absPath,
|
|
15542
|
-
rememberReadRange(ctx, absPath,
|
|
17610
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17611
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
|
|
15543
17612
|
return {
|
|
15544
|
-
text: summarizeFile(input.path,
|
|
17613
|
+
text: summarizeFile(input.path, stat17.size, allLines),
|
|
15545
17614
|
total_lines: total,
|
|
15546
17615
|
encoding: "utf8",
|
|
15547
17616
|
truncated: total > 200,
|
|
@@ -15549,13 +17618,13 @@ var readTool = {
|
|
|
15549
17618
|
};
|
|
15550
17619
|
}
|
|
15551
17620
|
if (limit === 0) {
|
|
15552
|
-
ctx.recordRead(absPath,
|
|
15553
|
-
rememberReadRange(ctx, absPath,
|
|
17621
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17622
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
|
|
15554
17623
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
15555
17624
|
}
|
|
15556
17625
|
if (offset > total) {
|
|
15557
|
-
ctx.recordRead(absPath,
|
|
15558
|
-
rememberReadRange(ctx, absPath,
|
|
17626
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17627
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
|
|
15559
17628
|
return {
|
|
15560
17629
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
15561
17630
|
total_lines: total,
|
|
@@ -15567,8 +17636,8 @@ var readTool = {
|
|
|
15567
17636
|
const truncated = offset - 1 + slice.length < total;
|
|
15568
17637
|
const width = String(offset + slice.length - 1).length;
|
|
15569
17638
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
15570
|
-
ctx.recordRead(absPath,
|
|
15571
|
-
rememberReadRange(ctx, absPath,
|
|
17639
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
|
|
17640
|
+
rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
|
|
15572
17641
|
return {
|
|
15573
17642
|
text: numbered,
|
|
15574
17643
|
total_lines: total,
|
|
@@ -15637,10 +17706,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
15637
17706
|
// src/replace.ts
|
|
15638
17707
|
import { expectDefined as expectDefined8 } from "@wrongstack/core";
|
|
15639
17708
|
import { spawn as spawn14 } from "node:child_process";
|
|
15640
|
-
import * as
|
|
15641
|
-
import * as
|
|
17709
|
+
import * as fs25 from "node:fs/promises";
|
|
17710
|
+
import * as path27 from "node:path";
|
|
15642
17711
|
import {
|
|
15643
|
-
atomicWrite as
|
|
17712
|
+
atomicWrite as atomicWrite3,
|
|
15644
17713
|
buildChildEnv as buildChildEnv9,
|
|
15645
17714
|
compileGlob as compileGlob5,
|
|
15646
17715
|
detectNewlineStyle as detectNewlineStyle2,
|
|
@@ -15711,11 +17780,11 @@ var replaceTool = {
|
|
|
15711
17780
|
const dryRun = input.dry_run ?? true;
|
|
15712
17781
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
15713
17782
|
const fileList = await resolveFiles2(filesInput, ctx, globRe);
|
|
15714
|
-
const realRoot = await
|
|
17783
|
+
const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
15715
17784
|
const results = [];
|
|
15716
17785
|
let totalReplacements = 0;
|
|
15717
17786
|
for (const absPath of fileList) {
|
|
15718
|
-
const lstat2 = await
|
|
17787
|
+
const lstat2 = await fs25.lstat(absPath).catch((err) => {
|
|
15719
17788
|
if (err.code === "ENOENT") return null;
|
|
15720
17789
|
throw err;
|
|
15721
17790
|
});
|
|
@@ -15723,17 +17792,17 @@ var replaceTool = {
|
|
|
15723
17792
|
if (lstat2.isSymbolicLink()) continue;
|
|
15724
17793
|
let realPath;
|
|
15725
17794
|
try {
|
|
15726
|
-
realPath = await
|
|
17795
|
+
realPath = await fs25.realpath(absPath);
|
|
15727
17796
|
} catch {
|
|
15728
17797
|
continue;
|
|
15729
17798
|
}
|
|
15730
|
-
const rel =
|
|
15731
|
-
if (rel.startsWith("..") ||
|
|
15732
|
-
const
|
|
15733
|
-
if (!
|
|
17799
|
+
const rel = path27.relative(realRoot, realPath);
|
|
17800
|
+
if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
|
|
17801
|
+
const stat17 = await fs25.stat(realPath).catch(() => null);
|
|
17802
|
+
if (!stat17?.isFile()) continue;
|
|
15734
17803
|
let content;
|
|
15735
17804
|
try {
|
|
15736
|
-
const buf = await
|
|
17805
|
+
const buf = await fs25.readFile(realPath);
|
|
15737
17806
|
if (isBinaryBuffer(buf)) continue;
|
|
15738
17807
|
content = buf.toString("utf8");
|
|
15739
17808
|
} catch {
|
|
@@ -15755,8 +17824,8 @@ var replaceTool = {
|
|
|
15755
17824
|
totalReplacements += count;
|
|
15756
17825
|
if (!dryRun) {
|
|
15757
17826
|
const newContent = toStyle2(newContentLf, style);
|
|
15758
|
-
await
|
|
15759
|
-
const written = await
|
|
17827
|
+
await atomicWrite3(realPath, newContent, { mode: stat17.mode & 511 });
|
|
17828
|
+
const written = await fs25.stat(realPath).catch(() => null);
|
|
15760
17829
|
if (written) {
|
|
15761
17830
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
15762
17831
|
}
|
|
@@ -15795,8 +17864,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
15795
17864
|
const resolved = [];
|
|
15796
17865
|
for (const p of parts) {
|
|
15797
17866
|
const absPath = safeResolve(p, ctx);
|
|
15798
|
-
const
|
|
15799
|
-
if (
|
|
17867
|
+
const stat17 = await fs25.stat(absPath).catch(() => null);
|
|
17868
|
+
if (stat17?.isFile()) {
|
|
15800
17869
|
resolved.push(absPath);
|
|
15801
17870
|
}
|
|
15802
17871
|
}
|
|
@@ -15814,13 +17883,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
15814
17883
|
return await globNative(pattern, base, extraGlob);
|
|
15815
17884
|
}
|
|
15816
17885
|
function checkRg() {
|
|
15817
|
-
return new Promise((
|
|
17886
|
+
return new Promise((resolve14) => {
|
|
15818
17887
|
try {
|
|
15819
17888
|
const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
|
|
15820
|
-
p.on("error", () =>
|
|
15821
|
-
p.on("close", (code) =>
|
|
17889
|
+
p.on("error", () => resolve14(false));
|
|
17890
|
+
p.on("close", (code) => resolve14(code === 0));
|
|
15822
17891
|
} catch {
|
|
15823
|
-
|
|
17892
|
+
resolve14(false);
|
|
15824
17893
|
}
|
|
15825
17894
|
});
|
|
15826
17895
|
}
|
|
@@ -15837,10 +17906,10 @@ function spawnRgFind(pattern, base) {
|
|
|
15837
17906
|
buf += chunk.toString();
|
|
15838
17907
|
});
|
|
15839
17908
|
return {
|
|
15840
|
-
promise: new Promise((
|
|
17909
|
+
promise: new Promise((resolve14, reject) => {
|
|
15841
17910
|
child.on("error", reject);
|
|
15842
17911
|
child.on("close", () => {
|
|
15843
|
-
|
|
17912
|
+
resolve14(buf.split("\n").filter(Boolean));
|
|
15844
17913
|
});
|
|
15845
17914
|
})
|
|
15846
17915
|
};
|
|
@@ -15851,16 +17920,16 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
15851
17920
|
const walk = async (dir) => {
|
|
15852
17921
|
let entries;
|
|
15853
17922
|
try {
|
|
15854
|
-
entries = await
|
|
17923
|
+
entries = await fs25.readdir(dir, { withFileTypes: true });
|
|
15855
17924
|
} catch {
|
|
15856
17925
|
return;
|
|
15857
17926
|
}
|
|
15858
17927
|
for (const e of entries) {
|
|
15859
17928
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
15860
|
-
const full =
|
|
17929
|
+
const full = path27.join(dir, e.name);
|
|
15861
17930
|
try {
|
|
15862
|
-
const
|
|
15863
|
-
if (
|
|
17931
|
+
const stat17 = await fs25.lstat(full);
|
|
17932
|
+
if (stat17.isSymbolicLink()) continue;
|
|
15864
17933
|
} catch {
|
|
15865
17934
|
continue;
|
|
15866
17935
|
}
|
|
@@ -15883,9 +17952,9 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
15883
17952
|
|
|
15884
17953
|
// src/scaffold.ts
|
|
15885
17954
|
init_util();
|
|
15886
|
-
import * as
|
|
15887
|
-
import * as
|
|
15888
|
-
import { atomicWrite as
|
|
17955
|
+
import * as fs26 from "node:fs/promises";
|
|
17956
|
+
import * as path28 from "node:path";
|
|
17957
|
+
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
15889
17958
|
var BUILT_IN_TEMPLATES = {
|
|
15890
17959
|
"npm-package": {
|
|
15891
17960
|
description: "Basic npm package with ESM",
|
|
@@ -16034,17 +18103,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
16034
18103
|
let filesCreated = 0;
|
|
16035
18104
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
16036
18105
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
16037
|
-
const joinedPath =
|
|
16038
|
-
const root =
|
|
16039
|
-
const target =
|
|
16040
|
-
const rel =
|
|
16041
|
-
if (rel.startsWith("..") ||
|
|
18106
|
+
const joinedPath = path28.join(cwd, resolvedPath);
|
|
18107
|
+
const root = path28.resolve(ctx.projectRoot);
|
|
18108
|
+
const target = path28.resolve(joinedPath);
|
|
18109
|
+
const rel = path28.relative(root, target);
|
|
18110
|
+
if (rel.startsWith("..") || path28.isAbsolute(rel)) {
|
|
16042
18111
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
16043
18112
|
}
|
|
16044
18113
|
const fullPath = target;
|
|
16045
18114
|
if (!dryRun) {
|
|
16046
|
-
await
|
|
16047
|
-
await
|
|
18115
|
+
await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
|
|
18116
|
+
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
16048
18117
|
}
|
|
16049
18118
|
files.push(resolvedPath);
|
|
16050
18119
|
filesCreated++;
|
|
@@ -16078,6 +18147,7 @@ var DEFAULT_NUM = 10;
|
|
|
16078
18147
|
var MAX_RESULTS = 50;
|
|
16079
18148
|
var TIMEOUT_MS3 = 15e3;
|
|
16080
18149
|
var CACHE_TTL_MS = 3e5;
|
|
18150
|
+
var CACHE_MAX_ENTRIES = 200;
|
|
16081
18151
|
var cache = /* @__PURE__ */ new Map();
|
|
16082
18152
|
var searchTool = {
|
|
16083
18153
|
name: "search",
|
|
@@ -16199,7 +18269,7 @@ var searchTool = {
|
|
|
16199
18269
|
}
|
|
16200
18270
|
const finalResults = ranked.slice(0, num);
|
|
16201
18271
|
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
16202
|
-
|
|
18272
|
+
pruneCacheEntries();
|
|
16203
18273
|
yield {
|
|
16204
18274
|
type: "partial_output",
|
|
16205
18275
|
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
@@ -16221,11 +18291,16 @@ var searchTool = {
|
|
|
16221
18291
|
};
|
|
16222
18292
|
}
|
|
16223
18293
|
};
|
|
16224
|
-
function
|
|
18294
|
+
function pruneCacheEntries() {
|
|
16225
18295
|
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
16226
18296
|
for (const [key, entry] of cache.entries()) {
|
|
16227
18297
|
if (entry.timestamp < cutoff) cache.delete(key);
|
|
16228
18298
|
}
|
|
18299
|
+
while (cache.size > CACHE_MAX_ENTRIES) {
|
|
18300
|
+
const oldestKey = cache.keys().next().value;
|
|
18301
|
+
if (oldestKey === void 0) break;
|
|
18302
|
+
cache.delete(oldestKey);
|
|
18303
|
+
}
|
|
16229
18304
|
}
|
|
16230
18305
|
function rankSearchResults(results, query) {
|
|
16231
18306
|
const seenUrls = /* @__PURE__ */ new Set();
|
|
@@ -16454,7 +18529,7 @@ function decodeHtmlEntities(text) {
|
|
|
16454
18529
|
}
|
|
16455
18530
|
|
|
16456
18531
|
// src/set-working-dir.ts
|
|
16457
|
-
import * as
|
|
18532
|
+
import * as fs27 from "node:fs/promises";
|
|
16458
18533
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
16459
18534
|
var setWorkingDirTool = {
|
|
16460
18535
|
name: "set_working_dir",
|
|
@@ -16493,7 +18568,7 @@ var setWorkingDirTool = {
|
|
|
16493
18568
|
};
|
|
16494
18569
|
}
|
|
16495
18570
|
try {
|
|
16496
|
-
await
|
|
18571
|
+
await fs27.access(resolved);
|
|
16497
18572
|
} catch {
|
|
16498
18573
|
try {
|
|
16499
18574
|
ctx.setWorkingDir(previous);
|
|
@@ -16518,11 +18593,11 @@ import {
|
|
|
16518
18593
|
formatTaskList as formatTaskList2
|
|
16519
18594
|
} from "@wrongstack/core";
|
|
16520
18595
|
import {
|
|
16521
|
-
mutateTasks as
|
|
18596
|
+
mutateTasks as mutateTasks3
|
|
16522
18597
|
} from "@wrongstack/core";
|
|
16523
18598
|
import {
|
|
16524
18599
|
addPlanItem as addPlanItem2,
|
|
16525
|
-
mutatePlan as
|
|
18600
|
+
mutatePlan as mutatePlan3,
|
|
16526
18601
|
formatPlan as formatPlan2
|
|
16527
18602
|
} from "@wrongstack/core";
|
|
16528
18603
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -16642,7 +18717,7 @@ var taskTool = {
|
|
|
16642
18717
|
let todosToReplace = null;
|
|
16643
18718
|
let file;
|
|
16644
18719
|
try {
|
|
16645
|
-
file = await
|
|
18720
|
+
file = await mutateTasks3(taskPath, sessionId, async (f) => {
|
|
16646
18721
|
switch (input.action) {
|
|
16647
18722
|
case "show":
|
|
16648
18723
|
break;
|
|
@@ -16832,6 +18907,7 @@ var taskTool = {
|
|
|
16832
18907
|
};
|
|
16833
18908
|
}
|
|
16834
18909
|
if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
|
|
18910
|
+
await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
|
|
16835
18911
|
if (early) return early;
|
|
16836
18912
|
if (didPlanify) {
|
|
16837
18913
|
const { title, details } = planifyMeta;
|
|
@@ -16845,7 +18921,7 @@ var taskTool = {
|
|
|
16845
18921
|
}
|
|
16846
18922
|
let formatted = "";
|
|
16847
18923
|
try {
|
|
16848
|
-
await
|
|
18924
|
+
await mutatePlan3(planPath, sessionId, (pf) => {
|
|
16849
18925
|
const { plan: updated } = addPlanItem2(pf, title, details || void 0);
|
|
16850
18926
|
formatted = formatPlan2(updated);
|
|
16851
18927
|
return updated;
|
|
@@ -16893,7 +18969,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
16893
18969
|
init_spawn_stream();
|
|
16894
18970
|
init_util();
|
|
16895
18971
|
init_legacy_bridge();
|
|
16896
|
-
import * as
|
|
18972
|
+
import * as path29 from "node:path";
|
|
16897
18973
|
var testTool = {
|
|
16898
18974
|
name: "test",
|
|
16899
18975
|
category: "Code Quality",
|
|
@@ -16996,11 +19072,11 @@ var testTool = {
|
|
|
16996
19072
|
}
|
|
16997
19073
|
};
|
|
16998
19074
|
async function detectRunner(cwd) {
|
|
16999
|
-
const { stat:
|
|
19075
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
17000
19076
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
17001
19077
|
for (const f of candidates) {
|
|
17002
19078
|
try {
|
|
17003
|
-
await
|
|
19079
|
+
await stat17(path29.join(cwd, f));
|
|
17004
19080
|
if (f.includes("vitest")) return "vitest";
|
|
17005
19081
|
if (f.includes("jest")) return "jest";
|
|
17006
19082
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -17077,8 +19153,7 @@ function parseResult(runner, result, duration) {
|
|
|
17077
19153
|
}
|
|
17078
19154
|
|
|
17079
19155
|
// src/todo.ts
|
|
17080
|
-
import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
|
|
17081
|
-
import { loadTasks, saveTasks } from "@wrongstack/core";
|
|
19156
|
+
import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
|
|
17082
19157
|
var todoTool = {
|
|
17083
19158
|
name: "todo",
|
|
17084
19159
|
category: "Session",
|
|
@@ -17087,7 +19162,7 @@ var todoTool = {
|
|
|
17087
19162
|
permission: "auto",
|
|
17088
19163
|
mutating: false,
|
|
17089
19164
|
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
17090
|
-
timeoutMs:
|
|
19165
|
+
timeoutMs: 5e3,
|
|
17091
19166
|
capabilities: ["session.todo"],
|
|
17092
19167
|
icon: "todo",
|
|
17093
19168
|
inputSchema: {
|
|
@@ -17139,16 +19214,21 @@ var todoTool = {
|
|
|
17139
19214
|
}
|
|
17140
19215
|
}
|
|
17141
19216
|
ctx.state.replaceTodos(items);
|
|
19217
|
+
await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
17142
19218
|
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
17143
19219
|
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
17144
19220
|
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
17145
19221
|
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
17146
19222
|
for (const item of items) {
|
|
17147
19223
|
if (item.promotedFromPlan) {
|
|
17148
|
-
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
19224
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
19225
|
+
item.promotedFromPlan
|
|
19226
|
+
);
|
|
17149
19227
|
}
|
|
17150
19228
|
if (item.promotedFromTask) {
|
|
17151
|
-
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
19229
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
19230
|
+
item.promotedFromTask
|
|
19231
|
+
);
|
|
17152
19232
|
}
|
|
17153
19233
|
}
|
|
17154
19234
|
for (const planId of completedPlanIds) {
|
|
@@ -17156,7 +19236,7 @@ var todoTool = {
|
|
|
17156
19236
|
const planPath = ctx.meta["plan.path"];
|
|
17157
19237
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
17158
19238
|
try {
|
|
17159
|
-
const plan = await
|
|
19239
|
+
const plan = await loadPlan2(planPath);
|
|
17160
19240
|
if (plan) {
|
|
17161
19241
|
const updated = setPlanItemStatus2(plan, planId, "done");
|
|
17162
19242
|
await savePlan(planPath, updated);
|
|
@@ -17169,7 +19249,7 @@ var todoTool = {
|
|
|
17169
19249
|
const taskPath = ctx.meta["task.path"];
|
|
17170
19250
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
17171
19251
|
try {
|
|
17172
|
-
const file = await
|
|
19252
|
+
const file = await loadTasks3(taskPath);
|
|
17173
19253
|
if (file) {
|
|
17174
19254
|
const task = file.tasks.find((t) => t.id === taskId);
|
|
17175
19255
|
if (task && task.status !== "completed") {
|
|
@@ -17466,8 +19546,8 @@ var toolUseTool = {
|
|
|
17466
19546
|
// src/tree.ts
|
|
17467
19547
|
init_util();
|
|
17468
19548
|
import { expectDefined as expectDefined10 } from "@wrongstack/core";
|
|
17469
|
-
import * as
|
|
17470
|
-
import * as
|
|
19549
|
+
import * as fs28 from "node:fs/promises";
|
|
19550
|
+
import * as path30 from "node:path";
|
|
17471
19551
|
var DEFAULT_IGNORE5 = [
|
|
17472
19552
|
"node_modules",
|
|
17473
19553
|
".git",
|
|
@@ -17608,7 +19688,7 @@ var treeTool = {
|
|
|
17608
19688
|
}
|
|
17609
19689
|
};
|
|
17610
19690
|
async function walkDir(dir, depth, opts) {
|
|
17611
|
-
const entries = await
|
|
19691
|
+
const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
17612
19692
|
const filtered = entries.filter((e) => {
|
|
17613
19693
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
17614
19694
|
if (opts.exclude.has(e.name)) return false;
|
|
@@ -17638,7 +19718,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17638
19718
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
17639
19719
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
17640
19720
|
const childPrefix = opts.prefix + connector;
|
|
17641
|
-
await walkDir(
|
|
19721
|
+
await walkDir(path30.join(dir, entry.name), depth + 1, {
|
|
17642
19722
|
...opts,
|
|
17643
19723
|
prefix: childPrefix,
|
|
17644
19724
|
isLast
|
|
@@ -17651,7 +19731,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
17651
19731
|
init_spawn_stream();
|
|
17652
19732
|
init_util();
|
|
17653
19733
|
init_legacy_bridge();
|
|
17654
|
-
import * as
|
|
19734
|
+
import * as path31 from "node:path";
|
|
17655
19735
|
var typecheckTool = {
|
|
17656
19736
|
name: "typecheck",
|
|
17657
19737
|
category: "Code Quality",
|
|
@@ -17750,12 +19830,12 @@ var typecheckTool = {
|
|
|
17750
19830
|
}
|
|
17751
19831
|
};
|
|
17752
19832
|
async function findTsConfig(cwd) {
|
|
17753
|
-
const { stat:
|
|
19833
|
+
const { stat: stat17 } = await import("node:fs/promises");
|
|
17754
19834
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
17755
19835
|
for (const f of candidates) {
|
|
17756
19836
|
try {
|
|
17757
|
-
const s = await
|
|
17758
|
-
if (s.isFile()) return
|
|
19837
|
+
const s = await stat17(path31.join(cwd, f));
|
|
19838
|
+
if (s.isFile()) return path31.join(cwd, f);
|
|
17759
19839
|
} catch {
|
|
17760
19840
|
}
|
|
17761
19841
|
}
|
|
@@ -17763,8 +19843,8 @@ async function findTsConfig(cwd) {
|
|
|
17763
19843
|
}
|
|
17764
19844
|
|
|
17765
19845
|
// src/write.ts
|
|
17766
|
-
import * as
|
|
17767
|
-
import { atomicWrite as
|
|
19846
|
+
import * as fs29 from "node:fs/promises";
|
|
19847
|
+
import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
|
|
17768
19848
|
init_util();
|
|
17769
19849
|
var writeTool = {
|
|
17770
19850
|
name: "write",
|
|
@@ -17828,14 +19908,14 @@ async function prepareWrite(input, ctx) {
|
|
|
17828
19908
|
let existed = false;
|
|
17829
19909
|
let prev = "";
|
|
17830
19910
|
try {
|
|
17831
|
-
const
|
|
17832
|
-
existed =
|
|
19911
|
+
const stat17 = await fs29.stat(absPath);
|
|
19912
|
+
existed = stat17.isFile();
|
|
17833
19913
|
if (existed) {
|
|
17834
19914
|
if (!ctx.hasRead(absPath)) {
|
|
17835
|
-
prev = await
|
|
17836
|
-
ctx.recordRead(absPath,
|
|
19915
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
19916
|
+
ctx.recordRead(absPath, stat17.mtimeMs, "write", sha256hex(prev));
|
|
17837
19917
|
} else {
|
|
17838
|
-
prev = await
|
|
19918
|
+
prev = await fs29.readFile(absPath, "utf8");
|
|
17839
19919
|
}
|
|
17840
19920
|
}
|
|
17841
19921
|
} catch (err) {
|
|
@@ -17847,11 +19927,11 @@ async function prepareWrite(input, ctx) {
|
|
|
17847
19927
|
}
|
|
17848
19928
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
17849
19929
|
signal?.throwIfAborted();
|
|
17850
|
-
await
|
|
19930
|
+
await atomicWrite5(prepared.absPath, input.content);
|
|
17851
19931
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
17852
19932
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
17853
|
-
const
|
|
17854
|
-
ctx.recordRead(prepared.absPath,
|
|
19933
|
+
const stat17 = await fs29.stat(prepared.absPath);
|
|
19934
|
+
ctx.recordRead(prepared.absPath, stat17.mtimeMs, "write", sha256hex(input.content));
|
|
17855
19935
|
ctx.session.recordFileChange({
|
|
17856
19936
|
path: prepared.absPath,
|
|
17857
19937
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -17876,6 +19956,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
17876
19956
|
|
|
17877
19957
|
// src/builtin.ts
|
|
17878
19958
|
var OPTIONAL_TOOLS = [
|
|
19959
|
+
...browserTools,
|
|
19960
|
+
e2ePlanTool,
|
|
17879
19961
|
installTool,
|
|
17880
19962
|
auditTool,
|
|
17881
19963
|
outdatedTool,
|
|
@@ -17939,6 +20021,8 @@ var TIER3_TOOLS = [
|
|
|
17939
20021
|
setWorkingDirTool
|
|
17940
20022
|
];
|
|
17941
20023
|
var builtinTools = [
|
|
20024
|
+
...browserTools,
|
|
20025
|
+
e2ePlanTool,
|
|
17942
20026
|
readTool,
|
|
17943
20027
|
writeTool,
|
|
17944
20028
|
editTool,
|