pr-checkmate 3.2.2 → 3.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.
Files changed (73) hide show
  1. package/.eslintrc.js +3 -9
  2. package/README.md +1 -1
  3. package/dist/cjs/cli.js +13 -0
  4. package/dist/cjs/config/pr-checkmate.template.json +393 -0
  5. package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -0
  6. package/dist/cjs/core/checks/languages/lint.js +12 -0
  7. package/dist/cjs/core/checks/languages/ruff-shared.js +6 -1
  8. package/dist/cjs/core/checks/pr/commitlint.js +4 -2
  9. package/dist/cjs/core/checks/quality/package-size.d.ts +23 -1
  10. package/dist/cjs/core/checks/quality/package-size.js +20 -4
  11. package/dist/cjs/core/checks/quality/spellcheck.js +8 -5
  12. package/dist/cjs/core/install/doctor.js +12 -0
  13. package/dist/cjs/core/install/grype-db.d.ts +25 -0
  14. package/dist/cjs/core/install/grype-db.js +45 -0
  15. package/dist/cjs/core/install/profiles.d.ts +1 -1
  16. package/dist/cjs/core/install/profiles.js +1 -1
  17. package/dist/cjs/core/install/reuse.js +6 -3
  18. package/dist/cjs/init.js +48 -2
  19. package/dist/cjs/scripts/copy-configs.js +9 -0
  20. package/dist/cjs/utils/own-deps.js +8 -1
  21. package/dist/cjs/utils/resolve-bin.d.ts +2 -0
  22. package/dist/cjs/utils/resolve-bin.js +5 -0
  23. package/dist/esm/cli.js +14 -1
  24. package/dist/esm/config/pr-checkmate.template.json +393 -0
  25. package/dist/esm/core/checks/dependencies/dependency-check.js +2 -2
  26. package/dist/esm/core/checks/dependencies/grype-scan.js +18 -0
  27. package/dist/esm/core/checks/dependencies/license-check.js +2 -2
  28. package/dist/esm/core/checks/dependencies/npm-audit.js +2 -2
  29. package/dist/esm/core/checks/git/diff-smell.js +1 -1
  30. package/dist/esm/core/checks/languages/cpp-format.js +1 -1
  31. package/dist/esm/core/checks/languages/lint.js +12 -3
  32. package/dist/esm/core/checks/languages/prettier.js +3 -3
  33. package/dist/esm/core/checks/languages/python-format.js +2 -2
  34. package/dist/esm/core/checks/languages/python-lint.js +2 -2
  35. package/dist/esm/core/checks/languages/ruff-shared.js +6 -1
  36. package/dist/esm/core/checks/languages/typecheck.js +1 -1
  37. package/dist/esm/core/checks/pr/commitlint.js +4 -2
  38. package/dist/esm/core/checks/pr/pr-body.js +1 -1
  39. package/dist/esm/core/checks/pr/pr-size-check.js +1 -1
  40. package/dist/esm/core/checks/pr/pr-title-lint.js +1 -1
  41. package/dist/esm/core/checks/quality/dead-code.js +1 -1
  42. package/dist/esm/core/checks/quality/duplicate-check.js +1 -1
  43. package/dist/esm/core/checks/quality/markdown-lint.js +1 -1
  44. package/dist/esm/core/checks/quality/package-size.d.ts +23 -1
  45. package/dist/esm/core/checks/quality/package-size.js +19 -4
  46. package/dist/esm/core/checks/quality/spellcheck.js +11 -8
  47. package/dist/esm/core/checks/security/diff-security.js +1 -1
  48. package/dist/esm/core/checks/security/security.js +1 -1
  49. package/dist/esm/core/install/doctor.js +12 -0
  50. package/dist/esm/core/install/grype-db.d.ts +25 -0
  51. package/dist/esm/core/install/grype-db.js +41 -0
  52. package/dist/esm/core/install/prepare.js +1 -1
  53. package/dist/esm/core/install/profiles.d.ts +1 -1
  54. package/dist/esm/core/install/profiles.js +1 -1
  55. package/dist/esm/core/install/reuse.js +6 -3
  56. package/dist/esm/core/run-checks.js +2 -2
  57. package/dist/esm/init.js +50 -4
  58. package/dist/esm/scripts/copy-configs.js +9 -0
  59. package/dist/esm/utils/own-deps.js +8 -1
  60. package/dist/esm/utils/resolve-bin.d.ts +2 -0
  61. package/dist/esm/utils/resolve-bin.js +4 -0
  62. package/eslint-default-rules.json +2 -8
  63. package/eslint.config.mjs +37 -8
  64. package/install-manifests/commitlint/package-lock.json +5 -4
  65. package/install-manifests/core/package-lock.json +5 -3
  66. package/install-manifests/js/package-lock.json +250 -571
  67. package/install-manifests/js/package.json +10 -8
  68. package/install-manifests/markdown/package-lock.json +16 -13
  69. package/install-manifests/markdown/package.json +3 -0
  70. package/install-manifests/spellcheck/package-lock.json +3 -3
  71. package/package.json +19 -15
  72. package/src/config/.eslintrc.js +3 -9
  73. package/src/config/eslint-default-rules.json +2 -8
@@ -6,7 +6,7 @@ import { getSourcePaths } from '../../../config/index.js';
6
6
  import { execa } from '../../../utils/execa.js';
7
7
  import { logger, resolvePackageBin } from '../../../utils/index.js';
8
8
  import { resolveScopedTargetFiles } from '../../targets.js';
9
- import { pass, warn, skip } from '../../types.js';
9
+ import { pass, skip, warn } from '../../types.js';
10
10
  import { logSample } from '../findings.js';
11
11
  /**
12
12
  * The files to hand jscpd, per language.
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import { GENERATED_DOC_FILES, getPackageConfigPath, hasLocalConfig, } from '../../../config/index.js';
5
5
  import { logger, resolvePackageBin } from '../../../utils/index.js';
6
6
  import { resolveTargetFiles } from '../../targets.js';
7
- import { pass, warn, skip } from '../../types.js';
7
+ import { pass, skip, warn } from '../../types.js';
8
8
  function loadUserMarkdownConfig(cwd) {
9
9
  const configPath = path.join(cwd, 'pr-checkmate.json');
10
10
  if (!fs.existsSync(configPath))
@@ -1,6 +1,27 @@
1
1
  import { Check } from '../../types.js';
2
+ /** The slice of `npm pack --dry-run --json` this check reads. */
3
+ interface PackedFile {
4
+ path: string;
5
+ size: number;
6
+ }
7
+ interface PackManifest {
8
+ name: string;
9
+ version: string;
10
+ size: number;
11
+ unpackedSize: number;
12
+ entryCount: number;
13
+ files: PackedFile[];
14
+ }
15
+ /**
16
+ * The manifest out of whatever `npm pack --json` printed.
17
+ *
18
+ * npm 10 prints `[ {manifest} ]`; npm 11 prints `{ "<name>": {manifest} }`. Reading
19
+ * only the first form turned every npm 11 runner into "could not describe this
20
+ * package" and the check into a permanent skip.
21
+ */
22
+ export declare function pickManifest(parsed: unknown): PackManifest | null;
2
23
  /** Why we could not read a published version. */
3
- export type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
24
+ type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
4
25
  /**
5
26
  * Tell "never published" apart from "we were not let in".
6
27
  *
@@ -34,3 +55,4 @@ export declare function classifyQueryFailure(stderr: string): QueryFailure;
34
55
  * { "Package Size": "error" }` makes it a gate for a repository that wants one.
35
56
  */
36
57
  export declare const packageSizeCheck: Check;
58
+ export {};
@@ -8,6 +8,22 @@ import { logSample } from '../findings.js';
8
8
  const DEFAULT_MAX_GROWTH = 20;
9
9
  /** How many of the files behind the growth are named. */
10
10
  const NAMED_FILES = 5;
11
+ /**
12
+ * The manifest out of whatever `npm pack --json` printed.
13
+ *
14
+ * npm 10 prints `[ {manifest} ]`; npm 11 prints `{ "<name>": {manifest} }`. Reading
15
+ * only the first form turned every npm 11 runner into "could not describe this
16
+ * package" and the check into a permanent skip.
17
+ */
18
+ export function pickManifest(parsed) {
19
+ const entries = Array.isArray(parsed)
20
+ ? parsed
21
+ : parsed !== null && typeof parsed === 'object'
22
+ ? Object.values(parsed)
23
+ : [];
24
+ const first = entries[0];
25
+ return first !== null && typeof first === 'object' ? first : null;
26
+ }
11
27
  /**
12
28
  * Ask npm what a tarball would contain, without producing one.
13
29
  *
@@ -41,10 +57,9 @@ async function packManifest(cwd, spec, registry) {
41
57
  if (result.exitCode !== 0)
42
58
  return { failure: classifyQueryFailure(result.stderr ?? '') };
43
59
  try {
44
- const parsed = JSON.parse(result.stdout);
45
- if (Array.isArray(parsed) && parsed.length > 0) {
46
- return { manifest: parsed[0] };
47
- }
60
+ const manifest = pickManifest(JSON.parse(result.stdout));
61
+ if (manifest)
62
+ return { manifest };
48
63
  }
49
64
  catch {
50
65
  // Fall through: npm answered with something that is not a manifest.
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { getPackageConfigPath, hasLocalConfig, IGNORE_PATTERNS, getSourcePaths, } from '../../../config/index.js';
3
+ import { getPackageConfigPath, getSourcePaths, hasLocalConfig, IGNORE_PATTERNS, } from '../../../config/index.js';
4
4
  import { execa } from '../../../utils/execa.js';
5
- import { logger, cleanupTempFile, matchesAnyGlob, resolvePackageBin, } from '../../../utils/index.js';
5
+ import { cleanupTempFile, logger, matchesAnyGlob, registeredToolPaths, resolvePackageBin, } from '../../../utils/index.js';
6
6
  import { resolveScopedTargetFiles } from '../../targets.js';
7
- import { pass, warn, skip } from '../../types.js';
7
+ import { pass, skip, warn } from '../../types.js';
8
8
  function loadUserCspellConfig(cwd) {
9
9
  const configPath = path.join(cwd, 'pr-checkmate.json');
10
10
  if (!fs.existsSync(configPath))
@@ -193,12 +193,15 @@ export function dropMissingLanguages(language, missing) {
193
193
  */
194
194
  function resolveFrom(cwd) {
195
195
  return id => {
196
- try {
197
- return require.resolve(id, { paths: [cwd] });
198
- }
199
- catch {
200
- return require.resolve(id);
196
+ // The client's copy first, then the fetched `spellcheck` profile (cspell's own
197
+ // dictionaries live there), then our tree.
198
+ for (const root of [cwd, ...registeredToolPaths()]) {
199
+ try {
200
+ return require.resolve(id, { paths: [root] });
201
+ }
202
+ catch { }
201
203
  }
204
+ return require.resolve(id);
202
205
  };
203
206
  }
204
207
  function createTempCspellConfig(cwd) {
@@ -1,7 +1,7 @@
1
1
  import { DIFF_SECURITY_IGNORE_DEFAULTS } from '../../../config/index.js';
2
2
  import { logger } from '../../../utils/index.js';
3
3
  import { skip } from '../../types.js';
4
- import { clientFindingOptions, emptyBuckets, mergeSeverityBuckets, reportSeverityBuckets, scanDiffPatterns, reviewLines, DEFAULT_DATAFLOW_WINDOW, } from '../findings.js';
4
+ import { clientFindingOptions, DEFAULT_DATAFLOW_WINDOW, emptyBuckets, mergeSeverityBuckets, reportSeverityBuckets, reviewLines, scanDiffPatterns, } from '../findings.js';
5
5
  const MD5_LABEL = 'MD5 (weak hash — use sha256 or stronger)';
6
6
  const SHA1_LABEL = 'SHA-1 (weak hash — use sha256 or stronger)';
7
7
  const SECURITY_PATTERNS = [
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { getPackageConfigPath, hasLocalConfig } from '../../../config/index.js';
5
5
  import { execa } from '../../../utils/execa.js';
6
6
  import { logger, resolveGitRef, resolvePackageBin } from '../../../utils/index.js';
7
- import { pass, fail, warn, skip } from '../../types.js';
7
+ import { fail, pass, skip, warn } from '../../types.js';
8
8
  /**
9
9
  * A HOME the scanner can actually write to.
10
10
  *
@@ -5,6 +5,7 @@ import { logger } from '../../utils/logger.js';
5
5
  import { packageVersion } from '../../utils/package-version.js';
6
6
  import { resolveCacheDir, stalePaths } from './cache.js';
7
7
  import { cachedProfiles, ensureTooling } from './ensure.js';
8
+ import { grypeDbState } from './grype-db.js';
8
9
  import { profileSatisfiedLocally } from './prepare.js';
9
10
  import { planProfiles, PROFILES } from './profiles.js';
10
11
  // Re-exported: `doctor` has been the home of this since ticket 064, and the
@@ -135,6 +136,17 @@ export async function runDoctor(cwd = process.cwd()) {
135
136
  lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
136
137
  lines.push(' run, or now with: npx pr-checkmate install');
137
138
  }
139
+ if (config.grypeScan?.enabled !== false) {
140
+ const db = await grypeDbState(cwd);
141
+ lines.push('');
142
+ lines.push(' Grype database (shared by all projects on this machine)');
143
+ if (db.state === 'missing-tool')
144
+ lines.push(' — grype is not installed (brew install grype)');
145
+ else if (db.state === 'ready')
146
+ lines.push(` ✅ up to date${db.built ? ` (built ${db.built})` : ''}`);
147
+ else
148
+ lines.push(` ⬇️ stale${db.built ? ` (built ${db.built})` : ''} — refresh: npx pr-checkmate install`);
149
+ }
138
150
  const stale = stalePaths(packageVersion(), config.install?.cacheDir);
139
151
  if (stale.length > 0) {
140
152
  lines.push('');
@@ -0,0 +1,25 @@
1
+ type GrypeDbState = {
2
+ state: 'missing-tool';
3
+ } | {
4
+ state: 'ready';
5
+ built?: string;
6
+ } | {
7
+ state: 'stale';
8
+ built?: string;
9
+ };
10
+ /**
11
+ * Is grype's vulnerability database usable right now? grype refuses one older than
12
+ * 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
13
+ * shared by every project on the machine, so one refresh serves them all.
14
+ */
15
+ export declare function grypeDbState(cwd: string): Promise<GrypeDbState>;
16
+ /**
17
+ * Download a fresh database when the current one is missing or stale.
18
+ *
19
+ * `timeoutMs` bounds the download: a check inside a run passes one so it can never
20
+ * look like a hung commit, `install` passes none because a person asked for exactly
21
+ * this and the download is the whole job. Returns why it is still unusable, or
22
+ * `null` when it is ready.
23
+ */
24
+ export declare function ensureGrypeDb(cwd: string, timeoutMs?: number): Promise<string | null>;
25
+ export {};
@@ -0,0 +1,41 @@
1
+ import { execa } from '../../utils/execa.js';
2
+ import { logger } from '../../utils/logger.js';
3
+ /** `Built:` line of `grype db status`, so `doctor` can say how old the data is. */
4
+ const builtOf = (stdout) => typeof stdout === 'string' ? /^Built:\s*(.+)$/m.exec(stdout)?.[1]?.trim() : undefined;
5
+ /**
6
+ * Is grype's vulnerability database usable right now? grype refuses one older than
7
+ * 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
8
+ * shared by every project on the machine, so one refresh serves them all.
9
+ */
10
+ export async function grypeDbState(cwd) {
11
+ const status = await execa('grype', ['db', 'status'], { cwd, reject: false, timeout: 30000 });
12
+ if (status.code === 'ENOENT')
13
+ return { state: 'missing-tool' };
14
+ const built = builtOf(status.stdout);
15
+ return status.exitCode === 0 ? { state: 'ready', built } : { state: 'stale', built };
16
+ }
17
+ /**
18
+ * Download a fresh database when the current one is missing or stale.
19
+ *
20
+ * `timeoutMs` bounds the download: a check inside a run passes one so it can never
21
+ * look like a hung commit, `install` passes none because a person asked for exactly
22
+ * this and the download is the whole job. Returns why it is still unusable, or
23
+ * `null` when it is ready.
24
+ */
25
+ export async function ensureGrypeDb(cwd, timeoutMs) {
26
+ const current = await grypeDbState(cwd);
27
+ if (current.state === 'missing-tool')
28
+ return 'ENOENT';
29
+ if (current.state === 'ready')
30
+ return null;
31
+ logger.info('[Grype Scan]: vulnerability database missing or stale — updating it');
32
+ const update = await execa('grype', ['db', 'update'], {
33
+ cwd,
34
+ reject: false,
35
+ ...(timeoutMs ? { timeout: timeoutMs } : {}),
36
+ });
37
+ if (update.timedOut) {
38
+ return `database update exceeded ${timeoutMs}ms — run "grype db update" once, then retry`;
39
+ }
40
+ return update.exitCode === 0 ? null : 'database update failed (offline?) — run "grype db update"';
41
+ }
@@ -1,7 +1,7 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { clearToolPaths, packagePresent, registerToolPaths } from '../../utils/resolve-bin.js';
3
3
  import { ensureTooling } from './ensure.js';
4
- import { PROFILES, planProfiles } from './profiles.js';
4
+ import { planProfiles, PROFILES } from './profiles.js';
5
5
  /**
6
6
  * Whether a profile needs fetching at all, or already ships inside the package.
7
7
  *
@@ -3,7 +3,7 @@ import type { Language, LanguageUsage } from '../../utils/detect-language.js';
3
3
  /**
4
4
  * A named bundle of tooling, and the checks that stop working without it.
5
5
  *
6
- * The package ships ~233 MB of tooling, and no repository needs all of it: a
6
+ * The tooling totals ~233 MB, and no repository needs all of it: a
7
7
  * Python service pays for the whole JavaScript toolchain, a repository with
8
8
  * Spellcheck switched off still pays for cspell. Profiles are how that stops —
9
9
  * they name which slice of the tooling a given repository actually needs.
@@ -20,9 +20,9 @@ export const PROFILES = [
20
20
  'typescript',
21
21
  'eslint',
22
22
  'eslint-config-prettier',
23
- 'eslint-plugin-import-x',
24
23
  'eslint-plugin-jsdoc',
25
24
  'eslint-plugin-prettier',
25
+ 'eslint-plugin-simple-import-sort',
26
26
  'eslint-plugin-sonarjs',
27
27
  '@typescript-eslint/eslint-plugin',
28
28
  '@typescript-eslint/parser',
@@ -5,15 +5,18 @@ import { ownPackageRoot } from '../../utils/resolve-bin.js';
5
5
  /**
6
6
  * The major version we declare for a tool, from our own manifest.
7
7
  *
8
- * Read from `dependencies` rather than from the installed tree: what matters is
8
+ * Read from the manifest rather than from the installed tree: what matters is
9
9
  * the version our configuration was written against, and a hoisted install can
10
10
  * resolve to something else entirely.
11
11
  */
12
12
  function ownMajor(pkgName) {
13
13
  try {
14
14
  const manifest = path.join(ownPackageRoot(), 'package.json');
15
- const deps = JSON.parse(fs.readFileSync(manifest, 'utf-8')).dependencies;
16
- const range = deps?.[pkgName];
15
+ const parsed = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
16
+ // Profile tooling is declared in `devDependencies` — it is fetched on demand
17
+ // rather than installed with us — but it is still the version our config was
18
+ // written against, and the manifest keeps both.
19
+ const range = parsed.dependencies?.[pkgName] ?? parsed.devDependencies?.[pkgName];
17
20
  if (!range)
18
21
  return null;
19
22
  // `^10.6.0`, `>=3.0.0`, `~5.1` — the leading number is the major in every
@@ -1,10 +1,10 @@
1
1
  import { isCiEnvironment, logger } from '../utils/index.js';
2
2
  import { packageVersion } from '../utils/package-version.js';
3
- import { Semaphore, resolveConcurrency, runPool, withTimeout } from '../utils/pool.js';
3
+ import { resolveConcurrency, runPool, Semaphore, withTimeout } from '../utils/pool.js';
4
4
  import { clearToolPaths } from '../utils/resolve-bin.js';
5
5
  import { ResultCollector } from '../utils/result-collector.js';
6
6
  import { buildContext } from './context.js';
7
- import { prepareTooling, noTooling } from './install/prepare.js';
7
+ import { noTooling, prepareTooling } from './install/prepare.js';
8
8
  import { REGISTRY } from './registry.js';
9
9
  import { report } from './reporters.js';
10
10
  import { PHASES, } from './types.js';
package/dist/esm/init.js CHANGED
@@ -2,11 +2,11 @@ import { fileURLToPath as __fileURLToPath } from 'node:url';
2
2
  import { dirname as __dirnameFn } from 'node:path';
3
3
  const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
4
4
  import fs from 'fs';
5
- import path from 'path';
6
5
  import inquirer from 'inquirer';
7
- import { DEAD_CODE_IGNORE_DEFAULTS, DEFAULT_COMMANDS, DIFF_SECURITY_IGNORE_DEFAULTS, documentation, DOCS_URL, DUPLICATE_IGNORE_DEFAULTS, FULL_CONFIG_URL, GENERATED_DOC_FILES, IGNORED_DIRS, } from './config/index.js';
6
+ import path from 'path';
7
+ import { DEAD_CODE_IGNORE_DEFAULTS, DEFAULT_COMMANDS, DIFF_SECURITY_IGNORE_DEFAULTS, DOCS_URL, documentation, DUPLICATE_IGNORE_DEFAULTS, FULL_CONFIG_URL, GENERATED_DOC_FILES, IGNORED_DIRS, } from './config/index.js';
8
8
  import { DEFAULT_DATAFLOW_WINDOW } from './core/checks/findings.js';
9
- import { logger, detectLanguages, findOwnPackageRoot } from './utils/index.js';
9
+ import { detectLanguages, findOwnPackageRoot, logger } from './utils/index.js';
10
10
  const CONFIG_FILE = path.resolve(process.cwd(), 'pr-checkmate.json');
11
11
  /**
12
12
  * Points editors at the bundled JSON schema. With the generated config reduced
@@ -264,8 +264,46 @@ const HOW_TO = [
264
264
  `Every option, annotated, with its default: ${FULL_CONFIG_URL}`,
265
265
  `Docs: ${DOCS_URL}`,
266
266
  ].join(' ');
267
+ // Written out at its defaults so the options are visible and editable in the file
268
+ // instead of having to be looked up. Same trade as `max-len`: the values stop
269
+ // tracking the package, which is the price of being able to see them.
270
+ const GRYPE_SCAN_SEED = {
271
+ '//': 'Anchore grype, cross-ecosystem. ON by default but skips when grype is absent from the runner. Only direct dependencies are reported.',
272
+ enabled: true,
273
+ minSeverity: 'medium',
274
+ '//ignore': 'Suppress by CVE/GHSA id or by package name.',
275
+ ignore: [],
276
+ '//timeoutMs': 'Cap on the scan. On timeout the check skips rather than fails.',
277
+ timeoutMs: 120000,
278
+ '//ci': 'Per-environment overrides (enabled, minSeverity, ignore, timeoutMs): "ci" applies on CI builds, "local" elsewhere. Example: "local": { "enabled": false }',
279
+ ci: {},
280
+ local: {},
281
+ };
282
+ /**
283
+ * Every option the template documents that the file does not already carry, at
284
+ * its default. Without this, options such as `vulnScan` (off by default) or
285
+ * `licenseCheck` exist only in the docs, and nobody edits what they cannot see.
286
+ * Comment keys travel with the option they describe, section headers with the
287
+ * first option after them.
288
+ */
289
+ function templateExtras(existing) {
290
+ const templatePath = path.join(__dirname, 'config', 'pr-checkmate.template.json');
291
+ if (!fs.existsSync(templatePath)) {
292
+ return {};
293
+ }
294
+ const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
295
+ const extras = {};
296
+ for (const [key, value] of Object.entries(template)) {
297
+ const option = key.startsWith('//') ? key.slice(2) : key;
298
+ if (key in existing || option in existing) {
299
+ continue;
300
+ }
301
+ extras[key] = value;
302
+ }
303
+ return extras;
304
+ }
267
305
  export function buildInitialConfig(sourcePath, languages) {
268
- return {
306
+ const base = {
269
307
  $schema: SCHEMA_PATH,
270
308
  '// how-to': HOW_TO,
271
309
  commands: DEFAULT_COMMANDS,
@@ -346,9 +384,13 @@ export function buildInitialConfig(sourcePath, languages) {
346
384
  dataflowWindow: DEFAULT_DATAFLOW_WINDOW,
347
385
  ignorePaths: [...DIFF_SECURITY_IGNORE_DEFAULTS],
348
386
  },
387
+ grypeScan: structuredClone(GRYPE_SCAN_SEED),
349
388
  documentation,
350
389
  languages,
351
390
  };
391
+ const extras = templateExtras(base);
392
+ const { documentation: docs, languages: langs, ...rest } = base;
393
+ return { ...rest, ...extras, documentation: docs, languages: langs };
352
394
  }
353
395
  /** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
354
396
  async function createConfig(root) {
@@ -397,6 +439,10 @@ async function updateConfig(root) {
397
439
  $schema: json.$schema ?? SCHEMA_PATH,
398
440
  '// how-to': json['// how-to'] ?? HOW_TO,
399
441
  ...json,
442
+ // An existing file predates this option, so nobody has seen it. Added only
443
+ // when missing; a client's own block is never touched.
444
+ grypeScan: json.grypeScan ?? structuredClone(GRYPE_SCAN_SEED),
445
+ ...templateExtras({ ...json, grypeScan: true }),
400
446
  languages,
401
447
  };
402
448
  fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
@@ -34,6 +34,15 @@ const configFiles = [
34
34
  source: 'src/config/pr-checkmate-workflow-docker.yml',
35
35
  dest: 'dist/esm/config/pr-checkmate-workflow-docker.yml',
36
36
  },
37
+ // Read by `init` to seed every option into a new pr-checkmate.json.
38
+ {
39
+ source: 'src/config/pr-checkmate.template.json',
40
+ dest: 'dist/cjs/config/pr-checkmate.template.json',
41
+ },
42
+ {
43
+ source: 'src/config/pr-checkmate.template.json',
44
+ dest: 'dist/esm/config/pr-checkmate.template.json',
45
+ },
37
46
  { source: 'src/config/pr-checkmate.schema.json', dest: 'pr-checkmate.schema.json' },
38
47
  ];
39
48
  /**
@@ -3,6 +3,7 @@ import { dirname as __dirnameFn } from 'node:path';
3
3
  const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
+ import { PROFILES } from '../core/install/profiles.js';
6
7
  const OWN_PACKAGE_NAME = 'pr-checkmate';
7
8
  const PACKAGE_JSON = 'package.json';
8
9
  let cachedClosure;
@@ -85,7 +86,13 @@ export function getOwnDependencyClosure() {
85
86
  }
86
87
  // BFS over the dependency graph, resolving each package relative to the parent
87
88
  // that declared it so nested (non-hoisted) installs resolve correctly.
88
- const queue = readDependencyNames(root).map(name => ({
89
+ //
90
+ // Profile tooling is ours even though it no longer sits in `dependencies`: it is
91
+ // fetched on demand, so the roots are what we declare plus what the profiles
92
+ // name. Without the second half every fetched tool is "the client's" and its
93
+ // licence blocks the client for a dependency they cannot remove.
94
+ const roots = new Set([...readDependencyNames(root), ...PROFILES.flatMap(p => p.packages)]);
95
+ const queue = [...roots].map(name => ({
89
96
  name,
90
97
  fromDir: root,
91
98
  }));
@@ -16,6 +16,8 @@ export interface ResolvedTool {
16
16
  * to a run from before there was anything to fetch.
17
17
  */
18
18
  export declare function registerToolPaths(dirs: string[]): void;
19
+ /** The roots registered for this run, for tools that load plugins by themselves. */
20
+ export declare function registeredToolPaths(): string[];
19
21
  /** Drop every registered profile root. For tests, and for a second run in-process. */
20
22
  export declare function clearToolPaths(): void;
21
23
  /** The root of the installed pr-checkmate package — where our own tooling lives. */
@@ -25,6 +25,10 @@ let profileRoots = [];
25
25
  export function registerToolPaths(dirs) {
26
26
  profileRoots = [...dirs];
27
27
  }
28
+ /** The roots registered for this run, for tools that load plugins by themselves. */
29
+ export function registeredToolPaths() {
30
+ return [...profileRoots];
31
+ }
28
32
  /** Drop every registered profile root. For tests, and for a second run in-process. */
29
33
  export function clearToolPaths() {
30
34
  profileRoots = [];
@@ -26,14 +26,8 @@
26
26
  "sonarjs/cognitive-complexity": ["warn", 30],
27
27
  "sonarjs/no-duplicate-string": ["warn", { "threshold": 3 }],
28
28
  "sonarjs/no-identical-functions": "warn",
29
- "import/order": [
30
- "warn",
31
- {
32
- "groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
33
- "alphabetize": { "order": "asc", "caseInsensitive": true }
34
- }
35
- ],
36
- "import/no-duplicates": "error",
29
+ "simple-import-sort/imports": "warn",
30
+ "no-duplicate-imports": ["error", { "allowSeparateTypeImports": true }],
37
31
  "jsdoc/check-param-names": "warn",
38
32
  "jsdoc/check-tag-names": "warn",
39
33
  "jsdoc/check-types": "warn",
package/eslint.config.mjs CHANGED
@@ -1,12 +1,41 @@
1
1
  import fs from 'fs';
2
2
  import { createRequire } from 'module';
3
3
  import path from 'path';
4
- import tsPlugin from '@typescript-eslint/eslint-plugin';
5
- import tsParser from '@typescript-eslint/parser';
6
- import prettierPlugin from 'eslint-plugin-prettier';
7
- import sonarjsPlugin from 'eslint-plugin-sonarjs';
8
- import importPlugin from 'eslint-plugin-import-x';
9
- import jsdocPlugin from 'eslint-plugin-jsdoc';
4
+ import { pathToFileURL } from 'url';
5
+
6
+ /**
7
+ * Plugins are loaded through the roots pr-checkmate registered for this run
8
+ * (`PR_CHECKMATE_TOOL_ROOTS`), because the `js` profile is fetched into a cache
9
+ * directory when a repository needs it and a bare `import` only searches the tree
10
+ * this file sits in. With no roots — development, or tooling that shipped beside
11
+ * the package — it is the plain import it always was.
12
+ */
13
+ const toolRoots = (process.env.PR_CHECKMATE_TOOL_ROOTS ?? '').split(path.delimiter).filter(Boolean);
14
+ const resolveFrom = createRequire(import.meta.url);
15
+
16
+ async function loadPlugin(name) {
17
+ for (const root of toolRoots) {
18
+ try {
19
+ const entry = resolveFrom.resolve(name, { paths: [root] });
20
+ const mod = await import(pathToFileURL(entry).href);
21
+
22
+ return mod.default ?? mod;
23
+ } catch {}
24
+ }
25
+ const mod = await import(name);
26
+
27
+ return mod.default ?? mod;
28
+ }
29
+
30
+ const [tsPlugin, tsParser, prettierPlugin, sonarjsPlugin, jsdocPlugin, simpleImportSort] =
31
+ await Promise.all([
32
+ loadPlugin('@typescript-eslint/eslint-plugin'),
33
+ loadPlugin('@typescript-eslint/parser'),
34
+ loadPlugin('eslint-plugin-prettier'),
35
+ loadPlugin('eslint-plugin-sonarjs'),
36
+ loadPlugin('eslint-plugin-jsdoc'),
37
+ loadPlugin('eslint-plugin-simple-import-sort'),
38
+ ]);
10
39
 
11
40
  /**
12
41
  * Default ESLint rules — single source of truth, shared with init.ts so the
@@ -15,7 +44,7 @@ import jsdocPlugin from 'eslint-plugin-jsdoc';
15
44
  *
16
45
  * SEVERITY POLICY: `error` is reserved for defects a human has to resolve —
17
46
  * `no-debugger`, `eqeqeq`, `prefer-const`, `@typescript-eslint/no-unused-vars`,
18
- * `@typescript-eslint/no-floating-promises`, `import/no-duplicates`. Everything
47
+ * `@typescript-eslint/no-floating-promises`, `no-duplicate-imports`. Everything
19
48
  * that is purely formatting is `warn`, even
20
49
  * though it is auto-fixable, precisely BECAUSE it is auto-fixable: ESLint exits 1
21
50
  * on errors only, and the ESLint check maps exit 1 to a blocking failure, so an
@@ -179,7 +208,7 @@ export default [
179
208
  '@typescript-eslint': tsPlugin,
180
209
  prettier: prettierPlugin,
181
210
  sonarjs: sonarjsPlugin,
182
- import: importPlugin,
211
+ 'simple-import-sort': simpleImportSort,
183
212
  jsdoc: jsdocPlugin,
184
213
  },
185
214
 
@@ -421,6 +421,7 @@
421
421
  "version": "26.6.2",
422
422
  "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
423
423
  "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
424
+ "devOptional": true,
424
425
  "license": "MIT",
425
426
  "dependencies": {
426
427
  "undici-types": "~8.9.0"
@@ -481,7 +482,6 @@
481
482
  "version": "6.0.1",
482
483
  "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-6.0.1.tgz",
483
484
  "integrity": "sha512-cs+LadpH7Kpw0M3k8wurk+sOVVDAENA0iK4OBOrkL94j5lEVYRJ4j3zd2bhY9qgzyrPqthdcYT3axzRN7AliMg==",
484
- "devOptional": true,
485
485
  "license": "MIT",
486
486
  "engines": {
487
487
  "node": ">=22"
@@ -616,9 +616,9 @@
616
616
  "license": "MIT"
617
617
  },
618
618
  "node_modules/fast-uri": {
619
- "version": "3.1.8",
620
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
621
- "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
619
+ "version": "3.1.9",
620
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
621
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
622
622
  "funding": [
623
623
  {
624
624
  "type": "github",
@@ -874,6 +874,7 @@
874
874
  "version": "8.9.0",
875
875
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
876
876
  "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
877
+ "devOptional": true,
877
878
  "license": "MIT"
878
879
  },
879
880
  "node_modules/wrap-ansi": {
@@ -376,6 +376,7 @@
376
376
  "version": "26.6.2",
377
377
  "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
378
378
  "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
379
+ "devOptional": true,
379
380
  "license": "MIT",
380
381
  "dependencies": {
381
382
  "undici-types": "~8.9.0"
@@ -906,9 +907,9 @@
906
907
  }
907
908
  },
908
909
  "node_modules/toml": {
909
- "version": "5.0.0",
910
- "resolved": "https://registry.npmjs.org/toml/-/toml-5.0.0.tgz",
911
- "integrity": "sha512-bsqEd+g7fzlrw7LEynQ6W6aOK/lTOlepz5x1sJyIV9fTZVJS2Q76nuju6FK+gZhW5bUpvB7mCEqdR0u4WziMFw==",
910
+ "version": "5.0.1",
911
+ "resolved": "https://registry.npmjs.org/toml/-/toml-5.0.1.tgz",
912
+ "integrity": "sha512-EqYNA/YvUNndrA2B3O2ojzLQoTQrBxeZ9UIjQwk8t10oe6n7dwclBwd9IdZQTOKwe6vWX+AXtl6IntXRbepdxQ==",
912
913
  "license": "MIT",
913
914
  "engines": {
914
915
  "node": ">=20"
@@ -918,6 +919,7 @@
918
919
  "version": "8.9.0",
919
920
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
920
921
  "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
922
+ "devOptional": true,
921
923
  "license": "MIT"
922
924
  },
923
925
  "node_modules/unicorn-magic": {