@prodcycle/prodcycle 0.4.2 → 0.6.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.
package/dist/cli.js CHANGED
@@ -34,7 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.isCiEnvironment = isCiEnvironment;
37
38
  const commander_1 = require("commander");
39
+ const child_process_1 = require("child_process");
38
40
  const fs = __importStar(require("fs"));
39
41
  const path = __importStar(require("path"));
40
42
  const index_1 = require("./index");
@@ -43,6 +45,7 @@ const sarif_1 = require("./formatters/sarif");
43
45
  const prompt_1 = require("./formatters/prompt");
44
46
  const KNOWN_COMMANDS = new Set([
45
47
  'scan',
48
+ 'scans',
46
49
  'gate',
47
50
  'hook',
48
51
  'init',
@@ -110,36 +113,116 @@ program
110
113
  .name('prodcycle')
111
114
  .description('Multi-framework policy-as-code compliance scanner for infrastructure and application code.')
112
115
  .version(PKG_VERSION);
116
+ /**
117
+ * Detect CI environment via well-known env vars set by the major
118
+ * platforms. When CI is detected, default `--format` flips to `sarif`
119
+ * (so output drops straight into GitHub code scanning / GitLab security
120
+ * dashboards / etc. without extra configuration). Users can still
121
+ * override with `--format table|json|prompt`.
122
+ *
123
+ * The flip is opt-out (set `--format table` explicitly to keep the
124
+ * human-readable output in CI logs). Heuristic, not load-bearing — if
125
+ * we miss a CI platform here the user gets the same default they
126
+ * would have anyway (`table`), they just have to add `--format sarif`
127
+ * by hand.
128
+ */
129
+ function isCiEnvironment(env = process.env) {
130
+ // Generic `CI`: match any non-empty value. Most platforms set `CI=true`
131
+ // but some (Drone CI, Woodpecker CI, custom Jenkins pipelines) use
132
+ // `CI=1` or another truthy string. Specific platforms below cover the
133
+ // happy path; this is a defense-in-depth fallback so we don't miss
134
+ // edge-case environments.
135
+ return ((env['CI'] != null && env['CI'] !== '') ||
136
+ env['GITHUB_ACTIONS'] === 'true' ||
137
+ env['GITLAB_CI'] === 'true' ||
138
+ env['CIRCLECI'] === 'true' ||
139
+ env['JENKINS_URL'] != null ||
140
+ env['BUILDKITE'] === 'true' ||
141
+ env['TRAVIS'] === 'true' ||
142
+ env['BITBUCKET_BUILD_NUMBER'] != null);
143
+ }
113
144
  // ── scan ────────────────────────────────────────────────────────────────────
114
145
  program
115
146
  .command('scan [repo_path]')
116
147
  .description('Scan a repository for compliance violations')
117
- .option('--framework <ids>', 'Comma-separated framework IDs to evaluate', 'soc2')
118
- .option('--format <format>', 'Output format: json, sarif, table, prompt', 'table')
119
- .option('--severity-threshold <severity>', 'Minimum severity to include in report', 'low')
148
+ // Default frameworks: all three. The unique value of this scanner is
149
+ // cross-framework evaluation in one pass; defaulting to `soc2` only
150
+ // hid the HIPAA + NIST CSF capability from users who never thought
151
+ // to override the flag. If users need only one framework they can
152
+ // still pass `--framework soc2` explicitly.
153
+ .option('--framework <ids>', 'Comma-separated framework IDs to evaluate', 'soc2,hipaa,nist-csf')
154
+ // Default format: `table` for interactive use, but auto-flipped to
155
+ // `sarif` when CI is detected (see isCiEnvironment above) so GitHub
156
+ // Code Scanning / GitLab dashboards pick the report up without any
157
+ // extra wiring. The CLI's --format flag overrides the auto-flip.
158
+ .option('--format <format>', 'Output format: json, sarif, table, prompt (auto-defaults to sarif in CI)', undefined)
159
+ // Default severity-threshold: `medium`. `low` includes too many
160
+ // tier-3 advisory findings that are typically noise unless the user
161
+ // explicitly opts in; `high` would hide medium-severity weak-crypto
162
+ // findings that ARE actionable. Medium is the right balance for
163
+ // first-time users.
164
+ .option('--severity-threshold <severity>', 'Minimum severity to include in report', 'medium')
120
165
  .option('--fail-on <levels>', 'Comma-separated severities that cause non-zero exit', 'critical,high')
121
166
  .option('--include <patterns>', 'Comma-separated glob patterns to include')
122
167
  .option('--exclude <patterns>', 'Comma-separated glob patterns to exclude')
123
168
  .option('--output <file>', 'Write report to file')
124
169
  .option('--api-url <url>', 'Compliance API base URL (or PC_API_URL env)')
125
170
  .option('--api-key <key>', 'API key for compliance API (or PC_API_KEY env)')
171
+ .option('--async', 'Use the async-validate flow (server returns 202 immediately; CLI polls until COMPLETED). Useful for large scans where holding a connection isn’t practical.')
172
+ .option('--chunked', 'Force the chunked-session flow regardless of payload size. The default already auto-falls-back to chunked when /validate returns 413 with a chunked-endpoint suggestion.')
173
+ .option('--pr <range>', 'Scan only files changed in a git diff range (e.g. "origin/main..HEAD"). Cuts CI scan time on large repos by skipping unchanged files. Requires baseDir to be the git repo root.')
126
174
  .action(async (repoPath, opts) => {
127
175
  try {
128
176
  const target = repoPath ?? '.';
129
- const frameworks = parseList(opts.framework) ?? ['soc2'];
177
+ const frameworks = parseList(opts.framework) ?? ['soc2', 'hipaa', 'nist-csf'];
130
178
  const failOn = parseList(opts.failOn) ?? ['critical', 'high'];
131
- const format = (opts.format ?? 'table');
132
- console.error(`Scanning ${path.resolve(target)} for ${frameworks.join(', ')}...`);
179
+ // Format resolution:
180
+ // 1. explicit --format wins
181
+ // 2. otherwise: sarif when CI is detected, table when interactive
182
+ // SARIF in CI lets GitHub code scanning / GitLab security dashboards
183
+ // ingest results with zero extra configuration; table in interactive
184
+ // shells gives the human-readable summary first-time users expect.
185
+ const format = (opts.format ?? (isCiEnvironment() ? 'sarif' : 'table'));
186
+ // --async and --chunked are mutually exclusive; pick the explicit
187
+ // mode if either flag is set, otherwise let `scan()` pick (sync
188
+ // with auto-fallback to chunked on 413).
189
+ let mode = 'sync';
190
+ if (opts.async && opts.chunked) {
191
+ console.error('scan: --async and --chunked are mutually exclusive.');
192
+ process.exit(2);
193
+ }
194
+ if (opts.async)
195
+ mode = 'async';
196
+ else if (opts.chunked)
197
+ mode = 'chunked';
198
+ // --pr: restrict the scan to files in `git diff --name-only <range>`.
199
+ // Empty diff → exit 0 immediately (nothing to scan).
200
+ let include = parseList(opts.include);
201
+ if (opts.pr) {
202
+ const changed = computeChangedFiles(target, opts.pr);
203
+ if (changed.length === 0) {
204
+ console.error(`No files changed in range "${opts.pr}". Nothing to scan.`);
205
+ process.exit(0);
206
+ }
207
+ console.error(`--pr ${opts.pr}: restricting scan to ${changed.length} changed file(s).`);
208
+ // Use the diff list as exact-match include patterns. minimatch treats
209
+ // ordinary paths (no glob chars) as literal matches against relPath.
210
+ include = changed;
211
+ }
212
+ console.error(`Scanning ${path.resolve(target)} for ${frameworks.join(', ')}` +
213
+ (mode === 'sync' ? '' : ` (${mode} mode)`) +
214
+ '...');
133
215
  const response = await (0, index_1.scan)({
134
216
  repoPath: target,
135
217
  frameworks,
136
218
  options: {
137
219
  severityThreshold: opts.severityThreshold,
138
220
  failOn: failOn,
139
- include: parseList(opts.include),
221
+ include,
140
222
  exclude: parseList(opts.exclude),
141
223
  apiUrl: opts.apiUrl,
142
224
  apiKey: opts.apiKey,
225
+ config: { mode },
143
226
  },
144
227
  });
145
228
  writeOutput(renderReport(response, format), opts.output);
@@ -196,6 +279,54 @@ program
196
279
  process.exit(2);
197
280
  }
198
281
  });
282
+ // ── scans ───────────────────────────────────────────────────────────────────
283
+ // Fetch the current status / final result of any scan by ID. Useful with
284
+ // `--async` to resume a poll loop after a CI step boundary, or to inspect
285
+ // a chunked session that was abandoned mid-flight.
286
+ program
287
+ .command('scans <scanId>')
288
+ .description('Get the status + findings of a scan by ID')
289
+ .option('--format <format>', 'Output format: json, sarif, table, prompt', 'json')
290
+ .option('--output <file>', 'Write report to file')
291
+ .option('--api-url <url>', 'Compliance API base URL (or PC_API_URL env)')
292
+ .option('--api-key <key>', 'API key for compliance API (or PC_API_KEY env)')
293
+ .action(async (scanId, opts) => {
294
+ try {
295
+ const format = (opts.format ?? 'json');
296
+ const { ComplianceApiClient } = await Promise.resolve().then(() => __importStar(require('./api-client')));
297
+ const client = new ComplianceApiClient(opts.apiUrl, opts.apiKey);
298
+ const scan = await client.getScan(scanId);
299
+ // Same scannerError / exit-code-2 plumbing as scan() / gate(): a
300
+ // user retrieving a stored scan that failed for scanner reasons
301
+ // must see the same distinction (exit 2, scannerError surfaced).
302
+ const scannerError = scan.scannerError;
303
+ const exitCode = scannerError ? 2 : scan.passed ? 0 : 1;
304
+ const payload = {
305
+ scanId,
306
+ passed: scan.passed,
307
+ status: scan.status ?? 'COMPLETED',
308
+ findings: scan.findings ?? [],
309
+ summary: scan.summary,
310
+ exitCode,
311
+ ...(scannerError ? { scannerError } : {}),
312
+ };
313
+ // Use the same renderer as `scan` so format=table/sarif/prompt all work.
314
+ writeOutput(renderReport(payload, format), opts.output);
315
+ if (scannerError)
316
+ (0, index_1.emitScannerErrorWarning)(scannerError);
317
+ // Exit 2 if scan is still in progress — the CLI run shouldn't gate on
318
+ // an indeterminate result.
319
+ if (scan.status === 'IN_PROGRESS') {
320
+ console.error(`Scan ${scanId} is still IN_PROGRESS. Re-run the same command to keep polling, or use 'pc scan --async' to wait for completion.`);
321
+ process.exit(2);
322
+ }
323
+ process.exit(exitCode);
324
+ }
325
+ catch (error) {
326
+ console.error(`✗ Error: ${error.message}`);
327
+ process.exit(2);
328
+ }
329
+ });
199
330
  // ── hook ────────────────────────────────────────────────────────────────────
200
331
  program
201
332
  .command('hook')
@@ -290,18 +421,22 @@ async function collectHookFiles(filePath) {
290
421
  // ── init ────────────────────────────────────────────────────────────────────
291
422
  program
292
423
  .command('init')
293
- .description('Configure compliance hooks for coding agents')
424
+ .description('Configure compliance hooks for coding agents and/or CI workflows')
294
425
  .option('--agent <agents>', 'Comma-separated agents to configure (claude, cursor, codex, opencode, github-copilot, gemini-cli). Use "all" to configure every agent. Default: auto-detect.')
426
+ .option('--ci <providers>', 'Comma-separated CI providers to scaffold (github, gitlab, circleci). Use "all" for every provider. Opt-in only \u2014 never auto-detected.')
295
427
  .option('--force', 'Overwrite existing compliance hook entries')
296
428
  .option('--dir <path>', 'Project directory to configure', '.')
297
429
  .action((opts) => {
298
430
  try {
299
431
  const dir = path.resolve(opts.dir ?? '.');
300
432
  const agents = resolveAgents(opts.agent, dir);
301
- if (agents.length === 0) {
302
- console.error('init: no agents selected and none auto-detected. ' +
303
- 'Use --agent <name> to configure explicitly (claude, cursor, codex, ' +
304
- 'opencode, github-copilot, gemini-cli, or "all").');
433
+ const ciProviders = resolveCiProviders(opts.ci);
434
+ if (agents.length === 0 && ciProviders.length === 0) {
435
+ console.error('init: nothing to do. ' +
436
+ 'Use --agent <name> to configure a coding agent (claude, cursor, codex, ' +
437
+ 'opencode, github-copilot, gemini-cli, or "all"), and/or --ci <provider> ' +
438
+ 'to scaffold CI workflows (github, gitlab, circleci, or "all"). ' +
439
+ 'Without --agent the CLI also auto-detects agents already in use.');
305
440
  process.exit(2);
306
441
  }
307
442
  let anyFailed = false;
@@ -312,6 +447,12 @@ program
312
447
  if (result.status === 'failed')
313
448
  anyFailed = true;
314
449
  }
450
+ for (const provider of ciProviders) {
451
+ const result = configureCiProvider(provider, dir, !!opts.force);
452
+ process.stdout.write(result.message + '\n');
453
+ if (result.status === 'failed')
454
+ anyFailed = true;
455
+ }
315
456
  process.exit(anyFailed ? 1 : 0);
316
457
  }
317
458
  catch (error) {
@@ -547,6 +688,224 @@ function configureInstructionFile(agent, dir, relPath, force, writtenPaths) {
547
688
  function escapeRegExp(s) {
548
689
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
549
690
  }
691
+ const ALL_CI_PROVIDERS = ['github', 'gitlab', 'circleci'];
692
+ function isCiProvider(name) {
693
+ return ALL_CI_PROVIDERS.includes(name);
694
+ }
695
+ function resolveCiProviders(userChoice) {
696
+ if (!userChoice)
697
+ return [];
698
+ const list = parseList(userChoice) ?? [];
699
+ if (list.length === 1 && list[0] === 'all')
700
+ return ALL_CI_PROVIDERS.slice();
701
+ const valid = [];
702
+ for (const name of list) {
703
+ if (isCiProvider(name))
704
+ valid.push(name);
705
+ else
706
+ console.error(`init: unknown CI provider "${name}" — ignoring`);
707
+ }
708
+ return valid;
709
+ }
710
+ function configureCiProvider(provider, dir, force) {
711
+ switch (provider) {
712
+ case 'github':
713
+ return writeCiFile(provider, dir, path.join('.github', 'workflows', 'prodcycle.yml'), GITHUB_WORKFLOW, force);
714
+ case 'gitlab':
715
+ return writeCiFile(provider, dir, '.gitlab-ci.prodcycle.yml', GITLAB_WORKFLOW, force);
716
+ case 'circleci':
717
+ return writeCiFile(provider, dir, path.join('.circleci', 'prodcycle.yml'), CIRCLECI_WORKFLOW, force);
718
+ }
719
+ }
720
+ function writeCiFile(provider, dir, relPath, content, force) {
721
+ const fullPath = path.join(dir, relPath);
722
+ if (fs.existsSync(fullPath) && !force) {
723
+ return {
724
+ status: 'already',
725
+ message: `[ci:${provider}] ${relPath} already exists. Use --force to overwrite.`,
726
+ };
727
+ }
728
+ const parent = path.dirname(fullPath);
729
+ if (!fs.existsSync(parent))
730
+ fs.mkdirSync(parent, { recursive: true });
731
+ fs.writeFileSync(fullPath, content);
732
+ // GitHub uses the `prodcycle/actions/compliance` action, which reads
733
+ // its key from `secrets.PRODCYCLE_API_KEY`. GitLab and CircleCI invoke
734
+ // the CLI directly, which reads `PC_API_KEY` from the environment.
735
+ const followup = provider === 'gitlab'
736
+ ? `Include it from .gitlab-ci.yml: \`include: '${relPath}'\`. `
737
+ : provider === 'circleci'
738
+ ? `Reference it from .circleci/config.yml or merge the contents in. `
739
+ : '';
740
+ const secretName = provider === 'github' ? 'PRODCYCLE_API_KEY' : 'PC_API_KEY';
741
+ return {
742
+ status: 'installed',
743
+ message: `[ci:${provider}] wrote ${fullPath}. ` +
744
+ followup +
745
+ `Set ${secretName} as a secret/variable in your ${provider} project before the first run.`,
746
+ };
747
+ }
748
+ // GitHub: delegate to the dedicated `prodcycle/actions/compliance` GitHub
749
+ // Action rather than calling the CLI directly. The action handles diff vs
750
+ // full-repo scan automatically (PR events vs push events), posts inline
751
+ // annotations on the PR diff, and writes a summary comment — none of
752
+ // which the CLI's own SARIF output reproduces. See
753
+ // https://github.com/prodcycle/actions for the full input reference.
754
+ const GITHUB_WORKFLOW = `name: Prodcycle Compliance
755
+
756
+ on:
757
+ pull_request:
758
+ push:
759
+ # Update this list to match your repo's default branch (e.g. master,
760
+ # develop). GitHub Actions does not support a dynamic
761
+ # \$default-branch / \${{ github.event.repository.default_branch }}
762
+ # value here, so the branch name has to be literal.
763
+ branches: [main]
764
+
765
+ jobs:
766
+ scan:
767
+ runs-on: ubuntu-latest
768
+ permissions:
769
+ contents: read
770
+ pull-requests: write
771
+ steps:
772
+ - uses: actions/checkout@v4
773
+ with:
774
+ fetch-depth: 0
775
+ - uses: prodcycle/actions/compliance@v2
776
+ with:
777
+ api-key: \${{ secrets.PRODCYCLE_API_KEY }}
778
+ `;
779
+ const GITLAB_WORKFLOW = `# Prodcycle compliance scan. Include from your main .gitlab-ci.yml:
780
+ # include:
781
+ # - local: .gitlab-ci.prodcycle.yml
782
+ #
783
+ # Set PC_API_KEY as a CI/CD variable (Settings → CI/CD → Variables) before
784
+ # the first run. Mark it Masked + Protected.
785
+
786
+ prodcycle:
787
+ stage: test
788
+ image: node:22-alpine
789
+ variables:
790
+ GIT_DEPTH: "0"
791
+ before_script:
792
+ - apk add --no-cache git
793
+ script:
794
+ - |
795
+ if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
796
+ git fetch --no-tags origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
797
+ npx --yes prodcycle scan . \\
798
+ --pr "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME..HEAD" \\
799
+ --format sarif --output prodcycle.sarif
800
+ else
801
+ npx --yes prodcycle scan . --format sarif --output prodcycle.sarif
802
+ fi
803
+ artifacts:
804
+ when: always
805
+ paths:
806
+ - prodcycle.sarif
807
+ reports:
808
+ sast: prodcycle.sarif
809
+ rules:
810
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
811
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
812
+ `;
813
+ const CIRCLECI_WORKFLOW = `# Prodcycle compliance scan. To use this, either replace .circleci/config.yml
814
+ # or include it as a continuation/orb. Minimum example:
815
+ #
816
+ # version: 2.1
817
+ # workflows:
818
+ # compliance:
819
+ # jobs:
820
+ # - prodcycle-scan
821
+ #
822
+ # Set PC_API_KEY as a project environment variable in CircleCI before the
823
+ # first run.
824
+ #
825
+ # CircleCI does not expose the PR target branch as a built-in env var
826
+ # (\`CIRCLE_BASE_BRANCH\` does not exist; see
827
+ # https://circleci.com/docs/reference/variables/), so to scope PR scans
828
+ # to changed files only, set a project-level env var \`PRODCYCLE_PR_BASE\`
829
+ # to the branch your PRs target (e.g. \`main\`, \`develop\`). When unset,
830
+ # this template runs a full-repo scan.
831
+
832
+ version: 2.1
833
+ jobs:
834
+ prodcycle-scan:
835
+ docker:
836
+ - image: cimg/node:22.0
837
+ steps:
838
+ - checkout
839
+ - run:
840
+ name: Run Prodcycle compliance scan
841
+ command: |
842
+ if [ -n "\${PRODCYCLE_PR_BASE:-}" ] && [ -n "\${CIRCLE_PULL_REQUEST:-}" ]; then
843
+ git fetch --no-tags origin "$PRODCYCLE_PR_BASE"
844
+ npx --yes prodcycle scan . \\
845
+ --pr "origin/$PRODCYCLE_PR_BASE..HEAD" \\
846
+ --format sarif --output prodcycle.sarif
847
+ else
848
+ npx --yes prodcycle scan . --format sarif --output prodcycle.sarif
849
+ fi
850
+ # \`when: always\` so the SARIF artifact uploads even when the scan
851
+ # exits non-zero — compliance scanners exit 1 when findings exist,
852
+ # which is precisely the case where you want the report preserved.
853
+ - store_artifacts:
854
+ path: prodcycle.sarif
855
+ destination: prodcycle-sarif
856
+ when: always
857
+
858
+ workflows:
859
+ compliance:
860
+ jobs:
861
+ - prodcycle-scan
862
+ `;
863
+ /**
864
+ * Compute the list of files changed in a git diff range, relative to repo root.
865
+ * Filters to ACMR (Added/Copied/Modified/Renamed) so deleted files don't get
866
+ * scanned (they're not on disk anymore, and walk() would skip them anyway).
867
+ *
868
+ * Errors handled explicitly:
869
+ * - `ENOENT` (git not in PATH) → actionable "git executable not found"
870
+ * - `ETIMEDOUT` (git stalled — credential helper / auth prompt / etc.)
871
+ * → fail fast with a 30s timeout so CI jobs don't hang
872
+ * - non-zero exit → forward git's stderr so the user can see e.g. the
873
+ * "fatal: bad revision" message and fix the range argument
874
+ *
875
+ * Output paths are normalised to the platform separator: git emits POSIX
876
+ * forward-slashes always, but the file walker on Windows produces
877
+ * back-slashed `relPath` values. Without this conversion the literal
878
+ * minimatch comparison silently excludes every changed file on Windows.
879
+ */
880
+ const GIT_DIFF_TIMEOUT_MS = 30_000;
881
+ function computeChangedFiles(repoPath, range) {
882
+ let stdout;
883
+ try {
884
+ stdout = (0, child_process_1.execFileSync)('git', ['-C', repoPath, 'diff', '--name-only', '--diff-filter=ACMR', range], {
885
+ encoding: 'utf8',
886
+ stdio: ['ignore', 'pipe', 'pipe'],
887
+ timeout: GIT_DIFF_TIMEOUT_MS,
888
+ });
889
+ }
890
+ catch (e) {
891
+ if (e?.code === 'ENOENT') {
892
+ console.error('--pr: git executable not found in PATH');
893
+ process.exit(2);
894
+ }
895
+ if (e?.code === 'ETIMEDOUT' || e?.signal === 'SIGTERM') {
896
+ console.error(`--pr: git diff timed out after ${GIT_DIFF_TIMEOUT_MS}ms (range "${range}"). ` +
897
+ 'Check that the range does not require network access or credentials.');
898
+ process.exit(2);
899
+ }
900
+ const stderr = e?.stderr?.toString?.() ?? e?.message ?? 'unknown error';
901
+ console.error(`--pr: git diff failed for range "${range}": ${stderr.trim()}`);
902
+ process.exit(2);
903
+ }
904
+ return stdout
905
+ .split('\n')
906
+ .map((s) => s.trim().split('/').join(path.sep))
907
+ .filter(Boolean);
908
+ }
550
909
  function readStdin() {
551
910
  return new Promise((resolve, reject) => {
552
911
  if (process.stdin.isTTY) {
@@ -559,4 +918,10 @@ function readStdin() {
559
918
  process.stdin.on('error', reject);
560
919
  });
561
920
  }
562
- program.parse(injectScanDefault(process.argv));
921
+ // Only auto-parse when invoked as a script (i.e. via the `prodcycle`
922
+ // bin entry). Importing this module from tests must NOT execute the
923
+ // CLI — otherwise `node --test` triggers a real `program.parse` and
924
+ // fails before the test cases can run.
925
+ if (require.main === module) {
926
+ program.parse(injectScanDefault(process.argv));
927
+ }
package/dist/index.d.ts CHANGED
@@ -1,35 +1,75 @@
1
- import { ScanOptions, GateOptions } from './api-client';
1
+ import { ScanOptions, GateOptions, BackfillError } from './api-client';
2
2
  export * from './api-client';
3
3
  export * from './formatters/table';
4
4
  export * from './formatters/prompt';
5
5
  export * from './formatters/sarif';
6
6
  /**
7
- * Scan a repository by collecting files and sending them to the API
7
+ * Set when the server-side scanner threw and the API was configured to
8
+ * fail closed (the default). When this is present, callers MUST treat
9
+ * `passed: false` as "scanner unavailable — cannot certify compliance"
10
+ * rather than "code is dirty." Mirrors the API's `ScannerErrorInfo`
11
+ * shape; see `packages/compliance-code-scanner/api/src/domain/services/
12
+ * compliance-scan.service.ts` (`ScannerErrorInfo`) for the field
13
+ * contract.
14
+ *
15
+ * Without this surfaced to the CLI's --output JSON, a benchmark or CI
16
+ * report shows `passed: false, findings: []` and the user can't tell
17
+ * whether the code passed (no findings, all clean) from whether the
18
+ * scanner failed (no findings because nothing got evaluated).
19
+ */
20
+ export interface ScannerError {
21
+ code: 'SCANNER_GATE_THREW';
22
+ message: string;
23
+ errorClass?: string;
24
+ errorCode?: string;
25
+ }
26
+ interface ScanReturn {
27
+ scanId?: string;
28
+ passed: boolean;
29
+ exitCode: number;
30
+ findings: unknown[];
31
+ report: unknown;
32
+ summary: unknown;
33
+ scannerError?: ScannerError;
34
+ /**
35
+ * Set when `validateChunked`'s findings-backfill GET failed. The summary
36
+ * still reflects the real finding count, but the structured findings are
37
+ * unavailable in this response. Callers should retry via `prodcycle scans
38
+ * <scanId>` to recover them. SARIF/JSON consumers branch on this to flag
39
+ * the result as incomplete rather than mistakenly clean.
40
+ */
41
+ backfillError?: BackfillError;
42
+ }
43
+ /**
44
+ * Format and write the scanner-error warning to stderr. Centralized so the
45
+ * wording stays consistent across `scan()`, `gate()`, and the `scans <id>`
46
+ * CLI subcommand.
47
+ */
48
+ export declare function emitScannerErrorWarning(scannerError: ScannerError): void;
49
+ /**
50
+ * Scan a repository by collecting files and sending them to the API.
51
+ *
52
+ * Modes (selectable via `options.config`):
53
+ * - default: synchronous validate; auto-falls-back to chunked sessions
54
+ * if the server returns 413 with `suggestedEndpoint=/v1/compliance/scans`
55
+ * - `mode: 'async'`: kicks off a 202 async-validate and polls until
56
+ * terminal (returns same shape as default)
57
+ * - `mode: 'chunked'`: explicit chunked-session flow regardless of size
8
58
  */
9
59
  export declare function scan(params: {
10
60
  repoPath: string;
11
61
  frameworks?: string[];
12
62
  options?: ScanOptions;
13
- }): Promise<{
14
- passed: boolean;
15
- exitCode: number;
16
- findings: never[];
17
- report: null;
18
- summary?: undefined;
19
- } | {
20
- passed: any;
21
- exitCode: number;
22
- findings: any;
23
- report: any;
24
- summary: any;
25
- }>;
63
+ }): Promise<ScanReturn>;
26
64
  /**
27
- * Gate code strings directly without writing to disk
65
+ * Gate code strings directly without writing to disk (low-latency hook
66
+ * endpoint, used by coding-agent post-edit hooks).
28
67
  */
29
68
  export declare function gate(options: GateOptions): Promise<{
30
- passed: any;
69
+ scannerError?: ScannerError | undefined;
70
+ passed: boolean;
31
71
  exitCode: number;
32
- findings: any;
33
- prompt: any;
34
- summary: any;
72
+ findings: unknown[];
73
+ prompt: string | undefined;
74
+ summary: unknown;
35
75
  }>;