@synkro-sh/cli 1.6.46 → 1.6.48
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 +246 -16
- 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 === "full") return "full";
|
|
8334
|
+
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
8335
|
+
try {
|
|
8336
|
+
const env = readFileSync8(CONFIG_PATH2, "utf-8");
|
|
8337
|
+
if (/^SYNKRO_HOOK_MODE=['"]?full['"]?\s*$/m.test(env)) return "full";
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
return "stub";
|
|
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,14 @@ 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;
|
|
8190
|
-
|
|
8356
|
+
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
8357
|
+
else if (a === "--legacy" || a === "--mode=full") opts.hookMode = "full";
|
|
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 === "full") opts.hookMode = "full";
|
|
8363
|
+
if (!opts.hookMode && process.env.SYNKRO_HOOK_MODE === "stub") opts.hookMode = "stub";
|
|
8191
8364
|
if (!opts.gatewayUrl) {
|
|
8192
8365
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
8193
8366
|
if (fromEnv) opts.gatewayUrl = fromEnv;
|
|
@@ -8298,7 +8471,7 @@ function ensureSynkroDir() {
|
|
|
8298
8471
|
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
8299
8472
|
mkdirSync8(join8(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
8300
8473
|
}
|
|
8301
|
-
function writeHookScripts() {
|
|
8474
|
+
function writeHookScripts(mode = "stub") {
|
|
8302
8475
|
const installExtractCorePath = join8(HOOKS_DIR, "installExtractCore.ts");
|
|
8303
8476
|
const bashScriptPath = join8(HOOKS_DIR, "cc-bash-judge.ts");
|
|
8304
8477
|
const bashFollowupScriptPath = join8(HOOKS_DIR, "cc-bash-followup.ts");
|
|
@@ -8318,6 +8491,56 @@ function writeHookScripts() {
|
|
|
8318
8491
|
const cursorEditCapturePath = join8(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
8319
8492
|
const cursorAgentCapturePath = join8(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
8320
8493
|
const mcpStdioProxyPath = join8(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
8494
|
+
if (mode === "stub") {
|
|
8495
|
+
const stubCommonPath = join8(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8496
|
+
const stubFiles = [
|
|
8497
|
+
[stubCommonPath, STUB_COMMON_TS],
|
|
8498
|
+
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
8499
|
+
[bashFollowupScriptPath, STUB_BASH_FOLLOWUP_TS],
|
|
8500
|
+
[editPrecheckScriptPath, STUB_EDIT_PRECHECK_TS],
|
|
8501
|
+
[cwePrecheckScriptPath, STUB_CWE_PRECHECK_TS],
|
|
8502
|
+
[cvePrecheckScriptPath, STUB_CVE_PRECHECK_TS],
|
|
8503
|
+
[planJudgeScriptPath, STUB_PLAN_JUDGE_TS],
|
|
8504
|
+
[agentJudgeScriptPath, STUB_AGENT_JUDGE_TS],
|
|
8505
|
+
[stopSummaryScriptPath, STUB_STOP_SUMMARY_TS],
|
|
8506
|
+
[sessionStartScriptPath, STUB_SESSION_START_TS],
|
|
8507
|
+
[transcriptSyncScriptPath, STUB_TRANSCRIPT_SYNC_TS],
|
|
8508
|
+
[userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
|
|
8509
|
+
[installScanScriptPath, STUB_INSTALL_SCAN_TS],
|
|
8510
|
+
[cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
|
|
8511
|
+
[cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
|
|
8512
|
+
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
8513
|
+
];
|
|
8514
|
+
for (const [p, content] of stubFiles) {
|
|
8515
|
+
writeFileSync7(p, content, "utf-8");
|
|
8516
|
+
chmodSync2(p, 493);
|
|
8517
|
+
}
|
|
8518
|
+
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8519
|
+
chmodSync2(mcpStdioProxyPath, 493);
|
|
8520
|
+
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
8521
|
+
try {
|
|
8522
|
+
unlinkSync4(join8(HOOKS_DIR, stale));
|
|
8523
|
+
} catch {
|
|
8524
|
+
}
|
|
8525
|
+
}
|
|
8526
|
+
return {
|
|
8527
|
+
bashScript: bashScriptPath,
|
|
8528
|
+
bashFollowupScript: bashFollowupScriptPath,
|
|
8529
|
+
editPrecheckScript: editPrecheckScriptPath,
|
|
8530
|
+
cwePrecheckScript: cwePrecheckScriptPath,
|
|
8531
|
+
cvePrecheckScript: cvePrecheckScriptPath,
|
|
8532
|
+
planJudgeScript: planJudgeScriptPath,
|
|
8533
|
+
agentJudgeScript: agentJudgeScriptPath,
|
|
8534
|
+
stopSummaryScript: stopSummaryScriptPath,
|
|
8535
|
+
sessionStartScript: sessionStartScriptPath,
|
|
8536
|
+
transcriptSyncScript: transcriptSyncScriptPath,
|
|
8537
|
+
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
8538
|
+
installScanScript: installScanScriptPath,
|
|
8539
|
+
cursorBashJudgeScript: cursorBashJudgePath,
|
|
8540
|
+
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8541
|
+
cursorAgentCaptureScript: cursorAgentCapturePath
|
|
8542
|
+
};
|
|
8543
|
+
}
|
|
8321
8544
|
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
8322
8545
|
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
8323
8546
|
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
@@ -8336,7 +8559,7 @@ function writeHookScripts() {
|
|
|
8336
8559
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
8337
8560
|
writeFileSync7(cursorAgentCapturePath, CURSOR_AGENT_CAPTURE_TS, "utf-8");
|
|
8338
8561
|
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");
|
|
8562
|
+
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
8563
|
chmodSync2(bashScriptPath, 493);
|
|
8341
8564
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
8342
8565
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -8356,6 +8579,10 @@ function writeHookScripts() {
|
|
|
8356
8579
|
chmodSync2(cursorAgentCapturePath, 493);
|
|
8357
8580
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8358
8581
|
chmodSync2(installExtractCorePath, 493);
|
|
8582
|
+
try {
|
|
8583
|
+
unlinkSync4(join8(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
8584
|
+
} catch {
|
|
8585
|
+
}
|
|
8359
8586
|
return {
|
|
8360
8587
|
bashScript: bashScriptPath,
|
|
8361
8588
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -8403,7 +8630,7 @@ function writeConfigEnv(opts) {
|
|
|
8403
8630
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8404
8631
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8405
8632
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8406
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8633
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.48")}`
|
|
8407
8634
|
];
|
|
8408
8635
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8409
8636
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8417,6 +8644,7 @@ function writeConfigEnv(opts) {
|
|
|
8417
8644
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
8418
8645
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
8419
8646
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
8647
|
+
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "stub", 8))}`);
|
|
8420
8648
|
lines.push("");
|
|
8421
8649
|
writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
8422
8650
|
chmodSync2(CONFIG_PATH2, 384);
|
|
@@ -8635,7 +8863,8 @@ async function installCommand(opts = {}) {
|
|
|
8635
8863
|
}
|
|
8636
8864
|
}
|
|
8637
8865
|
ensureSynkroDir();
|
|
8638
|
-
const
|
|
8866
|
+
const hookMode = opts.hookMode || resolvePersistedHookMode();
|
|
8867
|
+
const scripts = writeHookScripts(hookMode);
|
|
8639
8868
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8640
8869
|
for (const mode of ["edit", "bash"]) {
|
|
8641
8870
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
@@ -8806,7 +9035,7 @@ async function installCommand(opts = {}) {
|
|
|
8806
9035
|
}
|
|
8807
9036
|
const synkroBundle = resolveSynkroBundle();
|
|
8808
9037
|
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 });
|
|
9038
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
|
|
8810
9039
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
8811
9040
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
8812
9041
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -9110,7 +9339,7 @@ function reconcileHarness() {
|
|
|
9110
9339
|
const wantCC = sf.harness.includes("claude-code");
|
|
9111
9340
|
const wantCursor = sf.harness.includes("cursor");
|
|
9112
9341
|
console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
|
|
9113
|
-
const scripts = writeHookScripts();
|
|
9342
|
+
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9114
9343
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9115
9344
|
const ccSettings = join8(homedir8(), ".claude", "settings.json");
|
|
9116
9345
|
if (wantCC) {
|
|
@@ -9468,6 +9697,7 @@ var init_install = __esm({
|
|
|
9468
9697
|
init_skillParser();
|
|
9469
9698
|
init_hookScripts();
|
|
9470
9699
|
init_hookScriptsTs();
|
|
9700
|
+
init_hookScriptsTs();
|
|
9471
9701
|
init_stub();
|
|
9472
9702
|
init_repoConnect();
|
|
9473
9703
|
init_projects();
|
|
@@ -9528,7 +9758,7 @@ rl.on('line', async (line) => {
|
|
|
9528
9758
|
});
|
|
9529
9759
|
|
|
9530
9760
|
// 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
|
|
9761
|
+
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
9762
|
import { join as join9 } from "path";
|
|
9533
9763
|
import { homedir as homedir9 } from "os";
|
|
9534
9764
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -9609,7 +9839,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
9609
9839
|
renameSync4(tmpPath, CLAUDE_JSON_PATH);
|
|
9610
9840
|
} catch (err) {
|
|
9611
9841
|
try {
|
|
9612
|
-
|
|
9842
|
+
unlinkSync5(tmpPath);
|
|
9613
9843
|
} catch {
|
|
9614
9844
|
}
|
|
9615
9845
|
try {
|
|
@@ -11488,7 +11718,7 @@ var args = process.argv.slice(2);
|
|
|
11488
11718
|
var cmd = args[0] || "";
|
|
11489
11719
|
var subArgs = args.slice(1);
|
|
11490
11720
|
function printVersion() {
|
|
11491
|
-
console.log("1.6.
|
|
11721
|
+
console.log("1.6.48");
|
|
11492
11722
|
}
|
|
11493
11723
|
function printHelp2() {
|
|
11494
11724
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|