@synkro-sh/cli 1.3.7 → 1.3.9
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 +476 -284
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -2606,7 +2606,9 @@ async function listAccessibleRepos(opts) {
|
|
|
2606
2606
|
}
|
|
2607
2607
|
async function pushSecretsToRepo(opts, owner, repo, secrets) {
|
|
2608
2608
|
const pubkey = await getRepoPublicKey(opts, owner, repo);
|
|
2609
|
-
|
|
2609
|
+
if (secrets.claudeCodeOauthToken) {
|
|
2610
|
+
await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
|
|
2611
|
+
}
|
|
2610
2612
|
await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
|
|
2611
2613
|
}
|
|
2612
2614
|
function writeWorkflowFile(repoRootPath) {
|
|
@@ -2750,8 +2752,30 @@ async function connectGithubAndSelectRepos() {
|
|
|
2750
2752
|
rl.close();
|
|
2751
2753
|
}
|
|
2752
2754
|
}
|
|
2753
|
-
async function promptRepoConnection() {
|
|
2755
|
+
async function promptRepoConnection(opts) {
|
|
2754
2756
|
const localRepo = detectGitRepo();
|
|
2757
|
+
if (opts?.linkRepo !== void 0) {
|
|
2758
|
+
if (opts.linkRepo && localRepo) {
|
|
2759
|
+
try {
|
|
2760
|
+
const existing = await listProjects();
|
|
2761
|
+
const alreadyLinked = existing.some(
|
|
2762
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
2763
|
+
);
|
|
2764
|
+
if (!alreadyLinked) {
|
|
2765
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
2766
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
2767
|
+
} else {
|
|
2768
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
2769
|
+
}
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
2772
|
+
}
|
|
2773
|
+
} else if (opts.linkRepo) {
|
|
2774
|
+
console.warn(" \u26A0 --link-repo specified but not in a git repo. Skipping.");
|
|
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,227 @@ 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 { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
2864
|
+
import { homedir as homedir4 } from "os";
|
|
2865
|
+
import { join as join5 } from "path";
|
|
2866
|
+
function readConfig() {
|
|
2867
|
+
if (!existsSync6(CONFIG_PATH)) return {};
|
|
2868
|
+
const out = {};
|
|
2869
|
+
for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
|
|
2870
|
+
const t = line.trim();
|
|
2871
|
+
if (!t || t.startsWith("#")) continue;
|
|
2872
|
+
const eq = t.indexOf("=");
|
|
2873
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
|
2874
|
+
}
|
|
2875
|
+
return out;
|
|
2876
|
+
}
|
|
2877
|
+
async function prompt(rl, q, opts = {}) {
|
|
2878
|
+
if (opts.silent) {
|
|
2879
|
+
process.stdout.write(q);
|
|
2880
|
+
const wasRaw = process.stdin.isRaw;
|
|
2881
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
2882
|
+
return await new Promise((resolve2) => {
|
|
2883
|
+
let chunk = "";
|
|
2884
|
+
const onData = (data) => {
|
|
2885
|
+
const s = data.toString("utf-8");
|
|
2886
|
+
if (s === "\r" || s === "\n" || s === "\r\n") {
|
|
2887
|
+
process.stdin.removeListener("data", onData);
|
|
2888
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
|
|
2889
|
+
process.stdout.write("\n");
|
|
2890
|
+
resolve2(chunk);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (s === "") {
|
|
2894
|
+
process.exit(130);
|
|
2895
|
+
}
|
|
2896
|
+
if (s === "\x7F" || s === "\b") {
|
|
2897
|
+
chunk = chunk.slice(0, -1);
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
chunk += s;
|
|
2901
|
+
};
|
|
2902
|
+
process.stdin.on("data", onData);
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
return await rl.question(q);
|
|
2906
|
+
}
|
|
2907
|
+
async function setupGithubCommand(opts = {}) {
|
|
2908
|
+
if (!isAuthenticated()) {
|
|
2909
|
+
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
2910
|
+
process.exit(1);
|
|
2911
|
+
}
|
|
2912
|
+
const config2 = readConfig();
|
|
2913
|
+
const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
2914
|
+
const jwt2 = getAccessToken();
|
|
2915
|
+
if (!jwt2) {
|
|
2916
|
+
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
|
|
2917
|
+
process.exit(1);
|
|
2918
|
+
}
|
|
2919
|
+
console.log("Requesting CI API key from Synkro...");
|
|
2920
|
+
let synkroCiApiKey;
|
|
2921
|
+
try {
|
|
2922
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
|
|
2923
|
+
method: "POST",
|
|
2924
|
+
headers: {
|
|
2925
|
+
"Authorization": `Bearer ${jwt2}`,
|
|
2926
|
+
"Content-Type": "application/json"
|
|
2927
|
+
},
|
|
2928
|
+
body: "{}"
|
|
2929
|
+
});
|
|
2930
|
+
if (!resp.ok) {
|
|
2931
|
+
const errText = await resp.text().catch(() => "");
|
|
2932
|
+
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
2933
|
+
process.exit(1);
|
|
2934
|
+
}
|
|
2935
|
+
const minted = await resp.json();
|
|
2936
|
+
synkroCiApiKey = minted.api_key;
|
|
2937
|
+
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
2940
|
+
process.exit(1);
|
|
2941
|
+
}
|
|
2942
|
+
const rl = createInterface2({ input, output });
|
|
2943
|
+
let ghToken = opts.githubToken || "";
|
|
2944
|
+
if (!ghToken) {
|
|
2945
|
+
console.log("Synkro PR scan setup\n");
|
|
2946
|
+
console.log("Requirements:");
|
|
2947
|
+
console.log(" \u2022 A GitHub personal access token with `repo` scope");
|
|
2948
|
+
console.log(" (create at https://github.com/settings/tokens?type=beta)\n");
|
|
2949
|
+
ghToken = (await prompt(rl, "GitHub token (paste): ", { silent: true })).trim();
|
|
2950
|
+
}
|
|
2951
|
+
if (!ghToken || !ghToken.startsWith("ghp_") && !ghToken.startsWith("github_pat_")) {
|
|
2952
|
+
console.error("Invalid GitHub token format. Expected ghp_... or github_pat_...");
|
|
2953
|
+
rl.close();
|
|
2954
|
+
process.exit(1);
|
|
2955
|
+
}
|
|
2956
|
+
let claudeToken = opts.claudeOauthToken || "";
|
|
2957
|
+
if (!claudeToken && !opts.skipClaudeToken) {
|
|
2958
|
+
try {
|
|
2959
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
2960
|
+
console.log("\nGenerating Claude Code OAuth token \u2014 complete the browser auth...");
|
|
2961
|
+
const tokenOutput = execSync5("claude setup-token", {
|
|
2962
|
+
encoding: "utf-8",
|
|
2963
|
+
timeout: 12e4,
|
|
2964
|
+
stdio: ["inherit", "pipe", "inherit"]
|
|
2965
|
+
}).trim();
|
|
2966
|
+
const tokenLine = tokenOutput.split("\n").find((l) => l.includes("sk-ant-oat01-"));
|
|
2967
|
+
claudeToken = tokenLine?.match(/(sk-ant-oat01-[A-Za-z0-9_-]+)/)?.[1] || "";
|
|
2968
|
+
if (claudeToken) {
|
|
2969
|
+
console.log(" \u2713 Captured Claude Code OAuth token.\n");
|
|
2970
|
+
} else {
|
|
2971
|
+
console.warn(" \u26A0 Could not capture token. PR scans will use BYOK inference only.\n");
|
|
2972
|
+
}
|
|
2973
|
+
} catch {
|
|
2974
|
+
console.warn(" \u26A0 claude setup-token failed. PR scans will use BYOK inference only.\n");
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
console.log("\nFetching accessible repos...");
|
|
2978
|
+
const repos = await listAccessibleRepos({ token: ghToken });
|
|
2979
|
+
if (repos.length === 0) {
|
|
2980
|
+
console.error("No accessible repos found. Verify the GitHub token has `repo` scope.");
|
|
2981
|
+
rl.close();
|
|
2982
|
+
process.exit(1);
|
|
2983
|
+
}
|
|
2984
|
+
console.log(`
|
|
2985
|
+
Found ${repos.length} accessible repo(s):
|
|
2986
|
+
`);
|
|
2987
|
+
repos.slice(0, 100).forEach((r, i) => {
|
|
2988
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
|
|
2989
|
+
});
|
|
2990
|
+
console.log();
|
|
2991
|
+
const selectionRaw = await prompt(rl, "Select repos to enable (comma-separated numbers, e.g. 1,3,5): ");
|
|
2992
|
+
const selectedIdx = selectionRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
|
|
2993
|
+
if (selectedIdx.length === 0) {
|
|
2994
|
+
console.error("No valid selections.");
|
|
2995
|
+
rl.close();
|
|
2996
|
+
process.exit(1);
|
|
2997
|
+
}
|
|
2998
|
+
const selected = selectedIdx.map((i) => repos[i]);
|
|
2999
|
+
console.log(`
|
|
3000
|
+
Will push secrets to ${selected.length} repo(s):`);
|
|
3001
|
+
for (const r of selected) console.log(` \u2022 ${r.full_name}`);
|
|
3002
|
+
console.log();
|
|
3003
|
+
const confirm = (await prompt(rl, "Continue? (yes/no): ")).trim().toLowerCase();
|
|
3004
|
+
if (confirm !== "yes" && confirm !== "y") {
|
|
3005
|
+
console.log("Cancelled.");
|
|
3006
|
+
rl.close();
|
|
3007
|
+
process.exit(0);
|
|
3008
|
+
}
|
|
3009
|
+
rl.close();
|
|
3010
|
+
console.log();
|
|
3011
|
+
for (const r of selected) {
|
|
3012
|
+
process.stdout.write(`Pushing secrets to ${r.full_name}... `);
|
|
3013
|
+
try {
|
|
3014
|
+
await pushSecretsToRepo(
|
|
3015
|
+
{ token: ghToken },
|
|
3016
|
+
r.owner,
|
|
3017
|
+
r.repo,
|
|
3018
|
+
{
|
|
3019
|
+
claudeCodeOauthToken: claudeToken,
|
|
3020
|
+
synkroApiKey: synkroCiApiKey
|
|
3021
|
+
}
|
|
3022
|
+
);
|
|
3023
|
+
console.log("\u2713");
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
console.log(`\u2717 (${err.message})`);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
console.log();
|
|
3029
|
+
const gitRoot = findGitRoot(process.cwd());
|
|
3030
|
+
if (gitRoot) {
|
|
3031
|
+
const written = writeWorkflowFile(gitRoot);
|
|
3032
|
+
if (written) {
|
|
3033
|
+
console.log(`Wrote workflow: ${written}`);
|
|
3034
|
+
console.log("Commit and push it to enable PR scanning.");
|
|
3035
|
+
}
|
|
3036
|
+
} else {
|
|
3037
|
+
console.log("Not in a git repo. To enable scanning, add this file to your repo:");
|
|
3038
|
+
console.log(` Path: ${WORKFLOW_RELATIVE_PATH}`);
|
|
3039
|
+
console.log(` Content: run \`synkro-cli setup-github\` from inside a repo to write it automatically`);
|
|
3040
|
+
}
|
|
3041
|
+
console.log();
|
|
3042
|
+
console.log("\u2713 PR scan setup complete.");
|
|
3043
|
+
console.log(`Secrets pushed: ${SECRET_NAMES.CLAUDE_OAUTH}, ${SECRET_NAMES.SYNKRO_API_KEY}`);
|
|
3044
|
+
console.log("Open a PR on any selected repo to trigger your first Synkro scan.");
|
|
3045
|
+
}
|
|
3046
|
+
var SYNKRO_DIR, CONFIG_PATH;
|
|
3047
|
+
var init_setupGithub = __esm({
|
|
3048
|
+
"cli/commands/setupGithub.ts"() {
|
|
3049
|
+
"use strict";
|
|
3050
|
+
init_githubSetup();
|
|
3051
|
+
init_stub();
|
|
3052
|
+
SYNKRO_DIR = join5(homedir4(), ".synkro");
|
|
3053
|
+
CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
|
|
2832
3057
|
// cli/commands/install.ts
|
|
2833
3058
|
var install_exports = {};
|
|
2834
3059
|
__export(install_exports, {
|
|
2835
3060
|
installCommand: () => installCommand,
|
|
2836
3061
|
parseArgs: () => parseArgs
|
|
2837
3062
|
});
|
|
2838
|
-
import { existsSync as
|
|
2839
|
-
import {
|
|
2840
|
-
import {
|
|
3063
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync5, readdirSync } from "fs";
|
|
3064
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
3065
|
+
import { stdin as stdinStream, stdout as stdoutStream } from "process";
|
|
3066
|
+
import { homedir as homedir5 } from "os";
|
|
3067
|
+
import { join as join6 } from "path";
|
|
2841
3068
|
import { execSync as execSync3 } from "child_process";
|
|
3069
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
3070
|
+
const rl = createInterface3({ input: stdinStream, output: stdoutStream });
|
|
3071
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
3072
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
3073
|
+
rl.close();
|
|
3074
|
+
if (answer === "") return defaultYes;
|
|
3075
|
+
return answer === "y" || answer === "yes";
|
|
3076
|
+
}
|
|
2842
3077
|
function sanitizeGatewayCandidate(raw) {
|
|
2843
3078
|
if (!raw) return void 0;
|
|
2844
3079
|
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
@@ -2851,6 +3086,17 @@ function parseArgs(argv) {
|
|
|
2851
3086
|
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
2852
3087
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
2853
3088
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
3089
|
+
else if (a === "--non-interactive") opts.nonInteractive = true;
|
|
3090
|
+
else if (a === "--link-repo") opts.linkRepo = true;
|
|
3091
|
+
else if (a === "--sync-transcripts") opts.syncTranscripts = true;
|
|
3092
|
+
else if (a === "--no-sync-transcripts") opts.syncTranscripts = false;
|
|
3093
|
+
else if (a === "--pr-scan=claude-oauth") opts.prScan = "claude-oauth";
|
|
3094
|
+
else if (a === "--pr-scan=byok") opts.prScan = "byok";
|
|
3095
|
+
else if (a === "--pr-scan=skip") opts.prScan = "skip";
|
|
3096
|
+
else if (a.startsWith("--pr-scan=")) opts.prScan = a.slice("--pr-scan=".length);
|
|
3097
|
+
else if (a.startsWith("--inference-key=")) opts.inferenceKey = a.slice("--inference-key=".length);
|
|
3098
|
+
else if (a.startsWith("--inference-model=")) opts.inferenceModel = a.slice("--inference-model=".length);
|
|
3099
|
+
else if (a.startsWith("--github-token=")) opts.githubToken = a.slice("--github-token=".length);
|
|
2854
3100
|
}
|
|
2855
3101
|
if (!opts.gatewayUrl) {
|
|
2856
3102
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
@@ -2859,7 +3105,7 @@ function parseArgs(argv) {
|
|
|
2859
3105
|
return opts;
|
|
2860
3106
|
}
|
|
2861
3107
|
function ensureSynkroDir() {
|
|
2862
|
-
mkdirSync5(
|
|
3108
|
+
mkdirSync5(SYNKRO_DIR2, { recursive: true });
|
|
2863
3109
|
mkdirSync5(HOOKS_DIR, { recursive: true });
|
|
2864
3110
|
mkdirSync5(BIN_DIR, { recursive: true });
|
|
2865
3111
|
mkdirSync5(OFFSETS_DIR, { recursive: true });
|
|
@@ -2873,13 +3119,13 @@ function writeGraderDaemon() {
|
|
|
2873
3119
|
chmodSync(GRADER_PRIMER_BASH_PATH, 420);
|
|
2874
3120
|
}
|
|
2875
3121
|
function writeHookScripts() {
|
|
2876
|
-
const bashScriptPath =
|
|
2877
|
-
const bashFollowupScriptPath =
|
|
2878
|
-
const editCaptureScriptPath =
|
|
2879
|
-
const editPrecheckScriptPath =
|
|
2880
|
-
const stopSummaryScriptPath =
|
|
2881
|
-
const sessionStartScriptPath =
|
|
2882
|
-
const transcriptSyncScriptPath =
|
|
3122
|
+
const bashScriptPath = join6(HOOKS_DIR, "cc-bash-judge.sh");
|
|
3123
|
+
const bashFollowupScriptPath = join6(HOOKS_DIR, "cc-bash-followup.sh");
|
|
3124
|
+
const editCaptureScriptPath = join6(HOOKS_DIR, "cc-edit-capture.sh");
|
|
3125
|
+
const editPrecheckScriptPath = join6(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
3126
|
+
const stopSummaryScriptPath = join6(HOOKS_DIR, "cc-stop-summary.sh");
|
|
3127
|
+
const sessionStartScriptPath = join6(HOOKS_DIR, "cc-session-start.sh");
|
|
3128
|
+
const transcriptSyncScriptPath = join6(HOOKS_DIR, "cc-transcript-sync.sh");
|
|
2883
3129
|
writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
|
|
2884
3130
|
writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
2885
3131
|
writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
@@ -2912,7 +3158,7 @@ function shellQuoteSingle(value) {
|
|
|
2912
3158
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2913
3159
|
}
|
|
2914
3160
|
function writeConfigEnv(opts) {
|
|
2915
|
-
const credsPath =
|
|
3161
|
+
const credsPath = join6(SYNKRO_DIR2, "credentials.json");
|
|
2916
3162
|
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
2917
3163
|
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
2918
3164
|
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
@@ -2925,14 +3171,14 @@ function writeConfigEnv(opts) {
|
|
|
2925
3171
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2926
3172
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2927
3173
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2928
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3174
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.9")}`
|
|
2929
3175
|
];
|
|
2930
3176
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2931
3177
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
2932
3178
|
if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
|
|
2933
3179
|
lines.push("");
|
|
2934
|
-
writeFileSync5(
|
|
2935
|
-
chmodSync(
|
|
3180
|
+
writeFileSync5(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
3181
|
+
chmodSync(CONFIG_PATH2, 384);
|
|
2936
3182
|
}
|
|
2937
3183
|
function assertGatewayAllowed(gatewayUrl) {
|
|
2938
3184
|
let parsed;
|
|
@@ -2957,19 +3203,19 @@ function assertGatewayAllowed(gatewayUrl) {
|
|
|
2957
3203
|
}
|
|
2958
3204
|
function isAlreadyInstalled() {
|
|
2959
3205
|
const requiredScripts = [
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3206
|
+
join6(HOOKS_DIR, "cc-bash-judge.sh"),
|
|
3207
|
+
join6(HOOKS_DIR, "cc-bash-followup.sh"),
|
|
3208
|
+
join6(HOOKS_DIR, "cc-edit-precheck.sh"),
|
|
3209
|
+
join6(HOOKS_DIR, "cc-edit-capture.sh"),
|
|
3210
|
+
join6(HOOKS_DIR, "cc-stop-summary.sh"),
|
|
3211
|
+
join6(HOOKS_DIR, "cc-session-start.sh")
|
|
2966
3212
|
];
|
|
2967
|
-
if (!requiredScripts.every((p) =>
|
|
2968
|
-
if (!
|
|
2969
|
-
const settingsPath =
|
|
2970
|
-
if (!
|
|
3213
|
+
if (!requiredScripts.every((p) => existsSync7(p))) return false;
|
|
3214
|
+
if (!existsSync7(CONFIG_PATH2)) return false;
|
|
3215
|
+
const settingsPath = join6(homedir5(), ".claude", "settings.json");
|
|
3216
|
+
if (!existsSync7(settingsPath)) return false;
|
|
2971
3217
|
try {
|
|
2972
|
-
const settings = JSON.parse(
|
|
3218
|
+
const settings = JSON.parse(readFileSync5(settingsPath, "utf-8"));
|
|
2973
3219
|
const hooks = settings?.hooks;
|
|
2974
3220
|
if (!hooks || typeof hooks !== "object") return false;
|
|
2975
3221
|
const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
|
|
@@ -3028,7 +3274,11 @@ async function installCommand(opts = {}) {
|
|
|
3028
3274
|
console.error("No access token available after auth.");
|
|
3029
3275
|
process.exit(1);
|
|
3030
3276
|
}
|
|
3031
|
-
|
|
3277
|
+
if (opts.nonInteractive) {
|
|
3278
|
+
await promptRepoConnection({ linkRepo: opts.linkRepo ?? true });
|
|
3279
|
+
} else {
|
|
3280
|
+
await promptRepoConnection();
|
|
3281
|
+
}
|
|
3032
3282
|
const agents = detectAgents();
|
|
3033
3283
|
if (agents.length === 0) {
|
|
3034
3284
|
console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
@@ -3052,9 +3302,9 @@ async function installCommand(opts = {}) {
|
|
|
3052
3302
|
`);
|
|
3053
3303
|
writeGraderDaemon();
|
|
3054
3304
|
for (const mode of ["edit", "bash"]) {
|
|
3055
|
-
const pidFile =
|
|
3305
|
+
const pidFile = join6(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
|
|
3056
3306
|
try {
|
|
3057
|
-
const pid = parseInt(
|
|
3307
|
+
const pid = parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
|
|
3058
3308
|
if (pid > 0) {
|
|
3059
3309
|
process.kill(pid, "SIGTERM");
|
|
3060
3310
|
console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
|
|
@@ -3122,38 +3372,158 @@ async function installCommand(opts = {}) {
|
|
|
3122
3372
|
} catch {
|
|
3123
3373
|
}
|
|
3124
3374
|
writeConfigEnv({ gatewayUrl, userId, orgId, email });
|
|
3125
|
-
console.log(`Wrote config to ${
|
|
3375
|
+
console.log(`Wrote config to ${CONFIG_PATH2}
|
|
3376
|
+
`);
|
|
3377
|
+
const repo = detectGitRepo2();
|
|
3378
|
+
if (repo && getClaudeProjectsFolder()) {
|
|
3379
|
+
const syncHistory = opts.nonInteractive ? opts.syncTranscripts ?? true : await promptYesNo("Sync Claude Code session history for this repo?");
|
|
3380
|
+
if (syncHistory) {
|
|
3381
|
+
try {
|
|
3382
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
3383
|
+
if (ingested > 0) {
|
|
3384
|
+
console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
|
|
3385
|
+
console.log(" This helps the safety judge understand your workflow.\n");
|
|
3386
|
+
}
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3126
3389
|
`);
|
|
3127
|
-
try {
|
|
3128
|
-
const repo = detectGitRepo2();
|
|
3129
|
-
if (repo) {
|
|
3130
|
-
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
3131
|
-
if (ingested > 0) {
|
|
3132
|
-
console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
|
|
3133
|
-
console.log(" This helps the safety judge understand your workflow.\n");
|
|
3134
3390
|
}
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3391
|
+
try {
|
|
3392
|
+
const result = await syncTranscriptsBulk(gatewayUrl, token, repo);
|
|
3393
|
+
if (result.messages > 0) {
|
|
3394
|
+
console.log(`Synced ${result.sessions} sessions (${result.messages} messages) from Claude Code history.`);
|
|
3395
|
+
console.log(" This data will be used to suggest guardrail rules.\n");
|
|
3396
|
+
}
|
|
3397
|
+
} catch (err) {
|
|
3398
|
+
console.warn(` \u26A0 Transcript sync skipped: ${err.message}
|
|
3138
3399
|
`);
|
|
3400
|
+
}
|
|
3401
|
+
} else {
|
|
3402
|
+
console.log(" Skipped session history sync.\n");
|
|
3403
|
+
}
|
|
3139
3404
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
if (
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3405
|
+
function detectProviderFromKey(key) {
|
|
3406
|
+
if (key.startsWith("sk-ant-")) return "anthropic";
|
|
3407
|
+
if (key.startsWith("sk-") || key.startsWith("sk-proj-")) return "openai";
|
|
3408
|
+
if (key.startsWith("AIza")) return "gemini";
|
|
3409
|
+
return null;
|
|
3410
|
+
}
|
|
3411
|
+
const PROVIDER_MODELS = {
|
|
3412
|
+
anthropic: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
|
|
3413
|
+
openai: ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"],
|
|
3414
|
+
gemini: ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"]
|
|
3415
|
+
};
|
|
3416
|
+
if (repo) {
|
|
3417
|
+
let ghConnected = false;
|
|
3418
|
+
let ghLogin = "";
|
|
3419
|
+
try {
|
|
3420
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/integrations/github/connect`, {
|
|
3421
|
+
headers: { "Authorization": `Bearer ${token}` }
|
|
3422
|
+
});
|
|
3423
|
+
if (resp.ok) {
|
|
3424
|
+
const data = await resp.json();
|
|
3425
|
+
ghConnected = data.connected;
|
|
3426
|
+
ghLogin = data.github_login || "";
|
|
3147
3427
|
}
|
|
3428
|
+
} catch {
|
|
3148
3429
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3430
|
+
if (ghConnected) {
|
|
3431
|
+
console.log(`GitHub is connected (${ghLogin || "linked via dashboard"}).`);
|
|
3432
|
+
}
|
|
3433
|
+
const prScanChoice = opts.nonInteractive ? opts.prScan ?? "skip" : null;
|
|
3434
|
+
const setupGh = prScanChoice !== null ? prScanChoice !== "skip" : await promptYesNo("Set up GitHub PR scanning?");
|
|
3435
|
+
if (setupGh) {
|
|
3436
|
+
let choice;
|
|
3437
|
+
if (prScanChoice) {
|
|
3438
|
+
choice = prScanChoice === "claude-oauth" ? "1" : prScanChoice === "byok" ? "2" : "3";
|
|
3439
|
+
} else {
|
|
3440
|
+
const rl = createInterface3({ input: stdinStream, output: stdoutStream });
|
|
3441
|
+
console.log("\nHow should PR scans authenticate for AI review?\n");
|
|
3442
|
+
console.log(" 1. Claude Code OAuth \u2014 opens browser, auto-captures token");
|
|
3443
|
+
console.log(" 2. Use your own API key \u2014 paste an Anthropic, OpenAI, or Gemini key");
|
|
3444
|
+
console.log(" 3. Skip for now\n");
|
|
3445
|
+
choice = (await rl.question("Choice [1/2/3]: ")).trim();
|
|
3446
|
+
rl.close();
|
|
3447
|
+
}
|
|
3448
|
+
if (choice === "1") {
|
|
3449
|
+
try {
|
|
3450
|
+
await setupGithubCommand({ githubToken: opts.githubToken });
|
|
3451
|
+
} catch (err) {
|
|
3452
|
+
console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
|
|
3453
|
+
console.warn(" Run `synkro-cli setup-github` to retry.\n");
|
|
3454
|
+
}
|
|
3455
|
+
} else if (choice === "2") {
|
|
3456
|
+
let apiKey = opts.inferenceKey || "";
|
|
3457
|
+
if (!apiKey) {
|
|
3458
|
+
const rl2 = createInterface3({ input: stdinStream, output: stdoutStream });
|
|
3459
|
+
apiKey = (await rl2.question("Paste your API key: ")).trim();
|
|
3460
|
+
rl2.close();
|
|
3461
|
+
}
|
|
3462
|
+
if (apiKey) {
|
|
3463
|
+
const provider = detectProviderFromKey(apiKey);
|
|
3464
|
+
if (provider) {
|
|
3465
|
+
let selectedModel = opts.inferenceModel || "";
|
|
3466
|
+
if (!selectedModel) {
|
|
3467
|
+
const models = PROVIDER_MODELS[provider];
|
|
3468
|
+
console.log(`
|
|
3469
|
+
Detected provider: ${provider}`);
|
|
3470
|
+
console.log(" Select a model for classification + grading:\n");
|
|
3471
|
+
models.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
|
|
3472
|
+
console.log();
|
|
3473
|
+
const rl3 = createInterface3({ input: stdinStream, output: stdoutStream });
|
|
3474
|
+
const modelChoice = (await rl3.question(` Model [1-${models.length}]: `)).trim();
|
|
3475
|
+
rl3.close();
|
|
3476
|
+
const modelIdx = parseInt(modelChoice, 10) - 1;
|
|
3477
|
+
selectedModel = modelIdx >= 0 && modelIdx < models.length ? models[modelIdx] : models[0];
|
|
3478
|
+
}
|
|
3479
|
+
console.log(` Using: ${selectedModel}`);
|
|
3480
|
+
try {
|
|
3481
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/settings/inference`, {
|
|
3482
|
+
method: "PUT",
|
|
3483
|
+
headers: {
|
|
3484
|
+
"Authorization": `Bearer ${token}`,
|
|
3485
|
+
"Content-Type": "application/json"
|
|
3486
|
+
},
|
|
3487
|
+
body: JSON.stringify({
|
|
3488
|
+
providers: { [provider]: { api_key: apiKey } },
|
|
3489
|
+
roles: {
|
|
3490
|
+
classification: { provider, model: selectedModel },
|
|
3491
|
+
grading: { provider, model: selectedModel }
|
|
3492
|
+
}
|
|
3493
|
+
})
|
|
3494
|
+
});
|
|
3495
|
+
if (resp.ok) {
|
|
3496
|
+
console.log(` \u2713 Saved ${provider} key and configured classification + grading with ${selectedModel}.
|
|
3497
|
+
`);
|
|
3498
|
+
} else {
|
|
3499
|
+
const err = await resp.text().catch(() => "");
|
|
3500
|
+
console.warn(` \u26A0 Failed to save key: ${err.slice(0, 200)}
|
|
3151
3501
|
`);
|
|
3502
|
+
}
|
|
3503
|
+
} catch (err) {
|
|
3504
|
+
console.warn(` \u26A0 Failed to save key: ${err.message}
|
|
3505
|
+
`);
|
|
3506
|
+
}
|
|
3507
|
+
} else {
|
|
3508
|
+
console.warn(" \u26A0 Could not detect provider from key format. Configure manually in the dashboard.\n");
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
try {
|
|
3512
|
+
await setupGithubCommand({ skipClaudeToken: true, githubToken: opts.githubToken });
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
console.warn(` \u26A0 GitHub setup failed: ${err.message}`);
|
|
3515
|
+
console.warn(" Run `synkro-cli setup-github` to retry.\n");
|
|
3516
|
+
}
|
|
3517
|
+
} else {
|
|
3518
|
+
console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
|
|
3519
|
+
}
|
|
3520
|
+
} else {
|
|
3521
|
+
console.log(" Skipped. Run `synkro-cli setup-github` anytime to enable PR scanning.\n");
|
|
3522
|
+
}
|
|
3152
3523
|
}
|
|
3153
3524
|
console.log("\u2713 Synkro installed.");
|
|
3154
3525
|
console.log();
|
|
3155
3526
|
console.log("Next steps:");
|
|
3156
|
-
console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
|
|
3157
3527
|
console.log(" \u2022 synkro-cli status (check what is configured)");
|
|
3158
3528
|
}
|
|
3159
3529
|
function detectGitRepo2() {
|
|
@@ -3168,17 +3538,17 @@ function detectGitRepo2() {
|
|
|
3168
3538
|
function getClaudeProjectsFolder() {
|
|
3169
3539
|
const cwd = process.cwd();
|
|
3170
3540
|
const sanitized = cwd.replace(/\//g, "-");
|
|
3171
|
-
const projectsDir =
|
|
3172
|
-
return
|
|
3541
|
+
const projectsDir = join6(homedir5(), ".claude", "projects", sanitized);
|
|
3542
|
+
return existsSync7(projectsDir) ? projectsDir : null;
|
|
3173
3543
|
}
|
|
3174
3544
|
function extractSessionInsights(projectsDir) {
|
|
3175
3545
|
const insights = [];
|
|
3176
3546
|
const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
3177
3547
|
for (const file of files) {
|
|
3178
3548
|
const sessionId = file.replace(".jsonl", "");
|
|
3179
|
-
const filePath =
|
|
3549
|
+
const filePath = join6(projectsDir, file);
|
|
3180
3550
|
try {
|
|
3181
|
-
const content =
|
|
3551
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3182
3552
|
const lines = content.split("\n").filter(Boolean);
|
|
3183
3553
|
for (let i = 0; i < lines.length; i++) {
|
|
3184
3554
|
try {
|
|
@@ -3254,7 +3624,7 @@ function extractTextContent(content) {
|
|
|
3254
3624
|
return "";
|
|
3255
3625
|
}
|
|
3256
3626
|
function parseTranscriptFile(filePath) {
|
|
3257
|
-
const content =
|
|
3627
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3258
3628
|
const lines = content.split("\n").filter(Boolean);
|
|
3259
3629
|
const messages = [];
|
|
3260
3630
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3305,7 +3675,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3305
3675
|
const sessions = [];
|
|
3306
3676
|
for (const file of batch) {
|
|
3307
3677
|
const sessionId = file.replace(".jsonl", "");
|
|
3308
|
-
const filePath =
|
|
3678
|
+
const filePath = join6(projectsDir, file);
|
|
3309
3679
|
try {
|
|
3310
3680
|
const allMessages = parseTranscriptFile(filePath);
|
|
3311
3681
|
const messages = allMessages.length > maxMessagesPerSession ? allMessages.slice(-maxMessagesPerSession) : allMessages;
|
|
@@ -3334,18 +3704,18 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
3334
3704
|
}
|
|
3335
3705
|
for (const file of batch) {
|
|
3336
3706
|
const sessionId = file.replace(".jsonl", "");
|
|
3337
|
-
const filePath =
|
|
3707
|
+
const filePath = join6(projectsDir, file);
|
|
3338
3708
|
try {
|
|
3339
|
-
const content =
|
|
3709
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3340
3710
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
3341
|
-
writeFileSync5(
|
|
3711
|
+
writeFileSync5(join6(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
3342
3712
|
} catch {
|
|
3343
3713
|
}
|
|
3344
3714
|
}
|
|
3345
3715
|
}
|
|
3346
3716
|
return { sessions: totalSessions, messages: totalMessages };
|
|
3347
3717
|
}
|
|
3348
|
-
var
|
|
3718
|
+
var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH, OFFSETS_DIR;
|
|
3349
3719
|
var init_install = __esm({
|
|
3350
3720
|
"cli/commands/install.ts"() {
|
|
3351
3721
|
"use strict";
|
|
@@ -3356,14 +3726,15 @@ var init_install = __esm({
|
|
|
3356
3726
|
init_graderDaemon();
|
|
3357
3727
|
init_stub();
|
|
3358
3728
|
init_repoConnect();
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3729
|
+
init_setupGithub();
|
|
3730
|
+
SYNKRO_DIR2 = join6(homedir5(), ".synkro");
|
|
3731
|
+
HOOKS_DIR = join6(SYNKRO_DIR2, "hooks");
|
|
3732
|
+
BIN_DIR = join6(SYNKRO_DIR2, "bin");
|
|
3733
|
+
CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
|
|
3734
|
+
GRADER_DAEMON_PATH = join6(BIN_DIR, "grader_daemon.py");
|
|
3735
|
+
GRADER_PRIMER_EDIT_PATH = join6(SYNKRO_DIR2, "grader-primer-edit.txt");
|
|
3736
|
+
GRADER_PRIMER_BASH_PATH = join6(SYNKRO_DIR2, "grader-primer-bash.txt");
|
|
3737
|
+
OFFSETS_DIR = join6(SYNKRO_DIR2, ".transcript-offsets");
|
|
3367
3738
|
}
|
|
3368
3739
|
});
|
|
3369
3740
|
|
|
@@ -3439,13 +3810,13 @@ var status_exports = {};
|
|
|
3439
3810
|
__export(status_exports, {
|
|
3440
3811
|
statusCommand: () => statusCommand
|
|
3441
3812
|
});
|
|
3442
|
-
import { existsSync as
|
|
3443
|
-
import { homedir as
|
|
3444
|
-
import { join as
|
|
3813
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
3814
|
+
import { homedir as homedir6 } from "os";
|
|
3815
|
+
import { join as join7 } from "path";
|
|
3445
3816
|
function readConfigEnv() {
|
|
3446
|
-
if (!
|
|
3817
|
+
if (!existsSync8(CONFIG_PATH3)) return {};
|
|
3447
3818
|
const out = {};
|
|
3448
|
-
const raw =
|
|
3819
|
+
const raw = readFileSync6(CONFIG_PATH3, "utf-8");
|
|
3449
3820
|
for (const line of raw.split("\n")) {
|
|
3450
3821
|
const trimmed = line.trim();
|
|
3451
3822
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -3476,10 +3847,10 @@ function statusCommand() {
|
|
|
3476
3847
|
console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
|
|
3477
3848
|
const info2 = getUserInfo();
|
|
3478
3849
|
const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
|
|
3479
|
-
const tierCacheFile =
|
|
3850
|
+
const tierCacheFile = join7(SYNKRO_DIR3, `.tier-cache-${userId}`);
|
|
3480
3851
|
let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
|
|
3481
|
-
if (!inferenceTier &&
|
|
3482
|
-
inferenceTier =
|
|
3852
|
+
if (!inferenceTier && existsSync8(tierCacheFile)) {
|
|
3853
|
+
inferenceTier = readFileSync6(tierCacheFile, "utf-8").trim() || null;
|
|
3483
3854
|
}
|
|
3484
3855
|
const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
|
|
3485
3856
|
console.log(` inference: ${tierLabel}`);
|
|
@@ -3506,19 +3877,19 @@ function statusCommand() {
|
|
|
3506
3877
|
}
|
|
3507
3878
|
}
|
|
3508
3879
|
console.log();
|
|
3509
|
-
const bashScript =
|
|
3510
|
-
const bashFollowupScript =
|
|
3511
|
-
const editPrecheckScript =
|
|
3512
|
-
const editCaptureScript =
|
|
3513
|
-
const stopSummaryScript =
|
|
3514
|
-
const sessionStartScript =
|
|
3880
|
+
const bashScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-judge.sh");
|
|
3881
|
+
const bashFollowupScript = join7(SYNKRO_DIR3, "hooks", "cc-bash-followup.sh");
|
|
3882
|
+
const editPrecheckScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-precheck.sh");
|
|
3883
|
+
const editCaptureScript = join7(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
|
|
3884
|
+
const stopSummaryScript = join7(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
|
|
3885
|
+
const sessionStartScript = join7(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
|
|
3515
3886
|
console.log("Hook scripts:");
|
|
3516
|
-
console.log(` ${
|
|
3517
|
-
console.log(` ${
|
|
3518
|
-
console.log(` ${
|
|
3519
|
-
console.log(` ${
|
|
3520
|
-
console.log(` ${
|
|
3521
|
-
console.log(` ${
|
|
3887
|
+
console.log(` ${existsSync8(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
|
|
3888
|
+
console.log(` ${existsSync8(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
|
|
3889
|
+
console.log(` ${existsSync8(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
|
|
3890
|
+
console.log(` ${existsSync8(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
|
|
3891
|
+
console.log(` ${existsSync8(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
|
|
3892
|
+
console.log(` ${existsSync8(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
|
|
3522
3893
|
console.log();
|
|
3523
3894
|
const mcp = inspectMcpConfig();
|
|
3524
3895
|
console.log("Guardrails MCP server (Claude Code):");
|
|
@@ -3530,7 +3901,7 @@ function statusCommand() {
|
|
|
3530
3901
|
console.log(` expected at ${mcp.configPath} \u2192 mcpServers.synkro-guardrails`);
|
|
3531
3902
|
}
|
|
3532
3903
|
}
|
|
3533
|
-
var
|
|
3904
|
+
var SYNKRO_DIR3, CONFIG_PATH3;
|
|
3534
3905
|
var init_status = __esm({
|
|
3535
3906
|
"cli/commands/status.ts"() {
|
|
3536
3907
|
"use strict";
|
|
@@ -3538,8 +3909,8 @@ var init_status = __esm({
|
|
|
3538
3909
|
init_agentDetect();
|
|
3539
3910
|
init_ccHookConfig();
|
|
3540
3911
|
init_mcpConfig();
|
|
3541
|
-
|
|
3542
|
-
|
|
3912
|
+
SYNKRO_DIR3 = join7(homedir6(), ".synkro");
|
|
3913
|
+
CONFIG_PATH3 = join7(SYNKRO_DIR3, "config.env");
|
|
3543
3914
|
}
|
|
3544
3915
|
});
|
|
3545
3916
|
|
|
@@ -3569,7 +3940,7 @@ var unlink_exports = {};
|
|
|
3569
3940
|
__export(unlink_exports, {
|
|
3570
3941
|
unlinkCommand: () => unlinkCommand
|
|
3571
3942
|
});
|
|
3572
|
-
import { createInterface as
|
|
3943
|
+
import { createInterface as createInterface4 } from "readline";
|
|
3573
3944
|
function ask2(rl, question) {
|
|
3574
3945
|
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
3575
3946
|
}
|
|
@@ -3597,7 +3968,7 @@ async function unlinkCommand() {
|
|
|
3597
3968
|
console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
|
|
3598
3969
|
});
|
|
3599
3970
|
console.log();
|
|
3600
|
-
const rl =
|
|
3971
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
3601
3972
|
try {
|
|
3602
3973
|
const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
|
|
3603
3974
|
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
|
|
@@ -3623,194 +3994,6 @@ var init_unlink = __esm({
|
|
|
3623
3994
|
}
|
|
3624
3995
|
});
|
|
3625
3996
|
|
|
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
3997
|
// cli/commands/scanPr.ts
|
|
3815
3998
|
var scanPr_exports = {};
|
|
3816
3999
|
__export(scanPr_exports, {
|
|
@@ -4346,7 +4529,16 @@ Usage:
|
|
|
4346
4529
|
synkro <command> [options] (alias)
|
|
4347
4530
|
|
|
4348
4531
|
Commands:
|
|
4349
|
-
install [
|
|
4532
|
+
install [options] Install Synkro hooks for detected agents (Claude Code, etc.)
|
|
4533
|
+
--force Reinstall from scratch
|
|
4534
|
+
--non-interactive Skip all interactive prompts (use flags)
|
|
4535
|
+
--link-repo Auto-link the local git repo
|
|
4536
|
+
--sync-transcripts Sync CC session history (default in non-interactive)
|
|
4537
|
+
--no-sync-transcripts Skip transcript sync
|
|
4538
|
+
--pr-scan=MODE PR scan auth: claude-oauth, byok, or skip
|
|
4539
|
+
--inference-key=KEY BYOK API key (Anthropic, OpenAI, or Gemini)
|
|
4540
|
+
--inference-model=M Model for classification + grading
|
|
4541
|
+
--github-token=TOK GitHub PAT for pushing secrets + workflow
|
|
4350
4542
|
login Authenticate with Synkro (browser OAuth via WorkOS)
|
|
4351
4543
|
logout Clear local credentials
|
|
4352
4544
|
status Show current setup state
|