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
@@ -204,12 +204,15 @@ function dropMissingLanguages(language, missing) {
204
204
  */
205
205
  function resolveFrom(cwd) {
206
206
  return id => {
207
- try {
208
- return require.resolve(id, { paths: [cwd] });
209
- }
210
- catch {
211
- return require.resolve(id);
207
+ // The client's copy first, then the fetched `spellcheck` profile (cspell's own
208
+ // dictionaries live there), then our tree.
209
+ for (const root of [cwd, ...(0, index_js_2.registeredToolPaths)()]) {
210
+ try {
211
+ return require.resolve(id, { paths: [root] });
212
+ }
213
+ catch { }
212
214
  }
215
+ return require.resolve(id);
213
216
  };
214
217
  }
215
218
  function createTempCspellConfig(cwd) {
@@ -16,6 +16,7 @@ const package_version_js_1 = require("../../utils/package-version.js");
16
16
  Object.defineProperty(exports, "packageVersion", { enumerable: true, get: function () { return package_version_js_1.packageVersion; } });
17
17
  const cache_js_1 = require("./cache.js");
18
18
  const ensure_js_1 = require("./ensure.js");
19
+ const grype_db_js_1 = require("./grype-db.js");
19
20
  const prepare_js_1 = require("./prepare.js");
20
21
  const profiles_js_1 = require("./profiles.js");
21
22
  /**
@@ -142,6 +143,17 @@ async function runDoctor(cwd = process.cwd()) {
142
143
  lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
143
144
  lines.push(' run, or now with: npx pr-checkmate install');
144
145
  }
146
+ if (config.grypeScan?.enabled !== false) {
147
+ const db = await (0, grype_db_js_1.grypeDbState)(cwd);
148
+ lines.push('');
149
+ lines.push(' Grype database (shared by all projects on this machine)');
150
+ if (db.state === 'missing-tool')
151
+ lines.push(' — grype is not installed (brew install grype)');
152
+ else if (db.state === 'ready')
153
+ lines.push(` ✅ up to date${db.built ? ` (built ${db.built})` : ''}`);
154
+ else
155
+ lines.push(` ⬇️ stale${db.built ? ` (built ${db.built})` : ''} — refresh: npx pr-checkmate install`);
156
+ }
145
157
  const stale = (0, cache_js_1.stalePaths)((0, package_version_js_1.packageVersion)(), config.install?.cacheDir);
146
158
  if (stale.length > 0) {
147
159
  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,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.grypeDbState = grypeDbState;
4
+ exports.ensureGrypeDb = ensureGrypeDb;
5
+ const execa_js_1 = require("../../utils/execa.js");
6
+ const logger_js_1 = require("../../utils/logger.js");
7
+ /** `Built:` line of `grype db status`, so `doctor` can say how old the data is. */
8
+ const builtOf = (stdout) => typeof stdout === 'string' ? /^Built:\s*(.+)$/m.exec(stdout)?.[1]?.trim() : undefined;
9
+ /**
10
+ * Is grype's vulnerability database usable right now? grype refuses one older than
11
+ * 5 days (exit code 1 from `db status`). The database lives in grype's own cache,
12
+ * shared by every project on the machine, so one refresh serves them all.
13
+ */
14
+ async function grypeDbState(cwd) {
15
+ const status = await (0, execa_js_1.execa)('grype', ['db', 'status'], { cwd, reject: false, timeout: 30000 });
16
+ if (status.code === 'ENOENT')
17
+ return { state: 'missing-tool' };
18
+ const built = builtOf(status.stdout);
19
+ return status.exitCode === 0 ? { state: 'ready', built } : { state: 'stale', built };
20
+ }
21
+ /**
22
+ * Download a fresh database when the current one is missing or stale.
23
+ *
24
+ * `timeoutMs` bounds the download: a check inside a run passes one so it can never
25
+ * look like a hung commit, `install` passes none because a person asked for exactly
26
+ * this and the download is the whole job. Returns why it is still unusable, or
27
+ * `null` when it is ready.
28
+ */
29
+ async function ensureGrypeDb(cwd, timeoutMs) {
30
+ const current = await grypeDbState(cwd);
31
+ if (current.state === 'missing-tool')
32
+ return 'ENOENT';
33
+ if (current.state === 'ready')
34
+ return null;
35
+ logger_js_1.logger.info('[Grype Scan]: vulnerability database missing or stale — updating it');
36
+ const update = await (0, execa_js_1.execa)('grype', ['db', 'update'], {
37
+ cwd,
38
+ reject: false,
39
+ ...(timeoutMs ? { timeout: timeoutMs } : {}),
40
+ });
41
+ if (update.timedOut) {
42
+ return `database update exceeded ${timeoutMs}ms — run "grype db update" once, then retry`;
43
+ }
44
+ return update.exitCode === 0 ? null : 'database update failed (offline?) — run "grype db update"';
45
+ }
@@ -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.
@@ -25,9 +25,9 @@ exports.PROFILES = [
25
25
  'typescript',
26
26
  'eslint',
27
27
  'eslint-config-prettier',
28
- 'eslint-plugin-import-x',
29
28
  'eslint-plugin-jsdoc',
30
29
  'eslint-plugin-prettier',
30
+ 'eslint-plugin-simple-import-sort',
31
31
  'eslint-plugin-sonarjs',
32
32
  '@typescript-eslint/eslint-plugin',
33
33
  '@typescript-eslint/parser',
@@ -12,15 +12,18 @@ const resolve_bin_js_1 = require("../../utils/resolve-bin.js");
12
12
  /**
13
13
  * The major version we declare for a tool, from our own manifest.
14
14
  *
15
- * Read from `dependencies` rather than from the installed tree: what matters is
15
+ * Read from the manifest rather than from the installed tree: what matters is
16
16
  * the version our configuration was written against, and a hoisted install can
17
17
  * resolve to something else entirely.
18
18
  */
19
19
  function ownMajor(pkgName) {
20
20
  try {
21
21
  const manifest = node_path_1.default.join((0, resolve_bin_js_1.ownPackageRoot)(), 'package.json');
22
- const deps = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8')).dependencies;
23
- const range = deps?.[pkgName];
22
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8'));
23
+ // Profile tooling is declared in `devDependencies` — it is fetched on demand
24
+ // rather than installed with us — but it is still the version our config was
25
+ // written against, and the manifest keeps both.
26
+ const range = parsed.dependencies?.[pkgName] ?? parsed.devDependencies?.[pkgName];
24
27
  if (!range)
25
28
  return null;
26
29
  // `^10.6.0`, `>=3.0.0`, `~5.1` — the leading number is the major in every
package/dist/cjs/init.js CHANGED
@@ -8,8 +8,8 @@ exports.setupWorkflow = setupWorkflow;
8
8
  exports.buildInitialConfig = buildInitialConfig;
9
9
  exports.init = init;
10
10
  const fs_1 = __importDefault(require("fs"));
11
- const path_1 = __importDefault(require("path"));
12
11
  const inquirer_1 = __importDefault(require("inquirer"));
12
+ const path_1 = __importDefault(require("path"));
13
13
  const index_js_1 = require("./config/index.js");
14
14
  const findings_js_1 = require("./core/checks/findings.js");
15
15
  const index_js_2 = require("./utils/index.js");
@@ -270,8 +270,46 @@ const HOW_TO = [
270
270
  `Every option, annotated, with its default: ${index_js_1.FULL_CONFIG_URL}`,
271
271
  `Docs: ${index_js_1.DOCS_URL}`,
272
272
  ].join(' ');
273
+ // Written out at its defaults so the options are visible and editable in the file
274
+ // instead of having to be looked up. Same trade as `max-len`: the values stop
275
+ // tracking the package, which is the price of being able to see them.
276
+ const GRYPE_SCAN_SEED = {
277
+ '//': 'Anchore grype, cross-ecosystem. ON by default but skips when grype is absent from the runner. Only direct dependencies are reported.',
278
+ enabled: true,
279
+ minSeverity: 'medium',
280
+ '//ignore': 'Suppress by CVE/GHSA id or by package name.',
281
+ ignore: [],
282
+ '//timeoutMs': 'Cap on the scan. On timeout the check skips rather than fails.',
283
+ timeoutMs: 120000,
284
+ '//ci': 'Per-environment overrides (enabled, minSeverity, ignore, timeoutMs): "ci" applies on CI builds, "local" elsewhere. Example: "local": { "enabled": false }',
285
+ ci: {},
286
+ local: {},
287
+ };
288
+ /**
289
+ * Every option the template documents that the file does not already carry, at
290
+ * its default. Without this, options such as `vulnScan` (off by default) or
291
+ * `licenseCheck` exist only in the docs, and nobody edits what they cannot see.
292
+ * Comment keys travel with the option they describe, section headers with the
293
+ * first option after them.
294
+ */
295
+ function templateExtras(existing) {
296
+ const templatePath = path_1.default.join(__dirname, 'config', 'pr-checkmate.template.json');
297
+ if (!fs_1.default.existsSync(templatePath)) {
298
+ return {};
299
+ }
300
+ const template = JSON.parse(fs_1.default.readFileSync(templatePath, 'utf-8'));
301
+ const extras = {};
302
+ for (const [key, value] of Object.entries(template)) {
303
+ const option = key.startsWith('//') ? key.slice(2) : key;
304
+ if (key in existing || option in existing) {
305
+ continue;
306
+ }
307
+ extras[key] = value;
308
+ }
309
+ return extras;
310
+ }
273
311
  function buildInitialConfig(sourcePath, languages) {
274
- return {
312
+ const base = {
275
313
  $schema: SCHEMA_PATH,
276
314
  '// how-to': HOW_TO,
277
315
  commands: index_js_1.DEFAULT_COMMANDS,
@@ -352,9 +390,13 @@ function buildInitialConfig(sourcePath, languages) {
352
390
  dataflowWindow: findings_js_1.DEFAULT_DATAFLOW_WINDOW,
353
391
  ignorePaths: [...index_js_1.DIFF_SECURITY_IGNORE_DEFAULTS],
354
392
  },
393
+ grypeScan: structuredClone(GRYPE_SCAN_SEED),
355
394
  documentation: index_js_1.documentation,
356
395
  languages,
357
396
  };
397
+ const extras = templateExtras(base);
398
+ const { documentation: docs, languages: langs, ...rest } = base;
399
+ return { ...rest, ...extras, documentation: docs, languages: langs };
358
400
  }
359
401
  /** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
360
402
  async function createConfig(root) {
@@ -403,6 +445,10 @@ async function updateConfig(root) {
403
445
  $schema: json.$schema ?? SCHEMA_PATH,
404
446
  '// how-to': json['// how-to'] ?? HOW_TO,
405
447
  ...json,
448
+ // An existing file predates this option, so nobody has seen it. Added only
449
+ // when missing; a client's own block is never touched.
450
+ grypeScan: json.grypeScan ?? structuredClone(GRYPE_SCAN_SEED),
451
+ ...templateExtras({ ...json, grypeScan: true }),
406
452
  languages,
407
453
  };
408
454
  fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
@@ -36,6 +36,15 @@ const configFiles = [
36
36
  source: 'src/config/pr-checkmate-workflow-docker.yml',
37
37
  dest: 'dist/esm/config/pr-checkmate-workflow-docker.yml',
38
38
  },
39
+ // Read by `init` to seed every option into a new pr-checkmate.json.
40
+ {
41
+ source: 'src/config/pr-checkmate.template.json',
42
+ dest: 'dist/cjs/config/pr-checkmate.template.json',
43
+ },
44
+ {
45
+ source: 'src/config/pr-checkmate.template.json',
46
+ dest: 'dist/esm/config/pr-checkmate.template.json',
47
+ },
39
48
  { source: 'src/config/pr-checkmate.schema.json', dest: 'pr-checkmate.schema.json' },
40
49
  ];
41
50
  /**
@@ -7,6 +7,7 @@ exports.findOwnPackageRoot = findOwnPackageRoot;
7
7
  exports.getOwnDependencyClosure = getOwnDependencyClosure;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const profiles_js_1 = require("../core/install/profiles.js");
10
11
  const OWN_PACKAGE_NAME = 'pr-checkmate';
11
12
  const PACKAGE_JSON = 'package.json';
12
13
  let cachedClosure;
@@ -89,7 +90,13 @@ function getOwnDependencyClosure() {
89
90
  }
90
91
  // BFS over the dependency graph, resolving each package relative to the parent
91
92
  // that declared it so nested (non-hoisted) installs resolve correctly.
92
- const queue = readDependencyNames(root).map(name => ({
93
+ //
94
+ // Profile tooling is ours even though it no longer sits in `dependencies`: it is
95
+ // fetched on demand, so the roots are what we declare plus what the profiles
96
+ // name. Without the second half every fetched tool is "the client's" and its
97
+ // licence blocks the client for a dependency they cannot remove.
98
+ const roots = new Set([...readDependencyNames(root), ...profiles_js_1.PROFILES.flatMap(p => p.packages)]);
99
+ const queue = [...roots].map(name => ({
93
100
  name,
94
101
  fromDir: root,
95
102
  }));
@@ -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. */
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerToolPaths = registerToolPaths;
7
+ exports.registeredToolPaths = registeredToolPaths;
7
8
  exports.clearToolPaths = clearToolPaths;
8
9
  exports.ownPackageRoot = ownPackageRoot;
9
10
  exports.packagePresent = packagePresent;
@@ -33,6 +34,10 @@ let profileRoots = [];
33
34
  function registerToolPaths(dirs) {
34
35
  profileRoots = [...dirs];
35
36
  }
37
+ /** The roots registered for this run, for tools that load plugins by themselves. */
38
+ function registeredToolPaths() {
39
+ return [...profileRoots];
40
+ }
36
41
  /** Drop every registered profile root. For tests, and for a second run in-process. */
37
42
  function clearToolPaths() {
38
43
  profileRoots = [];
package/dist/esm/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { loadProjectConfig } from './config/loader.js';
2
3
  import { COMMAND_TO_CHECK, FLAGS, STANDALONE_COMMANDS, TOOLING_COMMANDS } from './core/commands.js';
3
4
  import { runChecks } from './core/index.js';
4
5
  import { installProfiles, pruneCache, runDoctor } from './core/install/doctor.js';
6
+ import { ensureGrypeDb } from './core/install/grype-db.js';
5
7
  import { init } from './init.js';
6
- import { logger, getUpdateNotice } from './utils/index.js';
8
+ import { getUpdateNotice, logger } from './utils/index.js';
7
9
  const argv = process.argv.slice(2);
8
10
  /**
9
11
  * Flags are separated from positional arguments so they compose with every command
@@ -88,6 +90,17 @@ void (async () => {
88
90
  for (const result of failed) {
89
91
  logger.error(`[install]: ❌ ${result.profiles.join(', ') || 'tooling'}: ${result.reason}`);
90
92
  }
93
+ // Warm grype's vulnerability database too, with no time cap: the download is
94
+ // hundreds of MB and the scan itself would only be allowed a couple of minutes.
95
+ // Best-effort — an absent grype or a failed download is a warning, not a
96
+ // failed install, since Grype Scan skips cleanly without it.
97
+ if (loadProjectConfig().grypeScan?.enabled !== false) {
98
+ const why = await ensureGrypeDb(process.cwd());
99
+ if (why && why !== 'ENOENT')
100
+ logger.warn(`[install]: ⚠️ grype: ${why}`);
101
+ else if (!why)
102
+ logger.info('[install]: grype vulnerability database is up to date');
103
+ }
91
104
  process.exit(failed.length > 0 ? 1 : 0);
92
105
  }
93
106
  if (cmd === 'doctor') {