@prodcycle/prodcycle 0.6.7 → 0.6.8
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/api-client.js +7 -2
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +80 -18
- package/dist/utils/config.d.ts +37 -0
- package/dist/utils/config.js +129 -0
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ComplianceApiClient = exports.ApiError = void 0;
|
|
4
4
|
exports.chunkFiles = chunkFiles;
|
|
5
|
+
const config_1 = require("./utils/config");
|
|
5
6
|
/**
|
|
6
7
|
* Error thrown for any non-2xx response. Carries the parsed body + status so
|
|
7
8
|
* callers can branch on `details.suggestedEndpoint` (413 → chunked-session
|
|
@@ -98,8 +99,12 @@ class ComplianceApiClient {
|
|
|
98
99
|
apiUrl;
|
|
99
100
|
apiKey;
|
|
100
101
|
constructor(apiUrl, apiKey) {
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
// Resolution order (for both): explicit arg → env var → user config
|
|
103
|
+
// file → default. See `utils/config.ts`. Read the config file once and
|
|
104
|
+
// resolve both values from it rather than re-reading per value.
|
|
105
|
+
const config = (0, config_1.readConfig)();
|
|
106
|
+
this.apiUrl = (0, config_1.resolveApiUrl)(apiUrl, config) || DEFAULT_API_URL;
|
|
107
|
+
this.apiKey = (0, config_1.resolveApiKey)(apiKey, config);
|
|
103
108
|
if (!this.apiKey &&
|
|
104
109
|
process.env.NODE_ENV !== 'test' &&
|
|
105
110
|
!process.env.PC_SUPPRESS_WARNINGS) {
|
package/dist/cli.d.ts
CHANGED
|
@@ -77,3 +77,15 @@ export declare function formatClaudeHookOutput(response: {
|
|
|
77
77
|
prompt?: string;
|
|
78
78
|
findings?: unknown[];
|
|
79
79
|
}): string;
|
|
80
|
+
/**
|
|
81
|
+
* Claude Code hook JSON for the "API key not usable" state.
|
|
82
|
+
*
|
|
83
|
+
* A missing or rejected key is a setup problem, not a compliance failure —
|
|
84
|
+
* so instead of blocking every edit with an error, we emit a `systemMessage`
|
|
85
|
+
* (shown to the user, never fed to the agent, never blocking) telling them
|
|
86
|
+
* how to fix it. `reason` distinguishes a key that is absent (`'missing'`)
|
|
87
|
+
* from one the API rejected (`'rejected'` — expired/revoked/wrong) so the
|
|
88
|
+
* message points at the right fix. The caller exits 0. Pure so it's
|
|
89
|
+
* unit-testable.
|
|
90
|
+
*/
|
|
91
|
+
export declare function formatClaudeHookSetupNotice(reason?: 'missing' | 'rejected'): string;
|
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.isCiEnvironment = isCiEnvironment;
|
|
|
38
38
|
exports.resolveHookFileKey = resolveHookFileKey;
|
|
39
39
|
exports.isClaudeCodeHookPayload = isClaudeCodeHookPayload;
|
|
40
40
|
exports.formatClaudeHookOutput = formatClaudeHookOutput;
|
|
41
|
+
exports.formatClaudeHookSetupNotice = formatClaudeHookSetupNotice;
|
|
41
42
|
const commander_1 = require("commander");
|
|
42
43
|
const child_process_1 = require("child_process");
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
@@ -46,6 +47,7 @@ const index_1 = require("./index");
|
|
|
46
47
|
const table_1 = require("./formatters/table");
|
|
47
48
|
const sarif_1 = require("./formatters/sarif");
|
|
48
49
|
const prompt_1 = require("./formatters/prompt");
|
|
50
|
+
const config_1 = require("./utils/config");
|
|
49
51
|
const KNOWN_COMMANDS = new Set([
|
|
50
52
|
'scan',
|
|
51
53
|
'scans',
|
|
@@ -342,6 +344,8 @@ program
|
|
|
342
344
|
.option('--api-url <url>', 'Compliance API base URL (or PC_API_URL env)')
|
|
343
345
|
.option('--api-key <key>', 'API key for compliance API (or PC_API_KEY env)')
|
|
344
346
|
.action(async (opts) => {
|
|
347
|
+
// Hoisted so the catch block can branch on it (see the auth-error case).
|
|
348
|
+
let claudeHook = false;
|
|
345
349
|
try {
|
|
346
350
|
const frameworks = parseList(opts.framework) ?? ['soc2', 'hipaa', 'nist-csf'];
|
|
347
351
|
const format = (opts.format ?? 'prompt');
|
|
@@ -351,13 +355,23 @@ program
|
|
|
351
355
|
process.exit(0);
|
|
352
356
|
return;
|
|
353
357
|
}
|
|
358
|
+
claudeHook = collected.claudeHook;
|
|
359
|
+
// Graceful unconfigured state: a missing API key is a setup problem,
|
|
360
|
+
// not a compliance failure. For a Claude Code hook, surface a one-line
|
|
361
|
+
// setup notice and exit 0 so the agent is not blocked on every edit.
|
|
362
|
+
// Non-Claude callers (CI) fall through and hard-fail as before.
|
|
363
|
+
if (claudeHook && !(0, config_1.resolveApiKey)(opts.apiKey)) {
|
|
364
|
+
process.stdout.write(formatClaudeHookSetupNotice('missing') + '\n');
|
|
365
|
+
process.exit(0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
354
368
|
const response = await (0, index_1.gate)({
|
|
355
369
|
files: collected.files,
|
|
356
370
|
frameworks,
|
|
357
371
|
apiUrl: opts.apiUrl,
|
|
358
372
|
apiKey: opts.apiKey,
|
|
359
373
|
});
|
|
360
|
-
if (
|
|
374
|
+
if (claudeHook && response.exitCode !== 2) {
|
|
361
375
|
// Invoked as a Claude Code PostToolUse hook. Emit Claude Code's
|
|
362
376
|
// native hook JSON on stdout so a violation is fed back to the
|
|
363
377
|
// agent as actionable feedback — this is what closes the compliance
|
|
@@ -379,6 +393,14 @@ program
|
|
|
379
393
|
process.exit(response.exitCode);
|
|
380
394
|
}
|
|
381
395
|
catch (error) {
|
|
396
|
+
// A rejected key (401/403) is a setup problem too \u2014 on the Claude Code
|
|
397
|
+
// hook path, treat it like the missing-key case rather than blocking
|
|
398
|
+
// every edit with an error.
|
|
399
|
+
if (claudeHook && isAuthError(error)) {
|
|
400
|
+
process.stdout.write(formatClaudeHookSetupNotice('rejected') + '\n');
|
|
401
|
+
process.exit(0);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
382
404
|
console.error(`\u2717 Error: ${error.message}`);
|
|
383
405
|
process.exit(2);
|
|
384
406
|
}
|
|
@@ -462,6 +484,35 @@ function formatClaudeHookOutput(response) {
|
|
|
462
484
|
: (0, prompt_1.formatPrompt)(response);
|
|
463
485
|
return JSON.stringify({ decision: 'block', reason });
|
|
464
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Claude Code hook JSON for the "API key not usable" state.
|
|
489
|
+
*
|
|
490
|
+
* A missing or rejected key is a setup problem, not a compliance failure —
|
|
491
|
+
* so instead of blocking every edit with an error, we emit a `systemMessage`
|
|
492
|
+
* (shown to the user, never fed to the agent, never blocking) telling them
|
|
493
|
+
* how to fix it. `reason` distinguishes a key that is absent (`'missing'`)
|
|
494
|
+
* from one the API rejected (`'rejected'` — expired/revoked/wrong) so the
|
|
495
|
+
* message points at the right fix. The caller exits 0. Pure so it's
|
|
496
|
+
* unit-testable.
|
|
497
|
+
*/
|
|
498
|
+
function formatClaudeHookSetupNotice(reason = 'missing') {
|
|
499
|
+
const detail = reason === 'rejected'
|
|
500
|
+
? 'the configured API key was rejected (it may be expired or revoked)'
|
|
501
|
+
: 'no API key found';
|
|
502
|
+
return JSON.stringify({
|
|
503
|
+
systemMessage: `ProdCycle compliance scanning is inactive — ${detail}. ` +
|
|
504
|
+
'Run `prodcycle init --api-key pc_...` to save a valid key, or set the ' +
|
|
505
|
+
'PC_API_KEY environment variable. See https://docs.prodcycle.com.',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* True when an error is the API rejecting our credentials (401/403) — i.e.
|
|
510
|
+
* a key that is missing, wrong, or revoked. Treated as a setup problem, not
|
|
511
|
+
* a scan failure, on the Claude Code hook path.
|
|
512
|
+
*/
|
|
513
|
+
function isAuthError(error) {
|
|
514
|
+
return error instanceof index_1.ApiError && (error.statusCode === 401 || error.statusCode === 403);
|
|
515
|
+
}
|
|
465
516
|
async function collectHookFiles(filePath) {
|
|
466
517
|
if (filePath) {
|
|
467
518
|
const absolute = path.resolve(filePath);
|
|
@@ -531,16 +582,28 @@ program
|
|
|
531
582
|
.option('--ci <providers>', 'Comma-separated CI providers to scaffold (github, gitlab, circleci). Use "all" for every provider. Opt-in only \u2014 never auto-detected.')
|
|
532
583
|
.option('--force', 'Overwrite existing compliance hook entries')
|
|
533
584
|
.option('--dir <path>', 'Project directory to configure', '.')
|
|
585
|
+
.option('--api-key <key>', 'Save this API key to the user config file (~/.config/prodcycle/config.json, chmod 600) so hooks authenticate without an env var')
|
|
534
586
|
.action((opts) => {
|
|
535
587
|
try {
|
|
536
588
|
const dir = path.resolve(opts.dir ?? '.');
|
|
589
|
+
// Persist the API key first so the post-install hints below see it.
|
|
590
|
+
if (opts.apiKey) {
|
|
591
|
+
const savedTo = (0, config_1.writeApiKey)(opts.apiKey);
|
|
592
|
+
process.stdout.write(`[config] saved API key to ${savedTo} (readable only by you).\n`);
|
|
593
|
+
}
|
|
537
594
|
const agents = resolveAgents(opts.agent, dir);
|
|
538
595
|
const ciProviders = resolveCiProviders(opts.ci);
|
|
539
596
|
if (agents.length === 0 && ciProviders.length === 0) {
|
|
597
|
+
if (opts.apiKey) {
|
|
598
|
+
// Saving the key was itself the requested action — nothing else to do.
|
|
599
|
+
process.exit(0);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
540
602
|
console.error('init: nothing to do. ' +
|
|
541
603
|
'Use --agent <name> to configure a coding agent (claude, cursor, codex, ' +
|
|
542
|
-
'opencode, github-copilot, gemini-cli, or "all"),
|
|
543
|
-
'to scaffold CI workflows (github, gitlab, circleci, or "all")
|
|
604
|
+
'opencode, github-copilot, gemini-cli, or "all"), --ci <provider> ' +
|
|
605
|
+
'to scaffold CI workflows (github, gitlab, circleci, or "all"), ' +
|
|
606
|
+
'and/or --api-key <key> to save your credentials. ' +
|
|
544
607
|
'Without --agent the CLI also auto-detects agents already in use.');
|
|
545
608
|
process.exit(2);
|
|
546
609
|
}
|
|
@@ -628,20 +691,19 @@ function configureAgent(agent, dir, force, writtenPaths) {
|
|
|
628
691
|
const CLAUDE_MATCHER = 'Write|Edit|MultiEdit';
|
|
629
692
|
const CLAUDE_COMMAND = 'prodcycle hook';
|
|
630
693
|
/**
|
|
631
|
-
* Build the post-install hint about
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
694
|
+
* Build the post-install hint about API-key setup. The hook authenticates
|
|
695
|
+
* against the hosted API using a key resolved from `--api-key`, the
|
|
696
|
+
* `PC_API_KEY` env var, or the user config file (see `utils/config.ts`).
|
|
697
|
+
* The config file is the robust option — unlike an env var, a GUI-launched
|
|
698
|
+
* editor picks it up with no relaunch — so when no key is found we point
|
|
699
|
+
* the user there.
|
|
637
700
|
*/
|
|
638
|
-
function apiKeyHint(
|
|
639
|
-
return
|
|
640
|
-
?
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
'
|
|
644
|
-
'profile). The hook calls the hosted API and fails without it.';
|
|
701
|
+
function apiKeyHint() {
|
|
702
|
+
return (0, config_1.resolveApiKey)()
|
|
703
|
+
? 'API key configured ✓'
|
|
704
|
+
: '⚠ No API key configured — run `prodcycle init --api-key pc_...` to ' +
|
|
705
|
+
'save one (or set the PC_API_KEY env var). The hook calls the hosted ' +
|
|
706
|
+
'API and fails without it.';
|
|
645
707
|
}
|
|
646
708
|
function configureClaudeCode(dir, force) {
|
|
647
709
|
const claudeDir = path.join(dir, '.claude');
|
|
@@ -690,7 +752,7 @@ function configureClaudeCode(dir, force) {
|
|
|
690
752
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
691
753
|
return {
|
|
692
754
|
status: 'installed',
|
|
693
|
-
message: `[claude] wrote PostToolUse hook to ${settingsPath}. ${apiKeyHint(
|
|
755
|
+
message: `[claude] wrote PostToolUse hook to ${settingsPath}. ${apiKeyHint()}`,
|
|
694
756
|
};
|
|
695
757
|
}
|
|
696
758
|
const CURSOR_COMMAND = 'prodcycle hook';
|
|
@@ -738,7 +800,7 @@ function configureCursor(dir, force) {
|
|
|
738
800
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
|
|
739
801
|
return {
|
|
740
802
|
status: 'installed',
|
|
741
|
-
message: `[cursor] wrote afterFileEdit hook to ${hooksPath}. ${apiKeyHint(
|
|
803
|
+
message: `[cursor] wrote afterFileEdit hook to ${hooksPath}. ${apiKeyHint()}`,
|
|
742
804
|
};
|
|
743
805
|
}
|
|
744
806
|
// ── Instruction-file agents (codex, opencode, github-copilot, gemini-cli) ───
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ProdcycleConfig {
|
|
2
|
+
api_key?: string;
|
|
3
|
+
api_url?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Path to the user-level config file:
|
|
7
|
+
* `$XDG_CONFIG_HOME/prodcycle/config.json`, falling back to
|
|
8
|
+
* `~/.config/prodcycle/config.json`. A non-absolute `XDG_CONFIG_HOME` is
|
|
9
|
+
* ignored, per the XDG Base Directory spec.
|
|
10
|
+
*/
|
|
11
|
+
export declare function configFilePath(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Read the user-level config file. Returns `{}` when the file is absent,
|
|
14
|
+
* unreadable, or malformed — a broken config must never crash a scan.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readConfig(): ProdcycleConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Persist the API key to the user-level config file, merging into any keys
|
|
19
|
+
* already present. The file is written `0600` (directory `0700`) so the
|
|
20
|
+
* credential is not world-readable. Returns the path written.
|
|
21
|
+
*/
|
|
22
|
+
export declare function writeApiKey(apiKey: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the API key, in precedence order: an explicit value (CLI
|
|
25
|
+
* `--api-key`), the `PC_API_KEY` env var, then the user-level config file.
|
|
26
|
+
* Returns `''` when none is configured.
|
|
27
|
+
*
|
|
28
|
+
* Pass an already-read `config` to avoid re-reading the file when the
|
|
29
|
+
* caller resolves several values at once (see `ComplianceApiClient`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveApiKey(explicit?: string, config?: ProdcycleConfig): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the API URL: explicit value → `PC_API_URL` env → config file →
|
|
34
|
+
* `undefined` (the caller then applies its own default). Accepts an
|
|
35
|
+
* already-read `config` — see `resolveApiKey`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveApiUrl(explicit?: string, config?: ProdcycleConfig): string | undefined;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// User-level ProdCycle configuration.
|
|
3
|
+
//
|
|
4
|
+
// Lets a developer save their API key once, on disk, instead of exporting
|
|
5
|
+
// `PC_API_KEY` into every shell — and, critically, into the environment a
|
|
6
|
+
// GUI-launched editor runs in, which does NOT inherit shell exports. The
|
|
7
|
+
// hook reads this file regardless of how the agent was launched, so
|
|
8
|
+
// `prodcycle init --api-key pc_...` is a one-time setup with no relaunch.
|
|
9
|
+
//
|
|
10
|
+
// Mirrors `python/src/prodcycle/utils/config.py` — the two must stay in
|
|
11
|
+
// lockstep.
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.configFilePath = configFilePath;
|
|
47
|
+
exports.readConfig = readConfig;
|
|
48
|
+
exports.writeApiKey = writeApiKey;
|
|
49
|
+
exports.resolveApiKey = resolveApiKey;
|
|
50
|
+
exports.resolveApiUrl = resolveApiUrl;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
// One-shot guard so a malformed config file warns at most once per process
|
|
55
|
+
// (both `resolveApiKey` and `resolveApiUrl` call `readConfig`).
|
|
56
|
+
let warnedMalformed = false;
|
|
57
|
+
/**
|
|
58
|
+
* Path to the user-level config file:
|
|
59
|
+
* `$XDG_CONFIG_HOME/prodcycle/config.json`, falling back to
|
|
60
|
+
* `~/.config/prodcycle/config.json`. A non-absolute `XDG_CONFIG_HOME` is
|
|
61
|
+
* ignored, per the XDG Base Directory spec.
|
|
62
|
+
*/
|
|
63
|
+
function configFilePath() {
|
|
64
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
65
|
+
const base = xdg && path.isAbsolute(xdg) ? xdg : path.join(os.homedir(), '.config');
|
|
66
|
+
return path.join(base, 'prodcycle', 'config.json');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the user-level config file. Returns `{}` when the file is absent,
|
|
70
|
+
* unreadable, or malformed — a broken config must never crash a scan.
|
|
71
|
+
*/
|
|
72
|
+
function readConfig() {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = fs.readFileSync(configFilePath(), 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return {}; // absent / unreadable — the expected case, not an error
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// fall through to the warning below
|
|
88
|
+
}
|
|
89
|
+
if (!warnedMalformed) {
|
|
90
|
+
warnedMalformed = true;
|
|
91
|
+
process.stderr.write(`Warning: ${configFilePath()} is not valid JSON — ignoring it.\n`);
|
|
92
|
+
}
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Persist the API key to the user-level config file, merging into any keys
|
|
97
|
+
* already present. The file is written `0600` (directory `0700`) so the
|
|
98
|
+
* credential is not world-readable. Returns the path written.
|
|
99
|
+
*/
|
|
100
|
+
function writeApiKey(apiKey) {
|
|
101
|
+
const file = configFilePath();
|
|
102
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
103
|
+
const config = readConfig();
|
|
104
|
+
config.api_key = apiKey;
|
|
105
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
106
|
+
// `mode` on writeFileSync is ignored when the file already exists, so set
|
|
107
|
+
// it explicitly — the credential must never be left world-readable.
|
|
108
|
+
fs.chmodSync(file, 0o600);
|
|
109
|
+
return file;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the API key, in precedence order: an explicit value (CLI
|
|
113
|
+
* `--api-key`), the `PC_API_KEY` env var, then the user-level config file.
|
|
114
|
+
* Returns `''` when none is configured.
|
|
115
|
+
*
|
|
116
|
+
* Pass an already-read `config` to avoid re-reading the file when the
|
|
117
|
+
* caller resolves several values at once (see `ComplianceApiClient`).
|
|
118
|
+
*/
|
|
119
|
+
function resolveApiKey(explicit, config) {
|
|
120
|
+
return explicit || process.env.PC_API_KEY || (config ?? readConfig()).api_key || '';
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the API URL: explicit value → `PC_API_URL` env → config file →
|
|
124
|
+
* `undefined` (the caller then applies its own default). Accepts an
|
|
125
|
+
* already-read `config` — see `resolveApiKey`.
|
|
126
|
+
*/
|
|
127
|
+
function resolveApiUrl(explicit, config) {
|
|
128
|
+
return explicit || process.env.PC_API_URL || (config ?? readConfig()).api_url || undefined;
|
|
129
|
+
}
|
package/package.json
CHANGED