@synkro-sh/cli 1.3.16 → 1.3.17
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 +630 -324
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -175,18 +175,20 @@ function installCCHooks(settingsPath, config2) {
|
|
|
175
175
|
],
|
|
176
176
|
[SYNKRO_MARKER]: true
|
|
177
177
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
hooks
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
if (!config2.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: config2.transcriptSyncScriptPath,
|
|
186
|
+
timeout: 3
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
[SYNKRO_MARKER]: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
190
192
|
writeSettingsAtomic(settingsPath, settings);
|
|
191
193
|
}
|
|
192
194
|
function uninstallCCHooks(settingsPath) {
|
|
@@ -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
|
|
|
@@ -2606,7 +2609,9 @@ async function listAccessibleRepos(opts) {
|
|
|
2606
2609
|
}
|
|
2607
2610
|
async function pushSecretsToRepo(opts, owner, repo, secrets) {
|
|
2608
2611
|
const pubkey = await getRepoPublicKey(opts, owner, repo);
|
|
2609
|
-
|
|
2612
|
+
if (secrets.claudeCodeOauthToken) {
|
|
2613
|
+
await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
|
|
2614
|
+
}
|
|
2610
2615
|
await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
|
|
2611
2616
|
}
|
|
2612
2617
|
function writeWorkflowFile(repoRootPath) {
|
|
@@ -2750,8 +2755,27 @@ async function connectGithubAndSelectRepos() {
|
|
|
2750
2755
|
rl.close();
|
|
2751
2756
|
}
|
|
2752
2757
|
}
|
|
2753
|
-
async function promptRepoConnection() {
|
|
2758
|
+
async function promptRepoConnection(opts) {
|
|
2754
2759
|
const localRepo = detectGitRepo();
|
|
2760
|
+
if (opts?.linkRepo && localRepo) {
|
|
2761
|
+
console.log("Connect repos to Synkro:\n");
|
|
2762
|
+
try {
|
|
2763
|
+
const existing = await listProjects();
|
|
2764
|
+
const alreadyLinked = existing.some(
|
|
2765
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
2766
|
+
);
|
|
2767
|
+
if (!alreadyLinked) {
|
|
2768
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
2769
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
2770
|
+
} else {
|
|
2771
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
2772
|
+
}
|
|
2773
|
+
} catch (err) {
|
|
2774
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
2775
|
+
}
|
|
2776
|
+
console.log();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2755
2779
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2756
2780
|
try {
|
|
2757
2781
|
console.log("Connect repos to Synkro:\n");
|
|
@@ -2829,16 +2853,282 @@ var init_repoConnect = __esm({
|
|
|
2829
2853
|
}
|
|
2830
2854
|
});
|
|
2831
2855
|
|
|
2856
|
+
// cli/commands/setupGithub.ts
|
|
2857
|
+
var setupGithub_exports = {};
|
|
2858
|
+
__export(setupGithub_exports, {
|
|
2859
|
+
setupGithubCommand: () => setupGithubCommand
|
|
2860
|
+
});
|
|
2861
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2862
|
+
import { stdin as input, stdout as output } from "process";
|
|
2863
|
+
import { execSync as execSync3, spawn as nodeSpawn } from "child_process";
|
|
2864
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
2865
|
+
import { homedir as homedir4 } from "os";
|
|
2866
|
+
import { join as join5 } from "path";
|
|
2867
|
+
function readConfig() {
|
|
2868
|
+
if (!existsSync6(CONFIG_PATH)) return {};
|
|
2869
|
+
const out = {};
|
|
2870
|
+
for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
|
|
2871
|
+
const t = line.trim();
|
|
2872
|
+
if (!t || t.startsWith("#")) continue;
|
|
2873
|
+
const eq = t.indexOf("=");
|
|
2874
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
2875
|
+
}
|
|
2876
|
+
return out;
|
|
2877
|
+
}
|
|
2878
|
+
async function prompt(rl, q, opts = {}) {
|
|
2879
|
+
if (opts.silent) {
|
|
2880
|
+
process.stdout.write(q);
|
|
2881
|
+
const wasRaw = process.stdin.isRaw;
|
|
2882
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
2883
|
+
return await new Promise((resolve2) => {
|
|
2884
|
+
let chunk = "";
|
|
2885
|
+
const onData = (data) => {
|
|
2886
|
+
const s = data.toString("utf-8");
|
|
2887
|
+
if (s === "\r" || s === "\n" || s === "\r\n") {
|
|
2888
|
+
process.stdin.removeListener("data", onData);
|
|
2889
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
2890
|
+
process.stdout.write("\n");
|
|
2891
|
+
resolve2(chunk);
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
if (s === "") {
|
|
2895
|
+
process.exit(130);
|
|
2896
|
+
}
|
|
2897
|
+
if (s === "\x7F" || s === "\b") {
|
|
2898
|
+
chunk = chunk.slice(0, -1);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
chunk += s;
|
|
2902
|
+
};
|
|
2903
|
+
process.stdin.on("data", onData);
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
return await rl.question(q);
|
|
2907
|
+
}
|
|
2908
|
+
function captureClaudeSetupToken() {
|
|
2909
|
+
return new Promise((resolve2, reject) => {
|
|
2910
|
+
const proc = nodeSpawn("script", ["-q", "/dev/null", "claude", "setup-token"], {
|
|
2911
|
+
stdio: ["inherit", "pipe", "inherit"]
|
|
2912
|
+
});
|
|
2913
|
+
let stdout = "";
|
|
2914
|
+
proc.stdout.on("data", (chunk) => {
|
|
2915
|
+
stdout += chunk.toString();
|
|
2916
|
+
});
|
|
2917
|
+
proc.on("error", (err) => reject(new Error(`Failed to spawn claude setup-token: ${err.message}`)));
|
|
2918
|
+
proc.on("close", (code) => {
|
|
2919
|
+
if (code !== 0) {
|
|
2920
|
+
reject(new Error(`claude setup-token exited with code ${code}`));
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const stripped = stdout.replace(/\r/g, "").replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b[^[\]]*/, "").replace(/\n/g, "");
|
|
2924
|
+
const match = stripped.match(/sk-ant-oat01-[A-Za-z0-9_-]+AA/);
|
|
2925
|
+
if (!match) {
|
|
2926
|
+
reject(new Error("Could not find token in claude setup-token output"));
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
resolve2(match[0]);
|
|
2930
|
+
});
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
async function setupGithubCommand(opts = {}) {
|
|
2934
|
+
if (!isAuthenticated()) {
|
|
2935
|
+
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
2936
|
+
process.exit(1);
|
|
2937
|
+
}
|
|
2938
|
+
const config2 = readConfig();
|
|
2939
|
+
const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
2940
|
+
const jwt2 = getAccessToken();
|
|
2941
|
+
if (!jwt2) {
|
|
2942
|
+
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
|
|
2943
|
+
process.exit(1);
|
|
2944
|
+
}
|
|
2945
|
+
console.log("Requesting CI API key from Synkro...");
|
|
2946
|
+
let synkroCiApiKey;
|
|
2947
|
+
try {
|
|
2948
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
|
|
2949
|
+
method: "POST",
|
|
2950
|
+
headers: {
|
|
2951
|
+
"Authorization": `Bearer ${jwt2}`,
|
|
2952
|
+
"Content-Type": "application/json"
|
|
2953
|
+
},
|
|
2954
|
+
body: "{}"
|
|
2955
|
+
});
|
|
2956
|
+
if (!resp.ok) {
|
|
2957
|
+
const errText = await resp.text().catch(() => "");
|
|
2958
|
+
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
2959
|
+
process.exit(1);
|
|
2960
|
+
}
|
|
2961
|
+
const minted = await resp.json();
|
|
2962
|
+
synkroCiApiKey = minted.api_key;
|
|
2963
|
+
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
2964
|
+
} catch (err) {
|
|
2965
|
+
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
2966
|
+
process.exit(1);
|
|
2967
|
+
}
|
|
2968
|
+
let ghToken;
|
|
2969
|
+
if (opts.githubToken) {
|
|
2970
|
+
ghToken = opts.githubToken;
|
|
2971
|
+
} else if (opts.nonInteractive) {
|
|
2972
|
+
try {
|
|
2973
|
+
ghToken = execSync3("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
2974
|
+
} catch {
|
|
2975
|
+
console.error("Could not get GitHub token from `gh auth token`. Run `gh auth login` first.");
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
} else {
|
|
2979
|
+
const rl = createInterface2({ input, output });
|
|
2980
|
+
console.log("Synkro PR scan setup\n");
|
|
2981
|
+
console.log("Requirements:");
|
|
2982
|
+
console.log(" \u2022 Claude Code installed and logged in (Pro, Max, Teams, or Enterprise)");
|
|
2983
|
+
console.log(" \u2022 A GitHub personal access token with `repo` scope");
|
|
2984
|
+
console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
|
|
2985
|
+
ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
|
|
2986
|
+
rl.close();
|
|
2987
|
+
if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
|
|
2988
|
+
console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
|
|
2989
|
+
process.exit(1);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
let claudeToken;
|
|
2993
|
+
if (!opts.skipClaudeToken) {
|
|
2994
|
+
console.log("Generating Claude Code OAuth token...");
|
|
2995
|
+
console.log(" A browser window will open \u2014 authorize with your Claude account.\n");
|
|
2996
|
+
try {
|
|
2997
|
+
claudeToken = await captureClaudeSetupToken();
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
console.error(`Failed to get Claude token: ${err instanceof Error ? err.message : String(err)}`);
|
|
3000
|
+
if (opts.nonInteractive) return;
|
|
3001
|
+
process.exit(1);
|
|
3002
|
+
}
|
|
3003
|
+
if (!claudeToken.startsWith("sk-ant-oat01-")) {
|
|
3004
|
+
console.error("Invalid token received from `claude setup-token`. Expected sk-ant-oat01-...");
|
|
3005
|
+
if (opts.nonInteractive) return;
|
|
3006
|
+
process.exit(1);
|
|
3007
|
+
}
|
|
3008
|
+
console.log(" Validating token...");
|
|
3009
|
+
try {
|
|
3010
|
+
const validateResult = execSync3(
|
|
3011
|
+
'claude --print --output-format json "say ok"',
|
|
3012
|
+
{ env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
|
|
3013
|
+
);
|
|
3014
|
+
const result = JSON.parse(validateResult);
|
|
3015
|
+
if (result.is_error) throw new Error(result.result || "auth failed");
|
|
3016
|
+
console.log(" \u2713 Token validated.\n");
|
|
3017
|
+
} catch (err) {
|
|
3018
|
+
console.error(`Token validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3019
|
+
if (opts.nonInteractive) return;
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
let selected;
|
|
3024
|
+
if (opts.nonInteractive) {
|
|
3025
|
+
let currentFullName = null;
|
|
3026
|
+
try {
|
|
3027
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3028
|
+
const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
|
|
3029
|
+
if (m) currentFullName = m[1];
|
|
3030
|
+
} catch {
|
|
3031
|
+
}
|
|
3032
|
+
if (!currentFullName) {
|
|
3033
|
+
console.warn(" \u26A0 Not in a GitHub repo. Skipping PR scan setup.");
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
const [owner, repo] = currentFullName.split("/");
|
|
3037
|
+
selected = [{ owner, repo, full_name: currentFullName }];
|
|
3038
|
+
console.log(` Auto-selected repo: ${currentFullName}`);
|
|
3039
|
+
} else {
|
|
3040
|
+
console.log("\nFetching accessible repos...");
|
|
3041
|
+
const repos = await listAccessibleRepos({ token: ghToken });
|
|
3042
|
+
if (repos.length === 0) {
|
|
3043
|
+
console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
|
|
3044
|
+
process.exit(1);
|
|
3045
|
+
}
|
|
3046
|
+
console.log(`
|
|
3047
|
+
Found ${repos.length} accessible repo(s):
|
|
3048
|
+
`);
|
|
3049
|
+
repos.slice(0, 100).forEach((r, i) => {
|
|
3050
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
|
|
3051
|
+
});
|
|
3052
|
+
console.log();
|
|
3053
|
+
const rl2 = createInterface2({ input, output });
|
|
3054
|
+
const selectionRaw = await prompt(rl2, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
|
|
3055
|
+
const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
|
|
3056
|
+
if (selectedIdx.length === 0) {
|
|
3057
|
+
console.error("No valid selections.");
|
|
3058
|
+
rl2.close();
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
selected = selectedIdx.map((i) => repos[i]);
|
|
3062
|
+
console.log(`
|
|
3063
|
+
Will push secrets to ${selected.length} repo(s):`);
|
|
3064
|
+
for (const r of selected) console.log(` \u2022 ${r.full_name}`);
|
|
3065
|
+
console.log();
|
|
3066
|
+
const confirm = (await prompt(rl2, "Continue? (yes/no): ")).trim().toLowerCase();
|
|
3067
|
+
if (confirm !== "yes" && confirm !== "y") {
|
|
3068
|
+
console.log("Cancelled.");
|
|
3069
|
+
rl2.close();
|
|
3070
|
+
process.exit(0);
|
|
3071
|
+
}
|
|
3072
|
+
rl2.close();
|
|
3073
|
+
}
|
|
3074
|
+
console.log();
|
|
3075
|
+
for (const r of selected) {
|
|
3076
|
+
process.stdout.write(`Pushing secrets to ${r.full_name}... `);
|
|
3077
|
+
try {
|
|
3078
|
+
await pushSecretsToRepo(
|
|
3079
|
+
{ token: ghToken },
|
|
3080
|
+
r.owner,
|
|
3081
|
+
r.repo,
|
|
3082
|
+
{
|
|
3083
|
+
claudeCodeOauthToken: claudeToken,
|
|
3084
|
+
synkroApiKey: synkroCiApiKey
|
|
3085
|
+
}
|
|
3086
|
+
);
|
|
3087
|
+
console.log("\u2713");
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
console.log(`\u2717 (${err.message})`);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
console.log();
|
|
3093
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
3094
|
+
if (gitRoot) {
|
|
3095
|
+
const written = writeWorkflowFile(gitRoot);
|
|
3096
|
+
if (written) {
|
|
3097
|
+
console.log(`Wrote workflow: ${written}`);
|
|
3098
|
+
console.log("Commit and push it to enable PR scanning.");
|
|
3099
|
+
}
|
|
3100
|
+
} else {
|
|
3101
|
+
console.log("Not in a git repo. To enable scanning, add this file to your repo:");
|
|
3102
|
+
console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
|
|
3103
|
+
console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
|
|
3104
|
+
}
|
|
3105
|
+
console.log();
|
|
3106
|
+
console.log("\u2713 PR scan setup complete.");
|
|
3107
|
+
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
3108
|
+
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
3109
|
+
}
|
|
3110
|
+
var SYNKRO_DIR, CONFIG_PATH;
|
|
3111
|
+
var init_setupGithub = __esm({
|
|
3112
|
+
"cli/commands/setupGithub.ts"() {
|
|
3113
|
+
"use strict";
|
|
3114
|
+
init_githubSetup();
|
|
3115
|
+
init_stub();
|
|
3116
|
+
SYNKRO_DIR = join5(homedir4(), ".synkro");
|
|
3117
|
+
CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
|
|
2832
3121
|
// cli/commands/install.ts
|
|
2833
3122
|
var install_exports = {};
|
|
2834
3123
|
__export(install_exports, {
|
|
2835
3124
|
installCommand: () => installCommand,
|
|
2836
3125
|
parseArgs: () => parseArgs
|
|
2837
3126
|
});
|
|
2838
|
-
import { existsSync as
|
|
2839
|
-
import { homedir as
|
|
2840
|
-
import { join as
|
|
2841
|
-
import { execSync as
|
|
3127
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
|
|
3128
|
+
import { homedir as homedir5 } from "os";
|
|
3129
|
+
import { join as join6 } from "path";
|
|
3130
|
+
import { execSync as execSync4 } from "child_process";
|
|
3131
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2842
3132
|
function sanitizeGatewayCandidate(raw) {
|
|
2843
3133
|
if (!raw) return void 0;
|
|
2844
3134
|
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
@@ -2851,6 +3141,8 @@ function parseArgs(argv) {
|
|
|
2851
3141
|
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
2852
3142
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
2853
3143
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
3144
|
+
else if (a === "--link-repo") opts.linkRepo = true;
|
|
3145
|
+
else if (a === "--pr-scan") opts.prScan = true;
|
|
2854
3146
|
}
|
|
2855
3147
|
if (!opts.gatewayUrl) {
|
|
2856
3148
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
@@ -2858,8 +3150,21 @@ function parseArgs(argv) {
|
|
|
2858
3150
|
}
|
|
2859
3151
|
return opts;
|
|
2860
3152
|
}
|
|
3153
|
+
async function promptTranscriptConsent() {
|
|
3154
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
3155
|
+
return new Promise((resolve2) => {
|
|
3156
|
+
rl.question(
|
|
3157
|
+
"Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
|
|
3158
|
+
(answer) => {
|
|
3159
|
+
rl.close();
|
|
3160
|
+
const trimmed = answer.trim().toLowerCase();
|
|
3161
|
+
resolve2(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
3162
|
+
}
|
|
3163
|
+
);
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
2861
3166
|
function ensureSynkroDir() {
|
|
2862
|
-
mkdirSync5(
|
|
3167
|
+
mkdirSync5(SYNKRO_DIR2, { recursive: true });
|
|
2863
3168
|
mkdirSync5(HOOKS_DIR, { recursive: true });
|
|
2864
3169
|
mkdirSync5(BIN_DIR, { recursive: true });
|
|
2865
3170
|
mkdirSync5(OFFSETS_DIR, { recursive: true });
|
|
@@ -2873,13 +3178,13 @@ function writeGraderDaemon() {
|
|
|
2873
3178
|
chmodSync(GRADER_PRIMER_BASH_PATH, 420);
|
|
2874
3179
|
}
|
|
2875
3180
|
function writeHookScripts() {
|
|
2876
|
-
const bashScriptPath =
|
|
2877
|
-
const bashFollowupScriptPath =
|
|
2878
|
-
const editCaptureScriptPath =
|
|
2879
|
-
const editPrecheckScriptPath =
|
|
2880
|
-
const stopSummaryScriptPath =
|
|
2881
|
-
const sessionStartScriptPath =
|
|
2882
|
-
const transcriptSyncScriptPath =
|
|
3181
|
+
const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
|
|
3182
|
+
const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
|
|
3183
|
+
const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
|
|
3184
|
+
const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
3185
|
+
const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
|
|
3186
|
+
const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
|
|
3187
|
+
const transcriptSyncScriptPath = join6(HOOKS_DIR, "cc-transcript-sync.sh");
|
|
2883
3188
|
writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
|
|
2884
3189
|
writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
2885
3190
|
writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
@@ -2912,7 +3217,7 @@ function shellQuoteSingle(value) {
|
|
|
2912
3217
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2913
3218
|
}
|
|
2914
3219
|
function writeConfigEnv(opts) {
|
|
2915
|
-
const credsPath =
|
|
3220
|
+
const credsPath = join6(SYNKRO_DIR2, "credentials.json");
|
|
2916
3221
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
2917
3222
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
2918
3223
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -2925,14 +3230,17 @@ function writeConfigEnv(opts) {
|
|
|
2925
3230
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2926
3231
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2927
3232
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2928
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3233
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.17")}`
|
|
2929
3234
|
];
|
|
2930
3235
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2931
3236
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
2932
3237
|
if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
|
|
3238
|
+
if (opts.transcriptConsent !== void 0) {
|
|
3239
|
+
lines.push(`SYNKRO_TRANSCRIPT_CONSENT=${shellQuoteSingle(opts.transcriptConsent ? "yes" : "no")}`);
|
|
3240
|
+
}
|
|
2933
3241
|
lines.push("");
|
|
2934
|
-
writeFileSync5(
|
|
2935
|
-
chmodSync(
|
|
3242
|
+
writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
3243
|
+
chmodSync(CONFIG_PATH2, 384);
|
|
2936
3244
|
}
|
|
2937
3245
|
function assertGatewayAllowed(gatewayUrl) {
|
|
2938
3246
|
let parsed;
|
|
@@ -2957,19 +3265,19 @@ function assertGatewayAllowed(gatewayUrl) {
|
|
|
2957
3265
|
}
|
|
2958
3266
|
function isAlreadyInstalled() {
|
|
2959
3267
|
const requiredScripts = [
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3268
|
+
join6(HOOKS_DIR, "cc-bash-judge.sh"),
|
|
3269
|
+
join6(HOOKS_DIR, "cc-bash-followup.sh"),
|
|
3270
|
+
join6(HOOKS_DIR, "cc-edit-precheck.sh"),
|
|
3271
|
+
join6(HOOKS_DIR, "cc-edit-capture.sh"),
|
|
3272
|
+
join6(HOOKS_DIR, "cc-stop-summary.sh"),
|
|
3273
|
+
join6(HOOKS_DIR, "cc-session-start.sh")
|
|
2966
3274
|
];
|
|
2967
|
-
if (!requiredScripts.every((p) =>
|
|
2968
|
-
if (!
|
|
2969
|
-
const settingsPath =
|
|
2970
|
-
if (!
|
|
3275
|
+
if (!requiredScripts.every((p) => existsSync7(p))) return false;
|
|
3276
|
+
if (!existsSync7(CONFIG_PATH2)) return false;
|
|
3277
|
+
const settingsPath = join6(homedir5(), ".claude", "settings.json");
|
|
3278
|
+
if (!existsSync7(settingsPath)) return false;
|
|
2971
3279
|
try {
|
|
2972
|
-
const settings = JSON.parse(
|
|
3280
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
2973
3281
|
const hooks = settings?.hooks;
|
|
2974
3282
|
if (!hooks || typeof hooks !== "object") return false;
|
|
2975
3283
|
const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
|
|
@@ -3028,7 +3336,7 @@ async function installCommand(opts = {}) {
|
|
|
3028
3336
|
console.error("No access token available after auth.");
|
|
3029
3337
|
process.exit(1);
|
|
3030
3338
|
}
|
|
3031
|
-
await promptRepoConnection();
|
|
3339
|
+
await promptRepoConnection({ linkRepo: opts.linkRepo });
|
|
3032
3340
|
const agents = detectAgents();
|
|
3033
3341
|
if (agents.length === 0) {
|
|
3034
3342
|
console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
@@ -3052,9 +3360,9 @@ async function installCommand(opts = {}) {
|
|
|
3052
3360
|
`);
|
|
3053
3361
|
writeGraderDaemon();
|
|
3054
3362
|
for (const mode of ["edit", "bash"]) {
|
|
3055
|
-
const pidFile =
|
|
3363
|
+
const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
|
|
3056
3364
|
try {
|
|
3057
|
-
const pid = parseInt(
|
|
3365
|
+
const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
|
|
3058
3366
|
if (pid > 0) {
|
|
3059
3367
|
process.kill(pid, "SIGTERM");
|
|
3060
3368
|
console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
|
|
@@ -3067,6 +3375,15 @@ async function installCommand(opts = {}) {
|
|
|
3067
3375
|
console.log(` ${GRADER_PRIMER_EDIT_PATH}`);
|
|
3068
3376
|
console.log(` ${GRADER_PRIMER_BASH_PATH}
|
|
3069
3377
|
`);
|
|
3378
|
+
let transcriptConsent = true;
|
|
3379
|
+
if (process.stdin.isTTY) {
|
|
3380
|
+
transcriptConsent = await promptTranscriptConsent();
|
|
3381
|
+
if (transcriptConsent) {
|
|
3382
|
+
console.log(" \u2713 Transcript collection enabled\n");
|
|
3383
|
+
} else {
|
|
3384
|
+
console.log(" \u2717 Transcript collection disabled \u2014 skipping transcript sync\n");
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3070
3387
|
let hasClaudeCode = false;
|
|
3071
3388
|
for (const agent of agents) {
|
|
3072
3389
|
if (agent.kind === "claude_code") {
|
|
@@ -3078,7 +3395,8 @@ async function installCommand(opts = {}) {
|
|
|
3078
3395
|
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
3079
3396
|
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
3080
3397
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
3081
|
-
transcriptSyncScriptPath: scripts.transcriptSyncScript
|
|
3398
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
3399
|
+
skipTranscriptSync: !transcriptConsent
|
|
3082
3400
|
});
|
|
3083
3401
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
3084
3402
|
}
|
|
@@ -3121,44 +3439,53 @@ async function installCommand(opts = {}) {
|
|
|
3121
3439
|
email = info.email;
|
|
3122
3440
|
} catch {
|
|
3123
3441
|
}
|
|
3124
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email });
|
|
3125
|
-
console.log(`Wrote config to ${
|
|
3442
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, transcriptConsent });
|
|
3443
|
+
console.log(`Wrote config to ${CONFIG_PATH2}
|
|
3126
3444
|
`);
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3445
|
+
if (transcriptConsent) {
|
|
3446
|
+
try {
|
|
3447
|
+
const repo = detectGitRepo2();
|
|
3448
|
+
if (repo) {
|
|
3449
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
3450
|
+
if (ingested > 0) {
|
|
3451
|
+
console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
|
|
3452
|
+
console.log(" This helps the safety judge understand your workflow.\n");
|
|
3453
|
+
}
|
|
3134
3454
|
}
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3455
|
+
} catch (err) {
|
|
3456
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3138
3457
|
`);
|
|
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
3458
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3459
|
+
try {
|
|
3460
|
+
const repo = detectGitRepo2();
|
|
3461
|
+
if (repo) {
|
|
3462
|
+
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
3463
|
+
if (result.messages > 0) {
|
|
3464
|
+
console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
|
|
3465
|
+
console.log(" This data will be used to suggest guardrail rules.\n");
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
console.warn(` \u26A0 Transcript sync skipped: ${err.message}
|
|
3151
3470
|
`);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
if (opts.prScan) {
|
|
3474
|
+
console.log();
|
|
3475
|
+
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
3476
|
+
await setupGithubCommand2({ nonInteractive: true });
|
|
3152
3477
|
}
|
|
3153
3478
|
console.log("\u2713 Synkro installed.");
|
|
3154
3479
|
console.log();
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3480
|
+
if (!opts.prScan) {
|
|
3481
|
+
console.log("Next steps:");
|
|
3482
|
+
console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
|
|
3483
|
+
console.log(" \u2022 synkro-cli status (check what is configured)");
|
|
3484
|
+
}
|
|
3158
3485
|
}
|
|
3159
3486
|
function detectGitRepo2() {
|
|
3160
3487
|
try {
|
|
3161
|
-
const remoteUrl =
|
|
3488
|
+
const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3162
3489
|
const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
|
|
3163
3490
|
return match ? match[1] : null;
|
|
3164
3491
|
} catch {
|
|
@@ -3168,17 +3495,17 @@ function detectGitRepo2() {
|
|
|
3168
3495
|
function getClaudeProjectsFolder() {
|
|
3169
3496
|
const cwd = process.cwd();
|
|
3170
3497
|
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
3171
|
-
const projectsDir =
|
|
3172
|
-
return
|
|
3498
|
+
const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
|
|
3499
|
+
return existsSync7(projectsDir) ? projectsDir : null;
|
|
3173
3500
|
}
|
|
3174
3501
|
function extractSessionInsights(projectsDir) {
|
|
3175
3502
|
const insights = [];
|
|
3176
3503
|
const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
3177
3504
|
for (const file of files) {
|
|
3178
3505
|
const sessionId = file.replace(".jsonl", "");
|
|
3179
|
-
const filePath =
|
|
3506
|
+
const filePath = join6(projectsDir, file);
|
|
3180
3507
|
try {
|
|
3181
|
-
const content =
|
|
3508
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3182
3509
|
const lines = content.split("\n").filter(Boolean);
|
|
3183
3510
|
for (let i = 0; i < lines.length; i++) {
|
|
3184
3511
|
try {
|
|
@@ -3254,7 +3581,7 @@ function extractTextContent(content) {
|
|
|
3254
3581
|
return "";
|
|
3255
3582
|
}
|
|
3256
3583
|
function parseTranscriptFile(filePath) {
|
|
3257
|
-
const content =
|
|
3584
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3258
3585
|
const lines = content.split("\n").filter(Boolean);
|
|
3259
3586
|
const messages = [];
|
|
3260
3587
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3305,7 +3632,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3305
3632
|
const sessions = [];
|
|
3306
3633
|
for (const file of batch) {
|
|
3307
3634
|
const sessionId = file.replace(".jsonl", "");
|
|
3308
|
-
const filePath =
|
|
3635
|
+
const filePath = join6(projectsDir, file);
|
|
3309
3636
|
try {
|
|
3310
3637
|
const allMessages = parseTranscriptFile(filePath);
|
|
3311
3638
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -3334,18 +3661,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3334
3661
|
}
|
|
3335
3662
|
for (const file of batch) {
|
|
3336
3663
|
const sessionId = file.replace(".jsonl", "");
|
|
3337
|
-
const filePath =
|
|
3664
|
+
const filePath = join6(projectsDir, file);
|
|
3338
3665
|
try {
|
|
3339
|
-
const content =
|
|
3666
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3340
3667
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
3341
|
-
writeFileSync5(
|
|
3668
|
+
writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
3342
3669
|
} catch {
|
|
3343
3670
|
}
|
|
3344
3671
|
}
|
|
3345
3672
|
}
|
|
3346
3673
|
return { sessions: totalSessions, messages: totalMessages };
|
|
3347
3674
|
}
|
|
3348
|
-
var
|
|
3675
|
+
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
|
|
3349
3676
|
var init_install = __esm({
|
|
3350
3677
|
"cli/commands/install.ts"() {
|
|
3351
3678
|
"use strict";
|
|
@@ -3356,14 +3683,14 @@ var init_install = __esm({
|
|
|
3356
3683
|
init_graderDaemon();
|
|
3357
3684
|
init_stub();
|
|
3358
3685
|
init_repoConnect();
|
|
3359
|
-
|
|
3360
|
-
HOOKS_DIR =
|
|
3361
|
-
BIN_DIR =
|
|
3362
|
-
|
|
3363
|
-
GRADER_DAEMON_PATH =
|
|
3364
|
-
GRADER_PRIMER_EDIT_PATH =
|
|
3365
|
-
GRADER_PRIMER_BASH_PATH =
|
|
3366
|
-
OFFSETS_DIR =
|
|
3686
|
+
SYNKRO_DIR2 = join6(homedir5(), ".synkro");
|
|
3687
|
+
HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
|
|
3688
|
+
BIN_DIR = join6(SYNKRO_DIR2, "bin");
|
|
3689
|
+
CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
|
|
3690
|
+
GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
|
|
3691
|
+
GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
|
|
3692
|
+
GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
|
|
3693
|
+
OFFSETS_DIR = join6(SYNKRO_DIR2, ".transcript-offsets");
|
|
3367
3694
|
}
|
|
3368
3695
|
});
|
|
3369
3696
|
|
|
@@ -3439,13 +3766,13 @@ var status_exports = {};
|
|
|
3439
3766
|
__export(status_exports, {
|
|
3440
3767
|
statusCommand: () => statusCommand
|
|
3441
3768
|
});
|
|
3442
|
-
import { existsSync as
|
|
3443
|
-
import { homedir as
|
|
3444
|
-
import { join as
|
|
3769
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
3770
|
+
import { homedir as homedir6 } from "os";
|
|
3771
|
+
import { join as join7 } from "path";
|
|
3445
3772
|
function readConfigEnv() {
|
|
3446
|
-
if (!
|
|
3773
|
+
if (!existsSync8(CONFIG_PATH3)) return {};
|
|
3447
3774
|
const out = {};
|
|
3448
|
-
const raw =
|
|
3775
|
+
const raw = readFileSync6(CONFIG_PATH3, "utf-8");
|
|
3449
3776
|
for (const line of raw.split("\n")) {
|
|
3450
3777
|
const trimmed = line.trim();
|
|
3451
3778
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -3476,10 +3803,10 @@ function statusCommand() {
|
|
|
3476
3803
|
console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
|
|
3477
3804
|
const info2 = getUserInfo();
|
|
3478
3805
|
const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
|
|
3479
|
-
const tierCacheFile =
|
|
3806
|
+
const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
|
|
3480
3807
|
let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
|
|
3481
|
-
if (!inferenceTier &&
|
|
3482
|
-
inferenceTier =
|
|
3808
|
+
if (!inferenceTier && existsSync8(tierCacheFile)) {
|
|
3809
|
+
inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
|
|
3483
3810
|
}
|
|
3484
3811
|
const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
|
|
3485
3812
|
console.log(` inference: ${tierLabel}`);
|
|
@@ -3506,19 +3833,19 @@ function statusCommand() {
|
|
|
3506
3833
|
}
|
|
3507
3834
|
}
|
|
3508
3835
|
console.log();
|
|
3509
|
-
const bashScript =
|
|
3510
|
-
const bashFollowupScript =
|
|
3511
|
-
const editPrecheckScript =
|
|
3512
|
-
const editCaptureScript =
|
|
3513
|
-
const stopSummaryScript =
|
|
3514
|
-
const sessionStartScript =
|
|
3836
|
+
const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
|
|
3837
|
+
const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
|
|
3838
|
+
const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
|
|
3839
|
+
const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
|
|
3840
|
+
const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
|
|
3841
|
+
const sessionStartScript = join7(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
|
|
3515
3842
|
console.log("Hook scripts:");
|
|
3516
|
-
console.log(` ${
|
|
3517
|
-
console.log(` ${
|
|
3518
|
-
console.log(` ${
|
|
3519
|
-
console.log(` ${
|
|
3520
|
-
console.log(` ${
|
|
3521
|
-
console.log(` ${
|
|
3843
|
+
console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
|
|
3844
|
+
console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
|
|
3845
|
+
console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
|
|
3846
|
+
console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
|
|
3847
|
+
console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
|
|
3848
|
+
console.log(` ${existsSync8(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
|
|
3522
3849
|
console.log();
|
|
3523
3850
|
const mcp = inspectMcpConfig();
|
|
3524
3851
|
console.log("Guardrails MCP server (Claude Code):");
|
|
@@ -3530,7 +3857,7 @@ function statusCommand() {
|
|
|
3530
3857
|
console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
|
|
3531
3858
|
}
|
|
3532
3859
|
}
|
|
3533
|
-
var
|
|
3860
|
+
var SYNKRO_DIR3, CONFIG_PATH3;
|
|
3534
3861
|
var init_status = __esm({
|
|
3535
3862
|
"cli/commands/status.ts"() {
|
|
3536
3863
|
"use strict";
|
|
@@ -3538,8 +3865,8 @@ var init_status = __esm({
|
|
|
3538
3865
|
init_agentDetect();
|
|
3539
3866
|
init_ccHookConfig();
|
|
3540
3867
|
init_mcpConfig();
|
|
3541
|
-
|
|
3542
|
-
|
|
3868
|
+
SYNKRO_DIR3 = join7(homedir6(), ".synkro");
|
|
3869
|
+
CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
|
|
3543
3870
|
}
|
|
3544
3871
|
});
|
|
3545
3872
|
|
|
@@ -3569,7 +3896,7 @@ var unlink_exports = {};
|
|
|
3569
3896
|
__export(unlink_exports, {
|
|
3570
3897
|
unlinkCommand: () => unlinkCommand
|
|
3571
3898
|
});
|
|
3572
|
-
import { createInterface as
|
|
3899
|
+
import { createInterface as createInterface4 } from "readline";
|
|
3573
3900
|
function ask2(rl, question) {
|
|
3574
3901
|
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
3575
3902
|
}
|
|
@@ -3597,7 +3924,7 @@ async function unlinkCommand() {
|
|
|
3597
3924
|
console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
|
|
3598
3925
|
});
|
|
3599
3926
|
console.log();
|
|
3600
|
-
const rl =
|
|
3927
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
3601
3928
|
try {
|
|
3602
3929
|
const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
|
|
3603
3930
|
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
|
|
@@ -3623,200 +3950,12 @@ var init_unlink = __esm({
|
|
|
3623
3950
|
}
|
|
3624
3951
|
});
|
|
3625
3952
|
|
|
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
3953
|
// cli/commands/scanPr.ts
|
|
3815
3954
|
var scanPr_exports = {};
|
|
3816
3955
|
__export(scanPr_exports, {
|
|
3817
3956
|
scanPrCommand: () => scanPrCommand
|
|
3818
3957
|
});
|
|
3819
|
-
import { execSync as
|
|
3958
|
+
import { execSync as execSync5, spawn } from "child_process";
|
|
3820
3959
|
function parseMatchSpec(condition) {
|
|
3821
3960
|
if (!condition.startsWith("match_spec:")) return null;
|
|
3822
3961
|
try {
|
|
@@ -3922,7 +4061,7 @@ function shouldSkipFile(filename) {
|
|
|
3922
4061
|
return SKIP_FILE_PATTERNS.some((p) => p.test(filename));
|
|
3923
4062
|
}
|
|
3924
4063
|
function ghJson(args2) {
|
|
3925
|
-
const out =
|
|
4064
|
+
const out = execSync5(`gh ${args2.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`, {
|
|
3926
4065
|
encoding: "utf-8",
|
|
3927
4066
|
maxBuffer: 16 * 1024 * 1024
|
|
3928
4067
|
});
|
|
@@ -3978,9 +4117,9 @@ ${hunks}`;
|
|
|
3978
4117
|
const t0 = Date.now();
|
|
3979
4118
|
const proc = spawn(
|
|
3980
4119
|
"claude",
|
|
3981
|
-
["--print", "--model", "claude-sonnet-4-6", "--output-format", "json", "--no-session-persistence"
|
|
4120
|
+
["--print", "--model", "claude-sonnet-4-6", "--output-format", "json", "--no-session-persistence"],
|
|
3982
4121
|
{
|
|
3983
|
-
stdio: ["
|
|
4122
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3984
4123
|
env: {
|
|
3985
4124
|
...process.env,
|
|
3986
4125
|
CLAUDE_CODE_OAUTH_TOKEN: claudeToken
|
|
@@ -3988,6 +4127,8 @@ ${hunks}`;
|
|
|
3988
4127
|
timeout: 12e4
|
|
3989
4128
|
}
|
|
3990
4129
|
);
|
|
4130
|
+
proc.stdin.write(fullPrompt);
|
|
4131
|
+
proc.stdin.end();
|
|
3991
4132
|
let stdout = "";
|
|
3992
4133
|
let stderr = "";
|
|
3993
4134
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -4036,19 +4177,174 @@ async function processInBatches(items, batchSize, fn) {
|
|
|
4036
4177
|
}
|
|
4037
4178
|
return results;
|
|
4038
4179
|
}
|
|
4039
|
-
function
|
|
4040
|
-
|
|
4180
|
+
function buildConsolidationPrompt(findings) {
|
|
4181
|
+
return `You are a senior security reviewer writing the final PR review. You received raw findings from an automated detector. Your job:
|
|
4041
4182
|
|
|
4042
|
-
|
|
4183
|
+
1. VERIFY \u2014 remove false positives or findings that are clearly wrong.
|
|
4184
|
+
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.
|
|
4185
|
+
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
4186
|
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4187
|
+
Output ONLY a JSON object (no prose, no markdown fences):
|
|
4188
|
+
{
|
|
4189
|
+
"summary": "<2-3 sentence overview for the review body \u2014 total issues, severity, what to do>",
|
|
4190
|
+
"comments": [
|
|
4191
|
+
{
|
|
4192
|
+
"path": "<file path>",
|
|
4193
|
+
"line": <integer, first affected line number \u2014 REQUIRED, never null>,
|
|
4194
|
+
"body": "<markdown comment \u2014 include affected lines if consolidated, e.g. 'Lines 1-8: ...'>"
|
|
4195
|
+
}
|
|
4196
|
+
]
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
Rules:
|
|
4200
|
+
- Each comment body should start with a severity emoji+label: \u{1F534} critical, \u{1F7E0} high, \u{1F7E1} medium, \u{1F535} low
|
|
4201
|
+
- If consolidating, mention all affected line numbers in the body
|
|
4202
|
+
- Keep comments short \u2014 developers read these in a PR, not a report
|
|
4203
|
+
- Maximum 15 comments. If more, pick the most critical and mention the rest in the summary.
|
|
4204
|
+
- If all findings are legitimate and distinct, keep them all (up to 15)
|
|
4205
|
+
- If you determine ALL findings are false positives, return {"summary": "No issues found after verification.", "comments": []}
|
|
4206
|
+
|
|
4207
|
+
Raw findings from detector:
|
|
4208
|
+
${JSON.stringify(findings, null, 2)}
|
|
4209
|
+
`;
|
|
4210
|
+
}
|
|
4211
|
+
function spawnOpusConsolidator(findings, claudeToken) {
|
|
4212
|
+
return new Promise((resolve2) => {
|
|
4213
|
+
const prompt2 = buildConsolidationPrompt(findings);
|
|
4214
|
+
const proc = spawn(
|
|
4215
|
+
"claude",
|
|
4216
|
+
["--print", "--model", "claude-opus-4-7", "--output-format", "json", "--no-session-persistence"],
|
|
4217
|
+
{
|
|
4218
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4219
|
+
env: {
|
|
4220
|
+
...process.env,
|
|
4221
|
+
CLAUDE_CODE_OAUTH_TOKEN: claudeToken
|
|
4222
|
+
},
|
|
4223
|
+
timeout: 12e4
|
|
4224
|
+
}
|
|
4049
4225
|
);
|
|
4050
|
-
|
|
4051
|
-
|
|
4226
|
+
proc.stdin.write(prompt2);
|
|
4227
|
+
proc.stdin.end();
|
|
4228
|
+
let stdout = "";
|
|
4229
|
+
let stderr = "";
|
|
4230
|
+
proc.stdout.on("data", (chunk) => {
|
|
4231
|
+
stdout += chunk.toString();
|
|
4232
|
+
});
|
|
4233
|
+
proc.stderr.on("data", (chunk) => {
|
|
4234
|
+
stderr += chunk.toString();
|
|
4235
|
+
});
|
|
4236
|
+
proc.on("close", (code) => {
|
|
4237
|
+
if (code !== 0) {
|
|
4238
|
+
console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
|
|
4239
|
+
resolve2(fallbackReview(findings));
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
try {
|
|
4243
|
+
const wrapper = JSON.parse(stdout);
|
|
4244
|
+
const responseText = (wrapper.result || wrapper.response || wrapper.text || "").trim();
|
|
4245
|
+
let txt = responseText;
|
|
4246
|
+
if (txt.startsWith("```")) {
|
|
4247
|
+
txt = txt.replace(/^```(?:json)?\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
4248
|
+
}
|
|
4249
|
+
const review = JSON.parse(txt);
|
|
4250
|
+
const comments = (review.comments || []).map((c) => ({
|
|
4251
|
+
path: c.path,
|
|
4252
|
+
line: c.line ?? 1,
|
|
4253
|
+
side: "RIGHT",
|
|
4254
|
+
body: c.body
|
|
4255
|
+
}));
|
|
4256
|
+
const maxSeverity = findings.reduce((max, f) => {
|
|
4257
|
+
const order = ["low", "medium", "high", "critical"];
|
|
4258
|
+
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
4259
|
+
}, "low");
|
|
4260
|
+
resolve2({ summary: review.summary || "", comments, severity: maxSeverity });
|
|
4261
|
+
} catch {
|
|
4262
|
+
console.warn(` failed to parse opus response, using fallback`);
|
|
4263
|
+
resolve2(fallbackReview(findings));
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
function fallbackReview(findings) {
|
|
4269
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4270
|
+
for (const f of findings) {
|
|
4271
|
+
const key = `${f.file}::${f.category}`;
|
|
4272
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
4273
|
+
grouped.get(key).push(f);
|
|
4274
|
+
}
|
|
4275
|
+
const comments = [];
|
|
4276
|
+
for (const [, group] of grouped) {
|
|
4277
|
+
const first = group[0];
|
|
4278
|
+
const lines = group.map((f) => f.line);
|
|
4279
|
+
const linesStr = lines.length > 1 ? `Lines ${lines.join(", ")}` : `Line ${lines[0]}`;
|
|
4280
|
+
const severityEmoji = first.severity === "critical" ? "\u{1F534}" : first.severity === "high" ? "\u{1F7E0}" : first.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
4281
|
+
comments.push({
|
|
4282
|
+
path: first.file,
|
|
4283
|
+
line: first.line,
|
|
4284
|
+
side: "RIGHT",
|
|
4285
|
+
body: `${severityEmoji} **${first.severity}: ${first.category}**
|
|
4286
|
+
|
|
4287
|
+
${linesStr}: ${first.description}
|
|
4288
|
+
|
|
4289
|
+
**Fix:** ${first.fix}`
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
const maxSeverity = findings.reduce((max, f) => {
|
|
4293
|
+
const order = ["low", "medium", "high", "critical"];
|
|
4294
|
+
return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
|
|
4295
|
+
}, "low");
|
|
4296
|
+
return {
|
|
4297
|
+
summary: `${findings.length} security finding(s) detected.`,
|
|
4298
|
+
comments: comments.slice(0, 15),
|
|
4299
|
+
severity: maxSeverity
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
function postPrReview(repo, prNumber, sha, review) {
|
|
4303
|
+
const preferredEvent = review.severity === "critical" || review.severity === "high" ? "REQUEST_CHANGES" : "COMMENT";
|
|
4304
|
+
function tryPost(event) {
|
|
4305
|
+
const body = JSON.stringify({
|
|
4306
|
+
commit_id: sha,
|
|
4307
|
+
body: `## \u{1F512} Synkro Security Review
|
|
4308
|
+
|
|
4309
|
+
${review.summary}`,
|
|
4310
|
+
event,
|
|
4311
|
+
comments: review.comments
|
|
4312
|
+
});
|
|
4313
|
+
try {
|
|
4314
|
+
execSync5(`gh api -X POST /repos/${repo}/pulls/${prNumber}/reviews --input -`, {
|
|
4315
|
+
encoding: "utf-8",
|
|
4316
|
+
input: body,
|
|
4317
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4318
|
+
});
|
|
4319
|
+
console.log(` \u2713 Posted PR review (${event}).`);
|
|
4320
|
+
return true;
|
|
4321
|
+
} catch (err) {
|
|
4322
|
+
const stderr = err.stderr?.toString() || "";
|
|
4323
|
+
const stdout = err.stdout?.toString() || "";
|
|
4324
|
+
const combined = stderr + stdout;
|
|
4325
|
+
if (combined.includes("own pull request") && event === "REQUEST_CHANGES") {
|
|
4326
|
+
return false;
|
|
4327
|
+
}
|
|
4328
|
+
console.warn(`Failed to post review: ${(stderr || stdout || err.message).slice(0, 200)}`);
|
|
4329
|
+
return false;
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (tryPost(preferredEvent)) return;
|
|
4333
|
+
if (preferredEvent === "REQUEST_CHANGES" && tryPost("COMMENT")) return;
|
|
4334
|
+
try {
|
|
4335
|
+
const fallbackBody = `## \u{1F512} Synkro Security Review
|
|
4336
|
+
|
|
4337
|
+
${review.summary}
|
|
4338
|
+
|
|
4339
|
+
` + review.comments.map((c) => `**${c.path}:${c.line}** \u2014 ${c.body}`).join("\n\n");
|
|
4340
|
+
execSync5(`gh api -X POST /repos/${repo}/issues/${prNumber}/comments --input -`, {
|
|
4341
|
+
encoding: "utf-8",
|
|
4342
|
+
input: JSON.stringify({ body: fallbackBody }),
|
|
4343
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
4344
|
+
});
|
|
4345
|
+
console.log(" \u2713 Posted fallback issue comment.");
|
|
4346
|
+
} catch (err2) {
|
|
4347
|
+
console.warn("Failed to post fallback comment:", err2.message);
|
|
4052
4348
|
}
|
|
4053
4349
|
}
|
|
4054
4350
|
function postCheckRun(repo, sha, conclusion, findings) {
|
|
@@ -4065,7 +4361,7 @@ function postCheckRun(repo, sha, conclusion, findings) {
|
|
|
4065
4361
|
}
|
|
4066
4362
|
});
|
|
4067
4363
|
try {
|
|
4068
|
-
|
|
4364
|
+
execSync5(`gh api -X POST /repos/${repo}/check-runs --input -`, {
|
|
4069
4365
|
encoding: "utf-8",
|
|
4070
4366
|
input: body,
|
|
4071
4367
|
stdio: ["pipe", "ignore", "pipe"]
|
|
@@ -4173,8 +4469,13 @@ async function scanPrCommand() {
|
|
|
4173
4469
|
console.log(`
|
|
4174
4470
|
Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${totalLatencyMs}ms
|
|
4175
4471
|
`);
|
|
4176
|
-
|
|
4177
|
-
|
|
4472
|
+
if (allFindings.length > 0) {
|
|
4473
|
+
console.log("Consolidating findings with Opus 4.7...");
|
|
4474
|
+
const review = await spawnOpusConsolidator(allFindings, claudeToken);
|
|
4475
|
+
console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
|
|
4476
|
+
if (review.comments.length > 0) {
|
|
4477
|
+
postPrReview(repo, prNumber, sha, review);
|
|
4478
|
+
}
|
|
4178
4479
|
}
|
|
4179
4480
|
const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
|
|
4180
4481
|
postCheckRun(repo, sha, conclusion, allFindings);
|
|
@@ -4336,8 +4637,8 @@ for (const envPath of envCandidates) {
|
|
|
4336
4637
|
const eqIndex = trimmed.indexOf("=");
|
|
4337
4638
|
if (eqIndex <= 0) continue;
|
|
4338
4639
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
4339
|
-
const value = trimmed.slice(eqIndex + 1).trim();
|
|
4340
|
-
if (!process.env[key]) process.env[key] = value;
|
|
4640
|
+
const value = trimmed.slice(eqIndex + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
4641
|
+
if (!process.env[key] && !value.startsWith("op://")) process.env[key] = value;
|
|
4341
4642
|
}
|
|
4342
4643
|
}
|
|
4343
4644
|
var args = process.argv.slice(2);
|
|
@@ -4358,7 +4659,6 @@ Commands:
|
|
|
4358
4659
|
link Link repos to a Synkro project (local git or GitHub OAuth)
|
|
4359
4660
|
unlink Remove repo links from Synkro projects
|
|
4360
4661
|
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
4662
|
update Refresh hook configs and judge prompts
|
|
4363
4663
|
disconnect [--purge] Remove Synkro hooks from agents (--purge also removes ~/.synkro)
|
|
4364
4664
|
uninstall Fully remove Synkro from this machine
|
|
@@ -4407,7 +4707,13 @@ async function main() {
|
|
|
4407
4707
|
}
|
|
4408
4708
|
case "setup-github": {
|
|
4409
4709
|
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
4410
|
-
|
|
4710
|
+
const ghOpts = {};
|
|
4711
|
+
for (const a of subArgs) {
|
|
4712
|
+
if (a === "--non-interactive") ghOpts.nonInteractive = true;
|
|
4713
|
+
else if (a === "--skip-claude-token") ghOpts.skipClaudeToken = true;
|
|
4714
|
+
else if (a.startsWith("--github-token=")) ghOpts.githubToken = a.slice("--github-token=".length);
|
|
4715
|
+
}
|
|
4716
|
+
await setupGithubCommand2(ghOpts);
|
|
4411
4717
|
break;
|
|
4412
4718
|
}
|
|
4413
4719
|
case "scan-pr": {
|