@synkro-sh/cli 1.6.46 → 1.6.47
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 +244 -15
- package/dist/bootstrap.js.map +1 -1
- package/package.json +5 -3
package/dist/bootstrap.js
CHANGED
|
@@ -804,7 +804,10 @@ synkro_post_with_retry() {
|
|
|
804
804
|
});
|
|
805
805
|
|
|
806
806
|
// cli/installer/hookScriptsTs.ts
|
|
807
|
-
|
|
807
|
+
function stubHook(surface, optsLiteral) {
|
|
808
|
+
return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
|
|
809
|
+
}
|
|
810
|
+
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
808
811
|
var init_hookScriptsTs = __esm({
|
|
809
812
|
"cli/installer/hookScriptsTs.ts"() {
|
|
810
813
|
"use strict";
|
|
@@ -6149,6 +6152,149 @@ async function main() {
|
|
|
6149
6152
|
|
|
6150
6153
|
main();
|
|
6151
6154
|
`;
|
|
6155
|
+
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6156
|
+
import { execSync } from 'node:child_process';
|
|
6157
|
+
import { homedir } from 'node:os';
|
|
6158
|
+
import { join } from 'node:path';
|
|
6159
|
+
|
|
6160
|
+
const HOME = homedir();
|
|
6161
|
+
const PORT = process.env.SYNKRO_MCP_PORT || '18931';
|
|
6162
|
+
|
|
6163
|
+
function loadMcpJwt(): string {
|
|
6164
|
+
try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
async function readStdin(): Promise<string> {
|
|
6168
|
+
const chunks: Buffer[] = [];
|
|
6169
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
6170
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
function isCursor(harnessOpt?: string): boolean {
|
|
6174
|
+
return harnessOpt === 'cursor' || process.argv.includes('--cursor') || process.env.SYNKRO_HOOK_FORMAT === 'cursor';
|
|
6175
|
+
}
|
|
6176
|
+
|
|
6177
|
+
function failOpen(harness: string): string {
|
|
6178
|
+
return harness === 'cursor' ? '{"permission":"allow"}' : '{}';
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
function out(s: string): void { try { process.stdout.write(s + '\n'); } catch {} }
|
|
6182
|
+
|
|
6183
|
+
function gitRoot(cwd: string): string {
|
|
6184
|
+
if (!cwd) return '';
|
|
6185
|
+
try {
|
|
6186
|
+
return execSync('git rev-parse --show-toplevel', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
6187
|
+
} catch { return ''; }
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
// Once-per-session onboarding hint for repos with no .synkro file.
|
|
6191
|
+
function noSynkroHint(sessionId: string): string | null {
|
|
6192
|
+
const dir = join(HOME, '.synkro', '.no-synkro-hint');
|
|
6193
|
+
try {
|
|
6194
|
+
mkdirSync(dir, { recursive: true });
|
|
6195
|
+
const key = (sessionId || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6196
|
+
const marker = join(dir, key);
|
|
6197
|
+
if (existsSync(marker)) return null;
|
|
6198
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
6199
|
+
} catch {}
|
|
6200
|
+
return '[synkro] No .synkro config in this repo — grading skipped. Run synkro install here to enable Synkro.';
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
function filePathFromToolInput(ti: any): string {
|
|
6204
|
+
if (!ti || typeof ti !== 'object') return '';
|
|
6205
|
+
return ti.file_path || ti.notebook_path || ti.path || ti.target_file || '';
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
interface StubOpts {
|
|
6209
|
+
needsFile?: boolean;
|
|
6210
|
+
needsTranscript?: boolean;
|
|
6211
|
+
// Ship the WHOLE transcript (not just the 200KB tail). Sync surfaces need it
|
|
6212
|
+
// so the server can compute stable absolute message indices + total usage.
|
|
6213
|
+
fullTranscript?: boolean;
|
|
6214
|
+
needsPlan?: boolean;
|
|
6215
|
+
telemetry?: boolean;
|
|
6216
|
+
harness?: string;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
|
|
6220
|
+
const harness = isCursor(opts.harness) ? 'cursor' : 'cc';
|
|
6221
|
+
try {
|
|
6222
|
+
const input = await readStdin();
|
|
6223
|
+
if (!input.trim()) { out(failOpen(harness)); return; }
|
|
6224
|
+
const payload = JSON.parse(input);
|
|
6225
|
+
|
|
6226
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
|
|
6227
|
+
const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
|
|
6228
|
+
const sessionId = String(payload.session_id || payload.conversation_id || '');
|
|
6229
|
+
const root = gitRoot(cwd);
|
|
6230
|
+
|
|
6231
|
+
// Dormancy: a repo is onboarded only if it has a .synkro at its git root.
|
|
6232
|
+
let synkroFileText = '';
|
|
6233
|
+
if (root && existsSync(join(root, '.synkro'))) {
|
|
6234
|
+
try { synkroFileText = readFileSync(join(root, '.synkro'), 'utf-8'); } catch {}
|
|
6235
|
+
}
|
|
6236
|
+
if (!synkroFileText) {
|
|
6237
|
+
if (opts.telemetry) { out(failOpen(harness)); return; }
|
|
6238
|
+
const hint = noSynkroHint(sessionId);
|
|
6239
|
+
if (!hint) { out(failOpen(harness)); return; }
|
|
6240
|
+
if (harness === 'cursor') out(JSON.stringify({ permission: 'allow', user_message: hint }));
|
|
6241
|
+
else out(JSON.stringify({ systemMessage: hint, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: hint } }));
|
|
6242
|
+
return;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
// Gather host-only inputs the container can't read.
|
|
6246
|
+
const envelope: any = { payload, harness, cwd: root || cwd, sessionId, synkroFileText };
|
|
6247
|
+
|
|
6248
|
+
if (opts.needsFile) {
|
|
6249
|
+
const fp = filePathFromToolInput(payload.tool_input || {});
|
|
6250
|
+
if (fp && existsSync(fp)) {
|
|
6251
|
+
try { envelope.baseContent = readFileSync(fp, 'utf-8').slice(0, 65536); } catch {}
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
if (opts.needsTranscript) {
|
|
6255
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
|
|
6256
|
+
if (tp && existsSync(tp)) {
|
|
6257
|
+
try {
|
|
6258
|
+
const t = readFileSync(tp, 'utf-8');
|
|
6259
|
+
envelope.transcript = (opts.fullTranscript || t.length <= 200000) ? t : t.slice(t.length - 200000);
|
|
6260
|
+
} catch {}
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
if (opts.needsPlan) {
|
|
6264
|
+
const ti = payload.tool_input || {};
|
|
6265
|
+
envelope.planText = String(ti.plan || ti.content || payload.plan || '');
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
const timeoutMs = opts.telemetry ? 6000 : 28000;
|
|
6269
|
+
const url = 'http://127.0.0.1:' + PORT + '/api/scan/' + surface + (harness === 'cursor' ? '?harness=cursor' : '');
|
|
6270
|
+
const resp = await fetch(url, {
|
|
6271
|
+
method: 'POST',
|
|
6272
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
|
|
6273
|
+
body: JSON.stringify(envelope),
|
|
6274
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
6275
|
+
});
|
|
6276
|
+
const text = await resp.text();
|
|
6277
|
+
out(resp.ok ? (text.trim() || failOpen(harness)) : failOpen(harness));
|
|
6278
|
+
} catch {
|
|
6279
|
+
out(failOpen(harness));
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
`;
|
|
6283
|
+
STUB_EDIT_PRECHECK_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6284
|
+
STUB_CWE_PRECHECK_TS = stubHook("cwe-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6285
|
+
STUB_CVE_PRECHECK_TS = stubHook("cve-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6286
|
+
STUB_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true }");
|
|
6287
|
+
STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
|
|
6288
|
+
STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
|
|
6289
|
+
STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
|
|
6290
|
+
STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
|
|
6291
|
+
STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
|
|
6292
|
+
STUB_TRANSCRIPT_SYNC_TS = stubHook("transcript-sync", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
|
|
6293
|
+
STUB_USER_PROMPT_SUBMIT_TS = stubHook("prompt-submit", "{ telemetry: true }");
|
|
6294
|
+
STUB_BASH_FOLLOWUP_TS = stubHook("bash-followup", "{ telemetry: true }");
|
|
6295
|
+
STUB_CURSOR_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
6296
|
+
STUB_CURSOR_EDIT_CAPTURE_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true, harness: 'cursor' }");
|
|
6297
|
+
STUB_CURSOR_AGENT_CAPTURE_TS = stubHook("agent-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
6152
6298
|
}
|
|
6153
6299
|
});
|
|
6154
6300
|
|
|
@@ -7494,10 +7640,20 @@ async function dockerInstall(opts = {}) {
|
|
|
7494
7640
|
} else {
|
|
7495
7641
|
mkdirSync7(join6(homedir6(), ".claude"), { recursive: true });
|
|
7496
7642
|
}
|
|
7497
|
-
|
|
7498
|
-
const
|
|
7499
|
-
if (
|
|
7500
|
-
|
|
7643
|
+
const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
7644
|
+
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
7645
|
+
if (skipPull && imageExistsLocally()) {
|
|
7646
|
+
console.log(` Using local image ${image} (SYNKRO_SKIP_PULL set \u2014 pull skipped).`);
|
|
7647
|
+
} else {
|
|
7648
|
+
console.log(` Pulling ${image}...`);
|
|
7649
|
+
const pull = spawnSync2("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
|
|
7650
|
+
if (pull.status !== 0) {
|
|
7651
|
+
if (imageExistsLocally()) {
|
|
7652
|
+
console.warn(` \u26A0 docker pull ${image} failed \u2014 falling back to the existing local image.`);
|
|
7653
|
+
} else {
|
|
7654
|
+
throw new DockerInstallError(`docker pull ${image} failed`);
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7501
7657
|
}
|
|
7502
7658
|
const existing = dockerStatus();
|
|
7503
7659
|
if (existing.running) {
|
|
@@ -8168,11 +8324,21 @@ __export(install_exports, {
|
|
|
8168
8324
|
syncSkillFiles: () => syncSkillFiles,
|
|
8169
8325
|
writeHookScripts: () => writeHookScripts
|
|
8170
8326
|
});
|
|
8171
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
|
|
8327
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
8172
8328
|
import { homedir as homedir8 } from "os";
|
|
8173
8329
|
import { join as join8 } from "path";
|
|
8174
8330
|
import { execSync as execSync6 } from "child_process";
|
|
8175
8331
|
import { createInterface as createInterface3 } from "readline";
|
|
8332
|
+
function resolvePersistedHookMode() {
|
|
8333
|
+
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
8334
|
+
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
8335
|
+
try {
|
|
8336
|
+
const env = readFileSync8(CONFIG_PATH2, "utf-8");
|
|
8337
|
+
if (/^SYNKRO_HOOK_MODE=['"]?stub['"]?\s*$/m.test(env)) return "stub";
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
return "full";
|
|
8341
|
+
}
|
|
8176
8342
|
function sanitizeGatewayCandidate(raw) {
|
|
8177
8343
|
if (!raw) return void 0;
|
|
8178
8344
|
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
@@ -8187,7 +8353,13 @@ function parseArgs(argv) {
|
|
|
8187
8353
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
8188
8354
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
8189
8355
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
8356
|
+
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
8357
|
+
else if (a === "--mode=full") opts.hookMode = "full";
|
|
8190
8358
|
}
|
|
8359
|
+
const modeIdx = argv.indexOf("--mode");
|
|
8360
|
+
if (modeIdx >= 0 && argv[modeIdx + 1] === "stub") opts.hookMode = "stub";
|
|
8361
|
+
if (modeIdx >= 0 && argv[modeIdx + 1] === "full") opts.hookMode = "full";
|
|
8362
|
+
if (!opts.hookMode && process.env.SYNKRO_HOOK_MODE === "stub") opts.hookMode = "stub";
|
|
8191
8363
|
if (!opts.gatewayUrl) {
|
|
8192
8364
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
8193
8365
|
if (fromEnv) opts.gatewayUrl = fromEnv;
|
|
@@ -8298,7 +8470,7 @@ function ensureSynkroDir() {
|
|
|
8298
8470
|
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
8299
8471
|
mkdirSync8(join8(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
8300
8472
|
}
|
|
8301
|
-
function writeHookScripts() {
|
|
8473
|
+
function writeHookScripts(mode = "full") {
|
|
8302
8474
|
const installExtractCorePath = join8(HOOKS_DIR, "installExtractCore.ts");
|
|
8303
8475
|
const bashScriptPath = join8(HOOKS_DIR, "cc-bash-judge.ts");
|
|
8304
8476
|
const bashFollowupScriptPath = join8(HOOKS_DIR, "cc-bash-followup.ts");
|
|
@@ -8318,6 +8490,56 @@ function writeHookScripts() {
|
|
|
8318
8490
|
const cursorEditCapturePath = join8(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
8319
8491
|
const cursorAgentCapturePath = join8(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
8320
8492
|
const mcpStdioProxyPath = join8(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
8493
|
+
if (mode === "stub") {
|
|
8494
|
+
const stubCommonPath = join8(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8495
|
+
const stubFiles = [
|
|
8496
|
+
[stubCommonPath, STUB_COMMON_TS],
|
|
8497
|
+
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
8498
|
+
[bashFollowupScriptPath, STUB_BASH_FOLLOWUP_TS],
|
|
8499
|
+
[editPrecheckScriptPath, STUB_EDIT_PRECHECK_TS],
|
|
8500
|
+
[cwePrecheckScriptPath, STUB_CWE_PRECHECK_TS],
|
|
8501
|
+
[cvePrecheckScriptPath, STUB_CVE_PRECHECK_TS],
|
|
8502
|
+
[planJudgeScriptPath, STUB_PLAN_JUDGE_TS],
|
|
8503
|
+
[agentJudgeScriptPath, STUB_AGENT_JUDGE_TS],
|
|
8504
|
+
[stopSummaryScriptPath, STUB_STOP_SUMMARY_TS],
|
|
8505
|
+
[sessionStartScriptPath, STUB_SESSION_START_TS],
|
|
8506
|
+
[transcriptSyncScriptPath, STUB_TRANSCRIPT_SYNC_TS],
|
|
8507
|
+
[userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
|
|
8508
|
+
[installScanScriptPath, STUB_INSTALL_SCAN_TS],
|
|
8509
|
+
[cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
|
|
8510
|
+
[cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
|
|
8511
|
+
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
8512
|
+
];
|
|
8513
|
+
for (const [p, content] of stubFiles) {
|
|
8514
|
+
writeFileSync7(p, content, "utf-8");
|
|
8515
|
+
chmodSync2(p, 493);
|
|
8516
|
+
}
|
|
8517
|
+
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8518
|
+
chmodSync2(mcpStdioProxyPath, 493);
|
|
8519
|
+
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
8520
|
+
try {
|
|
8521
|
+
unlinkSync4(join8(HOOKS_DIR, stale));
|
|
8522
|
+
} catch {
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
return {
|
|
8526
|
+
bashScript: bashScriptPath,
|
|
8527
|
+
bashFollowupScript: bashFollowupScriptPath,
|
|
8528
|
+
editPrecheckScript: editPrecheckScriptPath,
|
|
8529
|
+
cwePrecheckScript: cwePrecheckScriptPath,
|
|
8530
|
+
cvePrecheckScript: cvePrecheckScriptPath,
|
|
8531
|
+
planJudgeScript: planJudgeScriptPath,
|
|
8532
|
+
agentJudgeScript: agentJudgeScriptPath,
|
|
8533
|
+
stopSummaryScript: stopSummaryScriptPath,
|
|
8534
|
+
sessionStartScript: sessionStartScriptPath,
|
|
8535
|
+
transcriptSyncScript: transcriptSyncScriptPath,
|
|
8536
|
+
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
8537
|
+
installScanScript: installScanScriptPath,
|
|
8538
|
+
cursorBashJudgeScript: cursorBashJudgePath,
|
|
8539
|
+
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8540
|
+
cursorAgentCaptureScript: cursorAgentCapturePath
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8321
8543
|
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
8322
8544
|
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
8323
8545
|
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
@@ -8336,7 +8558,7 @@ function writeHookScripts() {
|
|
|
8336
8558
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
8337
8559
|
writeFileSync7(cursorAgentCapturePath, CURSOR_AGENT_CAPTURE_TS, "utf-8");
|
|
8338
8560
|
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8339
|
-
writeFileSync7(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
8561
|
+
writeFileSync7(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
8340
8562
|
chmodSync2(bashScriptPath, 493);
|
|
8341
8563
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
8342
8564
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -8356,6 +8578,10 @@ function writeHookScripts() {
|
|
|
8356
8578
|
chmodSync2(cursorAgentCapturePath, 493);
|
|
8357
8579
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8358
8580
|
chmodSync2(installExtractCorePath, 493);
|
|
8581
|
+
try {
|
|
8582
|
+
unlinkSync4(join8(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
8583
|
+
} catch {
|
|
8584
|
+
}
|
|
8359
8585
|
return {
|
|
8360
8586
|
bashScript: bashScriptPath,
|
|
8361
8587
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -8403,7 +8629,7 @@ function writeConfigEnv(opts) {
|
|
|
8403
8629
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8404
8630
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8405
8631
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8406
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8632
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.47")}`
|
|
8407
8633
|
];
|
|
8408
8634
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8409
8635
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8417,6 +8643,7 @@ function writeConfigEnv(opts) {
|
|
|
8417
8643
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
8418
8644
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
8419
8645
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
8646
|
+
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "full", 8))}`);
|
|
8420
8647
|
lines.push("");
|
|
8421
8648
|
writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
8422
8649
|
chmodSync2(CONFIG_PATH2, 384);
|
|
@@ -8635,7 +8862,8 @@ async function installCommand(opts = {}) {
|
|
|
8635
8862
|
}
|
|
8636
8863
|
}
|
|
8637
8864
|
ensureSynkroDir();
|
|
8638
|
-
const
|
|
8865
|
+
const hookMode = opts.hookMode || resolvePersistedHookMode();
|
|
8866
|
+
const scripts = writeHookScripts(hookMode);
|
|
8639
8867
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8640
8868
|
for (const mode of ["edit", "bash"]) {
|
|
8641
8869
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
@@ -8806,7 +9034,7 @@ async function installCommand(opts = {}) {
|
|
|
8806
9034
|
}
|
|
8807
9035
|
const synkroBundle = resolveSynkroBundle();
|
|
8808
9036
|
const persistedMode = resolveDeploymentMode();
|
|
8809
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode });
|
|
9037
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
|
|
8810
9038
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
8811
9039
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
8812
9040
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -9110,7 +9338,7 @@ function reconcileHarness() {
|
|
|
9110
9338
|
const wantCC = sf.harness.includes("claude-code");
|
|
9111
9339
|
const wantCursor = sf.harness.includes("cursor");
|
|
9112
9340
|
console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
|
|
9113
|
-
const scripts = writeHookScripts();
|
|
9341
|
+
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9114
9342
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9115
9343
|
const ccSettings = join8(homedir8(), ".claude", "settings.json");
|
|
9116
9344
|
if (wantCC) {
|
|
@@ -9468,6 +9696,7 @@ var init_install = __esm({
|
|
|
9468
9696
|
init_skillParser();
|
|
9469
9697
|
init_hookScripts();
|
|
9470
9698
|
init_hookScriptsTs();
|
|
9699
|
+
init_hookScriptsTs();
|
|
9471
9700
|
init_stub();
|
|
9472
9701
|
init_repoConnect();
|
|
9473
9702
|
init_projects();
|
|
@@ -9528,7 +9757,7 @@ rl.on('line', async (line) => {
|
|
|
9528
9757
|
});
|
|
9529
9758
|
|
|
9530
9759
|
// cli/local-cc/install.ts
|
|
9531
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as
|
|
9760
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
9532
9761
|
import { join as join9 } from "path";
|
|
9533
9762
|
import { homedir as homedir9 } from "os";
|
|
9534
9763
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -9609,7 +9838,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
9609
9838
|
renameSync4(tmpPath, CLAUDE_JSON_PATH);
|
|
9610
9839
|
} catch (err) {
|
|
9611
9840
|
try {
|
|
9612
|
-
|
|
9841
|
+
unlinkSync5(tmpPath);
|
|
9613
9842
|
} catch {
|
|
9614
9843
|
}
|
|
9615
9844
|
try {
|
|
@@ -11488,7 +11717,7 @@ var args = process.argv.slice(2);
|
|
|
11488
11717
|
var cmd = args[0] || "";
|
|
11489
11718
|
var subArgs = args.slice(1);
|
|
11490
11719
|
function printVersion() {
|
|
11491
|
-
console.log("1.6.
|
|
11720
|
+
console.log("1.6.47");
|
|
11492
11721
|
}
|
|
11493
11722
|
function printHelp2() {
|
|
11494
11723
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|