@promptci/core 0.0.1 → 0.2.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/LICENSE +201 -201
- package/README.md +29 -30
- package/dist/action-pinning.d.ts +37 -0
- package/dist/action-pinning.d.ts.map +1 -0
- package/dist/action-pinning.js +277 -0
- package/dist/action-pinning.js.map +1 -0
- package/dist/agent-practices.d.ts +3 -1
- package/dist/agent-practices.d.ts.map +1 -1
- package/dist/agent-practices.js +198 -94
- package/dist/agent-practices.js.map +1 -1
- package/dist/ai-config.d.ts +178 -0
- package/dist/ai-config.d.ts.map +1 -0
- package/dist/ai-config.js +721 -0
- package/dist/ai-config.js.map +1 -0
- package/dist/alias-files.d.ts +33 -0
- package/dist/alias-files.d.ts.map +1 -0
- package/dist/alias-files.js +60 -0
- package/dist/alias-files.js.map +1 -0
- package/dist/baseline.d.ts +17 -1
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +5 -1
- package/dist/baseline.js.map +1 -1
- package/dist/buried-critical.d.ts +17 -0
- package/dist/buried-critical.d.ts.map +1 -0
- package/dist/buried-critical.js +107 -0
- package/dist/buried-critical.js.map +1 -0
- package/dist/canonical-owner.d.ts.map +1 -1
- package/dist/canonical-owner.js +60 -30
- package/dist/canonical-owner.js.map +1 -1
- package/dist/ci-alignment.d.ts.map +1 -1
- package/dist/ci-alignment.js +3 -2
- package/dist/ci-alignment.js.map +1 -1
- package/dist/command-validity.d.ts +16 -0
- package/dist/command-validity.d.ts.map +1 -1
- package/dist/command-validity.js +387 -134
- package/dist/command-validity.js.map +1 -1
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/conflicts.js +4 -44
- package/dist/conflicts.js.map +1 -1
- package/dist/context-analysis.d.ts.map +1 -1
- package/dist/context-analysis.js +13 -2
- package/dist/context-analysis.js.map +1 -1
- package/dist/context-bloat.d.ts +20 -1
- package/dist/context-bloat.d.ts.map +1 -1
- package/dist/context-bloat.js +99 -15
- package/dist/context-bloat.js.map +1 -1
- package/dist/context-optimizer.d.ts.map +1 -1
- package/dist/context-optimizer.js +18 -54
- package/dist/context-optimizer.js.map +1 -1
- package/dist/copilot-instructions-detector.d.ts +24 -0
- package/dist/copilot-instructions-detector.d.ts.map +1 -0
- package/dist/copilot-instructions-detector.js +145 -0
- package/dist/copilot-instructions-detector.js.map +1 -0
- package/dist/cursor-rules-detector.d.ts +17 -0
- package/dist/cursor-rules-detector.d.ts.map +1 -0
- package/dist/cursor-rules-detector.js +110 -0
- package/dist/cursor-rules-detector.js.map +1 -0
- package/dist/custom-rules.d.ts +52 -0
- package/dist/custom-rules.d.ts.map +1 -0
- package/dist/custom-rules.js +303 -0
- package/dist/custom-rules.js.map +1 -0
- package/dist/dead-references.d.ts +8 -2
- package/dist/dead-references.d.ts.map +1 -1
- package/dist/dead-references.js +315 -68
- package/dist/dead-references.js.map +1 -1
- package/dist/dependency-staleness.d.ts +35 -0
- package/dist/dependency-staleness.d.ts.map +1 -0
- package/dist/dependency-staleness.js +1001 -0
- package/dist/dependency-staleness.js.map +1 -0
- package/dist/detectors.d.ts +1 -3
- package/dist/detectors.d.ts.map +1 -1
- package/dist/detectors.js +74 -56
- package/dist/detectors.js.map +1 -1
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +15 -9
- package/dist/duplicates.js.map +1 -1
- package/dist/evidence.d.ts +39 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +78 -0
- package/dist/evidence.js.map +1 -0
- package/dist/finding-id.d.ts +21 -0
- package/dist/finding-id.d.ts.map +1 -0
- package/dist/finding-id.js +31 -0
- package/dist/finding-id.js.map +1 -0
- package/dist/fix-engine.d.ts +11 -0
- package/dist/fix-engine.d.ts.map +1 -1
- package/dist/fix-engine.js +88 -71
- package/dist/fix-engine.js.map +1 -1
- package/dist/frameworks/dotnet.js +2 -2
- package/dist/frameworks/dotnet.js.map +1 -1
- package/dist/frameworks/python.d.ts.map +1 -1
- package/dist/frameworks/python.js +5 -2
- package/dist/frameworks/python.js.map +1 -1
- package/dist/frameworks/rust.d.ts.map +1 -1
- package/dist/frameworks/rust.js +2 -1
- package/dist/frameworks/rust.js.map +1 -1
- package/dist/git-symlinks.d.ts +30 -0
- package/dist/git-symlinks.d.ts.map +1 -0
- package/dist/git-symlinks.js +161 -0
- package/dist/git-symlinks.js.map +1 -0
- package/dist/gitignore.d.ts +22 -0
- package/dist/gitignore.d.ts.map +1 -0
- package/dist/gitignore.js +157 -0
- package/dist/gitignore.js.map +1 -0
- package/dist/health-score.d.ts.map +1 -1
- package/dist/health-score.js +35 -5
- package/dist/health-score.js.map +1 -1
- package/dist/hooks-settings-detector.d.ts +17 -0
- package/dist/hooks-settings-detector.d.ts.map +1 -0
- package/dist/hooks-settings-detector.js +228 -0
- package/dist/hooks-settings-detector.js.map +1 -0
- package/dist/index.d.ts +29 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -6
- package/dist/index.js.map +1 -1
- package/dist/makefile.d.ts +37 -0
- package/dist/makefile.d.ts.map +1 -0
- package/dist/makefile.js +156 -0
- package/dist/makefile.js.map +1 -0
- package/dist/manifest-consistency.d.ts +6 -0
- package/dist/manifest-consistency.d.ts.map +1 -1
- package/dist/manifest-consistency.js +75 -10
- package/dist/manifest-consistency.js.map +1 -1
- package/dist/markdown-fences.d.ts +112 -0
- package/dist/markdown-fences.d.ts.map +1 -0
- package/dist/markdown-fences.js +174 -0
- package/dist/markdown-fences.js.map +1 -0
- package/dist/mcp-config-detector.d.ts +20 -0
- package/dist/mcp-config-detector.d.ts.map +1 -0
- package/dist/mcp-config-detector.js +237 -0
- package/dist/mcp-config-detector.js.map +1 -0
- package/dist/missing-context.d.ts.map +1 -1
- package/dist/missing-context.js +2 -52
- package/dist/missing-context.js.map +1 -1
- package/dist/model-budgets.d.ts +69 -0
- package/dist/model-budgets.d.ts.map +1 -0
- package/dist/model-budgets.js +110 -0
- package/dist/model-budgets.js.map +1 -0
- package/dist/negative-instructions.d.ts +26 -0
- package/dist/negative-instructions.d.ts.map +1 -0
- package/dist/negative-instructions.js +169 -0
- package/dist/negative-instructions.js.map +1 -0
- package/dist/path-containment.d.ts +41 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +123 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/project-type.d.ts +12 -2
- package/dist/project-type.d.ts.map +1 -1
- package/dist/project-type.js +25 -12
- package/dist/project-type.js.map +1 -1
- package/dist/prompt-cache.d.ts.map +1 -1
- package/dist/prompt-cache.js +33 -24
- package/dist/prompt-cache.js.map +1 -1
- package/dist/promptci-gitignore.d.ts +34 -0
- package/dist/promptci-gitignore.d.ts.map +1 -0
- package/dist/promptci-gitignore.js +42 -0
- package/dist/promptci-gitignore.js.map +1 -0
- package/dist/repo-context.d.ts +80 -0
- package/dist/repo-context.d.ts.map +1 -1
- package/dist/repo-context.js +170 -16
- package/dist/repo-context.js.map +1 -1
- package/dist/report.d.ts +8 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +163 -63
- package/dist/report.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +47 -4
- package/dist/scan.js.map +1 -1
- package/dist/scanner.d.ts +61 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +367 -70
- package/dist/scanner.js.map +1 -1
- package/dist/security-pack.d.ts +20 -0
- package/dist/security-pack.d.ts.map +1 -1
- package/dist/security-pack.js +33 -14
- package/dist/security-pack.js.map +1 -1
- package/dist/skill-supply-chain.d.ts +132 -0
- package/dist/skill-supply-chain.d.ts.map +1 -0
- package/dist/skill-supply-chain.js +2954 -0
- package/dist/skill-supply-chain.js.map +1 -0
- package/dist/skills-detector.d.ts +23 -0
- package/dist/skills-detector.d.ts.map +1 -0
- package/dist/skills-detector.js +279 -0
- package/dist/skills-detector.js.map +1 -0
- package/dist/stale-instructions.d.ts +4 -1
- package/dist/stale-instructions.d.ts.map +1 -1
- package/dist/stale-instructions.js +53 -55
- package/dist/stale-instructions.js.map +1 -1
- package/dist/subagents-detector.d.ts +19 -0
- package/dist/subagents-detector.d.ts.map +1 -0
- package/dist/subagents-detector.js +155 -0
- package/dist/subagents-detector.js.map +1 -0
- package/dist/suppression.d.ts +2 -0
- package/dist/suppression.d.ts.map +1 -1
- package/dist/suppression.js +16 -47
- package/dist/suppression.js.map +1 -1
- package/dist/types.d.ts +62 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +47 -1
- package/dist/types.js.map +1 -1
- package/dist/vague-guidance.d.ts +15 -2
- package/dist/vague-guidance.d.ts.map +1 -1
- package/dist/vague-guidance.js +44 -41
- package/dist/vague-guidance.js.map +1 -1
- package/dist/windsurf-detector.d.ts +22 -0
- package/dist/windsurf-detector.d.ts.map +1 -0
- package/dist/windsurf-detector.js +96 -0
- package/dist/windsurf-detector.js.map +1 -0
- package/dist/within-section-dedup.d.ts +16 -0
- package/dist/within-section-dedup.d.ts.map +1 -0
- package/dist/within-section-dedup.js +148 -0
- package/dist/within-section-dedup.js.map +1 -0
- package/dist/workspace-manifests.d.ts +80 -0
- package/dist/workspace-manifests.d.ts.map +1 -0
- package/dist/workspace-manifests.js +333 -0
- package/dist/workspace-manifests.js.map +1 -0
- package/package.json +11 -8
- package/dist/cheap-model-routing.d.ts +0 -4
- package/dist/cheap-model-routing.d.ts.map +0 -1
- package/dist/cheap-model-routing.js +0 -43
- package/dist/cheap-model-routing.js.map +0 -1
- package/dist/cli-output-filter.d.ts +0 -16
- package/dist/cli-output-filter.d.ts.map +0 -1
- package/dist/cli-output-filter.js +0 -88
- package/dist/cli-output-filter.js.map +0 -1
- package/dist/context-cost.d.ts +0 -4
- package/dist/context-cost.d.ts.map +0 -1
- package/dist/context-cost.js +0 -70
- package/dist/context-cost.js.map +0 -1
- package/dist/context-recommender.d.ts +0 -3
- package/dist/context-recommender.d.ts.map +0 -1
- package/dist/context-recommender.js +0 -329
- package/dist/context-recommender.js.map +0 -1
- package/dist/llm.d.ts +0 -6
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js +0 -67
- package/dist/llm.js.map +0 -1
- package/dist/mcp-detector.d.ts +0 -4
- package/dist/mcp-detector.d.ts.map +0 -1
- package/dist/mcp-detector.js +0 -131
- package/dist/mcp-detector.js.map +0 -1
- package/dist/native-tool-preference.d.ts +0 -20
- package/dist/native-tool-preference.d.ts.map +0 -1
- package/dist/native-tool-preference.js +0 -94
- package/dist/native-tool-preference.js.map +0 -1
- package/dist/output-verbosity.d.ts +0 -3
- package/dist/output-verbosity.d.ts.map +0 -1
- package/dist/output-verbosity.js +0 -83
- package/dist/output-verbosity.js.map +0 -1
- package/dist/product-boundary.d.ts +0 -11
- package/dist/product-boundary.d.ts.map +0 -1
- package/dist/product-boundary.js +0 -301
- package/dist/product-boundary.js.map +0 -1
package/dist/command-validity.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
|
+
import { fencedBlocks, scanFencedLines } from './markdown-fences.js';
|
|
5
|
+
import { fileIdPath } from './finding-id.js';
|
|
6
|
+
import { anyWorkspaceHasScript, isKnownBinary, selectWorkspaces } from './workspace-manifests.js';
|
|
7
|
+
import { makefileDefines, readMakefile } from './makefile.js';
|
|
8
|
+
import { realPathWithinRoot } from './path-containment.js';
|
|
4
9
|
/**
|
|
5
10
|
* Command Validity Detector
|
|
6
11
|
*
|
|
@@ -36,7 +41,18 @@ function fileExists(repoRoot, relativePath) {
|
|
|
36
41
|
return false;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
|
-
function
|
|
44
|
+
function isDirectory(repoRoot, relativePath) {
|
|
45
|
+
try {
|
|
46
|
+
return fs.statSync(path.resolve(repoRoot, relativePath)).isDirectory();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function readPackageScripts(repoRoot, packageJsonPath) {
|
|
53
|
+
// A manifest committed as a symlink to a file outside the repo is not read.
|
|
54
|
+
if (!realPathWithinRoot(repoRoot, packageJsonPath))
|
|
55
|
+
return null;
|
|
40
56
|
try {
|
|
41
57
|
const raw = fs.readFileSync(packageJsonPath, 'utf8');
|
|
42
58
|
const parsed = JSON.parse(raw);
|
|
@@ -101,8 +117,14 @@ function workspaceHasScript(context, selector, scriptName) {
|
|
|
101
117
|
const normalizedSelector = normalizePnpmFilter(selector);
|
|
102
118
|
if (!normalizedSelector)
|
|
103
119
|
return false;
|
|
120
|
+
// Workspace manifests discovered while building the context (declared
|
|
121
|
+
// `workspaces` globs, any layout). The path-based lookup below is only for
|
|
122
|
+
// contexts built without them.
|
|
123
|
+
if (context.workspaces) {
|
|
124
|
+
return selectWorkspaces(context, normalizedSelector).some((ws) => !!ws.scripts[scriptName]);
|
|
125
|
+
}
|
|
104
126
|
for (const candidate of workspacePackageJsonCandidates(context.repoRoot, normalizedSelector)) {
|
|
105
|
-
const pkg = readPackageScripts(candidate);
|
|
127
|
+
const pkg = readPackageScripts(context.repoRoot, candidate);
|
|
106
128
|
if (!pkg)
|
|
107
129
|
continue;
|
|
108
130
|
const packageDir = path.dirname(candidate).replace(/\\/g, '/');
|
|
@@ -129,17 +151,32 @@ const PNPM_BUILTINS = new Set([
|
|
|
129
151
|
'install', 'i', 'add', 'remove', 'uninstall', 'test', 'it', 'init',
|
|
130
152
|
'create', 'publish', 'link', 'unlink', 'outdated', 'audit', 'update',
|
|
131
153
|
'up', 'exec', 'dlx', 'run', 'store', 'list', 'ls', 'why', 'pack',
|
|
132
|
-
'recursive', 'm', 'multi', 'prune',
|
|
154
|
+
'recursive', 'm', 'multi', 'prune', 'version', 'run-script',
|
|
155
|
+
'env', 'config', 'fetch', 'rebuild', 'rb', 'patch', 'patch-commit', 'deploy',
|
|
156
|
+
'import', 'licenses', 'root', 'bin', 'setup', 'self-update', 'dedupe', 'doctor',
|
|
157
|
+
'approve-builds', 'sbom', 'cache',
|
|
133
158
|
]);
|
|
134
159
|
const NPM_BUILTINS = new Set([
|
|
135
160
|
'install', 'i', 'ci', 'uninstall', 'remove', 'update', 'outdated',
|
|
136
161
|
'audit', 'start', 'stop', 'restart', 'test', 'it', 'init', 'create',
|
|
137
|
-
'publish', 'link', 'unlink', 'pack', 'exec', 'run',
|
|
162
|
+
'publish', 'link', 'unlink', 'pack', 'exec', 'run', 'run-script', 'version',
|
|
138
163
|
]);
|
|
139
164
|
const YARN_BUILTINS = new Set([
|
|
140
165
|
'install', 'add', 'remove', 'upgrade', 'outdated', 'audit', 'init',
|
|
141
|
-
'create', 'publish', 'link', 'unlink', 'why', 'pack', 'run',
|
|
166
|
+
'create', 'publish', 'link', 'unlink', 'why', 'pack', 'run', 'version',
|
|
167
|
+
'run-script', 'workspaces', 'exec', 'dlx', 'up', 'info', 'config', 'cache',
|
|
168
|
+
'bin', 'set', 'node', 'global', 'dedupe', 'patch', 'rebuild', 'plugin',
|
|
169
|
+
'constraints', 'unplug', 'npm',
|
|
170
|
+
]);
|
|
171
|
+
/** `bun <name>` runs a script (or file) only when <name> is not one of bun's own commands. */
|
|
172
|
+
export const BUN_BUILTINS = new Set([
|
|
173
|
+
'install', 'i', 'add', 'a', 'remove', 'rm', 'update', 'test', 'build', 'init',
|
|
174
|
+
'create', 'c', 'x', 'bunx', 'pm', 'link', 'unlink', 'upgrade', 'repl', 'outdated',
|
|
175
|
+
'publish', 'patch', 'audit', 'info', 'why', 'completions', 'help', 'feedback',
|
|
176
|
+
'run', 'exec',
|
|
142
177
|
]);
|
|
178
|
+
/** npm and pnpm treat `run-script` as an alias of `run`. */
|
|
179
|
+
const RUN_ALIASES = new Set(['run', 'run-script']);
|
|
143
180
|
/**
|
|
144
181
|
* CV6: Resolves a `cd <target>` against the current repo-relative directory so
|
|
145
182
|
* that a `cd apps/web && pnpm test:e2e` line validates `test:e2e` against
|
|
@@ -154,6 +191,10 @@ function resolveCwd(current, target) {
|
|
|
154
191
|
if (!cleaned || cleaned.startsWith('/') || cleaned.startsWith('~') || /^[a-zA-Z]:[\\/]/.test(cleaned)) {
|
|
155
192
|
return null; // absolute or home path — not resolvable to a repo-relative dir
|
|
156
193
|
}
|
|
194
|
+
// `cd -`, `cd $DIR`, `cd "$(git rev-parse --show-toplevel)"`, `cd %TEMP%`,
|
|
195
|
+
// `cd <dir>`: computed or placeholder targets name no knowable directory.
|
|
196
|
+
if (/^-|[$`%<>{}*?]/.test(cleaned))
|
|
197
|
+
return null;
|
|
157
198
|
const parts = current === '.' ? [] : current.split('/');
|
|
158
199
|
for (const seg of cleaned.replace(/\\/g, '/').split('/')) {
|
|
159
200
|
if (seg === '' || seg === '.')
|
|
@@ -214,80 +255,57 @@ const SHELL_FENCE_LANGS = new Set([
|
|
|
214
255
|
/** Extracts commands from a single instruction-file content string. */
|
|
215
256
|
function extractCommands(content) {
|
|
216
257
|
const commands = [];
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
fenceChar = openMatch[1][0] ?? null;
|
|
243
|
-
fenceLen = openMatch[1].length;
|
|
244
|
-
const lang = (openMatch[2] ?? '').toLowerCase();
|
|
245
|
-
inShellBlock = lang === '' || SHELL_FENCE_LANGS.has(lang);
|
|
246
|
-
blockStartLine = i + 1;
|
|
247
|
-
blockContent = [];
|
|
258
|
+
// Fenced blocks first, prose second, then sorted back into document order
|
|
259
|
+
// below — findings quote the first occurrence of a command, so the order the
|
|
260
|
+
// two passes produce has to match the order a single interleaved pass did.
|
|
261
|
+
//
|
|
262
|
+
// Only CLOSED blocks contribute: an unclosed fence is a malformed document,
|
|
263
|
+
// and its tail was never meant to read as example commands. Non-shell fences
|
|
264
|
+
// (```json, ```ts, …) are skipped by language — tracking them at all is what
|
|
265
|
+
// keeps a ```json block's bare ``` closer from being misread as an opener,
|
|
266
|
+
// which used to invert the in-block state for the rest of the file.
|
|
267
|
+
for (const block of fencedBlocks(content)) {
|
|
268
|
+
// The shared scanner reports the full info string ('{.bash}', 'json,');
|
|
269
|
+
// classify on its leading identifier run, matching the historical opener
|
|
270
|
+
// regex `([a-zA-Z0-9_-]*)`. That keeps a pandoc-attribute fence like
|
|
271
|
+
// ```{.bash} classified as bare-and-therefore-shell (identifier run is
|
|
272
|
+
// empty) instead of being silently skipped as an unknown language.
|
|
273
|
+
const lang = /^[a-zA-Z0-9_-]*/.exec(block.lang)[0];
|
|
274
|
+
const isShell = lang === '' || SHELL_FENCE_LANGS.has(lang);
|
|
275
|
+
if (!isShell || !block.closed)
|
|
276
|
+
continue;
|
|
277
|
+
// Each entry's index corresponds 1:1 to its physical line offset from the
|
|
278
|
+
// block's first content line, so blank leading lines cannot shift the
|
|
279
|
+
// reported line number.
|
|
280
|
+
for (let j = 0; j < block.lines.length; j++) {
|
|
281
|
+
const rawTrimmed = block.lines[j].trim();
|
|
282
|
+
if (rawTrimmed.startsWith('#'))
|
|
248
283
|
continue;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
else {
|
|
252
|
-
const closeMatch = /^ {0,3}([`~]{3,})\s*$/.exec(line);
|
|
253
|
-
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLen) {
|
|
254
|
-
inBlock = false;
|
|
255
|
-
if (inShellBlock) {
|
|
256
|
-
for (let j = 0; j < blockContent.length; j++) {
|
|
257
|
-
const raw = blockContent[j];
|
|
258
|
-
const rawTrimmed = raw.trim();
|
|
259
|
-
if (rawTrimmed.startsWith('#'))
|
|
260
|
-
continue;
|
|
261
|
-
const trimmed = rawTrimmed.replace(/^\$\s*/, '');
|
|
262
|
-
if (!trimmed)
|
|
263
|
-
continue;
|
|
264
|
-
// CV6: each documented line is treated as an independent example run
|
|
265
|
-
// from the repo root, so `cd` only affects commands chained after it
|
|
266
|
-
// on the SAME line (e.g. `cd apps/web && pnpm test:e2e`). Setup blocks
|
|
267
|
-
// are written this way — repeating `cd apps/web` on separate lines —
|
|
268
|
-
// so a standalone `cd` must not bleed into later lines.
|
|
269
|
-
let lineCwd = '.';
|
|
270
|
-
for (const seg of splitOnAmpersand(trimmed)) {
|
|
271
|
-
const cdMatch = /^cd\s+(.+)$/.exec(seg);
|
|
272
|
-
if (cdMatch) {
|
|
273
|
-
lineCwd = resolveCwd(lineCwd, cdMatch[1].trim());
|
|
274
|
-
}
|
|
275
|
-
commands.push({ text: seg, line: blockStartLine + j + 1, cwd: lineCwd });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
inShellBlock = false;
|
|
280
|
-
fenceChar = null;
|
|
281
|
-
fenceLen = 0;
|
|
284
|
+
const trimmed = rawTrimmed.replace(/^\$\s*/, '');
|
|
285
|
+
if (!trimmed)
|
|
282
286
|
continue;
|
|
287
|
+
// CV6: each documented line is treated as an independent example run
|
|
288
|
+
// from the repo root, so `cd` only affects commands chained after it
|
|
289
|
+
// on the SAME line (e.g. `cd apps/web && pnpm test:e2e`). Setup blocks
|
|
290
|
+
// are written this way — repeating `cd apps/web` on separate lines —
|
|
291
|
+
// so a standalone `cd` must not bleed into later lines.
|
|
292
|
+
let lineCwd = '.';
|
|
293
|
+
for (const seg of splitOnAmpersand(trimmed)) {
|
|
294
|
+
const cdMatch = /^cd\s+(.+)$/.exec(seg);
|
|
295
|
+
if (cdMatch) {
|
|
296
|
+
lineCwd = resolveCwd(lineCwd, cdMatch[1].trim());
|
|
297
|
+
}
|
|
298
|
+
commands.push({ text: seg, line: block.contentStartLine + j, cwd: lineCwd, fenced: true });
|
|
283
299
|
}
|
|
284
|
-
// Still inside the fence — accumulate only if it's a shell-ish block;
|
|
285
|
-
// non-shell fences (```json, ```ts, …) are tracked for state but their
|
|
286
|
-
// content is never scanned for commands.
|
|
287
|
-
if (inShellBlock)
|
|
288
|
-
blockContent.push(line);
|
|
289
|
-
continue;
|
|
290
300
|
}
|
|
301
|
+
}
|
|
302
|
+
for (const fenceLine of scanFencedLines(content)) {
|
|
303
|
+
if (fenceLine.inFence)
|
|
304
|
+
continue;
|
|
305
|
+
const i = fenceLine.lineNumber - 1;
|
|
306
|
+
// A trailing CR would block the prose patterns below: `.` does not match
|
|
307
|
+
// it, so the end-of-line lookahead never fires on a CRLF file.
|
|
308
|
+
const line = fenceLine.text.replace(/\r$/, '');
|
|
291
309
|
// Inline backticks
|
|
292
310
|
for (const m of line.matchAll(/`([^`]+)`/g)) {
|
|
293
311
|
const inner = m[1].trim();
|
|
@@ -312,7 +330,8 @@ function extractCommands(content) {
|
|
|
312
330
|
commands.push({ text: fullCmd, line: i + 1 });
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
|
-
|
|
333
|
+
// Stable sort: segments split from one line keep their left-to-right order.
|
|
334
|
+
return commands.sort((a, b) => a.line - b.line);
|
|
316
335
|
}
|
|
317
336
|
// ── Command validation ────────────────────────────────────────────────────────
|
|
318
337
|
/** Placeholder patterns that indicate a command is intentionally abstract. */
|
|
@@ -324,10 +343,15 @@ const PLACEHOLDER_RE = /[<{]|\.{3,}|your-file|your-script|example|placeholder/i;
|
|
|
324
343
|
* like `--filter` or `--loader` gets mistaken for the subcommand, and its
|
|
325
344
|
* VALUE (a package name or loader module) gets mistaken for a file path.
|
|
326
345
|
*/
|
|
327
|
-
const FLAGS_WITH_VALUE = new Set([
|
|
346
|
+
const FLAGS_WITH_VALUE = new Set([
|
|
347
|
+
'--filter', '-F', '--cwd', '-C', '--dir', '--loader', '--require', '-r', '--workspace', '--prefix',
|
|
348
|
+
]);
|
|
328
349
|
function flagTakesValue(tool, flag) {
|
|
329
350
|
if (tool === 'pnpm' && flag === '-r')
|
|
330
351
|
return false;
|
|
352
|
+
// npm `-w <workspace>` takes a value; pnpm's `-w` (workspace root) does not.
|
|
353
|
+
if (flag === '-w')
|
|
354
|
+
return tool === 'npm';
|
|
331
355
|
return FLAGS_WITH_VALUE.has(flag);
|
|
332
356
|
}
|
|
333
357
|
/**
|
|
@@ -341,8 +365,8 @@ function flagTakesValue(tool, flag) {
|
|
|
341
365
|
* "ts-node/esm" (the loader flag's VALUE, which happens to contain a slash)
|
|
342
366
|
* as the file being executed, instead of the real target "src/x.ts".
|
|
343
367
|
*/
|
|
344
|
-
function firstNonFlagArgIndex(parts) {
|
|
345
|
-
let idx =
|
|
368
|
+
function firstNonFlagArgIndex(parts, startIdx = 1) {
|
|
369
|
+
let idx = startIdx;
|
|
346
370
|
const tool = parts[0] ?? '';
|
|
347
371
|
while (idx < parts.length) {
|
|
348
372
|
const p = parts[idx];
|
|
@@ -355,30 +379,72 @@ function firstNonFlagArgIndex(parts) {
|
|
|
355
379
|
}
|
|
356
380
|
return idx;
|
|
357
381
|
}
|
|
358
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
382
|
+
/** The `--filter`/`-F` (pnpm, bun) or `-w`/`--workspace` (npm) selector, if any. */
|
|
383
|
+
function workspaceSelector(tool, parts) {
|
|
384
|
+
const filter = pnpmFilterValue(parts);
|
|
385
|
+
if (filter)
|
|
386
|
+
return filter;
|
|
387
|
+
if (tool !== 'npm')
|
|
388
|
+
return undefined;
|
|
389
|
+
for (let i = 1; i < parts.length; i++) {
|
|
390
|
+
const part = parts[i];
|
|
391
|
+
if (part === '-w' || part === '--workspace')
|
|
392
|
+
return parts[i + 1];
|
|
393
|
+
if (part.startsWith('--workspace='))
|
|
394
|
+
return part.slice('--workspace='.length);
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
/** `pnpm -r` / `pnpm --recursive` / `npm --workspaces`: the script may live in any workspace. */
|
|
399
|
+
function runsInAllWorkspaces(tool, parts) {
|
|
400
|
+
if (tool === 'pnpm')
|
|
401
|
+
return parts.some((p) => p === '-r' || p === '--recursive');
|
|
402
|
+
if (tool === 'npm')
|
|
403
|
+
return parts.some((p) => p === '--workspaces' || p === '-ws');
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
/** A directory override (`--cwd`, `-C`, `--prefix`) re-roots the command like a `cd` would. */
|
|
407
|
+
function cwdOverride(parts) {
|
|
408
|
+
for (let i = 1; i < parts.length; i++) {
|
|
409
|
+
const part = parts[i];
|
|
410
|
+
if (part === '--cwd' || part === '-C' || part === '--dir' || part === '--prefix')
|
|
411
|
+
return parts[i + 1];
|
|
412
|
+
const eq = /^(?:--cwd|--dir|--prefix)=(.+)$/.exec(part);
|
|
413
|
+
if (eq)
|
|
414
|
+
return eq[1];
|
|
415
|
+
}
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
function findMissingScript(context, cwd, parts, scriptName, builtins, tool, lookup, onScript) {
|
|
376
419
|
const names = scriptName.includes('/')
|
|
377
420
|
? scriptName.split('/').map((s) => s.trim()).filter(Boolean)
|
|
378
421
|
: [scriptName];
|
|
422
|
+
// Recorded before any early return so callers know this command was judged —
|
|
423
|
+
// including when it could not be checked (unknown working directory).
|
|
424
|
+
for (const name of names)
|
|
425
|
+
onScript?.(name);
|
|
426
|
+
// Unknown working directory (unresolvable `cd`) — cannot verify.
|
|
427
|
+
if (cwd === null)
|
|
428
|
+
return null;
|
|
429
|
+
const override = cwdOverride(parts);
|
|
430
|
+
if (override !== undefined) {
|
|
431
|
+
const resolved = resolveCwd(cwd, override);
|
|
432
|
+
if (resolved === null)
|
|
433
|
+
return null;
|
|
434
|
+
cwd = resolved;
|
|
435
|
+
}
|
|
436
|
+
const selector = lookup.workspaceName ?? workspaceSelector(tool, parts);
|
|
437
|
+
const allWorkspaces = runsInAllWorkspaces(tool, parts);
|
|
438
|
+
const inWorkspaceScope = (name) => allWorkspaces ? anyWorkspaceHasScript(context, name) : workspaceHasScript(context, selector, name);
|
|
439
|
+
// With discovered workspaces, an explicit selector means ONLY the selected
|
|
440
|
+
// packages: the root manifest's same-named script is not what runs, and a
|
|
441
|
+
// selector that matches no package is itself the error.
|
|
442
|
+
const selectedWorkspaces = selector && context.workspaces && !allWorkspaces
|
|
443
|
+
? selectWorkspaces(context, selector.replace(/^['"]|['"]$/g, '').replace(/^\.\.\./, '').replace(/\.\.\.$/, '').trim())
|
|
444
|
+
: undefined;
|
|
379
445
|
const inSubdir = cwd !== '.' && cwd !== '';
|
|
380
446
|
const dirPkg = inSubdir
|
|
381
|
-
? readPackageScripts(path.join(context.repoRoot, cwd, 'package.json'))
|
|
447
|
+
? readPackageScripts(context.repoRoot, path.join(context.repoRoot, cwd, 'package.json'))
|
|
382
448
|
: null;
|
|
383
449
|
// cd'd into a directory whose package.json we can't read — don't guess.
|
|
384
450
|
if (inSubdir && !dirPkg)
|
|
@@ -390,6 +456,23 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool) {
|
|
|
390
456
|
for (const name of names) {
|
|
391
457
|
if (builtins.has(name))
|
|
392
458
|
continue;
|
|
459
|
+
// `pnpm vitest run`, `yarn tsc --noEmit`: no script of that name, but it is
|
|
460
|
+
// an installed dependency's binary — the package manager runs that instead.
|
|
461
|
+
if (lookup.allowBinary && isKnownBinary(context, name))
|
|
462
|
+
continue;
|
|
463
|
+
if (selectedWorkspaces) {
|
|
464
|
+
if (selectedWorkspaces.length === 0) {
|
|
465
|
+
if (context.workspacesTruncated)
|
|
466
|
+
continue; // only a sample of the workspace was read
|
|
467
|
+
return `no workspace matches "${selector}" for ${tool} script "${name}"`;
|
|
468
|
+
}
|
|
469
|
+
if (selectedWorkspaces.some((ws) => !!ws.scripts[name]))
|
|
470
|
+
continue;
|
|
471
|
+
const target = selectedWorkspaces.length === 1
|
|
472
|
+
? `${selectedWorkspaces[0].name ?? selectedWorkspaces[0].dir} (${selectedWorkspaces[0].dir}/package.json)`
|
|
473
|
+
: `the ${selectedWorkspaces.length} workspaces selected by "${selector}"`;
|
|
474
|
+
return `${tool} script "${name}" does not appear in ${target} scripts`;
|
|
475
|
+
}
|
|
393
476
|
if (dirPkg) {
|
|
394
477
|
// A command that runs inside a subdir resolves scripts ONLY from that
|
|
395
478
|
// subdir's manifest (pnpm/npm/yarn do not search parent directories) or
|
|
@@ -398,12 +481,12 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool) {
|
|
|
398
481
|
// script.
|
|
399
482
|
if (dirPkg.scripts[name])
|
|
400
483
|
continue;
|
|
401
|
-
if (
|
|
484
|
+
if (inWorkspaceScope(name))
|
|
402
485
|
continue;
|
|
403
486
|
return `${tool} script "${name}" does not appear in ${cwd}/package.json scripts`;
|
|
404
487
|
}
|
|
405
|
-
// Repo-root context: the
|
|
406
|
-
if (
|
|
488
|
+
// Repo-root context: the workspace target(s) or the root manifest.
|
|
489
|
+
if (inWorkspaceScope(name))
|
|
407
490
|
continue;
|
|
408
491
|
if (context.packageJson.scripts[name])
|
|
409
492
|
continue;
|
|
@@ -411,6 +494,141 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool) {
|
|
|
411
494
|
}
|
|
412
495
|
return null;
|
|
413
496
|
}
|
|
497
|
+
const BUILTINS_BY_TOOL = {
|
|
498
|
+
pnpm: PNPM_BUILTINS,
|
|
499
|
+
npm: NPM_BUILTINS,
|
|
500
|
+
yarn: YARN_BUILTINS,
|
|
501
|
+
// bun's own commands are filtered before this lookup (bare form only), so
|
|
502
|
+
// `bun run test` is judged against the manifest like any other script.
|
|
503
|
+
bun: new Set(),
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* Resolves which script (or file, for bun) a `pnpm`/`npm`/`yarn`/`bun` command
|
|
507
|
+
* invokes and checks it. Handles the `run` / `run-script` forms, the bare
|
|
508
|
+
* `pnpm <script>` / `yarn <script>` / `bun <script>` forms (which fall through
|
|
509
|
+
* to an installed binary), `yarn workspace <name> <script>`, and workspace or
|
|
510
|
+
* directory selectors.
|
|
511
|
+
*/
|
|
512
|
+
function validatePackageManagerScript(tool, parts, context, cwd, baseDir, onScript) {
|
|
513
|
+
const subIdx = firstNonFlagArgIndex(parts);
|
|
514
|
+
const sub = parts[subIdx];
|
|
515
|
+
if (!sub)
|
|
516
|
+
return null;
|
|
517
|
+
let explicitRun = false;
|
|
518
|
+
let nameIdx = subIdx;
|
|
519
|
+
let workspaceName;
|
|
520
|
+
if (RUN_ALIASES.has(sub)) {
|
|
521
|
+
explicitRun = true;
|
|
522
|
+
nameIdx = firstNonFlagArgIndex(parts, subIdx + 1);
|
|
523
|
+
}
|
|
524
|
+
else if (tool === 'npm') {
|
|
525
|
+
return null; // a bare `npm <x>` is an npm command, never a script
|
|
526
|
+
}
|
|
527
|
+
else if (tool === 'yarn' && sub === 'workspace') {
|
|
528
|
+
workspaceName = parts[subIdx + 1];
|
|
529
|
+
nameIdx = subIdx + 2;
|
|
530
|
+
if (RUN_ALIASES.has(parts[nameIdx] ?? '')) {
|
|
531
|
+
explicitRun = true;
|
|
532
|
+
nameIdx++;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const scriptName = parts[nameIdx];
|
|
536
|
+
if (!scriptName || scriptName.startsWith('-'))
|
|
537
|
+
return null;
|
|
538
|
+
if (tool === 'bun') {
|
|
539
|
+
// `bun <x>` / `bun run <x>` also run files — a path is not a script name.
|
|
540
|
+
if (!explicitRun && BUN_BUILTINS.has(scriptName))
|
|
541
|
+
return null;
|
|
542
|
+
if (isLikelyFilePath(scriptName)) {
|
|
543
|
+
if (baseDir && /\.[cm]?[jt]sx?$/.test(scriptName) && !fileExists(baseDir, scriptName)) {
|
|
544
|
+
return `File "${scriptName}" referenced by "bun" does not appear to exist in the repository`;
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return findMissingScript(context, cwd, parts, scriptName, BUILTINS_BY_TOOL[tool], tool,
|
|
550
|
+
// npm/pnpm `run` only run scripts; every other form can fall through to a binary.
|
|
551
|
+
{ allowBinary: !(explicitRun && (tool === 'npm' || tool === 'pnpm')), workspaceName }, onScript);
|
|
552
|
+
}
|
|
553
|
+
// ── make targets ──────────────────────────────────────────────────────────────
|
|
554
|
+
/** make options whose value is the NEXT token (unless attached: `-Cdir`, `-j4`). */
|
|
555
|
+
const MAKE_FLAGS_WITH_VALUE = new Set([
|
|
556
|
+
'-C', '-f', '-o', '-W', '-I', '-E', '--directory', '--file', '--makefile', '--old-file', '--what-if',
|
|
557
|
+
'--include-dir', '--new-file', '--assume-old', '--assume-new', '--eval',
|
|
558
|
+
]);
|
|
559
|
+
/** Words that follow "make" in English prose (`make sure`), never checked as targets in a code span. */
|
|
560
|
+
const MAKE_PROSE_WORDS = new Set([
|
|
561
|
+
'sure', 'it', 'this', 'that', 'them', 'these', 'those', 'certain', 'sense', 'changes', 'room', 'use', 'way',
|
|
562
|
+
]);
|
|
563
|
+
/** make options with an OPTIONAL numeric value (`-j`, `-j 4`, `-l 2.5`). */
|
|
564
|
+
const MAKE_FLAGS_WITH_OPTIONAL_NUMBER = new Set(['-j', '-l', '--jobs', '--load-average', '--max-load']);
|
|
565
|
+
/**
|
|
566
|
+
* pcic-2b6.10: `make <target>` against the Makefile make would read. Reports a
|
|
567
|
+
* target only when a Makefile exists in the command's directory, this reader
|
|
568
|
+
* could enumerate every target it defines (no `include`, no computed names),
|
|
569
|
+
* and neither an explicit target, a `.PHONY` name nor a pattern rule covers it.
|
|
570
|
+
* Variable assignments (`make X=1`), file-path goals (`make build/app.o`) and
|
|
571
|
+
* names make could build from a same-stem source via its built-in implicit
|
|
572
|
+
* rules (`make hello` next to `hello.c`) are never reported.
|
|
573
|
+
*/
|
|
574
|
+
function validateMakeTargets(parts, context, cwd, makefileFor) {
|
|
575
|
+
let dir = cwd;
|
|
576
|
+
const goals = [];
|
|
577
|
+
for (let i = 1; i < parts.length; i++) {
|
|
578
|
+
const p = parts[i];
|
|
579
|
+
// Shell plumbing ends the make invocation.
|
|
580
|
+
if (/^(?:[|&<>#]|\d*>)/.test(p))
|
|
581
|
+
break;
|
|
582
|
+
if (p.startsWith('-')) {
|
|
583
|
+
const eq = p.indexOf('=');
|
|
584
|
+
const name = eq === -1 ? p : p.slice(0, eq);
|
|
585
|
+
if (name === '-f' || name === '--file' || name === '--makefile' || /^-f./.test(p))
|
|
586
|
+
return null; // another file
|
|
587
|
+
let value;
|
|
588
|
+
if (MAKE_FLAGS_WITH_VALUE.has(name)) {
|
|
589
|
+
value = eq !== -1 ? p.slice(eq + 1) : parts[++i];
|
|
590
|
+
}
|
|
591
|
+
else if (/^-C./.test(p)) {
|
|
592
|
+
value = p.slice(2);
|
|
593
|
+
}
|
|
594
|
+
else if (MAKE_FLAGS_WITH_OPTIONAL_NUMBER.has(p) && /^\d+(?:\.\d+)?$/.test(parts[i + 1] ?? '')) {
|
|
595
|
+
i++;
|
|
596
|
+
}
|
|
597
|
+
if ((name === '-C' || name === '--directory' || /^-C./.test(p)) && value !== undefined) {
|
|
598
|
+
dir = resolveCwd(dir, value);
|
|
599
|
+
}
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (p.includes('='))
|
|
603
|
+
continue; // `make CC=clang build`
|
|
604
|
+
goals.push(p);
|
|
605
|
+
}
|
|
606
|
+
if (dir === null || goals.length === 0)
|
|
607
|
+
return null;
|
|
608
|
+
const facts = makefileFor(dir);
|
|
609
|
+
if (!facts || facts.unverifiable)
|
|
610
|
+
return null;
|
|
611
|
+
const baseDir = dir === '.' ? context.repoRoot : path.join(context.repoRoot, dir);
|
|
612
|
+
let entries;
|
|
613
|
+
for (const goal of goals) {
|
|
614
|
+
const target = goal.replace(/^['"]|['"]$/g, '');
|
|
615
|
+
if (!target || isLikelyFilePath(target) || !/^[A-Za-z0-9_.+-]+$/.test(target))
|
|
616
|
+
continue;
|
|
617
|
+
if (makefileDefines(facts, target))
|
|
618
|
+
continue;
|
|
619
|
+
// make can build `target` from `target.c` etc. with its built-in rules.
|
|
620
|
+
try {
|
|
621
|
+
entries ??= fs.readdirSync(baseDir);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
entries = [];
|
|
625
|
+
}
|
|
626
|
+
if (entries.some((e) => e === target || e.startsWith(`${target}.`)))
|
|
627
|
+
continue;
|
|
628
|
+
return `make target "${target}" does not appear in ${facts.file}`;
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
414
632
|
/**
|
|
415
633
|
* Validates a single command segment against the repo context.
|
|
416
634
|
* Returns a human-readable error string, or null if the command looks valid.
|
|
@@ -418,7 +636,7 @@ function findMissingScript(context, cwd, parts, scriptName, builtins, tool) {
|
|
|
418
636
|
* @param cwd Repo-relative directory the command runs in ('.' = root, null =
|
|
419
637
|
* unknown after an unresolvable `cd` — validation is skipped).
|
|
420
638
|
*/
|
|
421
|
-
function validateSegment(seg, context, cwd = '.') {
|
|
639
|
+
function validateSegment(seg, context, cwd = '.', onScript, opts = {}) {
|
|
422
640
|
if (PLACEHOLDER_RE.test(seg))
|
|
423
641
|
return null;
|
|
424
642
|
const parts = seg.trim().split(/\s+/);
|
|
@@ -432,35 +650,8 @@ function validateSegment(seg, context, cwd = '.') {
|
|
|
432
650
|
? path.join(context.repoRoot, cwd)
|
|
433
651
|
: context.repoRoot;
|
|
434
652
|
// ── Package manager scripts ────────────────────────────────────────────────
|
|
435
|
-
if (tool === 'pnpm') {
|
|
436
|
-
|
|
437
|
-
const sub = parts[subIdx];
|
|
438
|
-
if (!sub)
|
|
439
|
-
return null;
|
|
440
|
-
const scriptName = sub === 'run' ? parts[subIdx + 1] : sub;
|
|
441
|
-
if (!scriptName)
|
|
442
|
-
return null;
|
|
443
|
-
return findMissingScript(context, cwd, parts, scriptName, PNPM_BUILTINS, 'pnpm');
|
|
444
|
-
}
|
|
445
|
-
if (tool === 'npm') {
|
|
446
|
-
const subIdx = firstNonFlagArgIndex(parts);
|
|
447
|
-
if (parts[subIdx] === 'run' && parts[subIdx + 1]) {
|
|
448
|
-
return findMissingScript(context, cwd, parts, parts[subIdx + 1], NPM_BUILTINS, 'npm');
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (tool === 'yarn') {
|
|
452
|
-
const subIdx = firstNonFlagArgIndex(parts);
|
|
453
|
-
const sub = parts[subIdx];
|
|
454
|
-
if (!sub)
|
|
455
|
-
return null;
|
|
456
|
-
const scriptName = sub === 'run'
|
|
457
|
-
? parts[subIdx + 1]
|
|
458
|
-
: sub === 'workspace'
|
|
459
|
-
? parts[subIdx + 2]
|
|
460
|
-
: sub;
|
|
461
|
-
if (!scriptName)
|
|
462
|
-
return null;
|
|
463
|
-
return findMissingScript(context, cwd, parts, scriptName, YARN_BUILTINS, 'yarn');
|
|
653
|
+
if (tool === 'pnpm' || tool === 'npm' || tool === 'yarn' || tool === 'bun') {
|
|
654
|
+
return validatePackageManagerScript(tool, parts, context, cwd, baseDir, onScript);
|
|
464
655
|
}
|
|
465
656
|
// ── File-executing commands ────────────────────────────────────────────────
|
|
466
657
|
const FILE_EXECUTORS = ['node', 'tsx', 'ts-node', 'bash', 'sh', 'python', 'python3'];
|
|
@@ -472,8 +663,29 @@ function validateSegment(seg, context, cwd = '.') {
|
|
|
472
663
|
return `File "${filePart}" referenced by "${tool}" does not appear to exist in the repository`;
|
|
473
664
|
}
|
|
474
665
|
}
|
|
666
|
+
// ── make <target> ──────────────────────────────────────────────────────────
|
|
667
|
+
if (tool === 'make' && cwd !== null && opts.makefileFor) {
|
|
668
|
+
// An instruction file that sits next to its own Makefile (`tools/AGENTS.md`
|
|
669
|
+
// beside `tools/Makefile`) documents that one, not the root's.
|
|
670
|
+
const fileDir = opts.fileDir ?? '.';
|
|
671
|
+
const makeCwd = cwd === '.' && fileDir !== '.' && opts.makefileFor(fileDir) ? fileDir : cwd;
|
|
672
|
+
// A code span like `make sure` is English, not a make invocation.
|
|
673
|
+
if (!opts.fenced && parts.length === 2 && MAKE_PROSE_WORDS.has(parts[1].toLowerCase()))
|
|
674
|
+
return null;
|
|
675
|
+
return validateMakeTargets(parts, context, makeCwd, opts.makefileFor);
|
|
676
|
+
}
|
|
475
677
|
// ── Direct script invocations: ./scripts/foo.sh ───────────────────────────
|
|
476
678
|
if (baseDir && (tool.startsWith('./') || tool.startsWith('../'))) {
|
|
679
|
+
// pcic-2b6.8: a bare `./src` or `./docs/` in an inline code span is a
|
|
680
|
+
// directory mention, not a script invocation — it is dead-references' to
|
|
681
|
+
// judge. Only a shell-fence line (meant to be run) still reports an absent
|
|
682
|
+
// extensionless `./gradlew`; an existing directory is never a missing script.
|
|
683
|
+
if (parts.length === 1 && path.extname(tool.replace(/\/+$/, '')) === '') {
|
|
684
|
+
if (isDirectory(baseDir, tool))
|
|
685
|
+
return null;
|
|
686
|
+
if (!opts.fenced || tool.endsWith('/'))
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
477
689
|
if (!fileExists(baseDir, tool)) {
|
|
478
690
|
return `Script "${tool}" does not appear to exist in the repository`;
|
|
479
691
|
}
|
|
@@ -510,12 +722,53 @@ function validateSegment(seg, context, cwd = '.') {
|
|
|
510
722
|
* out of scope.
|
|
511
723
|
*/
|
|
512
724
|
export function detectCommandValidity(context) {
|
|
725
|
+
// Copies: the cached analysis is shared with manifest-consistency.
|
|
726
|
+
return analyzeCommands(context).issues.map((issue) => ({ ...issue }));
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Both command-validity and manifest-consistency need the analysis; compute it
|
|
730
|
+
* once per context.
|
|
731
|
+
*/
|
|
732
|
+
const analysisCache = new WeakMap();
|
|
733
|
+
export function analyzeCommands(context) {
|
|
734
|
+
let analysis = analysisCache.get(context);
|
|
735
|
+
if (!analysis) {
|
|
736
|
+
analysis = computeCommandAnalysis(context);
|
|
737
|
+
analysisCache.set(context, analysis);
|
|
738
|
+
}
|
|
739
|
+
return analysis;
|
|
740
|
+
}
|
|
741
|
+
function computeCommandAnalysis(context) {
|
|
513
742
|
const issues = [];
|
|
743
|
+
const evaluatedScripts = new Map();
|
|
514
744
|
const reported = new Set();
|
|
745
|
+
// Makefiles by repo-relative directory: the root one comes from the context,
|
|
746
|
+
// subdirectory ones (`make -C dir`, `cd dir && make`) are read once each.
|
|
747
|
+
const makefiles = new Map();
|
|
748
|
+
const makefileFor = (dir) => {
|
|
749
|
+
if (dir === '.' || dir === '')
|
|
750
|
+
return context.makefile;
|
|
751
|
+
if (!makefiles.has(dir))
|
|
752
|
+
makefiles.set(dir, readMakefile(context.repoRoot, dir));
|
|
753
|
+
return makefiles.get(dir);
|
|
754
|
+
};
|
|
515
755
|
for (const file of context.files) {
|
|
516
756
|
const commands = extractCommands(file.content);
|
|
757
|
+
const evaluated = new Set();
|
|
758
|
+
const fileDir = path.relative(context.repoRoot, path.dirname(file.path)).replace(/\\/g, '/') || '.';
|
|
759
|
+
evaluatedScripts.set(file.path, evaluated);
|
|
517
760
|
for (const cmd of commands) {
|
|
518
|
-
|
|
761
|
+
// pcic-2b6.13: `cmd.cwd ?? '.'` used to turn an unknown directory (null,
|
|
762
|
+
// after `cd /srv/app` or `cd $DIR`) into the repo root and validate the
|
|
763
|
+
// command against the root manifest. Only a command with no cwd at all
|
|
764
|
+
// (prose, which never carries a `cd`) runs from the root; null stays
|
|
765
|
+
// unverifiable.
|
|
766
|
+
const cwd = cmd.cwd === undefined ? '.' : cmd.cwd;
|
|
767
|
+
const error = validateSegment(cmd.text, context, cwd, (name) => evaluated.add(`${cmd.line}:${name}`), {
|
|
768
|
+
fenced: cmd.fenced === true,
|
|
769
|
+
makefileFor,
|
|
770
|
+
fileDir: fileDir.startsWith('..') ? '.' : fileDir,
|
|
771
|
+
});
|
|
519
772
|
if (!error)
|
|
520
773
|
continue;
|
|
521
774
|
// Deduplicate same command text appearing twice in the same file
|
|
@@ -524,7 +777,7 @@ export function detectCommandValidity(context) {
|
|
|
524
777
|
continue;
|
|
525
778
|
reported.add(dedupeKey);
|
|
526
779
|
issues.push({
|
|
527
|
-
id: issueId(file
|
|
780
|
+
id: issueId(fileIdPath(file), cmd.text),
|
|
528
781
|
severity: 'warning',
|
|
529
782
|
category: 'command_validity',
|
|
530
783
|
title: `Possible reference to missing command or file: \`${cmd.text.split(/\s+/).slice(0, 3).join(' ')}\``,
|
|
@@ -538,6 +791,6 @@ export function detectCommandValidity(context) {
|
|
|
538
791
|
});
|
|
539
792
|
}
|
|
540
793
|
}
|
|
541
|
-
return issues;
|
|
794
|
+
return { issues, evaluatedScripts };
|
|
542
795
|
}
|
|
543
796
|
//# sourceMappingURL=command-validity.js.map
|