@synkro-sh/cli 1.3.16 → 1.3.18
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 +685 -358
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -102,7 +102,7 @@ function removeSynkroEntries(events, eventName) {
|
|
|
102
102
|
if (!Array.isArray(arr)) return;
|
|
103
103
|
events[eventName] = arr.filter((entry) => !isSynkroEntry(entry));
|
|
104
104
|
}
|
|
105
|
-
function installCCHooks(settingsPath,
|
|
105
|
+
function installCCHooks(settingsPath, config) {
|
|
106
106
|
const settings = readSettings(settingsPath);
|
|
107
107
|
settings.hooks = settings.hooks ?? {};
|
|
108
108
|
removeSynkroEntries(settings.hooks, "PreToolUse");
|
|
@@ -119,7 +119,7 @@ function installCCHooks(settingsPath, config2) {
|
|
|
119
119
|
hooks: [
|
|
120
120
|
{
|
|
121
121
|
type: "command",
|
|
122
|
-
command:
|
|
122
|
+
command: config.bashJudgeScriptPath,
|
|
123
123
|
timeout: 15
|
|
124
124
|
}
|
|
125
125
|
],
|
|
@@ -130,7 +130,7 @@ function installCCHooks(settingsPath, config2) {
|
|
|
130
130
|
hooks: [
|
|
131
131
|
{
|
|
132
132
|
type: "command",
|
|
133
|
-
command:
|
|
133
|
+
command: config.editPrecheckScriptPath,
|
|
134
134
|
timeout: 15
|
|
135
135
|
}
|
|
136
136
|
],
|
|
@@ -141,7 +141,7 @@ function installCCHooks(settingsPath, config2) {
|
|
|
141
141
|
hooks: [
|
|
142
142
|
{
|
|
143
143
|
type: "command",
|
|
144
|
-
command:
|
|
144
|
+
command: config.editCaptureScriptPath,
|
|
145
145
|
timeout: 20
|
|
146
146
|
}
|
|
147
147
|
],
|
|
@@ -152,7 +152,7 @@ function installCCHooks(settingsPath, config2) {
|
|
|
152
152
|
hooks: [
|
|
153
153
|
{
|
|
154
154
|
type: "command",
|
|
155
|
-
command:
|
|
155
|
+
command: config.bashFollowupScriptPath
|
|
156
156
|
}
|
|
157
157
|
],
|
|
158
158
|
[SYNKRO_MARKER]: true
|
|
@@ -161,7 +161,7 @@ function installCCHooks(settingsPath, config2) {
|
|
|
161
161
|
hooks: [
|
|
162
162
|
{
|
|
163
163
|
type: "command",
|
|
164
|
-
command:
|
|
164
|
+
command: config.stopSummaryScriptPath
|
|
165
165
|
}
|
|
166
166
|
],
|
|
167
167
|
[SYNKRO_MARKER]: true
|
|
@@ -170,23 +170,25 @@ function installCCHooks(settingsPath, config2) {
|
|
|
170
170
|
hooks: [
|
|
171
171
|
{
|
|
172
172
|
type: "command",
|
|
173
|
-
command:
|
|
174
|
-
}
|
|
175
|
-
],
|
|
176
|
-
[SYNKRO_MARKER]: true
|
|
177
|
-
});
|
|
178
|
-
settings.hooks.Stop = settings.hooks.Stop ?? [];
|
|
179
|
-
removeSynkroEntries(settings.hooks, "Stop");
|
|
180
|
-
settings.hooks.Stop.push({
|
|
181
|
-
hooks: [
|
|
182
|
-
{
|
|
183
|
-
type: "command",
|
|
184
|
-
command: config2.transcriptSyncScriptPath,
|
|
185
|
-
timeout: 3
|
|
173
|
+
command: config.sessionStartScriptPath
|
|
186
174
|
}
|
|
187
175
|
],
|
|
188
176
|
[SYNKRO_MARKER]: true
|
|
189
177
|
});
|
|
178
|
+
if (!config.skipTranscriptSync) {
|
|
179
|
+
settings.hooks.Stop = settings.hooks.Stop ?? [];
|
|
180
|
+
removeSynkroEntries(settings.hooks, "Stop");
|
|
181
|
+
settings.hooks.Stop.push({
|
|
182
|
+
hooks: [
|
|
183
|
+
{
|
|
184
|
+
type: "command",
|
|
185
|
+
command: config.transcriptSyncScriptPath,
|
|
186
|
+
timeout: 3
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
[SYNKRO_MARKER]: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
190
192
|
writeSettingsAtomic(settingsPath, settings);
|
|
191
193
|
}
|
|
192
194
|
function uninstallCCHooks(settingsPath) {
|
|
@@ -250,50 +252,50 @@ function readClaudeJson() {
|
|
|
250
252
|
throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
|
-
function writeClaudeJsonAtomic(
|
|
255
|
+
function writeClaudeJsonAtomic(config) {
|
|
254
256
|
mkdirSync2(dirname2(CC_CONFIG_PATH), { recursive: true });
|
|
255
257
|
const tmpPath = `${CC_CONFIG_PATH}.synkro.tmp`;
|
|
256
|
-
writeFileSync2(tmpPath, JSON.stringify(
|
|
258
|
+
writeFileSync2(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
257
259
|
renameSync2(tmpPath, CC_CONFIG_PATH);
|
|
258
260
|
}
|
|
259
261
|
function installMcpConfig(opts) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
for (const [name, entry] of Object.entries(
|
|
263
|
-
if (entry?.[SYNKRO_MARKER2] === true) delete
|
|
262
|
+
const config = readClaudeJson();
|
|
263
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
264
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
265
|
+
if (entry?.[SYNKRO_MARKER2] === true) delete config.mcpServers[name];
|
|
264
266
|
}
|
|
265
267
|
const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
|
|
266
|
-
|
|
268
|
+
config.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
267
269
|
type: "http",
|
|
268
270
|
url,
|
|
269
271
|
headers: { Authorization: `Bearer ${opts.bearerToken}` },
|
|
270
272
|
[SYNKRO_MARKER2]: true
|
|
271
273
|
};
|
|
272
|
-
writeClaudeJsonAtomic(
|
|
274
|
+
writeClaudeJsonAtomic(config);
|
|
273
275
|
return { path: CC_CONFIG_PATH, url };
|
|
274
276
|
}
|
|
275
277
|
function uninstallMcpConfig() {
|
|
276
278
|
if (!existsSync3(CC_CONFIG_PATH)) return false;
|
|
277
|
-
const
|
|
278
|
-
if (!
|
|
279
|
+
const config = readClaudeJson();
|
|
280
|
+
if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
|
|
279
281
|
let removed = false;
|
|
280
|
-
for (const [name, entry] of Object.entries(
|
|
282
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
281
283
|
if (entry?.[SYNKRO_MARKER2] === true) {
|
|
282
|
-
delete
|
|
284
|
+
delete config.mcpServers[name];
|
|
283
285
|
removed = true;
|
|
284
286
|
}
|
|
285
287
|
}
|
|
286
288
|
if (!removed) return false;
|
|
287
|
-
if (Object.keys(
|
|
288
|
-
writeClaudeJsonAtomic(
|
|
289
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
290
|
+
writeClaudeJsonAtomic(config);
|
|
289
291
|
return true;
|
|
290
292
|
}
|
|
291
293
|
function inspectMcpConfig() {
|
|
292
294
|
if (!existsSync3(CC_CONFIG_PATH)) {
|
|
293
295
|
return { installed: false, configPath: CC_CONFIG_PATH };
|
|
294
296
|
}
|
|
295
|
-
const
|
|
296
|
-
const entry =
|
|
297
|
+
const config = readClaudeJson();
|
|
298
|
+
const entry = config.mcpServers?.[SYNKRO_SERVER_NAME];
|
|
297
299
|
if (!entry || entry[SYNKRO_MARKER2] !== true) {
|
|
298
300
|
return { installed: false, configPath: CC_CONFIG_PATH };
|
|
299
301
|
}
|
|
@@ -1535,6 +1537,7 @@ GATEWAY_URL="\${SYNKRO_GATEWAY_URL:-https://api.synkro.sh}"
|
|
|
1535
1537
|
CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
|
|
1536
1538
|
|
|
1537
1539
|
if [ ! -f "$CREDS_PATH" ]; then echo '{}'; exit 0; fi
|
|
1540
|
+
if [ "\${SYNKRO_TRANSCRIPT_CONSENT:-yes}" = "no" ]; then echo '{}'; exit 0; fi
|
|
1538
1541
|
JWT=$(jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null)
|
|
1539
1542
|
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
1540
1543
|
|
|
@@ -2437,10 +2440,12 @@ var init_auth = __esm({
|
|
|
2437
2440
|
});
|
|
2438
2441
|
|
|
2439
2442
|
// cli/api/projects.ts
|
|
2440
|
-
|
|
2443
|
+
function setApiBaseUrl(url) {
|
|
2444
|
+
API_URL = url;
|
|
2445
|
+
}
|
|
2441
2446
|
async function callApi(method, endpoint, body) {
|
|
2442
2447
|
if (!API_URL) {
|
|
2443
|
-
throw new Error("
|
|
2448
|
+
throw new Error("API URL not configured. Run `synkro install` first.");
|
|
2444
2449
|
}
|
|
2445
2450
|
const url = `${API_URL}${endpoint}`;
|
|
2446
2451
|
const accessToken = getAccessToken();
|
|
@@ -2477,8 +2482,7 @@ var init_projects = __esm({
|
|
|
2477
2482
|
"cli/api/projects.ts"() {
|
|
2478
2483
|
"use strict";
|
|
2479
2484
|
init_auth();
|
|
2480
|
-
|
|
2481
|
-
API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
|
|
2485
|
+
API_URL = "https://api.synkro.sh/api";
|
|
2482
2486
|
}
|
|
2483
2487
|
});
|
|
2484
2488
|
|
|
@@ -2606,7 +2610,9 @@ async function listAccessibleRepos(opts) {
|
|
|
2606
2610
|
}
|
|
2607
2611
|
async function pushSecretsToRepo(opts, owner, repo, secrets) {
|
|
2608
2612
|
const pubkey = await getRepoPublicKey(opts, owner, repo);
|
|
2609
|
-
|
|
2613
|
+
if (secrets.claudeCodeOauthToken) {
|
|
2614
|
+
await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
|
|
2615
|
+
}
|
|
2610
2616
|
await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
|
|
2611
2617
|
}
|
|
2612
2618
|
function writeWorkflowFile(repoRootPath) {
|
|
@@ -2750,8 +2756,27 @@ async function connectGithubAndSelectRepos() {
|
|
|
2750
2756
|
rl.close();
|
|
2751
2757
|
}
|
|
2752
2758
|
}
|
|
2753
|
-
async function promptRepoConnection() {
|
|
2759
|
+
async function promptRepoConnection(opts) {
|
|
2754
2760
|
const localRepo = detectGitRepo();
|
|
2761
|
+
if (opts?.linkRepo && localRepo) {
|
|
2762
|
+
console.log("Connect repos to Synkro:\n");
|
|
2763
|
+
try {
|
|
2764
|
+
const existing = await listProjects();
|
|
2765
|
+
const alreadyLinked = existing.some(
|
|
2766
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
2767
|
+
);
|
|
2768
|
+
if (!alreadyLinked) {
|
|
2769
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
2770
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
2771
|
+
} else {
|
|
2772
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
2773
|
+
}
|
|
2774
|
+
} catch (err) {
|
|
2775
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
2776
|
+
}
|
|
2777
|
+
console.log();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2755
2780
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2756
2781
|
try {
|
|
2757
2782
|
console.log("Connect repos to Synkro:\n");
|
|
@@ -2829,16 +2854,282 @@ var init_repoConnect = __esm({
|
|
|
2829
2854
|
}
|
|
2830
2855
|
});
|
|
2831
2856
|
|
|
2857
|
+
// cli/commands/setupGithub.ts
|
|
2858
|
+
var setupGithub_exports = {};
|
|
2859
|
+
__export(setupGithub_exports, {
|
|
2860
|
+
setupGithubCommand: () => setupGithubCommand
|
|
2861
|
+
});
|
|
2862
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2863
|
+
import { stdin as input, stdout as output } from "process";
|
|
2864
|
+
import { execSync as execSync3, spawn as nodeSpawn } from "child_process";
|
|
2865
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
2866
|
+
import { homedir as homedir4 } from "os";
|
|
2867
|
+
import { join as join5 } from "path";
|
|
2868
|
+
function readConfig() {
|
|
2869
|
+
if (!existsSync6(CONFIG_PATH)) return {};
|
|
2870
|
+
const out = {};
|
|
2871
|
+
for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
|
|
2872
|
+
const t = line.trim();
|
|
2873
|
+
if (!t || t.startsWith("#")) continue;
|
|
2874
|
+
const eq = t.indexOf("=");
|
|
2875
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
2876
|
+
}
|
|
2877
|
+
return out;
|
|
2878
|
+
}
|
|
2879
|
+
async function prompt(rl, q, opts = {}) {
|
|
2880
|
+
if (opts.silent) {
|
|
2881
|
+
process.stdout.write(q);
|
|
2882
|
+
const wasRaw = process.stdin.isRaw;
|
|
2883
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
2884
|
+
return await new Promise((resolve2) => {
|
|
2885
|
+
let chunk = "";
|
|
2886
|
+
const onData = (data) => {
|
|
2887
|
+
const s = data.toString("utf-8");
|
|
2888
|
+
if (s === "\r" || s === "\n" || s === "\r\n") {
|
|
2889
|
+
process.stdin.removeListener("data", onData);
|
|
2890
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
2891
|
+
process.stdout.write("\n");
|
|
2892
|
+
resolve2(chunk);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (s === "") {
|
|
2896
|
+
process.exit(130);
|
|
2897
|
+
}
|
|
2898
|
+
if (s === "\x7F" || s === "\b") {
|
|
2899
|
+
chunk = chunk.slice(0, -1);
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
chunk += s;
|
|
2903
|
+
};
|
|
2904
|
+
process.stdin.on("data", onData);
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
return await rl.question(q);
|
|
2908
|
+
}
|
|
2909
|
+
function captureClaudeSetupToken() {
|
|
2910
|
+
return new Promise((resolve2, reject) => {
|
|
2911
|
+
const proc = nodeSpawn("script", ["-q", "/dev/null", "claude", "setup-token"], {
|
|
2912
|
+
stdio: ["inherit", "pipe", "inherit"]
|
|
2913
|
+
});
|
|
2914
|
+
let stdout = "";
|
|
2915
|
+
proc.stdout.on("data", (chunk) => {
|
|
2916
|
+
stdout += chunk.toString();
|
|
2917
|
+
});
|
|
2918
|
+
proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
|
|
2919
|
+
proc.on("close", (code) => {
|
|
2920
|
+
if (code !== 0) {
|
|
2921
|
+
reject(new Error(`claude setup-token exited with code ${code}`));
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const stripped = stdout.replace(/\r/g, "").replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b[^[\]]*/, "").replace(/\n/g, "");
|
|
2925
|
+
const match = stripped.match(/sk-ant-oat01-[A-Za-z0-9_-]+AA/);
|
|
2926
|
+
if (!match) {
|
|
2927
|
+
reject(new Error("Could not find token in claude setup-token output"));
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
resolve2(match[0]);
|
|
2931
|
+
});
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
async function setupGithubCommand(opts = {}) {
|
|
2935
|
+
if (!isAuthenticated()) {
|
|
2936
|
+
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
2937
|
+
process.exit(1);
|
|
2938
|
+
}
|
|
2939
|
+
const config = readConfig();
|
|
2940
|
+
const gatewayUrl = (config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
2941
|
+
const jwt2 = getAccessToken();
|
|
2942
|
+
if (!jwt2) {
|
|
2943
|
+
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
|
|
2944
|
+
process.exit(1);
|
|
2945
|
+
}
|
|
2946
|
+
console.log("Requesting CI API key from Synkro...");
|
|
2947
|
+
let synkroCiApiKey;
|
|
2948
|
+
try {
|
|
2949
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
|
|
2950
|
+
method: "POST",
|
|
2951
|
+
headers: {
|
|
2952
|
+
"Authorization": `Bearer ${jwt2}`,
|
|
2953
|
+
"Content-Type": "application/json"
|
|
2954
|
+
},
|
|
2955
|
+
body: "{}"
|
|
2956
|
+
});
|
|
2957
|
+
if (!resp.ok) {
|
|
2958
|
+
const errText = await resp.text().catch(() => "");
|
|
2959
|
+
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
2960
|
+
process.exit(1);
|
|
2961
|
+
}
|
|
2962
|
+
const minted = await resp.json();
|
|
2963
|
+
synkroCiApiKey = minted.api_key;
|
|
2964
|
+
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
let ghToken;
|
|
2970
|
+
if (opts.githubToken) {
|
|
2971
|
+
ghToken = opts.githubToken;
|
|
2972
|
+
} else if (opts.nonInteractive) {
|
|
2973
|
+
try {
|
|
2974
|
+
ghToken = execSync3("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
2975
|
+
} catch {
|
|
2976
|
+
console.error("Could not get GitHub token from `gh auth token`. Run `gh auth login` first.");
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
} else {
|
|
2980
|
+
const rl = createInterface2({ input, output });
|
|
2981
|
+
console.log("Synkro PR scan setup\n");
|
|
2982
|
+
console.log("Requirements:");
|
|
2983
|
+
console.log(" \u2022 Claude Code installed and logged in (Pro, Max, Teams, or Enterprise)");
|
|
2984
|
+
console.log(" \u2022 A GitHub personal access token with `repo` scope");
|
|
2985
|
+
console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
|
|
2986
|
+
ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
|
|
2987
|
+
rl.close();
|
|
2988
|
+
if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
|
|
2989
|
+
console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
|
|
2990
|
+
process.exit(1);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
let claudeToken;
|
|
2994
|
+
if (!opts.skipClaudeToken) {
|
|
2995
|
+
console.log("Generating Claude Code OAuth token...");
|
|
2996
|
+
console.log(" A browser window will open \u2014 authorize with your Claude account.\n");
|
|
2997
|
+
try {
|
|
2998
|
+
claudeToken = await captureClaudeSetupToken();
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
console.error(`Failed to get Claude token: ${err instanceof Error ? err.message : String(err)}`);
|
|
3001
|
+
if (opts.nonInteractive) return;
|
|
3002
|
+
process.exit(1);
|
|
3003
|
+
}
|
|
3004
|
+
if (!claudeToken.startsWith("sk-ant-oat01-")) {
|
|
3005
|
+
console.error("Invalid token received from `claude setup-token`. Expected sk-ant-oat01-...");
|
|
3006
|
+
if (opts.nonInteractive) return;
|
|
3007
|
+
process.exit(1);
|
|
3008
|
+
}
|
|
3009
|
+
console.log(" Validating token...");
|
|
3010
|
+
try {
|
|
3011
|
+
const validateResult = execSync3(
|
|
3012
|
+
'claude --print --output-format json "say ok"',
|
|
3013
|
+
{ env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
|
|
3014
|
+
);
|
|
3015
|
+
const result = JSON.parse(validateResult);
|
|
3016
|
+
if (result.is_error) throw new Error(result.result || "auth failed");
|
|
3017
|
+
console.log(" \u2713 Token validated.\n");
|
|
3018
|
+
} catch (err) {
|
|
3019
|
+
console.error(`Token validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3020
|
+
if (opts.nonInteractive) return;
|
|
3021
|
+
process.exit(1);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
let selected;
|
|
3025
|
+
if (opts.nonInteractive) {
|
|
3026
|
+
let currentFullName = null;
|
|
3027
|
+
try {
|
|
3028
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3029
|
+
const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
|
|
3030
|
+
if (m) currentFullName = m[1];
|
|
3031
|
+
} catch {
|
|
3032
|
+
}
|
|
3033
|
+
if (!currentFullName) {
|
|
3034
|
+
console.warn(" \u26A0 Not in a GitHub repo. Skipping PR scan setup.");
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
const [owner, repo] = currentFullName.split("/");
|
|
3038
|
+
selected = [{ owner, repo, full_name: currentFullName }];
|
|
3039
|
+
console.log(` Auto-selected repo: ${currentFullName}`);
|
|
3040
|
+
} else {
|
|
3041
|
+
console.log("\nFetching accessible repos...");
|
|
3042
|
+
const repos = await listAccessibleRepos({ token: ghToken });
|
|
3043
|
+
if (repos.length === 0) {
|
|
3044
|
+
console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
console.log(`
|
|
3048
|
+
Found ${repos.length} accessible repo(s):
|
|
3049
|
+
`);
|
|
3050
|
+
repos.slice(0, 100).forEach((r, i) => {
|
|
3051
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
|
|
3052
|
+
});
|
|
3053
|
+
console.log();
|
|
3054
|
+
const rl2 = createInterface2({ input, output });
|
|
3055
|
+
const selectionRaw = await prompt(rl2, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
|
|
3056
|
+
const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
|
|
3057
|
+
if (selectedIdx.length === 0) {
|
|
3058
|
+
console.error("No valid selections.");
|
|
3059
|
+
rl2.close();
|
|
3060
|
+
process.exit(1);
|
|
3061
|
+
}
|
|
3062
|
+
selected = selectedIdx.map((i) => repos[i]);
|
|
3063
|
+
console.log(`
|
|
3064
|
+
Will push secrets to ${selected.length} repo(s):`);
|
|
3065
|
+
for (const r of selected) console.log(` \u2022 ${r.full_name}`);
|
|
3066
|
+
console.log();
|
|
3067
|
+
const confirm = (await prompt(rl2, "Continue? (yes/no): ")).trim().toLowerCase();
|
|
3068
|
+
if (confirm !== "yes" && confirm !== "y") {
|
|
3069
|
+
console.log("Cancelled.");
|
|
3070
|
+
rl2.close();
|
|
3071
|
+
process.exit(0);
|
|
3072
|
+
}
|
|
3073
|
+
rl2.close();
|
|
3074
|
+
}
|
|
3075
|
+
console.log();
|
|
3076
|
+
for (const r of selected) {
|
|
3077
|
+
process.stdout.write(`Pushing secrets to ${r.full_name}... `);
|
|
3078
|
+
try {
|
|
3079
|
+
await pushSecretsToRepo(
|
|
3080
|
+
{ token: ghToken },
|
|
3081
|
+
r.owner,
|
|
3082
|
+
r.repo,
|
|
3083
|
+
{
|
|
3084
|
+
claudeCodeOauthToken: claudeToken,
|
|
3085
|
+
synkroApiKey: synkroCiApiKey
|
|
3086
|
+
}
|
|
3087
|
+
);
|
|
3088
|
+
console.log("\u2713");
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
console.log(`\u2717 (${err.message})`);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
console.log();
|
|
3094
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
3095
|
+
if (gitRoot) {
|
|
3096
|
+
const written = writeWorkflowFile(gitRoot);
|
|
3097
|
+
if (written) {
|
|
3098
|
+
console.log(`Wrote workflow: ${written}`);
|
|
3099
|
+
console.log("Commit and push it to enable PR scanning.");
|
|
3100
|
+
}
|
|
3101
|
+
} else {
|
|
3102
|
+
console.log("Not in a git repo. To enable scanning, add this file to your repo:");
|
|
3103
|
+
console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
|
|
3104
|
+
console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
|
|
3105
|
+
}
|
|
3106
|
+
console.log();
|
|
3107
|
+
console.log("\u2713 PR scan setup complete.");
|
|
3108
|
+
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
3109
|
+
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
3110
|
+
}
|
|
3111
|
+
var SYNKRO_DIR, CONFIG_PATH;
|
|
3112
|
+
var init_setupGithub = __esm({
|
|
3113
|
+
"cli/commands/setupGithub.ts"() {
|
|
3114
|
+
"use strict";
|
|
3115
|
+
init_githubSetup();
|
|
3116
|
+
init_stub();
|
|
3117
|
+
SYNKRO_DIR = join5(homedir4(), ".synkro");
|
|
3118
|
+
CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
|
|
2832
3122
|
// cli/commands/install.ts
|
|
2833
3123
|
var install_exports = {};
|
|
2834
3124
|
__export(install_exports, {
|
|
2835
3125
|
installCommand: () => installCommand,
|
|
2836
3126
|
parseArgs: () => parseArgs
|
|
2837
3127
|
});
|
|
2838
|
-
import { existsSync as
|
|
2839
|
-
import { homedir as
|
|
2840
|
-
import { join as
|
|
2841
|
-
import { execSync as
|
|
3128
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
|
|
3129
|
+
import { homedir as homedir5 } from "os";
|
|
3130
|
+
import { join as join6 } from "path";
|
|
3131
|
+
import { execSync as execSync4 } from "child_process";
|
|
3132
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2842
3133
|
function sanitizeGatewayCandidate(raw) {
|
|
2843
3134
|
if (!raw) return void 0;
|
|
2844
3135
|
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
@@ -2851,6 +3142,8 @@ function parseArgs(argv) {
|
|
|
2851
3142
|
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
2852
3143
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
2853
3144
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
3145
|
+
else if (a === "--link-repo") opts.linkRepo = true;
|
|
3146
|
+
else if (a === "--pr-scan") opts.prScan = true;
|
|
2854
3147
|
}
|
|
2855
3148
|
if (!opts.gatewayUrl) {
|
|
2856
3149
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
@@ -2858,8 +3151,21 @@ function parseArgs(argv) {
|
|
|
2858
3151
|
}
|
|
2859
3152
|
return opts;
|
|
2860
3153
|
}
|
|
3154
|
+
async function promptTranscriptConsent() {
|
|
3155
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
3156
|
+
return new Promise((resolve2) => {
|
|
3157
|
+
rl.question(
|
|
3158
|
+
"Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
|
|
3159
|
+
(answer) => {
|
|
3160
|
+
rl.close();
|
|
3161
|
+
const trimmed = answer.trim().toLowerCase();
|
|
3162
|
+
resolve2(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
3163
|
+
}
|
|
3164
|
+
);
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
2861
3167
|
function ensureSynkroDir() {
|
|
2862
|
-
mkdirSync5(
|
|
3168
|
+
mkdirSync5(SYNKRO_DIR2, { recursive: true });
|
|
2863
3169
|
mkdirSync5(HOOKS_DIR, { recursive: true });
|
|
2864
3170
|
mkdirSync5(BIN_DIR, { recursive: true });
|
|
2865
3171
|
mkdirSync5(OFFSETS_DIR, { recursive: true });
|
|
@@ -2873,13 +3179,13 @@ function writeGraderDaemon() {
|
|
|
2873
3179
|
chmodSync(GRADER_PRIMER_BASH_PATH, 420);
|
|
2874
3180
|
}
|
|
2875
3181
|
function writeHookScripts() {
|
|
2876
|
-
const bashScriptPath =
|
|
2877
|
-
const bashFollowupScriptPath =
|
|
2878
|
-
const editCaptureScriptPath =
|
|
2879
|
-
const editPrecheckScriptPath =
|
|
2880
|
-
const stopSummaryScriptPath =
|
|
2881
|
-
const sessionStartScriptPath =
|
|
2882
|
-
const transcriptSyncScriptPath =
|
|
3182
|
+
const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
|
|
3183
|
+
const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
|
|
3184
|
+
const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
|
|
3185
|
+
const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
3186
|
+
const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
|
|
3187
|
+
const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
|
|
3188
|
+
const transcriptSyncScriptPath = join6(HOOKS_DIR, "cc-transcript-sync.sh");
|
|
2883
3189
|
writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
|
|
2884
3190
|
writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
2885
3191
|
writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
@@ -2912,7 +3218,7 @@ function shellQuoteSingle(value) {
|
|
|
2912
3218
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2913
3219
|
}
|
|
2914
3220
|
function writeConfigEnv(opts) {
|
|
2915
|
-
const credsPath =
|
|
3221
|
+
const credsPath = join6(SYNKRO_DIR2, "credentials.json");
|
|
2916
3222
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
2917
3223
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
2918
3224
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -2925,14 +3231,17 @@ function writeConfigEnv(opts) {
|
|
|
2925
3231
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2926
3232
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2927
3233
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2928
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3234
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.18")}`
|
|
2929
3235
|
];
|
|
2930
3236
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2931
3237
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
2932
3238
|
if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
|
|
3239
|
+
if (opts.transcriptConsent !== void 0) {
|
|
3240
|
+
lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
|
|
3241
|
+
}
|
|
2933
3242
|
lines.push("");
|
|
2934
|
-
writeFileSync5(
|
|
2935
|
-
chmodSync(
|
|
3243
|
+
writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
3244
|
+
chmodSync(CONFIG_PATH2, 384);
|
|
2936
3245
|
}
|
|
2937
3246
|
function assertGatewayAllowed(gatewayUrl) {
|
|
2938
3247
|
let parsed;
|
|
@@ -2957,19 +3266,19 @@ function assertGatewayAllowed(gatewayUrl) {
|
|
|
2957
3266
|
}
|
|
2958
3267
|
function isAlreadyInstalled() {
|
|
2959
3268
|
const requiredScripts = [
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3269
|
+
join6(HOOKS_DIR, "cc-bash-judge.sh"),
|
|
3270
|
+
join6(HOOKS_DIR, "cc-bash-followup.sh"),
|
|
3271
|
+
join6(HOOKS_DIR, "cc-edit-precheck.sh"),
|
|
3272
|
+
join6(HOOKS_DIR, "cc-edit-capture.sh"),
|
|
3273
|
+
join6(HOOKS_DIR, "cc-stop-summary.sh"),
|
|
3274
|
+
join6(HOOKS_DIR, "cc-session-start.sh")
|
|
2966
3275
|
];
|
|
2967
|
-
if (!requiredScripts.every((p) =>
|
|
2968
|
-
if (!
|
|
2969
|
-
const settingsPath =
|
|
2970
|
-
if (!
|
|
3276
|
+
if (!requiredScripts.every((p) => existsSync7(p))) return false;
|
|
3277
|
+
if (!existsSync7(CONFIG_PATH2)) return false;
|
|
3278
|
+
const settingsPath = join6(homedir5(), ".claude", "settings.json");
|
|
3279
|
+
if (!existsSync7(settingsPath)) return false;
|
|
2971
3280
|
try {
|
|
2972
|
-
const settings = JSON.parse(
|
|
3281
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
2973
3282
|
const hooks = settings?.hooks;
|
|
2974
3283
|
if (!hooks || typeof hooks !== "object") return false;
|
|
2975
3284
|
const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
|
|
@@ -2991,6 +3300,24 @@ async function installCommand(opts = {}) {
|
|
|
2991
3300
|
process.exit(1);
|
|
2992
3301
|
}
|
|
2993
3302
|
if (!opts.force && isAuthenticated() && isAlreadyInstalled()) {
|
|
3303
|
+
setApiBaseUrl(`${gatewayUrl}/api`);
|
|
3304
|
+
await ensureValidToken();
|
|
3305
|
+
const currentRepo = detectGitRepo2();
|
|
3306
|
+
if (currentRepo) {
|
|
3307
|
+
try {
|
|
3308
|
+
const projects = await listProjects();
|
|
3309
|
+
const alreadyLinked = projects.some(
|
|
3310
|
+
(p) => p.repos?.some((r) => r.full_name === currentRepo)
|
|
3311
|
+
);
|
|
3312
|
+
if (!alreadyLinked) {
|
|
3313
|
+
console.log(`Synkro is installed. This repo (${currentRepo}) is not linked yet.
|
|
3314
|
+
`);
|
|
3315
|
+
await promptRepoConnection();
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
} catch {
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
2994
3321
|
console.log("\u2713 Synkro is already installed and configured.");
|
|
2995
3322
|
console.log(" Run `synkro-cli update` to refresh hook scripts and judge prompts.");
|
|
2996
3323
|
console.log(" Run `synkro-cli install --force` to reinstall from scratch.");
|
|
@@ -3028,7 +3355,8 @@ async function installCommand(opts = {}) {
|
|
|
3028
3355
|
console.error("No access token available after auth.");
|
|
3029
3356
|
process.exit(1);
|
|
3030
3357
|
}
|
|
3031
|
-
|
|
3358
|
+
setApiBaseUrl(`${gatewayUrl}/api`);
|
|
3359
|
+
await promptRepoConnection({ linkRepo: opts.linkRepo });
|
|
3032
3360
|
const agents = detectAgents();
|
|
3033
3361
|
if (agents.length === 0) {
|
|
3034
3362
|
console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
@@ -3052,9 +3380,9 @@ async function installCommand(opts = {}) {
|
|
|
3052
3380
|
`);
|
|
3053
3381
|
writeGraderDaemon();
|
|
3054
3382
|
for (const mode of ["edit", "bash"]) {
|
|
3055
|
-
const pidFile =
|
|
3383
|
+
const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
|
|
3056
3384
|
try {
|
|
3057
|
-
const pid = parseInt(
|
|
3385
|
+
const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
|
|
3058
3386
|
if (pid > 0) {
|
|
3059
3387
|
process.kill(pid, "SIGTERM");
|
|
3060
3388
|
console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
|
|
@@ -3067,6 +3395,15 @@ async function installCommand(opts = {}) {
|
|
|
3067
3395
|
console.log(` ${GRADER_PRIMER_EDIT_PATH}`);
|
|
3068
3396
|
console.log(` ${GRADER_PRIMER_BASH_PATH}
|
|
3069
3397
|
`);
|
|
3398
|
+
let transcriptConsent = true;
|
|
3399
|
+
if (process.stdin.isTTY) {
|
|
3400
|
+
transcriptConsent = await promptTranscriptConsent();
|
|
3401
|
+
if (transcriptConsent) {
|
|
3402
|
+
console.log(" \u2713 Transcript collection enabled\n");
|
|
3403
|
+
} else {
|
|
3404
|
+
console.log(" \u2717 Transcript collection disabled \u2014 skipping transcript sync\n");
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3070
3407
|
let hasClaudeCode = false;
|
|
3071
3408
|
for (const agent of agents) {
|
|
3072
3409
|
if (agent.kind === "claude_code") {
|
|
@@ -3078,7 +3415,8 @@ async function installCommand(opts = {}) {
|
|
|
3078
3415
|
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
3079
3416
|
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
3080
3417
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
3081
|
-
transcriptSyncScriptPath: scripts.transcriptSyncScript
|
|
3418
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
3419
|
+
skipTranscriptSync: !transcriptConsent
|
|
3082
3420
|
});
|
|
3083
3421
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
3084
3422
|
}
|
|
@@ -3121,44 +3459,53 @@ async function installCommand(opts = {}) {
|
|
|
3121
3459
|
email = info.email;
|
|
3122
3460
|
} catch {
|
|
3123
3461
|
}
|
|
3124
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email });
|
|
3125
|
-
console.log(`Wrote config to ${
|
|
3462
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, transcriptConsent });
|
|
3463
|
+
console.log(`Wrote config to ${CONFIG_PATH2}
|
|
3126
3464
|
`);
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3465
|
+
if (transcriptConsent) {
|
|
3466
|
+
try {
|
|
3467
|
+
const repo = detectGitRepo2();
|
|
3468
|
+
if (repo) {
|
|
3469
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
3470
|
+
if (ingested > 0) {
|
|
3471
|
+
console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
|
|
3472
|
+
console.log(" This helps the safety judge understand your workflow.\n");
|
|
3473
|
+
}
|
|
3134
3474
|
}
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3475
|
+
} catch (err) {
|
|
3476
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3138
3477
|
`);
|
|
3139
|
-
}
|
|
3140
|
-
try {
|
|
3141
|
-
const repo = detectGitRepo2();
|
|
3142
|
-
if (repo) {
|
|
3143
|
-
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
3144
|
-
if (result.messages > 0) {
|
|
3145
|
-
console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
|
|
3146
|
-
console.log(" This data will be used to suggest guardrail rules.\n");
|
|
3147
|
-
}
|
|
3148
3478
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3479
|
+
try {
|
|
3480
|
+
const repo = detectGitRepo2();
|
|
3481
|
+
if (repo) {
|
|
3482
|
+
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
3483
|
+
if (result.messages > 0) {
|
|
3484
|
+
console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
|
|
3485
|
+
console.log(" This data will be used to suggest guardrail rules.\n");
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
} catch (err) {
|
|
3489
|
+
console.warn(` \u26A0 Transcript sync skipped: ${err.message}
|
|
3151
3490
|
`);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
if (opts.prScan) {
|
|
3494
|
+
console.log();
|
|
3495
|
+
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
3496
|
+
await setupGithubCommand2({ nonInteractive: true });
|
|
3152
3497
|
}
|
|
3153
3498
|
console.log("\u2713 Synkro installed.");
|
|
3154
3499
|
console.log();
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3500
|
+
if (!opts.prScan) {
|
|
3501
|
+
console.log("Next steps:");
|
|
3502
|
+
console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
|
|
3503
|
+
console.log(" \u2022 synkro-cli status (check what is configured)");
|
|
3504
|
+
}
|
|
3158
3505
|
}
|
|
3159
3506
|
function detectGitRepo2() {
|
|
3160
3507
|
try {
|
|
3161
|
-
const remoteUrl =
|
|
3508
|
+
const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3162
3509
|
const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
|
|
3163
3510
|
return match ? match[1] : null;
|
|
3164
3511
|
} catch {
|
|
@@ -3168,17 +3515,17 @@ function detectGitRepo2() {
|
|
|
3168
3515
|
function getClaudeProjectsFolder() {
|
|
3169
3516
|
const cwd = process.cwd();
|
|
3170
3517
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
3171
|
-
const projectsDir =
|
|
3172
|
-
return
|
|
3518
|
+
const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
|
|
3519
|
+
return existsSync7(projectsDir) ? projectsDir : null;
|
|
3173
3520
|
}
|
|
3174
3521
|
function extractSessionInsights(projectsDir) {
|
|
3175
3522
|
const insights = [];
|
|
3176
3523
|
const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
3177
3524
|
for (const file of files) {
|
|
3178
3525
|
const sessionId = file.replace(".jsonl", "");
|
|
3179
|
-
const filePath =
|
|
3526
|
+
const filePath = join6(projectsDir, file);
|
|
3180
3527
|
try {
|
|
3181
|
-
const content =
|
|
3528
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3182
3529
|
const lines = content.split("\n").filter(Boolean);
|
|
3183
3530
|
for (let i = 0; i < lines.length; i++) {
|
|
3184
3531
|
try {
|
|
@@ -3254,7 +3601,7 @@ function extractTextContent(content) {
|
|
|
3254
3601
|
return "";
|
|
3255
3602
|
}
|
|
3256
3603
|
function parseTranscriptFile(filePath) {
|
|
3257
|
-
const content =
|
|
3604
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3258
3605
|
const lines = content.split("\n").filter(Boolean);
|
|
3259
3606
|
const messages = [];
|
|
3260
3607
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3305,7 +3652,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3305
3652
|
const sessions = [];
|
|
3306
3653
|
for (const file of batch) {
|
|
3307
3654
|
const sessionId = file.replace(".jsonl", "");
|
|
3308
|
-
const filePath =
|
|
3655
|
+
const filePath = join6(projectsDir, file);
|
|
3309
3656
|
try {
|
|
3310
3657
|
const allMessages = parseTranscriptFile(filePath);
|
|
3311
3658
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -3334,18 +3681,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3334
3681
|
}
|
|
3335
3682
|
for (const file of batch) {
|
|
3336
3683
|
const sessionId = file.replace(".jsonl", "");
|
|
3337
|
-
const filePath =
|
|
3684
|
+
const filePath = join6(projectsDir, file);
|
|
3338
3685
|
try {
|
|
3339
|
-
const content =
|
|
3686
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3340
3687
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
3341
|
-
writeFileSync5(
|
|
3688
|
+
writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
3342
3689
|
} catch {
|
|
3343
3690
|
}
|
|
3344
3691
|
}
|
|
3345
3692
|
}
|
|
3346
3693
|
return { sessions: totalSessions, messages: totalMessages };
|
|
3347
3694
|
}
|
|
3348
|
-
var
|
|
3695
|
+
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
|
|
3349
3696
|
var init_install = __esm({
|
|
3350
3697
|
"cli/commands/install.ts"() {
|
|
3351
3698
|
"use strict";
|
|
@@ -3356,14 +3703,15 @@ var init_install = __esm({
|
|
|
3356
3703
|
init_graderDaemon();
|
|
3357
3704
|
init_stub();
|
|
3358
3705
|
init_repoConnect();
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3706
|
+
init_projects();
|
|
3707
|
+
SYNKRO_DIR2 = join6(homedir5(), ".synkro");
|
|
3708
|
+
HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
|
|
3709
|
+
BIN_DIR = join6(SYNKRO_DIR2, "bin");
|
|
3710
|
+
CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
|
|
3711
|
+
GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
|
|
3712
|
+
GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
|
|
3713
|
+
GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
|
|
3714
|
+
OFFSETS_DIR = join6(SYNKRO_DIR2, ".transcript-offsets");
|
|
3367
3715
|
}
|
|
3368
3716
|
});
|
|
3369
3717
|
|
|
@@ -3439,13 +3787,13 @@ var status_exports = {};
|
|
|
3439
3787
|
__export(status_exports, {
|
|
3440
3788
|
statusCommand: () => statusCommand
|
|
3441
3789
|
});
|
|
3442
|
-
import { existsSync as
|
|
3443
|
-
import { homedir as
|
|
3444
|
-
import { join as
|
|
3790
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
3791
|
+
import { homedir as homedir6 } from "os";
|
|
3792
|
+
import { join as join7 } from "path";
|
|
3445
3793
|
function readConfigEnv() {
|
|
3446
|
-
if (!
|
|
3794
|
+
if (!existsSync8(CONFIG_PATH3)) return {};
|
|
3447
3795
|
const out = {};
|
|
3448
|
-
const raw =
|
|
3796
|
+
const raw = readFileSync6(CONFIG_PATH3, "utf-8");
|
|
3449
3797
|
for (const line of raw.split("\n")) {
|
|
3450
3798
|
const trimmed = line.trim();
|
|
3451
3799
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -3469,21 +3817,21 @@ function statusCommand() {
|
|
|
3469
3817
|
console.log("Authentication: \u2717 not logged in (run: synkro-cli login)");
|
|
3470
3818
|
}
|
|
3471
3819
|
console.log();
|
|
3472
|
-
const
|
|
3820
|
+
const config = readConfigEnv();
|
|
3473
3821
|
console.log("Config:");
|
|
3474
|
-
console.log(` gateway: ${
|
|
3475
|
-
console.log(` credentials: ${
|
|
3476
|
-
console.log(` tier: ${
|
|
3822
|
+
console.log(` gateway: ${config.SYNKRO_GATEWAY_URL ?? "(unset)"}`);
|
|
3823
|
+
console.log(` credentials: ${config.SYNKRO_CREDENTIALS_PATH ?? "(unset)"}`);
|
|
3824
|
+
console.log(` tier: ${config.SYNKRO_TIER ?? "(unset)"}`);
|
|
3477
3825
|
const info2 = getUserInfo();
|
|
3478
|
-
const userId = info2?.id ??
|
|
3479
|
-
const tierCacheFile =
|
|
3480
|
-
let inferenceTier =
|
|
3481
|
-
if (!inferenceTier &&
|
|
3482
|
-
inferenceTier =
|
|
3826
|
+
const userId = info2?.id ?? config.SYNKRO_USER_ID ?? "default";
|
|
3827
|
+
const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
|
|
3828
|
+
let inferenceTier = config.SYNKRO_INFERENCE_TIER || null;
|
|
3829
|
+
if (!inferenceTier && existsSync8(tierCacheFile)) {
|
|
3830
|
+
inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
|
|
3483
3831
|
}
|
|
3484
3832
|
const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
|
|
3485
3833
|
console.log(` inference: ${tierLabel}`);
|
|
3486
|
-
console.log(` version: ${
|
|
3834
|
+
console.log(` version: ${config.SYNKRO_VERSION ?? "(unset)"}`);
|
|
3487
3835
|
console.log();
|
|
3488
3836
|
const agents = detectAgents();
|
|
3489
3837
|
console.log("Detected agents:");
|
|
@@ -3506,19 +3854,19 @@ function statusCommand() {
|
|
|
3506
3854
|
}
|
|
3507
3855
|
}
|
|
3508
3856
|
console.log();
|
|
3509
|
-
const bashScript =
|
|
3510
|
-
const bashFollowupScript =
|
|
3511
|
-
const editPrecheckScript =
|
|
3512
|
-
const editCaptureScript =
|
|
3513
|
-
const stopSummaryScript =
|
|
3514
|
-
const sessionStartScript =
|
|
3857
|
+
const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
|
|
3858
|
+
const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
|
|
3859
|
+
const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
|
|
3860
|
+
const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
|
|
3861
|
+
const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
|
|
3862
|
+
const sessionStartScript = join7(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
|
|
3515
3863
|
console.log("Hook scripts:");
|
|
3516
|
-
console.log(` ${
|
|
3517
|
-
console.log(` ${
|
|
3518
|
-
console.log(` ${
|
|
3519
|
-
console.log(` ${
|
|
3520
|
-
console.log(` ${
|
|
3521
|
-
console.log(` ${
|
|
3864
|
+
console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
|
|
3865
|
+
console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
|
|
3866
|
+
console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
|
|
3867
|
+
console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
|
|
3868
|
+
console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
|
|
3869
|
+
console.log(` ${existsSync8(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
|
|
3522
3870
|
console.log();
|
|
3523
3871
|
const mcp = inspectMcpConfig();
|
|
3524
3872
|
console.log("Guardrails MCP server (Claude Code):");
|
|
@@ -3530,7 +3878,7 @@ function statusCommand() {
|
|
|
3530
3878
|
console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
|
|
3531
3879
|
}
|
|
3532
3880
|
}
|
|
3533
|
-
var
|
|
3881
|
+
var SYNKRO_DIR3, CONFIG_PATH3;
|
|
3534
3882
|
var init_status = __esm({
|
|
3535
3883
|
"cli/commands/status.ts"() {
|
|
3536
3884
|
"use strict";
|
|
@@ -3538,8 +3886,8 @@ var init_status = __esm({
|
|
|
3538
3886
|
init_agentDetect();
|
|
3539
3887
|
init_ccHookConfig();
|
|
3540
3888
|
init_mcpConfig();
|
|
3541
|
-
|
|
3542
|
-
|
|
3889
|
+
SYNKRO_DIR3 = join7(homedir6(), ".synkro");
|
|
3890
|
+
CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
|
|
3543
3891
|
}
|
|
3544
3892
|
});
|
|
3545
3893
|
|
|
@@ -3569,7 +3917,7 @@ var unlink_exports = {};
|
|
|
3569
3917
|
__export(unlink_exports, {
|
|
3570
3918
|
unlinkCommand: () => unlinkCommand
|
|
3571
3919
|
});
|
|
3572
|
-
import { createInterface as
|
|
3920
|
+
import { createInterface as createInterface4 } from "readline";
|
|
3573
3921
|
function ask2(rl, question) {
|
|
3574
3922
|
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
3575
3923
|
}
|
|
@@ -3597,7 +3945,7 @@ async function unlinkCommand() {
|
|
|
3597
3945
|
console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
|
|
3598
3946
|
});
|
|
3599
3947
|
console.log();
|
|
3600
|
-
const rl =
|
|
3948
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
3601
3949
|
try {
|
|
3602
3950
|
const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
|
|
3603
3951
|
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
|
|
@@ -3623,200 +3971,12 @@ var init_unlink = __esm({
|
|
|
3623
3971
|
}
|
|
3624
3972
|
});
|
|
3625
3973
|
|
|
3626
|
-
// cli/commands/setupGithub.ts
|
|
3627
|
-
var setupGithub_exports = {};
|
|
3628
|
-
__export(setupGithub_exports, {
|
|
3629
|
-
setupGithubCommand: () => setupGithubCommand
|
|
3630
|
-
});
|
|
3631
|
-
import { createInterface as createInterface3 } from "readline/promises";
|
|
3632
|
-
import { stdin as input, stdout as output } from "process";
|
|
3633
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
3634
|
-
import { homedir as homedir6 } from "os";
|
|
3635
|
-
import { join as join7 } from "path";
|
|
3636
|
-
function readConfig() {
|
|
3637
|
-
if (!existsSync8(CONFIG_PATH3)) return {};
|
|
3638
|
-
const out = {};
|
|
3639
|
-
for (const line of readFileSync6(CONFIG_PATH3, "utf-8").split("\n")) {
|
|
3640
|
-
const t = line.trim();
|
|
3641
|
-
if (!t || t.startsWith("#")) continue;
|
|
3642
|
-
const eq = t.indexOf("=");
|
|
3643
|
-
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
|
3644
|
-
}
|
|
3645
|
-
return out;
|
|
3646
|
-
}
|
|
3647
|
-
async function prompt(rl, q, opts = {}) {
|
|
3648
|
-
if (opts.silent) {
|
|
3649
|
-
process.stdout.write(q);
|
|
3650
|
-
const wasRaw = process.stdin.isRaw;
|
|
3651
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
3652
|
-
return await new Promise((resolve2) => {
|
|
3653
|
-
let chunk = "";
|
|
3654
|
-
const onData = (data) => {
|
|
3655
|
-
const s = data.toString("utf-8");
|
|
3656
|
-
if (s === "\r" || s === "\n" || s === "\r\n") {
|
|
3657
|
-
process.stdin.removeListener("data", onData);
|
|
3658
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
3659
|
-
process.stdout.write("\n");
|
|
3660
|
-
resolve2(chunk);
|
|
3661
|
-
return;
|
|
3662
|
-
}
|
|
3663
|
-
if (s === "") {
|
|
3664
|
-
process.exit(130);
|
|
3665
|
-
}
|
|
3666
|
-
if (s === "\x7F" || s === "\b") {
|
|
3667
|
-
chunk = chunk.slice(0, -1);
|
|
3668
|
-
return;
|
|
3669
|
-
}
|
|
3670
|
-
chunk += s;
|
|
3671
|
-
};
|
|
3672
|
-
process.stdin.on("data", onData);
|
|
3673
|
-
});
|
|
3674
|
-
}
|
|
3675
|
-
return await rl.question(q);
|
|
3676
|
-
}
|
|
3677
|
-
async function setupGithubCommand() {
|
|
3678
|
-
if (!isAuthenticated()) {
|
|
3679
|
-
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
3680
|
-
process.exit(1);
|
|
3681
|
-
}
|
|
3682
|
-
const config2 = readConfig();
|
|
3683
|
-
const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
3684
|
-
const jwt2 = getAccessToken();
|
|
3685
|
-
if (!jwt2) {
|
|
3686
|
-
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
|
|
3687
|
-
process.exit(1);
|
|
3688
|
-
}
|
|
3689
|
-
console.log("Requesting CI API key from Synkro...");
|
|
3690
|
-
let synkroCiApiKey;
|
|
3691
|
-
try {
|
|
3692
|
-
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
|
|
3693
|
-
method: "POST",
|
|
3694
|
-
headers: {
|
|
3695
|
-
"Authorization": `Bearer ${jwt2}`,
|
|
3696
|
-
"Content-Type": "application/json"
|
|
3697
|
-
},
|
|
3698
|
-
body: "{}"
|
|
3699
|
-
});
|
|
3700
|
-
if (!resp.ok) {
|
|
3701
|
-
const errText = await resp.text().catch(() => "");
|
|
3702
|
-
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
3703
|
-
process.exit(1);
|
|
3704
|
-
}
|
|
3705
|
-
const minted = await resp.json();
|
|
3706
|
-
synkroCiApiKey = minted.api_key;
|
|
3707
|
-
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
3708
|
-
} catch (err) {
|
|
3709
|
-
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
3710
|
-
process.exit(1);
|
|
3711
|
-
}
|
|
3712
|
-
const rl = createInterface3({ input, output });
|
|
3713
|
-
console.log("Synkro PR scan setup\n");
|
|
3714
|
-
console.log("Requirements:");
|
|
3715
|
-
console.log(" \u2022 Claude Code Pro or Max subscription (for `claude setup-token`)");
|
|
3716
|
-
console.log(" \u2022 A GitHub personal access token with `repo` scope");
|
|
3717
|
-
console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
|
|
3718
|
-
const ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
|
|
3719
|
-
if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
|
|
3720
|
-
console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
|
|
3721
|
-
rl.close();
|
|
3722
|
-
process.exit(1);
|
|
3723
|
-
}
|
|
3724
|
-
console.log("\nNow get your Claude Code OAuth token:");
|
|
3725
|
-
console.log(" 1. In another terminal, run: claude setup-token");
|
|
3726
|
-
console.log(" 2. Complete the browser flow");
|
|
3727
|
-
console.log(" 3. Copy the resulting sk-ant-oat01-... token\n");
|
|
3728
|
-
const claudeToken = (await prompt(rl, "Claude Code OAuth token (paste): ", { silent: true })).trim();
|
|
3729
|
-
if (!claudeToken.startsWith("sk-ant-oat01-")) {
|
|
3730
|
-
console.error("Invalid token. Expected sk-ant-oat01-... \u2014 generate one with `claude setup-token`.");
|
|
3731
|
-
rl.close();
|
|
3732
|
-
process.exit(1);
|
|
3733
|
-
}
|
|
3734
|
-
console.log("\nFetching accessible repos...");
|
|
3735
|
-
const repos = await listAccessibleRepos({ token: ghToken });
|
|
3736
|
-
if (repos.length === 0) {
|
|
3737
|
-
console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
|
|
3738
|
-
rl.close();
|
|
3739
|
-
process.exit(1);
|
|
3740
|
-
}
|
|
3741
|
-
console.log(`
|
|
3742
|
-
Found ${repos.length} accessible repo(s):
|
|
3743
|
-
`);
|
|
3744
|
-
repos.slice(0, 100).forEach((r, i) => {
|
|
3745
|
-
console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
|
|
3746
|
-
});
|
|
3747
|
-
console.log();
|
|
3748
|
-
const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
|
|
3749
|
-
const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
|
|
3750
|
-
if (selectedIdx.length === 0) {
|
|
3751
|
-
console.error("No valid selections.");
|
|
3752
|
-
rl.close();
|
|
3753
|
-
process.exit(1);
|
|
3754
|
-
}
|
|
3755
|
-
const selected = selectedIdx.map((i) => repos[i]);
|
|
3756
|
-
console.log(`
|
|
3757
|
-
Will push secrets to ${selected.length} repo(s):`);
|
|
3758
|
-
for (const r of selected) console.log(` \u2022 ${r.full_name}`);
|
|
3759
|
-
console.log();
|
|
3760
|
-
const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
|
|
3761
|
-
if (confirm !== "yes" && confirm !== "y") {
|
|
3762
|
-
console.log("Cancelled.");
|
|
3763
|
-
rl.close();
|
|
3764
|
-
process.exit(0);
|
|
3765
|
-
}
|
|
3766
|
-
rl.close();
|
|
3767
|
-
console.log();
|
|
3768
|
-
for (const r of selected) {
|
|
3769
|
-
process.stdout.write(`Pushing secrets to ${r.full_name}... `);
|
|
3770
|
-
try {
|
|
3771
|
-
await pushSecretsToRepo(
|
|
3772
|
-
{ token: ghToken },
|
|
3773
|
-
r.owner,
|
|
3774
|
-
r.repo,
|
|
3775
|
-
{
|
|
3776
|
-
claudeCodeOauthToken: claudeToken,
|
|
3777
|
-
synkroApiKey: synkroCiApiKey
|
|
3778
|
-
}
|
|
3779
|
-
);
|
|
3780
|
-
console.log("\u2713");
|
|
3781
|
-
} catch (err) {
|
|
3782
|
-
console.log(`\u2717 (${err.message})`);
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
console.log();
|
|
3786
|
-
const gitRoot = findGitRoot(process.cwd());
|
|
3787
|
-
if (gitRoot) {
|
|
3788
|
-
const written = writeWorkflowFile(gitRoot);
|
|
3789
|
-
if (written) {
|
|
3790
|
-
console.log(`Wrote workflow: ${written}`);
|
|
3791
|
-
console.log("Commit and push it to enable PR scanning.");
|
|
3792
|
-
}
|
|
3793
|
-
} else {
|
|
3794
|
-
console.log("Not in a git repo. To enable scanning, add this file to your repo:");
|
|
3795
|
-
console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
|
|
3796
|
-
console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
|
|
3797
|
-
}
|
|
3798
|
-
console.log();
|
|
3799
|
-
console.log("\u2713 PR scan setup complete.");
|
|
3800
|
-
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
3801
|
-
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
3802
|
-
}
|
|
3803
|
-
var SYNKRO_DIR3, CONFIG_PATH3;
|
|
3804
|
-
var init_setupGithub = __esm({
|
|
3805
|
-
"cli/commands/setupGithub.ts"() {
|
|
3806
|
-
"use strict";
|
|
3807
|
-
init_githubSetup();
|
|
3808
|
-
init_stub();
|
|
3809
|
-
SYNKRO_DIR3 = join7(homedir6(), ".synkro");
|
|
3810
|
-
CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
|
|
3811
|
-
}
|
|
3812
|
-
});
|
|
3813
|
-
|
|
3814
3974
|
// cli/commands/scanPr.ts
|
|
3815
3975
|
var scanPr_exports = {};
|
|
3816
3976
|
__export(scanPr_exports, {
|
|
3817
3977
|
scanPrCommand: () => scanPrCommand
|
|
3818
3978
|
});
|
|
3819
|
-
import { execSync as
|
|
3979
|
+
import { execSync as execSync5, spawn } from "child_process";
|
|
3820
3980
|
function parseMatchSpec(condition) {
|
|
3821
3981
|
if (!condition.startsWith("match_spec:")) return null;
|
|
3822
3982
|
try {
|
|
@@ -3922,7 +4082,7 @@ function shouldSkipFile(filename) {
|
|
|
3922
4082
|
return SKIP_FILE_PATTERNS.some((p) => p.test(filename));
|
|
3923
4083
|
}
|
|
3924
4084
|
function ghJson(args2) {
|
|
3925
|
-
const out =
|
|
4085
|
+
const out = execSync5(`gh ${args2.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`, {
|
|
3926
4086
|
encoding: "utf-8",
|
|
3927
4087
|
maxBuffer: 16 * 1024 * 1024
|
|
3928
4088
|
});
|
|
@@ -3978,9 +4138,9 @@ ${hunks}`;
|
|
|
3978
4138
|
const t0 = Date.now();
|
|
3979
4139
|
const proc = spawn(
|
|
3980
4140
|
"claude",
|
|
3981
|
-
["--print", "--model", "claude-sonnet-4-6", "--output-format", "json", "--no-session-persistence"
|
|
4141
|
+
["--print", "--model", "claude-sonnet-4-6", "--output-format", "json", "--no-session-persistence"],
|
|
3982
4142
|
{
|
|
3983
|
-
stdio: ["
|
|
4143
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3984
4144
|
env: {
|
|
3985
4145
|
...process.env,
|
|
3986
4146
|
CLAUDE_CODE_OAUTH_TOKEN: claudeToken
|
|
@@ -3988,6 +4148,8 @@ ${hunks}`;
|
|
|
3988
4148
|
timeout: 12e4
|
|
3989
4149
|
}
|
|
3990
4150
|
);
|
|
4151
|
+
proc.stdin.write(fullPrompt);
|
|
4152
|
+
proc.stdin.end();
|
|
3991
4153
|
let stdout = "";
|
|
3992
4154
|
let stderr = "";
|
|
3993
4155
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -4036,19 +4198,174 @@ async function processInBatches(items, batchSize, fn) {
|
|
|
4036
4198
|
}
|
|
4037
4199
|
return results;
|
|
4038
4200
|
}
|
|
4039
|
-
function
|
|
4040
|
-
|
|
4201
|
+
function buildConsolidationPrompt(findings) {
|
|
4202
|
+
return `You are a senior security reviewer writing the final PR review. You received raw findings from an automated detector. Your job:
|
|
4041
4203
|
|
|
4042
|
-
|
|
4204
|
+
1. VERIFY \u2014 remove false positives or findings that are clearly wrong.
|
|
4205
|
+
2. CONSOLIDATE \u2014 if multiple findings describe the same underlying issue in the same file (e.g. 8 hardcoded secrets), merge them into ONE comment pinned to the first line, listing all affected lines. If findings are genuinely different issues (e.g. SQL injection on line 5 AND a hardcoded secret on line 12), keep them separate.
|
|
4206
|
+
3. WRITE \u2014 produce concise, actionable review comments. No fluff. Each comment should name the issue and say what to do. One sentence max for description, one for fix.
|
|
4043
4207
|
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4208
|
+
Output ONLY a JSON object (no prose, no markdown fences):
|
|
4209
|
+
{
|
|
4210
|
+
"summary": "<2-3 sentence overview for the review body \u2014 total issues, severity, what to do>",
|
|
4211
|
+
"comments": [
|
|
4212
|
+
{
|
|
4213
|
+
"path": "<file path>",
|
|
4214
|
+
"line": <integer, first affected line number \u2014 REQUIRED, never null>,
|
|
4215
|
+
"body": "<markdown comment \u2014 include affected lines if consolidated, e.g. 'Lines 1-8: ...'>"
|
|
4216
|
+
}
|
|
4217
|
+
]
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
Rules:
|
|
4221
|
+
- Each comment body should start with a severity emoji+label: \u{1F534} critical, \u{1F7E0} high, \u{1F7E1} medium, \u{1F535} low
|
|
4222
|
+
- If consolidating, mention all affected line numbers in the body
|
|
4223
|
+
- Keep comments short \u2014 developers read these in a PR, not a report
|
|
4224
|
+
- Maximum 15 comments. If more, pick the most critical and mention the rest in the summary.
|
|
4225
|
+
- If all findings are legitimate and distinct, keep them all (up to 15)
|
|
4226
|
+
- If you determine ALL findings are false positives, return {"summary": "No issues found after verification.", "comments": []}
|
|
4227
|
+
|
|
4228
|
+
Raw findings from detector:
|
|
4229
|
+
${JSON.stringify(findings, null, 2)}
|
|
4230
|
+
`;
|
|
4231
|
+
}
|
|
4232
|
+
function spawnOpusConsolidator(findings, claudeToken) {
|
|
4233
|
+
return new Promise((resolve2) => {
|
|
4234
|
+
const prompt2 = buildConsolidationPrompt(findings);
|
|
4235
|
+
const proc = spawn(
|
|
4236
|
+
"claude",
|
|
4237
|
+
["--print", "--model", "claude-opus-4-7", "--output-format", "json", "--no-session-persistence"],
|
|
4238
|
+
{
|
|
4239
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4240
|
+
env: {
|
|
4241
|
+
...process.env,
|
|
4242
|
+
CLAUDE_CODE_OAUTH_TOKEN: claudeToken
|
|
4243
|
+
},
|
|
4244
|
+
timeout: 12e4
|
|
4245
|
+
}
|
|
4049
4246
|
);
|
|
4050
|
-
|
|
4051
|
-
|
|
4247
|
+
proc.stdin.write(prompt2);
|
|
4248
|
+
proc.stdin.end();
|
|
4249
|
+
let stdout = "";
|
|
4250
|
+
let stderr = "";
|
|
4251
|
+
proc.stdout.on("data", (chunk) => {
|
|
4252
|
+
stdout += chunk.toString();
|
|
4253
|
+
});
|
|
4254
|
+
proc.stderr.on("data", (chunk) => {
|
|
4255
|
+
stderr += chunk.toString();
|
|
4256
|
+
});
|
|
4257
|
+
proc.on("close", (code) => {
|
|
4258
|
+
if (code !== 0) {
|
|
4259
|
+
console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
|
|
4260
|
+
resolve2(fallbackReview(findings));
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
try {
|
|
4264
|
+
const wrapper = JSON.parse(stdout);
|
|
4265
|
+
const responseText = (wrapper.result || wrapper.response || wrapper.text || "").trim();
|
|
4266
|
+
let txt = responseText;
|
|
4267
|
+
if (txt.startsWith("```")) {
|
|
4268
|
+
txt = txt.replace(/^```(?:json)?\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
4269
|
+
}
|
|
4270
|
+
const review = JSON.parse(txt);
|
|
4271
|
+
const comments = (review.comments || []).map((c) => ({
|
|
4272
|
+
path: c.path,
|
|
4273
|
+
line: c.line ?? 1,
|
|
4274
|
+
side: "RIGHT",
|
|
4275
|
+
body: c.body
|
|
4276
|
+
}));
|
|
4277
|
+
const maxSeverity = findings.reduce((max, f) => {
|
|
4278
|
+
const order = ["low", "medium", "high", "critical"];
|
|
4279
|
+
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
4280
|
+
}, "low");
|
|
4281
|
+
resolve2({ summary: review.summary || "", comments, severity: maxSeverity });
|
|
4282
|
+
} catch {
|
|
4283
|
+
console.warn(` failed to parse opus response, using fallback`);
|
|
4284
|
+
resolve2(fallbackReview(findings));
|
|
4285
|
+
}
|
|
4286
|
+
});
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
function fallbackReview(findings) {
|
|
4290
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4291
|
+
for (const f of findings) {
|
|
4292
|
+
const key = `${f.file}::${f.category}`;
|
|
4293
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
4294
|
+
grouped.get(key).push(f);
|
|
4295
|
+
}
|
|
4296
|
+
const comments = [];
|
|
4297
|
+
for (const [, group] of grouped) {
|
|
4298
|
+
const first = group[0];
|
|
4299
|
+
const lines = group.map((f) => f.line);
|
|
4300
|
+
const linesStr = lines.length > 1 ? `Lines ${lines.join(", ")}` : `Line ${lines[0]}`;
|
|
4301
|
+
const severityEmoji = first.severity === "critical" ? "\u{1F534}" : first.severity === "high" ? "\u{1F7E0}" : first.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
4302
|
+
comments.push({
|
|
4303
|
+
path: first.file,
|
|
4304
|
+
line: first.line,
|
|
4305
|
+
side: "RIGHT",
|
|
4306
|
+
body: `${severityEmoji} **${first.severity}: ${first.category}**
|
|
4307
|
+
|
|
4308
|
+
${linesStr}: ${first.description}
|
|
4309
|
+
|
|
4310
|
+
**Fix:** ${first.fix}`
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
const maxSeverity = findings.reduce((max, f) => {
|
|
4314
|
+
const order = ["low", "medium", "high", "critical"];
|
|
4315
|
+
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
4316
|
+
}, "low");
|
|
4317
|
+
return {
|
|
4318
|
+
summary: `${findings.length} security finding(s) detected.`,
|
|
4319
|
+
comments: comments.slice(0, 15),
|
|
4320
|
+
severity: maxSeverity
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
function postPrReview(repo, prNumber, sha, review) {
|
|
4324
|
+
const preferredEvent = review.severity === "critical" || review.severity === "high" ? "REQUEST_CHANGES" : "COMMENT";
|
|
4325
|
+
function tryPost(event) {
|
|
4326
|
+
const body = JSON.stringify({
|
|
4327
|
+
commit_id: sha,
|
|
4328
|
+
body: `## \u{1F512} Synkro Security Review
|
|
4329
|
+
|
|
4330
|
+
${review.summary}`,
|
|
4331
|
+
event,
|
|
4332
|
+
comments: review.comments
|
|
4333
|
+
});
|
|
4334
|
+
try {
|
|
4335
|
+
execSync5(`gh api -X POST /repos/${repo}/pulls/${prNumber}/reviews --input -`, {
|
|
4336
|
+
encoding: "utf-8",
|
|
4337
|
+
input: body,
|
|
4338
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4339
|
+
});
|
|
4340
|
+
console.log(` \u2713 Posted PR review (${event}).`);
|
|
4341
|
+
return true;
|
|
4342
|
+
} catch (err) {
|
|
4343
|
+
const stderr = err.stderr?.toString() || "";
|
|
4344
|
+
const stdout = err.stdout?.toString() || "";
|
|
4345
|
+
const combined = stderr + stdout;
|
|
4346
|
+
if (combined.includes("own pull request") && event === "REQUEST_CHANGES") {
|
|
4347
|
+
return false;
|
|
4348
|
+
}
|
|
4349
|
+
console.warn(`Failed to post review: ${(stderr || stdout || err.message).slice(0, 200)}`);
|
|
4350
|
+
return false;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
if (tryPost(preferredEvent)) return;
|
|
4354
|
+
if (preferredEvent === "REQUEST_CHANGES" && tryPost("COMMENT")) return;
|
|
4355
|
+
try {
|
|
4356
|
+
const fallbackBody = `## \u{1F512} Synkro Security Review
|
|
4357
|
+
|
|
4358
|
+
${review.summary}
|
|
4359
|
+
|
|
4360
|
+
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
4361
|
+
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
4362
|
+
encoding: "utf-8",
|
|
4363
|
+
input: JSON.stringify({ body: fallbackBody }),
|
|
4364
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
4365
|
+
});
|
|
4366
|
+
console.log(" \u2713 Posted fallback issue comment.");
|
|
4367
|
+
} catch (err2) {
|
|
4368
|
+
console.warn("Failed to post fallback comment:", err2.message);
|
|
4052
4369
|
}
|
|
4053
4370
|
}
|
|
4054
4371
|
function postCheckRun(repo, sha, conclusion, findings) {
|
|
@@ -4065,7 +4382,7 @@ function postCheckRun(repo, sha, conclusion, findings) {
|
|
|
4065
4382
|
}
|
|
4066
4383
|
});
|
|
4067
4384
|
try {
|
|
4068
|
-
|
|
4385
|
+
execSync5(`gh api -X POST /repos/${repo}/check-runs --input -`, {
|
|
4069
4386
|
encoding: "utf-8",
|
|
4070
4387
|
input: body,
|
|
4071
4388
|
stdio: ["pipe", "ignore", "pipe"]
|
|
@@ -4173,8 +4490,13 @@ async function scanPrCommand() {
|
|
|
4173
4490
|
console.log(`
|
|
4174
4491
|
Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${totalLatencyMs}ms
|
|
4175
4492
|
`);
|
|
4176
|
-
|
|
4177
|
-
|
|
4493
|
+
if (allFindings.length > 0) {
|
|
4494
|
+
console.log("Consolidating findings with Opus 4.7...");
|
|
4495
|
+
const review = await spawnOpusConsolidator(allFindings, claudeToken);
|
|
4496
|
+
console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
|
|
4497
|
+
if (review.comments.length > 0) {
|
|
4498
|
+
postPrReview(repo, prNumber, sha, review);
|
|
4499
|
+
}
|
|
4178
4500
|
}
|
|
4179
4501
|
const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
|
|
4180
4502
|
postCheckRun(repo, sha, conclusion, allFindings);
|
|
@@ -4336,8 +4658,8 @@ for (const envPath of envCandidates) {
|
|
|
4336
4658
|
const eqIndex = trimmed.indexOf("=");
|
|
4337
4659
|
if (eqIndex <= 0) continue;
|
|
4338
4660
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
4339
|
-
const value = trimmed.slice(eqIndex + 1).trim();
|
|
4340
|
-
if (!process.env[key]) process.env[key] = value;
|
|
4661
|
+
const value = trimmed.slice(eqIndex + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
4662
|
+
if (!process.env[key] && !value.startsWith("op://")) process.env[key] = value;
|
|
4341
4663
|
}
|
|
4342
4664
|
}
|
|
4343
4665
|
var args = process.argv.slice(2);
|
|
@@ -4358,7 +4680,6 @@ Commands:
|
|
|
4358
4680
|
link Link repos to a Synkro project (local git or GitHub OAuth)
|
|
4359
4681
|
unlink Remove repo links from Synkro projects
|
|
4360
4682
|
setup-github Configure GitHub PR scanning (push secrets + workflow file)
|
|
4361
|
-
scan-pr Run a PR scan (used by GitHub Actions, not for direct invocation)
|
|
4362
4683
|
update Refresh hook configs and judge prompts
|
|
4363
4684
|
disconnect [--purge] Remove Synkro hooks from agents (--purge also removes ~/.synkro)
|
|
4364
4685
|
uninstall Fully remove Synkro from this machine
|
|
@@ -4407,7 +4728,13 @@ async function main() {
|
|
|
4407
4728
|
}
|
|
4408
4729
|
case "setup-github": {
|
|
4409
4730
|
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
4410
|
-
|
|
4731
|
+
const ghOpts = {};
|
|
4732
|
+
for (const a of subArgs) {
|
|
4733
|
+
if (a === "--non-interactive") ghOpts.nonInteractive = true;
|
|
4734
|
+
else if (a === "--skip-claude-token") ghOpts.skipClaudeToken = true;
|
|
4735
|
+
else if (a.startsWith("--github-token=")) ghOpts.githubToken = a.slice("--github-token=".length);
|
|
4736
|
+
}
|
|
4737
|
+
await setupGithubCommand2(ghOpts);
|
|
4411
4738
|
break;
|
|
4412
4739
|
}
|
|
4413
4740
|
case "scan-pr": {
|