@synkro-sh/cli 1.3.20 → 1.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +234 -63
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -2495,6 +2495,14 @@ var init_workflowTemplate = __esm({
|
|
|
2495
2495
|
on:
|
|
2496
2496
|
pull_request:
|
|
2497
2497
|
types: [opened, synchronize, reopened]
|
|
2498
|
+
workflow_dispatch:
|
|
2499
|
+
inputs:
|
|
2500
|
+
pr_number:
|
|
2501
|
+
description: PR number to scan
|
|
2502
|
+
required: true
|
|
2503
|
+
sha:
|
|
2504
|
+
description: Commit SHA to scan
|
|
2505
|
+
required: true
|
|
2498
2506
|
|
|
2499
2507
|
jobs:
|
|
2500
2508
|
scan:
|
|
@@ -2507,6 +2515,7 @@ jobs:
|
|
|
2507
2515
|
- uses: actions/checkout@v4
|
|
2508
2516
|
with:
|
|
2509
2517
|
fetch-depth: 0
|
|
2518
|
+
ref: \${{ inputs.sha || github.event.pull_request.head.sha }}
|
|
2510
2519
|
|
|
2511
2520
|
- name: Cache npm globals
|
|
2512
2521
|
id: cache-npm-global
|
|
@@ -2527,9 +2536,9 @@ jobs:
|
|
|
2527
2536
|
CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
2528
2537
|
SYNKRO_API_KEY: \${{ secrets.SYNKRO_API_KEY }}
|
|
2529
2538
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2530
|
-
SYNKRO_PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2539
|
+
SYNKRO_PR_NUMBER: \${{ inputs.pr_number || github.event.pull_request.number }}
|
|
2531
2540
|
SYNKRO_REPO: \${{ github.repository }}
|
|
2532
|
-
SYNKRO_SHA: \${{ github.event.pull_request.head.sha }}
|
|
2541
|
+
SYNKRO_SHA: \${{ inputs.sha || github.event.pull_request.head.sha }}
|
|
2533
2542
|
SYNKRO_GATEWAY_URL: \${{ vars.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh' }}
|
|
2534
2543
|
`;
|
|
2535
2544
|
WORKFLOW_PATH = ".github/workflows/synkro.yml";
|
|
@@ -2721,14 +2730,14 @@ function waitForGithubToken() {
|
|
|
2721
2730
|
});
|
|
2722
2731
|
}
|
|
2723
2732
|
function openBrowser2(url) {
|
|
2724
|
-
const { execFile:
|
|
2733
|
+
const { execFile: execFile3 } = __require("child_process");
|
|
2725
2734
|
const plat = process.platform;
|
|
2726
2735
|
const cb = (err) => {
|
|
2727
2736
|
if (err) console.log(` Open this URL manually: ${url}`);
|
|
2728
2737
|
};
|
|
2729
|
-
if (plat === "darwin")
|
|
2730
|
-
else if (plat === "win32")
|
|
2731
|
-
else
|
|
2738
|
+
if (plat === "darwin") execFile3("open", [url], cb);
|
|
2739
|
+
else if (plat === "win32") execFile3("cmd", ["/c", "start", "", url], cb);
|
|
2740
|
+
else execFile3("xdg-open", [url], cb);
|
|
2732
2741
|
}
|
|
2733
2742
|
async function connectGithubAndSelectRepos() {
|
|
2734
2743
|
const url = `${SYNKRO_WEB_AUTH_URL2}/cli-github?port=${GITHUB_PORT}`;
|
|
@@ -2862,14 +2871,16 @@ var init_repoConnect = __esm({
|
|
|
2862
2871
|
// cli/commands/setupGithub.ts
|
|
2863
2872
|
var setupGithub_exports = {};
|
|
2864
2873
|
__export(setupGithub_exports, {
|
|
2874
|
+
connectGitHub: () => connectGitHub,
|
|
2865
2875
|
setupGithubCommand: () => setupGithubCommand
|
|
2866
2876
|
});
|
|
2867
2877
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2868
2878
|
import { stdin as input, stdout as output } from "process";
|
|
2869
2879
|
import { execSync as execSync3, spawn as nodeSpawn } from "child_process";
|
|
2870
2880
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
2871
|
-
import { homedir as homedir4 } from "os";
|
|
2881
|
+
import { homedir as homedir4, platform as platform2 } from "os";
|
|
2872
2882
|
import { join as join5 } from "path";
|
|
2883
|
+
import { execFile as execFile2 } from "child_process";
|
|
2873
2884
|
function readConfig() {
|
|
2874
2885
|
if (!existsSync6(CONFIG_PATH)) return {};
|
|
2875
2886
|
const out = {};
|
|
@@ -2897,9 +2908,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
2897
2908
|
resolve2(chunk);
|
|
2898
2909
|
return;
|
|
2899
2910
|
}
|
|
2900
|
-
if (s === "")
|
|
2901
|
-
process.exit(130);
|
|
2902
|
-
}
|
|
2911
|
+
if (s === "") process.exit(130);
|
|
2903
2912
|
if (s === "\x7F" || s === "\b") {
|
|
2904
2913
|
chunk = chunk.slice(0, -1);
|
|
2905
2914
|
return;
|
|
@@ -2911,6 +2920,30 @@ async function prompt(rl, q, opts = {}) {
|
|
|
2911
2920
|
}
|
|
2912
2921
|
return await rl.question(q);
|
|
2913
2922
|
}
|
|
2923
|
+
function openBrowser3(url) {
|
|
2924
|
+
const os = platform2();
|
|
2925
|
+
let bin;
|
|
2926
|
+
let args2;
|
|
2927
|
+
switch (os) {
|
|
2928
|
+
case "darwin":
|
|
2929
|
+
bin = "open";
|
|
2930
|
+
args2 = [url];
|
|
2931
|
+
break;
|
|
2932
|
+
case "win32":
|
|
2933
|
+
bin = "cmd";
|
|
2934
|
+
args2 = ["/c", "start", "", url];
|
|
2935
|
+
break;
|
|
2936
|
+
default:
|
|
2937
|
+
bin = "xdg-open";
|
|
2938
|
+
args2 = [url];
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
execFile2(bin, args2, () => {
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
function sleep(ms) {
|
|
2945
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2946
|
+
}
|
|
2914
2947
|
function captureClaudeSetupToken() {
|
|
2915
2948
|
return new Promise((resolve2, reject) => {
|
|
2916
2949
|
const proc = nodeSpawn("script", ["-q", "/dev/null", "claude", "setup-token"], {
|
|
@@ -2936,6 +2969,68 @@ function captureClaudeSetupToken() {
|
|
|
2936
2969
|
});
|
|
2937
2970
|
});
|
|
2938
2971
|
}
|
|
2972
|
+
async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
|
|
2973
|
+
const resp = await fetch(`${gatewayUrl}${path}`, {
|
|
2974
|
+
...opts,
|
|
2975
|
+
headers: {
|
|
2976
|
+
"Authorization": `Bearer ${jwt2}`,
|
|
2977
|
+
"Content-Type": "application/json",
|
|
2978
|
+
...opts.headers || {}
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
if (!resp.ok) {
|
|
2982
|
+
const text = await resp.text().catch(() => "");
|
|
2983
|
+
throw new Error(`API ${resp.status}: ${text.slice(0, 200)}`);
|
|
2984
|
+
}
|
|
2985
|
+
return resp.json();
|
|
2986
|
+
}
|
|
2987
|
+
async function connectGitHub(gatewayUrl, jwt2, opts = {}) {
|
|
2988
|
+
try {
|
|
2989
|
+
const result = await apiCall(
|
|
2990
|
+
gatewayUrl,
|
|
2991
|
+
jwt2,
|
|
2992
|
+
"/api/v1/cli/github-token"
|
|
2993
|
+
);
|
|
2994
|
+
if (result.connected && result.token) {
|
|
2995
|
+
if (!opts.silent) console.log(" \u2713 GitHub already connected via Synkro.");
|
|
2996
|
+
return result.token;
|
|
2997
|
+
}
|
|
2998
|
+
} catch {
|
|
2999
|
+
}
|
|
3000
|
+
if (!opts.silent) console.log(" Opening browser to authorize GitHub...");
|
|
3001
|
+
try {
|
|
3002
|
+
const authResp = await apiCall(
|
|
3003
|
+
gatewayUrl,
|
|
3004
|
+
jwt2,
|
|
3005
|
+
"/api/pipes-widget/authorize/github",
|
|
3006
|
+
{ method: "POST", body: "{}" }
|
|
3007
|
+
);
|
|
3008
|
+
openBrowser3(authResp.url);
|
|
3009
|
+
if (!opts.silent) console.log(" Waiting for authorization...");
|
|
3010
|
+
} catch (err) {
|
|
3011
|
+
if (!opts.silent) console.error(` Failed to start GitHub authorization: ${err.message}`);
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
const deadline = Date.now() + 12e4;
|
|
3015
|
+
while (Date.now() < deadline) {
|
|
3016
|
+
await sleep(2e3);
|
|
3017
|
+
try {
|
|
3018
|
+
const result = await apiCall(
|
|
3019
|
+
gatewayUrl,
|
|
3020
|
+
jwt2,
|
|
3021
|
+
"/api/v1/cli/github-token"
|
|
3022
|
+
);
|
|
3023
|
+
if (result.connected && result.token) {
|
|
3024
|
+
if (!opts.silent) console.log("\n \u2713 GitHub connected!");
|
|
3025
|
+
return result.token;
|
|
3026
|
+
}
|
|
3027
|
+
} catch {
|
|
3028
|
+
}
|
|
3029
|
+
if (!opts.silent) process.stdout.write(".");
|
|
3030
|
+
}
|
|
3031
|
+
if (!opts.silent) console.error("\n Timed out waiting for GitHub authorization.");
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
2939
3034
|
async function setupGithubCommand(opts = {}) {
|
|
2940
3035
|
if (!isAuthenticated()) {
|
|
2941
3036
|
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
@@ -2951,20 +3046,12 @@ async function setupGithubCommand(opts = {}) {
|
|
|
2951
3046
|
console.log("Requesting CI API key from Synkro...");
|
|
2952
3047
|
let synkroCiApiKey;
|
|
2953
3048
|
try {
|
|
2954
|
-
const
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
body: "{}"
|
|
2961
|
-
});
|
|
2962
|
-
if (!resp.ok) {
|
|
2963
|
-
const errText = await resp.text().catch(() => "");
|
|
2964
|
-
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
2965
|
-
process.exit(1);
|
|
2966
|
-
}
|
|
2967
|
-
const minted = await resp.json();
|
|
3049
|
+
const minted = await apiCall(
|
|
3050
|
+
gatewayUrl,
|
|
3051
|
+
jwt2,
|
|
3052
|
+
"/api/v1/cli/ci-api-key",
|
|
3053
|
+
{ method: "POST", body: "{}" }
|
|
3054
|
+
);
|
|
2968
3055
|
synkroCiApiKey = minted.api_key;
|
|
2969
3056
|
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
2970
3057
|
} catch (err) {
|
|
@@ -2976,24 +3063,33 @@ async function setupGithubCommand(opts = {}) {
|
|
|
2976
3063
|
ghToken = opts.githubToken;
|
|
2977
3064
|
} else if (opts.nonInteractive) {
|
|
2978
3065
|
try {
|
|
2979
|
-
|
|
3066
|
+
const result = await apiCall(
|
|
3067
|
+
gatewayUrl,
|
|
3068
|
+
jwt2,
|
|
3069
|
+
"/api/v1/cli/github-token"
|
|
3070
|
+
);
|
|
3071
|
+
if (result.connected && result.token) {
|
|
3072
|
+
ghToken = result.token;
|
|
3073
|
+
} else {
|
|
3074
|
+
throw new Error("not connected");
|
|
3075
|
+
}
|
|
2980
3076
|
} catch {
|
|
2981
|
-
|
|
2982
|
-
|
|
3077
|
+
try {
|
|
3078
|
+
ghToken = execSync3("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3079
|
+
} catch {
|
|
3080
|
+
console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
2983
3083
|
}
|
|
2984
3084
|
} else {
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
console.log(" \u2022 A GitHub personal access token with `repo` scope");
|
|
2990
|
-
console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
|
|
2991
|
-
ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
|
|
2992
|
-
rl.close();
|
|
2993
|
-
if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
|
|
2994
|
-
console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
|
|
3085
|
+
console.log("\nConnecting to GitHub...");
|
|
3086
|
+
const token = await connectGitHub(gatewayUrl, jwt2);
|
|
3087
|
+
if (!token) {
|
|
3088
|
+
console.error("GitHub connection failed. Try again.");
|
|
2995
3089
|
process.exit(1);
|
|
2996
3090
|
}
|
|
3091
|
+
ghToken = token;
|
|
3092
|
+
console.log();
|
|
2997
3093
|
}
|
|
2998
3094
|
let claudeToken;
|
|
2999
3095
|
if (!opts.skipClaudeToken) {
|
|
@@ -3043,10 +3139,10 @@ async function setupGithubCommand(opts = {}) {
|
|
|
3043
3139
|
selected = [{ owner, repo, full_name: currentFullName }];
|
|
3044
3140
|
console.log(` Auto-selected repo: ${currentFullName}`);
|
|
3045
3141
|
} else {
|
|
3046
|
-
console.log("
|
|
3142
|
+
console.log("Fetching accessible repos...");
|
|
3047
3143
|
const repos = await listAccessibleRepos({ token: ghToken });
|
|
3048
3144
|
if (repos.length === 0) {
|
|
3049
|
-
console.error("No accessible repos found.
|
|
3145
|
+
console.error("No accessible repos found. Check your GitHub permissions.");
|
|
3050
3146
|
process.exit(1);
|
|
3051
3147
|
}
|
|
3052
3148
|
console.log(`
|
|
@@ -3148,7 +3244,6 @@ function parseArgs(argv) {
|
|
|
3148
3244
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
3149
3245
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
3150
3246
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
3151
|
-
else if (a === "--pr-scan") opts.prScan = true;
|
|
3152
3247
|
}
|
|
3153
3248
|
if (!opts.gatewayUrl) {
|
|
3154
3249
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
@@ -3238,7 +3333,7 @@ function writeConfigEnv(opts) {
|
|
|
3238
3333
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3239
3334
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3240
3335
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3241
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3336
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.22")}`
|
|
3242
3337
|
];
|
|
3243
3338
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3244
3339
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -3258,10 +3353,48 @@ function collectLocalMetadata() {
|
|
|
3258
3353
|
}
|
|
3259
3354
|
try {
|
|
3260
3355
|
const remote = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
3261
|
-
const
|
|
3356
|
+
const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
3357
|
+
const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
3358
|
+
const m = sshMatch || httpMatch;
|
|
3262
3359
|
if (m) meta.active_repo = m[1];
|
|
3263
3360
|
} catch {
|
|
3264
3361
|
}
|
|
3362
|
+
try {
|
|
3363
|
+
meta.cc_version = execSync4("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
3364
|
+
} catch {
|
|
3365
|
+
}
|
|
3366
|
+
const claudeDir = join6(homedir5(), ".claude");
|
|
3367
|
+
try {
|
|
3368
|
+
const settings = JSON.parse(readFileSync5(join6(claudeDir, "settings.json"), "utf-8"));
|
|
3369
|
+
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
3370
|
+
if (plugins.length) meta.enabled_plugins = plugins;
|
|
3371
|
+
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
try {
|
|
3375
|
+
const mcpCache = JSON.parse(readFileSync5(join6(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
3376
|
+
const mcpNames = Object.keys(mcpCache);
|
|
3377
|
+
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
3378
|
+
} catch {
|
|
3379
|
+
}
|
|
3380
|
+
try {
|
|
3381
|
+
const mcpList = execSync4("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
|
|
3382
|
+
const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
|
|
3383
|
+
if (connected.length) meta.mcp_servers_connected = connected;
|
|
3384
|
+
} catch {
|
|
3385
|
+
}
|
|
3386
|
+
try {
|
|
3387
|
+
const sessionsDir = join6(claudeDir, "sessions");
|
|
3388
|
+
const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
3389
|
+
for (const f of files) {
|
|
3390
|
+
const s = JSON.parse(readFileSync5(join6(sessionsDir, f), "utf-8"));
|
|
3391
|
+
if (s.version) {
|
|
3392
|
+
meta.cc_version = meta.cc_version || s.version;
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
} catch {
|
|
3397
|
+
}
|
|
3265
3398
|
return meta;
|
|
3266
3399
|
}
|
|
3267
3400
|
async function fetchUserProfile(gatewayUrl, token) {
|
|
@@ -3398,6 +3531,13 @@ async function installCommand(opts = {}) {
|
|
|
3398
3531
|
console.error("No access token available after auth.");
|
|
3399
3532
|
process.exit(1);
|
|
3400
3533
|
}
|
|
3534
|
+
console.log("\nConnecting to GitHub...");
|
|
3535
|
+
const ghToken = await connectGitHub(gatewayUrl, token);
|
|
3536
|
+
if (!ghToken) {
|
|
3537
|
+
console.error("GitHub connection is required. Re-run `synkro-cli install` to try again.");
|
|
3538
|
+
process.exit(1);
|
|
3539
|
+
}
|
|
3540
|
+
console.log();
|
|
3401
3541
|
setApiBaseUrl(`${gatewayUrl}/api`);
|
|
3402
3542
|
await promptRepoConnection({ linkRepo: opts.linkRepo });
|
|
3403
3543
|
const agents = detectAgents();
|
|
@@ -3535,18 +3675,9 @@ async function installCommand(opts = {}) {
|
|
|
3535
3675
|
`);
|
|
3536
3676
|
}
|
|
3537
3677
|
}
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
3541
|
-
await setupGithubCommand2({ nonInteractive: true });
|
|
3542
|
-
}
|
|
3678
|
+
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
3679
|
+
await setupGithubCommand2({ nonInteractive: true, githubToken: ghToken });
|
|
3543
3680
|
console.log("\u2713 Synkro installed.");
|
|
3544
|
-
console.log();
|
|
3545
|
-
if (!opts.prScan) {
|
|
3546
|
-
console.log("Next steps:");
|
|
3547
|
-
console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
|
|
3548
|
-
console.log(" \u2022 synkro-cli status (check what is configured)");
|
|
3549
|
-
}
|
|
3550
3681
|
}
|
|
3551
3682
|
function detectGitRepo2() {
|
|
3552
3683
|
try {
|
|
@@ -3751,6 +3882,7 @@ var init_install = __esm({
|
|
|
3751
3882
|
init_stub();
|
|
3752
3883
|
init_repoConnect();
|
|
3753
3884
|
init_projects();
|
|
3885
|
+
init_setupGithub();
|
|
3754
3886
|
SYNKRO_DIR2 = join6(homedir5(), ".synkro");
|
|
3755
3887
|
HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
|
|
3756
3888
|
BIN_DIR = join6(SYNKRO_DIR2, "bin");
|
|
@@ -4249,6 +4381,19 @@ function getPrFiles(repo, prNumber) {
|
|
|
4249
4381
|
]);
|
|
4250
4382
|
return data;
|
|
4251
4383
|
}
|
|
4384
|
+
async function fetchScanContext(gatewayUrl, apiKey, repo, prNumber, sha) {
|
|
4385
|
+
try {
|
|
4386
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/api/pr-scans/scan-context?repo=${encodeURIComponent(repo)}&pr_number=${prNumber}&sha=${sha}`;
|
|
4387
|
+
const resp = await fetch(url, {
|
|
4388
|
+
headers: { "x-synkro-api-key": apiKey },
|
|
4389
|
+
signal: AbortSignal.timeout(15e3)
|
|
4390
|
+
});
|
|
4391
|
+
if (!resp.ok) return { scan_all: true };
|
|
4392
|
+
return await resp.json();
|
|
4393
|
+
} catch {
|
|
4394
|
+
return { scan_all: true };
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4252
4397
|
function getFileDiffWithLines(file) {
|
|
4253
4398
|
if (!file.patch) return { hunks: "", newFileLineMap: /* @__PURE__ */ new Map() };
|
|
4254
4399
|
const lines = file.patch.split("\n");
|
|
@@ -4343,13 +4488,16 @@ ${hunks}`;
|
|
|
4343
4488
|
});
|
|
4344
4489
|
});
|
|
4345
4490
|
}
|
|
4346
|
-
async function processInBatches(items,
|
|
4347
|
-
const results =
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4491
|
+
async function processInBatches(items, concurrency, fn) {
|
|
4492
|
+
const results = new Array(items.length);
|
|
4493
|
+
let next = 0;
|
|
4494
|
+
async function worker() {
|
|
4495
|
+
while (next < items.length) {
|
|
4496
|
+
const idx = next++;
|
|
4497
|
+
results[idx] = await fn(items[idx], idx, items.length);
|
|
4498
|
+
}
|
|
4352
4499
|
}
|
|
4500
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
4353
4501
|
return results;
|
|
4354
4502
|
}
|
|
4355
4503
|
function buildConsolidationPrompt(findings) {
|
|
@@ -4606,11 +4754,34 @@ async function scanPrCommand() {
|
|
|
4606
4754
|
console.error("Failed to fetch PR files:", err.message);
|
|
4607
4755
|
process.exit(2);
|
|
4608
4756
|
}
|
|
4757
|
+
const scanCtx = await fetchScanContext(gatewayUrl, synkroApiKey, repo, prNumber, sha);
|
|
4758
|
+
if (scanCtx.skip) {
|
|
4759
|
+
console.log(`Already scanned at ${sha.slice(0, 7)}, skipping.
|
|
4760
|
+
`);
|
|
4761
|
+
postCheckRun(repo, sha, "success", []);
|
|
4762
|
+
await postEventToBackend({
|
|
4763
|
+
gatewayUrl,
|
|
4764
|
+
apiKey: synkroApiKey,
|
|
4765
|
+
repo,
|
|
4766
|
+
prNumber,
|
|
4767
|
+
sha,
|
|
4768
|
+
findings: [],
|
|
4769
|
+
filesScanned: 0,
|
|
4770
|
+
totalLatencyMs: 0
|
|
4771
|
+
});
|
|
4772
|
+
return;
|
|
4773
|
+
}
|
|
4774
|
+
const changedFiles = !scanCtx.scan_all && scanCtx.files ? new Set(scanCtx.files) : null;
|
|
4775
|
+
if (changedFiles) {
|
|
4776
|
+
console.log(`Incremental scan: ${changedFiles.size} file(s) changed since last scan (${scanCtx.last_sha?.slice(0, 7)}).
|
|
4777
|
+
`);
|
|
4778
|
+
}
|
|
4609
4779
|
const eligible = files.filter((f) => {
|
|
4610
4780
|
if (f.status === "removed") return false;
|
|
4611
4781
|
if (shouldSkipFile(f.filename)) return false;
|
|
4612
4782
|
if (f.additions + f.deletions > MAX_DIFF_LINES_PER_FILE) return false;
|
|
4613
4783
|
if (!f.patch) return false;
|
|
4784
|
+
if (changedFiles && !changedFiles.has(f.filename)) return false;
|
|
4614
4785
|
return true;
|
|
4615
4786
|
});
|
|
4616
4787
|
console.log(`${files.length} files in PR, ${eligible.length} eligible for scan.
|
|
@@ -4631,12 +4802,12 @@ async function scanPrCommand() {
|
|
|
4631
4802
|
return;
|
|
4632
4803
|
}
|
|
4633
4804
|
const t0 = Date.now();
|
|
4634
|
-
const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file) => {
|
|
4635
|
-
process.stdout.write(`
|
|
4805
|
+
const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file, idx, total) => {
|
|
4806
|
+
process.stdout.write(`[${idx + 1}/${total}] ${file.filename}...`);
|
|
4636
4807
|
const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
|
|
4637
4808
|
const llmResult = await spawnClaudeJudge(file, claudeToken, promptHeader);
|
|
4638
4809
|
const merged = [...literalFindings, ...llmResult.findings];
|
|
4639
|
-
console.log(` ${merged.length} finding(s) (${
|
|
4810
|
+
console.log(` ${merged.length === 0 ? "clean" : `${merged.length} finding(s)`} (${(llmResult.latencyMs / 1e3).toFixed(1)}s)`);
|
|
4640
4811
|
return { findings: merged, latencyMs: llmResult.latencyMs };
|
|
4641
4812
|
});
|
|
4642
4813
|
const totalLatencyMs = Date.now() - t0;
|
|
@@ -4690,7 +4861,7 @@ var init_scanPr = __esm({
|
|
|
4690
4861
|
/go\.sum$/
|
|
4691
4862
|
];
|
|
4692
4863
|
MAX_DIFF_LINES_PER_FILE = 1e3;
|
|
4693
|
-
MAX_PARALLEL_FILES =
|
|
4864
|
+
MAX_PARALLEL_FILES = 10;
|
|
4694
4865
|
}
|
|
4695
4866
|
});
|
|
4696
4867
|
|