@synkro-sh/cli 1.6.34 → 1.6.36

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 CHANGED
@@ -1198,6 +1198,7 @@ export interface SynkroFileConfig {
1198
1198
  version: number;
1199
1199
  harness: ('claude-code' | 'cursor')[];
1200
1200
  grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
1201
+ workers: { claude?: number; cursor?: number };
1201
1202
  ruleset: string;
1202
1203
  skills: string[];
1203
1204
  scanning: { cwe: boolean; cve: boolean };
@@ -1207,11 +1208,60 @@ const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
1207
1208
  version: 1,
1208
1209
  harness: ['claude-code', 'cursor'],
1209
1210
  grader: { pool: 'auto' },
1211
+ workers: {},
1210
1212
  ruleset: 'default',
1211
1213
  skills: [],
1212
1214
  scanning: { cwe: true, cve: true },
1213
1215
  };
1214
1216
 
1217
+ function parseSynkroYaml(raw: string): Record<string, any> {
1218
+ const result: Record<string, any> = {};
1219
+ const lines = raw.split('\\n');
1220
+ let currentKey = '';
1221
+ let currentObj: Record<string, any> | null = null;
1222
+ let currentArr: string[] | null = null;
1223
+
1224
+ for (const line of lines) {
1225
+ if (!line.trim() || line.trim().startsWith('#')) continue;
1226
+
1227
+ if (line.match(/^\\S/) && line.includes(':')) {
1228
+ if (currentObj && currentKey) result[currentKey] = currentObj;
1229
+ if (currentArr && currentKey) result[currentKey] = currentArr;
1230
+ currentObj = null;
1231
+ currentArr = null;
1232
+
1233
+ const colonIdx = line.indexOf(':');
1234
+ const key = line.slice(0, colonIdx).trim();
1235
+ const val = line.slice(colonIdx + 1).trim();
1236
+ currentKey = key;
1237
+
1238
+ if (val) {
1239
+ if (val === '[]') result[key] = [];
1240
+ else if (val === 'true') result[key] = true;
1241
+ else if (val === 'false') result[key] = false;
1242
+ else if (/^\\d+$/.test(val)) result[key] = parseInt(val, 10);
1243
+ else result[key] = val;
1244
+ currentKey = '';
1245
+ }
1246
+ } else if (line.match(/^ - /)) {
1247
+ if (!currentArr) currentArr = [];
1248
+ currentArr.push(line.replace(/^ - /, '').trim());
1249
+ } else if (line.match(/^ \\S/) && line.includes(':')) {
1250
+ if (!currentObj) currentObj = {};
1251
+ const colonIdx = line.indexOf(':');
1252
+ const k = line.slice(0, colonIdx).trim();
1253
+ const v = line.slice(colonIdx + 1).trim();
1254
+ if (v === 'true') currentObj[k] = true;
1255
+ else if (v === 'false') currentObj[k] = false;
1256
+ else if (/^\\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
1257
+ else currentObj[k] = v;
1258
+ }
1259
+ }
1260
+ if (currentObj && currentKey) result[currentKey] = currentObj;
1261
+ if (currentArr && currentKey) result[currentKey] = currentArr;
1262
+ return result;
1263
+ }
1264
+
1215
1265
  let _synkroFileCache: SynkroFileConfig | undefined;
1216
1266
 
1217
1267
  export function loadSynkroFile(cwd?: string): SynkroFileConfig {
@@ -1221,7 +1271,8 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
1221
1271
  const fp = root + '/.synkro';
1222
1272
  try {
1223
1273
  if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
1224
- const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
1274
+ const raw = readFileSync(fp, 'utf-8');
1275
+ const parsed = raw.trimStart().startsWith('{') ? JSON.parse(raw) : parseSynkroYaml(raw);
1225
1276
  const validHarness = ['claude-code', 'cursor'] as const;
1226
1277
  const harness = Array.isArray(parsed.harness)
1227
1278
  ? parsed.harness.filter((h: string) => validHarness.includes(h as any))
@@ -1233,6 +1284,10 @@ export function loadSynkroFile(cwd?: string): SynkroFileConfig {
1233
1284
  pool: ['auto', 'claude', 'cursor'].includes(parsed.grader?.pool) ? parsed.grader.pool : 'auto',
1234
1285
  mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
1235
1286
  },
1287
+ workers: {
1288
+ ...(typeof parsed.workers?.claude === 'number' ? { claude: parsed.workers.claude } : {}),
1289
+ ...(typeof parsed.workers?.cursor === 'number' ? { cursor: parsed.workers.cursor } : {}),
1290
+ },
1236
1291
  ruleset: typeof parsed.ruleset === 'string' ? parsed.ruleset : 'default',
1237
1292
  skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s: unknown) => typeof s === 'string') : [],
1238
1293
  scanning: {
@@ -7108,18 +7163,64 @@ function normalizeProvider(p) {
7108
7163
  if (v === "cursor") return "cursor";
7109
7164
  return null;
7110
7165
  }
7111
- function readSynkroFilePool() {
7166
+ function parseSynkroYaml(raw) {
7167
+ const result = {};
7168
+ const lines = raw.split("\n");
7169
+ let currentKey = "";
7170
+ let currentObj = null;
7171
+ let currentArr = null;
7172
+ for (const line of lines) {
7173
+ if (!line.trim() || line.trim().startsWith("#")) continue;
7174
+ if (/^\S/.test(line) && line.includes(":")) {
7175
+ if (currentObj && currentKey) result[currentKey] = currentObj;
7176
+ if (currentArr && currentKey) result[currentKey] = currentArr;
7177
+ currentObj = null;
7178
+ currentArr = null;
7179
+ const ci = line.indexOf(":");
7180
+ const key = line.slice(0, ci).trim();
7181
+ const val = line.slice(ci + 1).trim();
7182
+ currentKey = key;
7183
+ if (val) {
7184
+ if (val === "[]") result[key] = [];
7185
+ else if (val === "true") result[key] = true;
7186
+ else if (val === "false") result[key] = false;
7187
+ else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
7188
+ else result[key] = val;
7189
+ currentKey = "";
7190
+ }
7191
+ } else if (/^ - /.test(line)) {
7192
+ if (!currentArr) currentArr = [];
7193
+ currentArr.push(line.replace(/^ - /, "").trim());
7194
+ } else if (/^ \S/.test(line) && line.includes(":")) {
7195
+ if (!currentObj) currentObj = {};
7196
+ const ci = line.indexOf(":");
7197
+ const k = line.slice(0, ci).trim();
7198
+ const v = line.slice(ci + 1).trim();
7199
+ if (v === "true") currentObj[k] = true;
7200
+ else if (v === "false") currentObj[k] = false;
7201
+ else if (/^\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
7202
+ else currentObj[k] = v;
7203
+ }
7204
+ }
7205
+ if (currentObj && currentKey) result[currentKey] = currentObj;
7206
+ if (currentArr && currentKey) result[currentKey] = currentArr;
7207
+ return result;
7208
+ }
7209
+ function readSynkroFileConfig() {
7112
7210
  try {
7113
7211
  const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
7114
- if (!root) return "auto";
7212
+ if (!root) return { pool: "auto" };
7115
7213
  const fp = join6(root, ".synkro");
7116
- if (!existsSync8(fp)) return "auto";
7117
- const parsed = JSON.parse(readFileSync7(fp, "utf-8"));
7118
- const pool = parsed?.grader?.pool;
7119
- if (pool === "cursor" || pool === "claude") return pool;
7214
+ if (!existsSync8(fp)) return { pool: "auto" };
7215
+ const raw = readFileSync7(fp, "utf-8");
7216
+ const parsed = raw.trimStart().startsWith("{") ? JSON.parse(raw) : parseSynkroYaml(raw);
7217
+ const pool = ["auto", "claude", "cursor"].includes(parsed?.grader?.pool) ? parsed.grader.pool : "auto";
7218
+ const cw = typeof parsed?.workers?.claude === "number" ? Math.max(0, Math.floor(parsed.workers.claude)) : void 0;
7219
+ const curw = typeof parsed?.workers?.cursor === "number" ? Math.max(0, Math.floor(parsed.workers.cursor)) : void 0;
7220
+ return { pool, claudeWorkers: cw, cursorWorkers: curw };
7120
7221
  } catch {
7121
7222
  }
7122
- return "auto";
7223
+ return { pool: "auto" };
7123
7224
  }
7124
7225
  function resolveWorkerConfig(rest) {
7125
7226
  let workers = 8;
@@ -7154,10 +7255,15 @@ function resolveWorkerConfig(rest) {
7154
7255
  workers = Math.min(workers, 64);
7155
7256
  let provs = providers;
7156
7257
  if (provs.length === 0) {
7157
- const synkroPool = readSynkroFilePool();
7158
- if (synkroPool === "cursor") {
7258
+ const sc = readSynkroFileConfig();
7259
+ if (sc.claudeWorkers != null || sc.cursorWorkers != null) {
7260
+ const cw = sc.claudeWorkers || 0;
7261
+ const curw = sc.cursorWorkers || 0;
7262
+ if (cw + curw > 0) return { claudeWorkers: cw, cursorWorkers: curw, explicit };
7263
+ }
7264
+ if (sc.pool === "cursor") {
7159
7265
  provs = ["cursor"];
7160
- } else if (synkroPool === "claude") {
7266
+ } else if (sc.pool === "claude") {
7161
7267
  provs = ["claude_code"];
7162
7268
  } else {
7163
7269
  provs = detectAgents().map((a) => a.kind);
@@ -8178,7 +8284,7 @@ function writeConfigEnv(opts) {
8178
8284
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
8179
8285
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
8180
8286
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
8181
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.34")}`
8287
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.36")}`
8182
8288
  ];
8183
8289
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
8184
8290
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -8586,19 +8692,32 @@ async function installCommand(opts = {}) {
8586
8692
  await promptCursorApiKey(opts);
8587
8693
  }
8588
8694
  console.log("Installing Synkro server container...");
8589
- const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
8590
- const synkroFilePool = readSynkroFilePool2();
8591
- let providers = [];
8592
- if (synkroFilePool === "cursor") {
8593
- providers = ["cursor"];
8594
- } else if (synkroFilePool === "claude") {
8595
- providers = ["claude_code"];
8695
+ const sf = readFullSynkroFile();
8696
+ let claudeWorkers;
8697
+ let cursorWorkers;
8698
+ if (sf && (sf.workers.claude != null || sf.workers.cursor != null)) {
8699
+ claudeWorkers = Math.max(0, Math.floor(sf.workers.claude || 0));
8700
+ cursorWorkers = Math.max(0, Math.floor(sf.workers.cursor || 0));
8701
+ if (claudeWorkers + cursorWorkers === 0) {
8702
+ claudeWorkers = 0;
8703
+ cursorWorkers = 8;
8704
+ }
8705
+ console.log(` .synkro: explicit workers \u2014 ${claudeWorkers} claude + ${cursorWorkers} cursor`);
8596
8706
  } else {
8597
- if (hasClaudeCode) providers.push("claude_code");
8598
- if (hasCursor) providers.push("cursor");
8707
+ const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
8708
+ const synkroFilePool = sf?.grader?.pool || readSynkroFilePool();
8709
+ let providers = [];
8710
+ if (synkroFilePool === "cursor") {
8711
+ providers = ["cursor"];
8712
+ } else if (synkroFilePool === "claude") {
8713
+ providers = ["claude_code"];
8714
+ } else {
8715
+ if (hasClaudeCode) providers.push("claude_code");
8716
+ if (hasCursor) providers.push("cursor");
8717
+ }
8718
+ ({ claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers));
8719
+ if (synkroFilePool !== "auto") console.log(` .synkro: grader pool set to ${synkroFilePool}`);
8599
8720
  }
8600
- const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
8601
- if (synkroFilePool !== "auto") console.log(` .synkro: grader pool set to ${synkroFilePool}`);
8602
8721
  console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
8603
8722
  const connectedRepo = detectGitRepo2() || void 0;
8604
8723
  const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
@@ -8704,6 +8823,52 @@ async function installCommand(opts = {}) {
8704
8823
  }
8705
8824
  console.log("\u2713 Synkro installed.");
8706
8825
  }
8826
+ function parseSynkroYaml2(raw) {
8827
+ const result = {};
8828
+ const lines = raw.split("\n");
8829
+ let currentKey = "";
8830
+ let currentObj = null;
8831
+ let currentArr = null;
8832
+ for (const line of lines) {
8833
+ if (!line.trim() || line.trim().startsWith("#")) continue;
8834
+ if (/^\S/.test(line) && line.includes(":")) {
8835
+ if (currentObj && currentKey) result[currentKey] = currentObj;
8836
+ if (currentArr && currentKey) result[currentKey] = currentArr;
8837
+ currentObj = null;
8838
+ currentArr = null;
8839
+ const colonIdx = line.indexOf(":");
8840
+ const key = line.slice(0, colonIdx).trim();
8841
+ const val = line.slice(colonIdx + 1).trim();
8842
+ currentKey = key;
8843
+ if (val) {
8844
+ if (val === "[]") result[key] = [];
8845
+ else if (val === "true") result[key] = true;
8846
+ else if (val === "false") result[key] = false;
8847
+ else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
8848
+ else result[key] = val;
8849
+ currentKey = "";
8850
+ }
8851
+ } else if (/^ - /.test(line)) {
8852
+ if (!currentArr) currentArr = [];
8853
+ currentArr.push(line.replace(/^ - /, "").trim());
8854
+ } else if (/^ \S/.test(line) && line.includes(":")) {
8855
+ if (!currentObj) currentObj = {};
8856
+ const colonIdx = line.indexOf(":");
8857
+ const k = line.slice(0, colonIdx).trim();
8858
+ const v = line.slice(colonIdx + 1).trim();
8859
+ if (v === "true") currentObj[k] = true;
8860
+ else if (v === "false") currentObj[k] = false;
8861
+ else if (/^\d+$/.test(v)) currentObj[k] = parseInt(v, 10);
8862
+ else currentObj[k] = v;
8863
+ }
8864
+ }
8865
+ if (currentObj && currentKey) result[currentKey] = currentObj;
8866
+ if (currentArr && currentKey) result[currentKey] = currentArr;
8867
+ return result;
8868
+ }
8869
+ function parseSynkroFileRaw(content) {
8870
+ return content.trimStart().startsWith("{") ? JSON.parse(content) : parseSynkroYaml2(content);
8871
+ }
8707
8872
  function writeSynkroFileIfMissing(opts) {
8708
8873
  try {
8709
8874
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
@@ -8723,28 +8888,38 @@ function writeSynkroFileIfMissing(opts) {
8723
8888
  harness.push("cursor");
8724
8889
  if (!opts.hasClaudeCode) pool = "cursor";
8725
8890
  }
8726
- const config = {
8727
- version: 1,
8728
- harness,
8729
- grader: {
8730
- pool,
8731
- mode: opts.gradingMode === "byok" ? "byok" : "local"
8732
- },
8733
- ruleset: "default",
8734
- scanning: { cwe: true, cve: true }
8735
- };
8736
- writeFileSync7(fp, JSON.stringify(config, null, 2) + "\n", "utf-8");
8737
- console.log(` .synkro: wrote ${fp} (pool=${pool}, mode=${config.grader.mode})`);
8891
+ const mode = opts.gradingMode === "byok" ? "byok" : "local";
8892
+ const yaml = [
8893
+ "version: 1",
8894
+ "",
8895
+ "harness:",
8896
+ ...harness.map((h) => ` - ${h}`),
8897
+ "",
8898
+ "grader:",
8899
+ ` pool: ${pool}`,
8900
+ ` mode: ${mode}`,
8901
+ "",
8902
+ "ruleset: default",
8903
+ "",
8904
+ "skills: []",
8905
+ "",
8906
+ "scanning:",
8907
+ " cwe: true",
8908
+ " cve: true",
8909
+ ""
8910
+ ].join("\n");
8911
+ writeFileSync7(fp, yaml, "utf-8");
8912
+ console.log(` .synkro: wrote ${fp} (pool=${pool}, mode=${mode})`);
8738
8913
  } catch {
8739
8914
  }
8740
8915
  }
8741
- function readSynkroFilePool2() {
8916
+ function readSynkroFilePool() {
8742
8917
  try {
8743
8918
  const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
8744
8919
  if (!root) return "auto";
8745
8920
  const fp = join8(root, ".synkro");
8746
8921
  if (!existsSync10(fp)) return "auto";
8747
- const parsed = JSON.parse(readFileSync9(fp, "utf-8"));
8922
+ const parsed = parseSynkroFileRaw(readFileSync9(fp, "utf-8"));
8748
8923
  const pool = parsed?.grader?.pool;
8749
8924
  if (pool === "cursor" || pool === "claude") return pool;
8750
8925
  } catch {
@@ -8757,7 +8932,7 @@ function readFullSynkroFile() {
8757
8932
  if (!root) return null;
8758
8933
  const fp = join8(root, ".synkro");
8759
8934
  if (!existsSync10(fp)) return null;
8760
- const parsed = JSON.parse(readFileSync9(fp, "utf-8"));
8935
+ const parsed = parseSynkroFileRaw(readFileSync9(fp, "utf-8"));
8761
8936
  const valid = ["claude-code", "cursor"];
8762
8937
  const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
8763
8938
  return {
@@ -8766,6 +8941,10 @@ function readFullSynkroFile() {
8766
8941
  pool: ["auto", "claude", "cursor"].includes(parsed.grader?.pool) ? parsed.grader.pool : "auto",
8767
8942
  mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
8768
8943
  },
8944
+ workers: {
8945
+ ...typeof parsed.workers?.claude === "number" ? { claude: parsed.workers.claude } : {},
8946
+ ...typeof parsed.workers?.cursor === "number" ? { cursor: parsed.workers.cursor } : {}
8947
+ },
8769
8948
  ruleset: parsed.ruleset || "default",
8770
8949
  skills: Array.isArray(parsed.skills) ? parsed.skills.filter((s) => typeof s === "string" && s.endsWith(".md")) : [],
8771
8950
  scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false },
@@ -8843,6 +9022,11 @@ function reconcileHarness() {
8843
9022
  if (uninstallCursorHooks(cursorHooks)) console.log(" \u2717 Cursor hooks removed");
8844
9023
  if (uninstallCursorMcpConfig()) console.log(" \u2717 Cursor MCP removed");
8845
9024
  }
9025
+ if (sf.workers.claude != null || sf.workers.cursor != null) {
9026
+ const cw = Math.max(0, Math.floor(sf.workers.claude || 0));
9027
+ const curw = Math.max(0, Math.floor(sf.workers.cursor || 0));
9028
+ if (cw + curw > 0) return { claudeWorkers: cw, cursorWorkers: curw };
9029
+ }
8846
9030
  const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
8847
9031
  const providers = [];
8848
9032
  if (sf.grader.pool === "cursor") {
@@ -11143,7 +11327,7 @@ var args = process.argv.slice(2);
11143
11327
  var cmd = args[0] || "";
11144
11328
  var subArgs = args.slice(1);
11145
11329
  function printVersion() {
11146
- console.log("1.6.34");
11330
+ console.log("1.6.36");
11147
11331
  }
11148
11332
  function printHelp2() {
11149
11333
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents