@synkro-sh/cli 1.3.20 → 1.3.21

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 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: execFile2 } = __require("child_process");
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") execFile2("open", [url], cb);
2730
- else if (plat === "win32") execFile2("cmd", ["/c", "start", "", url], cb);
2731
- else execFile2("xdg-open", [url], cb);
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}`;
@@ -2868,8 +2877,9 @@ import { createInterface as createInterface2 } from "readline/promises";
2868
2877
  import { stdin as input, stdout as output } from "process";
2869
2878
  import { execSync as execSync3, spawn as nodeSpawn } from "child_process";
2870
2879
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2871
- import { homedir as homedir4 } from "os";
2880
+ import { homedir as homedir4, platform as platform2 } from "os";
2872
2881
  import { join as join5 } from "path";
2882
+ import { execFile as execFile2 } from "child_process";
2873
2883
  function readConfig() {
2874
2884
  if (!existsSync6(CONFIG_PATH)) return {};
2875
2885
  const out = {};
@@ -2897,9 +2907,7 @@ async function prompt(rl, q, opts = {}) {
2897
2907
  resolve2(chunk);
2898
2908
  return;
2899
2909
  }
2900
- if (s === "") {
2901
- process.exit(130);
2902
- }
2910
+ if (s === "") process.exit(130);
2903
2911
  if (s === "\x7F" || s === "\b") {
2904
2912
  chunk = chunk.slice(0, -1);
2905
2913
  return;
@@ -2911,6 +2919,30 @@ async function prompt(rl, q, opts = {}) {
2911
2919
  }
2912
2920
  return await rl.question(q);
2913
2921
  }
2922
+ function openBrowser3(url) {
2923
+ const os = platform2();
2924
+ let bin;
2925
+ let args2;
2926
+ switch (os) {
2927
+ case "darwin":
2928
+ bin = "open";
2929
+ args2 = [url];
2930
+ break;
2931
+ case "win32":
2932
+ bin = "cmd";
2933
+ args2 = ["/c", "start", "", url];
2934
+ break;
2935
+ default:
2936
+ bin = "xdg-open";
2937
+ args2 = [url];
2938
+ break;
2939
+ }
2940
+ execFile2(bin, args2, () => {
2941
+ });
2942
+ }
2943
+ function sleep(ms) {
2944
+ return new Promise((r) => setTimeout(r, ms));
2945
+ }
2914
2946
  function captureClaudeSetupToken() {
2915
2947
  return new Promise((resolve2, reject) => {
2916
2948
  const proc = nodeSpawn("script", ["-q", "/dev/null", "claude", "setup-token"], {
@@ -2936,6 +2968,21 @@ function captureClaudeSetupToken() {
2936
2968
  });
2937
2969
  });
2938
2970
  }
2971
+ async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
2972
+ const resp = await fetch(`${gatewayUrl}${path}`, {
2973
+ ...opts,
2974
+ headers: {
2975
+ "Authorization": `Bearer ${jwt2}`,
2976
+ "Content-Type": "application/json",
2977
+ ...opts.headers || {}
2978
+ }
2979
+ });
2980
+ if (!resp.ok) {
2981
+ const text = await resp.text().catch(() => "");
2982
+ throw new Error(`API ${resp.status}: ${text.slice(0, 200)}`);
2983
+ }
2984
+ return resp.json();
2985
+ }
2939
2986
  async function setupGithubCommand(opts = {}) {
2940
2987
  if (!isAuthenticated()) {
2941
2988
  console.error("Not authenticated. Run `synkro-cli login` first.");
@@ -2951,20 +2998,12 @@ async function setupGithubCommand(opts = {}) {
2951
2998
  console.log("Requesting CI API key from Synkro...");
2952
2999
  let synkroCiApiKey;
2953
3000
  try {
2954
- const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
2955
- method: "POST",
2956
- headers: {
2957
- "Authorization": `Bearer ${jwt2}`,
2958
- "Content-Type": "application/json"
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();
3001
+ const minted = await apiCall(
3002
+ gatewayUrl,
3003
+ jwt2,
3004
+ "/api/v1/cli/ci-api-key",
3005
+ { method: "POST", body: "{}" }
3006
+ );
2968
3007
  synkroCiApiKey = minted.api_key;
2969
3008
  console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
2970
3009
  } catch (err) {
@@ -2976,23 +3015,78 @@ async function setupGithubCommand(opts = {}) {
2976
3015
  ghToken = opts.githubToken;
2977
3016
  } else if (opts.nonInteractive) {
2978
3017
  try {
2979
- ghToken = execSync3("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
3018
+ const result = await apiCall(
3019
+ gatewayUrl,
3020
+ jwt2,
3021
+ "/api/v1/cli/github-token"
3022
+ );
3023
+ if (result.connected && result.token) {
3024
+ ghToken = result.token;
3025
+ } else {
3026
+ throw new Error("not connected");
3027
+ }
2980
3028
  } catch {
2981
- console.error("Could not get GitHub token from `gh auth token`. Run `gh auth login` first.");
2982
- return;
3029
+ try {
3030
+ ghToken = execSync3("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
3031
+ } catch {
3032
+ console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
3033
+ return;
3034
+ }
2983
3035
  }
2984
3036
  } else {
2985
- const rl = createInterface2({ input, output });
2986
- console.log("Synkro PR scan setup\n");
2987
- console.log("Requirements:");
2988
- console.log(" \u2022 Claude Code installed and logged in (Pro, Max, Teams, or Enterprise)");
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_...");
2995
- process.exit(1);
3037
+ console.log("\nConnecting to GitHub...");
3038
+ let connected = false;
3039
+ try {
3040
+ const result = await apiCall(
3041
+ gatewayUrl,
3042
+ jwt2,
3043
+ "/api/v1/cli/github-token"
3044
+ );
3045
+ if (result.connected && result.token) {
3046
+ ghToken = result.token;
3047
+ connected = true;
3048
+ console.log(" \u2713 GitHub already connected via Synkro.\n");
3049
+ }
3050
+ } catch {
3051
+ }
3052
+ if (!connected) {
3053
+ console.log(" Opening browser to authorize GitHub...");
3054
+ try {
3055
+ const authResp = await apiCall(
3056
+ gatewayUrl,
3057
+ jwt2,
3058
+ "/api/pipes-widget/authorize/github",
3059
+ { method: "POST", body: "{}" }
3060
+ );
3061
+ openBrowser3(authResp.url);
3062
+ console.log(" Waiting for authorization...");
3063
+ } catch (err) {
3064
+ console.error(`Failed to start GitHub authorization: ${err.message}`);
3065
+ process.exit(1);
3066
+ }
3067
+ const deadline = Date.now() + 12e4;
3068
+ ghToken = "";
3069
+ while (Date.now() < deadline) {
3070
+ await sleep(2e3);
3071
+ try {
3072
+ const result = await apiCall(
3073
+ gatewayUrl,
3074
+ jwt2,
3075
+ "/api/v1/cli/github-token"
3076
+ );
3077
+ if (result.connected && result.token) {
3078
+ ghToken = result.token;
3079
+ break;
3080
+ }
3081
+ } catch {
3082
+ }
3083
+ process.stdout.write(".");
3084
+ }
3085
+ if (!ghToken) {
3086
+ console.error("\n Timed out waiting for GitHub authorization. Try again.");
3087
+ process.exit(1);
3088
+ }
3089
+ console.log("\n \u2713 GitHub connected!\n");
2996
3090
  }
2997
3091
  }
2998
3092
  let claudeToken;
@@ -3043,10 +3137,10 @@ async function setupGithubCommand(opts = {}) {
3043
3137
  selected = [{ owner, repo, full_name: currentFullName }];
3044
3138
  console.log(` Auto-selected repo: ${currentFullName}`);
3045
3139
  } else {
3046
- console.log("\nFetching accessible repos...");
3140
+ console.log("Fetching accessible repos...");
3047
3141
  const repos = await listAccessibleRepos({ token: ghToken });
3048
3142
  if (repos.length === 0) {
3049
- console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3143
+ console.error("No accessible repos found. Check your GitHub permissions.");
3050
3144
  process.exit(1);
3051
3145
  }
3052
3146
  console.log(`
@@ -3238,7 +3332,7 @@ function writeConfigEnv(opts) {
3238
3332
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3239
3333
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3240
3334
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3241
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.20")}`
3335
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.21")}`
3242
3336
  ];
3243
3337
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3244
3338
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -3258,10 +3352,48 @@ function collectLocalMetadata() {
3258
3352
  }
3259
3353
  try {
3260
3354
  const remote = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 3e3 }).trim();
3261
- const m = remote.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
3355
+ const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
3356
+ const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
3357
+ const m = sshMatch || httpMatch;
3262
3358
  if (m) meta.active_repo = m[1];
3263
3359
  } catch {
3264
3360
  }
3361
+ try {
3362
+ meta.cc_version = execSync4("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
3363
+ } catch {
3364
+ }
3365
+ const claudeDir = join6(homedir5(), ".claude");
3366
+ try {
3367
+ const settings = JSON.parse(readFileSync5(join6(claudeDir, "settings.json"), "utf-8"));
3368
+ const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
3369
+ if (plugins.length) meta.enabled_plugins = plugins;
3370
+ if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
3371
+ } catch {
3372
+ }
3373
+ try {
3374
+ const mcpCache = JSON.parse(readFileSync5(join6(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
3375
+ const mcpNames = Object.keys(mcpCache);
3376
+ if (mcpNames.length) meta.mcp_servers = mcpNames;
3377
+ } catch {
3378
+ }
3379
+ try {
3380
+ const mcpList = execSync4("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
3381
+ const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
3382
+ if (connected.length) meta.mcp_servers_connected = connected;
3383
+ } catch {
3384
+ }
3385
+ try {
3386
+ const sessionsDir = join6(claudeDir, "sessions");
3387
+ const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
3388
+ for (const f of files) {
3389
+ const s = JSON.parse(readFileSync5(join6(sessionsDir, f), "utf-8"));
3390
+ if (s.version) {
3391
+ meta.cc_version = meta.cc_version || s.version;
3392
+ break;
3393
+ }
3394
+ }
3395
+ } catch {
3396
+ }
3265
3397
  return meta;
3266
3398
  }
3267
3399
  async function fetchUserProfile(gatewayUrl, token) {
@@ -4343,13 +4475,16 @@ ${hunks}`;
4343
4475
  });
4344
4476
  });
4345
4477
  }
4346
- async function processInBatches(items, batchSize, fn) {
4347
- const results = [];
4348
- for (let i = 0; i < items.length; i += batchSize) {
4349
- const batch = items.slice(i, i + batchSize);
4350
- const batchResults = await Promise.all(batch.map(fn));
4351
- results.push(...batchResults);
4478
+ async function processInBatches(items, concurrency, fn) {
4479
+ const results = new Array(items.length);
4480
+ let next = 0;
4481
+ async function worker() {
4482
+ while (next < items.length) {
4483
+ const idx = next++;
4484
+ results[idx] = await fn(items[idx], idx, items.length);
4485
+ }
4352
4486
  }
4487
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
4353
4488
  return results;
4354
4489
  }
4355
4490
  function buildConsolidationPrompt(findings) {
@@ -4631,12 +4766,12 @@ async function scanPrCommand() {
4631
4766
  return;
4632
4767
  }
4633
4768
  const t0 = Date.now();
4634
- const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file) => {
4635
- process.stdout.write(`Scanning ${file.filename}...`);
4769
+ const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file, idx, total) => {
4770
+ process.stdout.write(`[${idx + 1}/${total}] ${file.filename}...`);
4636
4771
  const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
4637
4772
  const llmResult = await spawnClaudeJudge(file, claudeToken, promptHeader);
4638
4773
  const merged = [...literalFindings, ...llmResult.findings];
4639
- console.log(` ${merged.length} finding(s) (${literalFindings.length} literal, ${llmResult.findings.length} llm; ${llmResult.latencyMs}ms)`);
4774
+ console.log(` ${merged.length === 0 ? "clean" : `${merged.length} finding(s)`} (${(llmResult.latencyMs / 1e3).toFixed(1)}s)`);
4640
4775
  return { findings: merged, latencyMs: llmResult.latencyMs };
4641
4776
  });
4642
4777
  const totalLatencyMs = Date.now() - t0;
@@ -4690,7 +4825,7 @@ var init_scanPr = __esm({
4690
4825
  /go\.sum$/
4691
4826
  ];
4692
4827
  MAX_DIFF_LINES_PER_FILE = 1e3;
4693
- MAX_PARALLEL_FILES = 5;
4828
+ MAX_PARALLEL_FILES = 10;
4694
4829
  }
4695
4830
  });
4696
4831