@wrongstack/plugins 0.300.0 → 0.301.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 +3 -1
- package/dist/config-validator.js +32 -2
- package/dist/index.js +1328 -49
- package/dist/path-guard/index.d.ts +14 -12
- package/dist/path-guard.js +1226 -38
- package/dist/plugin-audit-catalog.js +7 -0
- package/dist/pr-drafter.js +8 -2
- package/dist/shell-check.js +1 -0
- package/dist/smart-rename.js +5 -1
- package/dist/template-engine.js +51 -6
- package/dist/test-generator.js +30 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4303,6 +4303,9 @@ function positionToLineCol(text, pos) {
|
|
|
4303
4303
|
const col = pos - upTo.lastIndexOf("\n");
|
|
4304
4304
|
return { line, col };
|
|
4305
4305
|
}
|
|
4306
|
+
function redactParseSnippet(message) {
|
|
4307
|
+
return message.replace(/"[\s\S]{0,200}?"(?= is not valid JSON)/g, '"\u2026"');
|
|
4308
|
+
}
|
|
4306
4309
|
function validateJson(text, isJsonc, fileName) {
|
|
4307
4310
|
const source = isJsonc ? stripJsonc(text) : text;
|
|
4308
4311
|
try {
|
|
@@ -4336,10 +4339,10 @@ function validateJson(text, isJsonc, fileName) {
|
|
|
4336
4339
|
const idx = source.indexOf(snippetMatch[1]);
|
|
4337
4340
|
if (idx >= 0) {
|
|
4338
4341
|
const { line } = positionToLineCol(source, idx);
|
|
4339
|
-
return [`JSON parse error near line ${line}
|
|
4342
|
+
return [`JSON parse error near line ${line}`];
|
|
4340
4343
|
}
|
|
4341
4344
|
}
|
|
4342
|
-
return [`JSON parse error: ${message.split("\n")[0]}`];
|
|
4345
|
+
return [`JSON parse error: ${redactParseSnippet(message.split("\n")[0] ?? "")}`];
|
|
4343
4346
|
}
|
|
4344
4347
|
}
|
|
4345
4348
|
function validateYaml(text) {
|
|
@@ -4476,10 +4479,12 @@ var plugin12 = {
|
|
|
4476
4479
|
const cfg = readConfig11(api.config.extensions?.["config-validator"]);
|
|
4477
4480
|
const hook = (input) => {
|
|
4478
4481
|
if (!cfg.enabled) return;
|
|
4482
|
+
if (input.toolResult?.isError) return;
|
|
4479
4483
|
state12.invocations += 1;
|
|
4480
4484
|
const ti = input.toolInput ?? {};
|
|
4481
4485
|
const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
|
|
4482
4486
|
if (typeof raw !== "string" || raw.length === 0) return;
|
|
4487
|
+
if (!withinProject(raw)) return;
|
|
4483
4488
|
const lower = raw.toLowerCase();
|
|
4484
4489
|
if (!cfg.extensions.some((ext) => lower.endsWith(ext))) return;
|
|
4485
4490
|
let text;
|
|
@@ -13166,11 +13171,15 @@ function compilePathGlob(pattern) {
|
|
|
13166
13171
|
let source = "";
|
|
13167
13172
|
for (let i = 0; i < normalized.length; i++) {
|
|
13168
13173
|
const ch = normalized[i];
|
|
13174
|
+
if (ch === "/" && normalized.slice(i) === "/**") {
|
|
13175
|
+
source += "(?:/(?:[^/]+(?:/[^/]+)*)?)?";
|
|
13176
|
+
break;
|
|
13177
|
+
}
|
|
13169
13178
|
if (ch === "*") {
|
|
13170
13179
|
if (normalized[i + 1] === "*") {
|
|
13171
|
-
|
|
13172
|
-
|
|
13173
|
-
|
|
13180
|
+
const followedBySlash = normalized[i + 2] === "/";
|
|
13181
|
+
source += followedBySlash ? "(?:[^/]+/)*" : "(?:[^/]+(?:/[^/]+)*)?";
|
|
13182
|
+
i += followedBySlash ? 2 : 1;
|
|
13174
13183
|
} else {
|
|
13175
13184
|
source += "[^/]*";
|
|
13176
13185
|
}
|
|
@@ -13185,31 +13194,1185 @@ function compilePathGlob(pattern) {
|
|
|
13185
13194
|
return new RegExp(`(?:^|/)${source}$`, "i");
|
|
13186
13195
|
}
|
|
13187
13196
|
function normalizePath2(p) {
|
|
13188
|
-
|
|
13197
|
+
const slashNormalized = p.replace(/\\/g, "/");
|
|
13198
|
+
const drive = /^[a-z]:/i.exec(slashNormalized)?.[0] ?? "";
|
|
13199
|
+
const absolute = slashNormalized.startsWith("/") || drive.length > 0;
|
|
13200
|
+
const body = drive ? slashNormalized.slice(drive.length).replace(/^\//, "") : slashNormalized;
|
|
13201
|
+
const segments = [];
|
|
13202
|
+
for (const segment of body.split("/")) {
|
|
13203
|
+
if (!segment || segment === ".") continue;
|
|
13204
|
+
if (segment === "..") {
|
|
13205
|
+
if (segments.length > 0 && segments.at(-1) !== "..") {
|
|
13206
|
+
segments.pop();
|
|
13207
|
+
} else if (!absolute) {
|
|
13208
|
+
segments.push(segment);
|
|
13209
|
+
}
|
|
13210
|
+
continue;
|
|
13211
|
+
}
|
|
13212
|
+
segments.push(segment);
|
|
13213
|
+
}
|
|
13214
|
+
const joined = segments.join("/");
|
|
13215
|
+
if (drive) return `${drive}/${joined}`.replace(/\/$/, "");
|
|
13216
|
+
if (slashNormalized.startsWith("/")) return `/${joined}`.replace(/\/$/, "") || "/";
|
|
13217
|
+
return joined;
|
|
13218
|
+
}
|
|
13219
|
+
function isAbsolutePath(path) {
|
|
13220
|
+
return /^(?:\/|[a-z]:\/)/i.test(path);
|
|
13221
|
+
}
|
|
13222
|
+
function resolveTargetPath(path, base) {
|
|
13223
|
+
const normalized = normalizePath2(path);
|
|
13224
|
+
const target = normalized === "" && /^\.\/?$/.test(path.trim()) ? "." : normalized;
|
|
13225
|
+
return base && !isAbsolutePath(target) ? normalizePath2(`${base}/${target}`) : target;
|
|
13226
|
+
}
|
|
13227
|
+
function relativeToInvocationCwd(path, invocationCwd) {
|
|
13228
|
+
const normalizedPath = normalizePath2(path).replace(/\/$/, "");
|
|
13229
|
+
const normalized = normalizedPath === "" && /^\.\/?$/.test(path.trim()) ? "." : normalizedPath;
|
|
13230
|
+
if (!invocationCwd || !isAbsolutePath(normalizePath2(invocationCwd))) return normalized;
|
|
13231
|
+
const root = normalizePath2(invocationCwd).replace(/\/$/, "");
|
|
13232
|
+
const pathForComparison = /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
13233
|
+
const rootForComparison = /^[a-z]:\//i.test(root) ? root.toLowerCase() : root;
|
|
13234
|
+
if (pathForComparison === rootForComparison) return ".";
|
|
13235
|
+
if (pathForComparison.startsWith(`${rootForComparison}/`)) {
|
|
13236
|
+
return normalized.slice(root.length + 1);
|
|
13237
|
+
}
|
|
13238
|
+
return normalized;
|
|
13239
|
+
}
|
|
13240
|
+
function effectiveToolCwd(toolInputCwd, invocationCwd) {
|
|
13241
|
+
if (typeof toolInputCwd !== "string" || toolInputCwd.length === 0) return invocationCwd;
|
|
13242
|
+
if (!invocationCwd || isAbsolutePath(normalizePath2(toolInputCwd))) return toolInputCwd;
|
|
13243
|
+
return resolveTargetPath(toolInputCwd, invocationCwd);
|
|
13189
13244
|
}
|
|
13190
13245
|
function matchesAny(path, patterns) {
|
|
13191
13246
|
const normalized = normalizePath2(path);
|
|
13192
13247
|
return patterns.some((re) => re.test(normalized));
|
|
13193
13248
|
}
|
|
13249
|
+
function isUnresolvedPathScope(path) {
|
|
13250
|
+
return /[*?]/.test(path);
|
|
13251
|
+
}
|
|
13252
|
+
function isRootPathScope(path) {
|
|
13253
|
+
const normalized = normalizePath2(path).replace(/\/$/, "");
|
|
13254
|
+
return normalized === "." || normalized === "";
|
|
13255
|
+
}
|
|
13256
|
+
function isDirectoryAmbiguousPath(path) {
|
|
13257
|
+
const normalized = normalizePath2(path).replace(/\/$/, "");
|
|
13258
|
+
if (isRootPathScope(normalized)) return true;
|
|
13259
|
+
const basename7 = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
13260
|
+
return path.endsWith("/") || basename7.length > 0 && !basename7.includes(".");
|
|
13261
|
+
}
|
|
13262
|
+
function hasConfiguredProtectedDescendant(path, patterns) {
|
|
13263
|
+
const normalized = normalizePath2(path).replace(/\/$/, "").toLowerCase();
|
|
13264
|
+
if (!normalized || isUnresolvedPathScope(normalized)) return false;
|
|
13265
|
+
return patterns.some((pattern) => {
|
|
13266
|
+
const prefix = staticPrefix(normalizePath2(pattern)).toLowerCase();
|
|
13267
|
+
return prefix.startsWith(`${normalized}/`);
|
|
13268
|
+
});
|
|
13269
|
+
}
|
|
13270
|
+
function staticPrefix(pattern) {
|
|
13271
|
+
const normalized = normalizePath2(pattern);
|
|
13272
|
+
if (normalized === ".") return "";
|
|
13273
|
+
const wildcardIndex = normalized.search(/[*?]/);
|
|
13274
|
+
return (wildcardIndex === -1 ? normalized : normalized.slice(0, wildcardIndex)).replace(
|
|
13275
|
+
/\/$/,
|
|
13276
|
+
""
|
|
13277
|
+
);
|
|
13278
|
+
}
|
|
13279
|
+
function hasPartialSegmentWildcard(pattern) {
|
|
13280
|
+
const normalized = normalizePath2(pattern);
|
|
13281
|
+
const wildcardIndex = normalized.search(/[*?]/);
|
|
13282
|
+
return wildcardIndex > 0 && normalized[wildcardIndex - 1] !== "/";
|
|
13283
|
+
}
|
|
13284
|
+
function globWitness(pattern) {
|
|
13285
|
+
return pattern.replace(/\*+/g, "").replace(/\?/g, "x");
|
|
13286
|
+
}
|
|
13287
|
+
function scopesMayOverlap(left, right) {
|
|
13288
|
+
const normalizedRight = normalizePath2(right);
|
|
13289
|
+
if (!normalizedRight.includes("/")) {
|
|
13290
|
+
const leftPrefix2 = staticPrefix(left);
|
|
13291
|
+
const candidate = leftPrefix2 ? `${leftPrefix2}/${globWitness(normalizedRight)}` : globWitness(normalizedRight);
|
|
13292
|
+
if (compilePathGlob(left).test(candidate)) return true;
|
|
13293
|
+
return hasPartialSegmentWildcard(left);
|
|
13294
|
+
}
|
|
13295
|
+
const leftPrefix = staticPrefix(left).toLowerCase();
|
|
13296
|
+
const rightPrefix = staticPrefix(right).toLowerCase();
|
|
13297
|
+
if (!leftPrefix || !rightPrefix) return true;
|
|
13298
|
+
if (leftPrefix === rightPrefix || leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`)) {
|
|
13299
|
+
return true;
|
|
13300
|
+
}
|
|
13301
|
+
return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
|
|
13302
|
+
}
|
|
13303
|
+
function targetIntersectsPatterns(target, patternTexts, patterns) {
|
|
13304
|
+
if (matchesAny(target.path, patterns)) return true;
|
|
13305
|
+
if (target.kind === "file") return false;
|
|
13306
|
+
const normalized = normalizePath2(target.path).replace(/\/$/, "");
|
|
13307
|
+
if (!isUnresolvedPathScope(normalized)) {
|
|
13308
|
+
if (normalized === "." || normalized === "") return patternTexts.length > 0;
|
|
13309
|
+
const descendantScope = `${normalized}/**`;
|
|
13310
|
+
return patternTexts.some(
|
|
13311
|
+
(pattern) => scopesMayOverlap(descendantScope, normalizePath2(pattern))
|
|
13312
|
+
);
|
|
13313
|
+
}
|
|
13314
|
+
return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath2(pattern)));
|
|
13315
|
+
}
|
|
13316
|
+
function targetFullyAllowed(target, allowTexts, allowRes) {
|
|
13317
|
+
if (target.kind === "file") return matchesAny(target.path, allowRes);
|
|
13318
|
+
const normalized = normalizePath2(target.path).replace(/\/$/, "");
|
|
13319
|
+
if (isUnresolvedPathScope(normalized)) {
|
|
13320
|
+
return allowTexts.some((allow, index) => {
|
|
13321
|
+
const allowNormalized = normalizePath2(allow);
|
|
13322
|
+
if (allowNormalized === normalized) return true;
|
|
13323
|
+
const targetPrefix = staticPrefix(normalized);
|
|
13324
|
+
const allowRe = allowRes[index];
|
|
13325
|
+
return targetPrefix.length > 0 && !hasPartialSegmentWildcard(normalized) && allowNormalized.endsWith("/**") && allowRe?.test(targetPrefix);
|
|
13326
|
+
});
|
|
13327
|
+
}
|
|
13328
|
+
return allowTexts.some((allow, index) => {
|
|
13329
|
+
const allowNormalized = normalizePath2(allow);
|
|
13330
|
+
const allowRe = allowRes[index];
|
|
13331
|
+
return allowNormalized.endsWith("/**") && allowRe?.test(`${normalized}/.path-guard-probe`);
|
|
13332
|
+
});
|
|
13333
|
+
}
|
|
13334
|
+
var WRITE_CAPABILITIES = /* @__PURE__ */ new Set(["fs.write", "fs.write.outside-project"]);
|
|
13335
|
+
var DISK_MUTATING_CAPABILITIES = /* @__PURE__ */ new Set(["package.install"]);
|
|
13336
|
+
var SHELL_CAPABILITIES = /* @__PURE__ */ new Set(["shell.arbitrary", "shell.restricted", "shell.exec"]);
|
|
13337
|
+
var LEGACY_SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell", "exec"]);
|
|
13338
|
+
var LEGACY_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
13339
|
+
"write",
|
|
13340
|
+
"edit",
|
|
13341
|
+
"patch",
|
|
13342
|
+
"scaffold",
|
|
13343
|
+
"format",
|
|
13344
|
+
"replace",
|
|
13345
|
+
"design",
|
|
13346
|
+
"install"
|
|
13347
|
+
]);
|
|
13348
|
+
function writesToDisk(input) {
|
|
13349
|
+
const capabilities = input.toolCapabilities;
|
|
13350
|
+
const capabilitiesUndeclared = !capabilities || capabilities.length === 0;
|
|
13351
|
+
const hasWriteCap = capabilities?.some((capability) => WRITE_CAPABILITIES.has(capability)) ?? false;
|
|
13352
|
+
const hasDiskMutatingCap = capabilities?.some((capability) => DISK_MUTATING_CAPABILITIES.has(capability)) ?? false;
|
|
13353
|
+
const hasCommandString = typeof input.toolInput?.["command"] === "string";
|
|
13354
|
+
if (hasCommandString && executesShell(input) && !hasWriteCap && !hasDiskMutatingCap && !(capabilitiesUndeclared && LEGACY_WRITE_TOOLS.has(input.toolName ?? "")))
|
|
13355
|
+
return false;
|
|
13356
|
+
if (!capabilitiesUndeclared) {
|
|
13357
|
+
if (hasWriteCap || hasDiskMutatingCap) return true;
|
|
13358
|
+
if (capabilities.includes("mcp.proxy")) {
|
|
13359
|
+
const segmentedName = (input.toolName ?? "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
13360
|
+
const filesystemWriter = /(?:^|_)filesystem(?:_|$)/.test(segmentedName) && /(?:^|_)(?:add|append|copy|create|delete|edit|mkdir|move|remove|rename|write)(?:_|$)/.test(
|
|
13361
|
+
segmentedName
|
|
13362
|
+
);
|
|
13363
|
+
const toolInput = input.toolInput ?? {};
|
|
13364
|
+
const hasKnownPath = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some(
|
|
13365
|
+
(field) => toolInput[field] !== void 0
|
|
13366
|
+
);
|
|
13367
|
+
if (filesystemWriter && hasKnownPath) return true;
|
|
13368
|
+
}
|
|
13369
|
+
return false;
|
|
13370
|
+
}
|
|
13371
|
+
if (input.toolMutating !== void 0) return input.toolMutating;
|
|
13372
|
+
return LEGACY_WRITE_TOOLS.has(input.toolName ?? "");
|
|
13373
|
+
}
|
|
13374
|
+
function executesShell(input) {
|
|
13375
|
+
const toolName = input.toolName ?? "";
|
|
13376
|
+
if (LEGACY_SHELL_TOOLS.has(toolName)) return true;
|
|
13377
|
+
if (toolName === "git") return false;
|
|
13378
|
+
const segmentedName = toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
13379
|
+
const isCommandStringRunner = /(?:^|[_.-])(?:command|exec|execute|shell|terminal)(?:$|[_.-])/i.test(segmentedName);
|
|
13380
|
+
const hasShellCapability = input.toolCapabilities?.some((capability) => SHELL_CAPABILITIES.has(capability)) ?? false;
|
|
13381
|
+
const hasCommandString = typeof input.toolInput?.["command"] === "string";
|
|
13382
|
+
return isCommandStringRunner && hasShellCapability || hasCommandString && input.toolMutating === true;
|
|
13383
|
+
}
|
|
13384
|
+
var PATH_FIELDS = [
|
|
13385
|
+
"path",
|
|
13386
|
+
"file_path",
|
|
13387
|
+
"filePath",
|
|
13388
|
+
"target",
|
|
13389
|
+
"target_path",
|
|
13390
|
+
"targetPath",
|
|
13391
|
+
"destination",
|
|
13392
|
+
"dest",
|
|
13393
|
+
"output_path",
|
|
13394
|
+
"outputPath",
|
|
13395
|
+
"out",
|
|
13396
|
+
"file"
|
|
13397
|
+
];
|
|
13398
|
+
var PATH_LIST_FIELDS = ["files", "paths"];
|
|
13399
|
+
var SOURCE_PATH_FIELDS = [
|
|
13400
|
+
"source",
|
|
13401
|
+
"from",
|
|
13402
|
+
"src",
|
|
13403
|
+
"input_path",
|
|
13404
|
+
"inputPath",
|
|
13405
|
+
"old_path",
|
|
13406
|
+
"oldPath"
|
|
13407
|
+
];
|
|
13408
|
+
var MOVE_TOOL_NAME = /(?:^|[_.-])(move|rename)(?:$|[_.-])/i;
|
|
13409
|
+
var DIRECTORY_DELETE_TOOL_NAME = /(?:^|[_.-])(?:rmdir|(?:delete|remove)[_.-](?:dir|directory))(?:$|[_.-])/i;
|
|
13410
|
+
function segmentedToolName(toolName) {
|
|
13411
|
+
return toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
13412
|
+
}
|
|
13413
|
+
function isMoveToolName(toolName) {
|
|
13414
|
+
return MOVE_TOOL_NAME.test(segmentedToolName(toolName));
|
|
13415
|
+
}
|
|
13416
|
+
function isDirectoryDeleteToolName(toolName) {
|
|
13417
|
+
return DIRECTORY_DELETE_TOOL_NAME.test(segmentedToolName(toolName));
|
|
13418
|
+
}
|
|
13419
|
+
function cleanPatchPath(raw) {
|
|
13420
|
+
const trimmed = raw.trim();
|
|
13421
|
+
if (!trimmed || trimmed === "/dev/null") return null;
|
|
13422
|
+
const quoted = /^"([^"]+)"(?:\s|$)/.exec(trimmed)?.[1];
|
|
13423
|
+
const withoutTimestamp = quoted ?? trimmed.replace(/(?:\t|\s+)\d{4}-\d{2}-\d{2}(?:\s.*)?$/, "").trim();
|
|
13424
|
+
if (!withoutTimestamp || withoutTimestamp === "/dev/null") return null;
|
|
13425
|
+
return withoutTimestamp.replace(/^[ab]\//, "");
|
|
13426
|
+
}
|
|
13427
|
+
function pathsFromPatch(patch) {
|
|
13428
|
+
const paths = [];
|
|
13429
|
+
const lines = patch.split(/\r?\n/);
|
|
13430
|
+
for (let index = 0; index + 2 < lines.length; index += 1) {
|
|
13431
|
+
const oldHeader = lines[index];
|
|
13432
|
+
const newHeader = lines[index + 1];
|
|
13433
|
+
const hunkHeader = lines[index + 2];
|
|
13434
|
+
if (!oldHeader?.startsWith("--- ") || !newHeader?.startsWith("+++ ")) continue;
|
|
13435
|
+
if (!hunkHeader?.startsWith("@@ ")) continue;
|
|
13436
|
+
const oldPath = cleanPatchPath(oldHeader.slice(4));
|
|
13437
|
+
const newPath = cleanPatchPath(newHeader.slice(4));
|
|
13438
|
+
if (oldPath) paths.push(oldPath);
|
|
13439
|
+
if (newPath) paths.push(newPath);
|
|
13440
|
+
}
|
|
13441
|
+
return paths;
|
|
13442
|
+
}
|
|
13443
|
+
function appendTarget(targets, value, kind, base) {
|
|
13444
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
13445
|
+
targets.push({
|
|
13446
|
+
path: resolveTargetPath(value, base),
|
|
13447
|
+
kind
|
|
13448
|
+
});
|
|
13449
|
+
}
|
|
13450
|
+
function appendTargetList(targets, value, directoryCapable, base) {
|
|
13451
|
+
const appendPath = (path) => {
|
|
13452
|
+
targets.push({
|
|
13453
|
+
path: resolveTargetPath(path, base),
|
|
13454
|
+
kind: isUnresolvedPathScope(path) || isRootPathScope(path) || directoryCapable && isDirectoryAmbiguousPath(path) ? "scope" : "file"
|
|
13455
|
+
});
|
|
13456
|
+
};
|
|
13457
|
+
if (typeof value === "string") {
|
|
13458
|
+
const paths = value.split(",").map((item) => item.trim()).filter((path) => path.length > 0);
|
|
13459
|
+
for (const path of paths) appendPath(path);
|
|
13460
|
+
return;
|
|
13461
|
+
}
|
|
13462
|
+
if (!Array.isArray(value)) return;
|
|
13463
|
+
for (const item of value) {
|
|
13464
|
+
if (typeof item !== "string") continue;
|
|
13465
|
+
const path = item.trim();
|
|
13466
|
+
if (path.length > 0) appendPath(path);
|
|
13467
|
+
}
|
|
13468
|
+
}
|
|
13469
|
+
function isReadOnlyInvocation(toolName, toolInput) {
|
|
13470
|
+
if (toolName === "git") {
|
|
13471
|
+
const command = toolInput["command"];
|
|
13472
|
+
if (command === "status" || command === "log" || command === "diff" || command === "fetch" || command === "commit" || command === "branch" || command === "push") {
|
|
13473
|
+
return true;
|
|
13474
|
+
}
|
|
13475
|
+
return command === "worktree" && toolInput["worktreeAction"] === "list";
|
|
13476
|
+
}
|
|
13477
|
+
if (toolName === "design") {
|
|
13478
|
+
const action = toolInput["action"] ?? "list";
|
|
13479
|
+
return action === "list" || action === "foundations" || action === "verify";
|
|
13480
|
+
}
|
|
13481
|
+
if (toolName === "format") return toolInput["check"] === true;
|
|
13482
|
+
if (toolName === "patch" || toolName === "scaffold") return toolInput["dry_run"] === true;
|
|
13483
|
+
if (toolName === "replace") return toolInput["dry_run"] !== false;
|
|
13484
|
+
return false;
|
|
13485
|
+
}
|
|
13486
|
+
function pathsFromToolInput(toolInput, toolName, invocationCwd) {
|
|
13487
|
+
const targets = [];
|
|
13488
|
+
const effectiveCwd = effectiveToolCwd(toolInput["cwd"], invocationCwd);
|
|
13489
|
+
const directoryDelete = isDirectoryDeleteToolName(toolName);
|
|
13490
|
+
for (const field of PATH_FIELDS) {
|
|
13491
|
+
const value = toolInput[field];
|
|
13492
|
+
appendTarget(
|
|
13493
|
+
targets,
|
|
13494
|
+
value,
|
|
13495
|
+
directoryDelete ? "deletion-scope" : typeof value === "string" && isUnresolvedPathScope(value) ? "scope" : "file",
|
|
13496
|
+
effectiveCwd
|
|
13497
|
+
);
|
|
13498
|
+
}
|
|
13499
|
+
const directoryCapableLists = toolName === "format" || toolName === "replace";
|
|
13500
|
+
for (const field of PATH_LIST_FIELDS) {
|
|
13501
|
+
appendTargetList(
|
|
13502
|
+
targets,
|
|
13503
|
+
toolInput[field],
|
|
13504
|
+
directoryCapableLists && field === "files",
|
|
13505
|
+
effectiveCwd
|
|
13506
|
+
);
|
|
13507
|
+
}
|
|
13508
|
+
if (toolName === "install") {
|
|
13509
|
+
appendTarget(targets, effectiveCwd ?? invocationCwd ?? ".", "scope");
|
|
13510
|
+
}
|
|
13511
|
+
if (toolName === "git") {
|
|
13512
|
+
const command = toolInput["command"];
|
|
13513
|
+
if (command === "worktree" && (toolInput["worktreeAction"] === "add" || toolInput["worktreeAction"] === "remove")) {
|
|
13514
|
+
appendTarget(targets, toolInput["worktreePath"] ?? toolInput["worktree_path"], "file");
|
|
13515
|
+
}
|
|
13516
|
+
}
|
|
13517
|
+
if (toolName === "scaffold") {
|
|
13518
|
+
const cwdValue = toolInput["cwd"];
|
|
13519
|
+
const cwd = typeof cwdValue === "string" && cwdValue.length > 0 ? cwdValue : invocationCwd ?? ".";
|
|
13520
|
+
const base = normalizePath2(cwd).replace(/\/$/, "");
|
|
13521
|
+
const name = typeof toolInput["name"] === "string" ? toolInput["name"] : "";
|
|
13522
|
+
const template = toolInput["template"];
|
|
13523
|
+
const filesByTemplate = {
|
|
13524
|
+
"npm-package": ["package.json", "tsconfig.json", "src/index.ts", "src/index.test.ts"],
|
|
13525
|
+
"cli-tool": ["package.json", "src/index.ts"],
|
|
13526
|
+
"react-component": name ? [`${name}.tsx`, `${name}.test.tsx`] : []
|
|
13527
|
+
};
|
|
13528
|
+
for (const file of typeof template === "string" ? filesByTemplate[template] ?? [] : []) {
|
|
13529
|
+
targets.push({ path: `${base}/${file}`, kind: "file" });
|
|
13530
|
+
}
|
|
13531
|
+
targets.push({ path: base, kind: "scope" });
|
|
13532
|
+
}
|
|
13533
|
+
if (isMoveToolName(toolName)) {
|
|
13534
|
+
for (const field of SOURCE_PATH_FIELDS) {
|
|
13535
|
+
appendTarget(targets, toolInput[field], "deletion-scope", effectiveCwd);
|
|
13536
|
+
}
|
|
13537
|
+
}
|
|
13538
|
+
const patch = toolInput["patch"];
|
|
13539
|
+
if (typeof patch === "string") {
|
|
13540
|
+
const patchTargets = pathsFromPatch(patch);
|
|
13541
|
+
const directory = toolInput["directory"];
|
|
13542
|
+
const baseValue = typeof directory === "string" && directory.length > 0 ? directory : invocationCwd;
|
|
13543
|
+
if (baseValue) {
|
|
13544
|
+
const base = normalizePath2(baseValue).replace(/\/$/, "");
|
|
13545
|
+
targets.push(
|
|
13546
|
+
...patchTargets.map((target) => ({ path: `${base}/${target}`, kind: "file" }))
|
|
13547
|
+
);
|
|
13548
|
+
} else {
|
|
13549
|
+
targets.push(...patchTargets.map((path) => ({ path, kind: "file" })));
|
|
13550
|
+
}
|
|
13551
|
+
}
|
|
13552
|
+
if (targets.length === 0) {
|
|
13553
|
+
const cwd = effectiveCwd ?? ".";
|
|
13554
|
+
targets.push({ path: normalizePath2(cwd).replace(/\/$/, "") || ".", kind: "scope" });
|
|
13555
|
+
}
|
|
13556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13557
|
+
return targets.map((target) => ({
|
|
13558
|
+
...target,
|
|
13559
|
+
path: relativeToInvocationCwd(target.path, invocationCwd)
|
|
13560
|
+
})).filter((target) => {
|
|
13561
|
+
const key = `${target.kind}:${target.path}`;
|
|
13562
|
+
if (seen.has(key)) return false;
|
|
13563
|
+
seen.add(key);
|
|
13564
|
+
return true;
|
|
13565
|
+
});
|
|
13566
|
+
}
|
|
13567
|
+
function operationLabel(toolName) {
|
|
13568
|
+
if (toolName === "write") return "write";
|
|
13569
|
+
if (toolName === "edit") return "edit";
|
|
13570
|
+
return `write via "${toolName}"`;
|
|
13571
|
+
}
|
|
13572
|
+
var VALUE_TAKING_GIT_OPTIONS = /* @__PURE__ */ new Set([
|
|
13573
|
+
"-C",
|
|
13574
|
+
"-c",
|
|
13575
|
+
"--git-dir",
|
|
13576
|
+
"--work-tree",
|
|
13577
|
+
"--namespace",
|
|
13578
|
+
"--super-prefix",
|
|
13579
|
+
"--exec-path",
|
|
13580
|
+
"--config-env",
|
|
13581
|
+
"--attr-source"
|
|
13582
|
+
]);
|
|
13583
|
+
var MAX_LAUNCHER_TOKENS = 256;
|
|
13584
|
+
var MAX_LAUNCHER_LENGTH = 64 * 1024;
|
|
13585
|
+
function boundedShellTokens(raw) {
|
|
13586
|
+
const tokens = [];
|
|
13587
|
+
let token = "";
|
|
13588
|
+
let tokenStart = -1;
|
|
13589
|
+
let quote = null;
|
|
13590
|
+
const limit = Math.min(raw.length, MAX_LAUNCHER_LENGTH);
|
|
13591
|
+
for (let index = 0; index < limit && tokens.length < MAX_LAUNCHER_TOKENS; index += 1) {
|
|
13592
|
+
const char = raw[index] ?? "";
|
|
13593
|
+
if (tokenStart < 0 && !/\s/.test(char)) tokenStart = index;
|
|
13594
|
+
if (char === "\\" && quote !== "'" && index + 1 < limit) {
|
|
13595
|
+
const escaped = raw[index + 1] ?? "";
|
|
13596
|
+
const shellEscaped = quote === '"' ? /[$\x60"\\\r\n]/.test(escaped) : /[\s'"\\;&|()`]/.test(escaped);
|
|
13597
|
+
token += shellEscaped ? escaped : `\\${escaped}`;
|
|
13598
|
+
index += 1;
|
|
13599
|
+
continue;
|
|
13600
|
+
}
|
|
13601
|
+
if (char === "'" || char === '"') {
|
|
13602
|
+
if (quote === char) quote = null;
|
|
13603
|
+
else if (quote === null) quote = char;
|
|
13604
|
+
else token += char;
|
|
13605
|
+
continue;
|
|
13606
|
+
}
|
|
13607
|
+
if (quote === null && /\s/.test(char)) {
|
|
13608
|
+
if (tokenStart >= 0) tokens.push({ value: token, start: tokenStart, end: index });
|
|
13609
|
+
token = "";
|
|
13610
|
+
tokenStart = -1;
|
|
13611
|
+
continue;
|
|
13612
|
+
}
|
|
13613
|
+
token += char;
|
|
13614
|
+
}
|
|
13615
|
+
if (tokenStart >= 0 && tokens.length < MAX_LAUNCHER_TOKENS) {
|
|
13616
|
+
tokens.push({ value: token, start: tokenStart, end: limit });
|
|
13617
|
+
}
|
|
13618
|
+
return tokens;
|
|
13619
|
+
}
|
|
13620
|
+
function shellTokens(raw) {
|
|
13621
|
+
return boundedShellTokens(raw).map((token) => token.value);
|
|
13622
|
+
}
|
|
13623
|
+
function gitInvocationArguments(command) {
|
|
13624
|
+
const argumentsList = [];
|
|
13625
|
+
const quotedIndexes = new Uint8Array(command.length);
|
|
13626
|
+
let activeQuote = null;
|
|
13627
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
13628
|
+
const char = command[index];
|
|
13629
|
+
if (isQuoteBoundary(command, index, activeQuote)) {
|
|
13630
|
+
quotedIndexes[index] = 1;
|
|
13631
|
+
activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
|
|
13632
|
+
} else if (activeQuote !== null) {
|
|
13633
|
+
quotedIndexes[index] = 1;
|
|
13634
|
+
}
|
|
13635
|
+
}
|
|
13636
|
+
const gitStart = /(?:^|[;&|\r\n]\s*|\(\s*|`\s*)git\b/gi;
|
|
13637
|
+
let match = gitStart.exec(command);
|
|
13638
|
+
while (match !== null) {
|
|
13639
|
+
const gitOffset = match[0].toLowerCase().lastIndexOf("git");
|
|
13640
|
+
if (gitOffset < 0 || quotedIndexes[match.index + gitOffset] === 1) {
|
|
13641
|
+
match = gitStart.exec(command);
|
|
13642
|
+
continue;
|
|
13643
|
+
}
|
|
13644
|
+
const start = gitStart.lastIndex;
|
|
13645
|
+
let quote = null;
|
|
13646
|
+
let end = start;
|
|
13647
|
+
for (; end < command.length; end += 1) {
|
|
13648
|
+
const char = command[end] ?? "";
|
|
13649
|
+
if (char === "\\" && quote !== "'") {
|
|
13650
|
+
end += 1;
|
|
13651
|
+
continue;
|
|
13652
|
+
}
|
|
13653
|
+
if (char === "'" || char === '"') {
|
|
13654
|
+
if (quote === char) quote = null;
|
|
13655
|
+
else if (quote === null) quote = char;
|
|
13656
|
+
continue;
|
|
13657
|
+
}
|
|
13658
|
+
if (quote === null && /[;&|)`\r\n]/.test(char)) break;
|
|
13659
|
+
}
|
|
13660
|
+
argumentsList.push(command.slice(start, end));
|
|
13661
|
+
gitStart.lastIndex = Math.max(end, start);
|
|
13662
|
+
match = gitStart.exec(command);
|
|
13663
|
+
}
|
|
13664
|
+
return argumentsList;
|
|
13665
|
+
}
|
|
13666
|
+
function gitSubcommandIndex(tokens) {
|
|
13667
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
13668
|
+
const token = tokens[index] ?? "";
|
|
13669
|
+
if (token === "--") return index + 1 < tokens.length ? index + 1 : -1;
|
|
13670
|
+
if (!token.startsWith("-")) return index;
|
|
13671
|
+
const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
|
|
13672
|
+
const hasAttachedValue = token.includes("=") || token.startsWith("-C") && token !== "-C";
|
|
13673
|
+
if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && !hasAttachedValue) index += 1;
|
|
13674
|
+
}
|
|
13675
|
+
return -1;
|
|
13676
|
+
}
|
|
13677
|
+
function commandDeletesImplicitScope(command) {
|
|
13678
|
+
const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
13679
|
+
for (const rawArguments of gitInvocationArguments(stripped)) {
|
|
13680
|
+
const tokens = shellTokens(rawArguments);
|
|
13681
|
+
const commandIndex = gitSubcommandIndex(tokens);
|
|
13682
|
+
const subcommand = commandIndex >= 0 ? tokens[commandIndex]?.toLowerCase() : void 0;
|
|
13683
|
+
const operands = tokens.slice(commandIndex + 1);
|
|
13684
|
+
if (/^(?:clean|restore|reset|checkout|switch)$/.test(subcommand ?? "")) return true;
|
|
13685
|
+
if (subcommand === "stash") {
|
|
13686
|
+
const action = operands.find((token) => !token.startsWith("-"))?.toLowerCase();
|
|
13687
|
+
if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) return true;
|
|
13688
|
+
}
|
|
13689
|
+
}
|
|
13690
|
+
return false;
|
|
13691
|
+
}
|
|
13692
|
+
function normalizeEnvSplitPayload(payload) {
|
|
13693
|
+
const removeUnbalanced = (value, quote) => {
|
|
13694
|
+
let boundaries = 0;
|
|
13695
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
13696
|
+
if (value[index] === quote && !quoteIsEscaped(value, index)) boundaries += 1;
|
|
13697
|
+
}
|
|
13698
|
+
return boundaries % 2 === 0 ? value : value.replaceAll(quote, "");
|
|
13699
|
+
};
|
|
13700
|
+
return removeUnbalanced(removeUnbalanced(payload, "'"), '"');
|
|
13701
|
+
}
|
|
13702
|
+
function unwrapEnvSplitStringAtBoundary(command) {
|
|
13703
|
+
const pattern = /(^|[;&|\r\n]\s*|\(\s*|`\s*)(?:[^\s;&|(){}]+[\\/])?env\b/gi;
|
|
13704
|
+
const match = pattern.exec(command);
|
|
13705
|
+
if (!match) return command;
|
|
13706
|
+
const boundary = match[1] ?? "";
|
|
13707
|
+
const afterStart = match.index + match[0].length;
|
|
13708
|
+
const after = command.slice(afterStart);
|
|
13709
|
+
const tokens = boundedShellTokens(after);
|
|
13710
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
13711
|
+
const token = tokens[index];
|
|
13712
|
+
if (!token) continue;
|
|
13713
|
+
const separator = firstUnquotedShellSeparator(after, token.start, token.end);
|
|
13714
|
+
const tokenEnd = separator ?? token.end;
|
|
13715
|
+
const tokenValue = boundedShellTokens(after.slice(token.start, tokenEnd))[0]?.value ?? "";
|
|
13716
|
+
const attachedShort = /^-[i0v]*S(.+)$/.exec(tokenValue)?.[1];
|
|
13717
|
+
const attachedLong = tokenValue.startsWith("--split-string=") ? tokenValue.slice("--split-string=".length) : void 0;
|
|
13718
|
+
const attachedPayload = attachedShort ?? attachedLong;
|
|
13719
|
+
if (attachedPayload !== void 0) {
|
|
13720
|
+
return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(attachedPayload)}${after.slice(tokenEnd)}`;
|
|
13721
|
+
}
|
|
13722
|
+
if (!/^-[i0v]*S$/.test(tokenValue) && tokenValue !== "--split-string") {
|
|
13723
|
+
if (separator !== void 0) return command;
|
|
13724
|
+
continue;
|
|
13725
|
+
}
|
|
13726
|
+
const payload = tokens[index + 1];
|
|
13727
|
+
if (!payload) return command;
|
|
13728
|
+
const payloadSeparator = firstUnquotedShellSeparator(after, payload.start, payload.end);
|
|
13729
|
+
const payloadEnd = payloadSeparator ?? payload.end;
|
|
13730
|
+
const payloadValue = boundedShellTokens(after.slice(payload.start, payloadEnd))[0]?.value ?? "";
|
|
13731
|
+
return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(payloadValue)}${after.slice(payloadEnd)}`;
|
|
13732
|
+
}
|
|
13733
|
+
return command;
|
|
13734
|
+
}
|
|
13735
|
+
function stripTransparentLaunchers(command) {
|
|
13736
|
+
let stripped = command;
|
|
13737
|
+
let previous;
|
|
13738
|
+
do {
|
|
13739
|
+
previous = stripped;
|
|
13740
|
+
let beforeUnwrap;
|
|
13741
|
+
do {
|
|
13742
|
+
beforeUnwrap = stripped;
|
|
13743
|
+
stripped = unwrapEnvSplitStringAtBoundary(stripped);
|
|
13744
|
+
} while (stripped !== beforeUnwrap);
|
|
13745
|
+
stripped = stripLauncherAtBoundary(stripped, "env", ENV_VALUE_TAKING);
|
|
13746
|
+
stripped = stripLauncherAtBoundary(stripped, "sudo", SUDO_VALUE_TAKING);
|
|
13747
|
+
} while (stripped !== previous);
|
|
13748
|
+
return stripped;
|
|
13749
|
+
}
|
|
13750
|
+
var SUDO_VALUE_TAKING = /* @__PURE__ */ new Set([
|
|
13751
|
+
"-u",
|
|
13752
|
+
"-g",
|
|
13753
|
+
"-h",
|
|
13754
|
+
"-p",
|
|
13755
|
+
"-C",
|
|
13756
|
+
"-R",
|
|
13757
|
+
"-D",
|
|
13758
|
+
"-r",
|
|
13759
|
+
"-t",
|
|
13760
|
+
"--user",
|
|
13761
|
+
"--group",
|
|
13762
|
+
"--host",
|
|
13763
|
+
"--prompt",
|
|
13764
|
+
"--close-from",
|
|
13765
|
+
"--chroot",
|
|
13766
|
+
"--chdir",
|
|
13767
|
+
"--role",
|
|
13768
|
+
"--type",
|
|
13769
|
+
"--command-timeout"
|
|
13770
|
+
]);
|
|
13771
|
+
var ENV_VALUE_TAKING = /* @__PURE__ */ new Set([
|
|
13772
|
+
"-u",
|
|
13773
|
+
"-C",
|
|
13774
|
+
"-P",
|
|
13775
|
+
"-a",
|
|
13776
|
+
"--argv0",
|
|
13777
|
+
"--unset",
|
|
13778
|
+
"--chdir",
|
|
13779
|
+
"--split-string"
|
|
13780
|
+
]);
|
|
13781
|
+
var ENV_FLAG_OPTIONS = /* @__PURE__ */ new Set([
|
|
13782
|
+
"-i",
|
|
13783
|
+
"-0",
|
|
13784
|
+
"-v",
|
|
13785
|
+
"--ignore-environment",
|
|
13786
|
+
"--null",
|
|
13787
|
+
"--debug",
|
|
13788
|
+
"--help",
|
|
13789
|
+
"--version"
|
|
13790
|
+
]);
|
|
13791
|
+
function firstUnquotedShellSeparator(raw, start, end) {
|
|
13792
|
+
let quote = null;
|
|
13793
|
+
for (let index = start; index < end; index += 1) {
|
|
13794
|
+
const char = raw[index];
|
|
13795
|
+
if (isQuoteBoundary(raw, index, quote)) {
|
|
13796
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
13797
|
+
continue;
|
|
13798
|
+
}
|
|
13799
|
+
if (quote === null && !quoteIsEscaped(raw, index) && /[;&|]/.test(char ?? "")) return index;
|
|
13800
|
+
}
|
|
13801
|
+
return void 0;
|
|
13802
|
+
}
|
|
13803
|
+
function launcherPrefixLength(after, valueTaking, flagOptions) {
|
|
13804
|
+
const tokens = boundedShellTokens(after);
|
|
13805
|
+
let consumed = 0;
|
|
13806
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
13807
|
+
const tok = tokens[i] ?? { value: "", start: 0, end: 0 };
|
|
13808
|
+
const token = tok.value;
|
|
13809
|
+
const separator = firstUnquotedShellSeparator(after, tok.start, tok.end);
|
|
13810
|
+
if (separator !== void 0) return consumed || separator;
|
|
13811
|
+
if (token === "--") return tok.end;
|
|
13812
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
|
|
13813
|
+
consumed = tok.end;
|
|
13814
|
+
continue;
|
|
13815
|
+
}
|
|
13816
|
+
if (token === "-" || !token.startsWith("-")) break;
|
|
13817
|
+
const optionName = token.split("=", 1)[0] ?? token;
|
|
13818
|
+
const combinedValueOption = [...valueTaking].find(
|
|
13819
|
+
(option) => option.length === 2 && token.startsWith(option) && token !== option
|
|
13820
|
+
);
|
|
13821
|
+
const combinedShortValueOption = token.startsWith("-") && !token.startsWith("--") ? [...valueTaking].find(
|
|
13822
|
+
(option) => option.length === 2 && token.includes(option[1] ?? "", 2)
|
|
13823
|
+
) : void 0;
|
|
13824
|
+
const hasAttachedValue = token.includes("=") || combinedValueOption !== void 0;
|
|
13825
|
+
if (!hasAttachedValue && (valueTaking.has(optionName) || combinedShortValueOption !== void 0)) {
|
|
13826
|
+
if (i + 1 >= tokens.length) return flagOptions ? void 0 : tok.end;
|
|
13827
|
+
const valueToken = tokens[i + 1] ?? tok;
|
|
13828
|
+
const valueSeparator = firstUnquotedShellSeparator(after, valueToken.start, valueToken.end);
|
|
13829
|
+
if (valueSeparator !== void 0) return consumed || valueSeparator;
|
|
13830
|
+
consumed = valueToken.end;
|
|
13831
|
+
i += 1;
|
|
13832
|
+
} else {
|
|
13833
|
+
const knownFlag = flagOptions?.has(optionName) || flagOptions !== void 0 && /^-[i0v]+$/.test(token);
|
|
13834
|
+
const knownAttachedValue = hasAttachedValue && (valueTaking.has(optionName) || combinedValueOption !== void 0);
|
|
13835
|
+
if (flagOptions && !knownFlag && !knownAttachedValue) return void 0;
|
|
13836
|
+
consumed = tok.end;
|
|
13837
|
+
}
|
|
13838
|
+
}
|
|
13839
|
+
return consumed;
|
|
13840
|
+
}
|
|
13841
|
+
function stripLauncherAtBoundary(command, launcher, valueTaking) {
|
|
13842
|
+
const pattern = new RegExp(
|
|
13843
|
+
`(^|[;&|\\r\\n]\\s*|\\(\\s*|\`\\s*)(?:[^\\s;&|(){}]+[\\\\/])?${launcher}\\b`,
|
|
13844
|
+
"i"
|
|
13845
|
+
);
|
|
13846
|
+
const match = pattern.exec(command);
|
|
13847
|
+
if (!match) return command;
|
|
13848
|
+
const boundary = match[1] ?? "";
|
|
13849
|
+
const afterStart = match.index + match[0].length;
|
|
13850
|
+
const after = command.slice(afterStart);
|
|
13851
|
+
const consumed = launcherPrefixLength(
|
|
13852
|
+
after,
|
|
13853
|
+
valueTaking,
|
|
13854
|
+
launcher === "env" ? ENV_FLAG_OPTIONS : void 0
|
|
13855
|
+
);
|
|
13856
|
+
if (consumed === void 0) {
|
|
13857
|
+
return `${command.slice(0, match.index)}${boundary}rm -rf **`;
|
|
13858
|
+
}
|
|
13859
|
+
return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
|
|
13860
|
+
}
|
|
13861
|
+
function commandRecursivelyDeletes(command) {
|
|
13862
|
+
const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
13863
|
+
if (/(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b[^;&|)`]*(?:-delete\b|-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)/i.test(
|
|
13864
|
+
stripped
|
|
13865
|
+
)) {
|
|
13866
|
+
return true;
|
|
13867
|
+
}
|
|
13868
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
13869
|
+
let match = destructive.exec(stripped);
|
|
13870
|
+
while (match !== null) {
|
|
13871
|
+
const tool = match[1]?.toLowerCase();
|
|
13872
|
+
if (tool === "rmdir") return true;
|
|
13873
|
+
const tokens = (match[2]?.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? []).map(
|
|
13874
|
+
(arg) => arg.replace(/^["']|["']$/g, "")
|
|
13875
|
+
);
|
|
13876
|
+
let recursive = false;
|
|
13877
|
+
for (const token of tokens) {
|
|
13878
|
+
if (token === "--") break;
|
|
13879
|
+
if (tool === "rm") {
|
|
13880
|
+
if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
|
|
13881
|
+
} else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
|
|
13882
|
+
recursive = true;
|
|
13883
|
+
}
|
|
13884
|
+
}
|
|
13885
|
+
if (recursive) return true;
|
|
13886
|
+
match = destructive.exec(stripped);
|
|
13887
|
+
}
|
|
13888
|
+
return false;
|
|
13889
|
+
}
|
|
13890
|
+
function quoteIsEscaped(command, index) {
|
|
13891
|
+
let backslashes = 0;
|
|
13892
|
+
for (let cursor = index - 1; cursor >= 0 && command[cursor] === "\\"; cursor -= 1) {
|
|
13893
|
+
backslashes += 1;
|
|
13894
|
+
}
|
|
13895
|
+
return backslashes % 2 === 1;
|
|
13896
|
+
}
|
|
13897
|
+
function isQuoteBoundary(command, index, activeQuote) {
|
|
13898
|
+
const char = command[index];
|
|
13899
|
+
if (char !== "'" && char !== '"') return false;
|
|
13900
|
+
if (activeQuote === "'") return char === "'";
|
|
13901
|
+
if (activeQuote !== null && char !== activeQuote) return false;
|
|
13902
|
+
return !quoteIsEscaped(command, index);
|
|
13903
|
+
}
|
|
13904
|
+
function executableCommandSubstitutions(command) {
|
|
13905
|
+
const bodies = [];
|
|
13906
|
+
let outerQuote = null;
|
|
13907
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
13908
|
+
const char = command[index];
|
|
13909
|
+
if (isQuoteBoundary(command, index, outerQuote)) {
|
|
13910
|
+
outerQuote = outerQuote === char ? null : char === "'" ? "'" : '"';
|
|
13911
|
+
continue;
|
|
13912
|
+
}
|
|
13913
|
+
if (outerQuote === "'" || quoteIsEscaped(command, index)) continue;
|
|
13914
|
+
if (char === "`") {
|
|
13915
|
+
let end2 = index + 1;
|
|
13916
|
+
while (end2 < command.length && (command[end2] !== "`" || quoteIsEscaped(command, end2))) {
|
|
13917
|
+
end2 += 1;
|
|
13918
|
+
}
|
|
13919
|
+
if (end2 < command.length) {
|
|
13920
|
+
bodies.push(command.slice(index + 1, end2));
|
|
13921
|
+
index = end2;
|
|
13922
|
+
}
|
|
13923
|
+
continue;
|
|
13924
|
+
}
|
|
13925
|
+
const commandSubstitution = char === "$" && command[index + 1] === "(";
|
|
13926
|
+
const processSubstitution = (char === ">" || char === "<") && command[index + 1] === "(";
|
|
13927
|
+
if (!commandSubstitution && !processSubstitution || // Arithmetic expansion evaluates an expression; it does not execute its
|
|
13928
|
+
// contents as a shell command substitution.
|
|
13929
|
+
commandSubstitution && command[index + 2] === "(")
|
|
13930
|
+
continue;
|
|
13931
|
+
let depth = 1;
|
|
13932
|
+
let innerQuote = null;
|
|
13933
|
+
let end = index + 2;
|
|
13934
|
+
for (; end < command.length; end += 1) {
|
|
13935
|
+
const innerChar = command[end];
|
|
13936
|
+
if (isQuoteBoundary(command, end, innerQuote)) {
|
|
13937
|
+
innerQuote = innerQuote === innerChar ? null : innerChar === "'" ? "'" : '"';
|
|
13938
|
+
continue;
|
|
13939
|
+
}
|
|
13940
|
+
if (innerQuote !== null) continue;
|
|
13941
|
+
if ((innerChar === "(" || innerChar === ")") && quoteIsEscaped(command, end)) continue;
|
|
13942
|
+
if (innerChar === "(") depth += 1;
|
|
13943
|
+
else if (innerChar === ")") {
|
|
13944
|
+
depth -= 1;
|
|
13945
|
+
if (depth === 0) break;
|
|
13946
|
+
}
|
|
13947
|
+
}
|
|
13948
|
+
if (depth === 0) {
|
|
13949
|
+
bodies.push(command.slice(index + 2, end));
|
|
13950
|
+
index = end;
|
|
13951
|
+
} else {
|
|
13952
|
+
break;
|
|
13953
|
+
}
|
|
13954
|
+
}
|
|
13955
|
+
return bodies;
|
|
13956
|
+
}
|
|
13957
|
+
var MAX_DESTRUCTIVE_TARGET_DEPTH = 64;
|
|
13958
|
+
function heredocDelimiterOnLine(line) {
|
|
13959
|
+
let quote = null;
|
|
13960
|
+
for (let index = 0; index < line.length - 1; index += 1) {
|
|
13961
|
+
const char = line[index];
|
|
13962
|
+
if (isQuoteBoundary(line, index, quote)) {
|
|
13963
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
13964
|
+
continue;
|
|
13965
|
+
}
|
|
13966
|
+
if (quote !== null || quoteIsEscaped(line, index) || char !== "<" || line[index - 1] === "<" || line[index + 1] !== "<" || line[index + 2] === "<")
|
|
13967
|
+
continue;
|
|
13968
|
+
let cursor = index + 2;
|
|
13969
|
+
const stripTabs = line[cursor] === "-";
|
|
13970
|
+
if (stripTabs) cursor += 1;
|
|
13971
|
+
while (line[cursor] === " " || line[cursor] === " ") cursor += 1;
|
|
13972
|
+
let delimiter2 = "";
|
|
13973
|
+
let delimiterQuote = null;
|
|
13974
|
+
let quoted = false;
|
|
13975
|
+
for (; cursor < line.length; cursor += 1) {
|
|
13976
|
+
const delimiterChar = line[cursor] ?? "";
|
|
13977
|
+
if (delimiterQuote !== null) {
|
|
13978
|
+
if (delimiterChar === delimiterQuote && !quoteIsEscaped(line, cursor)) {
|
|
13979
|
+
delimiterQuote = null;
|
|
13980
|
+
quoted = true;
|
|
13981
|
+
} else if (delimiterChar === "\\" && delimiterQuote === '"' && cursor + 1 < line.length) {
|
|
13982
|
+
quoted = true;
|
|
13983
|
+
cursor += 1;
|
|
13984
|
+
delimiter2 += line[cursor] ?? "";
|
|
13985
|
+
} else {
|
|
13986
|
+
delimiter2 += delimiterChar;
|
|
13987
|
+
}
|
|
13988
|
+
continue;
|
|
13989
|
+
}
|
|
13990
|
+
if (delimiterChar === "'" || delimiterChar === '"') {
|
|
13991
|
+
delimiterQuote = delimiterChar;
|
|
13992
|
+
quoted = true;
|
|
13993
|
+
continue;
|
|
13994
|
+
}
|
|
13995
|
+
if (delimiterChar === "\\" && cursor + 1 < line.length) {
|
|
13996
|
+
quoted = true;
|
|
13997
|
+
cursor += 1;
|
|
13998
|
+
delimiter2 += line[cursor] ?? "";
|
|
13999
|
+
continue;
|
|
14000
|
+
}
|
|
14001
|
+
if (/\s|[;&|<>]/.test(delimiterChar)) break;
|
|
14002
|
+
delimiter2 += delimiterChar;
|
|
14003
|
+
}
|
|
14004
|
+
return delimiter2.length > 0 ? { delimiter: delimiter2, start: index, end: cursor, quoted, stripTabs } : null;
|
|
14005
|
+
}
|
|
14006
|
+
return null;
|
|
14007
|
+
}
|
|
14008
|
+
function commandSegmentBeforeHeredoc(prefix) {
|
|
14009
|
+
let segmentStart = 0;
|
|
14010
|
+
let quote = null;
|
|
14011
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
14012
|
+
const char = prefix[index];
|
|
14013
|
+
if (isQuoteBoundary(prefix, index, quote)) {
|
|
14014
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
14015
|
+
continue;
|
|
14016
|
+
}
|
|
14017
|
+
if (quote === null && !quoteIsEscaped(prefix, index) && /[;&|\r\n]/.test(char ?? "")) {
|
|
14018
|
+
segmentStart = index + 1;
|
|
14019
|
+
}
|
|
14020
|
+
}
|
|
14021
|
+
return prefix.slice(segmentStart).trim();
|
|
14022
|
+
}
|
|
14023
|
+
function maskNonExecutingHeredocBodies(command) {
|
|
14024
|
+
const lines = command.split(/(?<=\n)/);
|
|
14025
|
+
let heredoc = null;
|
|
14026
|
+
const maskBody = (start, end, quoted) => {
|
|
14027
|
+
const body = lines.slice(start, end).join("");
|
|
14028
|
+
const substitutions = quoted ? [] : executableCommandSubstitutions(body);
|
|
14029
|
+
for (let index = start; index < end; index += 1) {
|
|
14030
|
+
const line = lines[index] ?? "";
|
|
14031
|
+
lines[index] = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
14032
|
+
}
|
|
14033
|
+
if (substitutions.length > 0 && start < end)
|
|
14034
|
+
lines[start] = `${substitutions.join(";")}${lines[start] ?? ""}`;
|
|
14035
|
+
};
|
|
14036
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
14037
|
+
const line = lines[index] ?? "";
|
|
14038
|
+
if (heredoc !== null) {
|
|
14039
|
+
const content = line.replace(/\r?\n$/, "");
|
|
14040
|
+
const terminator = heredoc.stripTabs ? content.replace(/^\t+/, "") : content;
|
|
14041
|
+
if (terminator === heredoc.delimiter) {
|
|
14042
|
+
maskBody(heredoc.bodyStart, index, heredoc.quoted);
|
|
14043
|
+
heredoc = null;
|
|
14044
|
+
}
|
|
14045
|
+
continue;
|
|
14046
|
+
}
|
|
14047
|
+
const marker = heredocDelimiterOnLine(line);
|
|
14048
|
+
if (!marker) continue;
|
|
14049
|
+
const prefix = line.slice(0, marker.start).trim();
|
|
14050
|
+
const suffix = line.slice(marker.end).trim();
|
|
14051
|
+
const owner = stripTransparentLaunchers(commandSegmentBeforeHeredoc(prefix));
|
|
14052
|
+
const commandBeforeMarker = `${lines.slice(0, index).join("")}${prefix}`;
|
|
14053
|
+
const whileReadLoopOwnsHeredoc = /^done$/i.test(owner) && /(?:^|[;&|\r\n])\s*while\s+read(?:\s|$)[\s\S]*\bdone\s*$/i.test(commandBeforeMarker);
|
|
14054
|
+
const receivesDataWithoutExecuting = /^(?:[^\s;&|]+[\\/])?(?:cat|tee)(?:\s|$)/i.test(owner) || /^(?:while\s+)?read(?:\s|$)/i.test(owner) || whileReadLoopOwnsHeredoc;
|
|
14055
|
+
const redirectedFileMatch = /^(?:>>|>\||>)\s*("[^"]+"|'[^']+'|[^\s;&|<>]+)/.exec(suffix);
|
|
14056
|
+
const redirectedFile = redirectedFileMatch?.[1]?.replace(/^['"]|['"]$/g, "");
|
|
14057
|
+
const remainingCommand = lines.slice(index + 1).join("");
|
|
14058
|
+
const executionSearch = `${suffix}
|
|
14059
|
+
${remainingCommand}`;
|
|
14060
|
+
const normalizedRedirectedFile = redirectedFile ? normalizePath2(redirectedFile) : void 0;
|
|
14061
|
+
const executesRedirectedFile = normalizedRedirectedFile !== void 0 && boundedShellTokens(executionSearch).some((token) => {
|
|
14062
|
+
if (normalizePath2(token.value) !== normalizedRedirectedFile) return false;
|
|
14063
|
+
const segmentStart = Math.max(
|
|
14064
|
+
executionSearch.lastIndexOf(";", token.start - 1),
|
|
14065
|
+
executionSearch.lastIndexOf("&", token.start - 1),
|
|
14066
|
+
executionSearch.lastIndexOf("|", token.start - 1),
|
|
14067
|
+
executionSearch.lastIndexOf("\n", token.start - 1),
|
|
14068
|
+
executionSearch.lastIndexOf("\r", token.start - 1)
|
|
14069
|
+
);
|
|
14070
|
+
const segmentTokens = shellTokens(executionSearch.slice(segmentStart + 1, token.start));
|
|
14071
|
+
const previous = segmentTokens.at(-1)?.toLowerCase();
|
|
14072
|
+
return previous === void 0 || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(previous);
|
|
14073
|
+
});
|
|
14074
|
+
const processSubstitution = /^(?:>>|>\||>)\s*>\s*\(\s*([^)]*)/.exec(suffix);
|
|
14075
|
+
const processCommand = boundedShellTokens(
|
|
14076
|
+
stripTransparentLaunchers(processSubstitution?.[1]?.trim() ?? "")
|
|
14077
|
+
)[0]?.value.replace(/^.*[\\/]/, "").toLowerCase();
|
|
14078
|
+
const executesBody = /^(?:\||;|&|\(|\{)/.test(suffix) || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(processCommand ?? "") || executesRedirectedFile;
|
|
14079
|
+
if (!receivesDataWithoutExecuting || executesBody) continue;
|
|
14080
|
+
heredoc = { ...marker, bodyStart: index + 1 };
|
|
14081
|
+
}
|
|
14082
|
+
if (heredoc !== null) maskBody(heredoc.bodyStart, lines.length, heredoc.quoted);
|
|
14083
|
+
return lines.join("");
|
|
14084
|
+
}
|
|
13194
14085
|
function destructiveTargets(command) {
|
|
14086
|
+
return destructiveTargetsAtDepth(command, 0);
|
|
14087
|
+
}
|
|
14088
|
+
function destructiveTargetsAtDepth(command, depth) {
|
|
14089
|
+
if (depth >= MAX_DESTRUCTIVE_TARGET_DEPTH) return ["**"];
|
|
14090
|
+
const normalizedCommand = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
13195
14091
|
const targets = [];
|
|
13196
|
-
const
|
|
13197
|
-
let
|
|
14092
|
+
const quotedIndexes = new Uint8Array(normalizedCommand.length);
|
|
14093
|
+
let activeQuote = null;
|
|
14094
|
+
for (let index = 0; index < normalizedCommand.length; index += 1) {
|
|
14095
|
+
const char = normalizedCommand[index];
|
|
14096
|
+
if (isQuoteBoundary(normalizedCommand, index, activeQuote)) {
|
|
14097
|
+
quotedIndexes[index] = 1;
|
|
14098
|
+
activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
|
|
14099
|
+
} else if (activeQuote !== null) {
|
|
14100
|
+
quotedIndexes[index] = 1;
|
|
14101
|
+
}
|
|
14102
|
+
}
|
|
14103
|
+
const tokenIsQuoted = (match, token) => {
|
|
14104
|
+
const offset = match[0].toLowerCase().indexOf(token.toLowerCase());
|
|
14105
|
+
return offset >= 0 && quotedIndexes[match.index + offset] === 1;
|
|
14106
|
+
};
|
|
14107
|
+
const shellOperandTokens = (raw) => {
|
|
14108
|
+
const tokens = shellTokens(raw);
|
|
14109
|
+
const redirectIndex = tokens.findIndex((token) => /^(?:\d*(?:<>|>>?|<)|&>>?)/.test(token));
|
|
14110
|
+
return redirectIndex === -1 ? tokens : tokens.slice(0, redirectIndex);
|
|
14111
|
+
};
|
|
14112
|
+
const shellArgs = (raw) => {
|
|
14113
|
+
const lastNonWhitespace = raw.search(/\s*$/) - 1;
|
|
14114
|
+
let quote2 = null;
|
|
14115
|
+
for (let index = 0; index < lastNonWhitespace; index += 1) {
|
|
14116
|
+
if (isQuoteBoundary(raw, index, quote2)) {
|
|
14117
|
+
const char = raw[index];
|
|
14118
|
+
quote2 = quote2 === char ? null : char === "'" ? "'" : '"';
|
|
14119
|
+
}
|
|
14120
|
+
}
|
|
14121
|
+
const args = raw[lastNonWhitespace] === ")" && quote2 === null && !quoteIsEscaped(raw, lastNonWhitespace) ? raw.slice(0, lastNonWhitespace) : raw;
|
|
14122
|
+
return shellOperandTokens(args).filter((arg) => arg.length > 0 && !arg.startsWith("-"));
|
|
14123
|
+
};
|
|
14124
|
+
for (const body of executableCommandSubstitutions(normalizedCommand)) {
|
|
14125
|
+
let executableBody = body.trim();
|
|
14126
|
+
while (executableBody.startsWith("(") && executableBody.endsWith(")")) {
|
|
14127
|
+
executableBody = executableBody.slice(1, -1).trim();
|
|
14128
|
+
}
|
|
14129
|
+
targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
|
|
14130
|
+
}
|
|
14131
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
14132
|
+
let m = destructive.exec(normalizedCommand);
|
|
13198
14133
|
while (m !== null) {
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
|
|
13202
|
-
|
|
13203
|
-
|
|
13204
|
-
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
|
|
14134
|
+
if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
|
|
14135
|
+
m = destructive.exec(normalizedCommand);
|
|
14136
|
+
}
|
|
14137
|
+
const copy = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(cp|install)\s+([^;&|\r\n]+)/gi;
|
|
14138
|
+
let c = copy.exec(normalizedCommand);
|
|
14139
|
+
while (c !== null) {
|
|
14140
|
+
if (tokenIsQuoted(c, c[1] ?? "")) {
|
|
14141
|
+
c = copy.exec(normalizedCommand);
|
|
14142
|
+
continue;
|
|
14143
|
+
}
|
|
14144
|
+
const tokens = shellTokens(c[2] ?? "");
|
|
14145
|
+
let destination;
|
|
14146
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
14147
|
+
const token = tokens[index];
|
|
14148
|
+
if (token === "-t" || token === "--target-directory") {
|
|
14149
|
+
destination = tokens[index + 1];
|
|
14150
|
+
break;
|
|
14151
|
+
}
|
|
14152
|
+
if (token?.startsWith("--target-directory=")) {
|
|
14153
|
+
destination = token.slice("--target-directory=".length);
|
|
14154
|
+
break;
|
|
14155
|
+
}
|
|
14156
|
+
}
|
|
14157
|
+
destination ??= tokens.filter((arg) => arg.length > 0 && !arg.startsWith("-")).at(-1);
|
|
14158
|
+
if (destination) targets.push(destination);
|
|
14159
|
+
c = copy.exec(normalizedCommand);
|
|
14160
|
+
}
|
|
14161
|
+
const tee = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)/gi;
|
|
14162
|
+
let t = tee.exec(normalizedCommand);
|
|
14163
|
+
while (t !== null) {
|
|
14164
|
+
if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
|
|
14165
|
+
t = tee.exec(normalizedCommand);
|
|
14166
|
+
}
|
|
14167
|
+
const dd = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)/gi;
|
|
14168
|
+
let d = dd.exec(normalizedCommand);
|
|
14169
|
+
while (d !== null) {
|
|
14170
|
+
if (!tokenIsQuoted(d, d[1] ?? "")) {
|
|
14171
|
+
const outputMatch = /(?:^|\s)of=("[^"]*"|'[^']*'|[^\s]+)/i.exec(d[2] ?? "");
|
|
14172
|
+
const output = outputMatch?.[1]?.replace(/^['"]|['"]$/g, "");
|
|
14173
|
+
if (output) targets.push(output);
|
|
14174
|
+
}
|
|
14175
|
+
d = dd.exec(normalizedCommand);
|
|
14176
|
+
}
|
|
14177
|
+
const overwrite = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(sed|ln)\s+([^;&|\r\n]+)/gi;
|
|
14178
|
+
let o = overwrite.exec(normalizedCommand);
|
|
14179
|
+
while (o !== null) {
|
|
14180
|
+
const rawArgs = o[2] ?? "";
|
|
14181
|
+
const tool = o[1]?.toLowerCase();
|
|
14182
|
+
if (tool && tokenIsQuoted(o, tool)) {
|
|
14183
|
+
o = overwrite.exec(normalizedCommand);
|
|
14184
|
+
continue;
|
|
14185
|
+
}
|
|
14186
|
+
if (tool === "sed" && /(?:^|\s)-i(?:[^\s]*)?(?:\s|$)/.test(rawArgs)) {
|
|
14187
|
+
const args = shellArgs(rawArgs);
|
|
14188
|
+
targets.push(...args.slice(1));
|
|
14189
|
+
} else if (tool === "ln" && /(?:^|\s)-[^\s]*f[^\s]*(?:\s|$)/.test(rawArgs)) {
|
|
14190
|
+
const destination = shellArgs(rawArgs).at(-1);
|
|
14191
|
+
if (destination) targets.push(destination);
|
|
14192
|
+
}
|
|
14193
|
+
o = overwrite.exec(normalizedCommand);
|
|
14194
|
+
}
|
|
14195
|
+
const xargsPipeline = /\b(?:echo|printf)\s+([^|]+)\|\s*xargs(?:\s+-[^\s]+)*\s+(?:sudo\s+)?(?:rm|rmdir|del|unlink|truncate|shred)\b/gi;
|
|
14196
|
+
let x = xargsPipeline.exec(normalizedCommand);
|
|
14197
|
+
while (x !== null) {
|
|
14198
|
+
if (!tokenIsQuoted(x, "xargs")) targets.push(...shellArgs(x[1] ?? ""));
|
|
14199
|
+
x = xargsPipeline.exec(normalizedCommand);
|
|
14200
|
+
}
|
|
14201
|
+
for (const rawArguments of gitInvocationArguments(normalizedCommand)) {
|
|
14202
|
+
const invocationTokens = shellOperandTokens(rawArguments);
|
|
14203
|
+
const commandIndex = gitSubcommandIndex(invocationTokens);
|
|
14204
|
+
if (commandIndex >= 0) {
|
|
14205
|
+
let gitCwd = "";
|
|
14206
|
+
let workTree;
|
|
14207
|
+
for (let index = 0; index < commandIndex; index += 1) {
|
|
14208
|
+
const token = invocationTokens[index] ?? "";
|
|
14209
|
+
if (token === "--") continue;
|
|
14210
|
+
const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
|
|
14211
|
+
let optionValue = token.includes("=") ? token.slice(token.indexOf("=") + 1) : void 0;
|
|
14212
|
+
if (token.startsWith("-C") && token !== "-C") optionValue = token.slice(2);
|
|
14213
|
+
if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && optionValue === void 0) {
|
|
14214
|
+
optionValue = invocationTokens[index + 1];
|
|
14215
|
+
index += 1;
|
|
14216
|
+
}
|
|
14217
|
+
if (optionName === "-C" && optionValue)
|
|
14218
|
+
gitCwd = resolveTargetPath(optionValue, gitCwd || void 0);
|
|
14219
|
+
if (optionName === "--work-tree" && optionValue) workTree = optionValue;
|
|
14220
|
+
}
|
|
14221
|
+
const subcommand = invocationTokens[commandIndex]?.toLowerCase();
|
|
14222
|
+
const tokens = invocationTokens.slice(commandIndex + 1);
|
|
14223
|
+
const gitTreeRoot = workTree ? resolveTargetPath(workTree, gitCwd || void 0) : gitCwd;
|
|
14224
|
+
const gitTarget = (target) => {
|
|
14225
|
+
const resolved = resolveTargetPath(target, gitTreeRoot || void 0);
|
|
14226
|
+
return target.endsWith("/") && !resolved.endsWith("/") ? `${resolved}/` : resolved;
|
|
14227
|
+
};
|
|
14228
|
+
const gitPathspecTargets = (pathspec) => {
|
|
14229
|
+
if (isUnresolvedPathScope(pathspec) || !isDirectoryAmbiguousPath(pathspec)) {
|
|
14230
|
+
return [gitTarget(pathspec)];
|
|
14231
|
+
}
|
|
14232
|
+
return [gitTarget(`${pathspec.replace(/\/$/, "")}/**`)];
|
|
14233
|
+
};
|
|
14234
|
+
const fileSourcedPathspecScope = (pathspecTokens) => {
|
|
14235
|
+
const usesPathspecFile = pathspecTokens.some(
|
|
14236
|
+
(token, index) => token.startsWith("--pathspec-from-file=") || token === "--pathspec-from-file" && pathspecTokens[index + 1] !== void 0
|
|
14237
|
+
);
|
|
14238
|
+
return usesPathspecFile ? gitTarget("**") : void 0;
|
|
14239
|
+
};
|
|
14240
|
+
if (subcommand === "clean") {
|
|
14241
|
+
const dryRun = tokens.some((t2) => t2 === "--dry-run" || /^-[^-]*n/.test(t2));
|
|
14242
|
+
if (!dryRun) {
|
|
14243
|
+
const operands = [];
|
|
14244
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
14245
|
+
const t2 = tokens[i] ?? "";
|
|
14246
|
+
if (t2 === "-e" || t2 === "--exclude" || t2 === "--exclude-from") {
|
|
14247
|
+
i += 1;
|
|
14248
|
+
continue;
|
|
14249
|
+
}
|
|
14250
|
+
if (t2.startsWith("--exclude=") || t2.startsWith("--exclude-from=") || /^-e.+/.test(t2) || t2.startsWith("-"))
|
|
14251
|
+
continue;
|
|
14252
|
+
operands.push(t2);
|
|
14253
|
+
}
|
|
14254
|
+
targets.push(
|
|
14255
|
+
...(operands.length ? operands : ["."]).map(
|
|
14256
|
+
(operand) => gitTarget(operand.endsWith("/") ? `${operand}**` : operand)
|
|
14257
|
+
)
|
|
14258
|
+
);
|
|
14259
|
+
}
|
|
14260
|
+
} else if (subcommand === "rm") {
|
|
14261
|
+
const writes = !tokens.includes("--cached") || tokens.includes("--worktree");
|
|
14262
|
+
if (writes) {
|
|
14263
|
+
const recursive = tokens.some((t2) => t2 === "--recursive" || /^-[^-]*r/.test(t2));
|
|
14264
|
+
const operands = tokens.filter((t2) => t2 !== "--" && !t2.startsWith("-"));
|
|
14265
|
+
targets.push(...operands.map((o2) => gitTarget(recursive ? `${o2}/**` : o2)));
|
|
14266
|
+
const unresolvedFileScope = fileSourcedPathspecScope(tokens);
|
|
14267
|
+
if (unresolvedFileScope) targets.push(unresolvedFileScope);
|
|
14268
|
+
}
|
|
14269
|
+
} else if (subcommand === "restore") {
|
|
14270
|
+
const staged = tokens.includes("--staged") || tokens.some((t2) => /^-[^-]*S/.test(t2));
|
|
14271
|
+
const worktree = tokens.includes("--worktree") || tokens.some((t2) => /^-[^-]*W/.test(t2));
|
|
14272
|
+
if (!staged || worktree) {
|
|
14273
|
+
const operands = [];
|
|
14274
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
14275
|
+
const token = tokens[index] ?? "";
|
|
14276
|
+
if (token === "--source" || token === "-s") {
|
|
14277
|
+
index += 1;
|
|
14278
|
+
continue;
|
|
14279
|
+
}
|
|
14280
|
+
if (token.startsWith("--source=") || /^-s.+/.test(token) && token !== "--staged") {
|
|
14281
|
+
continue;
|
|
14282
|
+
}
|
|
14283
|
+
if (token !== "--" && !token.startsWith("-")) operands.push(token);
|
|
14284
|
+
}
|
|
14285
|
+
targets.push(...operands.flatMap(gitPathspecTargets));
|
|
14286
|
+
const unresolvedFileScope = fileSourcedPathspecScope(tokens);
|
|
14287
|
+
if (unresolvedFileScope) targets.push(unresolvedFileScope);
|
|
14288
|
+
}
|
|
14289
|
+
} else if (subcommand === "checkout" || subcommand === "switch") {
|
|
14290
|
+
const separator = tokens.indexOf("--");
|
|
14291
|
+
if (separator >= 0) {
|
|
14292
|
+
const paths = tokens.slice(separator + 1);
|
|
14293
|
+
targets.push(...paths.flatMap(gitPathspecTargets));
|
|
14294
|
+
} else {
|
|
14295
|
+
const createFlags = subcommand === "checkout" ? /* @__PURE__ */ new Set(["-b", "-B", "--branch", "--orphan"]) : /* @__PURE__ */ new Set(["-c", "-C", "--create", "--force-create"]);
|
|
14296
|
+
const createIndex = tokens.findIndex((token) => createFlags.has(token));
|
|
14297
|
+
const branchNameIndex = createIndex >= 0 ? createIndex + 1 : -1;
|
|
14298
|
+
const operands = tokens.filter(
|
|
14299
|
+
(token, index) => !token.startsWith("-") && index !== branchNameIndex
|
|
14300
|
+
);
|
|
14301
|
+
if (operands.length > 0) targets.push(gitTarget("."));
|
|
14302
|
+
}
|
|
14303
|
+
} else if (subcommand === "stash") {
|
|
14304
|
+
const action = tokens.find((token) => !token.startsWith("-"))?.toLowerCase();
|
|
14305
|
+
if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) {
|
|
14306
|
+
targets.push(gitTarget("."));
|
|
14307
|
+
}
|
|
14308
|
+
} else if (subcommand === "reset" && tokens.some((t2) => t2 === "--hard" || t2 === "--merge" || t2 === "--keep"))
|
|
14309
|
+
targets.push(gitTarget("."));
|
|
14310
|
+
}
|
|
14311
|
+
}
|
|
14312
|
+
const findDelete = /(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b([^;&|)`]*(?:\s-delete(?:\s|$)|\s-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)[^;&|)`]*)/gi;
|
|
14313
|
+
let f = findDelete.exec(normalizedCommand);
|
|
14314
|
+
while (f !== null) {
|
|
14315
|
+
const tokens = shellTokens(f[1] ?? "");
|
|
14316
|
+
const expressionIndex = tokens.findIndex(
|
|
14317
|
+
(token) => token.startsWith("-") || token === "!" || token === "("
|
|
14318
|
+
);
|
|
14319
|
+
const roots = (expressionIndex === -1 ? tokens : tokens.slice(0, expressionIndex)).filter(
|
|
14320
|
+
(token) => token.length > 0
|
|
14321
|
+
);
|
|
14322
|
+
targets.push(...roots.length > 0 ? roots : ["."]);
|
|
14323
|
+
f = findDelete.exec(normalizedCommand);
|
|
14324
|
+
}
|
|
14325
|
+
const shellWrapper = /\b(?:ba|z|k)?sh\s+-c\s+(['"])(.*?)\1/gi;
|
|
14326
|
+
let w = shellWrapper.exec(normalizedCommand);
|
|
14327
|
+
while (w !== null) {
|
|
14328
|
+
if (w[2] && !tokenIsQuoted(w, w[0].split(/\s/)[0] ?? "")) {
|
|
14329
|
+
targets.push(...destructiveTargetsAtDepth(w[2], depth + 1));
|
|
14330
|
+
}
|
|
14331
|
+
w = shellWrapper.exec(normalizedCommand);
|
|
14332
|
+
}
|
|
14333
|
+
let quote = null;
|
|
14334
|
+
for (let index = 0; index < normalizedCommand.length; index += 1) {
|
|
14335
|
+
const char = normalizedCommand[index];
|
|
14336
|
+
if (char === "\n") {
|
|
14337
|
+
quote = null;
|
|
14338
|
+
continue;
|
|
14339
|
+
}
|
|
14340
|
+
if (isQuoteBoundary(normalizedCommand, index, quote)) {
|
|
14341
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
14342
|
+
continue;
|
|
14343
|
+
}
|
|
14344
|
+
if (quote !== null || char !== ">") continue;
|
|
14345
|
+
const redirectsBothStreams = normalizedCommand[index + 1] === "&";
|
|
14346
|
+
const overridesNoclobber = normalizedCommand[index + 1] === "|";
|
|
14347
|
+
if (redirectsBothStreams || overridesNoclobber || normalizedCommand[index + 1] === ">") {
|
|
14348
|
+
index += 1;
|
|
14349
|
+
}
|
|
14350
|
+
while (normalizedCommand[index + 1] === " " || normalizedCommand[index + 1] === " ") {
|
|
14351
|
+
index += 1;
|
|
14352
|
+
}
|
|
14353
|
+
const targetQuote = normalizedCommand[index + 1];
|
|
14354
|
+
let end = index + 1;
|
|
14355
|
+
let target;
|
|
14356
|
+
if (targetQuote === "'" || targetQuote === '"') {
|
|
14357
|
+
end = normalizedCommand.indexOf(targetQuote, index + 2);
|
|
14358
|
+
if (end === -1) continue;
|
|
14359
|
+
target = normalizedCommand.slice(index + 2, end);
|
|
14360
|
+
} else {
|
|
14361
|
+
while (end < normalizedCommand.length && !/[\s;&|>()]/.test(normalizedCommand[end] ?? "")) {
|
|
14362
|
+
end += 1;
|
|
14363
|
+
}
|
|
14364
|
+
target = normalizedCommand.slice(index + 1, end);
|
|
14365
|
+
}
|
|
14366
|
+
if (target && !(redirectsBothStreams && (/^\d+$/.test(target) || target === "-"))) {
|
|
13208
14367
|
targets.push(target);
|
|
13209
14368
|
}
|
|
13210
|
-
|
|
14369
|
+
index = end;
|
|
13211
14370
|
}
|
|
13212
|
-
return
|
|
14371
|
+
return [
|
|
14372
|
+
...new Set(
|
|
14373
|
+
targets.map((target) => target.replace(/^['"]|['"]$/g, "")).filter((target) => target !== "/dev/null" && target.toLowerCase() !== "nul")
|
|
14374
|
+
)
|
|
14375
|
+
];
|
|
13213
14376
|
}
|
|
13214
14377
|
var plugin38 = {
|
|
13215
14378
|
name: "path-guard",
|
|
@@ -13231,7 +14394,7 @@ var plugin38 = {
|
|
|
13231
14394
|
protect: {
|
|
13232
14395
|
type: "array",
|
|
13233
14396
|
items: { type: "string" },
|
|
13234
|
-
description: "Glob patterns for protected paths. Replaces the default set when present."
|
|
14397
|
+
description: "Glob patterns for protected paths. Replaces the default set when present. Writer globs whose unresolved scope overlaps protection are blocked; narrow the target or add an `allow` glob."
|
|
13235
14398
|
},
|
|
13236
14399
|
allow: {
|
|
13237
14400
|
type: "array",
|
|
@@ -13256,21 +14419,23 @@ var plugin38 = {
|
|
|
13256
14419
|
const cfg = readConfig33(api.config.extensions?.["path-guard"]);
|
|
13257
14420
|
const protectRes = cfg.protect.map(compilePathGlob);
|
|
13258
14421
|
const allowRes = cfg.allow.map(compilePathGlob);
|
|
13259
|
-
const verdict = (path, tool, operation) => {
|
|
14422
|
+
const verdict = (path, tool, operation, isScope = false) => {
|
|
14423
|
+
const subject = isScope ? `write scope "${path}" may include a protected path \u2014 narrow it or add an \`allow\` glob` : `"${path}" is a protected path`;
|
|
14424
|
+
const matchContext = isScope ? 'its unresolved scope overlaps config.extensions["path-guard"].protect' : 'matched by config.extensions["path-guard"].protect';
|
|
13260
14425
|
if (cfg.mode === "block") {
|
|
13261
14426
|
state35.blocks += 1;
|
|
13262
14427
|
state35.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
|
|
13263
14428
|
api.metrics.counter("blocks");
|
|
13264
14429
|
return {
|
|
13265
14430
|
decision: "block",
|
|
13266
|
-
reason: `path-guard:
|
|
14431
|
+
reason: `path-guard: ${subject} (${matchContext}) \u2014 ${operation} refused. If this change is intentional, ask the user to do it, add an \`allow\` glob, or set mode: "warn".`
|
|
13267
14432
|
};
|
|
13268
14433
|
}
|
|
13269
14434
|
state35.warns += 1;
|
|
13270
14435
|
api.metrics.counter("warns");
|
|
13271
14436
|
return {
|
|
13272
14437
|
decision: "allow",
|
|
13273
|
-
additionalContext: `path-guard (warn mode):
|
|
14438
|
+
additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
|
|
13274
14439
|
};
|
|
13275
14440
|
};
|
|
13276
14441
|
const hook = (input) => {
|
|
@@ -13278,28 +14443,56 @@ var plugin38 = {
|
|
|
13278
14443
|
state35.invocations += 1;
|
|
13279
14444
|
const toolName = input.toolName ?? "";
|
|
13280
14445
|
const ti = input.toolInput ?? {};
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
14446
|
+
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
14447
|
+
const commandArgs = Array.isArray(ti["args"]) ? ti["args"].filter((arg) => typeof arg === "string") : [];
|
|
14448
|
+
const commandForInspection = [
|
|
14449
|
+
command,
|
|
14450
|
+
...commandArgs.map((arg) => /^[\w./:@%+=,-]+$/.test(arg) ? arg : JSON.stringify(arg))
|
|
14451
|
+
].join(" ");
|
|
14452
|
+
if (command && executesShell({
|
|
14453
|
+
toolName: input.toolName,
|
|
14454
|
+
toolInput: ti,
|
|
14455
|
+
toolCapabilities: input.toolCapabilities,
|
|
14456
|
+
toolMutating: input.toolMutating
|
|
14457
|
+
})) {
|
|
14458
|
+
const shellTargets = destructiveTargets(commandForInspection);
|
|
14459
|
+
const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
|
|
14460
|
+
const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
|
|
14461
|
+
const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
|
|
14462
|
+
for (const path of shellTargets) {
|
|
14463
|
+
const target = {
|
|
14464
|
+
path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
|
|
14465
|
+
kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
|
|
14466
|
+
};
|
|
14467
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
|
|
14468
|
+
const protectedShellTarget = targetIntersectsPatterns(target, cfg.protect, protectRes) || matchesAny(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes);
|
|
14469
|
+
if (protectedShellTarget) {
|
|
14470
|
+
return verdict(
|
|
14471
|
+
target.path,
|
|
14472
|
+
toolName,
|
|
14473
|
+
"destructive shell command",
|
|
14474
|
+
target.kind !== "file"
|
|
14475
|
+
);
|
|
13297
14476
|
}
|
|
13298
14477
|
}
|
|
14478
|
+
const writes = writesToDisk({ ...input, toolInput: ti });
|
|
14479
|
+
const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
|
|
14480
|
+
const needsImplicitWriteScope = writes && ((input.toolCapabilities?.some(
|
|
14481
|
+
(capability) => DISK_MUTATING_CAPABILITIES.has(capability)
|
|
14482
|
+
) ?? false) || (!input.toolCapabilities || input.toolCapabilities.length === 0) && LEGACY_WRITE_TOOLS.has(toolName));
|
|
14483
|
+
if (!writes || !hasStructuredTarget && !needsImplicitWriteScope) return;
|
|
14484
|
+
}
|
|
14485
|
+
if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
|
|
14486
|
+
const targets = pathsFromToolInput(ti, toolName, input.cwd);
|
|
14487
|
+
for (const target of targets) {
|
|
14488
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
|
|
14489
|
+
if (targetIntersectsPatterns(target, cfg.protect, protectRes)) {
|
|
14490
|
+
return verdict(target.path, toolName, operationLabel(toolName), target.kind !== "file");
|
|
14491
|
+
}
|
|
13299
14492
|
}
|
|
13300
14493
|
return;
|
|
13301
14494
|
};
|
|
13302
|
-
state35.hookUnregister = api.registerHook("PreToolUse", "
|
|
14495
|
+
state35.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
|
|
13303
14496
|
name: "path-guard",
|
|
13304
14497
|
stage: "validate",
|
|
13305
14498
|
failurePolicy: "closed",
|
|
@@ -14064,9 +15257,15 @@ var plugin40 = {
|
|
|
14064
15257
|
}
|
|
14065
15258
|
}
|
|
14066
15259
|
},
|
|
14067
|
-
|
|
15260
|
+
// Writes the draft to disk (`writeFile` below), so `mutating: false` was
|
|
15261
|
+
// not just a missing capability — it actively LIED, letting even a gate
|
|
15262
|
+
// that reads `mutating` correctly through. It also flipped the tool onto
|
|
15263
|
+
// the wrong side of `smoke.test.ts`, which skips `mutating: true` tools:
|
|
15264
|
+
// this one was exercised by the suite and wrote to the repo during it.
|
|
15265
|
+
permission: "confirm",
|
|
14068
15266
|
category: "Workflow",
|
|
14069
|
-
mutating:
|
|
15267
|
+
mutating: true,
|
|
15268
|
+
capabilities: ["fs.write"],
|
|
14070
15269
|
async execute(input) {
|
|
14071
15270
|
if (!cfg.enabled) return { ok: false, error: "pr-drafter is disabled" };
|
|
14072
15271
|
const draft = await buildDraft(cfg, api.llm);
|
|
@@ -18112,6 +19311,7 @@ import { readdir as readdir5 } from "node:fs/promises";
|
|
|
18112
19311
|
import { isAbsolute as isAbsolute24, join as join6, relative as relative24, resolve as resolve24 } from "node:path";
|
|
18113
19312
|
var API_VERSION33 = "^0.1.10";
|
|
18114
19313
|
function withinProject6(p) {
|
|
19314
|
+
if (p.startsWith("-")) return false;
|
|
18115
19315
|
const root = process.cwd();
|
|
18116
19316
|
const resolved = isAbsolute24(p) ? resolve24(p) : resolve24(root, p);
|
|
18117
19317
|
const rel = relative24(root, resolved);
|
|
@@ -18501,9 +19701,13 @@ var plugin52 = {
|
|
|
18501
19701
|
},
|
|
18502
19702
|
required: ["path", "oldName", "newName"]
|
|
18503
19703
|
},
|
|
18504
|
-
|
|
19704
|
+
// Rewrites source files on `apply: true`. `auto` + no declared
|
|
19705
|
+
// capability meant no confirmation prompt and no read-only-mode block —
|
|
19706
|
+
// `readonly-permission-policy` keys on `capabilities`, not `mutating`.
|
|
19707
|
+
permission: "confirm",
|
|
18505
19708
|
category: "Development",
|
|
18506
19709
|
mutating: true,
|
|
19710
|
+
capabilities: ["fs.write"],
|
|
18507
19711
|
async execute(input) {
|
|
18508
19712
|
if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
|
|
18509
19713
|
const rawPath = input.path;
|
|
@@ -19140,6 +20344,33 @@ function validateRelativeTemplatePath(field, value) {
|
|
|
19140
20344
|
}
|
|
19141
20345
|
return null;
|
|
19142
20346
|
}
|
|
20347
|
+
var PROTECTED_WRITE_PREFIXES = [
|
|
20348
|
+
".git/",
|
|
20349
|
+
".husky/",
|
|
20350
|
+
".github/workflows/",
|
|
20351
|
+
".wrongstack/",
|
|
20352
|
+
".claude/",
|
|
20353
|
+
"node_modules/"
|
|
20354
|
+
];
|
|
20355
|
+
var PROTECTED_WRITE_FILES = /* @__PURE__ */ new Set([
|
|
20356
|
+
"package.json",
|
|
20357
|
+
"pnpm-workspace.yaml",
|
|
20358
|
+
"package-lock.json",
|
|
20359
|
+
"pnpm-lock.yaml",
|
|
20360
|
+
"yarn.lock",
|
|
20361
|
+
".npmrc",
|
|
20362
|
+
".gitattributes"
|
|
20363
|
+
]);
|
|
20364
|
+
function validateWritableTemplateTarget(field, value) {
|
|
20365
|
+
const baseError = validateRelativeTemplatePath(field, value);
|
|
20366
|
+
if (baseError) return baseError;
|
|
20367
|
+
const norm = value.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
20368
|
+
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
20369
|
+
if (PROTECTED_WRITE_PREFIXES.some((p) => norm.startsWith(p)) || PROTECTED_WRITE_FILES.has(base) || base === ".env" || base.startsWith(".env.")) {
|
|
20370
|
+
return `${field} "${value}" is a protected path (VCS hooks, CI workflows, dependency manifests, or agent configuration). Write it with the \`write\` tool instead, which prompts for confirmation.`;
|
|
20371
|
+
}
|
|
20372
|
+
return null;
|
|
20373
|
+
}
|
|
19143
20374
|
var plugin54 = {
|
|
19144
20375
|
name: "template-engine",
|
|
19145
20376
|
version: "0.1.0",
|
|
@@ -19185,7 +20416,14 @@ var plugin54 = {
|
|
|
19185
20416
|
},
|
|
19186
20417
|
required: ["template", "variables"]
|
|
19187
20418
|
},
|
|
19188
|
-
|
|
20419
|
+
// Writes caller-supplied content to a caller-supplied path. `auto` +
|
|
20420
|
+
// no declared capability meant no confirmation prompt, no read-only-mode
|
|
20421
|
+
// block (`readonly-permission-policy` keys on `capabilities`, not
|
|
20422
|
+
// `mutating`), and no PreToolUse hook — plugin tools reach the executor
|
|
20423
|
+
// through `plugin_manager action:'use'`, which bypasses it.
|
|
20424
|
+
permission: "confirm",
|
|
20425
|
+
capabilities: ["fs.write"],
|
|
20426
|
+
riskTier: "destructive",
|
|
19189
20427
|
category: "Project",
|
|
19190
20428
|
mutating: true,
|
|
19191
20429
|
async execute(input) {
|
|
@@ -19206,9 +20444,13 @@ var plugin54 = {
|
|
|
19206
20444
|
return { ok: false, error: String(err) };
|
|
19207
20445
|
}
|
|
19208
20446
|
if (output_path) {
|
|
19209
|
-
const pathError =
|
|
20447
|
+
const pathError = validateWritableTemplateTarget("output_path", output_path);
|
|
19210
20448
|
if (pathError) return { ok: false, error: pathError };
|
|
19211
|
-
|
|
20449
|
+
try {
|
|
20450
|
+
await writeFile5(output_path, result, "utf-8");
|
|
20451
|
+
} catch (err) {
|
|
20452
|
+
return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
|
|
20453
|
+
}
|
|
19212
20454
|
return {
|
|
19213
20455
|
ok: true,
|
|
19214
20456
|
output_path,
|
|
@@ -19244,7 +20486,10 @@ var plugin54 = {
|
|
|
19244
20486
|
},
|
|
19245
20487
|
required: ["template_path", "variables"]
|
|
19246
20488
|
},
|
|
19247
|
-
|
|
20489
|
+
// Same reasoning as `template_expand` above.
|
|
20490
|
+
permission: "confirm",
|
|
20491
|
+
capabilities: ["fs.write"],
|
|
20492
|
+
riskTier: "destructive",
|
|
19248
20493
|
mutating: true,
|
|
19249
20494
|
async execute(input) {
|
|
19250
20495
|
const template_path = input["template_path"];
|
|
@@ -19272,9 +20517,13 @@ var plugin54 = {
|
|
|
19272
20517
|
return { ok: false, error: `Template rendering failed: ${err}` };
|
|
19273
20518
|
}
|
|
19274
20519
|
if (output_path) {
|
|
19275
|
-
const pathError =
|
|
20520
|
+
const pathError = validateWritableTemplateTarget("output_path", output_path);
|
|
19276
20521
|
if (pathError) return { ok: false, error: pathError };
|
|
19277
|
-
|
|
20522
|
+
try {
|
|
20523
|
+
await writeFile5(output_path, result, "utf-8");
|
|
20524
|
+
} catch (err) {
|
|
20525
|
+
return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
|
|
20526
|
+
}
|
|
19278
20527
|
return {
|
|
19279
20528
|
ok: true,
|
|
19280
20529
|
template_path,
|
|
@@ -20085,6 +21334,30 @@ function readConfig50(raw) {
|
|
|
20085
21334
|
maxSourceChars: typeof r["maxSourceChars"] === "number" && r["maxSourceChars"] >= 1e3 && r["maxSourceChars"] <= 1e5 ? r["maxSourceChars"] : DEFAULTS47.maxSourceChars
|
|
20086
21335
|
};
|
|
20087
21336
|
}
|
|
21337
|
+
var SOURCE_EXTENSIONS = [
|
|
21338
|
+
".ts",
|
|
21339
|
+
".tsx",
|
|
21340
|
+
".js",
|
|
21341
|
+
".jsx",
|
|
21342
|
+
".mjs",
|
|
21343
|
+
".cjs",
|
|
21344
|
+
".mts",
|
|
21345
|
+
".cts",
|
|
21346
|
+
".py",
|
|
21347
|
+
".go",
|
|
21348
|
+
".rs",
|
|
21349
|
+
".java",
|
|
21350
|
+
".kt",
|
|
21351
|
+
".rb",
|
|
21352
|
+
".php",
|
|
21353
|
+
".cs",
|
|
21354
|
+
".swift",
|
|
21355
|
+
".c",
|
|
21356
|
+
".h",
|
|
21357
|
+
".cc",
|
|
21358
|
+
".cpp",
|
|
21359
|
+
".hpp"
|
|
21360
|
+
];
|
|
20088
21361
|
function withinProject9(p) {
|
|
20089
21362
|
if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
|
|
20090
21363
|
const root = process.cwd();
|
|
@@ -20312,6 +21585,12 @@ var plugin57 = {
|
|
|
20312
21585
|
if (!withinProject9(rawPath)) {
|
|
20313
21586
|
return { ok: false, error: "path is outside the project root" };
|
|
20314
21587
|
}
|
|
21588
|
+
if (!SOURCE_EXTENSIONS.some((ext) => rawPath.toLowerCase().endsWith(ext))) {
|
|
21589
|
+
return {
|
|
21590
|
+
ok: false,
|
|
21591
|
+
error: `test generation only reads source files (${SOURCE_EXTENSIONS.join(", ")}); refusing "${rawPath}"`
|
|
21592
|
+
};
|
|
21593
|
+
}
|
|
20315
21594
|
const resolved = resolve27(process.cwd(), rawPath);
|
|
20316
21595
|
state54.generateCount += 1;
|
|
20317
21596
|
let result;
|