@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 +904 -761
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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
|
-
//
|
|
1574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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
|
-
|
|
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/
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
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
|
-
|
|
5293
|
+
const data = await resp.json();
|
|
5294
|
+
return { version: data.prompts?.version ?? "unknown" };
|
|
5307
5295
|
}
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
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
|
-
|
|
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
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
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
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
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
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
5396
|
+
if (platform2() !== "darwin") {
|
|
5765
5397
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
5766
5398
|
}
|
|
5767
|
-
mkdirSync6(
|
|
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>${
|
|
5418
|
+
<string>${join5(SYNKRO_DIR, "claude-creds-refresh.log")}</string>
|
|
5787
5419
|
<key>StandardOutPath</key>
|
|
5788
|
-
<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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
5462
|
+
return readFileSync5(CLAUDE_CREDS_FILE, "utf-8");
|
|
5831
5463
|
} catch {
|
|
5832
5464
|
return null;
|
|
5833
5465
|
}
|
|
5834
5466
|
}
|
|
5835
|
-
var
|
|
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
|
-
|
|
5840
|
-
CLAUDE_CREDS_DIR =
|
|
5841
|
-
CLAUDE_CREDS_FILE =
|
|
5842
|
-
CURSOR_CREDS_DIR =
|
|
5843
|
-
CURSOR_API_KEY_FILE =
|
|
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 =
|
|
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: () =>
|
|
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
|
|
5880
|
-
import { homedir as
|
|
5881
|
-
import { join as
|
|
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
|
|
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 =
|
|
5973
|
-
if (
|
|
5604
|
+
const hostClaudeJson = join6(homedir6(), ".claude.json");
|
|
5605
|
+
if (existsSync7(hostClaudeJson)) {
|
|
5974
5606
|
copyFileSync(hostClaudeJson, CLAUDE_HOST_STATE_FILE);
|
|
5975
5607
|
}
|
|
5976
|
-
if (!
|
|
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(
|
|
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
|
-
`${
|
|
5674
|
+
`${SYNKRO_DIR2}:/data/synkro-host:ro`,
|
|
6043
5675
|
"-v",
|
|
6044
5676
|
`${credsDir}:/home/synkro/.claude:rw`,
|
|
6045
5677
|
"-v",
|
|
6046
|
-
`${
|
|
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
|
-
|
|
6100
|
-
|
|
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
|
|
6107
|
-
const
|
|
6108
|
-
|
|
6109
|
-
|
|
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
|
|
6120
|
-
const
|
|
6121
|
-
|
|
6122
|
-
|
|
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 (
|
|
6125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
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
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
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
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
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("
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
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
|
-
|
|
6189
|
-
if (
|
|
6190
|
-
|
|
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.
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
console.log(
|
|
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
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
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
|
-
|
|
6210
|
-
if (
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
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("
|
|
6219
|
-
|
|
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(
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
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(
|
|
6230
|
-
const
|
|
6231
|
-
if (
|
|
6232
|
-
|
|
6233
|
-
|
|
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
|
-
|
|
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(
|
|
6246
|
-
|
|
6247
|
-
|
|
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,
|
|
6262
|
-
var
|
|
6263
|
-
"cli/
|
|
6274
|
+
var SYNKRO_DIR3, CONFIG_PATH;
|
|
6275
|
+
var init_setupGithub = __esm({
|
|
6276
|
+
"cli/commands/setupGithub.ts"() {
|
|
6264
6277
|
"use strict";
|
|
6265
|
-
|
|
6266
|
-
|
|
6278
|
+
init_githubSetup();
|
|
6279
|
+
init_stub();
|
|
6267
6280
|
SYNKRO_DIR3 = join7(homedir7(), ".synkro");
|
|
6268
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 (!
|
|
8992
|
-
const envContent =
|
|
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.
|
|
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();
|