pr-checkmate 1.23.24 → 2.1.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 (105) hide show
  1. package/README.md +89 -13
  2. package/cspell.json +11 -9
  3. package/dist/cli.js +37 -64
  4. package/dist/config/constants.d.ts +51 -3
  5. package/dist/config/constants.js +73 -11
  6. package/dist/config/pr-checkmate-workflow-docker.yml +79 -0
  7. package/dist/config/pr-checkmate-workflow.yml +65 -326
  8. package/dist/core/checks/dependencies/banned-imports.js +23 -24
  9. package/dist/core/checks/dependencies/circular-deps.js +1 -0
  10. package/dist/core/checks/dependencies/dependency-check.js +1 -0
  11. package/dist/core/checks/dependencies/grype-scan.js +1 -0
  12. package/dist/core/checks/dependencies/npm-audit.js +6 -1
  13. package/dist/core/checks/dependencies/vuln-scan.js +1 -0
  14. package/dist/core/checks/findings.d.ts +198 -19
  15. package/dist/core/checks/findings.js +474 -22
  16. package/dist/core/checks/git/custom-rules.js +21 -16
  17. package/dist/core/checks/git/diff-smell.d.ts +17 -0
  18. package/dist/core/checks/git/diff-smell.js +43 -24
  19. package/dist/core/checks/git/large-files.js +14 -2
  20. package/dist/core/checks/git/leftover-debug.js +23 -14
  21. package/dist/core/checks/git/lockfile-drift.js +2 -1
  22. package/dist/core/checks/git/merge-conflict.js +19 -8
  23. package/dist/core/checks/git/missing-tests.js +22 -6
  24. package/dist/core/checks/git/todo-fixme.js +10 -9
  25. package/dist/core/checks/languages/shellcheck.d.ts +1 -1
  26. package/dist/core/checks/languages/shellcheck.js +1 -1
  27. package/dist/core/checks/languages/typecheck.js +1 -0
  28. package/dist/core/checks/pr/pr-size-check.js +2 -1
  29. package/dist/core/checks/quality/config-validation.d.ts +14 -4
  30. package/dist/core/checks/quality/config-validation.js +119 -17
  31. package/dist/core/checks/quality/dead-code.js +1 -0
  32. package/dist/core/checks/quality/duplicate-check.js +1 -0
  33. package/dist/core/checks/quality/license-header.js +12 -2
  34. package/dist/core/checks/quality/markdown-lint.js +7 -2
  35. package/dist/core/checks/quality/spellcheck.d.ts +50 -0
  36. package/dist/core/checks/quality/spellcheck.js +141 -13
  37. package/dist/core/checks/quality/yaml-lint.js +22 -9
  38. package/dist/core/checks/security/diff-security.js +115 -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/reporters.d.ts +13 -2
  59. package/dist/core/reporters.js +145 -104
  60. package/dist/core/run-checks.d.ts +25 -1
  61. package/dist/core/run-checks.js +124 -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 +284 -263
  66. package/dist/interfaces/index.d.ts +120 -0
  67. package/dist/scripts/copy-configs.js +4 -0
  68. package/dist/scripts/generate-install-manifests.d.ts +39 -0
  69. package/dist/scripts/generate-install-manifests.js +121 -0
  70. package/dist/scripts/publish-github.js +18 -4
  71. package/dist/utils/detect-language.d.ts +37 -4
  72. package/dist/utils/detect-language.js +113 -19
  73. package/dist/utils/http.d.ts +59 -0
  74. package/dist/utils/http.js +119 -0
  75. package/dist/utils/logger.d.ts +52 -6
  76. package/dist/utils/logger.js +125 -33
  77. package/dist/utils/pool.d.ts +49 -0
  78. package/dist/utils/pool.js +124 -0
  79. package/dist/utils/update-notifier.d.ts +9 -0
  80. package/dist/utils/update-notifier.js +57 -3
  81. package/eslint-default-rules.json +1 -1
  82. package/eslint.config.mjs +14 -2
  83. package/install-manifests/commitlint/package-lock.json +1028 -0
  84. package/install-manifests/commitlint/package.json +9 -0
  85. package/install-manifests/core/package-lock.json +1335 -0
  86. package/install-manifests/core/package.json +11 -0
  87. package/install-manifests/cpp/package-lock.json +29 -0
  88. package/install-manifests/cpp/package.json +8 -0
  89. package/install-manifests/duplicate/package-lock.json +113 -0
  90. package/install-manifests/duplicate/package.json +8 -0
  91. package/install-manifests/js/package-lock.json +5099 -0
  92. package/install-manifests/js/package.json +26 -0
  93. package/install-manifests/markdown/package-lock.json +1281 -0
  94. package/install-manifests/markdown/package.json +8 -0
  95. package/install-manifests/python/package-lock.json +21 -0
  96. package/install-manifests/python/package.json +8 -0
  97. package/install-manifests/spellcheck/package-lock.json +1047 -0
  98. package/install-manifests/spellcheck/package.json +8 -0
  99. package/package.json +14 -28
  100. package/pr-checkmate.schema.json +1062 -104
  101. package/src/config/cspell.json +11 -9
  102. package/src/config/eslint-default-rules.json +1 -1
  103. package/src/config/pr-checkmate-workflow-docker.yml +79 -0
  104. package/src/config/pr-checkmate-workflow.yml +65 -326
  105. package/src/config/pr-checkmate.schema.json +1062 -104
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,22 +42,125 @@ 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) {
218
- const workflowSource = path_1.default.join(__dirname, 'config', 'pr-checkmate-workflow.yml');
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) {
94
+ // Which template a repository gets is decided by whether it actually installs
95
+ // us. Both are correct for their own case and wrong for the other:
96
+ //
97
+ // - a dependency means `npx pr-checkmate` runs the copy the lockfile pins,
98
+ // which is fast and reproducible;
99
+ // - no dependency means `npx` resolves to the newest release and downloads it
100
+ // on every job — 547 MB, about five minutes, and a major version can land
101
+ // without anyone changing a line.
102
+ //
103
+ // Asking the client to notice that difference in a comment is asking them to
104
+ // read a file they scaffolded precisely so they would not have to.
105
+ const source = installsPackage(root)
106
+ ? 'pr-checkmate-workflow.yml'
107
+ : 'pr-checkmate-workflow-docker.yml';
108
+ const workflowSource = path_1.default.join(__dirname, 'config', source);
219
109
  const workflowDir = path_1.default.join(root, '.github', 'workflows');
220
110
  const workflowDest = path_1.default.join(workflowDir, 'pr-checkmate.yml');
221
111
  if (!fs_1.default.existsSync(workflowSource)) {
222
112
  utils_1.logger.warn('[init]: ⚠️ Workflow template not found, skipping workflow setup');
223
113
  return;
224
114
  }
115
+ if (fs_1.default.existsSync(workflowDest)) {
116
+ utils_1.logger.info('[init]: ⏭️ .github/workflows/pr-checkmate.yml already exists — left untouched');
117
+ return;
118
+ }
225
119
  if (!fs_1.default.existsSync(workflowDir)) {
226
120
  fs_1.default.mkdirSync(workflowDir, { recursive: true });
227
121
  }
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!');
122
+ const template = fs_1.default.readFileSync(workflowSource, 'utf-8').replace(/__IMAGE_TAG__/g, imageTag());
123
+ const workflow = mode === 'advisory'
124
+ ? template.replace(RUN_STEP_ANCHOR, `${ADVISORY_NOTICE}\n${RUN_STEP_ANCHOR}\n${ADVISORY_FLAG}`)
125
+ : template;
126
+ fs_1.default.writeFileSync(workflowDest, workflow);
127
+ utils_1.logger.info(`[init]: ✅ Created .github/workflows/pr-checkmate.yml (${mode === 'strict' ? 'blocking' : 'advisory'})`);
128
+ }
129
+ /**
130
+ * Whether this repository installs pr-checkmate rather than reaching for it on
131
+ * the pipeline. A missing or unreadable package.json answers "no", which is the
132
+ * safe way round: the container variant works either way, while the npm variant
133
+ * assumes an install that may not happen.
134
+ */
135
+ function installsPackage(root) {
136
+ try {
137
+ const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf-8'));
138
+ return Boolean(pkg.dependencies?.['pr-checkmate'] ?? pkg.devDependencies?.['pr-checkmate']);
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ /**
145
+ * The image tag written into the generated workflow: our own major.
146
+ *
147
+ * Pinned to a major rather than to `latest`, so patches and features arrive on
148
+ * their own while a change that can start failing builds does not. Pinned to a
149
+ * major rather than an exact version, so a client is not left on a release with
150
+ * a known problem because nobody remembered to bump a YAML file.
151
+ */
152
+ function imageTag() {
153
+ try {
154
+ const manifest = path_1.default.resolve(__dirname, '..', 'package.json');
155
+ const version = JSON.parse(fs_1.default.readFileSync(manifest, 'utf-8')).version;
156
+ return version.split('.')[0] || 'latest';
157
+ }
158
+ catch {
159
+ return 'latest';
160
+ }
231
161
  }
162
+ /** Where the advisory banner is spliced in — the step that runs the checks. */
163
+ const RUN_STEP_ANCHOR = ' - name: Run PR CheckMate';
232
164
  /** Create a tsconfig.json scaffold if the consumer repo lacks one. */
233
165
  function setupTsConfig(root, sourcePath) {
234
166
  const tsconfigPath = path_1.default.join(root, 'tsconfig.json');
@@ -254,107 +186,196 @@ function setupTsConfig(root, sourcePath) {
254
186
  fs_1.default.writeFileSync(tsconfigPath, JSON.stringify(newTsConfig, null, 2));
255
187
  utils_1.logger.info('[init]: ✅ Created tsconfig.json using your sourcePath');
256
188
  }
189
+ /**
190
+ * The file `init` writes for a new project — the client's decisions, nothing else.
191
+ *
192
+ * Every other setting is deliberately absent. The config is deep-merged over the
193
+ * built-in defaults when it is loaded, so a key that repeats a default changes
194
+ * nothing: it only lengthens the file and freezes today's value in someone
195
+ * else's repository. Three hundred generated lines also read as a wall of
196
+ * settings to maintain, when there is nothing to maintain.
197
+ *
198
+ * Discoverability moves to two places that cannot go stale — `$schema`, which
199
+ * makes an editor autocomplete every option with its description, and the
200
+ * annotated reference shipped inside the package.
201
+ */
202
+ /** The one-line answer to "how do I change this" — see {@link buildInitialConfig}. */
203
+ const REPORTING_HINT = [
204
+ 'Results post to the PR/MR automatically.',
205
+ 'GitHub: keep `permissions: pull-requests: write` AND pass the token to the step',
206
+ '(`env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`) — Actions does not provide it',
207
+ 'on its own, and without it the comment is silently skipped. The generated',
208
+ 'workflow already does both.',
209
+ 'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add',
210
+ 'it as CI/CD variable GITLAB_TOKEN.',
211
+ 'Bitbucket: create a token (Repo → Settings → Access tokens, scope',
212
+ 'pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
213
+ 'The token lives in YOUR CI variables, never in this file. Without it checks',
214
+ 'still run and print to the log — only the comment is skipped.',
215
+ ].join(' ');
216
+ /**
217
+ * Every check that fails the run out of the box, written into the generated
218
+ * config at its real default.
219
+ *
220
+ * Not a copy of a default for its own sake — this one is the gate's teeth, and
221
+ * the client is entitled to see which checks can stop their merge without
222
+ * reading our source or discovering it from a red pipeline. Written as `"error"`
223
+ * so the edit is in place: change the value, or delete the line to inherit
224
+ * whatever we ship next.
225
+ *
226
+ * Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
227
+ * block too, but only for rules the client writes themselves, so listing them
228
+ * would suggest a gate that is inert until they act. And `Dockerfile Security`
229
+ * looks like it belongs here and does not: every one of its patterns is rated
230
+ * `warn`, so it never fails on its own.
231
+ *
232
+ * Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
233
+ * every name here to the registry, which catches a rename but not a change of
234
+ * heart about what should block.
235
+ */
236
+ exports.BLOCKING_BY_DEFAULT = [
237
+ 'Security Scan',
238
+ 'Sensitive Files',
239
+ 'Diff Security',
240
+ 'Workflow Security',
241
+ 'Merge Conflict',
242
+ 'NPM Audit',
243
+ 'License Check',
244
+ 'Dependencies',
245
+ 'TypeScript',
246
+ 'ESLint',
247
+ 'YAML Lint',
248
+ ];
249
+ const HOW_TO = [
250
+ 'Only what differs from the defaults belongs here — everything else is implicit',
251
+ 'and stays current on upgrade, so this file stays short.',
252
+ 'Turn a check off: "severity": { "Spellcheck": "off" }',
253
+ 'Make one advisory: "severity": { "Sensitive Files": "warn" }',
254
+ 'Make everything advisory while adopting: "severityDefaults": { "failOn": "warn" }',
255
+ 'Skip a file or folder: "excludePaths": ["src/generated/**", "vendor/legacy.ts"]',
256
+ 'Check names are the ones printed in the report.',
257
+ `Every option, annotated, with its default: ${config_1.FULL_CONFIG_URL}`,
258
+ `Docs: ${config_1.DOCS_URL}`,
259
+ ].join(' ');
260
+ function buildInitialConfig(sourcePath, languages) {
261
+ return {
262
+ $schema: SCHEMA_PATH,
263
+ '// how-to': HOW_TO,
264
+ commands: config_1.DEFAULT_COMMANDS,
265
+ '// reporting': REPORTING_HINT,
266
+ sourcePath,
267
+ // Which checks can stop a merge — named, but NOT written as values.
268
+ //
269
+ // Writing `"Workflow Security": "error"` reads like a statement of the
270
+ // default and is not one: the map promotes as well as demotes, so an entry
271
+ // of "error" turns that check's ADVISORY findings into failures too. On a
272
+ // real client repository that flipped 54 warnings into a red build. The
273
+ // names belong in a comment; the map stays empty until the client means
274
+ // something by it.
275
+ '// severity': [
276
+ `These checks fail a run by default: ${exports.BLOCKING_BY_DEFAULT.join(', ')}.`,
277
+ 'To stand one down, add it below with "warn" (stays in the report, stops',
278
+ 'blocking) or "off" (does not run at all).',
279
+ 'Adding a check with "error" is not a restatement of the default — it also',
280
+ "PROMOTES that check's advisory findings to failures, which is stricter.",
281
+ ].join(' '),
282
+ severity: {},
283
+ // The knobs people reach for first.
284
+ //
285
+ // Two shapes appear here, and the difference is not cosmetic. A list whose
286
+ // values are APPENDED to the defaults is seeded empty: empty changes nothing,
287
+ // so it cannot freeze today's default in someone else's repository. A list
288
+ // that REPLACES its default is written out in full instead — seeded empty it
289
+ // would silently switch the default off, and left out entirely it would be
290
+ // invisible, which is how a repository ends up spellchecking its own
291
+ // CHANGELOG and reporting every test fixture as duplication.
292
+ //
293
+ // Written-out lists are a deliberate trade: they stop tracking the package,
294
+ // so a later release that adds a directory to the defaults will not reach a
295
+ // config that already spells the list out. That is the price of being able
296
+ // to see and edit the value, and it is the one people asked for.
297
+ excludePaths: [],
298
+ // REPLACES the default. Copied out so the list is editable in place —
299
+ // note that ESLint does not read it; ESLint honours `eslint.ignorePatterns`.
300
+ ignoreDirs: [...config_1.IGNORED_DIRS],
301
+ // Spelling: `words` and `ignorePaths` are both APPENDED to the bundled lists,
302
+ // so both stay empty. What the bundled list already skips is spelled out in
303
+ // the sibling comment rather than duplicated here.
304
+ '// cspell': `Already skipped by default: ${config_1.GENERATED_DOC_FILES.join(', ')}, test and mock paths, lockfiles, minified bundles, and every directory in "ignoreDirs". Anything listed below is ADDED to that.`,
305
+ cspell: { words: [], ignorePaths: [] },
306
+ // `max-len` is written out at its default rather than left implicit: it is
307
+ // the rule people hit first and tune first, and a line limit is a house
308
+ // style, not a defect — arguing with it through documentation wastes
309
+ // everyone's time when the number could just be sitting here, editable.
310
+ eslint: { rules: { 'max-len': ['warn', { code: 300 }] }, ignorePatterns: [] },
311
+ markdownlint: { ignores: [] },
312
+ dependency: { ignore: [] },
313
+ circularDeps: { ignore: [] },
314
+ yamlLint: { ignore: [] },
315
+ // The three lists that REPLACE their defaults, written out. Each one exists
316
+ // because test code trips a check that production code does not: suites
317
+ // repeat their setup, the runner is the only consumer of what they export,
318
+ // and a security test's whole job may be to call the dangerous function.
319
+ // Delete an entry to have that check start reporting on tests again.
320
+ duplicate: { ignore: [...config_1.DUPLICATE_IGNORE_DEFAULTS] },
321
+ deadCode: { ignoreFiles: [...config_1.DEAD_CODE_IGNORE_DEFAULTS] },
322
+ diffSecurity: { ignorePaths: [...config_1.DIFF_SECURITY_IGNORE_DEFAULTS] },
323
+ documentation: config_1.documentation,
324
+ languages,
325
+ };
326
+ }
257
327
  /** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
258
328
  async function createConfig(root) {
259
329
  const sourcePath = await promptSourcePath();
330
+ const gateMode = await promptGateMode();
260
331
  const languages = await (0, utils_1.detectLanguages)(root);
261
332
  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));
333
+ const config = buildInitialConfig(sourcePath, languages);
334
+ fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
287
335
  utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
288
336
  utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
289
337
  and cspell words in pr-checkmate.json`);
290
- setupWorkflow(root);
338
+ setupWorkflow(root, gateMode);
291
339
  setupTsConfig(root, sourcePath);
292
340
  }
293
- function isPlainObject(value) {
294
- return typeof value === 'object' && value !== null && !Array.isArray(value);
295
- }
296
341
  /**
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.
342
+ * Re-run setup: refresh the detected languages and leave everything else alone.
299
343
  *
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.
344
+ * It used to merge current defaults into the client's file, which is why the
345
+ * update notice insisted `init` must not be skipped after an upgrade. Defaults
346
+ * now apply when the config is loaded, so there is nothing to top up.
305
347
  */
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
348
  async function updateConfig(root) {
341
349
  utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
342
350
  try {
343
351
  const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
344
352
  const json = JSON.parse(raw);
345
- const languages = await (0, utils_1.detectLanguages)(root);
353
+ // Re-detection has to obey the config it is about to rewrite: a client who
354
+ // tuned `detect` (or `ignoreDirs`) precisely because detection was wrong
355
+ // would otherwise get the wrong answer written back over the right one on
356
+ // the next `init`.
357
+ const languages = await (0, utils_1.detectLanguages)(root, json.ignoreDirs, json.detect);
346
358
  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.
359
+ // The client's file is left exactly as they wrote it. Defaults are no longer
360
+ // copied into it, because they never needed to be: they are merged in when
361
+ // the config is loaded, so a new default reaches every repository on upgrade
362
+ // without anyone editing anything. Writing them here only grew the file and
363
+ // made `init` a step nobody could safely skip after a release.
364
+ //
365
+ // `languages` is still refreshed — it is a fact about the repository, not a
366
+ // preference. `$schema` and the how-to hint are added when missing, because
367
+ // an existing config is exactly the case where nobody has seen them: the
368
+ // people who most need to know how to switch a check off are the ones whose
369
+ // file predates the advice.
350
370
  const updated = {
351
- ...mergeWithDefaults(DEFAULT_CONFIG, json),
352
- // Refresh detected languages on every init so new source dirs are picked up
371
+ $schema: json.$schema ?? SCHEMA_PATH,
372
+ '// how-to': json['// how-to'] ?? HOW_TO,
373
+ ...json,
353
374
  languages,
354
375
  };
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!');
376
+ fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
377
+ utils_1.logger.info('[init]: 🔧 Refreshed the detected languages in pr-checkmate.json.');
378
+ utils_1.logger.info(`[init]: 💡 Everything else is left as you wrote it — defaults apply on their own. Options: ${config_1.DOCS_URL}`);
358
379
  }
359
380
  catch (e) {
360
381
  utils_1.logger.error(`[init]: ❌ Failed to update pr-checkmate.json: ${e}`);