@synkro-sh/cli 1.3.19 → 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";
@@ -2652,7 +2661,9 @@ import { createInterface } from "readline";
2652
2661
  function detectGitRepo() {
2653
2662
  try {
2654
2663
  const remoteUrl = execSync2("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
2655
- const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
2664
+ const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
2665
+ const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
2666
+ const match = sshMatch || httpMatch;
2656
2667
  if (!match) return null;
2657
2668
  const fullName = match[1];
2658
2669
  return { fullName, shortName: fullName.split("/").pop() || fullName };
@@ -2719,11 +2730,14 @@ function waitForGithubToken() {
2719
2730
  });
2720
2731
  }
2721
2732
  function openBrowser2(url) {
2722
- const { execFile: execFile2 } = __require("child_process");
2733
+ const { execFile: execFile3 } = __require("child_process");
2723
2734
  const plat = process.platform;
2724
- if (plat === "darwin") execFile2("open", [url]);
2725
- else if (plat === "win32") execFile2("cmd", ["/c", "start", "", url]);
2726
- else execFile2("xdg-open", [url]);
2735
+ const cb = (err) => {
2736
+ if (err) console.log(` Open this URL manually: ${url}`);
2737
+ };
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);
2727
2741
  }
2728
2742
  async function connectGithubAndSelectRepos() {
2729
2743
  const url = `${SYNKRO_WEB_AUTH_URL2}/cli-github?port=${GITHUB_PORT}`;
@@ -2863,8 +2877,9 @@ import { createInterface as createInterface2 } from "readline/promises";
2863
2877
  import { stdin as input, stdout as output } from "process";
2864
2878
  import { execSync as execSync3, spawn as nodeSpawn } from "child_process";
2865
2879
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2866
- import { homedir as homedir4 } from "os";
2880
+ import { homedir as homedir4, platform as platform2 } from "os";
2867
2881
  import { join as join5 } from "path";
2882
+ import { execFile as execFile2 } from "child_process";
2868
2883
  function readConfig() {
2869
2884
  if (!existsSync6(CONFIG_PATH)) return {};
2870
2885
  const out = {};
@@ -2892,9 +2907,7 @@ async function prompt(rl, q, opts = {}) {
2892
2907
  resolve2(chunk);
2893
2908
  return;
2894
2909
  }
2895
- if (s === "") {
2896
- process.exit(130);
2897
- }
2910
+ if (s === "") process.exit(130);
2898
2911
  if (s === "\x7F" || s === "\b") {
2899
2912
  chunk = chunk.slice(0, -1);
2900
2913
  return;
@@ -2906,6 +2919,30 @@ async function prompt(rl, q, opts = {}) {
2906
2919
  }
2907
2920
  return await rl.question(q);
2908
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
+ }
2909
2946
  function captureClaudeSetupToken() {
2910
2947
  return new Promise((resolve2, reject) => {
2911
2948
  const proc = nodeSpawn("script", ["-q", "/dev/null", "claude", "setup-token"], {
@@ -2931,6 +2968,21 @@ function captureClaudeSetupToken() {
2931
2968
  });
2932
2969
  });
2933
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
+ }
2934
2986
  async function setupGithubCommand(opts = {}) {
2935
2987
  if (!isAuthenticated()) {
2936
2988
  console.error("Not authenticated. Run `synkro-cli login` first.");
@@ -2946,20 +2998,12 @@ async function setupGithubCommand(opts = {}) {
2946
2998
  console.log("Requesting CI API key from Synkro...");
2947
2999
  let synkroCiApiKey;
2948
3000
  try {
2949
- const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
2950
- method: "POST",
2951
- headers: {
2952
- "Authorization": `Bearer ${jwt2}`,
2953
- "Content-Type": "application/json"
2954
- },
2955
- body: "{}"
2956
- });
2957
- if (!resp.ok) {
2958
- const errText = await resp.text().catch(() => "");
2959
- console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
2960
- process.exit(1);
2961
- }
2962
- 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
+ );
2963
3007
  synkroCiApiKey = minted.api_key;
2964
3008
  console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
2965
3009
  } catch (err) {
@@ -2971,23 +3015,78 @@ async function setupGithubCommand(opts = {}) {
2971
3015
  ghToken = opts.githubToken;
2972
3016
  } else if (opts.nonInteractive) {
2973
3017
  try {
2974
- 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
+ }
2975
3028
  } catch {
2976
- console.error("Could not get GitHub token from `gh auth token`. Run `gh auth login` first.");
2977
- 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
+ }
2978
3035
  }
2979
3036
  } else {
2980
- const rl = createInterface2({ input, output });
2981
- console.log("Synkro PR scan setup\n");
2982
- console.log("Requirements:");
2983
- console.log(" \u2022 Claude Code installed and logged in (Pro, Max, Teams, or Enterprise)");
2984
- console.log(" \u2022 A GitHub personal access token with `repo` scope");
2985
- console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
2986
- ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
2987
- rl.close();
2988
- if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
2989
- console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
2990
- 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");
2991
3090
  }
2992
3091
  }
2993
3092
  let claudeToken;
@@ -3038,10 +3137,10 @@ async function setupGithubCommand(opts = {}) {
3038
3137
  selected = [{ owner, repo, full_name: currentFullName }];
3039
3138
  console.log(` Auto-selected repo: ${currentFullName}`);
3040
3139
  } else {
3041
- console.log("\nFetching accessible repos...");
3140
+ console.log("Fetching accessible repos...");
3042
3141
  const repos = await listAccessibleRepos({ token: ghToken });
3043
3142
  if (repos.length === 0) {
3044
- console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3143
+ console.error("No accessible repos found. Check your GitHub permissions.");
3045
3144
  process.exit(1);
3046
3145
  }
3047
3146
  console.log(`
@@ -3233,7 +3332,7 @@ function writeConfigEnv(opts) {
3233
3332
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3234
3333
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3235
3334
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3236
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.19")}`
3335
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.21")}`
3237
3336
  ];
3238
3337
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3239
3338
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -3253,10 +3352,48 @@ function collectLocalMetadata() {
3253
3352
  }
3254
3353
  try {
3255
3354
  const remote = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 3e3 }).trim();
3256
- 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;
3257
3358
  if (m) meta.active_repo = m[1];
3258
3359
  } catch {
3259
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
+ }
3260
3397
  return meta;
3261
3398
  }
3262
3399
  async function fetchUserProfile(gatewayUrl, token) {
@@ -3546,7 +3683,9 @@ async function installCommand(opts = {}) {
3546
3683
  function detectGitRepo2() {
3547
3684
  try {
3548
3685
  const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
3549
- const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
3686
+ const sshMatch = remoteUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
3687
+ const httpMatch = remoteUrl.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
3688
+ const match = sshMatch || httpMatch;
3550
3689
  return match ? match[1] : null;
3551
3690
  } catch {
3552
3691
  return null;
@@ -4336,13 +4475,16 @@ ${hunks}`;
4336
4475
  });
4337
4476
  });
4338
4477
  }
4339
- async function processInBatches(items, batchSize, fn) {
4340
- const results = [];
4341
- for (let i = 0; i < items.length; i += batchSize) {
4342
- const batch = items.slice(i, i + batchSize);
4343
- const batchResults = await Promise.all(batch.map(fn));
4344
- 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
+ }
4345
4486
  }
4487
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
4346
4488
  return results;
4347
4489
  }
4348
4490
  function buildConsolidationPrompt(findings) {
@@ -4624,12 +4766,12 @@ async function scanPrCommand() {
4624
4766
  return;
4625
4767
  }
4626
4768
  const t0 = Date.now();
4627
- const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file) => {
4628
- 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}...`);
4629
4771
  const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
4630
4772
  const llmResult = await spawnClaudeJudge(file, claudeToken, promptHeader);
4631
4773
  const merged = [...literalFindings, ...llmResult.findings];
4632
- 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)`);
4633
4775
  return { findings: merged, latencyMs: llmResult.latencyMs };
4634
4776
  });
4635
4777
  const totalLatencyMs = Date.now() - t0;
@@ -4683,7 +4825,7 @@ var init_scanPr = __esm({
4683
4825
  /go\.sum$/
4684
4826
  ];
4685
4827
  MAX_DIFF_LINES_PER_FILE = 1e3;
4686
- MAX_PARALLEL_FILES = 5;
4828
+ MAX_PARALLEL_FILES = 10;
4687
4829
  }
4688
4830
  });
4689
4831