codeproof 1.0.2 → 1.0.4

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/.env ADDED
@@ -0,0 +1 @@
1
+ AI_API_URL = "https://api-risk-fgef.onrender.com/predict"
@@ -37,8 +37,13 @@ export async function runReportDashboard({ cwd }) {
37
37
 
38
38
  if (!latestReport) {
39
39
  logWarn("No reports found. Run `codeproof run` first.");
40
- } else {
41
- const integration = config?.integration || {};
40
+ return;
41
+ }
42
+
43
+ const integration = config?.integration || {};
44
+ const integrationEnabled = features.integration && Boolean(integration.enabled);
45
+
46
+ if (integrationEnabled) {
42
47
  // Integrations are fail-open: never throw on network errors.
43
48
  withFailOpenIntegration(() => {
44
49
  sendReportToServer(latestReport, {
@@ -46,10 +51,13 @@ export async function runReportDashboard({ cwd }) {
46
51
  endpointUrl: integration.endpointUrl
47
52
  });
48
53
  });
54
+ logInfo("Report sent to server.");
55
+ } else {
56
+ reportFeatureDisabled("Integration", verbose, logInfo);
57
+ }
49
58
 
50
- if (latestReport?.projectId) {
51
- logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
52
- }
59
+ if (latestReport?.projectId) {
60
+ logInfo(`View dashboard: https://dashboard.codeproof.dev/project/${latestReport.projectId}`);
53
61
  }
54
62
  } else {
55
63
  reportFeatureDisabled("Reporting", verbose, logInfo);
package/commands/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { ensureGitRepo, getGitRoot, getStagedFiles } from "../utils/git.js";
3
+ import { ensureGitRepo, getGitRoot, getStagedFiles, getRepoIdentifier, getProjectName } from "../utils/git.js";
4
4
  import { logError, logInfo, logSuccess, logWarn } from "../utils/logger.js";
5
5
  import { buildScanTargets } from "../utils/fileScanner.js";
6
6
  import { runRuleEngine } from "../engine/ruleEngine.js";
@@ -110,7 +110,7 @@ export async function runCli({ args = [], cwd }) {
110
110
  }
111
111
 
112
112
  const aiDecisions = aiInputs.length > 0
113
- ? withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
113
+ ? await withFailOpenAiEscalation(features.aiEscalation, () => analyze(aiInputs, projectContext))
114
114
  : [];
115
115
  const { blockFindings, warnFindings, aiReviewed, exitCode } = mergeDecisions({
116
116
  baselineFindings: [...findings, ...escalations],
@@ -118,14 +118,18 @@ export async function runCli({ args = [], cwd }) {
118
118
  });
119
119
 
120
120
  if (features.reporting) {
121
- withFailOpenReporting(() => {
121
+ await withFailOpenReporting(async () => {
122
122
  const timestamp = new Date().toISOString();
123
123
  const reportId = randomUUID();
124
124
  const projectId = config.projectId || "";
125
125
  const clientId = getClientId();
126
+ const projectName = getProjectName(gitRoot);
127
+ const repoIdentifier = getRepoIdentifier(gitRoot);
126
128
  const report = buildReport({
127
129
  projectRoot: gitRoot,
128
130
  projectId,
131
+ projectName,
132
+ repoIdentifier,
129
133
  clientId,
130
134
  reportId,
131
135
  scanMode,
@@ -134,62 +138,88 @@ export async function runCli({ args = [], cwd }) {
134
138
  aiReviewed,
135
139
  timestamp
136
140
  });
137
- // Reporting is fail-open: never block commits if logging fails.
141
+ // Report is saved to file and sent to server regardless of findings
142
+ logInfo("Saving report to file...");
138
143
  writeReport({ projectRoot: gitRoot, report });
144
+ logSuccess("Report saved locally.");
139
145
 
140
146
  const integration = config?.integration || {};
141
147
  const integrationEnabled = features.integration && Boolean(integration.enabled);
148
+
149
+ // Always send to server in pre-commit or manual mode
142
150
  if (integrationEnabled) {
143
- withFailOpenIntegration(() => {
151
+ logInfo("Syncing report to server...");
152
+ await withFailOpenIntegration(async () => {
144
153
  // Network calls are fail-open; never affect exit codes.
145
- sendReportToServer(report, {
154
+ return await sendReportToServer(report, {
146
155
  enabled: true,
147
156
  endpointUrl: integration.endpointUrl
148
157
  });
149
158
  });
159
+ logSuccess("Report synced to server.");
150
160
  } else {
151
161
  reportFeatureDisabled("Integration", verbose, logInfo);
152
162
  }
153
163
  }, () => {
154
- logWarn("Failed to write CodeProof report. Continuing without blocking.");
164
+ logWarn("Failed to process report. Continuing without blocking.");
155
165
  });
156
166
  } else {
157
167
  reportFeatureDisabled("Reporting", verbose, logInfo);
158
168
  }
159
169
 
160
170
  if (blockFindings.length > 0) {
161
- logError(`Baseline rule violations (${blockFindings.length}):`);
171
+ logError(`\n❌ CRITICAL ISSUES FOUND (${blockFindings.length}):\n`);
162
172
  for (const finding of blockFindings) {
163
173
  const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
164
- logError(
165
- `${finding.ruleId} [${finding.severity}/${finding.confidence}] ${relative}:${finding.line} ${finding.message}`
166
- );
167
- logError(` ${finding.snippet}`);
174
+ logError(` • ${finding.ruleId.toUpperCase()}`);
175
+ logError(` File: ${relative}:${finding.line}`);
176
+ logError(` Issue: ${finding.message}`);
177
+ // console.log(` Code: ${finding.snippet}`);
178
+ logError("");
168
179
  }
169
180
  }
170
181
 
171
182
  if (warnFindings.length > 0) {
172
- logWarn(`Baseline warnings (${warnFindings.length}):`);
173
- for (const finding of warnFindings) {
174
- const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
175
- logWarn(
176
- `${finding.ruleId} [${finding.severity}/${finding.confidence}] ${relative}:${finding.line} ${finding.message}`
177
- );
178
- logWarn(` ${finding.snippet}`);
183
+ // Filter to show only HIGH risk warnings
184
+ const highRiskWarnings = warnFindings.filter(f => f.confidence === "high");
185
+ if (highRiskWarnings.length > 0) {
186
+ logWarn(`\n⚠️ HIGH RISK WARNINGS (${highRiskWarnings.length}):\n`);
187
+ for (const finding of highRiskWarnings) {
188
+ const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
189
+ logWarn(` ${finding.ruleId.toUpperCase()}`);
190
+ logWarn(` File: ${relative}:${finding.line}`);
191
+ logWarn(` Issue: ${finding.message}`);
192
+ // console.log(` Code: ${finding.snippet}`);
193
+ logWarn("");
194
+ }
179
195
  }
196
+ // comment out low risk warnings
197
+ // const lowRiskWarnings = warnFindings.filter(f => f.confidence !== "high");
198
+ // if (lowRiskWarnings.length > 0) {
199
+ // logWarn(`Baseline warnings (${lowRiskWarnings.length}):`);
200
+ // for (const finding of lowRiskWarnings) {
201
+ // const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
202
+ // logWarn(
203
+ // `${finding.ruleId} [${finding.severity}/${finding.confidence}] ${relative}:${finding.line} ${finding.message}`
204
+ // );
205
+ // logWarn(` ${finding.snippet}`);
206
+ // }
207
+ // }
180
208
  }
181
209
 
182
210
  if (aiReviewed.length > 0) {
183
- logWarn(`AI-reviewed findings (${aiReviewed.length}):`);
211
+ logWarn(`\n🤖 AI-REVIEWED FINDINGS (${aiReviewed.length}):\n`);
184
212
  for (const entry of aiReviewed) {
185
213
  const { finding, decision } = entry;
186
214
  const relative = path.relative(gitRoot, finding.filePath) || finding.filePath;
187
- logWarn(
188
- `${finding.ruleId} [${decision.verdict}/${decision.confidence.toFixed(2)}] ${relative}:${finding.line} ${decision.explanation}`
189
- );
215
+ const verdict = decision.verdict === "block" ? "BLOCKED" : "WARNING";
216
+ logWarn(` • ${finding.ruleId.toUpperCase()} [${verdict}]`);
217
+ logWarn(` File: ${relative}:${finding.line}`);
218
+ logWarn(` Analysis: ${decision.explanation}`);
190
219
  if (decision.suggestedFix) {
191
- logWarn(` Suggested fix: ${decision.suggestedFix}`);
220
+ logWarn(` Fix: ${decision.suggestedFix}`);
192
221
  }
222
+ logWarn("");
193
223
  }
194
224
  }
195
225
 
@@ -18,9 +18,13 @@ export function reportFeatureDisabled(name, verbose, logInfo) {
18
18
  logInfo(`${name} disabled by feature flag.`);
19
19
  }
20
20
 
21
- export function withFailOpenReporting(action, onError) {
21
+ export async function withFailOpenReporting(action, onError) {
22
22
  try {
23
- return action();
23
+ const result = action();
24
+ if (result && typeof result.then === 'function') {
25
+ return await result;
26
+ }
27
+ return result;
24
28
  } catch {
25
29
  if (onError) {
26
30
  onError();
@@ -29,21 +33,24 @@ export function withFailOpenReporting(action, onError) {
29
33
  }
30
34
  }
31
35
 
32
- export function withFailOpenIntegration(action) {
36
+ export async function withFailOpenIntegration(action) {
33
37
  try {
34
- action();
38
+ const result = action();
39
+ if (result && typeof result.then === 'function') {
40
+ await result;
41
+ }
35
42
  } catch {
36
43
  // Integration failures are ignored to avoid affecting commits.
37
44
  }
38
45
  }
39
46
 
40
- export function withFailOpenAiEscalation(enabled, action) {
47
+ export async function withFailOpenAiEscalation(enabled, action) {
41
48
  if (!enabled) {
42
49
  return [];
43
50
  }
44
51
 
45
52
  try {
46
- return action();
53
+ return await action();
47
54
  } catch {
48
55
  // AI failures downgrade to warnings by returning no decisions.
49
56
  return [];
@@ -1,13 +1,23 @@
1
+ import http from "http";
2
+ import https from "https";
3
+ import { logWarn } from "../utils/logger.js";
4
+
1
5
  // AI contextual analysis layer. Only low-confidence findings reach this stage.
2
6
  // Regex-first keeps the fast baseline deterministic; AI is a cautious fallback.
3
7
 
4
- function callModel(payload) {
5
- void payload;
6
- // Stubbed: No provider hardcoded. Return null to trigger safe fallback.
7
- return null;
8
+ const DEFAULT_TIMEOUT_MS = 5000;
9
+
10
+ function getAiConfig() {
11
+ const apiUrl = process.env.AI_API_URL || "";
12
+ const timeoutMs = Number(process.env.AI_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
13
+ return { apiUrl, timeoutMs };
8
14
  }
9
15
 
10
- function fallbackDecision(finding) {
16
+ function fallbackDecision(finding, reason) {
17
+ if (reason) {
18
+ logWarn(`AI escalation failed: ${reason}`);
19
+ }
20
+
11
21
  return {
12
22
  findingId: finding.findingId,
13
23
  verdict: "warn",
@@ -17,33 +27,115 @@ function fallbackDecision(finding) {
17
27
  };
18
28
  }
19
29
 
20
- export function analyze(findings, projectContext) {
21
- const payload = {
22
- version: 1,
23
- projectContext,
24
- findings: findings.map((finding) => ({
25
- findingId: finding.findingId,
26
- ruleId: finding.ruleId,
27
- filePath: finding.filePath,
28
- fileType: finding.fileType,
29
- isTestLike: finding.isTestLike,
30
- snippet: finding.snippet
31
- }))
30
+ function postJsonWithTimeout({ url, payload, timeoutMs }) {
31
+ return new Promise((resolve, reject) => {
32
+ let parsedUrl;
33
+ try {
34
+ parsedUrl = new URL(url);
35
+ } catch {
36
+ reject(new Error("Invalid AI_API_URL"));
37
+ return;
38
+ }
39
+
40
+ const data = JSON.stringify(payload);
41
+ const transport = parsedUrl.protocol === "http:" ? http : https;
42
+
43
+ const request = transport.request(
44
+ {
45
+ method: "POST",
46
+ hostname: parsedUrl.hostname,
47
+ port: parsedUrl.port || (parsedUrl.protocol === "http:" ? 80 : 443),
48
+ path: `${parsedUrl.pathname}${parsedUrl.search}`,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "Content-Length": Buffer.byteLength(data)
52
+ },
53
+ timeout: timeoutMs
54
+ },
55
+ (res) => {
56
+ let body = "";
57
+ res.setEncoding("utf8");
58
+ res.on("data", (chunk) => {
59
+ body += chunk;
60
+ });
61
+ res.on("end", () => {
62
+ if (res.statusCode && res.statusCode >= 400) {
63
+ reject(new Error(`AI API responded with ${res.statusCode}`));
64
+ return;
65
+ }
66
+ try {
67
+ resolve(JSON.parse(body));
68
+ } catch {
69
+ reject(new Error("AI API returned invalid JSON"));
70
+ }
71
+ });
72
+ }
73
+ );
74
+
75
+ request.on("timeout", () => {
76
+ request.destroy(new Error("AI request timed out"));
77
+ });
78
+
79
+ request.on("error", (err) => {
80
+ reject(err);
81
+ });
82
+
83
+ request.write(data);
84
+ request.end();
85
+ });
86
+ }
87
+
88
+ async function callModel(finding) {
89
+ const { apiUrl, timeoutMs } = getAiConfig();
90
+
91
+ if (!apiUrl) {
92
+ throw new Error("AI_API_URL is not configured");
93
+ }
94
+
95
+ const response = await postJsonWithTimeout({
96
+ url: apiUrl,
97
+ timeoutMs,
98
+ payload: {
99
+ code: finding.snippet || ""
100
+ }
101
+ });
102
+
103
+ if (!response || typeof response.found !== "boolean") {
104
+ throw new Error("AI API returned invalid payload");
105
+ }
106
+
107
+ const verdict = response.found && response.risk === "Critical" ? "block" : "warn";
108
+ const confidence = response.found ? 0.85 : 0.35;
109
+ const explanation = response.found
110
+ ? `AI detected ${response.risk} risk: ${response.secret || "secret value"}`
111
+ : "AI did not detect risk in this code snippet.";
112
+
113
+ return {
114
+ findingId: finding.findingId,
115
+ verdict,
116
+ confidence,
117
+ explanation,
118
+ suggestedFix: response.found ? "Move secrets to environment variables." : undefined
32
119
  };
120
+ }
33
121
 
34
- const response = callModel(payload);
122
+ export async function analyze(findings, projectContext) {
123
+ void projectContext;
35
124
 
36
- if (!response || !Array.isArray(response.decisions)) {
37
- return findings.map(fallbackDecision);
125
+ if (!Array.isArray(findings) || findings.length === 0) {
126
+ return [];
38
127
  }
39
128
 
40
- return response.decisions.map((decision) => ({
41
- findingId: decision.findingId,
42
- verdict: decision.verdict || "warn",
43
- confidence: typeof decision.confidence === "number" ? decision.confidence : 0.5,
44
- explanation: decision.explanation || "AI decision provided.",
45
- suggestedFix: decision.suggestedFix
46
- }));
129
+ return Promise.all(
130
+ findings.map(async (finding) => {
131
+ try {
132
+ return await callModel(finding);
133
+ } catch (error) {
134
+ const reason = error instanceof Error ? error.message : "AI call failed";
135
+ return fallbackDecision(finding, reason);
136
+ }
137
+ })
138
+ );
47
139
  }
48
140
 
49
141
 
@@ -18,14 +18,14 @@ function getHookBlock() {
18
18
  "fi",
19
19
  "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
20
20
  "if [ -f \"$CONFIG_PATH\" ]; then",
21
- " ENFORCEMENT=$(node -e \"const fs=require('fs');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
21
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');const path=process.argv[1];try{const c=JSON.parse(fs.readFileSync(path,'utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\" \"$CONFIG_PATH\")",
22
22
  " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
23
23
  " echo \"CodeProof enforcement is temporarily disabled.\"",
24
24
  " echo \"Commit allowed.\"",
25
25
  " exit 0",
26
26
  " fi",
27
27
  "fi",
28
- "CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
28
+ "CODEPROOF_PRECOMMIT=1 npx codeproof run --precommit",
29
29
  "RESULT=$?",
30
30
  "if [ $RESULT -ne 0 ]; then",
31
31
  " echo \"CodeProof checks failed. Commit blocked.\"",
@@ -55,14 +55,14 @@ export function installPreCommitHook(gitRoot) {
55
55
  "fi",
56
56
  "CONFIG_PATH=\"$GIT_ROOT/codeproof.config.json\"",
57
57
  "if [ -f \"$CONFIG_PATH\" ]; then",
58
- " ENFORCEMENT=$(node -e \"const fs=require('fs');try{const c=JSON.parse(fs.readFileSync('$CONFIG_PATH','utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\")",
58
+ " ENFORCEMENT=$(node -e \"const fs=require('fs');const path=process.argv[1];try{const c=JSON.parse(fs.readFileSync(path,'utf8'));console.log((c.enforcement||'enabled').toLowerCase());}catch(e){console.log('enabled');}\" \"$CONFIG_PATH\")",
59
59
  " if [ \"$ENFORCEMENT\" = \"disabled\" ]; then",
60
60
  " echo \"CodeProof enforcement is temporarily disabled.\"",
61
61
  " echo \"Commit allowed.\"",
62
62
  " exit 0",
63
63
  " fi",
64
64
  "fi",
65
- "CODEPROOF_PRECOMMIT=1 codeproof run --precommit",
65
+ "CODEPROOF_PRECOMMIT=1 npx codeproof run --precommit",
66
66
  "RESULT=$?",
67
67
  "if [ $RESULT -ne 0 ]; then",
68
68
  " echo \"CodeProof checks failed. Commit blocked.\"",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeproof",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "CodeProof CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,8 @@ function normalizeSeverity(value) {
46
46
  export function buildReport({
47
47
  projectRoot,
48
48
  projectId,
49
+ projectName,
50
+ repoIdentifier,
49
51
  clientId,
50
52
  reportId,
51
53
  scanMode,
@@ -85,17 +87,24 @@ export function buildReport({
85
87
  ? "allowed_with_warnings"
86
88
  : "allowed";
87
89
 
90
+ // Server-compatible format
88
91
  return {
89
- reportId,
90
- timestamp,
91
92
  projectId,
92
93
  clientId,
93
- scanMode,
94
- summary: {
95
- totalFilesScanned: filesScannedCount,
96
- totalFindings: findings.length,
97
- blocksCount,
98
- warningsCount
94
+ project: {
95
+ name: projectName || "Unknown Project",
96
+ repoIdentifier: repoIdentifier || projectRoot
97
+ },
98
+ report: {
99
+ timestamp,
100
+ scanMode,
101
+ summary: {
102
+ filesScanned: filesScannedCount,
103
+ findings: findings.length,
104
+ blocks: blocksCount,
105
+ warnings: warningsCount,
106
+ finalVerdict
107
+ }
99
108
  },
100
109
  findings,
101
110
  finalVerdict
@@ -44,6 +44,22 @@ export function writeReport({ projectRoot, report }) {
44
44
  const reportDir = getReportDir(projectRoot);
45
45
  ensureReportDir(reportDir);
46
46
 
47
+ // Cleanup old temp files before creating new ones
48
+ try {
49
+ const files = fs.readdirSync(reportDir);
50
+ for (const file of files) {
51
+ if (file.startsWith('.tmp-')) {
52
+ try {
53
+ fs.unlinkSync(path.join(reportDir, file));
54
+ } catch {
55
+ // Ignore errors on cleanup
56
+ }
57
+ }
58
+ }
59
+ } catch {
60
+ // Ignore cleanup errors
61
+ }
62
+
47
63
  // Per-run JSON keeps every audit entry immutable and easy to archive.
48
64
  let reportNumber = getNextReportNumber(reportDir);
49
65
  let reportPath = path.join(reportDir, `${REPORT_PREFIX}${reportNumber}${REPORT_SUFFIX}`);
@@ -55,8 +71,21 @@ export function writeReport({ projectRoot, report }) {
55
71
  // Use numeric sequencing over timestamps to avoid collisions in fast CI runs.
56
72
  const tempPath = path.join(reportDir, `.tmp-${process.pid}-${Date.now()}.json`);
57
73
  const payload = JSON.stringify(report, null, 2) + "\n";
58
- fs.writeFileSync(tempPath, payload, "utf8");
59
- fs.renameSync(tempPath, reportPath);
74
+
75
+ try {
76
+ fs.writeFileSync(tempPath, payload, "utf8");
77
+ fs.renameSync(tempPath, reportPath);
78
+ } catch (err) {
79
+ // Cleanup temp file on error
80
+ try {
81
+ if (fs.existsSync(tempPath)) {
82
+ fs.unlinkSync(tempPath);
83
+ }
84
+ } catch {
85
+ // Ignore cleanup errors
86
+ }
87
+ throw err;
88
+ }
60
89
 
61
90
  return reportPath;
62
91
  }
@@ -4,11 +4,17 @@ import https from "https";
4
4
  // Boundary: integration layer only. Must not import CLI, rule engine, or reporting.
5
5
  // Network calls are fail-open to avoid impacting commits or developer flow.
6
6
 
7
- const DEFAULT_ENDPOINT = "https://api.codeproof.dev/report";
7
+ const DEFAULT_ENDPOINT = "http://127.0.0.1:4000/api/reports";
8
8
 
9
- export function sendReportToServer(report, options = {}) {
9
+ export async function sendReportToServer(report, options = {}) {
10
10
  const enabled = Boolean(options.enabled);
11
+
12
+ // console.log("[API Client] sendReportToServer called");
13
+ // console.log("[API Client] enabled:", enabled);
14
+ // console.log("[API Client] options:", JSON.stringify(options, null, 2));
15
+
11
16
  if (!enabled) {
17
+ // console.log("[API Client] Integration disabled, skipping");
12
18
  return;
13
19
  }
14
20
 
@@ -16,41 +22,75 @@ export function sendReportToServer(report, options = {}) {
16
22
  ? options.endpointUrl.trim()
17
23
  : DEFAULT_ENDPOINT;
18
24
 
19
- try {
20
- const url = new URL(endpointUrl);
21
- const payload = JSON.stringify(report);
22
- const transport = url.protocol === "http:" ? http : https;
23
-
24
- const request = transport.request(
25
- {
26
- method: "POST",
27
- hostname: url.hostname,
28
- port: url.port || (url.protocol === "http:" ? 80 : 443),
29
- path: `${url.pathname}${url.search}`,
30
- headers: {
31
- "Content-Type": "application/json",
32
- "Content-Length": Buffer.byteLength(payload)
25
+ // console.log("[API Client] Target endpoint:", endpointUrl);
26
+ // console.log("[API Client] Report summary:", {
27
+ // projectId: report.projectId,
28
+ // clientId: report.clientId,
29
+ // findingsCount: report.findings?.length || 0
30
+ // });
31
+
32
+ return new Promise((resolve) => {
33
+ try {
34
+ const url = new URL(endpointUrl);
35
+ const payload = JSON.stringify(report);
36
+ const transport = url.protocol === "http:" ? http : https;
37
+
38
+ // console.log("[API Client] URL parsed - protocol:", url.protocol, "hostname:", url.hostname, "port:", url.port, "pathname:", url.pathname);
39
+ // console.log("[API Client] Payload size:", Buffer.byteLength(payload), "bytes");
40
+ // console.log("[API Client] Payload preview:", payload.substring(0, 200) + "...");
41
+
42
+ const portNumber = url.port ? parseInt(url.port, 10) : (url.protocol === "http:" ? 80 : 443);
43
+
44
+ // console.log("[API Client] Sending POST to:", `${url.protocol}//${url.hostname}:${portNumber}${url.pathname}`);
45
+
46
+ const request = transport.request(
47
+ {
48
+ method: "POST",
49
+ hostname: url.hostname,
50
+ port: portNumber,
51
+ path: `${url.pathname}${url.search}`,
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Content-Length": Buffer.byteLength(payload)
55
+ },
56
+ timeout: 5000
33
57
  },
34
- timeout: 2000
35
- },
36
- (res) => {
37
- // UX: fail-open integrations never read or store server responses.
38
- res.resume();
39
- }
40
- );
41
-
42
- request.on("timeout", () => {
43
- // Integrations are fail-open: timeout should not block or throw.
44
- request.destroy();
45
- });
46
-
47
- request.on("error", () => {
48
- // Integrations are fail-open: network errors are ignored silently.
49
- });
50
-
51
- request.write(payload);
52
- request.end();
53
- } catch {
54
- // Integrations are fail-open: invalid URLs or serialization issues are ignored silently.
55
- }
58
+ (res) => {
59
+ // console.log("[API Client] Response received:", res.statusCode);
60
+ let body = "";
61
+ res.on("data", (chunk) => {
62
+ body += chunk;
63
+ });
64
+ res.on("end", () => {
65
+ // console.log("[API Client] Response body:", body);
66
+ if (res.statusCode === 201) {
67
+ // Report sent successfully
68
+ } else {
69
+ console.error("[API Client] Server returned status:", res.statusCode);
70
+ }
71
+ resolve();
72
+ });
73
+ res.resume();
74
+ }
75
+ );
76
+
77
+ request.on("timeout", () => {
78
+ console.error("[API Client] Request timeout");
79
+ request.destroy();
80
+ resolve();
81
+ });
82
+
83
+ request.on("error", (err) => {
84
+ console.error("[API Client] Request error:", err.message);
85
+ resolve();
86
+ });
87
+
88
+ request.write(payload);
89
+ request.end();
90
+ // console.log("[API Client] Request sent");
91
+ } catch (err) {
92
+ console.error("[API Client] Exception:", err.message);
93
+ resolve();
94
+ }
95
+ });
56
96
  }
package/utils/git.js CHANGED
@@ -44,3 +44,20 @@ export function getStagedFiles(cwd) {
44
44
  .map((line) => line.trim())
45
45
  .filter(Boolean);
46
46
  }
47
+
48
+ export function getRepoIdentifier(gitRoot) {
49
+ try {
50
+ const result = runGit(["config", "--get", "remote.origin.url"], gitRoot);
51
+ if (result.status === 0) {
52
+ return String(result.stdout).trim() || gitRoot;
53
+ }
54
+ } catch {
55
+ // Fallback to directory name
56
+ }
57
+ return gitRoot;
58
+ }
59
+
60
+ export function getProjectName(gitRoot) {
61
+ const parts = gitRoot.split(/[\\/]/);
62
+ return parts[parts.length - 1] || "Unknown";
63
+ }