@prodcycle/prodcycle 0.6.6 → 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 +39 -0
- package/dist/cli.js +146 -11
- 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
|
@@ -50,3 +50,42 @@ export declare function resolveHookFileKey(inputPath: string, realpathFile: stri
|
|
|
50
50
|
ok: false;
|
|
51
51
|
error: string;
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* True when a parsed `hook` stdin payload is a Claude Code hook event.
|
|
55
|
+
*
|
|
56
|
+
* Claude Code stamps every hook payload with a `hook_event_name` field
|
|
57
|
+
* (e.g. `"PostToolUse"`); no other supported input shape carries it. We
|
|
58
|
+
* key on its presence to switch `hook` into Claude-Code-native JSON
|
|
59
|
+
* output — see `formatClaudeHookOutput`. Pure so it's unit-testable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isClaudeCodeHookPayload(payload: unknown): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Render a `hook` scan result as Claude Code PostToolUse hook JSON.
|
|
64
|
+
*
|
|
65
|
+
* Returns `''` when the scan passed — a clean result needs no agent
|
|
66
|
+
* feedback, so the hook stays silent. On violations it returns
|
|
67
|
+
* `{"decision":"block","reason":...}`: Claude Code feeds `reason` back to
|
|
68
|
+
* the agent as actionable feedback, which is what closes the compliance
|
|
69
|
+
* self-healing loop.
|
|
70
|
+
*
|
|
71
|
+
* The caller MUST exit 0 for Claude Code to parse this — Claude Code
|
|
72
|
+
* ignores hook JSON on any non-zero exit. Pure so it's unit-testable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatClaudeHookOutput(response: {
|
|
75
|
+
exitCode: number;
|
|
76
|
+
passed?: boolean;
|
|
77
|
+
prompt?: string;
|
|
78
|
+
findings?: unknown[];
|
|
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
|
@@ -36,6 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.isCiEnvironment = isCiEnvironment;
|
|
38
38
|
exports.resolveHookFileKey = resolveHookFileKey;
|
|
39
|
+
exports.isClaudeCodeHookPayload = isClaudeCodeHookPayload;
|
|
40
|
+
exports.formatClaudeHookOutput = formatClaudeHookOutput;
|
|
41
|
+
exports.formatClaudeHookSetupNotice = formatClaudeHookSetupNotice;
|
|
39
42
|
const commander_1 = require("commander");
|
|
40
43
|
const child_process_1 = require("child_process");
|
|
41
44
|
const fs = __importStar(require("fs"));
|
|
@@ -44,6 +47,7 @@ const index_1 = require("./index");
|
|
|
44
47
|
const table_1 = require("./formatters/table");
|
|
45
48
|
const sarif_1 = require("./formatters/sarif");
|
|
46
49
|
const prompt_1 = require("./formatters/prompt");
|
|
50
|
+
const config_1 = require("./utils/config");
|
|
47
51
|
const KNOWN_COMMANDS = new Set([
|
|
48
52
|
'scan',
|
|
49
53
|
'scans',
|
|
@@ -340,25 +344,63 @@ program
|
|
|
340
344
|
.option('--api-url <url>', 'Compliance API base URL (or PC_API_URL env)')
|
|
341
345
|
.option('--api-key <key>', 'API key for compliance API (or PC_API_KEY env)')
|
|
342
346
|
.action(async (opts) => {
|
|
347
|
+
// Hoisted so the catch block can branch on it (see the auth-error case).
|
|
348
|
+
let claudeHook = false;
|
|
343
349
|
try {
|
|
344
350
|
const frameworks = parseList(opts.framework) ?? ['soc2', 'hipaa', 'nist-csf'];
|
|
345
351
|
const format = (opts.format ?? 'prompt');
|
|
346
|
-
const
|
|
347
|
-
if (!
|
|
352
|
+
const collected = await collectHookFiles(opts.file);
|
|
353
|
+
if (!collected || Object.keys(collected.files).length === 0) {
|
|
348
354
|
// No files to check — exit clean so the agent proceeds.
|
|
349
355
|
process.exit(0);
|
|
350
356
|
return;
|
|
351
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
|
+
}
|
|
352
368
|
const response = await (0, index_1.gate)({
|
|
353
|
-
files,
|
|
369
|
+
files: collected.files,
|
|
354
370
|
frameworks,
|
|
355
371
|
apiUrl: opts.apiUrl,
|
|
356
372
|
apiKey: opts.apiKey,
|
|
357
373
|
});
|
|
374
|
+
if (claudeHook && response.exitCode !== 2) {
|
|
375
|
+
// Invoked as a Claude Code PostToolUse hook. Emit Claude Code's
|
|
376
|
+
// native hook JSON on stdout so a violation is fed back to the
|
|
377
|
+
// agent as actionable feedback — this is what closes the compliance
|
|
378
|
+
// self-healing loop. Claude Code only parses hook JSON when the
|
|
379
|
+
// command exits 0, so we exit 0 here even on violations and let
|
|
380
|
+
// `decision: "block"` carry the signal.
|
|
381
|
+
//
|
|
382
|
+
// exitCode 2 (scanner unavailable) deliberately falls through to
|
|
383
|
+
// the generic path below: it exits 2, and Claude Code surfaces a
|
|
384
|
+
// hook's stderr on a 2 — gate() already wrote the scanner-error
|
|
385
|
+
// warning there, so the agent still learns the scan was inconclusive.
|
|
386
|
+
const claudeJson = formatClaudeHookOutput(response);
|
|
387
|
+
if (claudeJson)
|
|
388
|
+
process.stdout.write(claudeJson + '\n');
|
|
389
|
+
process.exit(0);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
358
392
|
writeOutput(renderReport(response, format), opts.output);
|
|
359
393
|
process.exit(response.exitCode);
|
|
360
394
|
}
|
|
361
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
|
+
}
|
|
362
404
|
console.error(`\u2717 Error: ${error.message}`);
|
|
363
405
|
process.exit(2);
|
|
364
406
|
}
|
|
@@ -409,6 +451,68 @@ function resolveHookFileKey(inputPath, realpathFile, realpathCwd) {
|
|
|
409
451
|
}
|
|
410
452
|
return { ok: true, key: relative };
|
|
411
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* True when a parsed `hook` stdin payload is a Claude Code hook event.
|
|
456
|
+
*
|
|
457
|
+
* Claude Code stamps every hook payload with a `hook_event_name` field
|
|
458
|
+
* (e.g. `"PostToolUse"`); no other supported input shape carries it. We
|
|
459
|
+
* key on its presence to switch `hook` into Claude-Code-native JSON
|
|
460
|
+
* output — see `formatClaudeHookOutput`. Pure so it's unit-testable.
|
|
461
|
+
*/
|
|
462
|
+
function isClaudeCodeHookPayload(payload) {
|
|
463
|
+
return (typeof payload === 'object' &&
|
|
464
|
+
payload !== null &&
|
|
465
|
+
typeof payload.hook_event_name === 'string');
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Render a `hook` scan result as Claude Code PostToolUse hook JSON.
|
|
469
|
+
*
|
|
470
|
+
* Returns `''` when the scan passed — a clean result needs no agent
|
|
471
|
+
* feedback, so the hook stays silent. On violations it returns
|
|
472
|
+
* `{"decision":"block","reason":...}`: Claude Code feeds `reason` back to
|
|
473
|
+
* the agent as actionable feedback, which is what closes the compliance
|
|
474
|
+
* self-healing loop.
|
|
475
|
+
*
|
|
476
|
+
* The caller MUST exit 0 for Claude Code to parse this — Claude Code
|
|
477
|
+
* ignores hook JSON on any non-zero exit. Pure so it's unit-testable.
|
|
478
|
+
*/
|
|
479
|
+
function formatClaudeHookOutput(response) {
|
|
480
|
+
if (response.exitCode === 0)
|
|
481
|
+
return '';
|
|
482
|
+
const reason = typeof response.prompt === 'string' && response.prompt.trim()
|
|
483
|
+
? response.prompt
|
|
484
|
+
: (0, prompt_1.formatPrompt)(response);
|
|
485
|
+
return JSON.stringify({ decision: 'block', reason });
|
|
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
|
+
}
|
|
412
516
|
async function collectHookFiles(filePath) {
|
|
413
517
|
if (filePath) {
|
|
414
518
|
const absolute = path.resolve(filePath);
|
|
@@ -427,7 +531,8 @@ async function collectHookFiles(filePath) {
|
|
|
427
531
|
console.error(resolved.error);
|
|
428
532
|
process.exit(2);
|
|
429
533
|
}
|
|
430
|
-
|
|
534
|
+
// `--file` is a manual / non-agent invocation — never Claude Code.
|
|
535
|
+
return { files: { [resolved.key]: content }, claudeHook: false };
|
|
431
536
|
}
|
|
432
537
|
const stdin = await readStdin();
|
|
433
538
|
if (!stdin.trim()) {
|
|
@@ -442,22 +547,25 @@ async function collectHookFiles(filePath) {
|
|
|
442
547
|
console.error(`hook: invalid JSON on stdin: ${e.message}`);
|
|
443
548
|
process.exit(2);
|
|
444
549
|
}
|
|
550
|
+
// Claude Code stamps its hook payloads with `hook_event_name`; detect it
|
|
551
|
+
// once here so every return below can flag a Claude Code invocation.
|
|
552
|
+
const claudeHook = isClaudeCodeHookPayload(payload);
|
|
445
553
|
// Shape 1: {"files": {path: content}} — gate-compatible
|
|
446
554
|
if (payload && typeof payload.files === 'object' && payload.files !== null) {
|
|
447
|
-
return payload.files;
|
|
555
|
+
return { files: payload.files, claudeHook };
|
|
448
556
|
}
|
|
449
557
|
// Shape 2: top-level single file. Shape 3: Claude Code tool_input nesting.
|
|
450
558
|
const candidate = payload?.tool_input ?? payload;
|
|
451
559
|
const hookFilePath = candidate?.file_path ?? candidate?.path;
|
|
452
560
|
const hookContent = candidate?.content ?? candidate?.new_string;
|
|
453
561
|
if (hookFilePath && typeof hookContent === 'string') {
|
|
454
|
-
return { [hookFilePath]: hookContent };
|
|
562
|
+
return { files: { [hookFilePath]: hookContent }, claudeHook };
|
|
455
563
|
}
|
|
456
564
|
if (hookFilePath && fs.existsSync(hookFilePath)) {
|
|
457
565
|
// Only a path was given — read from disk so post-edit hooks still work
|
|
458
566
|
// when the agent doesn't ship the content inline.
|
|
459
567
|
const content = fs.readFileSync(hookFilePath, 'utf8');
|
|
460
|
-
return { [hookFilePath]: content };
|
|
568
|
+
return { files: { [hookFilePath]: content }, claudeHook };
|
|
461
569
|
}
|
|
462
570
|
console.error('hook: stdin payload not recognized. Expected one of:\n' +
|
|
463
571
|
' {"files": {"path": "content"}}\n' +
|
|
@@ -474,16 +582,28 @@ program
|
|
|
474
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.')
|
|
475
583
|
.option('--force', 'Overwrite existing compliance hook entries')
|
|
476
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')
|
|
477
586
|
.action((opts) => {
|
|
478
587
|
try {
|
|
479
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
|
+
}
|
|
480
594
|
const agents = resolveAgents(opts.agent, dir);
|
|
481
595
|
const ciProviders = resolveCiProviders(opts.ci);
|
|
482
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
|
+
}
|
|
483
602
|
console.error('init: nothing to do. ' +
|
|
484
603
|
'Use --agent <name> to configure a coding agent (claude, cursor, codex, ' +
|
|
485
|
-
'opencode, github-copilot, gemini-cli, or "all"),
|
|
486
|
-
'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. ' +
|
|
487
607
|
'Without --agent the CLI also auto-detects agents already in use.');
|
|
488
608
|
process.exit(2);
|
|
489
609
|
}
|
|
@@ -570,6 +690,21 @@ function configureAgent(agent, dir, force, writtenPaths) {
|
|
|
570
690
|
}
|
|
571
691
|
const CLAUDE_MATCHER = 'Write|Edit|MultiEdit';
|
|
572
692
|
const CLAUDE_COMMAND = 'prodcycle hook';
|
|
693
|
+
/**
|
|
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.
|
|
700
|
+
*/
|
|
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.';
|
|
707
|
+
}
|
|
573
708
|
function configureClaudeCode(dir, force) {
|
|
574
709
|
const claudeDir = path.join(dir, '.claude');
|
|
575
710
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -617,7 +752,7 @@ function configureClaudeCode(dir, force) {
|
|
|
617
752
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
618
753
|
return {
|
|
619
754
|
status: 'installed',
|
|
620
|
-
message: `[claude] wrote PostToolUse hook to ${settingsPath}.
|
|
755
|
+
message: `[claude] wrote PostToolUse hook to ${settingsPath}. ${apiKeyHint()}`,
|
|
621
756
|
};
|
|
622
757
|
}
|
|
623
758
|
const CURSOR_COMMAND = 'prodcycle hook';
|
|
@@ -665,7 +800,7 @@ function configureCursor(dir, force) {
|
|
|
665
800
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
|
|
666
801
|
return {
|
|
667
802
|
status: 'installed',
|
|
668
|
-
message: `[cursor] wrote afterFileEdit hook to ${hooksPath}.
|
|
803
|
+
message: `[cursor] wrote afterFileEdit hook to ${hooksPath}. ${apiKeyHint()}`,
|
|
669
804
|
};
|
|
670
805
|
}
|
|
671
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