pr-checkmate 1.23.20 → 1.23.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -56,8 +56,51 @@ const COMMAND_TO_CHECK = {
56
56
  };
57
57
  /** Commands that run the whole pipeline (or no pipeline at all) and take no siblings. */
58
58
  const STANDALONE_COMMANDS = ['init', 'all', 'precommit'];
59
- const args = process.argv.slice(2);
59
+ /** Flags accepted anywhere on the command line, with what each one means. */
60
+ const FLAGS = {
61
+ '--full': 'review every tracked file instead of the diff (the default outside CI)',
62
+ };
63
+ const argv = process.argv.slice(2);
64
+ /**
65
+ * Flags are separated from positional arguments so they compose with every command
66
+ * — scope is orthogonal to which checks run, hence `pr-checkmate lint --full` as
67
+ * well as `pr-checkmate all --full`.
68
+ */
69
+ const flags = argv.filter(arg => arg.startsWith('-'));
70
+ const args = argv.filter(arg => !arg.startsWith('-'));
60
71
  const cmd = args[0];
72
+ /**
73
+ * An unrecognised flag aborts the run. Ignoring it would report success for a scope
74
+ * the user did not ask for: a misspelling of `--full` must not quietly review the
75
+ * diff and print a green summary.
76
+ */
77
+ function rejectUnknownFlags() {
78
+ const unknown = flags.filter(flag => !FLAGS[flag]);
79
+ if (unknown.length === 0)
80
+ return;
81
+ const known = Object.entries(FLAGS)
82
+ .map(([flag, help]) => `${flag} (${help})`)
83
+ .join(', ');
84
+ utils_1.logger.error(`[cli]: Unknown flag(s) "${unknown.join('", "')}". Available: ${known}`);
85
+ process.exit(1);
86
+ }
87
+ /**
88
+ * `--full` and `precommit` ask for different things — every tracked file versus the
89
+ * staged changes only. `planDiffRange` resolves the conflict in favour of `--full`,
90
+ * which would silently turn a pre-commit hook into a whole-repo scan, so the
91
+ * contradiction is refused here instead.
92
+ */
93
+ function rejectConflictingScope() {
94
+ if (cmd === 'precommit' && flags.includes('--full')) {
95
+ utils_1.logger.error('[cli]: "precommit --full" is contradictory — precommit reviews the staged changes. Run "pr-checkmate all --full" for a whole-repo review.');
96
+ process.exit(1);
97
+ }
98
+ if (cmd === 'init' && flags.length > 0) {
99
+ utils_1.logger.error(`[cli]: "init" takes no flags, got: ${flags.join(', ')}.`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+ const full = flags.includes('--full');
61
104
  /**
62
105
  * Reject extra arguments after a standalone command. Silently ignoring them would
63
106
  * report success for work that never ran — `pr-checkmate init lint` must not look
@@ -75,6 +118,8 @@ function rejectExtraArgs() {
75
118
  }
76
119
  void (async () => {
77
120
  try {
121
+ rejectUnknownFlags();
122
+ rejectConflictingScope();
78
123
  rejectExtraArgs();
79
124
  if (cmd === 'init') {
80
125
  await (0, init_1.init)();
@@ -86,7 +131,7 @@ void (async () => {
86
131
  // Fetch the update notice concurrently (best-effort, never blocks/affects
87
132
  // the exit code), but print it LAST — after the summary — so it's noticeable.
88
133
  const noticePromise = (0, utils_1.getUpdateNotice)();
89
- const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true });
134
+ const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true, full });
90
135
  const notice = await noticePromise.catch(() => null);
91
136
  if (notice) {
92
137
  utils_1.logger.info('');
@@ -109,7 +154,7 @@ void (async () => {
109
154
  process.exit(1);
110
155
  }
111
156
  const checkNames = [...new Set(args.map(arg => COMMAND_TO_CHECK[arg]))];
112
- const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true });
157
+ const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true, full });
113
158
  process.exit(report.ok ? 0 : 1);
114
159
  }
115
160
  catch (err) {
@@ -55,6 +55,7 @@ export declare const SOURCE_EXTENSIONS: {
55
55
  export declare const DEFAULT_COMMANDS: {
56
56
  'npx pr-checkmate init': string;
57
57
  'npx pr-checkmate all': string;
58
+ 'npx pr-checkmate all --full': string;
58
59
  'npx pr-checkmate lint': string;
59
60
  'npx pr-checkmate prettier': string;
60
61
  'npx pr-checkmate deps': string;
@@ -181,7 +181,8 @@ exports.SOURCE_EXTENSIONS = {
181
181
  };
182
182
  exports.DEFAULT_COMMANDS = {
183
183
  'npx pr-checkmate init': 'Update pr-checkmate.json',
184
- 'npx pr-checkmate all': 'Run all checks',
184
+ 'npx pr-checkmate all': 'Run all checks (the PR diff in CI, every tracked file locally)',
185
+ 'npx pr-checkmate all --full': 'Run all checks over every tracked file, CI included',
185
186
  'npx pr-checkmate lint': 'Lint code using ESLint',
186
187
  'npx pr-checkmate prettier': 'Format code using Prettier',
187
188
  'npx pr-checkmate deps': 'Check project dependencies',
@@ -57,6 +57,13 @@ exports.lintCheck = {
57
57
  // parser, breaking TypeScript linting. Only pass it on the legacy path.
58
58
  if (version < 9)
59
59
  args.push('--ext', '.ts,.tsx,.js,.jsx');
60
+ // Files are named explicitly, so any of them that the config ignores produces a
61
+ // "File ignored because of a matching ignore pattern" warning. That is a fact
62
+ // about the config, not about the client's code, and a whole-repo run surfaces
63
+ // one per ignored-but-tracked file (`.eslintrc.js`, generated sources). The flag
64
+ // suppresses the warning only — nothing changes about which files are linted.
65
+ if (version >= 9)
66
+ args.push('--no-warn-ignored');
60
67
  // Only auto-fix when the caller allows file mutations.
61
68
  if (ctx.write)
62
69
  args.push('--fix');
@@ -63,9 +63,20 @@ exports.commitlintCheck = {
63
63
  const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
64
64
  const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
65
65
  let tempConfigFile = null;
66
- const from = ctx.baseSha ?? 'HEAD~1';
67
- const to = ctx.headSha ?? 'HEAD';
68
- const args = ['--from', from, '--to', to, '--verbose'];
66
+ // With no diff range (a full-repo review, or `--full`) the check still has a
67
+ // meaningful subject: the newest commit message. `HEAD~1` cannot be assumed to
68
+ // exist, though — it is absent on a repository's first commit and in a shallow
69
+ // clone (`actions/checkout` fetches depth 1 by default). Passing it blind made
70
+ // commitlint die with an unhandled "ambiguous argument 'HEAD~1..HEAD'" stack
71
+ // trace in the client's log, and the check then reported "commit message
72
+ // violations found" — a verdict about code quality for what was a crash.
73
+ // `--last` lints exactly the newest commit and needs no ancestor.
74
+ const range = ctx.baseSha
75
+ ? ['--from', ctx.baseSha, '--to', ctx.headSha ?? 'HEAD']
76
+ : (await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD~1'))
77
+ ? ['--from', 'HEAD~1', '--to', ctx.headSha ?? 'HEAD']
78
+ : ['--last'];
79
+ const args = [...range, '--verbose'];
69
80
  try {
70
81
  if (hasLocal) {
71
82
  utils_1.logger.info('[Commitlint]: Using local config');
@@ -1,4 +1,11 @@
1
1
  import { Check } from '../../types';
2
+ /**
3
+ * True when a parser problem describes something actually wrong with the file —
4
+ * a syntax error, a duplicate key, a tab used for indentation.
5
+ */
6
+ export declare function isFileDefect(problem: {
7
+ code?: string;
8
+ }): boolean;
2
9
  /**
3
10
  * Bundled YAML linter (the `yaml` package — no runner dependency, always
4
11
  * available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.yamlLintCheck = void 0;
7
+ exports.isFileDefect = isFileDefect;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const yaml_1 = require("yaml");
@@ -15,6 +16,28 @@ const findings_1 = require("../findings");
15
16
  function shortMessage(message) {
16
17
  return message.split('\n')[0].trim();
17
18
  }
19
+ /**
20
+ * Parser problems that are not defects in the file.
21
+ *
22
+ * `TAG_RESOLVE_FAILED` means the parser met an application-specific tag it has no
23
+ * schema for. YAML is explicitly extensible there, so the document is valid and the
24
+ * parser simply is not the application that defines the tag: `!Ref`, `!Sub`,
25
+ * `!GetAtt`, `!Equals` in CloudFormation and SAM templates, `!include` in Home
26
+ * Assistant, `!vault` in Ansible. Reporting those calls a whole ecosystem's valid
27
+ * files broken — one CloudFormation template produced **557 blocking findings** for
28
+ * a client, none of which was a real problem.
29
+ *
30
+ * Matched on the parser's `code`, not on message text, so a wording change upstream
31
+ * cannot silently re-enable it.
32
+ */
33
+ const NON_DEFECT_CODES = new Set(['TAG_RESOLVE_FAILED']);
34
+ /**
35
+ * True when a parser problem describes something actually wrong with the file —
36
+ * a syntax error, a duplicate key, a tab used for indentation.
37
+ */
38
+ function isFileDefect(problem) {
39
+ return problem.code === undefined || !NON_DEFECT_CODES.has(problem.code);
40
+ }
18
41
  /**
19
42
  * Bundled YAML linter (the `yaml` package — no runner dependency, always
20
43
  * available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
@@ -60,7 +83,7 @@ exports.yamlLintCheck = {
60
83
  // so a single malformed file cannot abort the whole check.
61
84
  const docs = (0, yaml_1.parseAllDocuments)(source, { prettyErrors: true });
62
85
  for (const doc of docs) {
63
- for (const problem of [...doc.errors, ...doc.warnings]) {
86
+ for (const problem of [...doc.errors, ...doc.warnings].filter(isFileDefect)) {
64
87
  const pos = problem.linePos?.[0];
65
88
  issues.push({
66
89
  file,
@@ -39,10 +39,30 @@ exports.securityCheck = {
39
39
  // times out in locked-down/offline CI (the scanner already falls back to
40
40
  // this exact version). Overridable via `security.gitleaks.version`.
41
41
  const gitleaksVersion = ctx.config.security?.gitleaks?.version ?? '8.30.0';
42
- const args = ['--diff-mode', 'ci', '--gitleaks-version', gitleaksVersion];
42
+ // The scan scope has to follow the run's scope. `--diff-mode ci` REQUIRES
43
+ // HEAD_SHA and aborts without it, so on a full-scan run (no diff range — the
44
+ // local default) it exited 2 and the secret scan silently became a warning
45
+ // about the scanner instead of a review of the code. `history` is the only
46
+ // whole-repository mode the scanner offers; it walks the full commit history,
47
+ // which took ~1s over 700 commits here but is worth bounding on a large
48
+ // monorepo via `security.gitleaks.depth`.
49
+ const isDelta = Boolean(ctx.baseSha);
50
+ const depth = ctx.config.security?.gitleaks?.depth;
51
+ // History mode needs a repository to walk. Without one the scanner exits 2
52
+ // after printing `fatal: not a git repository` and "An unexpected error
53
+ // occurred" — alarming output about the tool, for a directory it was never
54
+ // able to review. Every other check skips when git is unusable; so does this.
55
+ if (!isDelta && !(await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD'))) {
56
+ return (0, types_1.skip)('git history unavailable');
57
+ }
58
+ const args = isDelta
59
+ ? ['--diff-mode', 'ci']
60
+ : ['--diff-mode', 'history', ...(depth ? ['--depth', String(depth)] : [])];
61
+ args.push('--gitleaks-version', gitleaksVersion);
43
62
  if (!hasClientConfig) {
44
63
  args.push('--config', (0, config_1.getPackageConfigPath)('gitleaks.toml'));
45
64
  }
65
+ utils_1.logger.info(`[Security Scan]: scanning ${isDelta ? 'the diff range' : `full history${depth ? ` (last ${depth} commits)` : ''}`}`);
46
66
  const env = {
47
67
  ...process.env,
48
68
  ...(ctx.headSha ? { HEAD_SHA: ctx.headSha } : {}),
@@ -1,6 +1,53 @@
1
1
  import type { CheckContext, RunOptions } from './types';
2
+ /**
3
+ * What a run should look at, before any git call is made. Separated from
4
+ * {@link resolveDiffRange} so the precedence rules are a pure function of the
5
+ * options and the environment — the part worth pinning with tests.
6
+ *
7
+ * - `full` — every tracked file. Checks fall back to `git ls-files` when the range
8
+ * is undefined, so this is expressed by resolving to no SHAs at all.
9
+ * - `staged` — index vs HEAD (pre-commit).
10
+ * - `explicit` — SHAs the caller supplied.
11
+ * - `github-event` — base SHA from the pull_request event payload.
12
+ * - `ref` — diff against a named ref (a PR's destination branch, or `HEAD^`).
13
+ */
14
+ type RangePlan = {
15
+ kind: 'full';
16
+ reason: string;
17
+ } | {
18
+ kind: 'staged';
19
+ } | {
20
+ kind: 'explicit';
21
+ baseSha?: string;
22
+ headSha?: string;
23
+ } | {
24
+ kind: 'github-event';
25
+ } | {
26
+ kind: 'ref';
27
+ ref: string;
28
+ reason: string;
29
+ };
30
+ /**
31
+ * Decide what to look at. Order matters and is the contract:
32
+ *
33
+ * 1. `full` wins over everything — an explicit "check all of it" must not be
34
+ * quietly narrowed by a CI variable that happens to be set.
35
+ * 2. `staged`, then caller-supplied SHAs.
36
+ * 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
37
+ * branch) — a gate exists to judge the diff.
38
+ * 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
39
+ * and delta is why CI runs are fast.
40
+ * 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
41
+ * fallback here, which meant a local `pr-checkmate all` judged only the last
42
+ * commit and could report a clean project with a `debugger` committed earlier.
43
+ *
44
+ * @param opts Run options as passed to `runChecks`.
45
+ * @param env Environment to read CI markers from. Defaults to the current process.
46
+ */
47
+ export declare function planDiffRange(opts: RunOptions, env?: NodeJS.ProcessEnv): RangePlan;
2
48
  /**
3
49
  * Assemble the shared CheckContext once per run. Language detection is skipped
4
50
  * when the caller supplies `languages` (e.g. PRValidator passes its ProjectType).
5
51
  */
6
52
  export declare function buildContext(opts?: RunOptions): Promise<CheckContext>;
53
+ export {};
@@ -3,6 +3,7 @@ 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.planDiffRange = planDiffRange;
6
7
  exports.buildContext = buildContext;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const loader_1 = require("../config/loader");
@@ -10,41 +11,71 @@ const utils_1 = require("../utils");
10
11
  const detect_language_1 = require("../utils/detect-language");
11
12
  const execa_1 = require("../utils/execa");
12
13
  /**
13
- * Resolve the diff range for delta-aware checks.
14
- * Prefers explicit opts, then GitHub PR event, then Bitbucket PR env, then git HEAD^.
15
- * Returns undefined SHAs when nothing usable is found → checks fall back to full scan.
14
+ * Decide what to look at. Order matters and is the contract:
15
+ *
16
+ * 1. `full` wins over everything — an explicit "check all of it" must not be
17
+ * quietly narrowed by a CI variable that happens to be set.
18
+ * 2. `staged`, then caller-supplied SHAs.
19
+ * 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
20
+ * branch) — a gate exists to judge the diff.
21
+ * 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
22
+ * and delta is why CI runs are fast.
23
+ * 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
24
+ * fallback here, which meant a local `pr-checkmate all` judged only the last
25
+ * commit and could report a clean project with a `debugger` committed earlier.
26
+ *
27
+ * @param opts Run options as passed to `runChecks`.
28
+ * @param env Environment to read CI markers from. Defaults to the current process.
16
29
  */
30
+ function planDiffRange(opts, env = process.env) {
31
+ if (opts.full)
32
+ return { kind: 'full', reason: 'requested' };
33
+ if (opts.staged)
34
+ return { kind: 'staged' };
35
+ if (opts.baseSha || opts.headSha) {
36
+ return { kind: 'explicit', baseSha: opts.baseSha, headSha: opts.headSha };
37
+ }
38
+ if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_EVENT_PATH) {
39
+ return { kind: 'github-event' };
40
+ }
41
+ if (env.BITBUCKET_PR_DESTINATION_BRANCH) {
42
+ return {
43
+ kind: 'ref',
44
+ ref: `origin/${env.BITBUCKET_PR_DESTINATION_BRANCH}`,
45
+ reason: 'PR build',
46
+ };
47
+ }
48
+ if ((0, utils_1.isCiEnvironment)(env))
49
+ return { kind: 'ref', ref: 'HEAD^', reason: 'CI build, no PR context' };
50
+ return { kind: 'full', reason: 'no CI detected' };
51
+ }
17
52
  /**
18
- * Resolve a git ref to a concrete commit SHA, or undefined when it can't be
19
- * resolved in this clone. `--verify --quiet` emits nothing (instead of echoing
20
- * the literal ref) and exits non-zero for a missing ref — e.g. `HEAD^` on a
21
- * single-commit repo, or a base branch not fetched in a shallow CI clone. That
22
- * way an unresolvable base yields `undefined` and callers fall back to a full
23
- * scan, rather than diffing against a garbage ref and silently checking nothing.
53
+ * Carry out a {@link RangePlan}. Every path that cannot produce a usable base
54
+ * commit returns no SHAs, which the checks read as "scan everything" — degrading
55
+ * to a full scan is always safer than diffing against a ref that isn't there and
56
+ * silently reviewing nothing.
57
+ *
58
+ * @returns The resolved range, plus a label for the log line.
24
59
  */
25
- async function resolveRef(cwd, ref) {
26
- const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
27
- cwd,
28
- reject: false,
29
- });
30
- return stdout.trim() || undefined;
31
- }
32
60
  async function resolveDiffRange(cwd, opts) {
61
+ const plan = planDiffRange(opts, process.env);
62
+ if (plan.kind === 'full')
63
+ return { scope: `full (${plan.reason})` };
33
64
  // Pre-commit mode: diff HEAD against the current index. `git write-tree`
34
65
  // materialises the staged index as a real tree object, so every check that
35
66
  // diffs baseSha..headSha sees exactly the staged changes — no per-check code.
36
- if (opts.staged) {
67
+ if (plan.kind === 'staged') {
37
68
  try {
38
69
  const head = (await (0, execa_1.execa)('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
39
70
  const tree = (await (0, execa_1.execa)('git', ['write-tree'], { cwd })).stdout.trim();
40
- return { baseSha: head, headSha: tree };
71
+ return { baseSha: head, headSha: tree, scope: 'staged' };
41
72
  }
42
73
  catch {
43
- return {};
74
+ return { scope: 'full (no staged range)' };
44
75
  }
45
76
  }
46
- if (opts.baseSha || opts.headSha) {
47
- return { baseSha: opts.baseSha, headSha: opts.headSha };
77
+ if (plan.kind === 'explicit') {
78
+ return { baseSha: plan.baseSha, headSha: plan.headSha, scope: 'delta (explicit)' };
48
79
  }
49
80
  let headSha;
50
81
  try {
@@ -52,30 +83,32 @@ async function resolveDiffRange(cwd, opts) {
52
83
  headSha = stdout.trim();
53
84
  }
54
85
  catch {
55
- return {};
86
+ return { scope: 'full (no HEAD)' };
56
87
  }
57
88
  // GitHub Actions: the PR event payload carries the base branch SHA. Read it
58
89
  // directly so delta works without relying on any client-set variable
59
90
  // (GITHUB_EVENT_* are injected by the runner).
60
- if (process.env.GITHUB_EVENT_NAME === 'pull_request' && process.env.GITHUB_EVENT_PATH) {
91
+ if (plan.kind === 'github-event') {
61
92
  try {
62
93
  const event = JSON.parse(node_fs_1.default.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf-8'));
63
94
  const eventBase = event?.pull_request?.base?.sha;
64
95
  // Only use it if the commit is actually present in this clone; otherwise
65
96
  // fall through so a shallow clone degrades to a full scan, not 0 files.
66
- const baseSha = eventBase ? await resolveRef(cwd, eventBase) : undefined;
97
+ const baseSha = eventBase ? await (0, utils_1.resolveGitRef)(cwd, eventBase) : undefined;
67
98
  if (baseSha)
68
- return { baseSha, headSha };
99
+ return { baseSha, headSha, scope: 'delta (PR event)' };
69
100
  }
70
101
  catch {
71
- // Malformed/unreadable event — fall through to the git-based resolution.
102
+ // Malformed/unreadable event — fall through to a full scan below.
72
103
  }
104
+ return { headSha, scope: 'full (PR base unavailable)' };
73
105
  }
74
- // Bitbucket exposes the PR destination branch as an env var (also runner-set).
75
- const bitbucketTarget = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
76
- const ref = bitbucketTarget ? `origin/${bitbucketTarget}` : 'HEAD^';
77
- const baseSha = await resolveRef(cwd, ref);
78
- return { baseSha, headSha };
106
+ const baseSha = await (0, utils_1.resolveGitRef)(cwd, plan.ref);
107
+ return {
108
+ baseSha,
109
+ headSha,
110
+ scope: baseSha ? `delta (${plan.ref}, ${plan.reason})` : `full (${plan.ref} unresolvable)`,
111
+ };
79
112
  }
80
113
  /**
81
114
  * Assemble the shared CheckContext once per run. Language detection is skipped
@@ -84,9 +117,15 @@ async function resolveDiffRange(cwd, opts) {
84
117
  async function buildContext(opts = {}) {
85
118
  const cwd = opts.cwd ?? process.cwd();
86
119
  const config = opts.config ?? (0, loader_1.loadProjectConfig)();
87
- const languages = opts.languages ?? (await (0, detect_language_1.detectLanguages)(cwd));
88
- const { baseSha, headSha } = await resolveDiffRange(cwd, opts);
89
- utils_1.logger.info(`[buildContext]: cwd=${cwd} languages=[${languages.join(', ') || 'none'}] write=${opts.write ?? false}`);
120
+ // Detection reads the client's `ignoreDirs` from the config already loaded here,
121
+ // rather than calling getIgnoredDirs() — that one resolves against process.cwd(),
122
+ // which is not necessarily the `cwd` under review.
123
+ const languages = opts.languages ?? (await (0, detect_language_1.detectLanguages)(cwd, config.ignoreDirs));
124
+ const { baseSha, headSha, scope } = await resolveDiffRange(cwd, opts);
125
+ // `scope` is logged because it decides what the whole run looks at, and getting
126
+ // it wrong is invisible in the results: a run that reviewed one commit and one
127
+ // that reviewed the repository both just print passes.
128
+ utils_1.logger.info(`[buildContext]: cwd=${cwd} languages=[${languages.join(', ') || 'none'}] write=${opts.write ?? false} scope=${scope}`);
90
129
  return {
91
130
  cwd,
92
131
  languages,
@@ -27,7 +27,7 @@ function excludeIgnoredDirs(files, keepDirs = []) {
27
27
  ignored.delete(dir);
28
28
  if (ignored.size === 0)
29
29
  return files;
30
- return files.filter(f => !f.split('/').some(segment => ignored.has(segment)));
30
+ return files.filter(f => !(0, utils_1.isUnderIgnoredDir)(f, ignored));
31
31
  }
32
32
  /**
33
33
  * Lists the files a language check should operate on, matched by pathspec `globs`
@@ -90,6 +90,15 @@ export interface RunOptions {
90
90
  * (`git write-tree`), so every delta-aware check works unchanged.
91
91
  */
92
92
  staged?: boolean;
93
+ /**
94
+ * Review every tracked file instead of a diff (`--full` on the CLI, and the
95
+ * default when no CI environment is detected). Takes precedence over every other
96
+ * range source, including CI variables — see `planDiffRange`. The 16 checks that
97
+ * are about a diff by definition (PR Size, Diff Security, Merge Conflict, …) gate
98
+ * their own `applies` on `baseSha`, so they drop out of the report rather than
99
+ * misreporting.
100
+ */
101
+ full?: boolean;
93
102
  }
94
103
  export interface CheckReport {
95
104
  results: CheckResult[];
package/dist/init.js CHANGED
@@ -11,20 +11,6 @@ const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const config_1 = require("./config");
12
12
  const utils_1 = require("./utils");
13
13
  const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
14
- /**
15
- * Load the canonical default ESLint rules (the same set eslint.config.mjs
16
- * applies) so the generated pr-checkmate.json lists every rule for the user to
17
- * change or disable. Falls back to an empty object if the file is unavailable.
18
- */
19
- function loadDefaultEslintRules() {
20
- try {
21
- return JSON.parse(fs_1.default.readFileSync((0, config_1.getPackageConfigPath)('eslint-default-rules.json'), 'utf-8'));
22
- }
23
- catch (err) {
24
- utils_1.logger.warn(`[init]: ⚠️ Could not load default ESLint rules: ${err}`);
25
- return {};
26
- }
27
- }
28
14
  /**
29
15
  * Default configuration structure with customization options
30
16
  */
@@ -48,10 +34,22 @@ const DEFAULT_CONFIG = {
48
34
  // Directories excluded from every check (matched on any path segment). Edit
49
35
  // freely — add your own generated/vendored dirs or remove ones you want scanned.
50
36
  ignoreDirs: config_1.IGNORED_DIRS,
51
- // ESLint — the full default rule set is written out so every rule is visible
52
- // and editable. Change any value, or set a rule to "off" to disable it.
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.
53
51
  eslint: {
54
- rules: loadDefaultEslintRules(),
52
+ rules: {},
55
53
  ignorePatterns: [],
56
54
  },
57
55
  // Prettier customization
@@ -61,6 +61,7 @@ export interface PRCheckMateConfig {
61
61
  gitleaks?: {
62
62
  enabled?: boolean;
63
63
  version?: string;
64
+ depth?: number;
64
65
  };
65
66
  'npm-audit'?: {
66
67
  enabled?: boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * True when running inside a CI/CD build.
3
+ *
4
+ * Decides the default review scope: a build looks at the diff (that is the point
5
+ * of a PR gate), a human at a terminal looks at the whole project — the local
6
+ * `HEAD^` default used to check only the last commit, so a repo could report a
7
+ * clean run with a `debugger` sitting in it.
8
+ *
9
+ * @param env Environment to inspect. Defaults to the current process.
10
+ */
11
+ export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCiEnvironment = isCiEnvironment;
4
+ /**
5
+ * Environment variables that mean "this is an automated build". `CI` alone covers
6
+ * GitHub Actions, GitLab, Bitbucket, CircleCI, Buildkite, Travis and most others;
7
+ * the rest are providers that either predate the convention or set only their own
8
+ * marker (Jenkins, TeamCity, Azure Pipelines, AWS CodeBuild).
9
+ *
10
+ * This answers a different question from `detectPlatform()` in reporters.ts, which
11
+ * picks WHERE to post a PR comment and therefore only knows the three platforms we
12
+ * can comment on. A Jenkins build is CI but has nowhere to post.
13
+ */
14
+ const CI_ENV_VARS = [
15
+ 'CI',
16
+ 'GITHUB_ACTIONS',
17
+ 'GITLAB_CI',
18
+ 'BITBUCKET_BUILD_NUMBER',
19
+ 'JENKINS_URL',
20
+ 'TEAMCITY_VERSION',
21
+ 'TF_BUILD',
22
+ 'CIRCLECI',
23
+ 'BUILDKITE',
24
+ 'CODEBUILD_BUILD_ID',
25
+ ];
26
+ /** Values that are present but mean "no" — some tooling exports `CI=false`. */
27
+ const FALSY = new Set(['', '0', 'false', 'no', 'off']);
28
+ /**
29
+ * True when running inside a CI/CD build.
30
+ *
31
+ * Decides the default review scope: a build looks at the diff (that is the point
32
+ * of a PR gate), a human at a terminal looks at the whole project — the local
33
+ * `HEAD^` default used to check only the last commit, so a repo could report a
34
+ * clean run with a `debugger` sitting in it.
35
+ *
36
+ * @param env Environment to inspect. Defaults to the current process.
37
+ */
38
+ function isCiEnvironment(env = process.env) {
39
+ return CI_ENV_VARS.some(name => {
40
+ const value = env[name];
41
+ return value !== undefined && !FALSY.has(value.trim().toLowerCase());
42
+ });
43
+ }
@@ -4,7 +4,12 @@ export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift'
4
4
  * with tracked source-file counts (breadth). A language is reported when it has a
5
5
  * marker file OR at least one tracked source file.
6
6
  *
7
+ * @param root Repository root.
8
+ * @param ignoreDirs Directory names whose contents do not count as the repo's own
9
+ * code. Defaults to the curated list; `buildContext` passes the client's
10
+ * `ignoreDirs` so a repo that vendors dependencies is described by its own
11
+ * sources. Marker files are unaffected — they are only ever read from `root`.
7
12
  * @returns languages ordered by tracked file count (most-used first). `.h` headers
8
13
  * are intentionally excluded from the C++ count since they are ambiguous with C.
9
14
  */
10
- export declare function detectLanguages(root?: string): Promise<Language[]>;
15
+ export declare function detectLanguages(root?: string, ignoreDirs?: string[]): Promise<Language[]>;