@synkro-sh/cli 1.3.7 → 1.3.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/bootstrap.js CHANGED
@@ -2606,7 +2606,9 @@ async function listAccessibleRepos(opts) {
2606
2606
  }
2607
2607
  async function pushSecretsToRepo(opts, owner, repo, secrets) {
2608
2608
  const pubkey = await getRepoPublicKey(opts, owner, repo);
2609
- await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
2609
+ if (secrets.claudeCodeOauthToken) {
2610
+ await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
2611
+ }
2610
2612
  await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
2611
2613
  }
2612
2614
  function writeWorkflowFile(repoRootPath) {
@@ -2829,16 +2831,224 @@ var init_repoConnect = __esm({
2829
2831
  }
2830
2832
  });
2831
2833
 
2834
+ // cli/commands/setupGithub.ts
2835
+ var setupGithub_exports = {};
2836
+ __export(setupGithub_exports, {
2837
+ setupGithubCommand: () => setupGithubCommand
2838
+ });
2839
+ import { createInterface as createInterface2 } from "readline/promises";
2840
+ import { stdin as input, stdout as output } from "process";
2841
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2842
+ import { homedir as homedir4 } from "os";
2843
+ import { join as join5 } from "path";
2844
+ function readConfig() {
2845
+ if (!existsSync6(CONFIG_PATH)) return {};
2846
+ const out = {};
2847
+ for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
2848
+ const t = line.trim();
2849
+ if (!t || t.startsWith("#")) continue;
2850
+ const eq = t.indexOf("=");
2851
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
2852
+ }
2853
+ return out;
2854
+ }
2855
+ async function prompt(rl, q, opts = {}) {
2856
+ if (opts.silent) {
2857
+ process.stdout.write(q);
2858
+ const wasRaw = process.stdin.isRaw;
2859
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
2860
+ return await new Promise((resolve2) => {
2861
+ let chunk = "";
2862
+ const onData = (data) => {
2863
+ const s = data.toString("utf-8");
2864
+ if (s === "\r" || s === "\n" || s === "\r\n") {
2865
+ process.stdin.removeListener("data", onData);
2866
+ if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
2867
+ process.stdout.write("\n");
2868
+ resolve2(chunk);
2869
+ return;
2870
+ }
2871
+ if (s === "") {
2872
+ process.exit(130);
2873
+ }
2874
+ if (s === "\x7F" || s === "\b") {
2875
+ chunk = chunk.slice(0, -1);
2876
+ return;
2877
+ }
2878
+ chunk += s;
2879
+ };
2880
+ process.stdin.on("data", onData);
2881
+ });
2882
+ }
2883
+ return await rl.question(q);
2884
+ }
2885
+ async function setupGithubCommand(opts = {}) {
2886
+ if (!isAuthenticated()) {
2887
+ console.error("Not authenticated. Run `synkro-cli login` first.");
2888
+ process.exit(1);
2889
+ }
2890
+ const config2 = readConfig();
2891
+ const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
2892
+ const jwt2 = getAccessToken();
2893
+ if (!jwt2) {
2894
+ console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
2895
+ process.exit(1);
2896
+ }
2897
+ console.log("Requesting CI API key from Synkro...");
2898
+ let synkroCiApiKey;
2899
+ try {
2900
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
2901
+ method: "POST",
2902
+ headers: {
2903
+ "Authorization": `Bearer ${jwt2}`,
2904
+ "Content-Type": "application/json"
2905
+ },
2906
+ body: "{}"
2907
+ });
2908
+ if (!resp.ok) {
2909
+ const errText = await resp.text().catch(() => "");
2910
+ console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
2911
+ process.exit(1);
2912
+ }
2913
+ const minted = await resp.json();
2914
+ synkroCiApiKey = minted.api_key;
2915
+ console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
2916
+ } catch (err) {
2917
+ console.error(`Failed to mint CI API key: ${err.message}`);
2918
+ process.exit(1);
2919
+ }
2920
+ const rl = createInterface2({ input, output });
2921
+ console.log("Synkro PR scan setup\n");
2922
+ console.log("Requirements:");
2923
+ console.log(" \u2022 A GitHub personal access token with `repo` scope");
2924
+ console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
2925
+ const ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
2926
+ if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
2927
+ console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
2928
+ rl.close();
2929
+ process.exit(1);
2930
+ }
2931
+ let claudeToken = opts.claudeOauthToken || "";
2932
+ if (!claudeToken && !opts.skipClaudeToken) {
2933
+ try {
2934
+ const { execSync: execSync5 } = await import("child_process");
2935
+ console.log("\nGenerating Claude Code OAuth token \u2014 complete the browser auth...");
2936
+ const tokenOutput = execSync5("claude setup-token", {
2937
+ encoding: "utf-8",
2938
+ timeout: 12e4,
2939
+ stdio: ["inherit", "pipe", "inherit"]
2940
+ }).trim();
2941
+ const tokenLine = tokenOutput.split("\n").find((l) => l.includes("sk-ant-oat01-"));
2942
+ claudeToken = tokenLine?.match(/(sk-ant-oat01-[A-Za-z0-9_-]+)/)?.[1] || "";
2943
+ if (claudeToken) {
2944
+ console.log(" \u2713 Captured Claude Code OAuth token.\n");
2945
+ } else {
2946
+ console.warn(" \u26A0 Could not capture token. PR scans will use BYOK inference only.\n");
2947
+ }
2948
+ } catch {
2949
+ console.warn(" \u26A0 claude setup-token failed. PR scans will use BYOK inference only.\n");
2950
+ }
2951
+ }
2952
+ console.log("\nFetching accessible repos...");
2953
+ const repos = await listAccessibleRepos({ token: ghToken });
2954
+ if (repos.length === 0) {
2955
+ console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
2956
+ rl.close();
2957
+ process.exit(1);
2958
+ }
2959
+ console.log(`
2960
+ Found ${repos.length} accessible repo(s):
2961
+ `);
2962
+ repos.slice(0, 100).forEach((r, i) => {
2963
+ console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
2964
+ });
2965
+ console.log();
2966
+ const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
2967
+ const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
2968
+ if (selectedIdx.length === 0) {
2969
+ console.error("No valid selections.");
2970
+ rl.close();
2971
+ process.exit(1);
2972
+ }
2973
+ const selected = selectedIdx.map((i) => repos[i]);
2974
+ console.log(`
2975
+ Will push secrets to ${selected.length} repo(s):`);
2976
+ for (const r of selected) console.log(` \u2022 ${r.full_name}`);
2977
+ console.log();
2978
+ const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
2979
+ if (confirm !== "yes" && confirm !== "y") {
2980
+ console.log("Cancelled.");
2981
+ rl.close();
2982
+ process.exit(0);
2983
+ }
2984
+ rl.close();
2985
+ console.log();
2986
+ for (const r of selected) {
2987
+ process.stdout.write(`Pushing secrets to ${r.full_name}... `);
2988
+ try {
2989
+ await pushSecretsToRepo(
2990
+ { token: ghToken },
2991
+ r.owner,
2992
+ r.repo,
2993
+ {
2994
+ claudeCodeOauthToken: claudeToken,
2995
+ synkroApiKey: synkroCiApiKey
2996
+ }
2997
+ );
2998
+ console.log("\u2713");
2999
+ } catch (err) {
3000
+ console.log(`\u2717 (${err.message})`);
3001
+ }
3002
+ }
3003
+ console.log();
3004
+ const gitRoot = findGitRoot(process.cwd());
3005
+ if (gitRoot) {
3006
+ const written = writeWorkflowFile(gitRoot);
3007
+ if (written) {
3008
+ console.log(`Wrote workflow: ${written}`);
3009
+ console.log("Commit and push it to enable PR scanning.");
3010
+ }
3011
+ } else {
3012
+ console.log("Not in a git repo. To enable scanning, add this file to your repo:");
3013
+ console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
3014
+ console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
3015
+ }
3016
+ console.log();
3017
+ console.log("\u2713 PR scan setup complete.");
3018
+ console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
3019
+ console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
3020
+ }
3021
+ var SYNKRO_DIR, CONFIG_PATH;
3022
+ var init_setupGithub = __esm({
3023
+ "cli/commands/setupGithub.ts"() {
3024
+ "use strict";
3025
+ init_githubSetup();
3026
+ init_stub();
3027
+ SYNKRO_DIR = join5(homedir4(), ".synkro");
3028
+ CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
3029
+ }
3030
+ });
3031
+
2832
3032
  // cli/commands/install.ts
2833
3033
  var install_exports = {};
2834
3034
  __export(install_exports, {
2835
3035
  installCommand: () => installCommand,
2836
3036
  parseArgs: () => parseArgs
2837
3037
  });
2838
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync4, readdirSync } from "fs";
2839
- import { homedir as homedir4 } from "os";
2840
- import { join as join5 } from "path";
3038
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
3039
+ import { createInterface as createInterface3 } from "readline/promises";
3040
+ import { stdin as stdinStream, stdout as stdoutStream } from "process";
3041
+ import { homedir as homedir5 } from "os";
3042
+ import { join as join6 } from "path";
2841
3043
  import { execSync as execSync3 } from "child_process";
3044
+ async function promptYesNo(question, defaultYes = true) {
3045
+ const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3046
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
3047
+ const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
3048
+ rl.close();
3049
+ if (answer === "") return defaultYes;
3050
+ return answer === "y" || answer === "yes";
3051
+ }
2842
3052
  function sanitizeGatewayCandidate(raw) {
2843
3053
  if (!raw) return void 0;
2844
3054
  return /^https?:\/\//.test(raw) ? raw : void 0;
@@ -2859,7 +3069,7 @@ function parseArgs(argv) {
2859
3069
  return opts;
2860
3070
  }
2861
3071
  function ensureSynkroDir() {
2862
- mkdirSync5(SYNKRO_DIR, { recursive: true });
3072
+ mkdirSync5(SYNKRO_DIR2, { recursive: true });
2863
3073
  mkdirSync5(HOOKS_DIR, { recursive: true });
2864
3074
  mkdirSync5(BIN_DIR, { recursive: true });
2865
3075
  mkdirSync5(OFFSETS_DIR, { recursive: true });
@@ -2873,13 +3083,13 @@ function writeGraderDaemon() {
2873
3083
  chmodSync(GRADER_PRIMER_BASH_PATH, 420);
2874
3084
  }
2875
3085
  function writeHookScripts() {
2876
- const bashScriptPath = join5(HOOKS_DIR, "cc-bash-judge.sh");
2877
- const bashFollowupScriptPath = join5(HOOKS_DIR, "cc-bash-followup.sh");
2878
- const editCaptureScriptPath = join5(HOOKS_DIR, "cc-edit-capture.sh");
2879
- const editPrecheckScriptPath = join5(HOOKS_DIR, "cc-edit-precheck.sh");
2880
- const stopSummaryScriptPath = join5(HOOKS_DIR, "cc-stop-summary.sh");
2881
- const sessionStartScriptPath = join5(HOOKS_DIR, "cc-session-start.sh");
2882
- const transcriptSyncScriptPath = join5(HOOKS_DIR, "cc-transcript-sync.sh");
3086
+ const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
3087
+ const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
3088
+ const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
3089
+ const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
3090
+ const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
3091
+ const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
3092
+ const transcriptSyncScriptPath = join6(HOOKS_DIR, "cc-transcript-sync.sh");
2883
3093
  writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
2884
3094
  writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
2885
3095
  writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
@@ -2912,7 +3122,7 @@ function shellQuoteSingle(value) {
2912
3122
  return `'${value.replace(/'/g, "'\\''")}'`;
2913
3123
  }
2914
3124
  function writeConfigEnv(opts) {
2915
- const credsPath = join5(SYNKRO_DIR, "credentials.json");
3125
+ const credsPath = join6(SYNKRO_DIR2, "credentials.json");
2916
3126
  const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
2917
3127
  const safeUserId = sanitizeConfigValue(opts.userId);
2918
3128
  const safeOrgId = sanitizeConfigValue(opts.orgId);
@@ -2925,14 +3135,14 @@ function writeConfigEnv(opts) {
2925
3135
  `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
2926
3136
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
2927
3137
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
2928
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.7")}`
3138
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.8")}`
2929
3139
  ];
2930
3140
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
2931
3141
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
2932
3142
  if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
2933
3143
  lines.push("");
2934
- writeFileSync5(CONFIG_PATH, lines.join("\n"), "utf-8");
2935
- chmodSync(CONFIG_PATH, 384);
3144
+ writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
3145
+ chmodSync(CONFIG_PATH2, 384);
2936
3146
  }
2937
3147
  function assertGatewayAllowed(gatewayUrl) {
2938
3148
  let parsed;
@@ -2957,19 +3167,19 @@ function assertGatewayAllowed(gatewayUrl) {
2957
3167
  }
2958
3168
  function isAlreadyInstalled() {
2959
3169
  const requiredScripts = [
2960
- join5(HOOKS_DIR, "cc-bash-judge.sh"),
2961
- join5(HOOKS_DIR, "cc-bash-followup.sh"),
2962
- join5(HOOKS_DIR, "cc-edit-precheck.sh"),
2963
- join5(HOOKS_DIR, "cc-edit-capture.sh"),
2964
- join5(HOOKS_DIR, "cc-stop-summary.sh"),
2965
- join5(HOOKS_DIR, "cc-session-start.sh")
3170
+ join6(HOOKS_DIR, "cc-bash-judge.sh"),
3171
+ join6(HOOKS_DIR, "cc-bash-followup.sh"),
3172
+ join6(HOOKS_DIR, "cc-edit-precheck.sh"),
3173
+ join6(HOOKS_DIR, "cc-edit-capture.sh"),
3174
+ join6(HOOKS_DIR, "cc-stop-summary.sh"),
3175
+ join6(HOOKS_DIR, "cc-session-start.sh")
2966
3176
  ];
2967
- if (!requiredScripts.every((p) => existsSync6(p))) return false;
2968
- if (!existsSync6(CONFIG_PATH)) return false;
2969
- const settingsPath = join5(homedir4(), ".claude", "settings.json");
2970
- if (!existsSync6(settingsPath)) return false;
3177
+ if (!requiredScripts.every((p) => existsSync7(p))) return false;
3178
+ if (!existsSync7(CONFIG_PATH2)) return false;
3179
+ const settingsPath = join6(homedir5(), ".claude", "settings.json");
3180
+ if (!existsSync7(settingsPath)) return false;
2971
3181
  try {
2972
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
3182
+ const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
2973
3183
  const hooks = settings?.hooks;
2974
3184
  if (!hooks || typeof hooks !== "object") return false;
2975
3185
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -3052,9 +3262,9 @@ async function installCommand(opts = {}) {
3052
3262
  `);
3053
3263
  writeGraderDaemon();
3054
3264
  for (const mode of ["edit", "bash"]) {
3055
- const pidFile = join5(SYNKRO_DIR, "daemon", mode, "daemon.pid");
3265
+ const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
3056
3266
  try {
3057
- const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
3267
+ const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
3058
3268
  if (pid > 0) {
3059
3269
  process.kill(pid, "SIGTERM");
3060
3270
  console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
@@ -3122,38 +3332,157 @@ async function installCommand(opts = {}) {
3122
3332
  } catch {
3123
3333
  }
3124
3334
  writeConfigEnv({ gatewayUrl, userId, orgId, email });
3125
- console.log(`Wrote config to ${CONFIG_PATH}
3335
+ console.log(`Wrote config to ${CONFIG_PATH2}
3336
+ `);
3337
+ const repo = detectGitRepo2();
3338
+ if (repo && getClaudeProjectsFolder()) {
3339
+ const syncHistory = await promptYesNo("Sync Claude Code session history for this repo?");
3340
+ if (syncHistory) {
3341
+ try {
3342
+ const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
3343
+ if (ingested > 0) {
3344
+ console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
3345
+ console.log(" This helps the safety judge understand your workflow.\n");
3346
+ }
3347
+ } catch (err) {
3348
+ console.warn(` \u26A0 Session indexing skipped: ${err.message}
3126
3349
  `);
3127
- try {
3128
- const repo = detectGitRepo2();
3129
- if (repo) {
3130
- const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
3131
- if (ingested > 0) {
3132
- console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
3133
- console.log(" This helps the safety judge understand your workflow.\n");
3134
3350
  }
3135
- }
3136
- } catch (err) {
3137
- console.warn(` \u26A0 Session indexing skipped: ${err.message}
3351
+ try {
3352
+ const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
3353
+ if (result.messages > 0) {
3354
+ console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
3355
+ console.log(" This data will be used to suggest guardrail rules.\n");
3356
+ }
3357
+ } catch (err) {
3358
+ console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3138
3359
  `);
3360
+ }
3361
+ } else {
3362
+ console.log(" Skipped session history sync.\n");
3363
+ }
3139
3364
  }
3140
- try {
3141
- const repo = detectGitRepo2();
3142
- if (repo) {
3143
- const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
3144
- if (result.messages > 0) {
3145
- console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
3146
- console.log(" This data will be used to suggest guardrail rules.\n");
3365
+ function detectProviderFromKey(key) {
3366
+ if (key.startsWith("sk-ant-")) return "anthropic";
3367
+ if (key.startsWith("sk-") || key.startsWith("sk-proj-")) return "openai";
3368
+ if (key.startsWith("AIza")) return "gemini";
3369
+ return null;
3370
+ }
3371
+ const PROVIDER_MODELS = {
3372
+ anthropic: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
3373
+ openai: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
3374
+ gemini: ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"]
3375
+ };
3376
+ if (repo) {
3377
+ let ghConnected = false;
3378
+ let ghLogin = "";
3379
+ try {
3380
+ const resp = await fetch(`${gatewayUrl}/api/v1/integrations/github/connect`, {
3381
+ headers: { "Authorization": `Bearer ${token}` }
3382
+ });
3383
+ if (resp.ok) {
3384
+ const data = await resp.json();
3385
+ ghConnected = data.connected;
3386
+ ghLogin = data.github_login || "";
3147
3387
  }
3388
+ } catch {
3148
3389
  }
3149
- } catch (err) {
3150
- console.warn(` \u26A0 Transcript sync skipped: ${err.message}
3390
+ if (ghConnected) {
3391
+ console.log(`GitHub is connected (${ghLogin || "linked via dashboard"}).`);
3392
+ }
3393
+ const setupGh = await promptYesNo("Set up GitHub PR scanning?");
3394
+ if (setupGh) {
3395
+ let hasInferenceKey = false;
3396
+ try {
3397
+ const resp = await fetch(`${gatewayUrl}/api/v1/settings/inference`, {
3398
+ headers: { "Authorization": `Bearer ${token}` }
3399
+ });
3400
+ if (resp.ok) {
3401
+ const data = await resp.json();
3402
+ hasInferenceKey = data.configured && !!data.roles?.classification;
3403
+ }
3404
+ } catch {
3405
+ }
3406
+ const rl = createInterface3({ input: stdinStream, output: stdoutStream });
3407
+ console.log("\nHow should PR scans authenticate for AI review?\n");
3408
+ console.log(" 1. Claude Code OAuth \u2014 opens browser, auto-captures token");
3409
+ console.log(" 2. Use your own API key \u2014 paste an Anthropic, OpenAI, or Gemini key");
3410
+ console.log(" 3. Skip for now\n");
3411
+ const choice = (await rl.question("Choice [1/2/3]: ")).trim();
3412
+ rl.close();
3413
+ if (choice === "1") {
3414
+ try {
3415
+ await setupGithubCommand();
3416
+ } catch (err) {
3417
+ console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3418
+ console.warn(" Run `synkro-cli setup-github` to retry.\n");
3419
+ }
3420
+ } else if (choice === "2") {
3421
+ const rl2 = createInterface3({ input: stdinStream, output: stdoutStream });
3422
+ const apiKey = (await rl2.question("Paste your API key: ")).trim();
3423
+ rl2.close();
3424
+ if (apiKey) {
3425
+ const provider = detectProviderFromKey(apiKey);
3426
+ if (provider) {
3427
+ const models = PROVIDER_MODELS[provider];
3428
+ console.log(`
3429
+ Detected provider: ${provider}`);
3430
+ console.log(" Select a model for classification + grading:\n");
3431
+ models.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
3432
+ console.log();
3433
+ const rl3 = createInterface3({ input: stdinStream, output: stdoutStream });
3434
+ const modelChoice = (await rl3.question(` Model [1-${models.length}]: `)).trim();
3435
+ rl3.close();
3436
+ const modelIdx = parseInt(modelChoice, 10) - 1;
3437
+ const selectedModel = modelIdx >= 0 && modelIdx < models.length ? models[modelIdx] : models[0];
3438
+ console.log(` Using: ${selectedModel}`);
3439
+ try {
3440
+ const resp = await fetch(`${gatewayUrl}/api/v1/settings/inference`, {
3441
+ method: "PUT",
3442
+ headers: {
3443
+ "Authorization": `Bearer ${token}`,
3444
+ "Content-Type": "application/json"
3445
+ },
3446
+ body: JSON.stringify({
3447
+ providers: { [provider]: { api_key: apiKey } },
3448
+ roles: {
3449
+ classification: { provider, model: selectedModel },
3450
+ grading: { provider, model: selectedModel }
3451
+ }
3452
+ })
3453
+ });
3454
+ if (resp.ok) {
3455
+ console.log(` \u2713 Saved ${provider} key and configured classification + grading with ${selectedModel}.
3456
+ `);
3457
+ } else {
3458
+ const err = await resp.text().catch(() => "");
3459
+ console.warn(` \u26A0 Failed to save key: ${err.slice(0, 200)}
3460
+ `);
3461
+ }
3462
+ } catch (err) {
3463
+ console.warn(` \u26A0 Failed to save key: ${err.message}
3151
3464
  `);
3465
+ }
3466
+ } else {
3467
+ console.warn(" \u26A0 Could not detect provider from key format. Configure manually in the dashboard.\n");
3468
+ }
3469
+ }
3470
+ try {
3471
+ await setupGithubCommand({ skipClaudeToken: true });
3472
+ } catch (err) {
3473
+ console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
3474
+ console.warn(" Run `synkro-cli setup-github` to retry.\n");
3475
+ }
3476
+ } else {
3477
+ console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3478
+ }
3479
+ } else {
3480
+ console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
3481
+ }
3152
3482
  }
3153
3483
  console.log("\u2713 Synkro installed.");
3154
3484
  console.log();
3155
3485
  console.log("Next steps:");
3156
- console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
3157
3486
  console.log(" \u2022 synkro-cli status (check what is configured)");
3158
3487
  }
3159
3488
  function detectGitRepo2() {
@@ -3168,17 +3497,17 @@ function detectGitRepo2() {
3168
3497
  function getClaudeProjectsFolder() {
3169
3498
  const cwd = process.cwd();
3170
3499
  const sanitized = cwd.replace(/\//g, "-");
3171
- const projectsDir = join5(homedir4(), ".claude", "projects", sanitized);
3172
- return existsSync6(projectsDir) ? projectsDir : null;
3500
+ const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
3501
+ return existsSync7(projectsDir) ? projectsDir : null;
3173
3502
  }
3174
3503
  function extractSessionInsights(projectsDir) {
3175
3504
  const insights = [];
3176
3505
  const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
3177
3506
  for (const file of files) {
3178
3507
  const sessionId = file.replace(".jsonl", "");
3179
- const filePath = join5(projectsDir, file);
3508
+ const filePath = join6(projectsDir, file);
3180
3509
  try {
3181
- const content = readFileSync4(filePath, "utf-8");
3510
+ const content = readFileSync5(filePath, "utf-8");
3182
3511
  const lines = content.split("\n").filter(Boolean);
3183
3512
  for (let i = 0; i < lines.length; i++) {
3184
3513
  try {
@@ -3254,7 +3583,7 @@ function extractTextContent(content) {
3254
3583
  return "";
3255
3584
  }
3256
3585
  function parseTranscriptFile(filePath) {
3257
- const content = readFileSync4(filePath, "utf-8");
3586
+ const content = readFileSync5(filePath, "utf-8");
3258
3587
  const lines = content.split("\n").filter(Boolean);
3259
3588
  const messages = [];
3260
3589
  for (let i = 0; i < lines.length; i++) {
@@ -3305,7 +3634,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3305
3634
  const sessions = [];
3306
3635
  for (const file of batch) {
3307
3636
  const sessionId = file.replace(".jsonl", "");
3308
- const filePath = join5(projectsDir, file);
3637
+ const filePath = join6(projectsDir, file);
3309
3638
  try {
3310
3639
  const allMessages = parseTranscriptFile(filePath);
3311
3640
  const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
@@ -3334,18 +3663,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
3334
3663
  }
3335
3664
  for (const file of batch) {
3336
3665
  const sessionId = file.replace(".jsonl", "");
3337
- const filePath = join5(projectsDir, file);
3666
+ const filePath = join6(projectsDir, file);
3338
3667
  try {
3339
- const content = readFileSync4(filePath, "utf-8");
3668
+ const content = readFileSync5(filePath, "utf-8");
3340
3669
  const lineCount = content.split("\n").filter(Boolean).length;
3341
- writeFileSync5(join5(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3670
+ writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
3342
3671
  } catch {
3343
3672
  }
3344
3673
  }
3345
3674
  }
3346
3675
  return { sessions: totalSessions, messages: totalMessages };
3347
3676
  }
3348
- var SYNKRO_DIR, HOOKS_DIR, BIN_DIR, CONFIG_PATH, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
3677
+ var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
3349
3678
  var init_install = __esm({
3350
3679
  "cli/commands/install.ts"() {
3351
3680
  "use strict";
@@ -3356,14 +3685,15 @@ var init_install = __esm({
3356
3685
  init_graderDaemon();
3357
3686
  init_stub();
3358
3687
  init_repoConnect();
3359
- SYNKRO_DIR = join5(homedir4(), ".synkro");
3360
- HOOKS_DIR = join5(SYNKRO_DIR, "hooks");
3361
- BIN_DIR = join5(SYNKRO_DIR, "bin");
3362
- CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
3363
- GRADER_DAEMON_PATH = join5(BIN_DIR, "grader_daemon.py");
3364
- GRADER_PRIMER_EDIT_PATH = join5(SYNKRO_DIR, "grader-primer-edit.txt");
3365
- GRADER_PRIMER_BASH_PATH = join5(SYNKRO_DIR, "grader-primer-bash.txt");
3366
- OFFSETS_DIR = join5(SYNKRO_DIR, ".transcript-offsets");
3688
+ init_setupGithub();
3689
+ SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3690
+ HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
3691
+ BIN_DIR = join6(SYNKRO_DIR2, "bin");
3692
+ CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
3693
+ GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
3694
+ GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
3695
+ GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
3696
+ OFFSETS_DIR = join6(SYNKRO_DIR2, ".transcript-offsets");
3367
3697
  }
3368
3698
  });
3369
3699
 
@@ -3439,13 +3769,13 @@ var status_exports = {};
3439
3769
  __export(status_exports, {
3440
3770
  statusCommand: () => statusCommand
3441
3771
  });
3442
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
3443
- import { homedir as homedir5 } from "os";
3444
- import { join as join6 } from "path";
3772
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3773
+ import { homedir as homedir6 } from "os";
3774
+ import { join as join7 } from "path";
3445
3775
  function readConfigEnv() {
3446
- if (!existsSync7(CONFIG_PATH2)) return {};
3776
+ if (!existsSync8(CONFIG_PATH3)) return {};
3447
3777
  const out = {};
3448
- const raw = readFileSync5(CONFIG_PATH2, "utf-8");
3778
+ const raw = readFileSync6(CONFIG_PATH3, "utf-8");
3449
3779
  for (const line of raw.split("\n")) {
3450
3780
  const trimmed = line.trim();
3451
3781
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -3476,10 +3806,10 @@ function statusCommand() {
3476
3806
  console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
3477
3807
  const info2 = getUserInfo();
3478
3808
  const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
3479
- const tierCacheFile = join6(SYNKRO_DIR2, `.tier-cache-${userId}`);
3809
+ const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
3480
3810
  let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
3481
- if (!inferenceTier && existsSync7(tierCacheFile)) {
3482
- inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
3811
+ if (!inferenceTier && existsSync8(tierCacheFile)) {
3812
+ inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
3483
3813
  }
3484
3814
  const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
3485
3815
  console.log(` inference: ${tierLabel}`);
@@ -3506,19 +3836,19 @@ function statusCommand() {
3506
3836
  }
3507
3837
  }
3508
3838
  console.log();
3509
- const bashScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-judge.sh");
3510
- const bashFollowupScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-followup.sh");
3511
- const editPrecheckScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-precheck.sh");
3512
- const editCaptureScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-capture.sh");
3513
- const stopSummaryScript = join6(SYNKRO_DIR2, "hooks", "cc-stop-summary.sh");
3514
- const sessionStartScript = join6(SYNKRO_DIR2, "hooks", "cc-session-start.sh");
3839
+ const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
3840
+ const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
3841
+ const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
3842
+ const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
3843
+ const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
3844
+ const sessionStartScript = join7(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
3515
3845
  console.log("Hook scripts:");
3516
- console.log(` ${existsSync7(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
3517
- console.log(` ${existsSync7(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
3518
- console.log(` ${existsSync7(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
3519
- console.log(` ${existsSync7(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
3520
- console.log(` ${existsSync7(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
3521
- console.log(` ${existsSync7(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
3846
+ console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
3847
+ console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
3848
+ console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
3849
+ console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
3850
+ console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
3851
+ console.log(` ${existsSync8(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
3522
3852
  console.log();
3523
3853
  const mcp = inspectMcpConfig();
3524
3854
  console.log("Guardrails MCP server (Claude Code):");
@@ -3530,7 +3860,7 @@ function statusCommand() {
3530
3860
  console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
3531
3861
  }
3532
3862
  }
3533
- var SYNKRO_DIR2, CONFIG_PATH2;
3863
+ var SYNKRO_DIR3, CONFIG_PATH3;
3534
3864
  var init_status = __esm({
3535
3865
  "cli/commands/status.ts"() {
3536
3866
  "use strict";
@@ -3538,8 +3868,8 @@ var init_status = __esm({
3538
3868
  init_agentDetect();
3539
3869
  init_ccHookConfig();
3540
3870
  init_mcpConfig();
3541
- SYNKRO_DIR2 = join6(homedir5(), ".synkro");
3542
- CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
3871
+ SYNKRO_DIR3 = join7(homedir6(), ".synkro");
3872
+ CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
3543
3873
  }
3544
3874
  });
3545
3875
 
@@ -3569,7 +3899,7 @@ var unlink_exports = {};
3569
3899
  __export(unlink_exports, {
3570
3900
  unlinkCommand: () => unlinkCommand
3571
3901
  });
3572
- import { createInterface as createInterface2 } from "readline";
3902
+ import { createInterface as createInterface4 } from "readline";
3573
3903
  function ask2(rl, question) {
3574
3904
  return new Promise((resolve2) => rl.question(question, resolve2));
3575
3905
  }
@@ -3597,7 +3927,7 @@ async function unlinkCommand() {
3597
3927
  console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
3598
3928
  });
3599
3929
  console.log();
3600
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
3930
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
3601
3931
  try {
3602
3932
  const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
3603
3933
  const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
@@ -3623,194 +3953,6 @@ var init_unlink = __esm({
3623
3953
  }
3624
3954
  });
3625
3955
 
3626
- // cli/commands/setupGithub.ts
3627
- var setupGithub_exports = {};
3628
- __export(setupGithub_exports, {
3629
- setupGithubCommand: () => setupGithubCommand
3630
- });
3631
- import { createInterface as createInterface3 } from "readline/promises";
3632
- import { stdin as input, stdout as output } from "process";
3633
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3634
- import { homedir as homedir6 } from "os";
3635
- import { join as join7 } from "path";
3636
- function readConfig() {
3637
- if (!existsSync8(CONFIG_PATH3)) return {};
3638
- const out = {};
3639
- for (const line of readFileSync6(CONFIG_PATH3, "utf-8").split("\n")) {
3640
- const t = line.trim();
3641
- if (!t || t.startsWith("#")) continue;
3642
- const eq = t.indexOf("=");
3643
- if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
3644
- }
3645
- return out;
3646
- }
3647
- async function prompt(rl, q, opts = {}) {
3648
- if (opts.silent) {
3649
- process.stdout.write(q);
3650
- const wasRaw = process.stdin.isRaw;
3651
- if (process.stdin.setRawMode) process.stdin.setRawMode(true);
3652
- return await new Promise((resolve2) => {
3653
- let chunk = "";
3654
- const onData = (data) => {
3655
- const s = data.toString("utf-8");
3656
- if (s === "\r" || s === "\n" || s === "\r\n") {
3657
- process.stdin.removeListener("data", onData);
3658
- if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
3659
- process.stdout.write("\n");
3660
- resolve2(chunk);
3661
- return;
3662
- }
3663
- if (s === "") {
3664
- process.exit(130);
3665
- }
3666
- if (s === "\x7F" || s === "\b") {
3667
- chunk = chunk.slice(0, -1);
3668
- return;
3669
- }
3670
- chunk += s;
3671
- };
3672
- process.stdin.on("data", onData);
3673
- });
3674
- }
3675
- return await rl.question(q);
3676
- }
3677
- async function setupGithubCommand() {
3678
- if (!isAuthenticated()) {
3679
- console.error("Not authenticated. Run `synkro-cli login` first.");
3680
- process.exit(1);
3681
- }
3682
- const config2 = readConfig();
3683
- const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
3684
- const jwt2 = getAccessToken();
3685
- if (!jwt2) {
3686
- console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
3687
- process.exit(1);
3688
- }
3689
- console.log("Requesting CI API key from Synkro...");
3690
- let synkroCiApiKey;
3691
- try {
3692
- const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
3693
- method: "POST",
3694
- headers: {
3695
- "Authorization": `Bearer ${jwt2}`,
3696
- "Content-Type": "application/json"
3697
- },
3698
- body: "{}"
3699
- });
3700
- if (!resp.ok) {
3701
- const errText = await resp.text().catch(() => "");
3702
- console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
3703
- process.exit(1);
3704
- }
3705
- const minted = await resp.json();
3706
- synkroCiApiKey = minted.api_key;
3707
- console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
3708
- } catch (err) {
3709
- console.error(`Failed to mint CI API key: ${err.message}`);
3710
- process.exit(1);
3711
- }
3712
- const rl = createInterface3({ input, output });
3713
- console.log("Synkro PR scan setup\n");
3714
- console.log("Requirements:");
3715
- console.log(" \u2022 Claude Code Pro or Max subscription (for `claude setup-token`)");
3716
- console.log(" \u2022 A GitHub personal access token with `repo` scope");
3717
- console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
3718
- const ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
3719
- if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
3720
- console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
3721
- rl.close();
3722
- process.exit(1);
3723
- }
3724
- console.log("\nNow get your Claude Code OAuth token:");
3725
- console.log(" 1. In another terminal, run: claude setup-token");
3726
- console.log(" 2. Complete the browser flow");
3727
- console.log(" 3. Copy the resulting sk-ant-oat01-... token\n");
3728
- const claudeToken = (await prompt(rl, "Claude Code OAuth token (paste): ", { silent: true })).trim();
3729
- if (!claudeToken.startsWith("sk-ant-oat01-")) {
3730
- console.error("Invalid token. Expected sk-ant-oat01-... \u2014 generate one with `claude setup-token`.");
3731
- rl.close();
3732
- process.exit(1);
3733
- }
3734
- console.log("\nFetching accessible repos...");
3735
- const repos = await listAccessibleRepos({ token: ghToken });
3736
- if (repos.length === 0) {
3737
- console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
3738
- rl.close();
3739
- process.exit(1);
3740
- }
3741
- console.log(`
3742
- Found ${repos.length} accessible repo(s):
3743
- `);
3744
- repos.slice(0, 100).forEach((r, i) => {
3745
- console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
3746
- });
3747
- console.log();
3748
- const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
3749
- const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
3750
- if (selectedIdx.length === 0) {
3751
- console.error("No valid selections.");
3752
- rl.close();
3753
- process.exit(1);
3754
- }
3755
- const selected = selectedIdx.map((i) => repos[i]);
3756
- console.log(`
3757
- Will push secrets to ${selected.length} repo(s):`);
3758
- for (const r of selected) console.log(` \u2022 ${r.full_name}`);
3759
- console.log();
3760
- const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
3761
- if (confirm !== "yes" && confirm !== "y") {
3762
- console.log("Cancelled.");
3763
- rl.close();
3764
- process.exit(0);
3765
- }
3766
- rl.close();
3767
- console.log();
3768
- for (const r of selected) {
3769
- process.stdout.write(`Pushing secrets to ${r.full_name}... `);
3770
- try {
3771
- await pushSecretsToRepo(
3772
- { token: ghToken },
3773
- r.owner,
3774
- r.repo,
3775
- {
3776
- claudeCodeOauthToken: claudeToken,
3777
- synkroApiKey: synkroCiApiKey
3778
- }
3779
- );
3780
- console.log("\u2713");
3781
- } catch (err) {
3782
- console.log(`\u2717 (${err.message})`);
3783
- }
3784
- }
3785
- console.log();
3786
- const gitRoot = findGitRoot(process.cwd());
3787
- if (gitRoot) {
3788
- const written = writeWorkflowFile(gitRoot);
3789
- if (written) {
3790
- console.log(`Wrote workflow: ${written}`);
3791
- console.log("Commit and push it to enable PR scanning.");
3792
- }
3793
- } else {
3794
- console.log("Not in a git repo. To enable scanning, add this file to your repo:");
3795
- console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
3796
- console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
3797
- }
3798
- console.log();
3799
- console.log("\u2713 PR scan setup complete.");
3800
- console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
3801
- console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
3802
- }
3803
- var SYNKRO_DIR3, CONFIG_PATH3;
3804
- var init_setupGithub = __esm({
3805
- "cli/commands/setupGithub.ts"() {
3806
- "use strict";
3807
- init_githubSetup();
3808
- init_stub();
3809
- SYNKRO_DIR3 = join7(homedir6(), ".synkro");
3810
- CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
3811
- }
3812
- });
3813
-
3814
3956
  // cli/commands/scanPr.ts
3815
3957
  var scanPr_exports = {};
3816
3958
  __export(scanPr_exports, {