@alex-dbs/coderuler 0.0.0-stage → 1.3.1
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/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, matchesGlob, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { TextDecoder } from 'node:util';
|
|
4
|
+
import { parseCsv } from '../scope/csv.js';
|
|
5
|
+
import { parseJson } from '../scope/json.js';
|
|
6
|
+
import { parseYaml } from '../scope/yaml.js';
|
|
7
|
+
const SKIPPED_ANY_DEPTH = new Set([
|
|
8
|
+
'.git',
|
|
9
|
+
'node_modules',
|
|
10
|
+
'.coderuler',
|
|
11
|
+
'old',
|
|
12
|
+
]);
|
|
13
|
+
const ROOT_GENERATED = new Set(['dist', 'coverage']);
|
|
14
|
+
const GLOB_MAGIC = /[*?[\]{}]/;
|
|
15
|
+
export function isReservedDirectory(name, rootLevel) {
|
|
16
|
+
return SKIPPED_ANY_DEPTH.has(name) || (rootLevel && ROOT_GENERATED.has(name));
|
|
17
|
+
}
|
|
18
|
+
function describe(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
function helperPath(root, input) {
|
|
22
|
+
if (input.length === 0)
|
|
23
|
+
throw new Error('Project path must not be empty');
|
|
24
|
+
if (isAbsolute(input)) {
|
|
25
|
+
throw new Error(`Project path ${input} must be relative to the project root`);
|
|
26
|
+
}
|
|
27
|
+
const absolute = resolve(root, input);
|
|
28
|
+
const relativePath = relative(root, absolute);
|
|
29
|
+
if (relativePath === '..' ||
|
|
30
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
31
|
+
isAbsolute(relativePath)) {
|
|
32
|
+
throw new Error(`Project path ${input} is outside the project root`);
|
|
33
|
+
}
|
|
34
|
+
return absolute;
|
|
35
|
+
}
|
|
36
|
+
async function lstatOrUndefined(absolute) {
|
|
37
|
+
try {
|
|
38
|
+
return await lstat(absolute, { bigint: true });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error.code;
|
|
42
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
43
|
+
return undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function traversablePath(root, filepath) {
|
|
48
|
+
const segments = filepath.split('/');
|
|
49
|
+
let absolute = root;
|
|
50
|
+
let missing = false;
|
|
51
|
+
for (const [index, segment] of segments.entries()) {
|
|
52
|
+
const ancestor = index < segments.length - 1;
|
|
53
|
+
if (!missing)
|
|
54
|
+
absolute = join(absolute, segment);
|
|
55
|
+
const stats = missing ? undefined : await lstatOrUndefined(absolute);
|
|
56
|
+
if (stats === undefined) {
|
|
57
|
+
missing = true;
|
|
58
|
+
if (ancestor && isReservedDirectory(segment, index === 0))
|
|
59
|
+
return undefined;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (stats.isSymbolicLink())
|
|
63
|
+
return undefined;
|
|
64
|
+
const actual = basename(await realpath(absolute));
|
|
65
|
+
segments[index] = actual;
|
|
66
|
+
if (stats.isDirectory() && isReservedDirectory(actual, index === 0))
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return segments.join('/');
|
|
70
|
+
}
|
|
71
|
+
async function assertPathBoundary(root, absolute, display) {
|
|
72
|
+
const relativePath = relative(root, absolute);
|
|
73
|
+
const segments = relativePath === '' ? [] : relativePath.split(sep);
|
|
74
|
+
let finalStats;
|
|
75
|
+
for (const [index, segment] of segments.entries()) {
|
|
76
|
+
const last = index === segments.length - 1;
|
|
77
|
+
const candidate = join(root, ...segments.slice(0, index + 1));
|
|
78
|
+
const stats = await lstatOrUndefined(candidate);
|
|
79
|
+
if (last)
|
|
80
|
+
finalStats = stats;
|
|
81
|
+
if (stats === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
if (!last && stats.isSymbolicLink()) {
|
|
84
|
+
throw new Error(`Project path ${display} passes through a symbolic link at ${segment}/`);
|
|
85
|
+
}
|
|
86
|
+
if (!stats.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
const actual = basename(await realpath(candidate));
|
|
89
|
+
if (isReservedDirectory(actual, index === 0)) {
|
|
90
|
+
throw new Error(last
|
|
91
|
+
? `Project path ${display} targets the reserved directory ${actual}/`
|
|
92
|
+
: `Project path ${display} is inside the reserved directory ${actual}/`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return finalStats;
|
|
96
|
+
}
|
|
97
|
+
async function assertPatternAllowed(root, pattern) {
|
|
98
|
+
if (pattern.length === 0)
|
|
99
|
+
throw new Error('Project glob pattern must not be empty');
|
|
100
|
+
if (isAbsolute(pattern)) {
|
|
101
|
+
throw new Error(`Project glob pattern ${pattern} must be relative to the project root`);
|
|
102
|
+
}
|
|
103
|
+
const segments = pattern.split('/');
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
if (segment === '..')
|
|
106
|
+
throw new Error(`Project glob pattern ${pattern} escapes the project root`);
|
|
107
|
+
}
|
|
108
|
+
for (const [index, segment] of segments.entries()) {
|
|
109
|
+
if (GLOB_MAGIC.test(segment))
|
|
110
|
+
break;
|
|
111
|
+
const candidate = join(root, ...segments.slice(0, index + 1));
|
|
112
|
+
if (candidate === root)
|
|
113
|
+
continue;
|
|
114
|
+
const stats = await lstatOrUndefined(candidate);
|
|
115
|
+
if (stats?.isSymbolicLink() === true)
|
|
116
|
+
break;
|
|
117
|
+
if (stats?.isDirectory() !== true)
|
|
118
|
+
continue;
|
|
119
|
+
const actual = basename(await realpath(candidate));
|
|
120
|
+
if (isReservedDirectory(actual, dirname(candidate) === root)) {
|
|
121
|
+
throw new Error(`Project glob pattern ${pattern} targets the reserved directory ${actual}/`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function literalDirectory(pattern) {
|
|
126
|
+
const segments = [];
|
|
127
|
+
for (const segment of pattern.split('/').slice(0, -1)) {
|
|
128
|
+
if (segment === '' || segment === '.')
|
|
129
|
+
continue;
|
|
130
|
+
if (!/^[\w .-]+$/.test(segment))
|
|
131
|
+
break;
|
|
132
|
+
segments.push(segment);
|
|
133
|
+
}
|
|
134
|
+
return segments.join('/');
|
|
135
|
+
}
|
|
136
|
+
async function walkInventory(directory, prefix, files, readDirectory) {
|
|
137
|
+
const entries = await readDirectory(directory);
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry.isSymbolicLink())
|
|
140
|
+
continue;
|
|
141
|
+
const relativePath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
if (isReservedDirectory(entry.name, prefix === '')) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
await walkInventory(join(directory, entry.name), relativePath, files, readDirectory);
|
|
147
|
+
}
|
|
148
|
+
else if (entry.isFile()) {
|
|
149
|
+
files.push({ path: relativePath, absolute: join(directory, entry.name) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function createProject(root, options = {}) {
|
|
154
|
+
const resolvedRoot = resolve(root);
|
|
155
|
+
const excludeFiles = new Set((options.excludeFiles ?? []).map((file) => resolve(file)));
|
|
156
|
+
const textCache = new Map();
|
|
157
|
+
let physicalRootPromise;
|
|
158
|
+
const directories = new Map();
|
|
159
|
+
const readDirectory = (directory) => {
|
|
160
|
+
let pending = directories.get(directory);
|
|
161
|
+
if (pending === undefined) {
|
|
162
|
+
pending = readdir(directory, { withFileTypes: true });
|
|
163
|
+
directories.set(directory, pending);
|
|
164
|
+
}
|
|
165
|
+
return pending;
|
|
166
|
+
};
|
|
167
|
+
async function readSource(absolute, display) {
|
|
168
|
+
const cached = textCache.get(absolute);
|
|
169
|
+
if (cached !== undefined)
|
|
170
|
+
return cached;
|
|
171
|
+
const pending = (async () => {
|
|
172
|
+
const inspected = await assertPathBoundary(resolvedRoot, absolute, display);
|
|
173
|
+
let stats;
|
|
174
|
+
try {
|
|
175
|
+
stats = inspected ?? await lstat(absolute);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new Error(`Project file ${display} could not be read: ${describe(error)}`, {
|
|
179
|
+
cause: error,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (stats.isSymbolicLink()) {
|
|
183
|
+
throw new Error(`Project path ${display} is a symbolic link; read the real file`);
|
|
184
|
+
}
|
|
185
|
+
if (!stats.isFile())
|
|
186
|
+
throw new Error(`Project file ${display} is not a file`);
|
|
187
|
+
let data;
|
|
188
|
+
try {
|
|
189
|
+
data = await readFile(absolute);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new Error(`Project file ${display} could not be read: ${describe(error)}`, {
|
|
193
|
+
cause: error,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(data);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw new Error(`Project file ${display} is not valid UTF-8`, { cause: error });
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
textCache.set(absolute, pending);
|
|
204
|
+
return pending;
|
|
205
|
+
}
|
|
206
|
+
const globFiles = async (patterns) => {
|
|
207
|
+
const list = typeof patterns === 'string' ? [patterns] : patterns;
|
|
208
|
+
for (const pattern of list)
|
|
209
|
+
await assertPatternAllowed(resolvedRoot, pattern);
|
|
210
|
+
if (list.length === 0)
|
|
211
|
+
return [];
|
|
212
|
+
const physical = await (physicalRootPromise ??= realpath(resolvedRoot));
|
|
213
|
+
const prefixes = [...new Set(list.map(literalDirectory))];
|
|
214
|
+
const roots = prefixes.filter(prefix => !prefixes.some(parent => parent !== prefix && (parent === '' || prefix.startsWith(`${parent}/`))));
|
|
215
|
+
const files = [];
|
|
216
|
+
for (const prefix of roots) {
|
|
217
|
+
let directory = physical;
|
|
218
|
+
let traversable = true;
|
|
219
|
+
const segments = prefix === '' ? [] : prefix.split('/');
|
|
220
|
+
for (const [index, segment] of segments.entries()) {
|
|
221
|
+
const entry = (await readDirectory(directory)).find(entry => entry.name === segment);
|
|
222
|
+
if (entry?.isDirectory() !== true || isReservedDirectory(entry.name, index === 0)) {
|
|
223
|
+
traversable = false;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
directory = join(directory, segment);
|
|
227
|
+
}
|
|
228
|
+
if (traversable)
|
|
229
|
+
await walkInventory(directory, prefix, files, readDirectory);
|
|
230
|
+
}
|
|
231
|
+
const matched = [];
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
if (excludeFiles.has(file.absolute))
|
|
234
|
+
continue;
|
|
235
|
+
if (list.some(pattern => matchesGlob(file.path, pattern)))
|
|
236
|
+
matched.push(file.path);
|
|
237
|
+
}
|
|
238
|
+
return matched.sort();
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
root: resolvedRoot,
|
|
242
|
+
glob: globFiles,
|
|
243
|
+
hasFiles: async (patterns) => (await globFiles(patterns)).length > 0,
|
|
244
|
+
async stat(path) {
|
|
245
|
+
const absolute = helperPath(resolvedRoot, path);
|
|
246
|
+
const inspected = await assertPathBoundary(resolvedRoot, absolute, path);
|
|
247
|
+
let stats;
|
|
248
|
+
try {
|
|
249
|
+
stats = inspected ?? await lstat(absolute);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error.code === 'ENOENT')
|
|
253
|
+
return undefined;
|
|
254
|
+
throw new Error(`Project path ${path} could not be inspected: ${describe(error)}`, {
|
|
255
|
+
cause: error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (stats.isSymbolicLink()) {
|
|
259
|
+
throw new Error(`Project path ${path} is a symbolic link; use the real path`);
|
|
260
|
+
}
|
|
261
|
+
if (stats.isFile())
|
|
262
|
+
return { kind: 'file' };
|
|
263
|
+
if (stats.isDirectory())
|
|
264
|
+
return { kind: 'directory' };
|
|
265
|
+
throw new Error(`Project path ${path} is neither a file nor a directory`);
|
|
266
|
+
},
|
|
267
|
+
async readText(path) {
|
|
268
|
+
return readSource(helperPath(resolvedRoot, path), path);
|
|
269
|
+
},
|
|
270
|
+
async readJson(path) {
|
|
271
|
+
const source = await readSource(helperPath(resolvedRoot, path), path);
|
|
272
|
+
try {
|
|
273
|
+
return parseJson(source);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
throw new Error(`Project file ${path} is not strict JSON: ${describe(error)}`, {
|
|
277
|
+
cause: error,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
async readYaml(path) {
|
|
282
|
+
const source = await readSource(helperPath(resolvedRoot, path), path);
|
|
283
|
+
try {
|
|
284
|
+
return parseYaml(source);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
throw new Error(`Project file ${path} is not valid YAML: ${describe(error)}`, {
|
|
288
|
+
cause: error,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
async readCsv(path, options) {
|
|
293
|
+
const source = await readSource(helperPath(resolvedRoot, path), path);
|
|
294
|
+
try {
|
|
295
|
+
return parseCsv(source, options);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
throw new Error(`Project file ${path} is not valid CSV: ${describe(error)}`, {
|
|
299
|
+
cause: error,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async function assertAliasTarget(physicalRoot, absolute, display) {
|
|
306
|
+
const rootStats = await lstat(physicalRoot, { bigint: true });
|
|
307
|
+
const segments = absolute.split(sep).filter(segment => segment !== '');
|
|
308
|
+
let current = sep;
|
|
309
|
+
let enteredRoot = false;
|
|
310
|
+
for (const [index, segment] of segments.entries()) {
|
|
311
|
+
const last = index === segments.length - 1;
|
|
312
|
+
const candidate = join(current, segment);
|
|
313
|
+
let stats = await lstatOrUndefined(candidate);
|
|
314
|
+
if (stats?.isSymbolicLink() === true) {
|
|
315
|
+
if (last) {
|
|
316
|
+
throw new Error(`Check target ${display} is a symbolic link; point check at the real file or directory`);
|
|
317
|
+
}
|
|
318
|
+
if (enteredRoot) {
|
|
319
|
+
throw new Error(`Check target ${display} passes through a symbolic link at ${segment}/`);
|
|
320
|
+
}
|
|
321
|
+
current = await realpath(candidate);
|
|
322
|
+
stats = await lstatOrUndefined(current);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
current = candidate;
|
|
326
|
+
}
|
|
327
|
+
if (!enteredRoot && stats?.dev === rootStats.dev && stats.ino === rootStats.ino) {
|
|
328
|
+
enteredRoot = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!enteredRoot)
|
|
332
|
+
throw new Error(`Check target ${display} is outside the project root`);
|
|
333
|
+
}
|
|
334
|
+
export async function resolveTarget(root, target) {
|
|
335
|
+
const resolvedRoot = resolve(root);
|
|
336
|
+
const physicalRoot = await realpath(resolvedRoot);
|
|
337
|
+
const input = target.length === 0 ? '.' : target;
|
|
338
|
+
const absolute = isAbsolute(input) ? resolve(input) : helperPath(resolvedRoot, input);
|
|
339
|
+
const spelled = relative(resolvedRoot, absolute);
|
|
340
|
+
const insideLexically = spelled !== '..' && !spelled.startsWith(`..${sep}`) && !isAbsolute(spelled);
|
|
341
|
+
if (insideLexically) {
|
|
342
|
+
await assertPathBoundary(resolvedRoot, absolute, input);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
await assertAliasTarget(physicalRoot, absolute, input);
|
|
346
|
+
}
|
|
347
|
+
let stats;
|
|
348
|
+
try {
|
|
349
|
+
stats = await lstat(absolute);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const code = error.code;
|
|
353
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
354
|
+
throw new Error(`Check target ${input} does not exist`, { cause: error });
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (stats.isSymbolicLink()) {
|
|
359
|
+
throw new Error(`Check target ${input} is a symbolic link; point check at the real file or directory`);
|
|
360
|
+
}
|
|
361
|
+
const physical = await realpath(absolute);
|
|
362
|
+
const escaped = relative(physicalRoot, physical);
|
|
363
|
+
if (escaped === '..' || escaped.startsWith(`..${sep}`) || isAbsolute(escaped)) {
|
|
364
|
+
throw new Error(`Check target ${input} leaves the project root through a symbolic link`);
|
|
365
|
+
}
|
|
366
|
+
await assertPathBoundary(physicalRoot, physical, input);
|
|
367
|
+
const relativePath = escaped === '' ? '.' : escaped.split(sep).join('/');
|
|
368
|
+
const kind = stats.isDirectory()
|
|
369
|
+
? 'directory'
|
|
370
|
+
: stats.isFile()
|
|
371
|
+
? 'file'
|
|
372
|
+
: undefined;
|
|
373
|
+
if (kind === undefined)
|
|
374
|
+
throw new Error(`Check target ${input} is neither a file nor a directory`);
|
|
375
|
+
return {
|
|
376
|
+
root: physicalRoot,
|
|
377
|
+
path: relativePath,
|
|
378
|
+
kind,
|
|
379
|
+
contains(filepath) {
|
|
380
|
+
if (kind === 'file')
|
|
381
|
+
return filepath === relativePath;
|
|
382
|
+
return (relativePath === '.' ||
|
|
383
|
+
filepath === relativePath ||
|
|
384
|
+
filepath.startsWith(`${relativePath}/`));
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PreparedRuleDependencies } from './dependencies.js';
|
|
2
|
+
export declare function assertRuleDependenciesCurrent(dependencies: PreparedRuleDependencies): void;
|
|
3
|
+
export declare function registerRuleDependencies(ruleFile: string, dependencies: PreparedRuleDependencies): void;
|
|
4
|
+
export declare function getRuleDependencies(ruleFile: string): PreparedRuleDependencies | undefined;
|