@wrongstack/plugins 0.300.0 → 0.302.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/path-guard.js
CHANGED
|
@@ -39,11 +39,15 @@ function compilePathGlob(pattern) {
|
|
|
39
39
|
let source = "";
|
|
40
40
|
for (let i = 0; i < normalized.length; i++) {
|
|
41
41
|
const ch = normalized[i];
|
|
42
|
+
if (ch === "/" && normalized.slice(i) === "/**") {
|
|
43
|
+
source += "(?:/(?:[^/]+(?:/[^/]+)*)?)?";
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
42
46
|
if (ch === "*") {
|
|
43
47
|
if (normalized[i + 1] === "*") {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
const followedBySlash = normalized[i + 2] === "/";
|
|
49
|
+
source += followedBySlash ? "(?:[^/]+/)*" : "(?:[^/]+(?:/[^/]+)*)?";
|
|
50
|
+
i += followedBySlash ? 2 : 1;
|
|
47
51
|
} else {
|
|
48
52
|
source += "[^/]*";
|
|
49
53
|
}
|
|
@@ -58,31 +62,1185 @@ function compilePathGlob(pattern) {
|
|
|
58
62
|
return new RegExp(`(?:^|/)${source}$`, "i");
|
|
59
63
|
}
|
|
60
64
|
function normalizePath(p) {
|
|
61
|
-
|
|
65
|
+
const slashNormalized = p.replace(/\\/g, "/");
|
|
66
|
+
const drive = /^[a-z]:/i.exec(slashNormalized)?.[0] ?? "";
|
|
67
|
+
const absolute = slashNormalized.startsWith("/") || drive.length > 0;
|
|
68
|
+
const body = drive ? slashNormalized.slice(drive.length).replace(/^\//, "") : slashNormalized;
|
|
69
|
+
const segments = [];
|
|
70
|
+
for (const segment of body.split("/")) {
|
|
71
|
+
if (!segment || segment === ".") continue;
|
|
72
|
+
if (segment === "..") {
|
|
73
|
+
if (segments.length > 0 && segments.at(-1) !== "..") {
|
|
74
|
+
segments.pop();
|
|
75
|
+
} else if (!absolute) {
|
|
76
|
+
segments.push(segment);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
segments.push(segment);
|
|
81
|
+
}
|
|
82
|
+
const joined = segments.join("/");
|
|
83
|
+
if (drive) return `${drive}/${joined}`.replace(/\/$/, "");
|
|
84
|
+
if (slashNormalized.startsWith("/")) return `/${joined}`.replace(/\/$/, "") || "/";
|
|
85
|
+
return joined;
|
|
86
|
+
}
|
|
87
|
+
function isAbsolutePath(path) {
|
|
88
|
+
return /^(?:\/|[a-z]:\/)/i.test(path);
|
|
89
|
+
}
|
|
90
|
+
function resolveTargetPath(path, base) {
|
|
91
|
+
const normalized = normalizePath(path);
|
|
92
|
+
const target = normalized === "" && /^\.\/?$/.test(path.trim()) ? "." : normalized;
|
|
93
|
+
return base && !isAbsolutePath(target) ? normalizePath(`${base}/${target}`) : target;
|
|
94
|
+
}
|
|
95
|
+
function relativeToInvocationCwd(path, invocationCwd) {
|
|
96
|
+
const normalizedPath = normalizePath(path).replace(/\/$/, "");
|
|
97
|
+
const normalized = normalizedPath === "" && /^\.\/?$/.test(path.trim()) ? "." : normalizedPath;
|
|
98
|
+
if (!invocationCwd || !isAbsolutePath(normalizePath(invocationCwd))) return normalized;
|
|
99
|
+
const root = normalizePath(invocationCwd).replace(/\/$/, "");
|
|
100
|
+
const pathForComparison = /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
101
|
+
const rootForComparison = /^[a-z]:\//i.test(root) ? root.toLowerCase() : root;
|
|
102
|
+
if (pathForComparison === rootForComparison) return ".";
|
|
103
|
+
if (pathForComparison.startsWith(`${rootForComparison}/`)) {
|
|
104
|
+
return normalized.slice(root.length + 1);
|
|
105
|
+
}
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
function effectiveToolCwd(toolInputCwd, invocationCwd) {
|
|
109
|
+
if (typeof toolInputCwd !== "string" || toolInputCwd.length === 0) return invocationCwd;
|
|
110
|
+
if (!invocationCwd || isAbsolutePath(normalizePath(toolInputCwd))) return toolInputCwd;
|
|
111
|
+
return resolveTargetPath(toolInputCwd, invocationCwd);
|
|
62
112
|
}
|
|
63
113
|
function matchesAny(path, patterns) {
|
|
64
114
|
const normalized = normalizePath(path);
|
|
65
115
|
return patterns.some((re) => re.test(normalized));
|
|
66
116
|
}
|
|
117
|
+
function isUnresolvedPathScope(path) {
|
|
118
|
+
return /[*?]/.test(path);
|
|
119
|
+
}
|
|
120
|
+
function isRootPathScope(path) {
|
|
121
|
+
const normalized = normalizePath(path).replace(/\/$/, "");
|
|
122
|
+
return normalized === "." || normalized === "";
|
|
123
|
+
}
|
|
124
|
+
function isDirectoryAmbiguousPath(path) {
|
|
125
|
+
const normalized = normalizePath(path).replace(/\/$/, "");
|
|
126
|
+
if (isRootPathScope(normalized)) return true;
|
|
127
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
128
|
+
return path.endsWith("/") || basename.length > 0 && !basename.includes(".");
|
|
129
|
+
}
|
|
130
|
+
function hasConfiguredProtectedDescendant(path, patterns) {
|
|
131
|
+
const normalized = normalizePath(path).replace(/\/$/, "").toLowerCase();
|
|
132
|
+
if (!normalized || isUnresolvedPathScope(normalized)) return false;
|
|
133
|
+
return patterns.some((pattern) => {
|
|
134
|
+
const prefix = staticPrefix(normalizePath(pattern)).toLowerCase();
|
|
135
|
+
return prefix.startsWith(`${normalized}/`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function staticPrefix(pattern) {
|
|
139
|
+
const normalized = normalizePath(pattern);
|
|
140
|
+
if (normalized === ".") return "";
|
|
141
|
+
const wildcardIndex = normalized.search(/[*?]/);
|
|
142
|
+
return (wildcardIndex === -1 ? normalized : normalized.slice(0, wildcardIndex)).replace(
|
|
143
|
+
/\/$/,
|
|
144
|
+
""
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function hasPartialSegmentWildcard(pattern) {
|
|
148
|
+
const normalized = normalizePath(pattern);
|
|
149
|
+
const wildcardIndex = normalized.search(/[*?]/);
|
|
150
|
+
return wildcardIndex > 0 && normalized[wildcardIndex - 1] !== "/";
|
|
151
|
+
}
|
|
152
|
+
function globWitness(pattern) {
|
|
153
|
+
return pattern.replace(/\*+/g, "").replace(/\?/g, "x");
|
|
154
|
+
}
|
|
155
|
+
function scopesMayOverlap(left, right) {
|
|
156
|
+
const normalizedRight = normalizePath(right);
|
|
157
|
+
if (!normalizedRight.includes("/")) {
|
|
158
|
+
const leftPrefix2 = staticPrefix(left);
|
|
159
|
+
const candidate = leftPrefix2 ? `${leftPrefix2}/${globWitness(normalizedRight)}` : globWitness(normalizedRight);
|
|
160
|
+
if (compilePathGlob(left).test(candidate)) return true;
|
|
161
|
+
return hasPartialSegmentWildcard(left);
|
|
162
|
+
}
|
|
163
|
+
const leftPrefix = staticPrefix(left).toLowerCase();
|
|
164
|
+
const rightPrefix = staticPrefix(right).toLowerCase();
|
|
165
|
+
if (!leftPrefix || !rightPrefix) return true;
|
|
166
|
+
if (leftPrefix === rightPrefix || leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
|
|
170
|
+
}
|
|
171
|
+
function targetIntersectsPatterns(target, patternTexts, patterns) {
|
|
172
|
+
if (matchesAny(target.path, patterns)) return true;
|
|
173
|
+
if (target.kind === "file") return false;
|
|
174
|
+
const normalized = normalizePath(target.path).replace(/\/$/, "");
|
|
175
|
+
if (!isUnresolvedPathScope(normalized)) {
|
|
176
|
+
if (normalized === "." || normalized === "") return patternTexts.length > 0;
|
|
177
|
+
const descendantScope = `${normalized}/**`;
|
|
178
|
+
return patternTexts.some(
|
|
179
|
+
(pattern) => scopesMayOverlap(descendantScope, normalizePath(pattern))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath(pattern)));
|
|
183
|
+
}
|
|
184
|
+
function targetFullyAllowed(target, allowTexts, allowRes) {
|
|
185
|
+
if (target.kind === "file") return matchesAny(target.path, allowRes);
|
|
186
|
+
const normalized = normalizePath(target.path).replace(/\/$/, "");
|
|
187
|
+
if (isUnresolvedPathScope(normalized)) {
|
|
188
|
+
return allowTexts.some((allow, index) => {
|
|
189
|
+
const allowNormalized = normalizePath(allow);
|
|
190
|
+
if (allowNormalized === normalized) return true;
|
|
191
|
+
const targetPrefix = staticPrefix(normalized);
|
|
192
|
+
const allowRe = allowRes[index];
|
|
193
|
+
return targetPrefix.length > 0 && !hasPartialSegmentWildcard(normalized) && allowNormalized.endsWith("/**") && allowRe?.test(targetPrefix);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return allowTexts.some((allow, index) => {
|
|
197
|
+
const allowNormalized = normalizePath(allow);
|
|
198
|
+
const allowRe = allowRes[index];
|
|
199
|
+
return allowNormalized.endsWith("/**") && allowRe?.test(`${normalized}/.path-guard-probe`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
var WRITE_CAPABILITIES = /* @__PURE__ */ new Set(["fs.write", "fs.write.outside-project"]);
|
|
203
|
+
var DISK_MUTATING_CAPABILITIES = /* @__PURE__ */ new Set(["package.install"]);
|
|
204
|
+
var SHELL_CAPABILITIES = /* @__PURE__ */ new Set(["shell.arbitrary", "shell.restricted", "shell.exec"]);
|
|
205
|
+
var LEGACY_SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell", "exec"]);
|
|
206
|
+
var LEGACY_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
207
|
+
"write",
|
|
208
|
+
"edit",
|
|
209
|
+
"patch",
|
|
210
|
+
"scaffold",
|
|
211
|
+
"format",
|
|
212
|
+
"replace",
|
|
213
|
+
"design",
|
|
214
|
+
"install"
|
|
215
|
+
]);
|
|
216
|
+
function writesToDisk(input) {
|
|
217
|
+
const capabilities = input.toolCapabilities;
|
|
218
|
+
const capabilitiesUndeclared = !capabilities || capabilities.length === 0;
|
|
219
|
+
const hasWriteCap = capabilities?.some((capability) => WRITE_CAPABILITIES.has(capability)) ?? false;
|
|
220
|
+
const hasDiskMutatingCap = capabilities?.some((capability) => DISK_MUTATING_CAPABILITIES.has(capability)) ?? false;
|
|
221
|
+
const hasCommandString = typeof input.toolInput?.["command"] === "string";
|
|
222
|
+
if (hasCommandString && executesShell(input) && !hasWriteCap && !hasDiskMutatingCap && !(capabilitiesUndeclared && LEGACY_WRITE_TOOLS.has(input.toolName ?? "")))
|
|
223
|
+
return false;
|
|
224
|
+
if (!capabilitiesUndeclared) {
|
|
225
|
+
if (hasWriteCap || hasDiskMutatingCap) return true;
|
|
226
|
+
if (capabilities.includes("mcp.proxy")) {
|
|
227
|
+
const segmentedName = (input.toolName ?? "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
228
|
+
const filesystemWriter = /(?:^|_)filesystem(?:_|$)/.test(segmentedName) && /(?:^|_)(?:add|append|copy|create|delete|edit|mkdir|move|remove|rename|write)(?:_|$)/.test(
|
|
229
|
+
segmentedName
|
|
230
|
+
);
|
|
231
|
+
const toolInput = input.toolInput ?? {};
|
|
232
|
+
const hasKnownPath = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some(
|
|
233
|
+
(field) => toolInput[field] !== void 0
|
|
234
|
+
);
|
|
235
|
+
if (filesystemWriter && hasKnownPath) return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (input.toolMutating !== void 0) return input.toolMutating;
|
|
240
|
+
return LEGACY_WRITE_TOOLS.has(input.toolName ?? "");
|
|
241
|
+
}
|
|
242
|
+
function executesShell(input) {
|
|
243
|
+
const toolName = input.toolName ?? "";
|
|
244
|
+
if (LEGACY_SHELL_TOOLS.has(toolName)) return true;
|
|
245
|
+
if (toolName === "git") return false;
|
|
246
|
+
const segmentedName = toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
247
|
+
const isCommandStringRunner = /(?:^|[_.-])(?:command|exec|execute|shell|terminal)(?:$|[_.-])/i.test(segmentedName);
|
|
248
|
+
const hasShellCapability = input.toolCapabilities?.some((capability) => SHELL_CAPABILITIES.has(capability)) ?? false;
|
|
249
|
+
const hasCommandString = typeof input.toolInput?.["command"] === "string";
|
|
250
|
+
return isCommandStringRunner && hasShellCapability || hasCommandString && input.toolMutating === true;
|
|
251
|
+
}
|
|
252
|
+
var PATH_FIELDS = [
|
|
253
|
+
"path",
|
|
254
|
+
"file_path",
|
|
255
|
+
"filePath",
|
|
256
|
+
"target",
|
|
257
|
+
"target_path",
|
|
258
|
+
"targetPath",
|
|
259
|
+
"destination",
|
|
260
|
+
"dest",
|
|
261
|
+
"output_path",
|
|
262
|
+
"outputPath",
|
|
263
|
+
"out",
|
|
264
|
+
"file"
|
|
265
|
+
];
|
|
266
|
+
var PATH_LIST_FIELDS = ["files", "paths"];
|
|
267
|
+
var SOURCE_PATH_FIELDS = [
|
|
268
|
+
"source",
|
|
269
|
+
"from",
|
|
270
|
+
"src",
|
|
271
|
+
"input_path",
|
|
272
|
+
"inputPath",
|
|
273
|
+
"old_path",
|
|
274
|
+
"oldPath"
|
|
275
|
+
];
|
|
276
|
+
var MOVE_TOOL_NAME = /(?:^|[_.-])(move|rename)(?:$|[_.-])/i;
|
|
277
|
+
var DIRECTORY_DELETE_TOOL_NAME = /(?:^|[_.-])(?:rmdir|(?:delete|remove)[_.-](?:dir|directory))(?:$|[_.-])/i;
|
|
278
|
+
function segmentedToolName(toolName) {
|
|
279
|
+
return toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
280
|
+
}
|
|
281
|
+
function isMoveToolName(toolName) {
|
|
282
|
+
return MOVE_TOOL_NAME.test(segmentedToolName(toolName));
|
|
283
|
+
}
|
|
284
|
+
function isDirectoryDeleteToolName(toolName) {
|
|
285
|
+
return DIRECTORY_DELETE_TOOL_NAME.test(segmentedToolName(toolName));
|
|
286
|
+
}
|
|
287
|
+
function cleanPatchPath(raw) {
|
|
288
|
+
const trimmed = raw.trim();
|
|
289
|
+
if (!trimmed || trimmed === "/dev/null") return null;
|
|
290
|
+
const quoted = /^"([^"]+)"(?:\s|$)/.exec(trimmed)?.[1];
|
|
291
|
+
const withoutTimestamp = quoted ?? trimmed.replace(/(?:\t|\s+)\d{4}-\d{2}-\d{2}(?:\s.*)?$/, "").trim();
|
|
292
|
+
if (!withoutTimestamp || withoutTimestamp === "/dev/null") return null;
|
|
293
|
+
return withoutTimestamp.replace(/^[ab]\//, "");
|
|
294
|
+
}
|
|
295
|
+
function pathsFromPatch(patch) {
|
|
296
|
+
const paths = [];
|
|
297
|
+
const lines = patch.split(/\r?\n/);
|
|
298
|
+
for (let index = 0; index + 2 < lines.length; index += 1) {
|
|
299
|
+
const oldHeader = lines[index];
|
|
300
|
+
const newHeader = lines[index + 1];
|
|
301
|
+
const hunkHeader = lines[index + 2];
|
|
302
|
+
if (!oldHeader?.startsWith("--- ") || !newHeader?.startsWith("+++ ")) continue;
|
|
303
|
+
if (!hunkHeader?.startsWith("@@ ")) continue;
|
|
304
|
+
const oldPath = cleanPatchPath(oldHeader.slice(4));
|
|
305
|
+
const newPath = cleanPatchPath(newHeader.slice(4));
|
|
306
|
+
if (oldPath) paths.push(oldPath);
|
|
307
|
+
if (newPath) paths.push(newPath);
|
|
308
|
+
}
|
|
309
|
+
return paths;
|
|
310
|
+
}
|
|
311
|
+
function appendTarget(targets, value, kind, base) {
|
|
312
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
313
|
+
targets.push({
|
|
314
|
+
path: resolveTargetPath(value, base),
|
|
315
|
+
kind
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function appendTargetList(targets, value, directoryCapable, base) {
|
|
319
|
+
const appendPath = (path) => {
|
|
320
|
+
targets.push({
|
|
321
|
+
path: resolveTargetPath(path, base),
|
|
322
|
+
kind: isUnresolvedPathScope(path) || isRootPathScope(path) || directoryCapable && isDirectoryAmbiguousPath(path) ? "scope" : "file"
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
if (typeof value === "string") {
|
|
326
|
+
const paths = value.split(",").map((item) => item.trim()).filter((path) => path.length > 0);
|
|
327
|
+
for (const path of paths) appendPath(path);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!Array.isArray(value)) return;
|
|
331
|
+
for (const item of value) {
|
|
332
|
+
if (typeof item !== "string") continue;
|
|
333
|
+
const path = item.trim();
|
|
334
|
+
if (path.length > 0) appendPath(path);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function isReadOnlyInvocation(toolName, toolInput) {
|
|
338
|
+
if (toolName === "git") {
|
|
339
|
+
const command = toolInput["command"];
|
|
340
|
+
if (command === "status" || command === "log" || command === "diff" || command === "fetch" || command === "commit" || command === "branch" || command === "push") {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return command === "worktree" && toolInput["worktreeAction"] === "list";
|
|
344
|
+
}
|
|
345
|
+
if (toolName === "design") {
|
|
346
|
+
const action = toolInput["action"] ?? "list";
|
|
347
|
+
return action === "list" || action === "foundations" || action === "verify";
|
|
348
|
+
}
|
|
349
|
+
if (toolName === "format") return toolInput["check"] === true;
|
|
350
|
+
if (toolName === "patch" || toolName === "scaffold") return toolInput["dry_run"] === true;
|
|
351
|
+
if (toolName === "replace") return toolInput["dry_run"] !== false;
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
function pathsFromToolInput(toolInput, toolName, invocationCwd) {
|
|
355
|
+
const targets = [];
|
|
356
|
+
const effectiveCwd = effectiveToolCwd(toolInput["cwd"], invocationCwd);
|
|
357
|
+
const directoryDelete = isDirectoryDeleteToolName(toolName);
|
|
358
|
+
for (const field of PATH_FIELDS) {
|
|
359
|
+
const value = toolInput[field];
|
|
360
|
+
appendTarget(
|
|
361
|
+
targets,
|
|
362
|
+
value,
|
|
363
|
+
directoryDelete ? "deletion-scope" : typeof value === "string" && isUnresolvedPathScope(value) ? "scope" : "file",
|
|
364
|
+
effectiveCwd
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const directoryCapableLists = toolName === "format" || toolName === "replace";
|
|
368
|
+
for (const field of PATH_LIST_FIELDS) {
|
|
369
|
+
appendTargetList(
|
|
370
|
+
targets,
|
|
371
|
+
toolInput[field],
|
|
372
|
+
directoryCapableLists && field === "files",
|
|
373
|
+
effectiveCwd
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (toolName === "install") {
|
|
377
|
+
appendTarget(targets, effectiveCwd ?? invocationCwd ?? ".", "scope");
|
|
378
|
+
}
|
|
379
|
+
if (toolName === "git") {
|
|
380
|
+
const command = toolInput["command"];
|
|
381
|
+
if (command === "worktree" && (toolInput["worktreeAction"] === "add" || toolInput["worktreeAction"] === "remove")) {
|
|
382
|
+
appendTarget(targets, toolInput["worktreePath"] ?? toolInput["worktree_path"], "file");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (toolName === "scaffold") {
|
|
386
|
+
const cwdValue = toolInput["cwd"];
|
|
387
|
+
const cwd = typeof cwdValue === "string" && cwdValue.length > 0 ? cwdValue : invocationCwd ?? ".";
|
|
388
|
+
const base = normalizePath(cwd).replace(/\/$/, "");
|
|
389
|
+
const name = typeof toolInput["name"] === "string" ? toolInput["name"] : "";
|
|
390
|
+
const template = toolInput["template"];
|
|
391
|
+
const filesByTemplate = {
|
|
392
|
+
"npm-package": ["package.json", "tsconfig.json", "src/index.ts", "src/index.test.ts"],
|
|
393
|
+
"cli-tool": ["package.json", "src/index.ts"],
|
|
394
|
+
"react-component": name ? [`${name}.tsx`, `${name}.test.tsx`] : []
|
|
395
|
+
};
|
|
396
|
+
for (const file of typeof template === "string" ? filesByTemplate[template] ?? [] : []) {
|
|
397
|
+
targets.push({ path: `${base}/${file}`, kind: "file" });
|
|
398
|
+
}
|
|
399
|
+
targets.push({ path: base, kind: "scope" });
|
|
400
|
+
}
|
|
401
|
+
if (isMoveToolName(toolName)) {
|
|
402
|
+
for (const field of SOURCE_PATH_FIELDS) {
|
|
403
|
+
appendTarget(targets, toolInput[field], "deletion-scope", effectiveCwd);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const patch = toolInput["patch"];
|
|
407
|
+
if (typeof patch === "string") {
|
|
408
|
+
const patchTargets = pathsFromPatch(patch);
|
|
409
|
+
const directory = toolInput["directory"];
|
|
410
|
+
const baseValue = typeof directory === "string" && directory.length > 0 ? directory : invocationCwd;
|
|
411
|
+
if (baseValue) {
|
|
412
|
+
const base = normalizePath(baseValue).replace(/\/$/, "");
|
|
413
|
+
targets.push(
|
|
414
|
+
...patchTargets.map((target) => ({ path: `${base}/${target}`, kind: "file" }))
|
|
415
|
+
);
|
|
416
|
+
} else {
|
|
417
|
+
targets.push(...patchTargets.map((path) => ({ path, kind: "file" })));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (targets.length === 0) {
|
|
421
|
+
const cwd = effectiveCwd ?? ".";
|
|
422
|
+
targets.push({ path: normalizePath(cwd).replace(/\/$/, "") || ".", kind: "scope" });
|
|
423
|
+
}
|
|
424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
425
|
+
return targets.map((target) => ({
|
|
426
|
+
...target,
|
|
427
|
+
path: relativeToInvocationCwd(target.path, invocationCwd)
|
|
428
|
+
})).filter((target) => {
|
|
429
|
+
const key = `${target.kind}:${target.path}`;
|
|
430
|
+
if (seen.has(key)) return false;
|
|
431
|
+
seen.add(key);
|
|
432
|
+
return true;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function operationLabel(toolName) {
|
|
436
|
+
if (toolName === "write") return "write";
|
|
437
|
+
if (toolName === "edit") return "edit";
|
|
438
|
+
return `write via "${toolName}"`;
|
|
439
|
+
}
|
|
440
|
+
var VALUE_TAKING_GIT_OPTIONS = /* @__PURE__ */ new Set([
|
|
441
|
+
"-C",
|
|
442
|
+
"-c",
|
|
443
|
+
"--git-dir",
|
|
444
|
+
"--work-tree",
|
|
445
|
+
"--namespace",
|
|
446
|
+
"--super-prefix",
|
|
447
|
+
"--exec-path",
|
|
448
|
+
"--config-env",
|
|
449
|
+
"--attr-source"
|
|
450
|
+
]);
|
|
451
|
+
var MAX_LAUNCHER_TOKENS = 256;
|
|
452
|
+
var MAX_LAUNCHER_LENGTH = 64 * 1024;
|
|
453
|
+
function boundedShellTokens(raw) {
|
|
454
|
+
const tokens = [];
|
|
455
|
+
let token = "";
|
|
456
|
+
let tokenStart = -1;
|
|
457
|
+
let quote = null;
|
|
458
|
+
const limit = Math.min(raw.length, MAX_LAUNCHER_LENGTH);
|
|
459
|
+
for (let index = 0; index < limit && tokens.length < MAX_LAUNCHER_TOKENS; index += 1) {
|
|
460
|
+
const char = raw[index] ?? "";
|
|
461
|
+
if (tokenStart < 0 && !/\s/.test(char)) tokenStart = index;
|
|
462
|
+
if (char === "\\" && quote !== "'" && index + 1 < limit) {
|
|
463
|
+
const escaped = raw[index + 1] ?? "";
|
|
464
|
+
const shellEscaped = quote === '"' ? /[$\x60"\\\r\n]/.test(escaped) : /[\s'"\\;&|()`]/.test(escaped);
|
|
465
|
+
token += shellEscaped ? escaped : `\\${escaped}`;
|
|
466
|
+
index += 1;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (char === "'" || char === '"') {
|
|
470
|
+
if (quote === char) quote = null;
|
|
471
|
+
else if (quote === null) quote = char;
|
|
472
|
+
else token += char;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (quote === null && /\s/.test(char)) {
|
|
476
|
+
if (tokenStart >= 0) tokens.push({ value: token, start: tokenStart, end: index });
|
|
477
|
+
token = "";
|
|
478
|
+
tokenStart = -1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
token += char;
|
|
482
|
+
}
|
|
483
|
+
if (tokenStart >= 0 && tokens.length < MAX_LAUNCHER_TOKENS) {
|
|
484
|
+
tokens.push({ value: token, start: tokenStart, end: limit });
|
|
485
|
+
}
|
|
486
|
+
return tokens;
|
|
487
|
+
}
|
|
488
|
+
function shellTokens(raw) {
|
|
489
|
+
return boundedShellTokens(raw).map((token) => token.value);
|
|
490
|
+
}
|
|
491
|
+
function gitInvocationArguments(command) {
|
|
492
|
+
const argumentsList = [];
|
|
493
|
+
const quotedIndexes = new Uint8Array(command.length);
|
|
494
|
+
let activeQuote = null;
|
|
495
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
496
|
+
const char = command[index];
|
|
497
|
+
if (isQuoteBoundary(command, index, activeQuote)) {
|
|
498
|
+
quotedIndexes[index] = 1;
|
|
499
|
+
activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
|
|
500
|
+
} else if (activeQuote !== null) {
|
|
501
|
+
quotedIndexes[index] = 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const gitStart = /(?:^|[;&|\r\n]\s*|\(\s*|`\s*)git\b/gi;
|
|
505
|
+
let match = gitStart.exec(command);
|
|
506
|
+
while (match !== null) {
|
|
507
|
+
const gitOffset = match[0].toLowerCase().lastIndexOf("git");
|
|
508
|
+
if (gitOffset < 0 || quotedIndexes[match.index + gitOffset] === 1) {
|
|
509
|
+
match = gitStart.exec(command);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const start = gitStart.lastIndex;
|
|
513
|
+
let quote = null;
|
|
514
|
+
let end = start;
|
|
515
|
+
for (; end < command.length; end += 1) {
|
|
516
|
+
const char = command[end] ?? "";
|
|
517
|
+
if (char === "\\" && quote !== "'") {
|
|
518
|
+
end += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (char === "'" || char === '"') {
|
|
522
|
+
if (quote === char) quote = null;
|
|
523
|
+
else if (quote === null) quote = char;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (quote === null && /[;&|)`\r\n]/.test(char)) break;
|
|
527
|
+
}
|
|
528
|
+
argumentsList.push(command.slice(start, end));
|
|
529
|
+
gitStart.lastIndex = Math.max(end, start);
|
|
530
|
+
match = gitStart.exec(command);
|
|
531
|
+
}
|
|
532
|
+
return argumentsList;
|
|
533
|
+
}
|
|
534
|
+
function gitSubcommandIndex(tokens) {
|
|
535
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
536
|
+
const token = tokens[index] ?? "";
|
|
537
|
+
if (token === "--") return index + 1 < tokens.length ? index + 1 : -1;
|
|
538
|
+
if (!token.startsWith("-")) return index;
|
|
539
|
+
const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
|
|
540
|
+
const hasAttachedValue = token.includes("=") || token.startsWith("-C") && token !== "-C";
|
|
541
|
+
if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && !hasAttachedValue) index += 1;
|
|
542
|
+
}
|
|
543
|
+
return -1;
|
|
544
|
+
}
|
|
545
|
+
function commandDeletesImplicitScope(command) {
|
|
546
|
+
const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
547
|
+
for (const rawArguments of gitInvocationArguments(stripped)) {
|
|
548
|
+
const tokens = shellTokens(rawArguments);
|
|
549
|
+
const commandIndex = gitSubcommandIndex(tokens);
|
|
550
|
+
const subcommand = commandIndex >= 0 ? tokens[commandIndex]?.toLowerCase() : void 0;
|
|
551
|
+
const operands = tokens.slice(commandIndex + 1);
|
|
552
|
+
if (/^(?:clean|restore|reset|checkout|switch)$/.test(subcommand ?? "")) return true;
|
|
553
|
+
if (subcommand === "stash") {
|
|
554
|
+
const action = operands.find((token) => !token.startsWith("-"))?.toLowerCase();
|
|
555
|
+
if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) return true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
function normalizeEnvSplitPayload(payload) {
|
|
561
|
+
const removeUnbalanced = (value, quote) => {
|
|
562
|
+
let boundaries = 0;
|
|
563
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
564
|
+
if (value[index] === quote && !quoteIsEscaped(value, index)) boundaries += 1;
|
|
565
|
+
}
|
|
566
|
+
return boundaries % 2 === 0 ? value : value.replaceAll(quote, "");
|
|
567
|
+
};
|
|
568
|
+
return removeUnbalanced(removeUnbalanced(payload, "'"), '"');
|
|
569
|
+
}
|
|
570
|
+
function unwrapEnvSplitStringAtBoundary(command) {
|
|
571
|
+
const pattern = /(^|[;&|\r\n]\s*|\(\s*|`\s*)(?:[^\s;&|(){}]+[\\/])?env\b/gi;
|
|
572
|
+
const match = pattern.exec(command);
|
|
573
|
+
if (!match) return command;
|
|
574
|
+
const boundary = match[1] ?? "";
|
|
575
|
+
const afterStart = match.index + match[0].length;
|
|
576
|
+
const after = command.slice(afterStart);
|
|
577
|
+
const tokens = boundedShellTokens(after);
|
|
578
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
579
|
+
const token = tokens[index];
|
|
580
|
+
if (!token) continue;
|
|
581
|
+
const separator = firstUnquotedShellSeparator(after, token.start, token.end);
|
|
582
|
+
const tokenEnd = separator ?? token.end;
|
|
583
|
+
const tokenValue = boundedShellTokens(after.slice(token.start, tokenEnd))[0]?.value ?? "";
|
|
584
|
+
const attachedShort = /^-[i0v]*S(.+)$/.exec(tokenValue)?.[1];
|
|
585
|
+
const attachedLong = tokenValue.startsWith("--split-string=") ? tokenValue.slice("--split-string=".length) : void 0;
|
|
586
|
+
const attachedPayload = attachedShort ?? attachedLong;
|
|
587
|
+
if (attachedPayload !== void 0) {
|
|
588
|
+
return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(attachedPayload)}${after.slice(tokenEnd)}`;
|
|
589
|
+
}
|
|
590
|
+
if (!/^-[i0v]*S$/.test(tokenValue) && tokenValue !== "--split-string") {
|
|
591
|
+
if (separator !== void 0) return command;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const payload = tokens[index + 1];
|
|
595
|
+
if (!payload) return command;
|
|
596
|
+
const payloadSeparator = firstUnquotedShellSeparator(after, payload.start, payload.end);
|
|
597
|
+
const payloadEnd = payloadSeparator ?? payload.end;
|
|
598
|
+
const payloadValue = boundedShellTokens(after.slice(payload.start, payloadEnd))[0]?.value ?? "";
|
|
599
|
+
return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(payloadValue)}${after.slice(payloadEnd)}`;
|
|
600
|
+
}
|
|
601
|
+
return command;
|
|
602
|
+
}
|
|
603
|
+
function stripTransparentLaunchers(command) {
|
|
604
|
+
let stripped = command;
|
|
605
|
+
let previous;
|
|
606
|
+
do {
|
|
607
|
+
previous = stripped;
|
|
608
|
+
let beforeUnwrap;
|
|
609
|
+
do {
|
|
610
|
+
beforeUnwrap = stripped;
|
|
611
|
+
stripped = unwrapEnvSplitStringAtBoundary(stripped);
|
|
612
|
+
} while (stripped !== beforeUnwrap);
|
|
613
|
+
stripped = stripLauncherAtBoundary(stripped, "env", ENV_VALUE_TAKING);
|
|
614
|
+
stripped = stripLauncherAtBoundary(stripped, "sudo", SUDO_VALUE_TAKING);
|
|
615
|
+
} while (stripped !== previous);
|
|
616
|
+
return stripped;
|
|
617
|
+
}
|
|
618
|
+
var SUDO_VALUE_TAKING = /* @__PURE__ */ new Set([
|
|
619
|
+
"-u",
|
|
620
|
+
"-g",
|
|
621
|
+
"-h",
|
|
622
|
+
"-p",
|
|
623
|
+
"-C",
|
|
624
|
+
"-R",
|
|
625
|
+
"-D",
|
|
626
|
+
"-r",
|
|
627
|
+
"-t",
|
|
628
|
+
"--user",
|
|
629
|
+
"--group",
|
|
630
|
+
"--host",
|
|
631
|
+
"--prompt",
|
|
632
|
+
"--close-from",
|
|
633
|
+
"--chroot",
|
|
634
|
+
"--chdir",
|
|
635
|
+
"--role",
|
|
636
|
+
"--type",
|
|
637
|
+
"--command-timeout"
|
|
638
|
+
]);
|
|
639
|
+
var ENV_VALUE_TAKING = /* @__PURE__ */ new Set([
|
|
640
|
+
"-u",
|
|
641
|
+
"-C",
|
|
642
|
+
"-P",
|
|
643
|
+
"-a",
|
|
644
|
+
"--argv0",
|
|
645
|
+
"--unset",
|
|
646
|
+
"--chdir",
|
|
647
|
+
"--split-string"
|
|
648
|
+
]);
|
|
649
|
+
var ENV_FLAG_OPTIONS = /* @__PURE__ */ new Set([
|
|
650
|
+
"-i",
|
|
651
|
+
"-0",
|
|
652
|
+
"-v",
|
|
653
|
+
"--ignore-environment",
|
|
654
|
+
"--null",
|
|
655
|
+
"--debug",
|
|
656
|
+
"--help",
|
|
657
|
+
"--version"
|
|
658
|
+
]);
|
|
659
|
+
function firstUnquotedShellSeparator(raw, start, end) {
|
|
660
|
+
let quote = null;
|
|
661
|
+
for (let index = start; index < end; index += 1) {
|
|
662
|
+
const char = raw[index];
|
|
663
|
+
if (isQuoteBoundary(raw, index, quote)) {
|
|
664
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (quote === null && !quoteIsEscaped(raw, index) && /[;&|]/.test(char ?? "")) return index;
|
|
668
|
+
}
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
function launcherPrefixLength(after, valueTaking, flagOptions) {
|
|
672
|
+
const tokens = boundedShellTokens(after);
|
|
673
|
+
let consumed = 0;
|
|
674
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
675
|
+
const tok = tokens[i] ?? { value: "", start: 0, end: 0 };
|
|
676
|
+
const token = tok.value;
|
|
677
|
+
const separator = firstUnquotedShellSeparator(after, tok.start, tok.end);
|
|
678
|
+
if (separator !== void 0) return consumed || separator;
|
|
679
|
+
if (token === "--") return tok.end;
|
|
680
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
|
|
681
|
+
consumed = tok.end;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (token === "-" || !token.startsWith("-")) break;
|
|
685
|
+
const optionName = token.split("=", 1)[0] ?? token;
|
|
686
|
+
const combinedValueOption = [...valueTaking].find(
|
|
687
|
+
(option) => option.length === 2 && token.startsWith(option) && token !== option
|
|
688
|
+
);
|
|
689
|
+
const combinedShortValueOption = token.startsWith("-") && !token.startsWith("--") ? [...valueTaking].find(
|
|
690
|
+
(option) => option.length === 2 && token.includes(option[1] ?? "", 2)
|
|
691
|
+
) : void 0;
|
|
692
|
+
const hasAttachedValue = token.includes("=") || combinedValueOption !== void 0;
|
|
693
|
+
if (!hasAttachedValue && (valueTaking.has(optionName) || combinedShortValueOption !== void 0)) {
|
|
694
|
+
if (i + 1 >= tokens.length) return flagOptions ? void 0 : tok.end;
|
|
695
|
+
const valueToken = tokens[i + 1] ?? tok;
|
|
696
|
+
const valueSeparator = firstUnquotedShellSeparator(after, valueToken.start, valueToken.end);
|
|
697
|
+
if (valueSeparator !== void 0) return consumed || valueSeparator;
|
|
698
|
+
consumed = valueToken.end;
|
|
699
|
+
i += 1;
|
|
700
|
+
} else {
|
|
701
|
+
const knownFlag = flagOptions?.has(optionName) || flagOptions !== void 0 && /^-[i0v]+$/.test(token);
|
|
702
|
+
const knownAttachedValue = hasAttachedValue && (valueTaking.has(optionName) || combinedValueOption !== void 0);
|
|
703
|
+
if (flagOptions && !knownFlag && !knownAttachedValue) return void 0;
|
|
704
|
+
consumed = tok.end;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return consumed;
|
|
708
|
+
}
|
|
709
|
+
function stripLauncherAtBoundary(command, launcher, valueTaking) {
|
|
710
|
+
const pattern = new RegExp(
|
|
711
|
+
`(^|[;&|\\r\\n]\\s*|\\(\\s*|\`\\s*)(?:[^\\s;&|(){}]+[\\\\/])?${launcher}\\b`,
|
|
712
|
+
"i"
|
|
713
|
+
);
|
|
714
|
+
const match = pattern.exec(command);
|
|
715
|
+
if (!match) return command;
|
|
716
|
+
const boundary = match[1] ?? "";
|
|
717
|
+
const afterStart = match.index + match[0].length;
|
|
718
|
+
const after = command.slice(afterStart);
|
|
719
|
+
const consumed = launcherPrefixLength(
|
|
720
|
+
after,
|
|
721
|
+
valueTaking,
|
|
722
|
+
launcher === "env" ? ENV_FLAG_OPTIONS : void 0
|
|
723
|
+
);
|
|
724
|
+
if (consumed === void 0) {
|
|
725
|
+
return `${command.slice(0, match.index)}${boundary}rm -rf **`;
|
|
726
|
+
}
|
|
727
|
+
return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
|
|
728
|
+
}
|
|
729
|
+
function commandRecursivelyDeletes(command) {
|
|
730
|
+
const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
731
|
+
if (/(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b[^;&|)`]*(?:-delete\b|-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)/i.test(
|
|
732
|
+
stripped
|
|
733
|
+
)) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
737
|
+
let match = destructive.exec(stripped);
|
|
738
|
+
while (match !== null) {
|
|
739
|
+
const tool = match[1]?.toLowerCase();
|
|
740
|
+
if (tool === "rmdir") return true;
|
|
741
|
+
const tokens = (match[2]?.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? []).map(
|
|
742
|
+
(arg) => arg.replace(/^["']|["']$/g, "")
|
|
743
|
+
);
|
|
744
|
+
let recursive = false;
|
|
745
|
+
for (const token of tokens) {
|
|
746
|
+
if (token === "--") break;
|
|
747
|
+
if (tool === "rm") {
|
|
748
|
+
if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
|
|
749
|
+
} else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
|
|
750
|
+
recursive = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (recursive) return true;
|
|
754
|
+
match = destructive.exec(stripped);
|
|
755
|
+
}
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
function quoteIsEscaped(command, index) {
|
|
759
|
+
let backslashes = 0;
|
|
760
|
+
for (let cursor = index - 1; cursor >= 0 && command[cursor] === "\\"; cursor -= 1) {
|
|
761
|
+
backslashes += 1;
|
|
762
|
+
}
|
|
763
|
+
return backslashes % 2 === 1;
|
|
764
|
+
}
|
|
765
|
+
function isQuoteBoundary(command, index, activeQuote) {
|
|
766
|
+
const char = command[index];
|
|
767
|
+
if (char !== "'" && char !== '"') return false;
|
|
768
|
+
if (activeQuote === "'") return char === "'";
|
|
769
|
+
if (activeQuote !== null && char !== activeQuote) return false;
|
|
770
|
+
return !quoteIsEscaped(command, index);
|
|
771
|
+
}
|
|
772
|
+
function executableCommandSubstitutions(command) {
|
|
773
|
+
const bodies = [];
|
|
774
|
+
let outerQuote = null;
|
|
775
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
776
|
+
const char = command[index];
|
|
777
|
+
if (isQuoteBoundary(command, index, outerQuote)) {
|
|
778
|
+
outerQuote = outerQuote === char ? null : char === "'" ? "'" : '"';
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (outerQuote === "'" || quoteIsEscaped(command, index)) continue;
|
|
782
|
+
if (char === "`") {
|
|
783
|
+
let end2 = index + 1;
|
|
784
|
+
while (end2 < command.length && (command[end2] !== "`" || quoteIsEscaped(command, end2))) {
|
|
785
|
+
end2 += 1;
|
|
786
|
+
}
|
|
787
|
+
if (end2 < command.length) {
|
|
788
|
+
bodies.push(command.slice(index + 1, end2));
|
|
789
|
+
index = end2;
|
|
790
|
+
}
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const commandSubstitution = char === "$" && command[index + 1] === "(";
|
|
794
|
+
const processSubstitution = (char === ">" || char === "<") && command[index + 1] === "(";
|
|
795
|
+
if (!commandSubstitution && !processSubstitution || // Arithmetic expansion evaluates an expression; it does not execute its
|
|
796
|
+
// contents as a shell command substitution.
|
|
797
|
+
commandSubstitution && command[index + 2] === "(")
|
|
798
|
+
continue;
|
|
799
|
+
let depth = 1;
|
|
800
|
+
let innerQuote = null;
|
|
801
|
+
let end = index + 2;
|
|
802
|
+
for (; end < command.length; end += 1) {
|
|
803
|
+
const innerChar = command[end];
|
|
804
|
+
if (isQuoteBoundary(command, end, innerQuote)) {
|
|
805
|
+
innerQuote = innerQuote === innerChar ? null : innerChar === "'" ? "'" : '"';
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (innerQuote !== null) continue;
|
|
809
|
+
if ((innerChar === "(" || innerChar === ")") && quoteIsEscaped(command, end)) continue;
|
|
810
|
+
if (innerChar === "(") depth += 1;
|
|
811
|
+
else if (innerChar === ")") {
|
|
812
|
+
depth -= 1;
|
|
813
|
+
if (depth === 0) break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (depth === 0) {
|
|
817
|
+
bodies.push(command.slice(index + 2, end));
|
|
818
|
+
index = end;
|
|
819
|
+
} else {
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return bodies;
|
|
824
|
+
}
|
|
825
|
+
var MAX_DESTRUCTIVE_TARGET_DEPTH = 64;
|
|
826
|
+
function heredocDelimiterOnLine(line) {
|
|
827
|
+
let quote = null;
|
|
828
|
+
for (let index = 0; index < line.length - 1; index += 1) {
|
|
829
|
+
const char = line[index];
|
|
830
|
+
if (isQuoteBoundary(line, index, quote)) {
|
|
831
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (quote !== null || quoteIsEscaped(line, index) || char !== "<" || line[index - 1] === "<" || line[index + 1] !== "<" || line[index + 2] === "<")
|
|
835
|
+
continue;
|
|
836
|
+
let cursor = index + 2;
|
|
837
|
+
const stripTabs = line[cursor] === "-";
|
|
838
|
+
if (stripTabs) cursor += 1;
|
|
839
|
+
while (line[cursor] === " " || line[cursor] === " ") cursor += 1;
|
|
840
|
+
let delimiter = "";
|
|
841
|
+
let delimiterQuote = null;
|
|
842
|
+
let quoted = false;
|
|
843
|
+
for (; cursor < line.length; cursor += 1) {
|
|
844
|
+
const delimiterChar = line[cursor] ?? "";
|
|
845
|
+
if (delimiterQuote !== null) {
|
|
846
|
+
if (delimiterChar === delimiterQuote && !quoteIsEscaped(line, cursor)) {
|
|
847
|
+
delimiterQuote = null;
|
|
848
|
+
quoted = true;
|
|
849
|
+
} else if (delimiterChar === "\\" && delimiterQuote === '"' && cursor + 1 < line.length) {
|
|
850
|
+
quoted = true;
|
|
851
|
+
cursor += 1;
|
|
852
|
+
delimiter += line[cursor] ?? "";
|
|
853
|
+
} else {
|
|
854
|
+
delimiter += delimiterChar;
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (delimiterChar === "'" || delimiterChar === '"') {
|
|
859
|
+
delimiterQuote = delimiterChar;
|
|
860
|
+
quoted = true;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (delimiterChar === "\\" && cursor + 1 < line.length) {
|
|
864
|
+
quoted = true;
|
|
865
|
+
cursor += 1;
|
|
866
|
+
delimiter += line[cursor] ?? "";
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (/\s|[;&|<>]/.test(delimiterChar)) break;
|
|
870
|
+
delimiter += delimiterChar;
|
|
871
|
+
}
|
|
872
|
+
return delimiter.length > 0 ? { delimiter, start: index, end: cursor, quoted, stripTabs } : null;
|
|
873
|
+
}
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
function commandSegmentBeforeHeredoc(prefix) {
|
|
877
|
+
let segmentStart = 0;
|
|
878
|
+
let quote = null;
|
|
879
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
880
|
+
const char = prefix[index];
|
|
881
|
+
if (isQuoteBoundary(prefix, index, quote)) {
|
|
882
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (quote === null && !quoteIsEscaped(prefix, index) && /[;&|\r\n]/.test(char ?? "")) {
|
|
886
|
+
segmentStart = index + 1;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return prefix.slice(segmentStart).trim();
|
|
890
|
+
}
|
|
891
|
+
function maskNonExecutingHeredocBodies(command) {
|
|
892
|
+
const lines = command.split(/(?<=\n)/);
|
|
893
|
+
let heredoc = null;
|
|
894
|
+
const maskBody = (start, end, quoted) => {
|
|
895
|
+
const body = lines.slice(start, end).join("");
|
|
896
|
+
const substitutions = quoted ? [] : executableCommandSubstitutions(body);
|
|
897
|
+
for (let index = start; index < end; index += 1) {
|
|
898
|
+
const line = lines[index] ?? "";
|
|
899
|
+
lines[index] = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
900
|
+
}
|
|
901
|
+
if (substitutions.length > 0 && start < end)
|
|
902
|
+
lines[start] = `${substitutions.join(";")}${lines[start] ?? ""}`;
|
|
903
|
+
};
|
|
904
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
905
|
+
const line = lines[index] ?? "";
|
|
906
|
+
if (heredoc !== null) {
|
|
907
|
+
const content = line.replace(/\r?\n$/, "");
|
|
908
|
+
const terminator = heredoc.stripTabs ? content.replace(/^\t+/, "") : content;
|
|
909
|
+
if (terminator === heredoc.delimiter) {
|
|
910
|
+
maskBody(heredoc.bodyStart, index, heredoc.quoted);
|
|
911
|
+
heredoc = null;
|
|
912
|
+
}
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const marker = heredocDelimiterOnLine(line);
|
|
916
|
+
if (!marker) continue;
|
|
917
|
+
const prefix = line.slice(0, marker.start).trim();
|
|
918
|
+
const suffix = line.slice(marker.end).trim();
|
|
919
|
+
const owner = stripTransparentLaunchers(commandSegmentBeforeHeredoc(prefix));
|
|
920
|
+
const commandBeforeMarker = `${lines.slice(0, index).join("")}${prefix}`;
|
|
921
|
+
const whileReadLoopOwnsHeredoc = /^done$/i.test(owner) && /(?:^|[;&|\r\n])\s*while\s+read(?:\s|$)[\s\S]*\bdone\s*$/i.test(commandBeforeMarker);
|
|
922
|
+
const receivesDataWithoutExecuting = /^(?:[^\s;&|]+[\\/])?(?:cat|tee)(?:\s|$)/i.test(owner) || /^(?:while\s+)?read(?:\s|$)/i.test(owner) || whileReadLoopOwnsHeredoc;
|
|
923
|
+
const redirectedFileMatch = /^(?:>>|>\||>)\s*("[^"]+"|'[^']+'|[^\s;&|<>]+)/.exec(suffix);
|
|
924
|
+
const redirectedFile = redirectedFileMatch?.[1]?.replace(/^['"]|['"]$/g, "");
|
|
925
|
+
const remainingCommand = lines.slice(index + 1).join("");
|
|
926
|
+
const executionSearch = `${suffix}
|
|
927
|
+
${remainingCommand}`;
|
|
928
|
+
const normalizedRedirectedFile = redirectedFile ? normalizePath(redirectedFile) : void 0;
|
|
929
|
+
const executesRedirectedFile = normalizedRedirectedFile !== void 0 && boundedShellTokens(executionSearch).some((token) => {
|
|
930
|
+
if (normalizePath(token.value) !== normalizedRedirectedFile) return false;
|
|
931
|
+
const segmentStart = Math.max(
|
|
932
|
+
executionSearch.lastIndexOf(";", token.start - 1),
|
|
933
|
+
executionSearch.lastIndexOf("&", token.start - 1),
|
|
934
|
+
executionSearch.lastIndexOf("|", token.start - 1),
|
|
935
|
+
executionSearch.lastIndexOf("\n", token.start - 1),
|
|
936
|
+
executionSearch.lastIndexOf("\r", token.start - 1)
|
|
937
|
+
);
|
|
938
|
+
const segmentTokens = shellTokens(executionSearch.slice(segmentStart + 1, token.start));
|
|
939
|
+
const previous = segmentTokens.at(-1)?.toLowerCase();
|
|
940
|
+
return previous === void 0 || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(previous);
|
|
941
|
+
});
|
|
942
|
+
const processSubstitution = /^(?:>>|>\||>)\s*>\s*\(\s*([^)]*)/.exec(suffix);
|
|
943
|
+
const processCommand = boundedShellTokens(
|
|
944
|
+
stripTransparentLaunchers(processSubstitution?.[1]?.trim() ?? "")
|
|
945
|
+
)[0]?.value.replace(/^.*[\\/]/, "").toLowerCase();
|
|
946
|
+
const executesBody = /^(?:\||;|&|\(|\{)/.test(suffix) || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(processCommand ?? "") || executesRedirectedFile;
|
|
947
|
+
if (!receivesDataWithoutExecuting || executesBody) continue;
|
|
948
|
+
heredoc = { ...marker, bodyStart: index + 1 };
|
|
949
|
+
}
|
|
950
|
+
if (heredoc !== null) maskBody(heredoc.bodyStart, lines.length, heredoc.quoted);
|
|
951
|
+
return lines.join("");
|
|
952
|
+
}
|
|
67
953
|
function destructiveTargets(command) {
|
|
954
|
+
return destructiveTargetsAtDepth(command, 0);
|
|
955
|
+
}
|
|
956
|
+
function destructiveTargetsAtDepth(command, depth) {
|
|
957
|
+
if (depth >= MAX_DESTRUCTIVE_TARGET_DEPTH) return ["**"];
|
|
958
|
+
const normalizedCommand = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
|
|
68
959
|
const targets = [];
|
|
69
|
-
const
|
|
70
|
-
let
|
|
960
|
+
const quotedIndexes = new Uint8Array(normalizedCommand.length);
|
|
961
|
+
let activeQuote = null;
|
|
962
|
+
for (let index = 0; index < normalizedCommand.length; index += 1) {
|
|
963
|
+
const char = normalizedCommand[index];
|
|
964
|
+
if (isQuoteBoundary(normalizedCommand, index, activeQuote)) {
|
|
965
|
+
quotedIndexes[index] = 1;
|
|
966
|
+
activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
|
|
967
|
+
} else if (activeQuote !== null) {
|
|
968
|
+
quotedIndexes[index] = 1;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const tokenIsQuoted = (match, token) => {
|
|
972
|
+
const offset = match[0].toLowerCase().indexOf(token.toLowerCase());
|
|
973
|
+
return offset >= 0 && quotedIndexes[match.index + offset] === 1;
|
|
974
|
+
};
|
|
975
|
+
const shellOperandTokens = (raw) => {
|
|
976
|
+
const tokens = shellTokens(raw);
|
|
977
|
+
const redirectIndex = tokens.findIndex((token) => /^(?:\d*(?:<>|>>?|<)|&>>?)/.test(token));
|
|
978
|
+
return redirectIndex === -1 ? tokens : tokens.slice(0, redirectIndex);
|
|
979
|
+
};
|
|
980
|
+
const shellArgs = (raw) => {
|
|
981
|
+
const lastNonWhitespace = raw.search(/\s*$/) - 1;
|
|
982
|
+
let quote2 = null;
|
|
983
|
+
for (let index = 0; index < lastNonWhitespace; index += 1) {
|
|
984
|
+
if (isQuoteBoundary(raw, index, quote2)) {
|
|
985
|
+
const char = raw[index];
|
|
986
|
+
quote2 = quote2 === char ? null : char === "'" ? "'" : '"';
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const args = raw[lastNonWhitespace] === ")" && quote2 === null && !quoteIsEscaped(raw, lastNonWhitespace) ? raw.slice(0, lastNonWhitespace) : raw;
|
|
990
|
+
return shellOperandTokens(args).filter((arg) => arg.length > 0 && !arg.startsWith("-"));
|
|
991
|
+
};
|
|
992
|
+
for (const body of executableCommandSubstitutions(normalizedCommand)) {
|
|
993
|
+
let executableBody = body.trim();
|
|
994
|
+
while (executableBody.startsWith("(") && executableBody.endsWith(")")) {
|
|
995
|
+
executableBody = executableBody.slice(1, -1).trim();
|
|
996
|
+
}
|
|
997
|
+
targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
|
|
998
|
+
}
|
|
999
|
+
const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
|
|
1000
|
+
let m = destructive.exec(normalizedCommand);
|
|
71
1001
|
while (m !== null) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1002
|
+
if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
|
|
1003
|
+
m = destructive.exec(normalizedCommand);
|
|
1004
|
+
}
|
|
1005
|
+
const copy = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(cp|install)\s+([^;&|\r\n]+)/gi;
|
|
1006
|
+
let c = copy.exec(normalizedCommand);
|
|
1007
|
+
while (c !== null) {
|
|
1008
|
+
if (tokenIsQuoted(c, c[1] ?? "")) {
|
|
1009
|
+
c = copy.exec(normalizedCommand);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const tokens = shellTokens(c[2] ?? "");
|
|
1013
|
+
let destination;
|
|
1014
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1015
|
+
const token = tokens[index];
|
|
1016
|
+
if (token === "-t" || token === "--target-directory") {
|
|
1017
|
+
destination = tokens[index + 1];
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
if (token?.startsWith("--target-directory=")) {
|
|
1021
|
+
destination = token.slice("--target-directory=".length);
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
destination ??= tokens.filter((arg) => arg.length > 0 && !arg.startsWith("-")).at(-1);
|
|
1026
|
+
if (destination) targets.push(destination);
|
|
1027
|
+
c = copy.exec(normalizedCommand);
|
|
1028
|
+
}
|
|
1029
|
+
const tee = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)/gi;
|
|
1030
|
+
let t = tee.exec(normalizedCommand);
|
|
1031
|
+
while (t !== null) {
|
|
1032
|
+
if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
|
|
1033
|
+
t = tee.exec(normalizedCommand);
|
|
1034
|
+
}
|
|
1035
|
+
const dd = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)/gi;
|
|
1036
|
+
let d = dd.exec(normalizedCommand);
|
|
1037
|
+
while (d !== null) {
|
|
1038
|
+
if (!tokenIsQuoted(d, d[1] ?? "")) {
|
|
1039
|
+
const outputMatch = /(?:^|\s)of=("[^"]*"|'[^']*'|[^\s]+)/i.exec(d[2] ?? "");
|
|
1040
|
+
const output = outputMatch?.[1]?.replace(/^['"]|['"]$/g, "");
|
|
1041
|
+
if (output) targets.push(output);
|
|
1042
|
+
}
|
|
1043
|
+
d = dd.exec(normalizedCommand);
|
|
1044
|
+
}
|
|
1045
|
+
const overwrite = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(sed|ln)\s+([^;&|\r\n]+)/gi;
|
|
1046
|
+
let o = overwrite.exec(normalizedCommand);
|
|
1047
|
+
while (o !== null) {
|
|
1048
|
+
const rawArgs = o[2] ?? "";
|
|
1049
|
+
const tool = o[1]?.toLowerCase();
|
|
1050
|
+
if (tool && tokenIsQuoted(o, tool)) {
|
|
1051
|
+
o = overwrite.exec(normalizedCommand);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (tool === "sed" && /(?:^|\s)-i(?:[^\s]*)?(?:\s|$)/.test(rawArgs)) {
|
|
1055
|
+
const args = shellArgs(rawArgs);
|
|
1056
|
+
targets.push(...args.slice(1));
|
|
1057
|
+
} else if (tool === "ln" && /(?:^|\s)-[^\s]*f[^\s]*(?:\s|$)/.test(rawArgs)) {
|
|
1058
|
+
const destination = shellArgs(rawArgs).at(-1);
|
|
1059
|
+
if (destination) targets.push(destination);
|
|
1060
|
+
}
|
|
1061
|
+
o = overwrite.exec(normalizedCommand);
|
|
1062
|
+
}
|
|
1063
|
+
const xargsPipeline = /\b(?:echo|printf)\s+([^|]+)\|\s*xargs(?:\s+-[^\s]+)*\s+(?:sudo\s+)?(?:rm|rmdir|del|unlink|truncate|shred)\b/gi;
|
|
1064
|
+
let x = xargsPipeline.exec(normalizedCommand);
|
|
1065
|
+
while (x !== null) {
|
|
1066
|
+
if (!tokenIsQuoted(x, "xargs")) targets.push(...shellArgs(x[1] ?? ""));
|
|
1067
|
+
x = xargsPipeline.exec(normalizedCommand);
|
|
1068
|
+
}
|
|
1069
|
+
for (const rawArguments of gitInvocationArguments(normalizedCommand)) {
|
|
1070
|
+
const invocationTokens = shellOperandTokens(rawArguments);
|
|
1071
|
+
const commandIndex = gitSubcommandIndex(invocationTokens);
|
|
1072
|
+
if (commandIndex >= 0) {
|
|
1073
|
+
let gitCwd = "";
|
|
1074
|
+
let workTree;
|
|
1075
|
+
for (let index = 0; index < commandIndex; index += 1) {
|
|
1076
|
+
const token = invocationTokens[index] ?? "";
|
|
1077
|
+
if (token === "--") continue;
|
|
1078
|
+
const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
|
|
1079
|
+
let optionValue = token.includes("=") ? token.slice(token.indexOf("=") + 1) : void 0;
|
|
1080
|
+
if (token.startsWith("-C") && token !== "-C") optionValue = token.slice(2);
|
|
1081
|
+
if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && optionValue === void 0) {
|
|
1082
|
+
optionValue = invocationTokens[index + 1];
|
|
1083
|
+
index += 1;
|
|
1084
|
+
}
|
|
1085
|
+
if (optionName === "-C" && optionValue)
|
|
1086
|
+
gitCwd = resolveTargetPath(optionValue, gitCwd || void 0);
|
|
1087
|
+
if (optionName === "--work-tree" && optionValue) workTree = optionValue;
|
|
1088
|
+
}
|
|
1089
|
+
const subcommand = invocationTokens[commandIndex]?.toLowerCase();
|
|
1090
|
+
const tokens = invocationTokens.slice(commandIndex + 1);
|
|
1091
|
+
const gitTreeRoot = workTree ? resolveTargetPath(workTree, gitCwd || void 0) : gitCwd;
|
|
1092
|
+
const gitTarget = (target) => {
|
|
1093
|
+
const resolved = resolveTargetPath(target, gitTreeRoot || void 0);
|
|
1094
|
+
return target.endsWith("/") && !resolved.endsWith("/") ? `${resolved}/` : resolved;
|
|
1095
|
+
};
|
|
1096
|
+
const gitPathspecTargets = (pathspec) => {
|
|
1097
|
+
if (isUnresolvedPathScope(pathspec) || !isDirectoryAmbiguousPath(pathspec)) {
|
|
1098
|
+
return [gitTarget(pathspec)];
|
|
1099
|
+
}
|
|
1100
|
+
return [gitTarget(`${pathspec.replace(/\/$/, "")}/**`)];
|
|
1101
|
+
};
|
|
1102
|
+
const fileSourcedPathspecScope = (pathspecTokens) => {
|
|
1103
|
+
const usesPathspecFile = pathspecTokens.some(
|
|
1104
|
+
(token, index) => token.startsWith("--pathspec-from-file=") || token === "--pathspec-from-file" && pathspecTokens[index + 1] !== void 0
|
|
1105
|
+
);
|
|
1106
|
+
return usesPathspecFile ? gitTarget("**") : void 0;
|
|
1107
|
+
};
|
|
1108
|
+
if (subcommand === "clean") {
|
|
1109
|
+
const dryRun = tokens.some((t2) => t2 === "--dry-run" || /^-[^-]*n/.test(t2));
|
|
1110
|
+
if (!dryRun) {
|
|
1111
|
+
const operands = [];
|
|
1112
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1113
|
+
const t2 = tokens[i] ?? "";
|
|
1114
|
+
if (t2 === "-e" || t2 === "--exclude" || t2 === "--exclude-from") {
|
|
1115
|
+
i += 1;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (t2.startsWith("--exclude=") || t2.startsWith("--exclude-from=") || /^-e.+/.test(t2) || t2.startsWith("-"))
|
|
1119
|
+
continue;
|
|
1120
|
+
operands.push(t2);
|
|
1121
|
+
}
|
|
1122
|
+
targets.push(
|
|
1123
|
+
...(operands.length ? operands : ["."]).map(
|
|
1124
|
+
(operand) => gitTarget(operand.endsWith("/") ? `${operand}**` : operand)
|
|
1125
|
+
)
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
} else if (subcommand === "rm") {
|
|
1129
|
+
const writes = !tokens.includes("--cached") || tokens.includes("--worktree");
|
|
1130
|
+
if (writes) {
|
|
1131
|
+
const recursive = tokens.some((t2) => t2 === "--recursive" || /^-[^-]*r/.test(t2));
|
|
1132
|
+
const operands = tokens.filter((t2) => t2 !== "--" && !t2.startsWith("-"));
|
|
1133
|
+
targets.push(...operands.map((o2) => gitTarget(recursive ? `${o2}/**` : o2)));
|
|
1134
|
+
const unresolvedFileScope = fileSourcedPathspecScope(tokens);
|
|
1135
|
+
if (unresolvedFileScope) targets.push(unresolvedFileScope);
|
|
1136
|
+
}
|
|
1137
|
+
} else if (subcommand === "restore") {
|
|
1138
|
+
const staged = tokens.includes("--staged") || tokens.some((t2) => /^-[^-]*S/.test(t2));
|
|
1139
|
+
const worktree = tokens.includes("--worktree") || tokens.some((t2) => /^-[^-]*W/.test(t2));
|
|
1140
|
+
if (!staged || worktree) {
|
|
1141
|
+
const operands = [];
|
|
1142
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1143
|
+
const token = tokens[index] ?? "";
|
|
1144
|
+
if (token === "--source" || token === "-s") {
|
|
1145
|
+
index += 1;
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (token.startsWith("--source=") || /^-s.+/.test(token) && token !== "--staged") {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (token !== "--" && !token.startsWith("-")) operands.push(token);
|
|
1152
|
+
}
|
|
1153
|
+
targets.push(...operands.flatMap(gitPathspecTargets));
|
|
1154
|
+
const unresolvedFileScope = fileSourcedPathspecScope(tokens);
|
|
1155
|
+
if (unresolvedFileScope) targets.push(unresolvedFileScope);
|
|
1156
|
+
}
|
|
1157
|
+
} else if (subcommand === "checkout" || subcommand === "switch") {
|
|
1158
|
+
const separator = tokens.indexOf("--");
|
|
1159
|
+
if (separator >= 0) {
|
|
1160
|
+
const paths = tokens.slice(separator + 1);
|
|
1161
|
+
targets.push(...paths.flatMap(gitPathspecTargets));
|
|
1162
|
+
} else {
|
|
1163
|
+
const createFlags = subcommand === "checkout" ? /* @__PURE__ */ new Set(["-b", "-B", "--branch", "--orphan"]) : /* @__PURE__ */ new Set(["-c", "-C", "--create", "--force-create"]);
|
|
1164
|
+
const createIndex = tokens.findIndex((token) => createFlags.has(token));
|
|
1165
|
+
const branchNameIndex = createIndex >= 0 ? createIndex + 1 : -1;
|
|
1166
|
+
const operands = tokens.filter(
|
|
1167
|
+
(token, index) => !token.startsWith("-") && index !== branchNameIndex
|
|
1168
|
+
);
|
|
1169
|
+
if (operands.length > 0) targets.push(gitTarget("."));
|
|
1170
|
+
}
|
|
1171
|
+
} else if (subcommand === "stash") {
|
|
1172
|
+
const action = tokens.find((token) => !token.startsWith("-"))?.toLowerCase();
|
|
1173
|
+
if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) {
|
|
1174
|
+
targets.push(gitTarget("."));
|
|
1175
|
+
}
|
|
1176
|
+
} else if (subcommand === "reset" && tokens.some((t2) => t2 === "--hard" || t2 === "--merge" || t2 === "--keep"))
|
|
1177
|
+
targets.push(gitTarget("."));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const findDelete = /(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b([^;&|)`]*(?:\s-delete(?:\s|$)|\s-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)[^;&|)`]*)/gi;
|
|
1181
|
+
let f = findDelete.exec(normalizedCommand);
|
|
1182
|
+
while (f !== null) {
|
|
1183
|
+
const tokens = shellTokens(f[1] ?? "");
|
|
1184
|
+
const expressionIndex = tokens.findIndex(
|
|
1185
|
+
(token) => token.startsWith("-") || token === "!" || token === "("
|
|
1186
|
+
);
|
|
1187
|
+
const roots = (expressionIndex === -1 ? tokens : tokens.slice(0, expressionIndex)).filter(
|
|
1188
|
+
(token) => token.length > 0
|
|
1189
|
+
);
|
|
1190
|
+
targets.push(...roots.length > 0 ? roots : ["."]);
|
|
1191
|
+
f = findDelete.exec(normalizedCommand);
|
|
1192
|
+
}
|
|
1193
|
+
const shellWrapper = /\b(?:ba|z|k)?sh\s+-c\s+(['"])(.*?)\1/gi;
|
|
1194
|
+
let w = shellWrapper.exec(normalizedCommand);
|
|
1195
|
+
while (w !== null) {
|
|
1196
|
+
if (w[2] && !tokenIsQuoted(w, w[0].split(/\s/)[0] ?? "")) {
|
|
1197
|
+
targets.push(...destructiveTargetsAtDepth(w[2], depth + 1));
|
|
1198
|
+
}
|
|
1199
|
+
w = shellWrapper.exec(normalizedCommand);
|
|
1200
|
+
}
|
|
1201
|
+
let quote = null;
|
|
1202
|
+
for (let index = 0; index < normalizedCommand.length; index += 1) {
|
|
1203
|
+
const char = normalizedCommand[index];
|
|
1204
|
+
if (char === "\n") {
|
|
1205
|
+
quote = null;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (isQuoteBoundary(normalizedCommand, index, quote)) {
|
|
1209
|
+
quote = quote === char ? null : char === "'" ? "'" : '"';
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (quote !== null || char !== ">") continue;
|
|
1213
|
+
const redirectsBothStreams = normalizedCommand[index + 1] === "&";
|
|
1214
|
+
const overridesNoclobber = normalizedCommand[index + 1] === "|";
|
|
1215
|
+
if (redirectsBothStreams || overridesNoclobber || normalizedCommand[index + 1] === ">") {
|
|
1216
|
+
index += 1;
|
|
1217
|
+
}
|
|
1218
|
+
while (normalizedCommand[index + 1] === " " || normalizedCommand[index + 1] === " ") {
|
|
1219
|
+
index += 1;
|
|
1220
|
+
}
|
|
1221
|
+
const targetQuote = normalizedCommand[index + 1];
|
|
1222
|
+
let end = index + 1;
|
|
1223
|
+
let target;
|
|
1224
|
+
if (targetQuote === "'" || targetQuote === '"') {
|
|
1225
|
+
end = normalizedCommand.indexOf(targetQuote, index + 2);
|
|
1226
|
+
if (end === -1) continue;
|
|
1227
|
+
target = normalizedCommand.slice(index + 2, end);
|
|
1228
|
+
} else {
|
|
1229
|
+
while (end < normalizedCommand.length && !/[\s;&|>()]/.test(normalizedCommand[end] ?? "")) {
|
|
1230
|
+
end += 1;
|
|
1231
|
+
}
|
|
1232
|
+
target = normalizedCommand.slice(index + 1, end);
|
|
1233
|
+
}
|
|
1234
|
+
if (target && !(redirectsBothStreams && (/^\d+$/.test(target) || target === "-"))) {
|
|
81
1235
|
targets.push(target);
|
|
82
1236
|
}
|
|
83
|
-
|
|
1237
|
+
index = end;
|
|
84
1238
|
}
|
|
85
|
-
return
|
|
1239
|
+
return [
|
|
1240
|
+
...new Set(
|
|
1241
|
+
targets.map((target) => target.replace(/^['"]|['"]$/g, "")).filter((target) => target !== "/dev/null" && target.toLowerCase() !== "nul")
|
|
1242
|
+
)
|
|
1243
|
+
];
|
|
86
1244
|
}
|
|
87
1245
|
var plugin = {
|
|
88
1246
|
name: "path-guard",
|
|
@@ -104,7 +1262,7 @@ var plugin = {
|
|
|
104
1262
|
protect: {
|
|
105
1263
|
type: "array",
|
|
106
1264
|
items: { type: "string" },
|
|
107
|
-
description: "Glob patterns for protected paths. Replaces the default set when present."
|
|
1265
|
+
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."
|
|
108
1266
|
},
|
|
109
1267
|
allow: {
|
|
110
1268
|
type: "array",
|
|
@@ -129,21 +1287,23 @@ var plugin = {
|
|
|
129
1287
|
const cfg = readConfig(api.config.extensions?.["path-guard"]);
|
|
130
1288
|
const protectRes = cfg.protect.map(compilePathGlob);
|
|
131
1289
|
const allowRes = cfg.allow.map(compilePathGlob);
|
|
132
|
-
const verdict = (path, tool, operation) => {
|
|
1290
|
+
const verdict = (path, tool, operation, isScope = false) => {
|
|
1291
|
+
const subject = isScope ? `write scope "${path}" may include a protected path \u2014 narrow it or add an \`allow\` glob` : `"${path}" is a protected path`;
|
|
1292
|
+
const matchContext = isScope ? 'its unresolved scope overlaps config.extensions["path-guard"].protect' : 'matched by config.extensions["path-guard"].protect';
|
|
133
1293
|
if (cfg.mode === "block") {
|
|
134
1294
|
state.blocks += 1;
|
|
135
1295
|
state.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
|
|
136
1296
|
api.metrics.counter("blocks");
|
|
137
1297
|
return {
|
|
138
1298
|
decision: "block",
|
|
139
|
-
reason: `path-guard:
|
|
1299
|
+
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".`
|
|
140
1300
|
};
|
|
141
1301
|
}
|
|
142
1302
|
state.warns += 1;
|
|
143
1303
|
api.metrics.counter("warns");
|
|
144
1304
|
return {
|
|
145
1305
|
decision: "allow",
|
|
146
|
-
additionalContext: `path-guard (warn mode):
|
|
1306
|
+
additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
|
|
147
1307
|
};
|
|
148
1308
|
};
|
|
149
1309
|
const hook = (input) => {
|
|
@@ -151,28 +1311,56 @@ var plugin = {
|
|
|
151
1311
|
state.invocations += 1;
|
|
152
1312
|
const toolName = input.toolName ?? "";
|
|
153
1313
|
const ti = input.toolInput ?? {};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1314
|
+
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
1315
|
+
const commandArgs = Array.isArray(ti["args"]) ? ti["args"].filter((arg) => typeof arg === "string") : [];
|
|
1316
|
+
const commandForInspection = [
|
|
1317
|
+
command,
|
|
1318
|
+
...commandArgs.map((arg) => /^[\w./:@%+=,-]+$/.test(arg) ? arg : JSON.stringify(arg))
|
|
1319
|
+
].join(" ");
|
|
1320
|
+
if (command && executesShell({
|
|
1321
|
+
toolName: input.toolName,
|
|
1322
|
+
toolInput: ti,
|
|
1323
|
+
toolCapabilities: input.toolCapabilities,
|
|
1324
|
+
toolMutating: input.toolMutating
|
|
1325
|
+
})) {
|
|
1326
|
+
const shellTargets = destructiveTargets(commandForInspection);
|
|
1327
|
+
const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
|
|
1328
|
+
const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
|
|
1329
|
+
const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
|
|
1330
|
+
for (const path of shellTargets) {
|
|
1331
|
+
const target = {
|
|
1332
|
+
path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
|
|
1333
|
+
kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
|
|
1334
|
+
};
|
|
1335
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
|
|
1336
|
+
const protectedShellTarget = targetIntersectsPatterns(target, cfg.protect, protectRes) || matchesAny(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes);
|
|
1337
|
+
if (protectedShellTarget) {
|
|
1338
|
+
return verdict(
|
|
1339
|
+
target.path,
|
|
1340
|
+
toolName,
|
|
1341
|
+
"destructive shell command",
|
|
1342
|
+
target.kind !== "file"
|
|
1343
|
+
);
|
|
170
1344
|
}
|
|
171
1345
|
}
|
|
1346
|
+
const writes = writesToDisk({ ...input, toolInput: ti });
|
|
1347
|
+
const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
|
|
1348
|
+
const needsImplicitWriteScope = writes && ((input.toolCapabilities?.some(
|
|
1349
|
+
(capability) => DISK_MUTATING_CAPABILITIES.has(capability)
|
|
1350
|
+
) ?? false) || (!input.toolCapabilities || input.toolCapabilities.length === 0) && LEGACY_WRITE_TOOLS.has(toolName));
|
|
1351
|
+
if (!writes || !hasStructuredTarget && !needsImplicitWriteScope) return;
|
|
1352
|
+
}
|
|
1353
|
+
if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
|
|
1354
|
+
const targets = pathsFromToolInput(ti, toolName, input.cwd);
|
|
1355
|
+
for (const target of targets) {
|
|
1356
|
+
if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
|
|
1357
|
+
if (targetIntersectsPatterns(target, cfg.protect, protectRes)) {
|
|
1358
|
+
return verdict(target.path, toolName, operationLabel(toolName), target.kind !== "file");
|
|
1359
|
+
}
|
|
172
1360
|
}
|
|
173
1361
|
return;
|
|
174
1362
|
};
|
|
175
|
-
state.hookUnregister = api.registerHook("PreToolUse", "
|
|
1363
|
+
state.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
|
|
176
1364
|
name: "path-guard",
|
|
177
1365
|
stage: "validate",
|
|
178
1366
|
failurePolicy: "closed",
|