@synkro-sh/cli 1.6.10 → 1.6.12

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
@@ -990,6 +990,12 @@ export interface HookConfig {
990
990
  policyName: string;
991
991
  rules: Rule[];
992
992
  scanExemptions: Array<{ path: string; cwe_id: string }>;
993
+ // User-owned data axes. gradingMode: 'local' (container worker pool) | 'byok'
994
+ // (LLM API with the user's own key). storageMode: 'local' (PGLite) | 'cloud'
995
+ // (Timescale). Sourced from config.env (the installed choice); the server
996
+ // value from /v1/hook/config is only a fallback when the env var is unset.
997
+ gradingMode: string;
998
+ storageMode: string;
993
999
  }
994
1000
 
995
1001
  export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
@@ -1000,6 +1006,8 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1000
1006
  policyName: '',
1001
1007
  rules: [],
1002
1008
  scanExemptions: [],
1009
+ gradingMode: process.env.SYNKRO_GRADING_MODE || 'local',
1010
+ storageMode: process.env.SYNKRO_STORAGE_MODE || 'local',
1003
1011
  };
1004
1012
 
1005
1013
  // Kick the telemetry spool drainer. Fire-and-forget: it runs concurrently
@@ -1047,6 +1055,9 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1047
1055
  config.captureDepth = data.capture_depth || 'local_only';
1048
1056
  config.tier = data.tier || 'standard';
1049
1057
  config.silent = data.silent_mode === true || data.silent_mode === 'true';
1058
+ // Env var (config.env, the installed choice) wins; server value is fallback.
1059
+ if (!process.env.SYNKRO_GRADING_MODE && data.grading_mode) config.gradingMode = data.grading_mode;
1060
+ if (!process.env.SYNKRO_STORAGE_MODE && data.storage_mode) config.storageMode = data.storage_mode;
1050
1061
  config.policyName = data.active_policy_name || '';
1051
1062
  if (Array.isArray(data.scan_exemptions)) {
1052
1063
  config.scanExemptions = data.scan_exemptions
@@ -1123,10 +1134,34 @@ async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port:
1123
1134
  return String(data.result || '');
1124
1135
  }
1125
1136
 
1137
+ // BYOK grading \u2014 grade via the cloud /v1/grade endpoint, which runs the same
1138
+ // grader prompt through an LLM API using the org's own provider key. Any
1139
+ // non-2xx (incl. 422 when no key is configured) throws, so the caller's
1140
+ // existing catch falls open \u2014 never a hard block on a grader error.
1141
+ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutMs: number): Promise<string> {
1142
+ const resp = await fetch(GATEWAY_URL + '/api/v1/grade', {
1143
+ method: 'POST',
1144
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1145
+ body: JSON.stringify({ surface, grader_prompt: prompt }),
1146
+ signal: AbortSignal.timeout(timeoutMs),
1147
+ });
1148
+ if (!resp.ok) {
1149
+ const text = await resp.text().catch(() => '');
1150
+ throw new Error('cloud grade ' + resp.status + ': ' + text.slice(0, 200));
1151
+ }
1152
+ const data = await resp.json() as { verdict?: string };
1153
+ return String(data.verdict || '');
1154
+ }
1155
+
1126
1156
  export async function localGrade(surface: string, prompt: string, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
1127
- if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1128
1157
  const jwt = loadJwt();
1129
1158
  if (!jwt) throw new Error('NO_JWT');
1159
+ // BYOK grading mode routes the grade through an LLM API instead of the
1160
+ // on-device channel worker pool. The grader prompt + parseVerdict are shared.
1161
+ if ((process.env.SYNKRO_GRADING_MODE || 'local') === 'byok') {
1162
+ return cloudGrade(surface, prompt, jwt, timeoutMs);
1163
+ }
1164
+ if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1130
1165
  return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
1131
1166
  }
1132
1167
 
@@ -1518,6 +1553,19 @@ export function parseVerdict(resp: string): Verdict {
1518
1553
 
1519
1554
  // \u2500\u2500\u2500 Telemetry Dispatch \u2500\u2500\u2500
1520
1555
 
1556
+ // Gated cloud telemetry POST \u2014 fires only when storage mode is 'cloud'. The
1557
+ // local PGLite spool (appendLocalTelemetry) always gets the data regardless,
1558
+ // so 'local' storage keeps everything on the machine.
1559
+ function shipCloud(jwt: string, path: string, body: Record<string, any>, timeoutMs = 3000): void {
1560
+ if ((process.env.SYNKRO_STORAGE_MODE || 'local') !== 'cloud') return;
1561
+ fetch(GATEWAY_URL + path, {
1562
+ method: 'POST',
1563
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1564
+ body: JSON.stringify(body),
1565
+ signal: AbortSignal.timeout(timeoutMs),
1566
+ }).catch(() => {});
1567
+ }
1568
+
1521
1569
  export function dispatchCapture(
1522
1570
  jwt: string,
1523
1571
  hookType: string,
@@ -1540,9 +1588,6 @@ export function dispatchCapture(
1540
1588
  // Fire-and-forget
1541
1589
  const eventId = 'evt_' + Date.now() + '_' + process.pid;
1542
1590
  const model = opts?.ccModel || 'unknown';
1543
- const sendFull =
1544
- captureDepth === 'full' ||
1545
- (captureDepth === 'evidence_on_violation' && ['block', 'warning', 'deny'].includes(verdictStr));
1546
1591
 
1547
1592
  const body: Record<string, any> = {
1548
1593
  capture_type: 'local_verdict',
@@ -1570,24 +1615,16 @@ export function dispatchCapture(
1570
1615
  }
1571
1616
  appendLocalTelemetry(localBody);
1572
1617
 
1573
- // local_only: no data leaves the machine
1574
- if (captureDepth === 'local_only') return;
1575
-
1576
- if (sendFull && opts) {
1577
- body.capture_depth = captureDepth;
1618
+ // Cloud copy carries the same full content as the local spool; shipCloud
1619
+ // gates on storage mode (no-op when storage is local).
1620
+ if (opts) {
1578
1621
  if (opts.command) body.command = opts.command;
1579
1622
  if (opts.reasoning) body.reasoning = opts.reasoning;
1580
1623
  if (opts.rulesChecked) body.rules_checked = opts.rulesChecked;
1581
1624
  if (opts.violatedRules) body.violated_rules = opts.violatedRules;
1582
1625
  if (opts.recentUserMessages) body.recent_user_messages = opts.recentUserMessages;
1583
1626
  }
1584
-
1585
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
1586
- method: 'POST',
1587
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1588
- body: JSON.stringify(body),
1589
- signal: AbortSignal.timeout(3000),
1590
- }).catch(() => {});
1627
+ shipCloud(jwt, '/api/v1/hook/capture', body);
1591
1628
  }
1592
1629
 
1593
1630
  // \u2500\u2500\u2500 Durable Telemetry Spool \u2500\u2500\u2500
@@ -2059,36 +2096,24 @@ export function dispatchFinding(
2059
2096
  };
2060
2097
  appendLocalTelemetry(localEntry);
2061
2098
 
2062
- if (captureDepth === 'local_only') return;
2063
-
2099
+ // Cloud copy carries the full finding; shipCloud gates on storage mode.
2064
2100
  const cloudBody: Record<string, any> = {
2065
2101
  finding_type: finding.finding_type,
2066
2102
  finding_id: finding.finding_id,
2067
2103
  severity: finding.severity,
2068
2104
  status: finding.status,
2069
2105
  session_id: finding.session_id,
2106
+ file_path: finding.file_path,
2107
+ package_name: finding.package_name,
2108
+ package_version: finding.package_version,
2109
+ fixed_version: finding.fixed_version,
2110
+ aliases: finding.aliases,
2111
+ references: finding.references,
2112
+ cwe_name: finding.cwe_name,
2113
+ detail: finding.detail,
2114
+ description: finding.description,
2070
2115
  };
2071
-
2072
- if (captureDepth === 'evidence_on_violation' || captureDepth === 'full') {
2073
- cloudBody.file_path = finding.file_path;
2074
- cloudBody.package_name = finding.package_name;
2075
- cloudBody.package_version = finding.package_version;
2076
- cloudBody.fixed_version = finding.fixed_version;
2077
- cloudBody.aliases = finding.aliases;
2078
- cloudBody.references = finding.references;
2079
- cloudBody.cwe_name = finding.cwe_name;
2080
- }
2081
- if (captureDepth === 'full') {
2082
- cloudBody.detail = finding.detail;
2083
- cloudBody.description = finding.description;
2084
- }
2085
-
2086
- fetch(GATEWAY_URL + '/api/v1/hook/finding', {
2087
- method: 'POST',
2088
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
2089
- body: JSON.stringify(cloudBody),
2090
- signal: AbortSignal.timeout(3000),
2091
- }).catch(() => {});
2116
+ shipCloud(jwt, '/api/v1/hook/finding', cloudBody);
2092
2117
  }
2093
2118
 
2094
2119
  // \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
@@ -3744,12 +3769,7 @@ async function main() {
3744
3769
  ...(sessionId ? { session_id: sessionId } : {}),
3745
3770
  };
3746
3771
  appendLocalTelemetry(usageBody);
3747
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3748
- method: 'POST',
3749
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3750
- body: JSON.stringify(usageBody),
3751
- signal: AbortSignal.timeout(3000),
3752
- }).catch(() => {});
3772
+ shipCloud(jwt, '/api/v1/hook/capture', usageBody);
3753
3773
  }
3754
3774
  }
3755
3775
 
@@ -3910,16 +3930,7 @@ async function main() {
3910
3930
  };
3911
3931
 
3912
3932
  appendLocalTelemetry(body);
3913
-
3914
- const config = await loadConfig(jwt);
3915
- if (config.captureDepth !== 'local_only') {
3916
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3917
- method: 'POST',
3918
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3919
- body: JSON.stringify(body),
3920
- signal: AbortSignal.timeout(3000),
3921
- }).catch(() => {});
3922
- }
3933
+ shipCloud(jwt, '/api/v1/hook/capture', body);
3923
3934
 
3924
3935
  outputEmpty();
3925
3936
  } catch {
@@ -3932,7 +3943,7 @@ main();
3932
3943
  TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
3933
3944
  import {
3934
3945
  loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
3935
- outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3946
+ outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL, readSessionLog,
3936
3947
  } from './_synkro-common.ts';
3937
3948
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3938
3949
  import { join, dirname } from 'node:path';
@@ -3976,12 +3987,7 @@ async function main() {
3976
3987
  session_id: sessionId,
3977
3988
  };
3978
3989
  appendLocalTelemetry(usageBody);
3979
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3980
- method: 'POST',
3981
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3982
- body: JSON.stringify(usageBody),
3983
- signal: AbortSignal.timeout(3000),
3984
- }).catch(() => {});
3990
+ shipCloud(jwt, '/api/v1/hook/capture', usageBody);
3985
3991
  }
3986
3992
 
3987
3993
  // Transcript consent gates only CLOUD transmission. Local persistence \u2014
@@ -4056,25 +4062,19 @@ async function main() {
4056
4062
  }).catch(() => {});
4057
4063
  }
4058
4064
 
4059
- // Cloud sync \u2014 only when consented and the org isn't local-only.
4060
- if (cloudConsent && gitRepo) {
4061
- let captureDepth = 'local_only';
4062
- try {
4063
- const r = await fetch(GATEWAY_URL + '/api/v1/hook/config', {
4064
- headers: { Authorization: 'Bearer ' + jwt },
4065
- signal: AbortSignal.timeout(3000),
4066
- });
4067
- const data = await r.json() as any;
4068
- captureDepth = data.capture_depth || 'local_only';
4069
- } catch {}
4070
- if (captureDepth !== 'local_only') {
4071
- fetch(GATEWAY_URL + '/api/v1/cli/sync-transcripts', {
4072
- method: 'POST',
4073
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4074
- body: JSON.stringify({ repo: gitRepo, sessions: [{ cc_session_id: sessionId, messages }] }),
4075
- signal: AbortSignal.timeout(10000),
4076
- }).catch(() => {});
4077
- }
4065
+ // Cloud sync \u2014 only when storage mode is cloud (and transcript consent
4066
+ // wasn't declined). Carries the transcript + the session step-log so cloud
4067
+ // storage matches the local PGLite shape.
4068
+ if (cloudConsent && gitRepo && (process.env.SYNKRO_STORAGE_MODE || 'local') === 'cloud') {
4069
+ fetch(GATEWAY_URL + '/api/v1/cli/sync-transcripts', {
4070
+ method: 'POST',
4071
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4072
+ body: JSON.stringify({
4073
+ repo: gitRepo,
4074
+ sessions: [{ cc_session_id: sessionId, messages, actions: readSessionLog(sessionId) }],
4075
+ }),
4076
+ signal: AbortSignal.timeout(10000),
4077
+ }).catch(() => {});
4078
4078
  }
4079
4079
 
4080
4080
  outputEmpty();
@@ -4511,12 +4511,7 @@ async function main() {
4511
4511
  if (repo) captureBody.repo = repo;
4512
4512
 
4513
4513
  appendLocalTelemetry(captureBody);
4514
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
4515
- method: 'POST',
4516
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4517
- body: JSON.stringify(captureBody),
4518
- signal: AbortSignal.timeout(10000),
4519
- }).catch(() => {});
4514
+ shipCloud(jwt, '/api/v1/hook/capture', captureBody, 10000);
4520
4515
 
4521
4516
  finish();
4522
4517
  } catch (e) {
@@ -5281,459 +5276,96 @@ var init_repoConnect = __esm({
5281
5276
  }
5282
5277
  });
5283
5278
 
5284
- // cli/commands/setupGithub.ts
5285
- var setupGithub_exports = {};
5286
- __export(setupGithub_exports, {
5287
- connectGitHub: () => connectGitHub,
5288
- setupGithubCommand: () => setupGithubCommand
5289
- });
5290
- import { createInterface as createInterface2 } from "readline/promises";
5291
- import { stdin as input, stdout as output } from "process";
5292
- import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
5293
- import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
5294
- import { homedir as homedir5, platform as platform2 } from "os";
5295
- import { join as join5 } from "path";
5296
- import { execFile as execFile2 } from "child_process";
5297
- function readConfig() {
5298
- if (!existsSync6(CONFIG_PATH)) return {};
5299
- const out = {};
5300
- for (const line of readFileSync5(CONFIG_PATH, "utf-8").split("\n")) {
5301
- const t = line.trim();
5302
- if (!t || t.startsWith("#")) continue;
5303
- const eq = t.indexOf("=");
5304
- if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
5279
+ // cli/installer/promptFetcher.ts
5280
+ async function fetchJudgePrompts(opts) {
5281
+ const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/hook/config`;
5282
+ const resp = await fetch(url, {
5283
+ method: "GET",
5284
+ headers: {
5285
+ "Authorization": `Bearer ${opts.jwt}`,
5286
+ "User-Agent": "synkro-cli/1.0"
5287
+ },
5288
+ signal: AbortSignal.timeout(5e3)
5289
+ });
5290
+ if (!resp.ok) {
5291
+ return { version: "unknown" };
5305
5292
  }
5306
- return out;
5293
+ const data = await resp.json();
5294
+ return { version: data.prompts?.version ?? "unknown" };
5307
5295
  }
5308
- async function prompt(rl, q, opts = {}) {
5309
- if (opts.silent) {
5310
- process.stdout.write(q);
5311
- const wasRaw = process.stdin.isRaw;
5312
- if (process.stdin.setRawMode) process.stdin.setRawMode(true);
5313
- return await new Promise((resolve3) => {
5314
- let chunk = "";
5315
- const onData = (data) => {
5316
- const s = data.toString("utf-8");
5317
- if (s === "\r" || s === "\n" || s === "\r\n") {
5318
- process.stdin.removeListener("data", onData);
5319
- if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
5320
- process.stdout.write("\n");
5321
- resolve3(chunk);
5322
- return;
5323
- }
5324
- if (s === "") process.exit(130);
5325
- if (s === "\x7F" || s === "\b") {
5326
- chunk = chunk.slice(0, -1);
5327
- return;
5328
- }
5329
- chunk += s;
5330
- };
5331
- process.stdin.on("data", onData);
5332
- });
5296
+ var init_promptFetcher = __esm({
5297
+ "cli/installer/promptFetcher.ts"() {
5298
+ "use strict";
5333
5299
  }
5334
- return await rl.question(q);
5300
+ });
5301
+
5302
+ // cli/local-cc/macKeychain.ts
5303
+ var macKeychain_exports = {};
5304
+ __export(macKeychain_exports, {
5305
+ CLAUDE_CREDS_DIR: () => CLAUDE_CREDS_DIR,
5306
+ CLAUDE_CREDS_FILE: () => CLAUDE_CREDS_FILE,
5307
+ CURSOR_API_KEY_FILE: () => CURSOR_API_KEY_FILE,
5308
+ CURSOR_CREDS_DIR: () => CURSOR_CREDS_DIR,
5309
+ KeychainExportError: () => KeychainExportError,
5310
+ SYNKRO_DIR: () => SYNKRO_DIR,
5311
+ credsAreStale: () => credsAreStale,
5312
+ cursorApiKeyConfigured: () => cursorApiKeyConfigured,
5313
+ exportKeychainCreds: () => exportKeychainCreds,
5314
+ loadRefreshAgent: () => loadRefreshAgent,
5315
+ needsKeychainBridge: () => needsKeychainBridge,
5316
+ readExportedCreds: () => readExportedCreds,
5317
+ readKeychainCreds: () => readKeychainCreds,
5318
+ refreshCreds: () => refreshCreds,
5319
+ uninstallRefreshAgent: () => uninstallRefreshAgent,
5320
+ validateCursorApiKey: () => validateCursorApiKey,
5321
+ writeCursorApiKey: () => writeCursorApiKey,
5322
+ writeRefreshAgent: () => writeRefreshAgent
5323
+ });
5324
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, chmodSync, readFileSync as readFileSync5, statSync } from "fs";
5325
+ import { homedir as homedir5, platform as platform2 } from "os";
5326
+ import { join as join5 } from "path";
5327
+ import { spawnSync } from "child_process";
5328
+ function needsKeychainBridge() {
5329
+ return platform2() === "darwin";
5335
5330
  }
5336
- function openBrowser3(url) {
5337
- const os = platform2();
5338
- let bin;
5339
- let args2;
5340
- switch (os) {
5341
- case "darwin":
5342
- bin = "open";
5343
- args2 = [url];
5344
- break;
5345
- case "win32":
5346
- bin = "cmd";
5347
- args2 = ["/c", "start", "", url];
5348
- break;
5349
- default:
5350
- bin = "xdg-open";
5351
- args2 = [url];
5352
- break;
5353
- }
5354
- execFile2(bin, args2, () => {
5331
+ function readKeychainCreds() {
5332
+ if (platform2() !== "darwin") return null;
5333
+ const r = spawnSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
5334
+ encoding: "utf-8",
5335
+ timeout: 5e3
5355
5336
  });
5337
+ if (r.status !== 0) return null;
5338
+ const blob = (r.stdout || "").trim();
5339
+ return blob || null;
5356
5340
  }
5357
- function sleep(ms) {
5358
- return new Promise((r) => setTimeout(r, ms));
5359
- }
5360
- function captureClaudeSetupToken() {
5361
- const tmpFile = join5(SYNKRO_DIR, `token-capture-${Date.now()}.raw`);
5362
- return new Promise((resolve3, reject) => {
5363
- const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
5364
- stdio: "inherit"
5365
- });
5366
- proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
5367
- proc.on("close", (code) => {
5368
- let raw = "";
5369
- try {
5370
- raw = readFileSync5(tmpFile, "utf-8");
5371
- } catch (e) {
5372
- reject(new Error(`Could not read script output file: ${e.message}`));
5373
- return;
5374
- }
5375
- try {
5376
- unlinkSync3(tmpFile);
5377
- } catch {
5378
- }
5379
- if (code !== 0) {
5380
- reject(new Error(`claude setup-token exited with code ${code}`));
5381
- return;
5382
- }
5383
- const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
5384
- let yellow = "";
5385
- let m;
5386
- while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
5387
- const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
5388
- if (!token) {
5389
- reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
5390
- return;
5391
- }
5392
- resolve3(token[0]);
5393
- });
5394
- });
5341
+ function exportKeychainCreds() {
5342
+ const blob = readKeychainCreds();
5343
+ if (!blob) return null;
5344
+ mkdirSync6(CLAUDE_CREDS_DIR, { recursive: true });
5345
+ chmodSync(CLAUDE_CREDS_DIR, 448);
5346
+ writeFileSync6(CLAUDE_CREDS_FILE, blob, "utf-8");
5347
+ chmodSync(CLAUDE_CREDS_FILE, 384);
5348
+ return CLAUDE_CREDS_FILE;
5395
5349
  }
5396
- async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
5397
- const resp = await fetch(`${gatewayUrl}${path}`, {
5398
- ...opts,
5399
- headers: {
5400
- "Authorization": `Bearer ${jwt2}`,
5401
- "Content-Type": "application/json",
5402
- ...opts.headers || {}
5403
- }
5404
- });
5405
- if (!resp.ok) {
5406
- const text = await resp.text().catch(() => "");
5407
- throw new Error(`API ${resp.status}: ${text.slice(0, 200)}`);
5350
+ function cursorApiKeyConfigured() {
5351
+ try {
5352
+ return existsSync6(CURSOR_API_KEY_FILE) && readFileSync5(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
5353
+ } catch {
5354
+ return false;
5408
5355
  }
5409
- return resp.json();
5410
5356
  }
5411
- async function connectGitHub(gatewayUrl, jwt2, opts = {}) {
5357
+ function writeCursorApiKey(key) {
5358
+ const trimmed = key.trim();
5359
+ if (!trimmed) return;
5360
+ mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
5361
+ chmodSync(CURSOR_CREDS_DIR, 448);
5362
+ writeFileSync6(CURSOR_API_KEY_FILE, trimmed, "utf-8");
5363
+ chmodSync(CURSOR_API_KEY_FILE, 384);
5364
+ }
5365
+ async function validateCursorApiKey() {
5366
+ let key;
5412
5367
  try {
5413
- const result = await apiCall(
5414
- gatewayUrl,
5415
- jwt2,
5416
- "/api/v1/cli/github-token"
5417
- );
5418
- if (result.connected && result.token) {
5419
- if (!opts.silent) console.log(" \u2713 GitHub already connected via Synkro.");
5420
- return result.token;
5421
- }
5422
- } catch {
5423
- }
5424
- if (!opts.silent) console.log(" Opening browser to authorize GitHub...");
5425
- try {
5426
- const authResp = await apiCall(
5427
- gatewayUrl,
5428
- jwt2,
5429
- "/api/pipes-widget/authorize/github",
5430
- { method: "POST", body: "{}" }
5431
- );
5432
- openBrowser3(authResp.url);
5433
- if (!opts.silent) console.log(" Waiting for authorization...");
5434
- } catch (err) {
5435
- if (!opts.silent) console.error(` Failed to start GitHub authorization: ${err.message}`);
5436
- return null;
5437
- }
5438
- const deadline = Date.now() + 12e4;
5439
- while (Date.now() < deadline) {
5440
- await sleep(2e3);
5441
- try {
5442
- const result = await apiCall(
5443
- gatewayUrl,
5444
- jwt2,
5445
- "/api/v1/cli/github-token"
5446
- );
5447
- if (result.connected && result.token) {
5448
- if (!opts.silent) console.log("\n \u2713 GitHub connected!");
5449
- return result.token;
5450
- }
5451
- } catch {
5452
- }
5453
- if (!opts.silent) process.stdout.write(".");
5454
- }
5455
- if (!opts.silent) console.error("\n Timed out waiting for GitHub authorization.");
5456
- return null;
5457
- }
5458
- async function setupGithubCommand(opts = {}) {
5459
- if (!isAuthenticated()) {
5460
- console.error("Not authenticated. Run `synkro-cli login` first.");
5461
- process.exit(1);
5462
- }
5463
- const config = readConfig();
5464
- const gatewayUrl = (config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
5465
- const jwt2 = getAccessToken();
5466
- if (!jwt2) {
5467
- console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
5468
- process.exit(1);
5469
- }
5470
- console.log("Requesting CI API key from Synkro...");
5471
- let synkroCiApiKey;
5472
- try {
5473
- const minted = await apiCall(
5474
- gatewayUrl,
5475
- jwt2,
5476
- "/api/v1/cli/ci-api-key",
5477
- { method: "POST", body: "{}" }
5478
- );
5479
- synkroCiApiKey = minted.api_key;
5480
- console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
5481
- } catch (err) {
5482
- console.error(`Failed to mint CI API key: ${err.message}`);
5483
- process.exit(1);
5484
- }
5485
- let ghToken;
5486
- if (opts.githubToken) {
5487
- ghToken = opts.githubToken;
5488
- } else if (opts.nonInteractive) {
5489
- try {
5490
- const result = await apiCall(
5491
- gatewayUrl,
5492
- jwt2,
5493
- "/api/v1/cli/github-token"
5494
- );
5495
- if (result.connected && result.token) {
5496
- ghToken = result.token;
5497
- } else {
5498
- throw new Error("not connected");
5499
- }
5500
- } catch {
5501
- try {
5502
- ghToken = execSync4("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
5503
- } catch {
5504
- console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
5505
- return;
5506
- }
5507
- }
5508
- } else {
5509
- console.log("\nConnecting to GitHub...");
5510
- const token = await connectGitHub(gatewayUrl, jwt2);
5511
- if (!token) {
5512
- console.error("GitHub connection failed. Try again.");
5513
- process.exit(1);
5514
- }
5515
- ghToken = token;
5516
- console.log();
5517
- }
5518
- let claudeToken;
5519
- if (!opts.skipClaudeToken) {
5520
- console.log("Generating Claude Code OAuth token...");
5521
- console.log(" A browser window will open \u2014 authorize with your Claude account.\n");
5522
- try {
5523
- claudeToken = await captureClaudeSetupToken();
5524
- } catch (err) {
5525
- console.error(`Failed to get Claude token: ${err instanceof Error ? err.message : String(err)}`);
5526
- if (opts.nonInteractive) return;
5527
- process.exit(1);
5528
- }
5529
- if (!claudeToken.startsWith("sk-ant-oat01-")) {
5530
- console.error("Invalid token received from `claude setup-token`. Expected sk-ant-oat01-...");
5531
- if (opts.nonInteractive) return;
5532
- process.exit(1);
5533
- }
5534
- console.log(" Validating token...");
5535
- try {
5536
- const validateResult = execSync4(
5537
- 'claude --print --output-format json "say ok"',
5538
- { env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
5539
- );
5540
- const result = JSON.parse(validateResult);
5541
- if (result.is_error) throw new Error(result.result || "auth failed");
5542
- console.log(" \u2713 Token validated.\n");
5543
- } catch (err) {
5544
- console.error(`Token validation failed: ${err instanceof Error ? err.message : String(err)}`);
5545
- if (opts.nonInteractive) return;
5546
- process.exit(1);
5547
- }
5548
- }
5549
- let selected;
5550
- if (opts.nonInteractive) {
5551
- let currentFullName = null;
5552
- try {
5553
- const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
5554
- const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
5555
- if (m) currentFullName = m[1];
5556
- } catch {
5557
- }
5558
- if (!currentFullName) {
5559
- console.warn(" \u26A0 Not in a GitHub repo. Skipping PR scan setup.");
5560
- return;
5561
- }
5562
- const [owner, repo] = currentFullName.split("/");
5563
- selected = [{ owner, repo, full_name: currentFullName }];
5564
- console.log(` Auto-selected repo: ${currentFullName}`);
5565
- } else {
5566
- console.log("Fetching accessible repos...");
5567
- const repos = await listAccessibleRepos({ token: ghToken });
5568
- if (repos.length === 0) {
5569
- console.error("No accessible repos found. Check your GitHub permissions.");
5570
- process.exit(1);
5571
- }
5572
- console.log(`
5573
- Found ${repos.length} accessible repo(s):
5574
- `);
5575
- repos.slice(0, 100).forEach((r, i) => {
5576
- console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
5577
- });
5578
- console.log();
5579
- const rl2 = createInterface2({ input, output });
5580
- const selectionRaw = await prompt(rl2, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
5581
- const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
5582
- if (selectedIdx.length === 0) {
5583
- console.error("No valid selections.");
5584
- rl2.close();
5585
- process.exit(1);
5586
- }
5587
- selected = selectedIdx.map((i) => repos[i]);
5588
- console.log(`
5589
- Will push secrets to ${selected.length} repo(s):`);
5590
- for (const r of selected) console.log(` \u2022 ${r.full_name}`);
5591
- console.log();
5592
- const confirm = (await prompt(rl2, "Continue? (yes/no): ")).trim().toLowerCase();
5593
- if (confirm !== "yes" && confirm !== "y") {
5594
- console.log("Cancelled.");
5595
- rl2.close();
5596
- process.exit(0);
5597
- }
5598
- rl2.close();
5599
- }
5600
- console.log();
5601
- for (const r of selected) {
5602
- process.stdout.write(`Pushing secrets to ${r.full_name}... `);
5603
- try {
5604
- await pushSecretsToRepo(
5605
- { token: ghToken },
5606
- r.owner,
5607
- r.repo,
5608
- {
5609
- claudeCodeOauthToken: claudeToken,
5610
- synkroApiKey: synkroCiApiKey
5611
- }
5612
- );
5613
- console.log("\u2713");
5614
- } catch (err) {
5615
- console.log(`\u2717 (${err.message})`);
5616
- }
5617
- }
5618
- console.log();
5619
- const gitRoot = findGitRoot(process.cwd());
5620
- if (gitRoot) {
5621
- const written = writeWorkflowFile(gitRoot);
5622
- if (written) {
5623
- console.log(`Wrote workflow: ${written}`);
5624
- console.log("Commit and push it to enable PR scanning.");
5625
- }
5626
- } else {
5627
- console.log("Not in a git repo. To enable scanning, add this file to your repo:");
5628
- console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
5629
- console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
5630
- }
5631
- console.log();
5632
- console.log("\u2713 PR scan setup complete.");
5633
- console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
5634
- console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
5635
- }
5636
- var SYNKRO_DIR, CONFIG_PATH;
5637
- var init_setupGithub = __esm({
5638
- "cli/commands/setupGithub.ts"() {
5639
- "use strict";
5640
- init_githubSetup();
5641
- init_stub();
5642
- SYNKRO_DIR = join5(homedir5(), ".synkro");
5643
- CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
5644
- }
5645
- });
5646
-
5647
- // cli/installer/promptFetcher.ts
5648
- async function fetchJudgePrompts(opts) {
5649
- const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/hook/config`;
5650
- const resp = await fetch(url, {
5651
- method: "GET",
5652
- headers: {
5653
- "Authorization": `Bearer ${opts.jwt}`,
5654
- "User-Agent": "synkro-cli/1.0"
5655
- },
5656
- signal: AbortSignal.timeout(5e3)
5657
- });
5658
- if (!resp.ok) {
5659
- return { version: "unknown" };
5660
- }
5661
- const data = await resp.json();
5662
- return { version: data.prompts?.version ?? "unknown" };
5663
- }
5664
- var init_promptFetcher = __esm({
5665
- "cli/installer/promptFetcher.ts"() {
5666
- "use strict";
5667
- }
5668
- });
5669
-
5670
- // cli/local-cc/macKeychain.ts
5671
- var macKeychain_exports = {};
5672
- __export(macKeychain_exports, {
5673
- CLAUDE_CREDS_DIR: () => CLAUDE_CREDS_DIR,
5674
- CLAUDE_CREDS_FILE: () => CLAUDE_CREDS_FILE,
5675
- CURSOR_API_KEY_FILE: () => CURSOR_API_KEY_FILE,
5676
- CURSOR_CREDS_DIR: () => CURSOR_CREDS_DIR,
5677
- KeychainExportError: () => KeychainExportError,
5678
- SYNKRO_DIR: () => SYNKRO_DIR2,
5679
- credsAreStale: () => credsAreStale,
5680
- cursorApiKeyConfigured: () => cursorApiKeyConfigured,
5681
- exportKeychainCreds: () => exportKeychainCreds,
5682
- loadRefreshAgent: () => loadRefreshAgent,
5683
- needsKeychainBridge: () => needsKeychainBridge,
5684
- readExportedCreds: () => readExportedCreds,
5685
- readKeychainCreds: () => readKeychainCreds,
5686
- refreshCreds: () => refreshCreds,
5687
- uninstallRefreshAgent: () => uninstallRefreshAgent,
5688
- validateCursorApiKey: () => validateCursorApiKey,
5689
- writeCursorApiKey: () => writeCursorApiKey,
5690
- writeRefreshAgent: () => writeRefreshAgent
5691
- });
5692
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
5693
- import { homedir as homedir6, platform as platform3 } from "os";
5694
- import { join as join6 } from "path";
5695
- import { spawnSync } from "child_process";
5696
- function needsKeychainBridge() {
5697
- return platform3() === "darwin";
5698
- }
5699
- function readKeychainCreds() {
5700
- if (platform3() !== "darwin") return null;
5701
- const r = spawnSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"], {
5702
- encoding: "utf-8",
5703
- timeout: 5e3
5704
- });
5705
- if (r.status !== 0) return null;
5706
- const blob = (r.stdout || "").trim();
5707
- return blob || null;
5708
- }
5709
- function exportKeychainCreds() {
5710
- const blob = readKeychainCreds();
5711
- if (!blob) return null;
5712
- mkdirSync6(CLAUDE_CREDS_DIR, { recursive: true });
5713
- chmodSync(CLAUDE_CREDS_DIR, 448);
5714
- writeFileSync6(CLAUDE_CREDS_FILE, blob, "utf-8");
5715
- chmodSync(CLAUDE_CREDS_FILE, 384);
5716
- return CLAUDE_CREDS_FILE;
5717
- }
5718
- function cursorApiKeyConfigured() {
5719
- try {
5720
- return existsSync7(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
5721
- } catch {
5722
- return false;
5723
- }
5724
- }
5725
- function writeCursorApiKey(key) {
5726
- const trimmed = key.trim();
5727
- if (!trimmed) return;
5728
- mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
5729
- chmodSync(CURSOR_CREDS_DIR, 448);
5730
- writeFileSync6(CURSOR_API_KEY_FILE, trimmed, "utf-8");
5731
- chmodSync(CURSOR_API_KEY_FILE, 384);
5732
- }
5733
- async function validateCursorApiKey() {
5734
- let key;
5735
- try {
5736
- key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
5368
+ key = readFileSync5(CURSOR_API_KEY_FILE, "utf-8").trim();
5737
5369
  } catch {
5738
5370
  return null;
5739
5371
  }
@@ -5752,7 +5384,7 @@ async function validateCursorApiKey() {
5752
5384
  }
5753
5385
  }
5754
5386
  function credsAreStale() {
5755
- if (!existsSync7(CLAUDE_CREDS_FILE)) return true;
5387
+ if (!existsSync6(CLAUDE_CREDS_FILE)) return true;
5756
5388
  try {
5757
5389
  const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
5758
5390
  return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
@@ -5761,10 +5393,10 @@ function credsAreStale() {
5761
5393
  }
5762
5394
  }
5763
5395
  function writeRefreshAgent(synkroBinPath) {
5764
- if (platform3() !== "darwin") {
5396
+ if (platform2() !== "darwin") {
5765
5397
  throw new KeychainExportError("writeRefreshAgent is darwin-only");
5766
5398
  }
5767
- mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
5399
+ mkdirSync6(join5(homedir5(), "Library", "LaunchAgents"), { recursive: true });
5768
5400
  const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
5769
5401
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
5770
5402
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -5783,9 +5415,9 @@ function writeRefreshAgent(synkroBinPath) {
5783
5415
  <key>RunAtLoad</key>
5784
5416
  <true/>
5785
5417
  <key>StandardErrorPath</key>
5786
- <string>${join6(SYNKRO_DIR2, "claude-creds-refresh.log")}</string>
5418
+ <string>${join5(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
5787
5419
  <key>StandardOutPath</key>
5788
- <string>${join6(SYNKRO_DIR2, "claude-creds-refresh.log")}</string>
5420
+ <string>${join5(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
5789
5421
  </dict>
5790
5422
  </plist>
5791
5423
  `;
@@ -5793,7 +5425,7 @@ function writeRefreshAgent(synkroBinPath) {
5793
5425
  return LAUNCHD_PLIST;
5794
5426
  }
5795
5427
  function loadRefreshAgent() {
5796
- if (platform3() !== "darwin") return;
5428
+ if (platform2() !== "darwin") return;
5797
5429
  spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
5798
5430
  encoding: "utf-8",
5799
5431
  timeout: 5e3
@@ -5809,13 +5441,13 @@ function loadRefreshAgent() {
5809
5441
  }
5810
5442
  }
5811
5443
  function uninstallRefreshAgent() {
5812
- if (platform3() !== "darwin") return;
5444
+ if (platform2() !== "darwin") return;
5813
5445
  spawnSync("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, LAUNCHD_PLIST], {
5814
5446
  encoding: "utf-8",
5815
5447
  timeout: 5e3
5816
5448
  });
5817
5449
  try {
5818
- if (existsSync7(LAUNCHD_PLIST)) {
5450
+ if (existsSync6(LAUNCHD_PLIST)) {
5819
5451
  __require("fs").unlinkSync(LAUNCHD_PLIST);
5820
5452
  }
5821
5453
  } catch {
@@ -5827,23 +5459,23 @@ function refreshCreds() {
5827
5459
  }
5828
5460
  function readExportedCreds() {
5829
5461
  try {
5830
- return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
5462
+ return readFileSync5(CLAUDE_CREDS_FILE, "utf-8");
5831
5463
  } catch {
5832
5464
  return null;
5833
5465
  }
5834
5466
  }
5835
- var SYNKRO_DIR2, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_INTERVAL_SECONDS, KeychainExportError;
5467
+ var SYNKRO_DIR, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_INTERVAL_SECONDS, KeychainExportError;
5836
5468
  var init_macKeychain = __esm({
5837
5469
  "cli/local-cc/macKeychain.ts"() {
5838
5470
  "use strict";
5839
- SYNKRO_DIR2 = join6(homedir6(), ".synkro");
5840
- CLAUDE_CREDS_DIR = join6(SYNKRO_DIR2, "claude-creds");
5841
- CLAUDE_CREDS_FILE = join6(CLAUDE_CREDS_DIR, ".credentials.json");
5842
- CURSOR_CREDS_DIR = join6(SYNKRO_DIR2, "cursor-creds");
5843
- CURSOR_API_KEY_FILE = join6(CURSOR_CREDS_DIR, "api-key");
5471
+ SYNKRO_DIR = join5(homedir5(), ".synkro");
5472
+ CLAUDE_CREDS_DIR = join5(SYNKRO_DIR, "claude-creds");
5473
+ CLAUDE_CREDS_FILE = join5(CLAUDE_CREDS_DIR, ".credentials.json");
5474
+ CURSOR_CREDS_DIR = join5(SYNKRO_DIR, "cursor-creds");
5475
+ CURSOR_API_KEY_FILE = join5(CURSOR_CREDS_DIR, "api-key");
5844
5476
  KEYCHAIN_SERVICE = "Claude Code-credentials";
5845
5477
  LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
5846
- LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5478
+ LAUNCHD_PLIST = join5(homedir5(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5847
5479
  REFRESH_INTERVAL_SECONDS = 45 * 60;
5848
5480
  KeychainExportError = class extends Error {
5849
5481
  constructor(message, cause) {
@@ -5860,7 +5492,7 @@ var init_macKeychain = __esm({
5860
5492
  var dockerInstall_exports = {};
5861
5493
  __export(dockerInstall_exports, {
5862
5494
  DockerInstallError: () => DockerInstallError,
5863
- SYNKRO_DIR: () => SYNKRO_DIR3,
5495
+ SYNKRO_DIR: () => SYNKRO_DIR2,
5864
5496
  assertDockerAvailable: () => assertDockerAvailable,
5865
5497
  dockerInstall: () => dockerInstall,
5866
5498
  dockerRemove: () => dockerRemove,
@@ -5876,9 +5508,9 @@ __export(dockerInstall_exports, {
5876
5508
  splitWorkers: () => splitWorkers,
5877
5509
  waitForContainerReady: () => waitForContainerReady
5878
5510
  });
5879
- import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync } from "fs";
5880
- import { homedir as homedir7 } from "os";
5881
- import { join as join7 } from "path";
5511
+ import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync } from "fs";
5512
+ import { homedir as homedir6 } from "os";
5513
+ import { join as join6 } from "path";
5882
5514
  import { spawnSync as spawnSync2 } from "child_process";
5883
5515
  function splitWorkers(total, providers) {
5884
5516
  const t = Math.max(0, Math.floor(total));
@@ -5953,7 +5585,7 @@ function assertDockerAvailable() {
5953
5585
  }
5954
5586
  function claudeCredsHostDir() {
5955
5587
  if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
5956
- return join7(homedir7(), ".claude");
5588
+ return join6(homedir6(), ".claude");
5957
5589
  }
5958
5590
  function resolveSynkroBin() {
5959
5591
  const which2 = spawnSync2("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
@@ -5969,11 +5601,11 @@ async function dockerInstall(opts = {}) {
5969
5601
  mkdirSync7(PGDATA_PATH, { recursive: true });
5970
5602
  mkdirSync7(BACKUP_DIR, { recursive: true });
5971
5603
  mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
5972
- const hostClaudeJson = join7(homedir7(), ".claude.json");
5973
- if (existsSync8(hostClaudeJson)) {
5604
+ const hostClaudeJson = join6(homedir6(), ".claude.json");
5605
+ if (existsSync7(hostClaudeJson)) {
5974
5606
  copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
5975
5607
  }
5976
- if (!existsSync8(MCP_JWT_PATH)) {
5608
+ if (!existsSync7(MCP_JWT_PATH)) {
5977
5609
  throw new DockerInstallError(
5978
5610
  `MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
5979
5611
  );
@@ -6001,7 +5633,7 @@ async function dockerInstall(opts = {}) {
6001
5633
  console.warn(` Plist written to ${plist} \u2014 load manually with launchctl bootstrap when ready.`);
6002
5634
  }
6003
5635
  } else {
6004
- mkdirSync7(join7(homedir7(), ".claude"), { recursive: true });
5636
+ mkdirSync7(join6(homedir6(), ".claude"), { recursive: true });
6005
5637
  }
6006
5638
  console.log(` Pulling ${image}...`);
6007
5639
  const pull = spawnSync2("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
@@ -6039,11 +5671,11 @@ async function dockerInstall(opts = {}) {
6039
5671
  // sidesteps Docker Desktop for macOS's unreliable single-file bind mounts
6040
5672
  // (which previously left a dangling symlink that blocked container start).
6041
5673
  "-v",
6042
- `${SYNKRO_DIR3}:/data/synkro-host:ro`,
5674
+ `${SYNKRO_DIR2}:/data/synkro-host:ro`,
6043
5675
  "-v",
6044
5676
  `${credsDir}:/home/synkro/.claude:rw`,
6045
5677
  "-v",
6046
- `${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
5678
+ `${join6(homedir6(), ".claude")}:/data/claude-host:ro`,
6047
5679
  "-v",
6048
5680
  `${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
6049
5681
  // Cursor creds — mounted RW so the in-container refresher can rotate the
@@ -6087,203 +5719,566 @@ async function waitForContainerReady(timeoutMs = 6e4) {
6087
5719
  }
6088
5720
  await new Promise((r) => setTimeout(r, 1e3));
6089
5721
  }
6090
- return false;
6091
- }
6092
- function dockerRemove() {
6093
- spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
6094
- }
6095
- function dockerStop() {
6096
- spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
6097
- spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
5722
+ return false;
5723
+ }
5724
+ function dockerRemove() {
5725
+ spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
5726
+ }
5727
+ function dockerStop() {
5728
+ spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
5729
+ spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
5730
+ }
5731
+ async function dockerUpdate(opts = {}) {
5732
+ if (dockerStatus().running) {
5733
+ await dockerSafeStop();
5734
+ }
5735
+ dockerRemove();
5736
+ await dockerInstall(opts);
5737
+ }
5738
+ function dockerStatus() {
5739
+ const r = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
5740
+ encoding: "utf-8",
5741
+ timeout: 5e3
5742
+ });
5743
+ const status = (r.stdout || "").trim();
5744
+ if (status !== "running") return { running: false };
5745
+ return {
5746
+ running: true,
5747
+ image: imageTag(),
5748
+ healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
5749
+ };
5750
+ }
5751
+ function readContainerConfig() {
5752
+ const r = spawnSync2("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
5753
+ encoding: "utf-8",
5754
+ timeout: 5e3
5755
+ });
5756
+ if (r.status !== 0 || !r.stdout) return null;
5757
+ let env;
5758
+ try {
5759
+ env = JSON.parse(r.stdout.trim());
5760
+ } catch {
5761
+ return null;
5762
+ }
5763
+ if (!Array.isArray(env)) return null;
5764
+ const get = (k) => {
5765
+ const hit = env.find((e) => typeof e === "string" && e.startsWith(k + "="));
5766
+ return hit ? hit.slice(k.length + 1) : void 0;
5767
+ };
5768
+ const num = (s) => {
5769
+ if (s === void 0) return void 0;
5770
+ const n = parseInt(s, 10);
5771
+ return Number.isFinite(n) ? n : void 0;
5772
+ };
5773
+ return {
5774
+ claudeWorkers: num(get("CLAUDE_WORKERS")),
5775
+ cursorWorkers: num(get("CURSOR_WORKERS")),
5776
+ connectedRepo: get("SYNKRO_CONNECTED_REPO") || void 0
5777
+ };
5778
+ }
5779
+ async function dockerSafeStop() {
5780
+ const status = dockerStatus();
5781
+ if (!status.running) {
5782
+ console.log(" Container is not running.");
5783
+ return { ok: true, pgdataCheck: checkPgdata() };
5784
+ }
5785
+ console.log(" Requesting data snapshot before shutdown...");
5786
+ let snapshot = { ok: false, error: "not attempted" };
5787
+ try {
5788
+ const resp = await fetch(`http://127.0.0.1:${HOST_MCP_PORT}/api/local/snapshot`, {
5789
+ method: "POST",
5790
+ headers: { "Content-Type": "application/json" },
5791
+ body: JSON.stringify({ reason: "cli-stop" }),
5792
+ signal: AbortSignal.timeout(2e4)
5793
+ });
5794
+ if (resp.ok) {
5795
+ snapshot = { ok: true };
5796
+ console.log(" \u2713 Snapshot saved.");
5797
+ } else {
5798
+ snapshot = { ok: false, error: `HTTP ${resp.status}` };
5799
+ console.warn(` \u26A0 Snapshot request failed (HTTP ${resp.status}). Proceeding with stop.`);
5800
+ }
5801
+ } catch (e) {
5802
+ snapshot = { ok: false, error: String(e).slice(0, 100) };
5803
+ console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
5804
+ }
5805
+ console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
5806
+ const stop = spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
5807
+ encoding: "utf-8",
5808
+ timeout: 45e3
5809
+ });
5810
+ const inspect = spawnSync2("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
5811
+ encoding: "utf-8",
5812
+ timeout: 5e3
5813
+ });
5814
+ const exitCode = parseInt((inspect.stdout || "").trim(), 10);
5815
+ if (exitCode === 0) {
5816
+ console.log(" \u2713 Container stopped cleanly (exit 0).");
5817
+ } else {
5818
+ console.warn(` \u26A0 Container exited with code ${exitCode}.`);
5819
+ }
5820
+ const pgCheck = checkPgdata();
5821
+ if (pgCheck.healthy) {
5822
+ console.log(` \u2713 pgdata looks healthy: ${pgCheck.details}`);
5823
+ } else {
5824
+ console.warn(` \u26A0 pgdata check: ${pgCheck.details}`);
5825
+ }
5826
+ return { ok: stop.status === 0, snapshot, exitCode, pgdataCheck: pgCheck };
5827
+ }
5828
+ async function dockerSafeStart() {
5829
+ const status = dockerStatus();
5830
+ if (status.running) {
5831
+ console.log(" Container is already running.");
5832
+ return { ok: true, pgdataState: "running" };
5833
+ }
5834
+ const exists = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
5835
+ encoding: "utf-8",
5836
+ timeout: 5e3
5837
+ });
5838
+ if (exists.status !== 0) {
5839
+ return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
5840
+ }
5841
+ const pgCheck = checkPgdata();
5842
+ if (existsSync7(PGDATA_PATH) && readdirSync(PGDATA_PATH).length > 0) {
5843
+ if (pgCheck.healthy) {
5844
+ console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
5845
+ } else {
5846
+ console.warn(` \u26A0 pgdata: ${pgCheck.details}`);
5847
+ console.log(" Starting anyway \u2014 entrypoint will attempt recovery from snapshots if needed.");
5848
+ }
5849
+ } else {
5850
+ console.log(" pgdata: no existing data \u2014 fresh start.");
5851
+ mkdirSync7(PGDATA_PATH, { recursive: true });
5852
+ }
5853
+ console.log(" Starting container...");
5854
+ const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
5855
+ encoding: "utf-8",
5856
+ timeout: 3e4
5857
+ });
5858
+ if (start.status !== 0) {
5859
+ return { ok: false, pgdataState: "start_failed", error: `docker start failed: ${(start.stderr || "").slice(0, 200)}` };
5860
+ }
5861
+ console.log(" Waiting for server to become healthy...");
5862
+ const ready = await waitForContainerReady(6e4);
5863
+ if (ready) {
5864
+ console.log(" \u2713 Server is healthy and ready.");
5865
+ return { ok: true, pgdataState: pgCheck.healthy ? "existing" : "recovered" };
5866
+ } else {
5867
+ return { ok: false, pgdataState: "unhealthy", error: "Server did not become healthy within 60s. Check: docker logs synkro-server" };
5868
+ }
5869
+ }
5870
+ async function dockerSafeRestart() {
5871
+ console.log(" === Stop ===");
5872
+ const stopResult = await dockerSafeStop();
5873
+ if (!stopResult.ok) {
5874
+ console.error(" Stop failed. Aborting restart.");
5875
+ return { ok: false, stop: stopResult, start: { ok: false, pgdataState: "not_started", error: "stop failed" } };
5876
+ }
5877
+ console.log("\n === Start ===");
5878
+ const startResult = await dockerSafeStart();
5879
+ return { ok: startResult.ok, stop: stopResult, start: startResult };
5880
+ }
5881
+ function checkPgdata() {
5882
+ if (!existsSync7(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
5883
+ const entries = readdirSync(PGDATA_PATH);
5884
+ if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
5885
+ const hasPidFile = entries.includes("postmaster.pid");
5886
+ const hasWalDir = entries.includes("pg_wal");
5887
+ const hasPgControl = entries.includes("global") || entries.includes("pg_control");
5888
+ if (hasPidFile) return { healthy: false, details: "stale postmaster.pid present (unclean shutdown)" };
5889
+ if (!hasWalDir) return { healthy: false, details: "pg_wal directory missing" };
5890
+ if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
5891
+ return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
5892
+ }
5893
+ var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
5894
+ var init_dockerInstall = __esm({
5895
+ "cli/local-cc/dockerInstall.ts"() {
5896
+ "use strict";
5897
+ init_agentDetect();
5898
+ init_macKeychain();
5899
+ SYNKRO_DIR2 = join6(homedir6(), ".synkro");
5900
+ MCP_JWT_PATH = join6(SYNKRO_DIR2, ".mcp-jwt");
5901
+ PGDATA_PATH = join6(SYNKRO_DIR2, "pgdata");
5902
+ CLAUDE_HOST_STATE_DIR = join6(SYNKRO_DIR2, "claude-host-state");
5903
+ CLAUDE_HOST_STATE_FILE = join6(CLAUDE_HOST_STATE_DIR, ".claude.json");
5904
+ HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
5905
+ HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
5906
+ HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
5907
+ HOST_PG_PORT = parseInt(process.env.SYNKRO_HOST_PG_PORT || "15433", 10);
5908
+ CONTAINER_NAME = "synkro-server";
5909
+ DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
5910
+ DockerInstallError = class extends Error {
5911
+ constructor(message, cause) {
5912
+ super(message);
5913
+ this.cause = cause;
5914
+ this.name = "DockerInstallError";
5915
+ }
5916
+ cause;
5917
+ };
5918
+ BACKUP_DIR = join6(SYNKRO_DIR2, "pgdata-backups");
5919
+ }
5920
+ });
5921
+
5922
+ // cli/commands/setupGithub.ts
5923
+ var setupGithub_exports = {};
5924
+ __export(setupGithub_exports, {
5925
+ connectGitHub: () => connectGitHub,
5926
+ setupGithubCommand: () => setupGithubCommand
5927
+ });
5928
+ import { createInterface as createInterface2 } from "readline/promises";
5929
+ import { stdin as input, stdout as output } from "process";
5930
+ import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
5931
+ import { existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
5932
+ import { homedir as homedir7, platform as platform4 } from "os";
5933
+ import { join as join7 } from "path";
5934
+ import { execFile as execFile2 } from "child_process";
5935
+ function readConfig() {
5936
+ if (!existsSync8(CONFIG_PATH)) return {};
5937
+ const out = {};
5938
+ for (const line of readFileSync6(CONFIG_PATH, "utf-8").split("\n")) {
5939
+ const t = line.trim();
5940
+ if (!t || t.startsWith("#")) continue;
5941
+ const eq = t.indexOf("=");
5942
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
5943
+ }
5944
+ return out;
5945
+ }
5946
+ async function prompt(rl, q, opts = {}) {
5947
+ if (opts.silent) {
5948
+ process.stdout.write(q);
5949
+ const wasRaw = process.stdin.isRaw;
5950
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
5951
+ return await new Promise((resolve3) => {
5952
+ let chunk = "";
5953
+ const onData = (data) => {
5954
+ const s = data.toString("utf-8");
5955
+ if (s === "\r" || s === "\n" || s === "\r\n") {
5956
+ process.stdin.removeListener("data", onData);
5957
+ if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
5958
+ process.stdout.write("\n");
5959
+ resolve3(chunk);
5960
+ return;
5961
+ }
5962
+ if (s === "") process.exit(130);
5963
+ if (s === "\x7F" || s === "\b") {
5964
+ chunk = chunk.slice(0, -1);
5965
+ return;
5966
+ }
5967
+ chunk += s;
5968
+ };
5969
+ process.stdin.on("data", onData);
5970
+ });
5971
+ }
5972
+ return await rl.question(q);
5973
+ }
5974
+ function openBrowser3(url) {
5975
+ const os = platform4();
5976
+ let bin;
5977
+ let args2;
5978
+ switch (os) {
5979
+ case "darwin":
5980
+ bin = "open";
5981
+ args2 = [url];
5982
+ break;
5983
+ case "win32":
5984
+ bin = "cmd";
5985
+ args2 = ["/c", "start", "", url];
5986
+ break;
5987
+ default:
5988
+ bin = "xdg-open";
5989
+ args2 = [url];
5990
+ break;
5991
+ }
5992
+ execFile2(bin, args2, () => {
5993
+ });
6098
5994
  }
6099
- async function dockerUpdate(opts = {}) {
6100
- if (dockerStatus().running) {
6101
- await dockerSafeStop();
6102
- }
6103
- dockerRemove();
6104
- await dockerInstall(opts);
5995
+ function sleep(ms) {
5996
+ return new Promise((r) => setTimeout(r, ms));
6105
5997
  }
6106
- function dockerStatus() {
6107
- const r = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
6108
- encoding: "utf-8",
6109
- timeout: 5e3
5998
+ function captureClaudeSetupToken() {
5999
+ const tmpFile = join7(SYNKRO_DIR3, `token-capture-${Date.now()}.raw`);
6000
+ return new Promise((resolve3, reject) => {
6001
+ const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
6002
+ stdio: "inherit"
6003
+ });
6004
+ proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
6005
+ proc.on("close", (code) => {
6006
+ let raw = "";
6007
+ try {
6008
+ raw = readFileSync6(tmpFile, "utf-8");
6009
+ } catch (e) {
6010
+ reject(new Error(`Could not read script output file: ${e.message}`));
6011
+ return;
6012
+ }
6013
+ try {
6014
+ unlinkSync3(tmpFile);
6015
+ } catch {
6016
+ }
6017
+ if (code !== 0) {
6018
+ reject(new Error(`claude setup-token exited with code ${code}`));
6019
+ return;
6020
+ }
6021
+ const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
6022
+ let yellow = "";
6023
+ let m;
6024
+ while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
6025
+ const token = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
6026
+ if (!token) {
6027
+ reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
6028
+ return;
6029
+ }
6030
+ resolve3(token[0]);
6031
+ });
6110
6032
  });
6111
- const status = (r.stdout || "").trim();
6112
- if (status !== "running") return { running: false };
6113
- return {
6114
- running: true,
6115
- image: imageTag(),
6116
- healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
6117
- };
6118
6033
  }
6119
- function readContainerConfig() {
6120
- const r = spawnSync2("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
6121
- encoding: "utf-8",
6122
- timeout: 5e3
6034
+ async function apiCall(gatewayUrl, jwt2, path, opts = {}) {
6035
+ const resp = await fetch(`${gatewayUrl}${path}`, {
6036
+ ...opts,
6037
+ headers: {
6038
+ "Authorization": `Bearer ${jwt2}`,
6039
+ "Content-Type": "application/json",
6040
+ ...opts.headers || {}
6041
+ }
6123
6042
  });
6124
- if (r.status !== 0 || !r.stdout) return null;
6125
- let env;
6043
+ if (!resp.ok) {
6044
+ const text = await resp.text().catch(() => "");
6045
+ throw new Error(`API ${resp.status}: ${text.slice(0, 200)}`);
6046
+ }
6047
+ return resp.json();
6048
+ }
6049
+ async function connectGitHub(gatewayUrl, jwt2, opts = {}) {
6126
6050
  try {
6127
- env = JSON.parse(r.stdout.trim());
6051
+ const result = await apiCall(
6052
+ gatewayUrl,
6053
+ jwt2,
6054
+ "/api/v1/cli/github-token"
6055
+ );
6056
+ if (result.connected && result.token) {
6057
+ if (!opts.silent) console.log(" \u2713 GitHub already connected via Synkro.");
6058
+ return result.token;
6059
+ }
6128
6060
  } catch {
6061
+ }
6062
+ if (!opts.silent) console.log(" Opening browser to authorize GitHub...");
6063
+ try {
6064
+ const authResp = await apiCall(
6065
+ gatewayUrl,
6066
+ jwt2,
6067
+ "/api/pipes-widget/authorize/github",
6068
+ { method: "POST", body: "{}" }
6069
+ );
6070
+ openBrowser3(authResp.url);
6071
+ if (!opts.silent) console.log(" Waiting for authorization...");
6072
+ } catch (err) {
6073
+ if (!opts.silent) console.error(` Failed to start GitHub authorization: ${err.message}`);
6129
6074
  return null;
6130
6075
  }
6131
- if (!Array.isArray(env)) return null;
6132
- const get = (k) => {
6133
- const hit = env.find((e) => typeof e === "string" && e.startsWith(k + "="));
6134
- return hit ? hit.slice(k.length + 1) : void 0;
6135
- };
6136
- const num = (s) => {
6137
- if (s === void 0) return void 0;
6138
- const n = parseInt(s, 10);
6139
- return Number.isFinite(n) ? n : void 0;
6140
- };
6141
- return {
6142
- claudeWorkers: num(get("CLAUDE_WORKERS")),
6143
- cursorWorkers: num(get("CURSOR_WORKERS")),
6144
- connectedRepo: get("SYNKRO_CONNECTED_REPO") || void 0
6145
- };
6076
+ const deadline = Date.now() + 12e4;
6077
+ while (Date.now() < deadline) {
6078
+ await sleep(2e3);
6079
+ try {
6080
+ const result = await apiCall(
6081
+ gatewayUrl,
6082
+ jwt2,
6083
+ "/api/v1/cli/github-token"
6084
+ );
6085
+ if (result.connected && result.token) {
6086
+ if (!opts.silent) console.log("\n \u2713 GitHub connected!");
6087
+ return result.token;
6088
+ }
6089
+ } catch {
6090
+ }
6091
+ if (!opts.silent) process.stdout.write(".");
6092
+ }
6093
+ if (!opts.silent) console.error("\n Timed out waiting for GitHub authorization.");
6094
+ return null;
6146
6095
  }
6147
- async function dockerSafeStop() {
6148
- const status = dockerStatus();
6149
- if (!status.running) {
6150
- console.log(" Container is not running.");
6151
- return { ok: true, pgdataCheck: checkPgdata() };
6096
+ async function setupGithubCommand(opts = {}) {
6097
+ if (!isAuthenticated()) {
6098
+ console.error("Not authenticated. Run `synkro-cli login` first.");
6099
+ process.exit(1);
6152
6100
  }
6153
- console.log(" Requesting data snapshot before shutdown...");
6154
- let snapshot = { ok: false, error: "not attempted" };
6155
- try {
6156
- const resp = await fetch(`http://127.0.0.1:${HOST_MCP_PORT}/api/local/snapshot`, {
6157
- method: "POST",
6158
- headers: { "Content-Type": "application/json" },
6159
- body: JSON.stringify({ reason: "cli-stop" }),
6160
- signal: AbortSignal.timeout(2e4)
6161
- });
6162
- if (resp.ok) {
6163
- snapshot = { ok: true };
6164
- console.log(" \u2713 Snapshot saved.");
6165
- } else {
6166
- snapshot = { ok: false, error: `HTTP ${resp.status}` };
6167
- console.warn(` \u26A0 Snapshot request failed (HTTP ${resp.status}). Proceeding with stop.`);
6168
- }
6169
- } catch (e) {
6170
- snapshot = { ok: false, error: String(e).slice(0, 100) };
6171
- console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
6101
+ const config = readConfig();
6102
+ const gatewayUrl = (config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
6103
+ const jwt2 = getAccessToken();
6104
+ if (!jwt2) {
6105
+ console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
6106
+ process.exit(1);
6172
6107
  }
6173
- console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
6174
- const stop = spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
6175
- encoding: "utf-8",
6176
- timeout: 45e3
6177
- });
6178
- const inspect = spawnSync2("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
6179
- encoding: "utf-8",
6180
- timeout: 5e3
6181
- });
6182
- const exitCode = parseInt((inspect.stdout || "").trim(), 10);
6183
- if (exitCode === 0) {
6184
- console.log(" \u2713 Container stopped cleanly (exit 0).");
6185
- } else {
6186
- console.warn(` \u26A0 Container exited with code ${exitCode}.`);
6108
+ console.log("Requesting CI API key from Synkro...");
6109
+ let synkroCiApiKey;
6110
+ try {
6111
+ const minted = await apiCall(
6112
+ gatewayUrl,
6113
+ jwt2,
6114
+ "/api/v1/cli/ci-api-key",
6115
+ { method: "POST", body: "{}" }
6116
+ );
6117
+ synkroCiApiKey = minted.api_key;
6118
+ console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
6119
+ } catch (err) {
6120
+ console.error(`Failed to mint CI API key: ${err.message}`);
6121
+ process.exit(1);
6187
6122
  }
6188
- const pgCheck = checkPgdata();
6189
- if (pgCheck.healthy) {
6190
- console.log(` \u2713 pgdata looks healthy: ${pgCheck.details}`);
6123
+ let ghToken;
6124
+ if (opts.githubToken) {
6125
+ ghToken = opts.githubToken;
6126
+ } else if (opts.nonInteractive) {
6127
+ try {
6128
+ const result = await apiCall(
6129
+ gatewayUrl,
6130
+ jwt2,
6131
+ "/api/v1/cli/github-token"
6132
+ );
6133
+ if (result.connected && result.token) {
6134
+ ghToken = result.token;
6135
+ } else {
6136
+ throw new Error("not connected");
6137
+ }
6138
+ } catch {
6139
+ try {
6140
+ ghToken = execSync4("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
6141
+ } catch {
6142
+ console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
6143
+ return;
6144
+ }
6145
+ }
6191
6146
  } else {
6192
- console.warn(` \u26A0 pgdata check: ${pgCheck.details}`);
6193
- }
6194
- return { ok: stop.status === 0, snapshot, exitCode, pgdataCheck: pgCheck };
6195
- }
6196
- async function dockerSafeStart() {
6197
- const status = dockerStatus();
6198
- if (status.running) {
6199
- console.log(" Container is already running.");
6200
- return { ok: true, pgdataState: "running" };
6147
+ console.log("\nConnecting to GitHub...");
6148
+ const token = await connectGitHub(gatewayUrl, jwt2);
6149
+ if (!token) {
6150
+ console.error("GitHub connection failed. Try again.");
6151
+ process.exit(1);
6152
+ }
6153
+ ghToken = token;
6154
+ console.log();
6201
6155
  }
6202
- const exists = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
6203
- encoding: "utf-8",
6204
- timeout: 5e3
6205
- });
6206
- if (exists.status !== 0) {
6207
- return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
6156
+ let claudeToken;
6157
+ if (!opts.skipClaudeToken) {
6158
+ console.log("Generating Claude Code OAuth token...");
6159
+ console.log(" A browser window will open \u2014 authorize with your Claude account.\n");
6160
+ try {
6161
+ claudeToken = await captureClaudeSetupToken();
6162
+ } catch (err) {
6163
+ console.error(`Failed to get Claude token: ${err instanceof Error ? err.message : String(err)}`);
6164
+ if (opts.nonInteractive) return;
6165
+ process.exit(1);
6166
+ }
6167
+ if (!claudeToken.startsWith("sk-ant-oat01-")) {
6168
+ console.error("Invalid token received from `claude setup-token`. Expected sk-ant-oat01-...");
6169
+ if (opts.nonInteractive) return;
6170
+ process.exit(1);
6171
+ }
6172
+ console.log(" Validating token...");
6173
+ try {
6174
+ const validateResult = execSync4(
6175
+ 'claude --print --output-format json "say ok"',
6176
+ { env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
6177
+ );
6178
+ const result = JSON.parse(validateResult);
6179
+ if (result.is_error) throw new Error(result.result || "auth failed");
6180
+ console.log(" \u2713 Token validated.\n");
6181
+ } catch (err) {
6182
+ console.error(`Token validation failed: ${err instanceof Error ? err.message : String(err)}`);
6183
+ if (opts.nonInteractive) return;
6184
+ process.exit(1);
6185
+ }
6208
6186
  }
6209
- const pgCheck = checkPgdata();
6210
- if (existsSync8(PGDATA_PATH) && readdirSync(PGDATA_PATH).length > 0) {
6211
- if (pgCheck.healthy) {
6212
- console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
6213
- } else {
6214
- console.warn(` \u26A0 pgdata: ${pgCheck.details}`);
6215
- console.log(" Starting anyway \u2014 entrypoint will attempt recovery from snapshots if needed.");
6187
+ let selected;
6188
+ if (opts.nonInteractive) {
6189
+ let currentFullName = null;
6190
+ try {
6191
+ const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
6192
+ const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
6193
+ if (m) currentFullName = m[1];
6194
+ } catch {
6216
6195
  }
6196
+ if (!currentFullName) {
6197
+ console.warn(" \u26A0 Not in a GitHub repo. Skipping PR scan setup.");
6198
+ return;
6199
+ }
6200
+ const [owner, repo] = currentFullName.split("/");
6201
+ selected = [{ owner, repo, full_name: currentFullName }];
6202
+ console.log(` Auto-selected repo: ${currentFullName}`);
6217
6203
  } else {
6218
- console.log(" pgdata: no existing data \u2014 fresh start.");
6219
- mkdirSync7(PGDATA_PATH, { recursive: true });
6204
+ console.log("Fetching accessible repos...");
6205
+ const repos = await listAccessibleRepos({ token: ghToken });
6206
+ if (repos.length === 0) {
6207
+ console.error("No accessible repos found. Check your GitHub permissions.");
6208
+ process.exit(1);
6209
+ }
6210
+ console.log(`
6211
+ Found ${repos.length} accessible repo(s):
6212
+ `);
6213
+ repos.slice(0, 100).forEach((r, i) => {
6214
+ console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
6215
+ });
6216
+ console.log();
6217
+ const rl2 = createInterface2({ input, output });
6218
+ const selectionRaw = await prompt(rl2, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
6219
+ const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
6220
+ if (selectedIdx.length === 0) {
6221
+ console.error("No valid selections.");
6222
+ rl2.close();
6223
+ process.exit(1);
6224
+ }
6225
+ selected = selectedIdx.map((i) => repos[i]);
6226
+ console.log(`
6227
+ Will push secrets to ${selected.length} repo(s):`);
6228
+ for (const r of selected) console.log(` \u2022 ${r.full_name}`);
6229
+ console.log();
6230
+ const confirm = (await prompt(rl2, "Continue? (yes/no): ")).trim().toLowerCase();
6231
+ if (confirm !== "yes" && confirm !== "y") {
6232
+ console.log("Cancelled.");
6233
+ rl2.close();
6234
+ process.exit(0);
6235
+ }
6236
+ rl2.close();
6220
6237
  }
6221
- console.log(" Starting container...");
6222
- const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
6223
- encoding: "utf-8",
6224
- timeout: 3e4
6225
- });
6226
- if (start.status !== 0) {
6227
- return { ok: false, pgdataState: "start_failed", error: `docker start failed: ${(start.stderr || "").slice(0, 200)}` };
6238
+ console.log();
6239
+ for (const r of selected) {
6240
+ process.stdout.write(`Pushing secrets to ${r.full_name}... `);
6241
+ try {
6242
+ await pushSecretsToRepo(
6243
+ { token: ghToken },
6244
+ r.owner,
6245
+ r.repo,
6246
+ {
6247
+ claudeCodeOauthToken: claudeToken,
6248
+ synkroApiKey: synkroCiApiKey
6249
+ }
6250
+ );
6251
+ console.log("\u2713");
6252
+ } catch (err) {
6253
+ console.log(`\u2717 (${err.message})`);
6254
+ }
6228
6255
  }
6229
- console.log(" Waiting for server to become healthy...");
6230
- const ready = await waitForContainerReady(6e4);
6231
- if (ready) {
6232
- console.log(" \u2713 Server is healthy and ready.");
6233
- return { ok: true, pgdataState: pgCheck.healthy ? "existing" : "recovered" };
6256
+ console.log();
6257
+ const gitRoot = findGitRoot(process.cwd());
6258
+ if (gitRoot) {
6259
+ const written = writeWorkflowFile(gitRoot);
6260
+ if (written) {
6261
+ console.log(`Wrote workflow: ${written}`);
6262
+ console.log("Commit and push it to enable PR scanning.");
6263
+ }
6234
6264
  } else {
6235
- return { ok: false, pgdataState: "unhealthy", error: "Server did not become healthy within 60s. Check: docker logs synkro-server" };
6236
- }
6237
- }
6238
- async function dockerSafeRestart() {
6239
- console.log(" === Stop ===");
6240
- const stopResult = await dockerSafeStop();
6241
- if (!stopResult.ok) {
6242
- console.error(" Stop failed. Aborting restart.");
6243
- return { ok: false, stop: stopResult, start: { ok: false, pgdataState: "not_started", error: "stop failed" } };
6265
+ console.log("Not in a git repo. To enable scanning, add this file to your repo:");
6266
+ console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
6267
+ console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
6244
6268
  }
6245
- console.log("\n === Start ===");
6246
- const startResult = await dockerSafeStart();
6247
- return { ok: startResult.ok, stop: stopResult, start: startResult };
6248
- }
6249
- function checkPgdata() {
6250
- if (!existsSync8(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
6251
- const entries = readdirSync(PGDATA_PATH);
6252
- if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
6253
- const hasPidFile = entries.includes("postmaster.pid");
6254
- const hasWalDir = entries.includes("pg_wal");
6255
- const hasPgControl = entries.includes("global") || entries.includes("pg_control");
6256
- if (hasPidFile) return { healthy: false, details: "stale postmaster.pid present (unclean shutdown)" };
6257
- if (!hasWalDir) return { healthy: false, details: "pg_wal directory missing" };
6258
- if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
6259
- return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
6269
+ console.log();
6270
+ console.log("\u2713 PR scan setup complete.");
6271
+ console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
6272
+ console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
6260
6273
  }
6261
- var SYNKRO_DIR3, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6262
- var init_dockerInstall = __esm({
6263
- "cli/local-cc/dockerInstall.ts"() {
6274
+ var SYNKRO_DIR3, CONFIG_PATH;
6275
+ var init_setupGithub = __esm({
6276
+ "cli/commands/setupGithub.ts"() {
6264
6277
  "use strict";
6265
- init_agentDetect();
6266
- init_macKeychain();
6278
+ init_githubSetup();
6279
+ init_stub();
6267
6280
  SYNKRO_DIR3 = join7(homedir7(), ".synkro");
6268
- MCP_JWT_PATH = join7(SYNKRO_DIR3, ".mcp-jwt");
6269
- PGDATA_PATH = join7(SYNKRO_DIR3, "pgdata");
6270
- CLAUDE_HOST_STATE_DIR = join7(SYNKRO_DIR3, "claude-host-state");
6271
- CLAUDE_HOST_STATE_FILE = join7(CLAUDE_HOST_STATE_DIR, ".claude.json");
6272
- HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
6273
- HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
6274
- HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
6275
- HOST_PG_PORT = parseInt(process.env.SYNKRO_HOST_PG_PORT || "15433", 10);
6276
- CONTAINER_NAME = "synkro-server";
6277
- DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
6278
- DockerInstallError = class extends Error {
6279
- constructor(message, cause) {
6280
- super(message);
6281
- this.cause = cause;
6282
- this.name = "DockerInstallError";
6283
- }
6284
- cause;
6285
- };
6286
- BACKUP_DIR = join7(SYNKRO_DIR3, "pgdata-backups");
6281
+ CONFIG_PATH = join7(SYNKRO_DIR3, "config.env");
6287
6282
  }
6288
6283
  });
6289
6284
 
@@ -6378,6 +6373,32 @@ async function promptCursorApiKey(opts) {
6378
6373
  console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
6379
6374
  }
6380
6375
  }
6376
+ async function promptGradingMode() {
6377
+ if (!process.stdin.isTTY) return "local";
6378
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
6379
+ return new Promise((resolve3) => {
6380
+ rl.question(
6381
+ "Where should grading run?\n local \u2014 on this machine, via the Synkro container (default)\n byok \u2014 via an LLM API using your own provider key\nChoose [local] / byok: ",
6382
+ (answer) => {
6383
+ rl.close();
6384
+ resolve3(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
6385
+ }
6386
+ );
6387
+ });
6388
+ }
6389
+ async function promptStorageMode() {
6390
+ if (!process.stdin.isTTY) return "local";
6391
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
6392
+ return new Promise((resolve3) => {
6393
+ rl.question(
6394
+ "Where should telemetry be stored?\n local \u2014 on this machine only (default)\n cloud \u2014 sent to Synkro cloud\nChoose [local] / cloud: ",
6395
+ (answer) => {
6396
+ rl.close();
6397
+ resolve3(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
6398
+ }
6399
+ );
6400
+ });
6401
+ }
6381
6402
  function ensureSynkroDir() {
6382
6403
  mkdirSync8(SYNKRO_DIR4, { recursive: true });
6383
6404
  mkdirSync8(HOOKS_DIR, { recursive: true });
@@ -6479,7 +6500,7 @@ function writeConfigEnv(opts) {
6479
6500
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6480
6501
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6481
6502
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6482
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.10")}`
6503
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.12")}`
6483
6504
  ];
6484
6505
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6485
6506
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -6491,6 +6512,8 @@ function writeConfigEnv(opts) {
6491
6512
  lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
6492
6513
  const safeMode = sanitizeConfigValue(opts.deploymentMode ?? "docker", 16);
6493
6514
  lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
6515
+ lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
6516
+ lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
6494
6517
  lines.push("");
6495
6518
  writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
6496
6519
  chmodSync2(CONFIG_PATH2, 384);
@@ -6650,30 +6673,7 @@ async function installCommand(opts = {}) {
6650
6673
  console.error("No access token available after auth.");
6651
6674
  process.exit(1);
6652
6675
  }
6653
- let ghToken = null;
6654
- if (process.stdin.isTTY) {
6655
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
6656
- const wantsPR = await new Promise((resolve3) => {
6657
- rl.question("Would you like to enable GitHub PR scanning? (y/N) ", (answer) => {
6658
- rl.close();
6659
- const trimmed = answer.trim().toLowerCase();
6660
- resolve3(trimmed === "y" || trimmed === "yes");
6661
- });
6662
- });
6663
- if (wantsPR) {
6664
- try {
6665
- ghToken = await connectGitHub(gatewayUrl, token);
6666
- } catch {
6667
- }
6668
- if (ghToken) {
6669
- console.log();
6670
- } else {
6671
- console.log(" Skipped. Run `synkro install` again to enable PR scanning.\n");
6672
- }
6673
- } else {
6674
- console.log(" Skipped PR scanning.\n");
6675
- }
6676
- }
6676
+ const ghToken = null;
6677
6677
  setApiBaseUrl(`${gatewayUrl}/api`);
6678
6678
  await promptRepoConnection({ linkRepo: opts.linkRepo });
6679
6679
  const detected = detectAgents();
@@ -6691,6 +6691,14 @@ async function installCommand(opts = {}) {
6691
6691
  console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
6692
6692
  `);
6693
6693
  }
6694
+ const gradingMode = await promptGradingMode();
6695
+ const storageMode = await promptStorageMode();
6696
+ console.log(` grading: ${gradingMode} storage: ${storageMode}
6697
+ `);
6698
+ if (gradingMode === "byok") {
6699
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
6700
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
6701
+ }
6694
6702
  ensureSynkroDir();
6695
6703
  const scripts = writeHookScripts();
6696
6704
  console.log("Wrote hook scripts:");
@@ -6768,7 +6776,7 @@ async function installCommand(opts = {}) {
6768
6776
  } catch {
6769
6777
  }
6770
6778
  const profile = await fetchUserProfile(gatewayUrl, token);
6771
- const useLocalMcp = profile.captureDepth === "local_only" || profile.localInference;
6779
+ const useLocalMcp = true;
6772
6780
  if (hasClaudeCode && !opts.noMcp) {
6773
6781
  if (useLocalMcp) {
6774
6782
  try {
@@ -6868,7 +6876,7 @@ async function installCommand(opts = {}) {
6868
6876
  }
6869
6877
  const synkroBundle = resolveSynkroBundle();
6870
6878
  const persistedMode = resolveDeploymentMode();
6871
- writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode });
6879
+ writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode });
6872
6880
  console.log(`Wrote config to ${CONFIG_PATH2}`);
6873
6881
  console.log(` inference: ${profile.inference} (server-side grading)`);
6874
6882
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
@@ -7157,7 +7165,6 @@ var init_install = __esm({
7157
7165
  init_stub();
7158
7166
  init_repoConnect();
7159
7167
  init_projects();
7160
- init_setupGithub();
7161
7168
  init_promptFetcher();
7162
7169
  init_dockerInstall();
7163
7170
  SYNKRO_DIR4 = join8(homedir8(), ".synkro");
@@ -8980,16 +8987,141 @@ var init_lifecycle = __esm({
8980
8987
  }
8981
8988
  });
8982
8989
 
8990
+ // cli/commands/config.ts
8991
+ var config_exports = {};
8992
+ __export(config_exports, {
8993
+ configCommand: () => configCommand
8994
+ });
8995
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
8996
+ import { join as join15 } from "path";
8997
+ import { homedir as homedir15 } from "os";
8998
+ function readConfigEnv() {
8999
+ if (!existsSync15(CONFIG_PATH5)) return {};
9000
+ const out = {};
9001
+ for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
9002
+ const t = line.trim();
9003
+ if (!t || t.startsWith("#")) continue;
9004
+ const eq = t.indexOf("=");
9005
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
9006
+ }
9007
+ return out;
9008
+ }
9009
+ function updateConfigValue(key, value) {
9010
+ if (!existsSync15(CONFIG_PATH5)) {
9011
+ console.error("No config found. Run `synkro install` first.");
9012
+ process.exit(1);
9013
+ }
9014
+ const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
9015
+ const pattern = new RegExp(`^${key}=`);
9016
+ let found = false;
9017
+ const updated = lines.map((line) => {
9018
+ if (pattern.test(line.trim())) {
9019
+ found = true;
9020
+ return `${key}='${value}'`;
9021
+ }
9022
+ return line;
9023
+ });
9024
+ if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
9025
+ writeFileSync10(CONFIG_PATH5, updated.join("\n"), "utf-8");
9026
+ }
9027
+ async function configCommand(args2) {
9028
+ if (args2.length === 0) {
9029
+ const config2 = readConfigEnv();
9030
+ console.log("Synkro config:\n");
9031
+ console.log(` grading: ${config2.SYNKRO_GRADING_MODE || "local"}`);
9032
+ console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
9033
+ console.log(` inference: ${config2.SYNKRO_INFERENCE || "fast"}`);
9034
+ console.log(` tier: ${config2.SYNKRO_TIER || "pro"}`);
9035
+ console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL || "https://api.synkro.sh"}`);
9036
+ console.log(` version: ${config2.SYNKRO_VERSION || "?"}`);
9037
+ console.log(`
9038
+ To change:`);
9039
+ console.log(` synkro config grading <local|byok> \u2014 where grading runs`);
9040
+ console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
9041
+ console.log(` synkro config --inference fast|standard`);
9042
+ return;
9043
+ }
9044
+ if (args2[0] === "grading") {
9045
+ const value = args2[1];
9046
+ if (value !== "local" && value !== "byok") {
9047
+ console.error("Usage: synkro config grading <local|byok>");
9048
+ process.exit(1);
9049
+ }
9050
+ updateConfigValue("SYNKRO_GRADING_MODE", value);
9051
+ console.log(`\u2713 Grading mode set to '${value}'.`);
9052
+ if (value === "byok") {
9053
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
9054
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.");
9055
+ }
9056
+ return;
9057
+ }
9058
+ if (args2[0] === "storage") {
9059
+ const value = args2[1];
9060
+ if (value !== "local" && value !== "cloud") {
9061
+ console.error("Usage: synkro config storage <local|cloud>");
9062
+ process.exit(1);
9063
+ }
9064
+ updateConfigValue("SYNKRO_STORAGE_MODE", value);
9065
+ console.log(`\u2713 Storage mode set to '${value}'.`);
9066
+ return;
9067
+ }
9068
+ let inferenceValue;
9069
+ for (const a of args2) {
9070
+ if (a.startsWith("--inference=")) inferenceValue = a.slice("--inference=".length);
9071
+ else if (a === "--inference" && args2.indexOf(a) + 1 < args2.length) inferenceValue = args2[args2.indexOf(a) + 1];
9072
+ }
9073
+ if (!inferenceValue || !["fast", "standard"].includes(inferenceValue)) {
9074
+ console.error("Usage: synkro config --inference fast|standard");
9075
+ process.exit(1);
9076
+ }
9077
+ if (!isAuthenticated()) {
9078
+ console.error("Not authenticated. Run `synkro login` first.");
9079
+ process.exit(1);
9080
+ }
9081
+ const token = getAccessToken();
9082
+ const config = readConfigEnv();
9083
+ const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
9084
+ try {
9085
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
9086
+ method: "PATCH",
9087
+ headers: {
9088
+ "Authorization": `Bearer ${token}`,
9089
+ "Content-Type": "application/json"
9090
+ },
9091
+ body: JSON.stringify({ fast_inference: inferenceValue === "fast" })
9092
+ });
9093
+ if (!resp.ok) {
9094
+ const errText = await resp.text().catch(() => "");
9095
+ console.error(`Failed to update: ${resp.status} ${errText.slice(0, 200)}`);
9096
+ process.exit(1);
9097
+ }
9098
+ } catch (err) {
9099
+ console.error(`Failed to reach server: ${err.message}`);
9100
+ process.exit(1);
9101
+ }
9102
+ updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
9103
+ console.log(`\u2713 Inference set to '${inferenceValue}'.`);
9104
+ }
9105
+ var SYNKRO_DIR6, CONFIG_PATH5;
9106
+ var init_config = __esm({
9107
+ "cli/commands/config.ts"() {
9108
+ "use strict";
9109
+ init_stub();
9110
+ SYNKRO_DIR6 = join15(homedir15(), ".synkro");
9111
+ CONFIG_PATH5 = join15(SYNKRO_DIR6, "config.env");
9112
+ }
9113
+ });
9114
+
8983
9115
  // cli/bootstrap.js
8984
- import { readFileSync as readFileSync12, existsSync as existsSync15 } from "fs";
9116
+ import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
8985
9117
  import { resolve as resolve2 } from "path";
8986
9118
  var envCandidates = [
8987
9119
  resolve2(process.cwd(), ".env"),
8988
9120
  resolve2(process.env.HOME ?? "", ".synkro", "config.env")
8989
9121
  ];
8990
9122
  for (const envPath of envCandidates) {
8991
- if (!existsSync15(envPath)) continue;
8992
- const envContent = readFileSync12(envPath, "utf-8");
9123
+ if (!existsSync16(envPath)) continue;
9124
+ const envContent = readFileSync13(envPath, "utf-8");
8993
9125
  for (const line of envContent.split("\n")) {
8994
9126
  const trimmed = line.trim();
8995
9127
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -9004,7 +9136,7 @@ var args = process.argv.slice(2);
9004
9136
  var cmd = args[0] || "";
9005
9137
  var subArgs = args.slice(1);
9006
9138
  function printVersion() {
9007
- console.log("1.6.10");
9139
+ console.log("1.6.12");
9008
9140
  }
9009
9141
  function printHelp2() {
9010
9142
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -9019,8 +9151,14 @@ Commands:
9019
9151
  start [opts] Start the server (with pgdata integrity check)
9020
9152
  restart [opts] Safe restart (stop \u2192 start, data preserved)
9021
9153
  update Pull the latest container image and safely restart
9154
+ config Show or change grading + storage modes
9022
9155
  version Show version
9023
9156
 
9157
+ config:
9158
+ synkro config show current settings
9159
+ synkro config grading <local|byok> where grading runs
9160
+ synkro config storage <local|cloud> where telemetry is stored
9161
+
9024
9162
  start/restart opts (recreate the worker pool):
9025
9163
  --workers N total grader workers (default 8, even-split)
9026
9164
  --providers a,b grading agents: claude, cursor (or both)
@@ -9087,6 +9225,11 @@ async function main() {
9087
9225
  await updateCommand2();
9088
9226
  break;
9089
9227
  }
9228
+ case "config": {
9229
+ const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9230
+ await configCommand2(args.slice(1));
9231
+ break;
9232
+ }
9090
9233
  default: {
9091
9234
  console.error(`Unknown command: ${cmd}`);
9092
9235
  printHelp2();