@synkro-sh/cli 1.6.5 → 1.6.7
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 +203 -35
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -733,7 +733,7 @@ var init_hookScriptsTs = __esm({
|
|
|
733
733
|
"use strict";
|
|
734
734
|
SYNKRO_COMMON_TS = `
|
|
735
735
|
// Shared Synkro hook utilities \u2014 imported by all hook scripts.
|
|
736
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
736
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync, readdirSync, statSync } from 'node:fs';
|
|
737
737
|
import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
|
|
738
738
|
import { homedir } from 'node:os';
|
|
739
739
|
import { execSync } from 'node:child_process';
|
|
@@ -1002,6 +1002,10 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1002
1002
|
scanExemptions: [],
|
|
1003
1003
|
};
|
|
1004
1004
|
|
|
1005
|
+
// Kick the telemetry spool drainer. Fire-and-forget: it runs concurrently
|
|
1006
|
+
// with the grade that follows this call, so it adds no latency to the hook.
|
|
1007
|
+
drainSpool().catch(() => {});
|
|
1008
|
+
|
|
1005
1009
|
// Local-first: fetch from the local MCP server (PGLite-backed) \u2014 zero network egress.
|
|
1006
1010
|
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
1007
1011
|
try {
|
|
@@ -1596,18 +1600,104 @@ export function dispatchCapture(
|
|
|
1596
1600
|
}).catch(() => {});
|
|
1597
1601
|
}
|
|
1598
1602
|
|
|
1603
|
+
// \u2500\u2500\u2500 Durable Telemetry Spool \u2500\u2500\u2500
|
|
1604
|
+
// Telemetry must survive process death, container restarts, and ingest-server
|
|
1605
|
+
// backpressure. Instead of a fire-and-forget POST (which silently dropped
|
|
1606
|
+
// captures under parallel load), every event is appended synchronously to a
|
|
1607
|
+
// local JSONL spool \u2014 a write that completes before the function returns and
|
|
1608
|
+
// outlives the hook process. A background drainer (kicked from loadConfig, so
|
|
1609
|
+
// it overlaps the multi-second grade) batch-ships the spool to the ingest
|
|
1610
|
+
// server and only deletes events after a confirmed write. Ingest is idempotent
|
|
1611
|
+
// (ON CONFLICT DO NOTHING on event_id), so a retried or double-drained event
|
|
1612
|
+
// is harmless.
|
|
1613
|
+
|
|
1614
|
+
const TELEMETRY_SPOOL = join(HOME, '.synkro', 'telemetry-spool.jsonl');
|
|
1615
|
+
const SPOOL_DRAIN_PREFIX = 'telemetry-spool.jsonl.draining.';
|
|
1616
|
+
|
|
1617
|
+
// appendLocalTelemetry \u2014 durably records one telemetry event. The synchronous
|
|
1618
|
+
// append IS the durability guarantee; nothing here can drop the event.
|
|
1599
1619
|
export function appendLocalTelemetry(body: Record<string, any>): void {
|
|
1600
1620
|
const event = { ...body, _ts: new Date().toISOString() };
|
|
1621
|
+
try {
|
|
1622
|
+
appendFileSync(TELEMETRY_SPOOL, JSON.stringify(event) + '\\n');
|
|
1623
|
+
} catch {}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// drainSpool \u2014 claims the spool via atomic rename, batch-ships it to the
|
|
1627
|
+
// ingest server, deletes on success, re-spools on failure. Fire-and-forget:
|
|
1628
|
+
// callers kick it and let it run concurrently with the grade.
|
|
1629
|
+
export async function drainSpool(): Promise<void> {
|
|
1630
|
+
const dir = join(HOME, '.synkro');
|
|
1631
|
+
const claimed: string[] = [];
|
|
1632
|
+
|
|
1633
|
+
// 1. Claim the live spool by atomic rename \u2014 a fresh spool takes new writes.
|
|
1634
|
+
try {
|
|
1635
|
+
if (existsSync(TELEMETRY_SPOOL) && statSync(TELEMETRY_SPOOL).size > 0) {
|
|
1636
|
+
const claim = join(dir, SPOOL_DRAIN_PREFIX + process.pid + '.' + Date.now());
|
|
1637
|
+
renameSync(TELEMETRY_SPOOL, claim);
|
|
1638
|
+
claimed.push(claim);
|
|
1639
|
+
}
|
|
1640
|
+
} catch {}
|
|
1641
|
+
|
|
1642
|
+
// 2. Recover orphaned claim files \u2014 a previous hook died mid-drain. Only
|
|
1643
|
+
// adopt claims older than 30s so we never steal another hook's in-flight drain.
|
|
1644
|
+
try {
|
|
1645
|
+
for (const f of readdirSync(dir)) {
|
|
1646
|
+
if (!f.startsWith(SPOOL_DRAIN_PREFIX)) continue;
|
|
1647
|
+
const full = join(dir, f);
|
|
1648
|
+
if (claimed.indexOf(full) !== -1) continue;
|
|
1649
|
+
try {
|
|
1650
|
+
if (Date.now() - statSync(full).mtimeMs > 30000) claimed.push(full);
|
|
1651
|
+
} catch {}
|
|
1652
|
+
}
|
|
1653
|
+
} catch {}
|
|
1654
|
+
if (claimed.length === 0) return;
|
|
1655
|
+
|
|
1656
|
+
// 3. Read every event out of the claimed files.
|
|
1657
|
+
const events: any[] = [];
|
|
1658
|
+
for (const f of claimed) {
|
|
1659
|
+
try {
|
|
1660
|
+
for (const line of readFileSync(f, 'utf-8').split('\\n')) {
|
|
1661
|
+
const t = line.trim();
|
|
1662
|
+
if (!t) continue;
|
|
1663
|
+
try { events.push(JSON.parse(t)); } catch {}
|
|
1664
|
+
}
|
|
1665
|
+
} catch {}
|
|
1666
|
+
}
|
|
1667
|
+
if (events.length === 0) {
|
|
1668
|
+
for (const f of claimed) { try { unlinkSync(f); } catch {} }
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// 4. Ship to /api/ingest/batch in chunks. A token is required by the server.
|
|
1601
1673
|
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
1602
1674
|
let mcpToken = '';
|
|
1603
1675
|
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
1604
|
-
if (!mcpToken) return;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1676
|
+
if (!mcpToken) return; // leave claim files; a later drain retries them
|
|
1677
|
+
|
|
1678
|
+
let allOk = true;
|
|
1679
|
+
for (let i = 0; i < events.length; i += 200) {
|
|
1680
|
+
try {
|
|
1681
|
+
const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/ingest/batch', {
|
|
1682
|
+
method: 'POST',
|
|
1683
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
|
|
1684
|
+
body: JSON.stringify({ events: events.slice(i, i + 200) }),
|
|
1685
|
+
signal: AbortSignal.timeout(8000),
|
|
1686
|
+
});
|
|
1687
|
+
if (!resp.ok) { allOk = false; break; }
|
|
1688
|
+
} catch { allOk = false; break; }
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// 5. Success \u2192 drop the claim files. Failure \u2192 re-spool for the next drain.
|
|
1692
|
+
if (allOk) {
|
|
1693
|
+
for (const f of claimed) { try { unlinkSync(f); } catch {} }
|
|
1694
|
+
} else {
|
|
1695
|
+
try {
|
|
1696
|
+
appendFileSync(TELEMETRY_SPOOL, events.map(e => JSON.stringify(e)).join('\\n') + '\\n');
|
|
1697
|
+
for (const f of claimed) { try { unlinkSync(f); } catch {} }
|
|
1698
|
+
} catch {}
|
|
1699
|
+
// if re-spool failed, claim files remain and are recovered as orphans later
|
|
1700
|
+
}
|
|
1611
1701
|
}
|
|
1612
1702
|
|
|
1613
1703
|
// \u2500\u2500\u2500 Rule Mode Lookup \u2500\u2500\u2500
|
|
@@ -5768,6 +5858,7 @@ __export(dockerInstall_exports, {
|
|
|
5768
5858
|
dockerStop: () => dockerStop,
|
|
5769
5859
|
dockerUpdate: () => dockerUpdate,
|
|
5770
5860
|
imageTag: () => imageTag,
|
|
5861
|
+
readContainerConfig: () => readContainerConfig,
|
|
5771
5862
|
resolveWorkerConfig: () => resolveWorkerConfig,
|
|
5772
5863
|
splitWorkers: () => splitWorkers,
|
|
5773
5864
|
waitForContainerReady: () => waitForContainerReady
|
|
@@ -6001,6 +6092,34 @@ function dockerStatus() {
|
|
|
6001
6092
|
healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
|
|
6002
6093
|
};
|
|
6003
6094
|
}
|
|
6095
|
+
function readContainerConfig() {
|
|
6096
|
+
const r = spawnSync2("docker", ["inspect", "--format", "{{json .Config.Env}}", CONTAINER_NAME], {
|
|
6097
|
+
encoding: "utf-8",
|
|
6098
|
+
timeout: 5e3
|
|
6099
|
+
});
|
|
6100
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
6101
|
+
let env;
|
|
6102
|
+
try {
|
|
6103
|
+
env = JSON.parse(r.stdout.trim());
|
|
6104
|
+
} catch {
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
if (!Array.isArray(env)) return null;
|
|
6108
|
+
const get = (k) => {
|
|
6109
|
+
const hit = env.find((e) => typeof e === "string" && e.startsWith(k + "="));
|
|
6110
|
+
return hit ? hit.slice(k.length + 1) : void 0;
|
|
6111
|
+
};
|
|
6112
|
+
const num = (s) => {
|
|
6113
|
+
if (s === void 0) return void 0;
|
|
6114
|
+
const n = parseInt(s, 10);
|
|
6115
|
+
return Number.isFinite(n) ? n : void 0;
|
|
6116
|
+
};
|
|
6117
|
+
return {
|
|
6118
|
+
claudeWorkers: num(get("CLAUDE_WORKERS")),
|
|
6119
|
+
cursorWorkers: num(get("CURSOR_WORKERS")),
|
|
6120
|
+
connectedRepo: get("SYNKRO_CONNECTED_REPO") || void 0
|
|
6121
|
+
};
|
|
6122
|
+
}
|
|
6004
6123
|
async function dockerSafeStop() {
|
|
6005
6124
|
const status = dockerStatus();
|
|
6006
6125
|
if (!status.running) {
|
|
@@ -6327,7 +6446,7 @@ function writeConfigEnv(opts) {
|
|
|
6327
6446
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6328
6447
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6329
6448
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6330
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6449
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.7")}`
|
|
6331
6450
|
];
|
|
6332
6451
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6333
6452
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7450,22 +7569,22 @@ import { existsSync as existsSync11, rmSync, readdirSync as readdirSync3 } from
|
|
|
7450
7569
|
import { homedir as homedir10 } from "os";
|
|
7451
7570
|
import { join as join10 } from "path";
|
|
7452
7571
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7453
|
-
|
|
7572
|
+
import { createInterface as createInterface4 } from "readline";
|
|
7573
|
+
async function tearDownLocalCC() {
|
|
7454
7574
|
const docker = dockerStatus();
|
|
7455
7575
|
if (docker.running) {
|
|
7456
7576
|
await dockerSafeStop();
|
|
7457
|
-
console.log("\u2713 stopped synkro-server container
|
|
7577
|
+
console.log("\u2713 stopped synkro-server container");
|
|
7458
7578
|
} else {
|
|
7459
7579
|
console.log("\xB7 no synkro-server container running");
|
|
7460
7580
|
}
|
|
7461
7581
|
dockerRemove();
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
}
|
|
7582
|
+
console.log("\u2713 removed synkro-server container");
|
|
7583
|
+
try {
|
|
7584
|
+
const image = imageTag();
|
|
7585
|
+
const r = spawnSync4("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
|
|
7586
|
+
console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
|
|
7587
|
+
} catch {
|
|
7469
7588
|
}
|
|
7470
7589
|
if (needsKeychainBridge()) {
|
|
7471
7590
|
try {
|
|
@@ -7477,10 +7596,31 @@ async function tearDownLocalCC(purge) {
|
|
|
7477
7596
|
uninstallLocalCC();
|
|
7478
7597
|
console.log("\u2713 cleaned ~/.claude.json entries");
|
|
7479
7598
|
}
|
|
7599
|
+
function confirmPurge() {
|
|
7600
|
+
console.log("\u26A0 WARNING \u2014 synkro uninstall --purge");
|
|
7601
|
+
console.log(" This permanently deletes ALL Synkro data on this machine,");
|
|
7602
|
+
console.log(" including every scan finding, telemetry record, and backup in");
|
|
7603
|
+
console.log(" ~/.synkro/pgdata and ~/.synkro/pgdata-backups. It cannot be undone.\n");
|
|
7604
|
+
if (!process.stdin.isTTY) {
|
|
7605
|
+
console.log(" Non-interactive shell \u2014 re-run in a terminal to confirm.");
|
|
7606
|
+
return Promise.resolve(false);
|
|
7607
|
+
}
|
|
7608
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
7609
|
+
return new Promise((resolve3) => {
|
|
7610
|
+
rl.question(" Type 'yes' to wipe everything (anything else cancels): ", (answer) => {
|
|
7611
|
+
rl.close();
|
|
7612
|
+
resolve3(answer.trim().toLowerCase() === "yes");
|
|
7613
|
+
});
|
|
7614
|
+
});
|
|
7615
|
+
}
|
|
7480
7616
|
async function disconnectCommand(args2 = []) {
|
|
7481
7617
|
const purge = args2.includes("--purge");
|
|
7482
|
-
|
|
7483
|
-
|
|
7618
|
+
if (purge && !await confirmPurge()) {
|
|
7619
|
+
console.log("\nAborted \u2014 nothing was removed.");
|
|
7620
|
+
return;
|
|
7621
|
+
}
|
|
7622
|
+
console.log("\nSynkro uninstall starting...\n");
|
|
7623
|
+
await tearDownLocalCC();
|
|
7484
7624
|
const agents = detectAgents();
|
|
7485
7625
|
let sawClaudeCode = false;
|
|
7486
7626
|
for (const agent of agents) {
|
|
@@ -7501,31 +7641,32 @@ async function disconnectCommand(args2 = []) {
|
|
|
7501
7641
|
const cursorMcpRemoved = uninstallCursorMcpConfig();
|
|
7502
7642
|
console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
|
|
7503
7643
|
}
|
|
7504
|
-
if (
|
|
7505
|
-
if (
|
|
7506
|
-
|
|
7507
|
-
|
|
7644
|
+
if (existsSync11(SYNKRO_DIR5)) {
|
|
7645
|
+
if (purge) {
|
|
7646
|
+
rmSync(SYNKRO_DIR5, { recursive: true, force: true });
|
|
7647
|
+
console.log(`\u2713 wiped ${SYNKRO_DIR5} entirely \u2014 including all scan data and backups`);
|
|
7648
|
+
} else {
|
|
7649
|
+
const keep = /* @__PURE__ */ new Set([join10(SYNKRO_DIR5, "pgdata"), join10(SYNKRO_DIR5, "pgdata-backups")]);
|
|
7508
7650
|
const preserved = [];
|
|
7509
7651
|
for (const entry of readdirSync3(SYNKRO_DIR5)) {
|
|
7510
7652
|
const full = join10(SYNKRO_DIR5, entry);
|
|
7511
|
-
if (full
|
|
7653
|
+
if (keep.has(full)) {
|
|
7512
7654
|
preserved.push(entry);
|
|
7513
7655
|
continue;
|
|
7514
7656
|
}
|
|
7515
7657
|
rmSync(full, { recursive: true, force: true });
|
|
7516
7658
|
}
|
|
7517
7659
|
if (preserved.length > 0) {
|
|
7518
|
-
console.log(`\u2713
|
|
7660
|
+
console.log(`\u2713 removed Synkro config from ${SYNKRO_DIR5} (kept your scan data: ${preserved.join(", ")})`);
|
|
7661
|
+
console.log(" run `synkro uninstall --purge` to delete that too");
|
|
7519
7662
|
} else {
|
|
7520
|
-
console.log(`\u2713
|
|
7663
|
+
console.log(`\u2713 removed ${SYNKRO_DIR5}`);
|
|
7521
7664
|
}
|
|
7522
|
-
} else {
|
|
7523
|
-
console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
|
|
7524
7665
|
}
|
|
7525
|
-
} else
|
|
7526
|
-
console.log(
|
|
7666
|
+
} else {
|
|
7667
|
+
console.log(`\xB7 ${SYNKRO_DIR5} already gone`);
|
|
7527
7668
|
}
|
|
7528
|
-
console.log("\nSynkro
|
|
7669
|
+
console.log(purge ? "\nSynkro fully removed \u2014 this machine is clean, as if it was never installed." : "\nSynkro uninstalled. Your scan data is preserved.");
|
|
7529
7670
|
}
|
|
7530
7671
|
var SYNKRO_DIR5;
|
|
7531
7672
|
var init_disconnect = __esm({
|
|
@@ -7722,7 +7863,8 @@ var lifecycle_exports = {};
|
|
|
7722
7863
|
__export(lifecycle_exports, {
|
|
7723
7864
|
restartCommand: () => restartCommand,
|
|
7724
7865
|
startCommand: () => startCommand,
|
|
7725
|
-
stopCommand: () => stopCommand
|
|
7866
|
+
stopCommand: () => stopCommand,
|
|
7867
|
+
updateCommand: () => updateCommand
|
|
7726
7868
|
});
|
|
7727
7869
|
async function stopCommand() {
|
|
7728
7870
|
assertDockerAvailable();
|
|
@@ -7758,6 +7900,26 @@ Start failed: ${result.error}`);
|
|
|
7758
7900
|
}
|
|
7759
7901
|
console.log("\nServer is running.");
|
|
7760
7902
|
}
|
|
7903
|
+
async function updateCommand() {
|
|
7904
|
+
assertDockerAvailable();
|
|
7905
|
+
const cfg = readContainerConfig();
|
|
7906
|
+
if (!cfg) {
|
|
7907
|
+
console.error("No synkro-server container found. Run `synkro install` first.");
|
|
7908
|
+
process.exit(1);
|
|
7909
|
+
}
|
|
7910
|
+
const claudeWorkers = cfg.claudeWorkers ?? 8;
|
|
7911
|
+
const cursorWorkers = cfg.cursorWorkers ?? 0;
|
|
7912
|
+
console.log("Synkro: updating to the latest container image");
|
|
7913
|
+
console.log(` preserving pool: ${claudeWorkers} claude + ${cursorWorkers} cursor worker(s)
|
|
7914
|
+
`);
|
|
7915
|
+
await dockerUpdate({ claudeWorkers, cursorWorkers, connectedRepo: cfg.connectedRepo });
|
|
7916
|
+
const ready = await waitForContainerReady(9e4);
|
|
7917
|
+
if (!ready) {
|
|
7918
|
+
console.error("\n\u26A0 container did not pass its health check within 90s \u2014 check: docker logs synkro-server");
|
|
7919
|
+
process.exit(1);
|
|
7920
|
+
}
|
|
7921
|
+
console.log("\nSynkro updated \u2014 now running the latest version.");
|
|
7922
|
+
}
|
|
7761
7923
|
async function restartCommand(rest = []) {
|
|
7762
7924
|
assertDockerAvailable();
|
|
7763
7925
|
const cfg = resolveWorkerConfig(rest);
|
|
@@ -7814,7 +7976,7 @@ var args = process.argv.slice(2);
|
|
|
7814
7976
|
var cmd = args[0] || "";
|
|
7815
7977
|
var subArgs = args.slice(1);
|
|
7816
7978
|
function printVersion() {
|
|
7817
|
-
console.log("1.6.
|
|
7979
|
+
console.log("1.6.7");
|
|
7818
7980
|
}
|
|
7819
7981
|
function printHelp() {
|
|
7820
7982
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -7824,10 +7986,11 @@ Usage:
|
|
|
7824
7986
|
|
|
7825
7987
|
Commands:
|
|
7826
7988
|
install [--force] Install or update Synkro
|
|
7827
|
-
uninstall [--purge] Remove Synkro
|
|
7989
|
+
uninstall [--purge] Remove Synkro (keeps scan data; --purge wipes that too)
|
|
7828
7990
|
stop Gracefully stop the server (snapshot + checkpoint)
|
|
7829
7991
|
start [opts] Start the server (with pgdata integrity check)
|
|
7830
7992
|
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
7993
|
+
update Pull the latest container image and safely restart
|
|
7831
7994
|
version Show version
|
|
7832
7995
|
|
|
7833
7996
|
start/restart opts (recreate the worker pool):
|
|
@@ -7886,6 +8049,11 @@ async function main() {
|
|
|
7886
8049
|
await restartCommand2(args.slice(1));
|
|
7887
8050
|
break;
|
|
7888
8051
|
}
|
|
8052
|
+
case "update": {
|
|
8053
|
+
const { updateCommand: updateCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
8054
|
+
await updateCommand2();
|
|
8055
|
+
break;
|
|
8056
|
+
}
|
|
7889
8057
|
default: {
|
|
7890
8058
|
console.error(`Unknown command: ${cmd}`);
|
|
7891
8059
|
printHelp();
|