@promptci/core 0.2.1 → 0.3.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/dist/ai-config.d.ts +13 -6
- package/dist/ai-config.d.ts.map +1 -1
- package/dist/ai-config.js +25 -78
- package/dist/ai-config.js.map +1 -1
- package/dist/close-match.d.ts +97 -0
- package/dist/close-match.d.ts.map +1 -0
- package/dist/close-match.js +215 -0
- package/dist/close-match.js.map +1 -0
- package/dist/command-validity.d.ts.map +1 -1
- package/dist/command-validity.js +309 -76
- package/dist/command-validity.js.map +1 -1
- package/dist/context-optimizer.d.ts.map +1 -1
- package/dist/context-optimizer.js +9 -2
- package/dist/context-optimizer.js.map +1 -1
- package/dist/dead-references.d.ts +8 -1
- package/dist/dead-references.d.ts.map +1 -1
- package/dist/dead-references.js +162 -243
- package/dist/dead-references.js.map +1 -1
- package/dist/dependency-staleness.d.ts.map +1 -1
- package/dist/dependency-staleness.js +24 -38
- package/dist/dependency-staleness.js.map +1 -1
- package/dist/detectors.js +1 -1
- package/dist/detectors.js.map +1 -1
- package/dist/discovery.d.ts +138 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +569 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fix-engine.d.ts.map +1 -1
- package/dist/fix-engine.js +75 -1
- package/dist/fix-engine.js.map +1 -1
- package/dist/frameworks/dotnet.d.ts.map +1 -1
- package/dist/frameworks/dotnet.js +10 -11
- package/dist/frameworks/dotnet.js.map +1 -1
- package/dist/frameworks/go.d.ts.map +1 -1
- package/dist/frameworks/go.js +8 -4
- package/dist/frameworks/go.js.map +1 -1
- package/dist/frameworks/nextjs.d.ts.map +1 -1
- package/dist/frameworks/nextjs.js +8 -17
- package/dist/frameworks/nextjs.js.map +1 -1
- package/dist/frameworks/python.d.ts.map +1 -1
- package/dist/frameworks/python.js +5 -3
- package/dist/frameworks/python.js.map +1 -1
- package/dist/frameworks/rust.d.ts.map +1 -1
- package/dist/frameworks/rust.js +3 -3
- package/dist/frameworks/rust.js.map +1 -1
- package/dist/frameworks/unity.d.ts.map +1 -1
- package/dist/frameworks/unity.js +6 -5
- package/dist/frameworks/unity.js.map +1 -1
- package/dist/gitignore.d.ts +31 -2
- package/dist/gitignore.d.ts.map +1 -1
- package/dist/gitignore.js +33 -8
- package/dist/gitignore.js.map +1 -1
- package/dist/hooks-settings-detector.d.ts.map +1 -1
- package/dist/hooks-settings-detector.js +5 -1
- package/dist/hooks-settings-detector.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/known-paths.d.ts +82 -0
- package/dist/known-paths.d.ts.map +1 -0
- package/dist/known-paths.js +152 -0
- package/dist/known-paths.js.map +1 -0
- package/dist/makefile.d.ts +11 -3
- package/dist/makefile.d.ts.map +1 -1
- package/dist/makefile.js +28 -22
- package/dist/makefile.js.map +1 -1
- package/dist/manifest-consistency.d.ts +1 -1
- package/dist/manifest-consistency.d.ts.map +1 -1
- package/dist/manifest-consistency.js +12 -8
- package/dist/manifest-consistency.js.map +1 -1
- package/dist/mcp-config-detector.d.ts.map +1 -1
- package/dist/mcp-config-detector.js +5 -1
- package/dist/mcp-config-detector.js.map +1 -1
- package/dist/project-type.d.ts +2 -1
- package/dist/project-type.d.ts.map +1 -1
- package/dist/project-type.js +36 -48
- package/dist/project-type.js.map +1 -1
- package/dist/prompt-cache.d.ts.map +1 -1
- package/dist/prompt-cache.js +13 -2
- package/dist/prompt-cache.js.map +1 -1
- package/dist/repo-context.d.ts +9 -0
- package/dist/repo-context.d.ts.map +1 -1
- package/dist/repo-context.js +32 -35
- package/dist/repo-context.js.map +1 -1
- package/dist/repo-tree.d.ts +281 -0
- package/dist/repo-tree.d.ts.map +1 -0
- package/dist/repo-tree.js +455 -0
- package/dist/repo-tree.js.map +1 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +3 -0
- package/dist/report.js.map +1 -1
- package/dist/sarif.d.ts +27 -0
- package/dist/sarif.d.ts.map +1 -0
- package/dist/sarif.js +357 -0
- package/dist/sarif.js.map +1 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +8 -2
- package/dist/scan.js.map +1 -1
- package/dist/scanner.d.ts +4 -3
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +36 -77
- package/dist/scanner.js.map +1 -1
- package/dist/security-pack.d.ts.map +1 -1
- package/dist/security-pack.js +34 -35
- package/dist/security-pack.js.map +1 -1
- package/dist/skill-supply-chain.d.ts.map +1 -1
- package/dist/skill-supply-chain.js +29 -19
- package/dist/skill-supply-chain.js.map +1 -1
- package/dist/skills-detector.d.ts.map +1 -1
- package/dist/skills-detector.js +11 -3
- package/dist/skills-detector.js.map +1 -1
- package/dist/token-estimates.d.ts +42 -0
- package/dist/token-estimates.d.ts.map +1 -0
- package/dist/token-estimates.js +200 -0
- package/dist/token-estimates.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-validity.d.ts","sourceRoot":"","sources":["../src/command-validity.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command-validity.d.ts","sourceRoot":"","sources":["../src/command-validity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAoUhD,8FAA8F;AAC9F,eAAO,MAAM,YAAY,EAAE,WAAW,CAAC,MAAM,CAK3C,CAAC;AAusBH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,EAAE,CAG3E;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;;;;;;OAQG;IACH,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CAC5C,CAAC;AAQF,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,eAAe,CAOrE"}
|
package/dist/command-validity.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
1
|
import * as path from 'node:path';
|
|
3
2
|
import * as crypto from 'node:crypto';
|
|
4
3
|
import { fencedBlocks, scanFencedLines } from './markdown-fences.js';
|
|
5
4
|
import { fileIdPath } from './finding-id.js';
|
|
6
5
|
import { anyWorkspaceHasScript, isKnownBinary, selectWorkspaces } from './workspace-manifests.js';
|
|
7
6
|
import { makefileDefines, readMakefile } from './makefile.js';
|
|
8
|
-
import {
|
|
7
|
+
import { incompleteCheckIssue, isAbsoluteAnywhere, treeFor } from './repo-tree.js';
|
|
8
|
+
import { closestUniqueMatch, isMechanicalRename } from './close-match.js';
|
|
9
9
|
/**
|
|
10
10
|
* Command Validity Detector
|
|
11
11
|
*
|
|
@@ -19,9 +19,24 @@ import { realPathWithinRoot } from './path-containment.js';
|
|
|
19
19
|
* - Prose patterns like "run pnpm test"
|
|
20
20
|
* - Multi-command lines joined with && are split and each segment validated
|
|
21
21
|
*
|
|
22
|
+
* Every path a command names is looked up through RepoTree (pcic-x1vl, the
|
|
23
|
+
* dead-reference containment of pcic-xhzb): inside the scan root only, with
|
|
24
|
+
* exact case. A path that leaves the root is not checked, and a directory that
|
|
25
|
+
* cannot be listed leaves its commands unchecked and named in one "check
|
|
26
|
+
* incomplete" warning instead of reported as missing.
|
|
27
|
+
*
|
|
22
28
|
* Findings are heuristic — cautious wording is intentional.
|
|
23
29
|
*/
|
|
24
30
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
31
|
+
/** Occurrences of a plain substring in one line of text (pcic-oonz). */
|
|
32
|
+
function countOnLine(line, needle) {
|
|
33
|
+
if (!needle)
|
|
34
|
+
return 0;
|
|
35
|
+
let count = 0;
|
|
36
|
+
for (let at = line.indexOf(needle); at !== -1; at = line.indexOf(needle, at + needle.length))
|
|
37
|
+
count++;
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
25
40
|
/** Stable per-finding ID based on file + command text. */
|
|
26
41
|
function issueId(filePath, cmdText) {
|
|
27
42
|
const hash = crypto
|
|
@@ -31,30 +46,101 @@ function issueId(filePath, cmdText) {
|
|
|
31
46
|
.slice(0, 12);
|
|
32
47
|
return `cmd-validity-${hash}`;
|
|
33
48
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Returned when the verdict depends on a directory that could not be listed:
|
|
51
|
+
* the command is neither validated nor reported, and the scan says what it
|
|
52
|
+
* skipped (pcic-x1vl, as dead-references does since pcic-xhzb).
|
|
53
|
+
*/
|
|
54
|
+
const UNJUDGED = Symbol('unjudged');
|
|
55
|
+
/** Separators in a documented path: `/` everywhere, `\` too where the platform treats it as one. */
|
|
56
|
+
const PATH_SEPARATORS = path.sep === '\\' ? /[\\/]/ : /\//;
|
|
57
|
+
/**
|
|
58
|
+
* pcic-x1vl: `rel`, a path a command names, joined to `fromDir` (inside the
|
|
59
|
+
* root) one segment at a time; `null` when the path cannot be placed inside
|
|
60
|
+
* the scan root without knowing where the root is. That is:
|
|
61
|
+
* - absolute in any platform's form (`/x`, `C:\x`, `C:x`, `\\host\x`), even
|
|
62
|
+
* when it happens to point into the root: its verdict would depend on where
|
|
63
|
+
* the repository is checked out;
|
|
64
|
+
* - computed from the environment (`~/x`, `$HOME/x`, `%APPDATA%\x`, backticks);
|
|
65
|
+
* - climbing above the root at any point, even when a later segment names the
|
|
66
|
+
* root's own directory again (`../<root name>/x`): resolving it would make
|
|
67
|
+
* the verdict depend on, and so reveal, the name of the directory the scan
|
|
68
|
+
* runs in.
|
|
69
|
+
*/
|
|
70
|
+
function withinRoot(tree, fromDir, rel) {
|
|
71
|
+
if (isAbsoluteAnywhere(rel) || /^~|[$`%]/.test(rel))
|
|
72
|
+
return null;
|
|
73
|
+
const parts = path.relative(tree.root, fromDir).split(path.sep).filter(Boolean);
|
|
74
|
+
if (parts.includes('..') || path.isAbsolute(parts.join(path.sep)))
|
|
75
|
+
return null;
|
|
76
|
+
for (const segment of rel.split(PATH_SEPARATORS)) {
|
|
77
|
+
if (segment === '' || segment === '.')
|
|
78
|
+
continue;
|
|
79
|
+
if (segment !== '..')
|
|
80
|
+
parts.push(segment);
|
|
81
|
+
else if (parts.pop() === undefined)
|
|
82
|
+
return null;
|
|
42
83
|
}
|
|
84
|
+
return path.join(tree.root, ...parts);
|
|
43
85
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
/**
|
|
87
|
+
* pcic-x1vl: what the repository holds at `rel`, a path a command names,
|
|
88
|
+
* resolved against the command's directory. The only way this detector asks
|
|
89
|
+
* the filesystem about such a path. A path {@link withinRoot} cannot place is
|
|
90
|
+
* `outside` before any lookup, and RepoTree never touches anything outside the
|
|
91
|
+
* scan root: one through a committed link out of the tree is `outside` too.
|
|
92
|
+
* Names match with exact case, so the verdict is the same on every platform,
|
|
93
|
+
* and the same as on Linux and in CI.
|
|
94
|
+
*/
|
|
95
|
+
function locate(tree, baseDir, rel) {
|
|
96
|
+
const abs = withinRoot(tree, baseDir, rel);
|
|
97
|
+
return abs === null ? { state: 'outside' } : tree.lookup(abs);
|
|
51
98
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The finding for a path a command needs, by what {@link locate} found.
|
|
101
|
+
* pcic-x1vl: a path outside the scan root is not judged, whether or not it
|
|
102
|
+
* exists. Reporting it would claim a file is missing without having looked,
|
|
103
|
+
* and checking it would tell whoever wrote the instruction file whether a path
|
|
104
|
+
* on the scanning machine exists (in the hosted product, the server's). A
|
|
105
|
+
* monorepo package scanned on its own can legitimately run `../../scripts/x.sh`.
|
|
106
|
+
*/
|
|
107
|
+
function missingUnless(found, message) {
|
|
108
|
+
if (found.state === 'missing')
|
|
109
|
+
return message;
|
|
110
|
+
if (found.state === 'unreadable')
|
|
111
|
+
return UNJUDGED;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The name and scripts of a package.json; `null` when there is none to read
|
|
116
|
+
* (or it does not parse); `unreadable` when a directory on the way could not
|
|
117
|
+
* be listed, so whether it exists is unknown; `unfetched` when a caller's
|
|
118
|
+
* inventory says the manifest is there but its bytes were never fetched
|
|
119
|
+
* (pcic-csf) — nothing failed, so callers stay silent rather than warn.
|
|
120
|
+
*/
|
|
121
|
+
function readPackageScripts(tree, packageJsonPath) {
|
|
122
|
+
// pcic-x1vl: looked up through the tree, so a manifest path that leaves the
|
|
123
|
+
// root (`cd` through a link out of the tree) is never touched, and a
|
|
124
|
+
// manifest committed as a link out of the tree is not read.
|
|
125
|
+
const found = tree.lookup(packageJsonPath);
|
|
126
|
+
if (found.state === 'unreadable')
|
|
127
|
+
return 'unreadable';
|
|
128
|
+
if (found.state !== 'found' || found.isDir)
|
|
129
|
+
return null;
|
|
130
|
+
// pcic-csf: a caller's inventory can say the manifest is there without its
|
|
131
|
+
// bytes being on disk. Its scripts are then unknown, not absent — reporting
|
|
132
|
+
// them missing is the false positive the inventory exists to remove. The two
|
|
133
|
+
// "cannot read" answers stay apart: `unreadable` is a real failure the scan
|
|
134
|
+
// should name, `unfetched` is nothing to name (see RepoTree.readText).
|
|
135
|
+
const read = tree.readText(packageJsonPath);
|
|
136
|
+
if (read.state === 'unreadable' || read.state === 'too-large')
|
|
137
|
+
return 'unreadable';
|
|
138
|
+
if (read.state === 'unfetched')
|
|
139
|
+
return 'unfetched';
|
|
140
|
+
if (read.state !== 'found')
|
|
55
141
|
return null;
|
|
56
142
|
try {
|
|
57
|
-
const raw =
|
|
143
|
+
const raw = read.content;
|
|
58
144
|
const parsed = JSON.parse(raw);
|
|
59
145
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
60
146
|
return null;
|
|
@@ -92,28 +178,37 @@ function normalizePnpmFilter(value) {
|
|
|
92
178
|
.replace(/^!/, '')
|
|
93
179
|
.trim();
|
|
94
180
|
}
|
|
95
|
-
|
|
181
|
+
/**
|
|
182
|
+
* The package.json files a workspace selector may name. `complete` is false
|
|
183
|
+
* when a workspace directory could not be listed (pcic-x1vl): a selector that
|
|
184
|
+
* matches nothing may still name a package in it.
|
|
185
|
+
*/
|
|
186
|
+
function workspacePackageJsonCandidates(tree, selector) {
|
|
96
187
|
const candidates = new Set();
|
|
188
|
+
let complete = true;
|
|
97
189
|
const selectorAsPath = selector.replace(/\\/g, '/');
|
|
98
190
|
if (selectorAsPath.startsWith('.') || selectorAsPath.includes('/')) {
|
|
99
|
-
|
|
191
|
+
// pcic-x1vl: a selector path that leaves the root is not looked up.
|
|
192
|
+
const dir = withinRoot(tree, tree.root, selectorAsPath);
|
|
193
|
+
if (dir !== null)
|
|
194
|
+
candidates.add(path.join(dir, 'package.json'));
|
|
100
195
|
}
|
|
101
196
|
for (const dir of ['apps', 'packages', 'libs']) {
|
|
102
|
-
const absDir = path.join(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
197
|
+
const absDir = path.join(tree.root, dir);
|
|
198
|
+
const entries = tree.entries(absDir);
|
|
199
|
+
if (entries === 'unreadable')
|
|
200
|
+
complete = false;
|
|
201
|
+
if (typeof entries === 'string')
|
|
202
|
+
continue; // workspace directory absent (or not inside the root)
|
|
203
|
+
for (const entry of entries.values()) {
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
candidates.add(path.join(absDir, entry.name, 'package.json'));
|
|
108
206
|
}
|
|
109
207
|
}
|
|
110
|
-
catch {
|
|
111
|
-
// Workspace directory absent.
|
|
112
|
-
}
|
|
113
208
|
}
|
|
114
|
-
return [...candidates];
|
|
209
|
+
return { candidates: [...candidates], complete };
|
|
115
210
|
}
|
|
116
|
-
function workspaceHasScript(context, selector, scriptName) {
|
|
211
|
+
function workspaceHasScript(context, tree, selector, scriptName) {
|
|
117
212
|
const normalizedSelector = normalizePnpmFilter(selector);
|
|
118
213
|
if (!normalizedSelector)
|
|
119
214
|
return false;
|
|
@@ -123,9 +218,16 @@ function workspaceHasScript(context, selector, scriptName) {
|
|
|
123
218
|
if (context.workspaces) {
|
|
124
219
|
return selectWorkspaces(context, normalizedSelector).some((ws) => !!ws.scripts[scriptName]);
|
|
125
220
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
221
|
+
const { candidates, complete } = workspacePackageJsonCandidates(tree, normalizedSelector);
|
|
222
|
+
let unknown = !complete;
|
|
223
|
+
let silent = false;
|
|
224
|
+
for (const candidate of candidates) {
|
|
225
|
+
const pkg = readPackageScripts(tree, candidate);
|
|
226
|
+
if (pkg === 'unreadable')
|
|
227
|
+
unknown = true;
|
|
228
|
+
if (pkg === 'unfetched')
|
|
229
|
+
silent = true;
|
|
230
|
+
if (!pkg || pkg === 'unreadable' || pkg === 'unfetched')
|
|
129
231
|
continue;
|
|
130
232
|
const packageDir = path.dirname(candidate).replace(/\\/g, '/');
|
|
131
233
|
const relDir = path.relative(context.repoRoot, packageDir).replace(/\\/g, '/');
|
|
@@ -135,7 +237,10 @@ function workspaceHasScript(context, selector, scriptName) {
|
|
|
135
237
|
if (selectorMatches && !!pkg.scripts[scriptName])
|
|
136
238
|
return true;
|
|
137
239
|
}
|
|
138
|
-
|
|
240
|
+
if (unknown)
|
|
241
|
+
return 'unknown';
|
|
242
|
+
// A manifest that exists but was never fetched could define the script.
|
|
243
|
+
return silent ? 'silent' : false;
|
|
139
244
|
}
|
|
140
245
|
/** Returns true when a path string looks like a file reference (not a flag). */
|
|
141
246
|
function isLikelyFilePath(str) {
|
|
@@ -188,9 +293,10 @@ function resolveCwd(current, target) {
|
|
|
188
293
|
if (current === null)
|
|
189
294
|
return null;
|
|
190
295
|
const cleaned = target.replace(/^['"]|['"]$/g, '').trim();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
296
|
+
// Absolute in any platform's form (`/x`, `C:\x`, drive-relative `C:x`, `\\host\x`)
|
|
297
|
+
// or home: not resolvable to a repo-relative dir (pcic-x1vl: on every platform alike).
|
|
298
|
+
if (!cleaned || isAbsoluteAnywhere(cleaned) || cleaned.startsWith('~'))
|
|
299
|
+
return null;
|
|
194
300
|
// `cd -`, `cd $DIR`, `cd "$(git rev-parse --show-toplevel)"`, `cd %TEMP%`,
|
|
195
301
|
// `cd <dir>`: computed or placeholder targets name no knowable directory.
|
|
196
302
|
if (/^-|[$`%<>{}*?]/.test(cleaned))
|
|
@@ -415,7 +521,7 @@ function cwdOverride(parts) {
|
|
|
415
521
|
}
|
|
416
522
|
return undefined;
|
|
417
523
|
}
|
|
418
|
-
function findMissingScript(context, cwd, parts, scriptName, builtins, tool, lookup, onScript) {
|
|
524
|
+
function findMissingScript(context, tree, cwd, parts, scriptName, builtins, tool, lookup, onScript, suggest) {
|
|
419
525
|
const names = scriptName.includes('/')
|
|
420
526
|
? scriptName.split('/').map((s) => s.trim()).filter(Boolean)
|
|
421
527
|
: [scriptName];
|
|
@@ -435,17 +541,45 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool, look
|
|
|
435
541
|
}
|
|
436
542
|
const selector = lookup.workspaceName ?? workspaceSelector(tool, parts);
|
|
437
543
|
const allWorkspaces = runsInAllWorkspaces(tool, parts);
|
|
438
|
-
const inWorkspaceScope = (name) => allWorkspaces ? anyWorkspaceHasScript(context, name) : workspaceHasScript(context, selector, name);
|
|
544
|
+
const inWorkspaceScope = (name) => allWorkspaces ? anyWorkspaceHasScript(context, name) : workspaceHasScript(context, tree, selector, name);
|
|
439
545
|
// With discovered workspaces, an explicit selector means ONLY the selected
|
|
440
546
|
// packages: the root manifest's same-named script is not what runs, and a
|
|
441
547
|
// selector that matches no package is itself the error.
|
|
442
548
|
const selectedWorkspaces = selector && context.workspaces && !allWorkspaces
|
|
443
549
|
? selectWorkspaces(context, selector.replace(/^['"]|['"]$/g, '').replace(/^\.\.\./, '').replace(/\.\.\.$/, '').trim())
|
|
444
550
|
: undefined;
|
|
551
|
+
/**
|
|
552
|
+
* pcic-oonz: offers the one close match for a name that is about to be
|
|
553
|
+
* reported missing, from the manifest(s) that would actually have run it.
|
|
554
|
+
*
|
|
555
|
+
* Deliberately silent for the slash shorthand (`pnpm lint/typecheck`): the
|
|
556
|
+
* rewrite would have to edit one component of a token that documents several
|
|
557
|
+
* scripts, which is not the mechanical single-token substitution the fix
|
|
558
|
+
* engine applies. Silent too whenever a workspace selector is in play
|
|
559
|
+
* without discovered workspaces to resolve it against — the candidate pool
|
|
560
|
+
* would then be the root manifest, which is not where the script would run.
|
|
561
|
+
*/
|
|
562
|
+
const propose = (name, pool) => {
|
|
563
|
+
if (!suggest || names.length !== 1)
|
|
564
|
+
return;
|
|
565
|
+
const candidate = closestUniqueMatch(name, pool);
|
|
566
|
+
if (candidate !== undefined) {
|
|
567
|
+
suggest({ missing: name, candidate, mechanical: isMechanicalRename(name, candidate) });
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const scopedToWorkspaces = selector !== undefined || allWorkspaces;
|
|
445
571
|
const inSubdir = cwd !== '.' && cwd !== '';
|
|
446
572
|
const dirPkg = inSubdir
|
|
447
|
-
? readPackageScripts(
|
|
573
|
+
? readPackageScripts(tree, path.join(tree.root, cwd, 'package.json'))
|
|
448
574
|
: null;
|
|
575
|
+
// pcic-csf: the manifest is there but was never fetched. Nothing failed and
|
|
576
|
+
// there is no location to name, so say nothing at all — a "check incomplete"
|
|
577
|
+
// warning here would name zero locations and tell the reader nothing.
|
|
578
|
+
if (dirPkg === 'unfetched')
|
|
579
|
+
return null;
|
|
580
|
+
// pcic-x1vl: a directory on the way could not be listed — say so, don't guess.
|
|
581
|
+
if (dirPkg === 'unreadable')
|
|
582
|
+
return UNJUDGED;
|
|
449
583
|
// cd'd into a directory whose package.json we can't read — don't guess.
|
|
450
584
|
if (inSubdir && !dirPkg)
|
|
451
585
|
return null;
|
|
@@ -471,6 +605,13 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool, look
|
|
|
471
605
|
const target = selectedWorkspaces.length === 1
|
|
472
606
|
? `${selectedWorkspaces[0].name ?? selectedWorkspaces[0].dir} (${selectedWorkspaces[0].dir}/package.json)`
|
|
473
607
|
: `the ${selectedWorkspaces.length} workspaces selected by "${selector}"`;
|
|
608
|
+
// The selected packages ARE the manifests that would run the script — all
|
|
609
|
+
// of them, so a candidate only SOME of them define would still leave the
|
|
610
|
+
// command broken where it is missing. The pool is therefore the
|
|
611
|
+
// intersection of their script names (which for one selected package is
|
|
612
|
+
// simply that package's scripts).
|
|
613
|
+
const pools = selectedWorkspaces.map((ws) => Object.keys(ws.scripts));
|
|
614
|
+
propose(name, pools.slice(1).reduce((shared, pool) => shared.filter((n) => pool.includes(n)), pools[0] ?? []));
|
|
474
615
|
return `${tool} script "${name}" does not appear in ${target} scripts`;
|
|
475
616
|
}
|
|
476
617
|
if (dirPkg) {
|
|
@@ -481,15 +622,26 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool, look
|
|
|
481
622
|
// script.
|
|
482
623
|
if (dirPkg.scripts[name])
|
|
483
624
|
continue;
|
|
484
|
-
|
|
625
|
+
const inWorkspace = inWorkspaceScope(name);
|
|
626
|
+
if (inWorkspace === true || inWorkspace === 'silent')
|
|
485
627
|
continue;
|
|
628
|
+
if (inWorkspace === 'unknown')
|
|
629
|
+
return UNJUDGED;
|
|
630
|
+
if (!scopedToWorkspaces)
|
|
631
|
+
propose(name, Object.keys(dirPkg.scripts));
|
|
486
632
|
return `${tool} script "${name}" does not appear in ${cwd}/package.json scripts`;
|
|
487
633
|
}
|
|
488
634
|
// Repo-root context: the workspace target(s) or the root manifest.
|
|
489
|
-
|
|
635
|
+
const inWorkspace = inWorkspaceScope(name);
|
|
636
|
+
if (inWorkspace === true || inWorkspace === 'silent')
|
|
490
637
|
continue;
|
|
491
638
|
if (context.packageJson.scripts[name])
|
|
492
639
|
continue;
|
|
640
|
+
// pcic-x1vl: a selected workspace may define it in a directory that could not be read.
|
|
641
|
+
if (inWorkspace === 'unknown')
|
|
642
|
+
return UNJUDGED;
|
|
643
|
+
if (!scopedToWorkspaces)
|
|
644
|
+
propose(name, Object.keys(context.packageJson.scripts));
|
|
493
645
|
return `${tool} script "${name}" does not appear in package.json scripts`;
|
|
494
646
|
}
|
|
495
647
|
return null;
|
|
@@ -509,7 +661,7 @@ const BUILTINS_BY_TOOL = {
|
|
|
509
661
|
* to an installed binary), `yarn workspace <name> <script>`, and workspace or
|
|
510
662
|
* directory selectors.
|
|
511
663
|
*/
|
|
512
|
-
function validatePackageManagerScript(tool, parts, context, cwd, baseDir, onScript) {
|
|
664
|
+
function validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, suggest) {
|
|
513
665
|
const subIdx = firstNonFlagArgIndex(parts);
|
|
514
666
|
const sub = parts[subIdx];
|
|
515
667
|
if (!sub)
|
|
@@ -540,15 +692,28 @@ function validatePackageManagerScript(tool, parts, context, cwd, baseDir, onScri
|
|
|
540
692
|
if (!explicitRun && BUN_BUILTINS.has(scriptName))
|
|
541
693
|
return null;
|
|
542
694
|
if (isLikelyFilePath(scriptName)) {
|
|
543
|
-
if (baseDir && /\.[cm]?[jt]sx?$/.test(scriptName)
|
|
544
|
-
return `File "${scriptName}" referenced by "bun" does not appear to exist in the repository
|
|
695
|
+
if (baseDir && /\.[cm]?[jt]sx?$/.test(scriptName)) {
|
|
696
|
+
return missingUnless(locate(tree, baseDir, scriptName), `File "${scriptName}" referenced by "bun" does not appear to exist in the repository`);
|
|
545
697
|
}
|
|
546
698
|
return null;
|
|
547
699
|
}
|
|
548
700
|
}
|
|
549
|
-
|
|
701
|
+
// pcic-oonz: the rewrite keeps everything up to and including the script
|
|
702
|
+
// name, so the text looked for on the line is the command as documented
|
|
703
|
+
// (`npm run bulid`, `pnpm --filter web lnit`) rather than a bare word.
|
|
704
|
+
const prefix = parts.slice(0, nameIdx).join(' ');
|
|
705
|
+
const onSuggestion = suggest
|
|
706
|
+
? ({ missing, candidate, mechanical }) => suggest({
|
|
707
|
+
find: prefix ? `${prefix} ${missing}` : missing,
|
|
708
|
+
replace: prefix ? `${prefix} ${candidate}` : candidate,
|
|
709
|
+
missing,
|
|
710
|
+
candidate,
|
|
711
|
+
mechanical,
|
|
712
|
+
})
|
|
713
|
+
: undefined;
|
|
714
|
+
return findMissingScript(context, tree, cwd, parts, scriptName, BUILTINS_BY_TOOL[tool], tool,
|
|
550
715
|
// npm/pnpm `run` only run scripts; every other form can fall through to a binary.
|
|
551
|
-
{ allowBinary: !(explicitRun && (tool === 'npm' || tool === 'pnpm')), workspaceName }, onScript);
|
|
716
|
+
{ allowBinary: !(explicitRun && (tool === 'npm' || tool === 'pnpm')), workspaceName }, onScript, onSuggestion);
|
|
552
717
|
}
|
|
553
718
|
// ── make targets ──────────────────────────────────────────────────────────────
|
|
554
719
|
/** make options whose value is the NEXT token (unless attached: `-Cdir`, `-j4`). */
|
|
@@ -571,7 +736,7 @@ const MAKE_FLAGS_WITH_OPTIONAL_NUMBER = new Set(['-j', '-l', '--jobs', '--load-a
|
|
|
571
736
|
* names make could build from a same-stem source via its built-in implicit
|
|
572
737
|
* rules (`make hello` next to `hello.c`) are never reported.
|
|
573
738
|
*/
|
|
574
|
-
function validateMakeTargets(parts,
|
|
739
|
+
function validateMakeTargets(parts, tree, cwd, makefileFor) {
|
|
575
740
|
let dir = cwd;
|
|
576
741
|
const goals = [];
|
|
577
742
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -608,7 +773,6 @@ function validateMakeTargets(parts, context, cwd, makefileFor) {
|
|
|
608
773
|
const facts = makefileFor(dir);
|
|
609
774
|
if (!facts || facts.unverifiable)
|
|
610
775
|
return null;
|
|
611
|
-
const baseDir = dir === '.' ? context.repoRoot : path.join(context.repoRoot, dir);
|
|
612
776
|
let entries;
|
|
613
777
|
for (const goal of goals) {
|
|
614
778
|
const target = goal.replace(/^['"]|['"]$/g, '');
|
|
@@ -617,11 +781,13 @@ function validateMakeTargets(parts, context, cwd, makefileFor) {
|
|
|
617
781
|
if (makefileDefines(facts, target))
|
|
618
782
|
continue;
|
|
619
783
|
// make can build `target` from `target.c` etc. with its built-in rules.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
784
|
+
// Listed through the tree (pcic-x1vl), like the Makefile it found there;
|
|
785
|
+
// a directory it cannot list leaves those rules unknowable, so stay silent.
|
|
786
|
+
if (entries === undefined) {
|
|
787
|
+
const listing = tree.entries(dir);
|
|
788
|
+
if (typeof listing === 'string')
|
|
789
|
+
return null;
|
|
790
|
+
entries = [...listing.keys()];
|
|
625
791
|
}
|
|
626
792
|
if (entries.some((e) => e === target || e.startsWith(`${target}.`)))
|
|
627
793
|
continue;
|
|
@@ -643,15 +809,16 @@ function validateSegment(seg, context, cwd = '.', onScript, opts = {}) {
|
|
|
643
809
|
if (parts.length === 0)
|
|
644
810
|
return null;
|
|
645
811
|
const tool = parts[0];
|
|
812
|
+
const tree = opts.tree ?? treeFor(context);
|
|
646
813
|
// Base directory for file-existence checks — respects a `cd`-established cwd.
|
|
647
814
|
const baseDir = cwd === null
|
|
648
815
|
? null
|
|
649
816
|
: cwd !== '.' && cwd !== ''
|
|
650
|
-
? path.join(
|
|
651
|
-
:
|
|
817
|
+
? path.join(tree.root, cwd)
|
|
818
|
+
: tree.root;
|
|
652
819
|
// ── Package manager scripts ────────────────────────────────────────────────
|
|
653
820
|
if (tool === 'pnpm' || tool === 'npm' || tool === 'yarn' || tool === 'bun') {
|
|
654
|
-
return validatePackageManagerScript(tool, parts, context, cwd, baseDir, onScript);
|
|
821
|
+
return validatePackageManagerScript(tool, parts, context, tree, cwd, baseDir, onScript, opts.suggestScript);
|
|
655
822
|
}
|
|
656
823
|
// ── File-executing commands ────────────────────────────────────────────────
|
|
657
824
|
const FILE_EXECUTORS = ['node', 'tsx', 'ts-node', 'bash', 'sh', 'python', 'python3'];
|
|
@@ -659,8 +826,10 @@ function validateSegment(seg, context, cwd = '.', onScript, opts = {}) {
|
|
|
659
826
|
// Skip flags (and known flags' values, e.g. `--loader ts-node/esm`) to
|
|
660
827
|
// find the actual script/file argument.
|
|
661
828
|
const filePart = parts[firstNonFlagArgIndex(parts)];
|
|
662
|
-
if (filePart && isLikelyFilePath(filePart)
|
|
663
|
-
|
|
829
|
+
if (filePart && isLikelyFilePath(filePart)) {
|
|
830
|
+
const verdict = missingUnless(locate(tree, baseDir, filePart), `File "${filePart}" referenced by "${tool}" does not appear to exist in the repository`);
|
|
831
|
+
if (verdict !== null)
|
|
832
|
+
return verdict;
|
|
664
833
|
}
|
|
665
834
|
}
|
|
666
835
|
// ── make <target> ──────────────────────────────────────────────────────────
|
|
@@ -672,39 +841,47 @@ function validateSegment(seg, context, cwd = '.', onScript, opts = {}) {
|
|
|
672
841
|
// A code span like `make sure` is English, not a make invocation.
|
|
673
842
|
if (!opts.fenced && parts.length === 2 && MAKE_PROSE_WORDS.has(parts[1].toLowerCase()))
|
|
674
843
|
return null;
|
|
675
|
-
return validateMakeTargets(parts,
|
|
844
|
+
return validateMakeTargets(parts, tree, makeCwd, opts.makefileFor);
|
|
676
845
|
}
|
|
677
846
|
// ── Direct script invocations: ./scripts/foo.sh ───────────────────────────
|
|
678
847
|
if (baseDir && (tool.startsWith('./') || tool.startsWith('../'))) {
|
|
848
|
+
const found = locate(tree, baseDir, tool);
|
|
679
849
|
// pcic-2b6.8: a bare `./src` or `./docs/` in an inline code span is a
|
|
680
850
|
// directory mention, not a script invocation — it is dead-references' to
|
|
681
851
|
// judge. Only a shell-fence line (meant to be run) still reports an absent
|
|
682
852
|
// extensionless `./gradlew`; an existing directory is never a missing script.
|
|
683
853
|
if (parts.length === 1 && path.extname(tool.replace(/\/+$/, '')) === '') {
|
|
684
|
-
if (
|
|
854
|
+
if (found.state === 'found' && found.isDir)
|
|
685
855
|
return null;
|
|
686
856
|
if (!opts.fenced || tool.endsWith('/'))
|
|
687
857
|
return null;
|
|
688
858
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
859
|
+
const verdict = missingUnless(found, `Script "${tool}" does not appear to exist in the repository`);
|
|
860
|
+
if (verdict !== null)
|
|
861
|
+
return verdict;
|
|
692
862
|
}
|
|
693
863
|
// ── docker compose -f <file> ───────────────────────────────────────────────
|
|
694
864
|
if (baseDir && tool === 'docker' && parts[1] === 'compose' && parts.includes('-f')) {
|
|
695
865
|
const fIdx = parts.indexOf('-f');
|
|
696
866
|
const composeFile = parts[fIdx + 1];
|
|
697
|
-
if (composeFile && isLikelyFilePath(composeFile)
|
|
698
|
-
|
|
867
|
+
if (composeFile && isLikelyFilePath(composeFile)) {
|
|
868
|
+
const verdict = missingUnless(locate(tree, baseDir, composeFile), `Docker Compose file "${composeFile}" does not appear to exist in the repository`);
|
|
869
|
+
if (verdict !== null)
|
|
870
|
+
return verdict;
|
|
699
871
|
}
|
|
700
872
|
}
|
|
701
873
|
// ── dotnet test <project-or-solution> ─────────────────────────────────────
|
|
702
874
|
if (baseDir && tool === 'dotnet' && parts[1] === 'test' && parts[2]) {
|
|
703
875
|
const target = parts[2];
|
|
704
876
|
if (isLikelyFilePath(target)) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
877
|
+
const found = locate(tree, baseDir, target);
|
|
878
|
+
// Outside the root: not judged at all, so the extension check below
|
|
879
|
+
// cannot tell an existing outside file from a missing one either.
|
|
880
|
+
if (found.state === 'outside')
|
|
881
|
+
return null;
|
|
882
|
+
const verdict = missingUnless(found, `dotnet test target "${target}" does not appear to exist`);
|
|
883
|
+
if (verdict !== null)
|
|
884
|
+
return verdict;
|
|
708
885
|
if (!/\.(sln|csproj|fsproj)$/.test(target)) {
|
|
709
886
|
return `dotnet test target "${target}" should be a .sln, .csproj, or .fsproj file`;
|
|
710
887
|
}
|
|
@@ -742,6 +919,9 @@ function computeCommandAnalysis(context) {
|
|
|
742
919
|
const issues = [];
|
|
743
920
|
const evaluatedScripts = new Map();
|
|
744
921
|
const reported = new Set();
|
|
922
|
+
const tree = treeFor(context);
|
|
923
|
+
// pcic-x1vl: commands left unchecked because part of the tree could not be listed.
|
|
924
|
+
const unchecked = new Map();
|
|
745
925
|
// Makefiles by repo-relative directory: the root one comes from the context,
|
|
746
926
|
// subdirectory ones (`make -C dir`, `cd dir && make`) are read once each.
|
|
747
927
|
const makefiles = new Map();
|
|
@@ -749,11 +929,14 @@ function computeCommandAnalysis(context) {
|
|
|
749
929
|
if (dir === '.' || dir === '')
|
|
750
930
|
return context.makefile;
|
|
751
931
|
if (!makefiles.has(dir))
|
|
752
|
-
makefiles.set(dir, readMakefile(context.repoRoot, dir));
|
|
932
|
+
makefiles.set(dir, readMakefile(context.repoRoot, dir, tree));
|
|
753
933
|
return makefiles.get(dir);
|
|
754
934
|
};
|
|
755
935
|
for (const file of context.files) {
|
|
756
936
|
const commands = extractCommands(file.content);
|
|
937
|
+
// pcic-oonz: the file's own lines, to confirm a rewrite's `find` text is
|
|
938
|
+
// unambiguous there before the finding claims to be auto-applicable.
|
|
939
|
+
const contentLines = file.content.split(/\r?\n/);
|
|
757
940
|
const evaluated = new Set();
|
|
758
941
|
const fileDir = path.relative(context.repoRoot, path.dirname(file.path)).replace(/\\/g, '/') || '.';
|
|
759
942
|
evaluatedScripts.set(file.path, evaluated);
|
|
@@ -764,33 +947,83 @@ function computeCommandAnalysis(context) {
|
|
|
764
947
|
// (prose, which never carries a `cd`) runs from the root; null stays
|
|
765
948
|
// unverifiable.
|
|
766
949
|
const cwd = cmd.cwd === undefined ? '.' : cmd.cwd;
|
|
950
|
+
let rewrite;
|
|
767
951
|
const error = validateSegment(cmd.text, context, cwd, (name) => evaluated.add(`${cmd.line}:${name}`), {
|
|
768
952
|
fenced: cmd.fenced === true,
|
|
769
953
|
makefileFor,
|
|
770
954
|
fileDir: fileDir.startsWith('..') ? '.' : fileDir,
|
|
955
|
+
tree,
|
|
956
|
+
suggestScript: (r) => { rewrite = r; },
|
|
771
957
|
});
|
|
772
958
|
if (!error)
|
|
773
959
|
continue;
|
|
960
|
+
if (error === UNJUDGED) {
|
|
961
|
+
const evidence = `Not checked: \`${cmd.text}\``;
|
|
962
|
+
const entry = unchecked.get(evidence) ?? { filePaths: new Set(), fileLines: new Map(), evidence };
|
|
963
|
+
entry.filePaths.add(file.path);
|
|
964
|
+
const seen = entry.fileLines.get(file.path);
|
|
965
|
+
if (seen === undefined || cmd.line < seen)
|
|
966
|
+
entry.fileLines.set(file.path, cmd.line);
|
|
967
|
+
unchecked.set(evidence, entry);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
774
970
|
// Deduplicate same command text appearing twice in the same file
|
|
775
971
|
const dedupeKey = `${file.path}|${cmd.text}`;
|
|
776
972
|
if (reported.has(dedupeKey))
|
|
777
973
|
continue;
|
|
778
974
|
reported.add(dedupeKey);
|
|
975
|
+
// pcic-oonz: a MISSPELLED script name with exactly one close match in the
|
|
976
|
+
// manifest that would have run it is a mechanical rewrite — provided the
|
|
977
|
+
// difference is a typo rather than a reference to a real sibling script
|
|
978
|
+
// (`isMechanicalRename`; `test:api` vs `test:app` is not), and the text
|
|
979
|
+
// to replace appears exactly once on the line, so the edit cannot land on
|
|
980
|
+
// another command that shares it.
|
|
981
|
+
const fix = rewrite?.mechanical && countOnLine(contentLines[cmd.line - 1] ?? '', rewrite.find) === 1
|
|
982
|
+
? rewrite
|
|
983
|
+
: undefined;
|
|
984
|
+
// A candidate too close to a different script to apply is still worth
|
|
985
|
+
// naming — cautiously, and in the recommendation rather than the summary,
|
|
986
|
+
// so nothing reads as a verdict.
|
|
987
|
+
const hint = rewrite && !fix
|
|
988
|
+
? ` The manifest defines "${rewrite.candidate}", which is close to "${rewrite.missing}"; ` +
|
|
989
|
+
'it may be what was meant, but it is near enough to be a different script, so check before changing it.'
|
|
990
|
+
: '';
|
|
779
991
|
issues.push({
|
|
780
992
|
id: issueId(fileIdPath(file), cmd.text),
|
|
781
993
|
severity: 'warning',
|
|
782
994
|
category: 'command_validity',
|
|
783
995
|
title: `Possible reference to missing command or file: \`${cmd.text.split(/\s+/).slice(0, 3).join(' ')}\``,
|
|
784
|
-
summary:
|
|
996
|
+
summary: fix
|
|
997
|
+
? `${error}. The closest script name the manifest defines is "${fix.candidate}".`
|
|
998
|
+
: error,
|
|
785
999
|
filePaths: [file.path],
|
|
786
1000
|
locations: [{ filePath: file.path, startLine: cmd.line, endLine: cmd.line }],
|
|
787
1001
|
evidence: [`Command: \`${cmd.text}\``, `In file: ${file.path}`],
|
|
788
1002
|
recommendation: 'Verify the command. If the script was renamed, update the instruction file. ' +
|
|
789
|
-
'If it is an illustrative example, wrap it in a placeholder like <script-name> to suppress this finding.'
|
|
1003
|
+
'If it is an illustrative example, wrap it in a placeholder like <script-name> to suppress this finding.' +
|
|
1004
|
+
hint,
|
|
1005
|
+
...(fix
|
|
1006
|
+
? {
|
|
1007
|
+
fixRecipe: `Rename the script in this command: \`${fix.find}\` -> \`${fix.replace}\`.`,
|
|
1008
|
+
// `applyFixRecipe` replaces that one occurrence on that one line.
|
|
1009
|
+
autoApplySafe: true,
|
|
1010
|
+
fixEdits: [{ filePath: file.path, line: cmd.line, find: fix.find, replace: fix.replace }],
|
|
1011
|
+
}
|
|
1012
|
+
: {}),
|
|
790
1013
|
confidence: 0.8,
|
|
791
1014
|
});
|
|
792
1015
|
}
|
|
793
1016
|
}
|
|
1017
|
+
if (unchecked.size > 0) {
|
|
1018
|
+
issues.push(incompleteCheckIssue(tree, [...unchecked.values()], {
|
|
1019
|
+
id: issueId('', '(command check incomplete)'),
|
|
1020
|
+
category: 'command_validity',
|
|
1021
|
+
title: 'Command check incomplete: part of the repository could not be read',
|
|
1022
|
+
unit: 'command',
|
|
1023
|
+
unknown: 'The files they name may or may not exist',
|
|
1024
|
+
finding: 'missing-command',
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
794
1027
|
return { issues, evaluatedScripts };
|
|
795
1028
|
}
|
|
796
1029
|
//# sourceMappingURL=command-validity.js.map
|