pr-checkmate 1.23.23 → 2.0.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 (101) hide show
  1. package/README.md +68 -15
  2. package/cspell.json +3 -8
  3. package/dist/cli.js +37 -62
  4. package/dist/config/constants.d.ts +21 -3
  5. package/dist/config/constants.js +33 -6
  6. package/dist/config/pr-checkmate-workflow.yml +33 -327
  7. package/dist/core/checks/dependencies/banned-imports.js +23 -24
  8. package/dist/core/checks/dependencies/circular-deps.js +1 -0
  9. package/dist/core/checks/dependencies/dependency-check.js +1 -0
  10. package/dist/core/checks/dependencies/grype-scan.js +1 -0
  11. package/dist/core/checks/dependencies/npm-audit.js +6 -1
  12. package/dist/core/checks/dependencies/vuln-scan.js +1 -0
  13. package/dist/core/checks/findings.d.ts +198 -19
  14. package/dist/core/checks/findings.js +474 -22
  15. package/dist/core/checks/git/custom-rules.js +21 -16
  16. package/dist/core/checks/git/diff-smell.d.ts +17 -0
  17. package/dist/core/checks/git/diff-smell.js +43 -24
  18. package/dist/core/checks/git/large-files.js +14 -2
  19. package/dist/core/checks/git/leftover-debug.js +23 -14
  20. package/dist/core/checks/git/lockfile-drift.js +2 -1
  21. package/dist/core/checks/git/merge-conflict.js +19 -8
  22. package/dist/core/checks/git/missing-tests.js +22 -6
  23. package/dist/core/checks/git/todo-fixme.js +10 -9
  24. package/dist/core/checks/languages/shellcheck.d.ts +8 -0
  25. package/dist/core/checks/languages/shellcheck.js +55 -0
  26. package/dist/core/checks/languages/typecheck.js +1 -0
  27. package/dist/core/checks/pr/pr-size-check.js +2 -1
  28. package/dist/core/checks/quality/config-validation.d.ts +14 -4
  29. package/dist/core/checks/quality/config-validation.js +119 -17
  30. package/dist/core/checks/quality/dead-code.js +1 -0
  31. package/dist/core/checks/quality/duplicate-check.js +1 -0
  32. package/dist/core/checks/quality/license-header.js +12 -2
  33. package/dist/core/checks/quality/spellcheck.d.ts +50 -0
  34. package/dist/core/checks/quality/spellcheck.js +141 -13
  35. package/dist/core/checks/quality/symlinks.d.ts +16 -0
  36. package/dist/core/checks/quality/symlinks.js +62 -0
  37. package/dist/core/checks/quality/yaml-lint.js +22 -9
  38. package/dist/core/checks/security/diff-security.js +111 -28
  39. package/dist/core/checks/security/dockerfile-security.js +23 -20
  40. package/dist/core/checks/security/migration-safety.js +19 -17
  41. package/dist/core/checks/security/sensitive-file-guard.js +37 -5
  42. package/dist/core/checks/security/workflow-security.d.ts +10 -0
  43. package/dist/core/checks/security/workflow-security.js +48 -26
  44. package/dist/core/commands.d.ts +27 -0
  45. package/dist/core/commands.js +91 -0
  46. package/dist/core/context.d.ts +12 -3
  47. package/dist/core/context.js +118 -16
  48. package/dist/core/install/cache.d.ts +39 -0
  49. package/dist/core/install/cache.js +99 -0
  50. package/dist/core/install/doctor.d.ts +35 -0
  51. package/dist/core/install/doctor.js +145 -0
  52. package/dist/core/install/ensure.d.ts +60 -0
  53. package/dist/core/install/ensure.js +169 -0
  54. package/dist/core/install/lock.d.ts +33 -0
  55. package/dist/core/install/lock.js +99 -0
  56. package/dist/core/install/profiles.d.ts +64 -0
  57. package/dist/core/install/profiles.js +170 -0
  58. package/dist/core/registry.js +4 -0
  59. package/dist/core/reporters.d.ts +13 -2
  60. package/dist/core/reporters.js +145 -104
  61. package/dist/core/run-checks.js +91 -14
  62. package/dist/core/targets.js +26 -4
  63. package/dist/core/types.d.ts +49 -1
  64. package/dist/init.d.ts +29 -14
  65. package/dist/init.js +204 -262
  66. package/dist/interfaces/index.d.ts +130 -0
  67. package/dist/scripts/generate-install-manifests.d.ts +39 -0
  68. package/dist/scripts/generate-install-manifests.js +121 -0
  69. package/dist/scripts/publish-github.js +18 -4
  70. package/dist/utils/detect-language.d.ts +37 -4
  71. package/dist/utils/detect-language.js +113 -19
  72. package/dist/utils/http.d.ts +59 -0
  73. package/dist/utils/http.js +119 -0
  74. package/dist/utils/logger.d.ts +52 -6
  75. package/dist/utils/logger.js +125 -33
  76. package/dist/utils/pool.d.ts +49 -0
  77. package/dist/utils/pool.js +124 -0
  78. package/dist/utils/update-notifier.d.ts +9 -0
  79. package/dist/utils/update-notifier.js +57 -3
  80. package/eslint.config.mjs +14 -2
  81. package/install-manifests/commitlint/package-lock.json +1028 -0
  82. package/install-manifests/commitlint/package.json +9 -0
  83. package/install-manifests/core/package-lock.json +1335 -0
  84. package/install-manifests/core/package.json +11 -0
  85. package/install-manifests/cpp/package-lock.json +29 -0
  86. package/install-manifests/cpp/package.json +8 -0
  87. package/install-manifests/duplicate/package-lock.json +113 -0
  88. package/install-manifests/duplicate/package.json +8 -0
  89. package/install-manifests/js/package-lock.json +5099 -0
  90. package/install-manifests/js/package.json +26 -0
  91. package/install-manifests/markdown/package-lock.json +1281 -0
  92. package/install-manifests/markdown/package.json +8 -0
  93. package/install-manifests/python/package-lock.json +21 -0
  94. package/install-manifests/python/package.json +8 -0
  95. package/install-manifests/spellcheck/package-lock.json +1047 -0
  96. package/install-manifests/spellcheck/package.json +8 -0
  97. package/package.json +13 -28
  98. package/pr-checkmate.schema.json +1070 -96
  99. package/src/config/cspell.json +3 -8
  100. package/src/config/pr-checkmate-workflow.yml +33 -327
  101. package/src/config/pr-checkmate.schema.json +1070 -96
package/dist/init.d.ts CHANGED
@@ -1,23 +1,38 @@
1
+ /** How strictly the generated workflow treats a failed run. */
2
+ type GateMode = 'strict' | 'advisory';
1
3
  /**
2
- * Merge current defaults into an existing config, recursively.
4
+ * Write the bundled GitHub Actions workflow into the consumer repo.
3
5
  *
4
- * The user's value always wins, so hand-tuned settings survive an `init` re-run,
5
- * while every new default field — at any nesting depth — gets added. Arrays are
6
- * replaced wholesale rather than concatenated: lists like `ignoreDirs` or
7
- * `cspell.words` are meant to be edited, and merging them would resurrect entries
8
- * the user deliberately removed. {@link REPLACED_OBJECT_PATHS} extends that rule to
9
- * the object-shaped lists.
6
+ * Never overwrites an existing file. `init` is not a one-time command — the
7
+ * update notice asks people to re-run it after every upgrade — so clobbering
8
+ * would quietly discard whatever they had tuned, including the gate setting they
9
+ * were just asked about.
10
+ */
11
+ export declare function setupWorkflow(root: string, mode: GateMode): void;
12
+ /**
13
+ * Every check that fails the run out of the box, written into the generated
14
+ * config at its real default.
15
+ *
16
+ * Not a copy of a default for its own sake — this one is the gate's teeth, and
17
+ * the client is entitled to see which checks can stop their merge without
18
+ * reading our source or discovering it from a red pipeline. Written as `"error"`
19
+ * so the edit is in place: change the value, or delete the line to inherit
20
+ * whatever we ship next.
10
21
  *
11
- * Informational `//`-prefixed keys are the one exception — they are documentation,
12
- * not settings, so the refreshed default text wins whenever a default exists.
22
+ * Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
23
+ * block too, but only for rules the client writes themselves, so listing them
24
+ * would suggest a gate that is inert until they act. And `Dockerfile Security`
25
+ * looks like it belongs here and does not: every one of its patterns is rated
26
+ * `warn`, so it never fails on its own.
13
27
  *
14
- * @param defaults Current defaults for this level of the config tree.
15
- * @param user The existing pr-checkmate.json content for the same level.
16
- * @param path Dot-joined position of `defaults` in the config tree, used to match
17
- * {@link REPLACED_OBJECT_PATHS}. Internal to the recursion; callers pass nothing.
28
+ * Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
29
+ * every name here to the registry, which catches a rename but not a change of
30
+ * heart about what should block.
18
31
  */
19
- export declare function mergeWithDefaults(defaults: Record<string, unknown>, user: Record<string, unknown>, path?: string): Record<string, unknown>;
32
+ export declare const BLOCKING_BY_DEFAULT: readonly ["Security Scan", "Sensitive Files", "Diff Security", "Workflow Security", "Merge Conflict", "NPM Audit", "License Check", "Dependencies", "TypeScript", "ESLint", "YAML Lint"];
33
+ export declare function buildInitialConfig(sourcePath: string, languages: string[]): Record<string, unknown>;
20
34
  /**
21
35
  * Initialize (or refresh) pr-checkmate configuration in the current project.
22
36
  */
23
37
  export declare function init(): Promise<void>;
38
+ export {};
package/dist/init.js CHANGED
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.mergeWithDefaults = mergeWithDefaults;
6
+ exports.BLOCKING_BY_DEFAULT = void 0;
7
+ exports.setupWorkflow = setupWorkflow;
8
+ exports.buildInitialConfig = buildInitialConfig;
7
9
  exports.init = init;
8
10
  const fs_1 = __importDefault(require("fs"));
9
11
  const path_1 = __importDefault(require("path"));
@@ -12,184 +14,11 @@ const config_1 = require("./config");
12
14
  const utils_1 = require("./utils");
13
15
  const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
14
16
  /**
15
- * Default configuration structure with customization options
17
+ * Points editors at the bundled JSON schema. With the generated config reduced
18
+ * to the client's own decisions, this line is what makes every other option
19
+ * discoverable: the editor autocompletes each key and shows its description.
16
20
  */
17
- const DEFAULT_CONFIG = {
18
- // Points editors (VS Code, etc.) at the bundled JSON schema for autocomplete
19
- // and inline validation of this file.
20
- $schema: './node_modules/pr-checkmate/pr-checkmate.schema.json',
21
- // Available CLI commands — kept at the very top as a quick reference.
22
- // Informational only; PR CheckMate ignores this at runtime.
23
- commands: config_1.DEFAULT_COMMANDS,
24
- // Human-readable hints. JSON has no comments, so these `//`-prefixed keys are
25
- // just informational strings — PR CheckMate ignores them at runtime.
26
- '// reporting': [
27
- 'Results post to the PR/MR automatically.',
28
- 'GitHub: no setup — the runner injects GITHUB_TOKEN; just keep `permissions: pull-requests: write` in the workflow.',
29
- 'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add it as CI/CD variable GITLAB_TOKEN.',
30
- 'Bitbucket: create a token (Repo → Settings → Access tokens, scope pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
31
- 'The token lives in YOUR CI variables, never in this file. Without it, checks still run and print to the log — only the comment is skipped.',
32
- ].join(' '),
33
- sourcePath: '.',
34
- // Directories excluded from every check (matched on any path segment). Edit
35
- // freely — add your own generated/vendored dirs or remove ones you want scanned.
36
- ignoreDirs: config_1.IGNORED_DIRS,
37
- // ESLint — `rules` starts EMPTY on purpose and holds overrides only, each merged
38
- // over the bundled default for that one rule.
39
- //
40
- // It used to be written out in full (all 27 rules with their severities) so every
41
- // rule was visible and editable in this file. That copy then won over the bundled
42
- // defaults forever: `eslint.rules` is in REPLACED_OBJECT_PATHS, so a later `init`
43
- // keeps the user's block wholesale, and improvements to the defaults never reached
44
- // a repo that had ever run `init`. Downgrading `prettier/prettier` from error to
45
- // warn — so formatting stops failing pipelines — is exactly such an improvement,
46
- // and it stopped at the config boundary for every existing client.
47
- //
48
- // The full list with defaults lives in the ESLint check documentation and in the
49
- // package's own eslint-default-rules.json; discoverability there costs nothing,
50
- // un-updatable defaults cost every client an edit.
51
- eslint: {
52
- rules: {},
53
- ignorePatterns: [],
54
- },
55
- // Prettier customization
56
- prettier: {
57
- rules: {},
58
- },
59
- // Spellcheck (cspell). `enabled: false` turns the check off — that part always
60
- // applies. The lists feed the cspell config pr-checkmate generates, so they are
61
- // ignored when the repo has a cspell.json of its own; all of them are appended to
62
- // the bundled defaults. `ignorePaths` takes globs to skip (e.g. "CHANGELOG.md",
63
- // "generated/**"), anchored to the repo root so a pattern never matches a
64
- // directory above the checkout.
65
- cspell: {
66
- enabled: true,
67
- words: [],
68
- ignorePaths: [],
69
- ignoreRegExpList: [],
70
- },
71
- // Markdownlint customization
72
- markdownlint: {
73
- rules: {},
74
- ignores: [],
75
- },
76
- // YAML lint (bundled `yaml` parser). Reports syntax errors, duplicate keys,
77
- // and tab/indentation problems in .yml/.yaml files. Blocking by default
78
- // (severity "error"); set severity "warn" to make it advisory, or enabled
79
- // false to disable. `ignore` takes glob patterns to skip (e.g. "charts/**").
80
- yamlLint: {
81
- enabled: true,
82
- ignore: [],
83
- },
84
- // TypeScript type check
85
- typecheck: {
86
- enabled: true,
87
- },
88
- // Diff code smell detection
89
- diffSmell: {
90
- enabled: true,
91
- },
92
- // PR description validation
93
- prBody: {
94
- enabled: true,
95
- minLength: 20,
96
- requireTicket: true,
97
- },
98
- // Duplicate code detection (jscpd)
99
- duplicate: {
100
- enabled: true,
101
- minLines: 5,
102
- minTokens: 50,
103
- // Test files/folders are excluded by default; edit to customise.
104
- ignore: config_1.DUPLICATE_IGNORE_DEFAULTS,
105
- // Only source-code formats are scanned. Data formats (yaml, json, markdown)
106
- // are intentionally absent — add them here to detect duplication in them too.
107
- formats: config_1.DUPLICATE_CODE_FORMATS,
108
- },
109
- // Dependency usage check (knip)
110
- dependency: {
111
- enabled: true,
112
- // Dependency names to skip (e.g. CLI-only tools invoked as binaries,
113
- // which knip can't see as imports and would report as unused).
114
- ignore: [],
115
- },
116
- // Circular-import check (dpdm, bundled — nothing needed on the runner). Uses
117
- // the project's own dpdm when it has one. Advisory by default (severity
118
- // "warn"): most repos have long-standing cycles through barrel files, so see
119
- // yours first, then set severity "error" to gate merges on them. enabled false
120
- // disables the check; `ignore` takes glob patterns kept out of the analysis
121
- // (generated or vendored code, or barrels whose cycles you tolerate). When the
122
- // analyzer cannot run, the check skips rather than passing or failing.
123
- circularDeps: {
124
- enabled: true,
125
- severity: 'warn',
126
- ignore: [],
127
- timeoutMs: 120000,
128
- },
129
- // Unused-exports check (ts-unused-exports)
130
- deadCode: {
131
- enabled: true,
132
- // Regex sources matched against file paths; matching files are dropped from
133
- // the report. Defaults to test/mock files, whose exports are used only by the
134
- // test runner. Add barrels/entry points here to exempt intentional exports.
135
- ignoreFiles: config_1.DEAD_CODE_IGNORE_DEFAULTS,
136
- },
137
- // User-defined rules: regex patterns run against added diff lines. The escape
138
- // hatch to enforce any project rule without code. Example entry:
139
- // { "name": "no-focused-tests", "pattern": "\\b(describe|it)\\.only\\(",
140
- // "message": "focused test left in", "severity": "error",
141
- // "include": ["src/**"], "exclude": ["**/*.test.*"] }
142
- customRules: [],
143
- // Universal per-check severity override, keyed by the check's display name (as
144
- // shown in the report). "error" fails the run, "warn" is advisory, "off"
145
- // disables the check. Empty by default — every check uses its built-in
146
- // severity. Examples: { "Merge Conflict": "warn", "ESLint": "error" }.
147
- severity: {},
148
- // Git / PR-diff heuristics (delta-aware). Merge Conflict is blocking by
149
- // default; the rest are advisory — promote any of them with `severity`.
150
- mergeConflict: { enabled: true },
151
- leftoverDebug: { enabled: true },
152
- lockfileDrift: { enabled: true },
153
- largeFiles: { enabled: true, maxSizeKb: 500, allow: [] },
154
- missingTests: { enabled: true },
155
- todoFixme: { enabled: true, requireTicket: false },
156
- migrationSafety: { enabled: true },
157
- // Security (regex diff scans + sensitive-file guard). Secret scan / npm audit
158
- // live under `security`.
159
- diffSecurity: { enabled: true },
160
- sensitiveFileGuard: { enabled: true },
161
- workflowSecurity: { enabled: true }, // GitHub Actions: injection, unpinned actions
162
- dockerfileSecurity: { enabled: true }, // Dockerfile: :latest, USER root, curl|bash, …
163
- caseCollision: { enabled: true }, // files differing only by case
164
- vulnScan: { enabled: false }, // cross-language dep vulns (osv-scanner, runner-dependency)
165
- // dep vulns (grype, non-blocking, runner-dependency; skips if grype absent).
166
- // Never walks `ignoreDirs`; `timeoutMs` caps the scan so it cannot stall a commit.
167
- grypeScan: { enabled: true, minSeverity: 'medium', timeoutMs: 120000 },
168
- // Test-coverage presence. Opt-in: enable once your CI produces a coverage
169
- // report (lcov/cobertura/jacoco/…); warns if none is found.
170
- coverage: { enabled: false },
171
- // PR metadata / dependency hygiene.
172
- prSize: { enabled: true, maxFiles: 50, maxLines: 1000 },
173
- commitlint: { enabled: true },
174
- // Dependencies you want to forbid. Example: { "name": "moment", "message": "use date-fns" }.
175
- bannedImports: [],
176
- // License/copyright header on new source files. Opt-in — set enabled + pattern.
177
- licenseHeader: { enabled: false, pattern: '', lines: 10 },
178
- outdatedDeps: { enabled: true, ignore: [] },
179
- // Per-language checks. Bundled (no client install): TypeScript, Python (Ruff),
180
- // C++ (clang-format). Runner-dependency (need the tool on the runner, skip
181
- // gracefully if absent): Swift, Kotlin, Go, Rust, C#, Ruby, PHP.
182
- python: { enabled: true, typecheck: true },
183
- cpp: { enabled: true },
184
- swift: { enabled: true },
185
- kotlin: { enabled: true },
186
- go: { enabled: true },
187
- rust: { enabled: true },
188
- csharp: { enabled: true },
189
- ruby: { enabled: true },
190
- php: { enabled: true },
191
- documentation: config_1.documentation,
192
- };
21
+ const SCHEMA_PATH = './node_modules/pr-checkmate/pr-checkmate.schema.json';
193
22
  const MANUAL_ENTRY = 'Enter manually';
194
23
  /** Prompt the user for the project source path, allowing a manual entry. */
195
24
  async function promptSourcePath() {
@@ -213,8 +42,55 @@ async function promptSourcePath() {
213
42
  ]);
214
43
  return answers.sourcePath === MANUAL_ENTRY ? answers.manualPath : answers.sourcePath;
215
44
  }
216
- /** Copy the bundled GitHub Actions workflow into the consumer repo. */
217
- function setupWorkflow(root) {
45
+ /**
46
+ * Ask whether the generated workflow should block merges.
47
+ *
48
+ * Two levers decide strictness and they are not interchangeable: this one says
49
+ * whether a failed run means anything to GitHub at all, while `severity` in
50
+ * pr-checkmate.json says which findings count as a failure. The recommended way
51
+ * into an existing codebase is a strict workflow plus a permissive config — the
52
+ * gate is real from day one, and checks are promoted one at a time.
53
+ */
54
+ async function promptGateMode() {
55
+ const { mode } = await inquirer_1.default.prompt([
56
+ {
57
+ type: 'select',
58
+ name: 'mode',
59
+ message: 'Should a failed check block the pull request?',
60
+ default: 'strict',
61
+ choices: [
62
+ { name: 'Yes — block the merge (recommended)', value: 'strict' },
63
+ { name: 'No — report only, never block', value: 'advisory' },
64
+ ],
65
+ },
66
+ ]);
67
+ return mode;
68
+ }
69
+ /** The banner an advisory workflow carries, so it reads as a decision, not an accident. */
70
+ const ADVISORY_NOTICE = [
71
+ ' # ADVISORY MODE — this run cannot block a merge.',
72
+ ' #',
73
+ ' # Chosen at `pr-checkmate init`. Reasonable while adopting the tool on an',
74
+ ' # existing codebase, but it also means a detected secret does not stop the',
75
+ ' # PR. To make this a real gate, delete the `continue-on-error` line below.',
76
+ ' #',
77
+ ' # Prefer the opposite arrangement: keep the gate real and relax individual',
78
+ ' # checks in pr-checkmate.json instead —',
79
+ ' # "severityDefaults": { "failOn": "warn" } everything advisory',
80
+ ' # "severity": { "Security Scan": "error" } promote one back to a gate',
81
+ ' # That way whatever you have already made clean stays enforced.',
82
+ ].join('\n');
83
+ /** Indented to sit inside the step, not beside it — YAML is unforgiving here. */
84
+ const ADVISORY_FLAG = ' continue-on-error: true';
85
+ /**
86
+ * Write the bundled GitHub Actions workflow into the consumer repo.
87
+ *
88
+ * Never overwrites an existing file. `init` is not a one-time command — the
89
+ * update notice asks people to re-run it after every upgrade — so clobbering
90
+ * would quietly discard whatever they had tuned, including the gate setting they
91
+ * were just asked about.
92
+ */
93
+ function setupWorkflow(root, mode) {
218
94
  const workflowSource = path_1.default.join(__dirname, 'config', 'pr-checkmate-workflow.yml');
219
95
  const workflowDir = path_1.default.join(root, '.github', 'workflows');
220
96
  const workflowDest = path_1.default.join(workflowDir, 'pr-checkmate.yml');
@@ -222,13 +98,22 @@ function setupWorkflow(root) {
222
98
  utils_1.logger.warn('[init]: ⚠️ Workflow template not found, skipping workflow setup');
223
99
  return;
224
100
  }
101
+ if (fs_1.default.existsSync(workflowDest)) {
102
+ utils_1.logger.info('[init]: ⏭️ .github/workflows/pr-checkmate.yml already exists — left untouched');
103
+ return;
104
+ }
225
105
  if (!fs_1.default.existsSync(workflowDir)) {
226
106
  fs_1.default.mkdirSync(workflowDir, { recursive: true });
227
107
  }
228
- fs_1.default.copyFileSync(workflowSource, workflowDest);
229
- utils_1.logger.info('[init]: ✅ Created .github/workflows/pr-checkmate.yml');
230
- utils_1.logger.info('[init]: GitHub Actions workflow is ready for your PRs!');
108
+ const template = fs_1.default.readFileSync(workflowSource, 'utf-8');
109
+ const workflow = mode === 'advisory'
110
+ ? template.replace(RUN_STEP_ANCHOR, `${ADVISORY_NOTICE}\n${RUN_STEP_ANCHOR}\n${ADVISORY_FLAG}`)
111
+ : template;
112
+ fs_1.default.writeFileSync(workflowDest, workflow);
113
+ utils_1.logger.info(`[init]: ✅ Created .github/workflows/pr-checkmate.yml (${mode === 'strict' ? 'blocking' : 'advisory'})`);
231
114
  }
115
+ /** Where the advisory banner is spliced in — the step that runs the checks. */
116
+ const RUN_STEP_ANCHOR = ' - name: Run PR CheckMate';
232
117
  /** Create a tsconfig.json scaffold if the consumer repo lacks one. */
233
118
  function setupTsConfig(root, sourcePath) {
234
119
  const tsconfigPath = path_1.default.join(root, 'tsconfig.json');
@@ -254,107 +139,164 @@ function setupTsConfig(root, sourcePath) {
254
139
  fs_1.default.writeFileSync(tsconfigPath, JSON.stringify(newTsConfig, null, 2));
255
140
  utils_1.logger.info('[init]: ✅ Created tsconfig.json using your sourcePath');
256
141
  }
142
+ /**
143
+ * The file `init` writes for a new project — the client's decisions, nothing else.
144
+ *
145
+ * Every other setting is deliberately absent. The config is deep-merged over the
146
+ * built-in defaults when it is loaded, so a key that repeats a default changes
147
+ * nothing: it only lengthens the file and freezes today's value in someone
148
+ * else's repository. Three hundred generated lines also read as a wall of
149
+ * settings to maintain, when there is nothing to maintain.
150
+ *
151
+ * Discoverability moves to two places that cannot go stale — `$schema`, which
152
+ * makes an editor autocomplete every option with its description, and the
153
+ * annotated reference shipped inside the package.
154
+ */
155
+ /** The one-line answer to "how do I change this" — see {@link buildInitialConfig}. */
156
+ const REPORTING_HINT = [
157
+ 'Results post to the PR/MR automatically.',
158
+ 'GitHub: keep `permissions: pull-requests: write` AND pass the token to the step',
159
+ '(`env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`) — Actions does not provide it',
160
+ 'on its own, and without it the comment is silently skipped. The generated',
161
+ 'workflow already does both.',
162
+ 'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add',
163
+ 'it as CI/CD variable GITLAB_TOKEN.',
164
+ 'Bitbucket: create a token (Repo → Settings → Access tokens, scope',
165
+ 'pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
166
+ 'The token lives in YOUR CI variables, never in this file. Without it checks',
167
+ 'still run and print to the log — only the comment is skipped.',
168
+ ].join(' ');
169
+ /**
170
+ * Every check that fails the run out of the box, written into the generated
171
+ * config at its real default.
172
+ *
173
+ * Not a copy of a default for its own sake — this one is the gate's teeth, and
174
+ * the client is entitled to see which checks can stop their merge without
175
+ * reading our source or discovering it from a red pipeline. Written as `"error"`
176
+ * so the edit is in place: change the value, or delete the line to inherit
177
+ * whatever we ship next.
178
+ *
179
+ * Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
180
+ * block too, but only for rules the client writes themselves, so listing them
181
+ * would suggest a gate that is inert until they act. And `Dockerfile Security`
182
+ * looks like it belongs here and does not: every one of its patterns is rated
183
+ * `warn`, so it never fails on its own.
184
+ *
185
+ * Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
186
+ * every name here to the registry, which catches a rename but not a change of
187
+ * heart about what should block.
188
+ */
189
+ exports.BLOCKING_BY_DEFAULT = [
190
+ 'Security Scan',
191
+ 'Sensitive Files',
192
+ 'Diff Security',
193
+ 'Workflow Security',
194
+ 'Merge Conflict',
195
+ 'NPM Audit',
196
+ 'License Check',
197
+ 'Dependencies',
198
+ 'TypeScript',
199
+ 'ESLint',
200
+ 'YAML Lint',
201
+ ];
202
+ const HOW_TO = [
203
+ 'Only what differs from the defaults belongs here — everything else is implicit',
204
+ 'and stays current on upgrade, so this file stays short.',
205
+ 'Turn a check off: "severity": { "Spellcheck": "off" }',
206
+ 'Make one advisory: "severity": { "Sensitive Files": "warn" }',
207
+ 'Make everything advisory while adopting: "severityDefaults": { "failOn": "warn" }',
208
+ 'Skip a file or folder: "excludePaths": ["src/generated/**", "vendor/legacy.ts"]',
209
+ 'Check names are the ones printed in the report.',
210
+ `Every option, annotated, with its default: ${config_1.FULL_CONFIG_URL}`,
211
+ `Docs: ${config_1.DOCS_URL}`,
212
+ ].join(' ');
213
+ function buildInitialConfig(sourcePath, languages) {
214
+ return {
215
+ $schema: SCHEMA_PATH,
216
+ '// how-to': HOW_TO,
217
+ commands: config_1.DEFAULT_COMMANDS,
218
+ '// reporting': REPORTING_HINT,
219
+ sourcePath,
220
+ // What can stop a merge, and how to stand any of it down. "warn" keeps the
221
+ // finding in the report without failing the run; "off" removes the check.
222
+ severity: Object.fromEntries(exports.BLOCKING_BY_DEFAULT.map(name => [name, 'error'])),
223
+ // The knobs people reach for first, seeded EMPTY.
224
+ //
225
+ // Empty is the whole point: an empty container changes nothing, so it cannot
226
+ // freeze today's default in someone else's repository, but it puts the edit
227
+ // one line away instead of behind a lookup. Which blocks appear here is
228
+ // decided by what actually makes noise in real repositories — spelling,
229
+ // lint rules, generated files, knip's guesses about unused dependencies.
230
+ //
231
+ // Three blocks are deliberately NOT here, because for them an empty value is
232
+ // not neutral — it REPLACES a non-empty default and makes things worse:
233
+ // ignoreDirs → seeded empty, nothing is ignored at all
234
+ // duplicate.ignore → test boilerplate starts reporting as duplication
235
+ // deadCode.ignoreFiles → test helpers start reporting as unused exports
236
+ // Anyone who needs those sets them deliberately, with the defaults in view.
237
+ excludePaths: [],
238
+ cspell: { words: [], ignorePaths: [] },
239
+ eslint: { rules: {}, ignorePatterns: [] },
240
+ markdownlint: { ignores: [] },
241
+ dependency: { ignore: [] },
242
+ circularDeps: { ignore: [] },
243
+ yamlLint: { ignore: [] },
244
+ documentation: config_1.documentation,
245
+ languages,
246
+ };
247
+ }
257
248
  /** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
258
249
  async function createConfig(root) {
259
250
  const sourcePath = await promptSourcePath();
251
+ const gateMode = await promptGateMode();
260
252
  const languages = await (0, utils_1.detectLanguages)(root);
261
253
  utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
262
- const config = {
263
- ...DEFAULT_CONFIG,
264
- sourcePath,
265
- languages,
266
- };
267
- // Omit config blocks for languages not present in the repo — a cleaner,
268
- // tailored file. This does NOT disable them: the key is simply absent, which
269
- // still defaults to enabled, and runtime language detection gates whether each
270
- // check runs. So a language added later auto-activates without editing config.
271
- const LANGUAGE_TOGGLE_KEYS = [
272
- 'python',
273
- 'cpp',
274
- 'swift',
275
- 'kotlin',
276
- 'go',
277
- 'rust',
278
- 'csharp',
279
- 'ruby',
280
- 'php',
281
- ];
282
- for (const key of LANGUAGE_TOGGLE_KEYS) {
283
- if (!languages.includes(key))
284
- delete config[key];
285
- }
286
- fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
254
+ const config = buildInitialConfig(sourcePath, languages);
255
+ fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
287
256
  utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
288
257
  utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
289
258
  and cspell words in pr-checkmate.json`);
290
- setupWorkflow(root);
259
+ setupWorkflow(root, gateMode);
291
260
  setupTsConfig(root, sourcePath);
292
261
  }
293
- function isPlainObject(value) {
294
- return typeof value === 'object' && value !== null && !Array.isArray(value);
295
- }
296
262
  /**
297
- * Config paths whose object value is a curated list, not a set of settings — the
298
- * user is meant to add and remove entries, exactly like an array.
263
+ * Re-run setup: refresh the detected languages and leave everything else alone.
299
264
  *
300
- * These are merged the way arrays are: the user's object replaces the default one
301
- * wholesale. Deep-merging them would hand back every rule the user deleted, and for
302
- * `eslint.rules` that is not cosmetic — Lint is a blocking check, so a re-run of
303
- * `init` would silently reinstate rules and start failing the next PR on them. The
304
- * documented way to switch a rule off is `"off"`, which survives either way.
265
+ * It used to merge current defaults into the client's file, which is why the
266
+ * update notice insisted `init` must not be skipped after an upgrade. Defaults
267
+ * now apply when the config is loaded, so there is nothing to top up.
305
268
  */
306
- const REPLACED_OBJECT_PATHS = new Set(['eslint.rules', 'prettier.rules', 'markdownlint.rules']);
307
- /**
308
- * Merge current defaults into an existing config, recursively.
309
- *
310
- * The user's value always wins, so hand-tuned settings survive an `init` re-run,
311
- * while every new default field — at any nesting depth — gets added. Arrays are
312
- * replaced wholesale rather than concatenated: lists like `ignoreDirs` or
313
- * `cspell.words` are meant to be edited, and merging them would resurrect entries
314
- * the user deliberately removed. {@link REPLACED_OBJECT_PATHS} extends that rule to
315
- * the object-shaped lists.
316
- *
317
- * Informational `//`-prefixed keys are the one exception — they are documentation,
318
- * not settings, so the refreshed default text wins whenever a default exists.
319
- *
320
- * @param defaults Current defaults for this level of the config tree.
321
- * @param user The existing pr-checkmate.json content for the same level.
322
- * @param path Dot-joined position of `defaults` in the config tree, used to match
323
- * {@link REPLACED_OBJECT_PATHS}. Internal to the recursion; callers pass nothing.
324
- */
325
- function mergeWithDefaults(defaults, user, path = '') {
326
- const merged = { ...defaults };
327
- for (const [key, userValue] of Object.entries(user)) {
328
- const defaultValue = defaults[key];
329
- const keyPath = path ? `${path}.${key}` : key;
330
- if (key.startsWith('//') && defaultValue !== undefined)
331
- continue;
332
- merged[key] =
333
- isPlainObject(defaultValue) && isPlainObject(userValue) && !REPLACED_OBJECT_PATHS.has(keyPath)
334
- ? mergeWithDefaults(defaultValue, userValue, keyPath)
335
- : userValue;
336
- }
337
- return merged;
338
- }
339
- /** Re-run setup: merge the existing config with current defaults. */
340
269
  async function updateConfig(root) {
341
270
  utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
342
271
  try {
343
272
  const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
344
273
  const json = JSON.parse(raw);
345
- const languages = await (0, utils_1.detectLanguages)(root);
274
+ // Re-detection has to obey the config it is about to rewrite: a client who
275
+ // tuned `detect` (or `ignoreDirs`) precisely because detection was wrong
276
+ // would otherwise get the wrong answer written back over the right one on
277
+ // the next `init`.
278
+ const languages = await (0, utils_1.detectLanguages)(root, json.ignoreDirs, json.detect);
346
279
  utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
347
- // Merge with defaults to add new fields at every depth — a hand-maintained
348
- // list of sections to merge would silently stop delivering new defaults to
349
- // existing configs the moment a section is forgotten.
280
+ // The client's file is left exactly as they wrote it. Defaults are no longer
281
+ // copied into it, because they never needed to be: they are merged in when
282
+ // the config is loaded, so a new default reaches every repository on upgrade
283
+ // without anyone editing anything. Writing them here only grew the file and
284
+ // made `init` a step nobody could safely skip after a release.
285
+ //
286
+ // `languages` is still refreshed — it is a fact about the repository, not a
287
+ // preference. `$schema` and the how-to hint are added when missing, because
288
+ // an existing config is exactly the case where nobody has seen them: the
289
+ // people who most need to know how to switch a check off are the ones whose
290
+ // file predates the advice.
350
291
  const updated = {
351
- ...mergeWithDefaults(DEFAULT_CONFIG, json),
352
- // Refresh detected languages on every init so new source dirs are picked up
292
+ $schema: json.$schema ?? SCHEMA_PATH,
293
+ '// how-to': json['// how-to'] ?? HOW_TO,
294
+ ...json,
353
295
  languages,
354
296
  };
355
- fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
356
- utils_1.logger.info('[init]: 🔧 Updated pr-checkmate.json with customization options.');
357
- utils_1.logger.info('[init]: 💡 You can now add custom ESLint rules, cspell words, and more!');
297
+ fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
298
+ utils_1.logger.info('[init]: 🔧 Refreshed the detected languages in pr-checkmate.json.');
299
+ utils_1.logger.info(`[init]: 💡 Everything else is left as you wrote it — defaults apply on their own. Options: ${config_1.DOCS_URL}`);
358
300
  }
359
301
  catch (e) {
360
302
  utils_1.logger.error(`[init]: ❌ Failed to update pr-checkmate.json: ${e}`);