@synkro-sh/cli 1.2.6 → 1.3.3
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 +1428 -363
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
4
14
|
var __esm = (fn, res) => function __init() {
|
|
5
15
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
16
|
};
|
|
17
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
18
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
19
|
+
};
|
|
7
20
|
var __export = (target, all) => {
|
|
8
21
|
for (var name in all)
|
|
9
22
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
23
|
};
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
26
|
+
for (let key of __getOwnPropNames(from))
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
28
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
29
|
+
}
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
33
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
34
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
35
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
36
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
37
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
38
|
+
mod
|
|
39
|
+
));
|
|
11
40
|
|
|
12
41
|
// cli/installer/agentDetect.ts
|
|
13
42
|
import { existsSync } from "fs";
|
|
@@ -96,7 +125,7 @@ function removeSynkroEntries(events, eventName) {
|
|
|
96
125
|
if (!Array.isArray(arr)) return;
|
|
97
126
|
events[eventName] = arr.filter((entry) => !isSynkroEntry(entry));
|
|
98
127
|
}
|
|
99
|
-
function installCCHooks(settingsPath,
|
|
128
|
+
function installCCHooks(settingsPath, config2) {
|
|
100
129
|
const settings = readSettings(settingsPath);
|
|
101
130
|
settings.hooks = settings.hooks ?? {};
|
|
102
131
|
removeSynkroEntries(settings.hooks, "PreToolUse");
|
|
@@ -109,11 +138,11 @@ function installCCHooks(settingsPath, config) {
|
|
|
109
138
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd ?? [];
|
|
110
139
|
settings.hooks.SessionStart = settings.hooks.SessionStart ?? [];
|
|
111
140
|
settings.hooks.PreToolUse.push({
|
|
112
|
-
matcher: "Bash",
|
|
141
|
+
matcher: "Bash|Read|Grep|Glob",
|
|
113
142
|
hooks: [
|
|
114
143
|
{
|
|
115
144
|
type: "command",
|
|
116
|
-
command:
|
|
145
|
+
command: config2.bashJudgeScriptPath,
|
|
117
146
|
timeout: 15
|
|
118
147
|
}
|
|
119
148
|
],
|
|
@@ -124,7 +153,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
124
153
|
hooks: [
|
|
125
154
|
{
|
|
126
155
|
type: "command",
|
|
127
|
-
command:
|
|
156
|
+
command: config2.editPrecheckScriptPath,
|
|
128
157
|
timeout: 15
|
|
129
158
|
}
|
|
130
159
|
],
|
|
@@ -135,7 +164,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
135
164
|
hooks: [
|
|
136
165
|
{
|
|
137
166
|
type: "command",
|
|
138
|
-
command:
|
|
167
|
+
command: config2.editCaptureScriptPath,
|
|
139
168
|
timeout: 20
|
|
140
169
|
}
|
|
141
170
|
],
|
|
@@ -146,7 +175,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
146
175
|
hooks: [
|
|
147
176
|
{
|
|
148
177
|
type: "command",
|
|
149
|
-
command:
|
|
178
|
+
command: config2.bashFollowupScriptPath
|
|
150
179
|
}
|
|
151
180
|
],
|
|
152
181
|
[SYNKRO_MARKER]: true
|
|
@@ -155,7 +184,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
155
184
|
hooks: [
|
|
156
185
|
{
|
|
157
186
|
type: "command",
|
|
158
|
-
command:
|
|
187
|
+
command: config2.stopSummaryScriptPath
|
|
159
188
|
}
|
|
160
189
|
],
|
|
161
190
|
[SYNKRO_MARKER]: true
|
|
@@ -164,7 +193,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
164
193
|
hooks: [
|
|
165
194
|
{
|
|
166
195
|
type: "command",
|
|
167
|
-
command:
|
|
196
|
+
command: config2.sessionStartScriptPath
|
|
168
197
|
}
|
|
169
198
|
],
|
|
170
199
|
[SYNKRO_MARKER]: true
|
|
@@ -232,50 +261,50 @@ function readClaudeJson() {
|
|
|
232
261
|
throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
|
|
233
262
|
}
|
|
234
263
|
}
|
|
235
|
-
function writeClaudeJsonAtomic(
|
|
264
|
+
function writeClaudeJsonAtomic(config2) {
|
|
236
265
|
mkdirSync2(dirname2(CC_CONFIG_PATH), { recursive: true });
|
|
237
266
|
const tmpPath = `${CC_CONFIG_PATH}.synkro.tmp`;
|
|
238
|
-
writeFileSync2(tmpPath, JSON.stringify(
|
|
267
|
+
writeFileSync2(tmpPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
239
268
|
renameSync2(tmpPath, CC_CONFIG_PATH);
|
|
240
269
|
}
|
|
241
270
|
function installMcpConfig(opts) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
for (const [name, entry] of Object.entries(
|
|
245
|
-
if (entry?.[SYNKRO_MARKER2] === true) delete
|
|
271
|
+
const config2 = readClaudeJson();
|
|
272
|
+
config2.mcpServers = config2.mcpServers ?? {};
|
|
273
|
+
for (const [name, entry] of Object.entries(config2.mcpServers)) {
|
|
274
|
+
if (entry?.[SYNKRO_MARKER2] === true) delete config2.mcpServers[name];
|
|
246
275
|
}
|
|
247
276
|
const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
|
|
248
|
-
|
|
277
|
+
config2.mcpServers[SYNKRO_SERVER_NAME] = {
|
|
249
278
|
type: "http",
|
|
250
279
|
url,
|
|
251
280
|
headers: { Authorization: `Bearer ${opts.bearerToken}` },
|
|
252
281
|
[SYNKRO_MARKER2]: true
|
|
253
282
|
};
|
|
254
|
-
writeClaudeJsonAtomic(
|
|
283
|
+
writeClaudeJsonAtomic(config2);
|
|
255
284
|
return { path: CC_CONFIG_PATH, url };
|
|
256
285
|
}
|
|
257
286
|
function uninstallMcpConfig() {
|
|
258
287
|
if (!existsSync3(CC_CONFIG_PATH)) return false;
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
288
|
+
const config2 = readClaudeJson();
|
|
289
|
+
if (!config2.mcpServers || Object.keys(config2.mcpServers).length === 0) return false;
|
|
261
290
|
let removed = false;
|
|
262
|
-
for (const [name, entry] of Object.entries(
|
|
291
|
+
for (const [name, entry] of Object.entries(config2.mcpServers)) {
|
|
263
292
|
if (entry?.[SYNKRO_MARKER2] === true) {
|
|
264
|
-
delete
|
|
293
|
+
delete config2.mcpServers[name];
|
|
265
294
|
removed = true;
|
|
266
295
|
}
|
|
267
296
|
}
|
|
268
297
|
if (!removed) return false;
|
|
269
|
-
if (Object.keys(
|
|
270
|
-
writeClaudeJsonAtomic(
|
|
298
|
+
if (Object.keys(config2.mcpServers).length === 0) delete config2.mcpServers;
|
|
299
|
+
writeClaudeJsonAtomic(config2);
|
|
271
300
|
return true;
|
|
272
301
|
}
|
|
273
302
|
function inspectMcpConfig() {
|
|
274
303
|
if (!existsSync3(CC_CONFIG_PATH)) {
|
|
275
304
|
return { installed: false, configPath: CC_CONFIG_PATH };
|
|
276
305
|
}
|
|
277
|
-
const
|
|
278
|
-
const entry =
|
|
306
|
+
const config2 = readClaudeJson();
|
|
307
|
+
const entry = config2.mcpServers?.[SYNKRO_SERVER_NAME];
|
|
279
308
|
if (!entry || entry[SYNKRO_MARKER2] !== true) {
|
|
280
309
|
return { installed: false, configPath: CC_CONFIG_PATH };
|
|
281
310
|
}
|
|
@@ -336,14 +365,30 @@ if [ -z "$PAYLOAD" ]; then
|
|
|
336
365
|
exit 0
|
|
337
366
|
fi
|
|
338
367
|
|
|
339
|
-
#
|
|
368
|
+
# Translate tool calls into a command string for the judge
|
|
340
369
|
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
370
|
+
case "$TOOL_NAME" in
|
|
371
|
+
Bash)
|
|
372
|
+
COMMAND=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
373
|
+
;;
|
|
374
|
+
Read)
|
|
375
|
+
FILE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
376
|
+
COMMAND="cat \${FILE_PATH}"
|
|
377
|
+
;;
|
|
378
|
+
Grep)
|
|
379
|
+
PATTERN=$(echo "$PAYLOAD" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
|
|
380
|
+
GREP_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.path // "."' 2>/dev/null)
|
|
381
|
+
COMMAND="grep -r '\${PATTERN}' \${GREP_PATH}"
|
|
382
|
+
;;
|
|
383
|
+
Glob)
|
|
384
|
+
PATTERN=$(echo "$PAYLOAD" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
|
|
385
|
+
COMMAND="find . -name '\${PATTERN}'"
|
|
386
|
+
;;
|
|
387
|
+
*)
|
|
388
|
+
echo '{}'
|
|
389
|
+
exit 0
|
|
390
|
+
;;
|
|
391
|
+
esac
|
|
347
392
|
if [ -z "$COMMAND" ]; then
|
|
348
393
|
echo '{}'
|
|
349
394
|
exit 0
|
|
@@ -364,7 +409,7 @@ TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/nul
|
|
|
364
409
|
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
365
410
|
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
366
411
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
367
|
-
TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
|
|
412
|
+
TOOL_INPUT=$(echo "$PAYLOAD" | jq -c --arg cmd "$COMMAND" '.tool_input // {} | . + {command: $cmd}' 2>/dev/null)
|
|
368
413
|
# Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
|
|
369
414
|
GIT_REPO=""
|
|
370
415
|
if command -v git >/dev/null 2>&1; then
|
|
@@ -384,11 +429,9 @@ if [ "\${SYNKRO_HEADLESS:-0}" = "1" ]; then IS_HEADLESS=1; fi
|
|
|
384
429
|
|
|
385
430
|
USER_INTENT=""
|
|
386
431
|
RECENT_USER_MESSAGES="[]"
|
|
432
|
+
RECENT_MESSAGES="[]"
|
|
387
433
|
RECENT_ACTIONS="[]"
|
|
388
434
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
389
|
-
# Last 5 user-role messages, oldest first. Lets the grader see consent
|
|
390
|
-
# that carried over from a recent prior turn \u2014 saying "i consent" two
|
|
391
|
-
# turns ago should not require re-prompting on this turn's command.
|
|
392
435
|
RECENT_USER_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
393
436
|
[.[]
|
|
394
437
|
| select(.type == "user")
|
|
@@ -399,16 +442,78 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
|
399
442
|
| select(. != null and . != "")
|
|
400
443
|
] | .[-5:]' 2>/dev/null || echo "[]")
|
|
401
444
|
USER_INTENT=$(echo "$RECENT_USER_MESSAGES" | jq -r '.[-1] // ""' 2>/dev/null || echo "")
|
|
402
|
-
#
|
|
403
|
-
|
|
445
|
+
# Interleaved assistant+user messages \u2014 lets the grader see what question
|
|
446
|
+
# each "yes" was answering (assistant text before user reply).
|
|
447
|
+
RECENT_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
404
448
|
[.[]
|
|
449
|
+
| select(.type == "assistant" or .type == "user")
|
|
450
|
+
| {
|
|
451
|
+
role: .type,
|
|
452
|
+
text: (
|
|
453
|
+
if .type == "assistant" then
|
|
454
|
+
[.message.content[]? | select(type == "object" and .type == "text") | .text // ""] | join(" ") | .[0:500]
|
|
455
|
+
else
|
|
456
|
+
(.message.content
|
|
457
|
+
| if type == "string" then .[0:500]
|
|
458
|
+
else ([.[]? | if type == "string" then . elif (type == "object" and .type == "text") then (.text // "") else "" end] | join(" ") | .[0:500])
|
|
459
|
+
end)
|
|
460
|
+
end
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
| select(.text != "" and .text != null and (.text | length) > 0)
|
|
464
|
+
] | .[-10:]' 2>/dev/null || echo "[]")
|
|
465
|
+
# Recent agent actions (last 5 tool_use blocks paired with results)
|
|
466
|
+
RECENT_ACTIONS=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
467
|
+
# tool_result blocks live in USER messages (Anthropic API format)
|
|
468
|
+
([ .[]
|
|
469
|
+
| select(.type == "user")
|
|
470
|
+
| .message.content[]?
|
|
471
|
+
| select(type == "object" and .type == "tool_result")
|
|
472
|
+
| { (.tool_use_id): (.content // "" | tostring | .[0:300]) }
|
|
473
|
+
] | add // {}) as $results
|
|
474
|
+
|
|
|
475
|
+
[ .[]
|
|
405
476
|
| select(.type == "assistant")
|
|
406
477
|
| .message.content[]?
|
|
407
478
|
| select(.type == "tool_use")
|
|
408
|
-
| {
|
|
479
|
+
| {
|
|
480
|
+
tool: .name,
|
|
481
|
+
input: (.input // {} | tostring | .[0:200]),
|
|
482
|
+
result: ($results[.id] // null)
|
|
483
|
+
}
|
|
409
484
|
] | .[-5:]' 2>/dev/null || echo "[]")
|
|
410
485
|
fi
|
|
411
486
|
|
|
487
|
+
CC_MODEL=""
|
|
488
|
+
CC_USAGE="{}"
|
|
489
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
490
|
+
_LAST_ASSISTANT=$(tail -50 "$TRANSCRIPT_PATH" | jq -c 'select(.type == "assistant")' 2>/dev/null | tail -1)
|
|
491
|
+
if [ -n "$_LAST_ASSISTANT" ]; then
|
|
492
|
+
CC_MODEL=$(echo "$_LAST_ASSISTANT" | jq -r '.message.model // empty' 2>/dev/null)
|
|
493
|
+
CC_USAGE=$(echo "$_LAST_ASSISTANT" | jq -c '{
|
|
494
|
+
input_tokens: .message.usage.input_tokens,
|
|
495
|
+
output_tokens: .message.usage.output_tokens,
|
|
496
|
+
cache_creation_input_tokens: .message.usage.cache_creation_input_tokens,
|
|
497
|
+
cache_read_input_tokens: .message.usage.cache_read_input_tokens,
|
|
498
|
+
service_tier: .message.usage.service_tier,
|
|
499
|
+
speed: .message.usage.speed
|
|
500
|
+
}' 2>/dev/null || echo "{}")
|
|
501
|
+
fi
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
# Extract session summary from CC compaction (free broad context)
|
|
505
|
+
SESSION_SUMMARY=""
|
|
506
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
507
|
+
_SUMMARY_LINE=$(grep -n '"This session is being continued' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 | cut -d: -f1)
|
|
508
|
+
if [ -n "$_SUMMARY_LINE" ]; then
|
|
509
|
+
SESSION_SUMMARY=$(sed -n "\${_SUMMARY_LINE}p" "$TRANSCRIPT_PATH" | jq -r '
|
|
510
|
+
.message.content
|
|
511
|
+
| if type == "string" then .[0:2000]
|
|
512
|
+
else ([.[]? | if type == "string" then . elif (type == "object" and .type == "text") then (.text // "") else "" end] | join(" ") | .[0:2000])
|
|
513
|
+
end' 2>/dev/null || echo "")
|
|
514
|
+
fi
|
|
515
|
+
fi
|
|
516
|
+
|
|
412
517
|
# Build POST body \u2014 always emit all fields (use null for empty optionals)
|
|
413
518
|
# Earlier version used \`select(length > 0)\` which made the entire object
|
|
414
519
|
# evaluate to nothing when any optional was empty. Don't do that.
|
|
@@ -416,21 +521,29 @@ BODY=$(jq -n \\
|
|
|
416
521
|
--argjson tool_input "$TOOL_INPUT" \\
|
|
417
522
|
--arg user_intent "$USER_INTENT" \\
|
|
418
523
|
--argjson recent_user_messages "$RECENT_USER_MESSAGES" \\
|
|
524
|
+
--argjson recent_messages "$RECENT_MESSAGES" \\
|
|
419
525
|
--argjson recent_actions "$RECENT_ACTIONS" \\
|
|
420
526
|
--arg session_id "$SESSION_ID" \\
|
|
421
527
|
--arg tool_use_id "$TOOL_USE_ID" \\
|
|
422
528
|
--arg cwd "$CWD" \\
|
|
423
529
|
--arg repo "$GIT_REPO" \\
|
|
530
|
+
--arg cc_model "$CC_MODEL" \\
|
|
531
|
+
--argjson cc_usage "$CC_USAGE" \\
|
|
532
|
+
--arg session_summary "$SESSION_SUMMARY" \\
|
|
424
533
|
'{
|
|
425
534
|
kind: "bash_judge",
|
|
426
535
|
tool_input: $tool_input,
|
|
427
536
|
user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
|
|
428
537
|
recent_user_messages: $recent_user_messages,
|
|
538
|
+
recent_messages: $recent_messages,
|
|
429
539
|
recent_actions: $recent_actions,
|
|
430
540
|
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
431
541
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
432
542
|
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
433
|
-
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
543
|
+
repo: (if ($repo | length) > 0 then $repo else null end),
|
|
544
|
+
cc_model: (if ($cc_model | length) > 0 then $cc_model else null end),
|
|
545
|
+
cc_usage: $cc_usage,
|
|
546
|
+
session_summary: (if ($session_summary | length) > 0 then $session_summary else null end)
|
|
434
547
|
}')
|
|
435
548
|
|
|
436
549
|
# Helper: refresh JWT via /api/auth/refresh and rewrite credentials.json.
|
|
@@ -546,23 +659,15 @@ if [ -z "$VERDICT" ]; then
|
|
|
546
659
|
fi
|
|
547
660
|
|
|
548
661
|
# Parse verdict \u2014 fail open on any parse error
|
|
549
|
-
SEVERITY=$(echo "$VERDICT" | jq -r '.severity // "
|
|
662
|
+
SEVERITY=$(echo "$VERDICT" | jq -r '.severity // "audit"' 2>/dev/null)
|
|
550
663
|
VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
|
|
551
664
|
REASONING=$(echo "$VERDICT" | jq -r '.reasoning // "matched dangerous-verb regex"' 2>/dev/null)
|
|
552
665
|
ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
|
|
553
666
|
CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/null)
|
|
554
667
|
|
|
555
668
|
# Severity-driven surfacing:
|
|
556
|
-
#
|
|
557
|
-
#
|
|
558
|
-
# medium \u2192 silent allow (echo {}) \u2014 Cerebras saw it, judged the
|
|
559
|
-
# intent fits, no surface
|
|
560
|
-
# low \u2192 silent allow (echo {}) \u2014 same
|
|
561
|
-
#
|
|
562
|
-
# The grader is fully context-aware now (intent + recent_actions + shape
|
|
563
|
-
# labels), so its severity grade is trustworthy. Low/medium decisions don't
|
|
564
|
-
# need to interrupt the user \u2014 surfacing them creates alert fatigue and
|
|
565
|
-
# trains the user to click-through on warnings that turn out to be benign.
|
|
669
|
+
# block \u2192 permissionDecision: "ask" (interactive) or "deny" (headless)
|
|
670
|
+
# audit \u2192 silent allow \u2014 logged but no interruption
|
|
566
671
|
|
|
567
672
|
ALT_SUFFIX=""
|
|
568
673
|
if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
|
|
@@ -570,26 +675,9 @@ if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
|
|
|
570
675
|
fi
|
|
571
676
|
|
|
572
677
|
case "$SEVERITY" in
|
|
573
|
-
|
|
574
|
-
synkro_log "bashGuard $CMD_SHORT \u2192 BLOCKED ($CATEGORY)"
|
|
575
|
-
PERMISSION_REASON="[synkro] BLOCKED \u2014 \${REASONING}\${ALT_SUFFIX}"
|
|
576
|
-
ADDITIONAL_CTX="Synkro safety judge (severity: critical, category: \${CATEGORY}).\${ALT_SUFFIX}"
|
|
577
|
-
jq -n \\
|
|
578
|
-
--arg ctx "$ADDITIONAL_CTX" \\
|
|
579
|
-
--arg reason "$PERMISSION_REASON" \\
|
|
580
|
-
'{
|
|
581
|
-
hookSpecificOutput: {
|
|
582
|
-
hookEventName: "PreToolUse",
|
|
583
|
-
permissionDecision: "deny",
|
|
584
|
-
permissionDecisionReason: $reason,
|
|
585
|
-
additionalContext: $ctx
|
|
586
|
-
}
|
|
587
|
-
}'
|
|
588
|
-
;;
|
|
589
|
-
high)
|
|
590
|
-
synkro_log "bashGuard $CMD_SHORT \u2192 FLAGGED ($CATEGORY)"
|
|
678
|
+
block)
|
|
591
679
|
PERMISSION_REASON="[synkro] \${REASONING}\${ALT_SUFFIX}"
|
|
592
|
-
ADDITIONAL_CTX="Synkro safety judge (severity:
|
|
680
|
+
ADDITIONAL_CTX="Synkro safety judge (severity: \${SEVERITY}, category: \${CATEGORY}). Reasoning: \${REASONING}.\${ALT_SUFFIX}"
|
|
593
681
|
if [ "$IS_HEADLESS" = "1" ]; then DECISION="deny"; else DECISION="ask"; fi
|
|
594
682
|
jq -n \\
|
|
595
683
|
--arg ctx "$ADDITIONAL_CTX" \\
|
|
@@ -604,7 +692,7 @@ case "$SEVERITY" in
|
|
|
604
692
|
}
|
|
605
693
|
}'
|
|
606
694
|
;;
|
|
607
|
-
|
|
695
|
+
audit)
|
|
608
696
|
synkro_log "bashGuard $CMD_SHORT \u2192 pass ($CATEGORY): $REASONING"
|
|
609
697
|
case "$CATEGORY" in
|
|
610
698
|
trivial_utility)
|
|
@@ -615,6 +703,21 @@ case "$SEVERITY" in
|
|
|
615
703
|
jq -n --arg m "[synkro] bashGuard \u2192 pass ($CATEGORY): $REASONING" '{systemMessage: $m}' ;;
|
|
616
704
|
esac
|
|
617
705
|
;;
|
|
706
|
+
*)
|
|
707
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 UNEXPECTED SEVERITY ($SEVERITY), blocking by default"
|
|
708
|
+
if [ "$IS_HEADLESS" = "1" ]; then DECISION="deny"; else DECISION="ask"; fi
|
|
709
|
+
jq -n \\
|
|
710
|
+
--arg decision "$DECISION" \\
|
|
711
|
+
--arg reason "[synkro] unexpected severity '\${SEVERITY}' \u2014 blocking by default. Please email team@synkro.sh to report this issue." \\
|
|
712
|
+
'{
|
|
713
|
+
hookSpecificOutput: {
|
|
714
|
+
hookEventName: "PreToolUse",
|
|
715
|
+
permissionDecision: $decision,
|
|
716
|
+
permissionDecisionReason: $reason,
|
|
717
|
+
additionalContext: "Synkro safety judge returned an unexpected severity value. This command has been blocked as a precaution. Please email team@synkro.sh with details of the command you were running."
|
|
718
|
+
}
|
|
719
|
+
}'
|
|
720
|
+
;;
|
|
618
721
|
esac
|
|
619
722
|
|
|
620
723
|
exit 0
|
|
@@ -983,8 +1086,14 @@ if [ "$DECISION" = "deny" ]; then
|
|
|
983
1086
|
synkro_log "editGuard $FILE_SHORT \u2192 BLOCKED: $DENY_REASON"
|
|
984
1087
|
echo "$RESP"
|
|
985
1088
|
else
|
|
986
|
-
|
|
987
|
-
|
|
1089
|
+
VERDICT_REASON=$(echo "$RESP" | jq -r '.reason // empty' 2>/dev/null)
|
|
1090
|
+
if [ -n "$VERDICT_REASON" ]; then
|
|
1091
|
+
synkro_log "editGuard $FILE_SHORT \u2192 pass: $VERDICT_REASON"
|
|
1092
|
+
RESP_WITH_MSG=$(echo "$RESP" | jq --arg m "[synkro] editGuard $FILE_SHORT \u2192 pass: $VERDICT_REASON" '. + {systemMessage: $m}')
|
|
1093
|
+
else
|
|
1094
|
+
synkro_log "editGuard $FILE_SHORT \u2192 pass"
|
|
1095
|
+
RESP_WITH_MSG=$(echo "$RESP" | jq --arg m "[synkro] editGuard $FILE_SHORT \u2192 pass" '. + {systemMessage: $m}')
|
|
1096
|
+
fi
|
|
988
1097
|
echo "$RESP_WITH_MSG"
|
|
989
1098
|
fi
|
|
990
1099
|
|
|
@@ -1232,8 +1341,13 @@ if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
|
|
|
1232
1341
|
exit 0
|
|
1233
1342
|
fi
|
|
1234
1343
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1344
|
+
if [ -n "$REASON" ]; then
|
|
1345
|
+
synkro_log "editScan $BASENAME \u2192 pass ($CATEGORY): $REASON"
|
|
1346
|
+
jq -n --arg m "[synkro] editScan $BASENAME \u2192 pass ($CATEGORY): $REASON" '{systemMessage: $m}'
|
|
1347
|
+
else
|
|
1348
|
+
synkro_log "editScan $BASENAME \u2192 pass"
|
|
1349
|
+
jq -n --arg m "[synkro] editScan $BASENAME \u2192 pass" '{systemMessage: $m}'
|
|
1350
|
+
fi
|
|
1237
1351
|
exit 0
|
|
1238
1352
|
`;
|
|
1239
1353
|
CC_STOP_SUMMARY_SCRIPT = `#!/bin/bash
|
|
@@ -1335,8 +1449,17 @@ if [ -z "$CWD" ]; then
|
|
|
1335
1449
|
exit 0
|
|
1336
1450
|
fi
|
|
1337
1451
|
|
|
1452
|
+
GIT_REPO=""
|
|
1453
|
+
if command -v git >/dev/null 2>&1; then
|
|
1454
|
+
_REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
|
|
1455
|
+
if [ -n "$_REMOTE" ]; then
|
|
1456
|
+
GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
|
|
1457
|
+
fi
|
|
1458
|
+
fi
|
|
1459
|
+
|
|
1338
1460
|
RESP=$(curl -sS -G "\${GATEWAY_URL}/api/v1/cli/session-context" \\
|
|
1339
1461
|
--data-urlencode "cwd=$CWD" \\
|
|
1462
|
+
--data-urlencode "repo=$GIT_REPO" \\
|
|
1340
1463
|
-H "Authorization: Bearer $JWT" \\
|
|
1341
1464
|
--max-time 2 2>/dev/null || echo "")
|
|
1342
1465
|
|
|
@@ -2075,12 +2198,64 @@ function getAccessToken() {
|
|
|
2075
2198
|
const creds = loadCredentials();
|
|
2076
2199
|
return creds?.access_token || null;
|
|
2077
2200
|
}
|
|
2201
|
+
function isTokenExpired() {
|
|
2202
|
+
const creds = loadCredentials();
|
|
2203
|
+
if (!creds) return true;
|
|
2204
|
+
try {
|
|
2205
|
+
const decoded = jwt.decode(creds.access_token);
|
|
2206
|
+
if (!decoded?.exp) return true;
|
|
2207
|
+
const expiresAt = decoded.exp * 1e3;
|
|
2208
|
+
const buffer = 5 * 60 * 1e3;
|
|
2209
|
+
return Date.now() > expiresAt - buffer;
|
|
2210
|
+
} catch {
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async function refreshToken() {
|
|
2215
|
+
const creds = loadCredentials();
|
|
2216
|
+
if (!creds?.refresh_token) return false;
|
|
2217
|
+
try {
|
|
2218
|
+
const response = await fetch(`${SYNKRO_WEB_AUTH_URL}/api/auth/refresh`, {
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: { "Content-Type": "application/json" },
|
|
2221
|
+
body: JSON.stringify({ refresh_token: creds.refresh_token })
|
|
2222
|
+
});
|
|
2223
|
+
if (!response.ok) return false;
|
|
2224
|
+
const data = await response.json();
|
|
2225
|
+
if (data.access_token) {
|
|
2226
|
+
saveCredentials({
|
|
2227
|
+
access_token: data.access_token,
|
|
2228
|
+
refresh_token: data.refresh_token || creds.refresh_token
|
|
2229
|
+
});
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
return false;
|
|
2233
|
+
} catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
async function ensureValidToken() {
|
|
2238
|
+
if (!isAuthenticated()) return false;
|
|
2239
|
+
if (isTokenExpired()) {
|
|
2240
|
+
if (!refreshPromise) {
|
|
2241
|
+
refreshPromise = refreshToken().finally(() => {
|
|
2242
|
+
refreshPromise = null;
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
const refreshed = await refreshPromise;
|
|
2246
|
+
if (!refreshed) {
|
|
2247
|
+
clearCredentials();
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2078
2253
|
function clearCredentials() {
|
|
2079
2254
|
if (existsSync4(AUTH_FILE)) {
|
|
2080
2255
|
unlinkSync2(AUTH_FILE);
|
|
2081
2256
|
}
|
|
2082
2257
|
}
|
|
2083
|
-
var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, ERROR_HTML;
|
|
2258
|
+
var PORT, RAW_WEB_AUTH_URL, SYNKRO_WEB_AUTH_URL, AUTH_FILE, ERROR_HTML, refreshPromise;
|
|
2084
2259
|
var init_stub = __esm({
|
|
2085
2260
|
"cli/auth/stub.ts"() {
|
|
2086
2261
|
"use strict";
|
|
@@ -2128,112 +2303,913 @@ var init_stub = __esm({
|
|
|
2128
2303
|
</body>
|
|
2129
2304
|
</html>
|
|
2130
2305
|
`;
|
|
2306
|
+
refreshPromise = null;
|
|
2131
2307
|
}
|
|
2132
2308
|
});
|
|
2133
2309
|
|
|
2134
|
-
//
|
|
2135
|
-
var
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2310
|
+
// ../../node_modules/.pnpm/dotenv@17.2.4/node_modules/dotenv/package.json
|
|
2311
|
+
var require_package = __commonJS({
|
|
2312
|
+
"../../node_modules/.pnpm/dotenv@17.2.4/node_modules/dotenv/package.json"(exports, module) {
|
|
2313
|
+
module.exports = {
|
|
2314
|
+
name: "dotenv",
|
|
2315
|
+
version: "17.2.4",
|
|
2316
|
+
description: "Loads environment variables from .env file",
|
|
2317
|
+
main: "lib/main.js",
|
|
2318
|
+
types: "lib/main.d.ts",
|
|
2319
|
+
exports: {
|
|
2320
|
+
".": {
|
|
2321
|
+
types: "./lib/main.d.ts",
|
|
2322
|
+
require: "./lib/main.js",
|
|
2323
|
+
default: "./lib/main.js"
|
|
2324
|
+
},
|
|
2325
|
+
"./config": "./config.js",
|
|
2326
|
+
"./config.js": "./config.js",
|
|
2327
|
+
"./lib/env-options": "./lib/env-options.js",
|
|
2328
|
+
"./lib/env-options.js": "./lib/env-options.js",
|
|
2329
|
+
"./lib/cli-options": "./lib/cli-options.js",
|
|
2330
|
+
"./lib/cli-options.js": "./lib/cli-options.js",
|
|
2331
|
+
"./package.json": "./package.json"
|
|
2332
|
+
},
|
|
2333
|
+
scripts: {
|
|
2334
|
+
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
2335
|
+
lint: "standard",
|
|
2336
|
+
pretest: "npm run lint && npm run dts-check",
|
|
2337
|
+
test: "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
2338
|
+
"test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
2339
|
+
prerelease: "npm test",
|
|
2340
|
+
release: "standard-version"
|
|
2341
|
+
},
|
|
2342
|
+
repository: {
|
|
2343
|
+
type: "git",
|
|
2344
|
+
url: "git://github.com/motdotla/dotenv.git"
|
|
2345
|
+
},
|
|
2346
|
+
homepage: "https://github.com/motdotla/dotenv#readme",
|
|
2347
|
+
funding: "https://dotenvx.com",
|
|
2348
|
+
keywords: [
|
|
2349
|
+
"dotenv",
|
|
2350
|
+
"env",
|
|
2351
|
+
".env",
|
|
2352
|
+
"environment",
|
|
2353
|
+
"variables",
|
|
2354
|
+
"config",
|
|
2355
|
+
"settings"
|
|
2356
|
+
],
|
|
2357
|
+
readmeFilename: "README.md",
|
|
2358
|
+
license: "BSD-2-Clause",
|
|
2359
|
+
devDependencies: {
|
|
2360
|
+
"@types/node": "^18.11.3",
|
|
2361
|
+
decache: "^4.6.2",
|
|
2362
|
+
sinon: "^14.0.1",
|
|
2363
|
+
standard: "^17.0.0",
|
|
2364
|
+
"standard-version": "^9.5.0",
|
|
2365
|
+
tap: "^19.2.0",
|
|
2366
|
+
typescript: "^4.8.4"
|
|
2367
|
+
},
|
|
2368
|
+
engines: {
|
|
2369
|
+
node: ">=12"
|
|
2370
|
+
},
|
|
2371
|
+
browser: {
|
|
2372
|
+
fs: false
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2139
2376
|
});
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2377
|
+
|
|
2378
|
+
// ../../node_modules/.pnpm/dotenv@17.2.4/node_modules/dotenv/lib/main.js
|
|
2379
|
+
var require_main = __commonJS({
|
|
2380
|
+
"../../node_modules/.pnpm/dotenv@17.2.4/node_modules/dotenv/lib/main.js"(exports, module) {
|
|
2381
|
+
"use strict";
|
|
2382
|
+
var fs = __require("fs");
|
|
2383
|
+
var path = __require("path");
|
|
2384
|
+
var os = __require("os");
|
|
2385
|
+
var crypto = __require("crypto");
|
|
2386
|
+
var packageJson = require_package();
|
|
2387
|
+
var version = packageJson.version;
|
|
2388
|
+
var TIPS = [
|
|
2389
|
+
"\u{1F510} encrypt with Dotenvx: https://dotenvx.com",
|
|
2390
|
+
"\u{1F510} prevent committing .env to code: https://dotenvx.com/precommit",
|
|
2391
|
+
"\u{1F510} prevent building .env in docker: https://dotenvx.com/prebuild",
|
|
2392
|
+
"\u{1F4E1} add observability to secrets: https://dotenvx.com/ops",
|
|
2393
|
+
"\u{1F465} sync secrets across teammates & machines: https://dotenvx.com/ops",
|
|
2394
|
+
"\u{1F5C2}\uFE0F backup and recover secrets: https://dotenvx.com/ops",
|
|
2395
|
+
"\u2705 audit secrets and track compliance: https://dotenvx.com/ops",
|
|
2396
|
+
"\u{1F504} add secrets lifecycle management: https://dotenvx.com/ops",
|
|
2397
|
+
"\u{1F511} add access controls to secrets: https://dotenvx.com/ops",
|
|
2398
|
+
"\u{1F6E0}\uFE0F run anywhere with `dotenvx run -- yourcommand`",
|
|
2399
|
+
"\u2699\uFE0F specify custom .env file path with { path: '/custom/path/.env' }",
|
|
2400
|
+
"\u2699\uFE0F enable debug logging with { debug: true }",
|
|
2401
|
+
"\u2699\uFE0F override existing env vars with { override: true }",
|
|
2402
|
+
"\u2699\uFE0F suppress all logs with { quiet: true }",
|
|
2403
|
+
"\u2699\uFE0F write to custom object with { processEnv: myObject }",
|
|
2404
|
+
"\u2699\uFE0F load multiple .env files with { path: ['.env.local', '.env'] }"
|
|
2405
|
+
];
|
|
2406
|
+
function _getRandomTip() {
|
|
2407
|
+
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
2408
|
+
}
|
|
2409
|
+
function parseBoolean(value) {
|
|
2410
|
+
if (typeof value === "string") {
|
|
2411
|
+
return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
|
|
2412
|
+
}
|
|
2413
|
+
return Boolean(value);
|
|
2414
|
+
}
|
|
2415
|
+
function supportsAnsi() {
|
|
2416
|
+
return process.stdout.isTTY;
|
|
2417
|
+
}
|
|
2418
|
+
function dim(text) {
|
|
2419
|
+
return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
|
|
2420
|
+
}
|
|
2421
|
+
var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
|
|
2422
|
+
function parse(src) {
|
|
2423
|
+
const obj = {};
|
|
2424
|
+
let lines = src.toString();
|
|
2425
|
+
lines = lines.replace(/\r\n?/mg, "\n");
|
|
2426
|
+
let match;
|
|
2427
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
2428
|
+
const key = match[1];
|
|
2429
|
+
let value = match[2] || "";
|
|
2430
|
+
value = value.trim();
|
|
2431
|
+
const maybeQuote = value[0];
|
|
2432
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
|
|
2433
|
+
if (maybeQuote === '"') {
|
|
2434
|
+
value = value.replace(/\\n/g, "\n");
|
|
2435
|
+
value = value.replace(/\\r/g, "\r");
|
|
2436
|
+
}
|
|
2437
|
+
obj[key] = value;
|
|
2438
|
+
}
|
|
2439
|
+
return obj;
|
|
2440
|
+
}
|
|
2441
|
+
function _parseVault(options) {
|
|
2442
|
+
options = options || {};
|
|
2443
|
+
const vaultPath = _vaultPath(options);
|
|
2444
|
+
options.path = vaultPath;
|
|
2445
|
+
const result = DotenvModule.configDotenv(options);
|
|
2446
|
+
if (!result.parsed) {
|
|
2447
|
+
const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
2448
|
+
err.code = "MISSING_DATA";
|
|
2449
|
+
throw err;
|
|
2450
|
+
}
|
|
2451
|
+
const keys = _dotenvKey(options).split(",");
|
|
2452
|
+
const length = keys.length;
|
|
2453
|
+
let decrypted;
|
|
2454
|
+
for (let i = 0; i < length; i++) {
|
|
2455
|
+
try {
|
|
2456
|
+
const key = keys[i].trim();
|
|
2457
|
+
const attrs = _instructions(result, key);
|
|
2458
|
+
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
2459
|
+
break;
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
if (i + 1 >= length) {
|
|
2462
|
+
throw error;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return DotenvModule.parse(decrypted);
|
|
2467
|
+
}
|
|
2468
|
+
function _warn(message) {
|
|
2469
|
+
console.error(`[dotenv@${version}][WARN] ${message}`);
|
|
2470
|
+
}
|
|
2471
|
+
function _debug(message) {
|
|
2472
|
+
console.log(`[dotenv@${version}][DEBUG] ${message}`);
|
|
2473
|
+
}
|
|
2474
|
+
function _log(message) {
|
|
2475
|
+
console.log(`[dotenv@${version}] ${message}`);
|
|
2476
|
+
}
|
|
2477
|
+
function _dotenvKey(options) {
|
|
2478
|
+
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
|
2479
|
+
return options.DOTENV_KEY;
|
|
2480
|
+
}
|
|
2481
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
2482
|
+
return process.env.DOTENV_KEY;
|
|
2483
|
+
}
|
|
2484
|
+
return "";
|
|
2485
|
+
}
|
|
2486
|
+
function _instructions(result, dotenvKey) {
|
|
2487
|
+
let uri;
|
|
2488
|
+
try {
|
|
2489
|
+
uri = new URL(dotenvKey);
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
if (error.code === "ERR_INVALID_URL") {
|
|
2492
|
+
const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
|
|
2493
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
2494
|
+
throw err;
|
|
2495
|
+
}
|
|
2496
|
+
throw error;
|
|
2497
|
+
}
|
|
2498
|
+
const key = uri.password;
|
|
2499
|
+
if (!key) {
|
|
2500
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing key part");
|
|
2501
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
2502
|
+
throw err;
|
|
2503
|
+
}
|
|
2504
|
+
const environment = uri.searchParams.get("environment");
|
|
2505
|
+
if (!environment) {
|
|
2506
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
|
|
2507
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
2508
|
+
throw err;
|
|
2509
|
+
}
|
|
2510
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
|
|
2511
|
+
const ciphertext = result.parsed[environmentKey];
|
|
2512
|
+
if (!ciphertext) {
|
|
2513
|
+
const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
|
|
2514
|
+
err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
|
|
2515
|
+
throw err;
|
|
2516
|
+
}
|
|
2517
|
+
return { ciphertext, key };
|
|
2518
|
+
}
|
|
2519
|
+
function _vaultPath(options) {
|
|
2520
|
+
let possibleVaultPath = null;
|
|
2521
|
+
if (options && options.path && options.path.length > 0) {
|
|
2522
|
+
if (Array.isArray(options.path)) {
|
|
2523
|
+
for (const filepath of options.path) {
|
|
2524
|
+
if (fs.existsSync(filepath)) {
|
|
2525
|
+
possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
} else {
|
|
2529
|
+
possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2532
|
+
possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
|
|
2533
|
+
}
|
|
2534
|
+
if (fs.existsSync(possibleVaultPath)) {
|
|
2535
|
+
return possibleVaultPath;
|
|
2536
|
+
}
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
function _resolveHome(envPath) {
|
|
2540
|
+
return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath;
|
|
2541
|
+
}
|
|
2542
|
+
function _configVault(options) {
|
|
2543
|
+
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
2544
|
+
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
2545
|
+
if (debug || !quiet) {
|
|
2546
|
+
_log("Loading env from encrypted .env.vault");
|
|
2547
|
+
}
|
|
2548
|
+
const parsed = DotenvModule._parseVault(options);
|
|
2549
|
+
let processEnv = process.env;
|
|
2550
|
+
if (options && options.processEnv != null) {
|
|
2551
|
+
processEnv = options.processEnv;
|
|
2552
|
+
}
|
|
2553
|
+
DotenvModule.populate(processEnv, parsed, options);
|
|
2554
|
+
return { parsed };
|
|
2555
|
+
}
|
|
2556
|
+
function configDotenv(options) {
|
|
2557
|
+
const dotenvPath = path.resolve(process.cwd(), ".env");
|
|
2558
|
+
let encoding = "utf8";
|
|
2559
|
+
let processEnv = process.env;
|
|
2560
|
+
if (options && options.processEnv != null) {
|
|
2561
|
+
processEnv = options.processEnv;
|
|
2562
|
+
}
|
|
2563
|
+
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
2564
|
+
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
2565
|
+
if (options && options.encoding) {
|
|
2566
|
+
encoding = options.encoding;
|
|
2567
|
+
} else {
|
|
2568
|
+
if (debug) {
|
|
2569
|
+
_debug("No encoding is specified. UTF-8 is used by default");
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
let optionPaths = [dotenvPath];
|
|
2573
|
+
if (options && options.path) {
|
|
2574
|
+
if (!Array.isArray(options.path)) {
|
|
2575
|
+
optionPaths = [_resolveHome(options.path)];
|
|
2576
|
+
} else {
|
|
2577
|
+
optionPaths = [];
|
|
2578
|
+
for (const filepath of options.path) {
|
|
2579
|
+
optionPaths.push(_resolveHome(filepath));
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
let lastError;
|
|
2584
|
+
const parsedAll = {};
|
|
2585
|
+
for (const path2 of optionPaths) {
|
|
2586
|
+
try {
|
|
2587
|
+
const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
|
|
2588
|
+
DotenvModule.populate(parsedAll, parsed, options);
|
|
2589
|
+
} catch (e) {
|
|
2590
|
+
if (debug) {
|
|
2591
|
+
_debug(`Failed to load ${path2} ${e.message}`);
|
|
2592
|
+
}
|
|
2593
|
+
lastError = e;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
2597
|
+
debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
|
|
2598
|
+
quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
|
|
2599
|
+
if (debug || !quiet) {
|
|
2600
|
+
const keysCount = Object.keys(populated).length;
|
|
2601
|
+
const shortPaths = [];
|
|
2602
|
+
for (const filePath of optionPaths) {
|
|
2603
|
+
try {
|
|
2604
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
2605
|
+
shortPaths.push(relative);
|
|
2606
|
+
} catch (e) {
|
|
2607
|
+
if (debug) {
|
|
2608
|
+
_debug(`Failed to load ${filePath} ${e.message}`);
|
|
2609
|
+
}
|
|
2610
|
+
lastError = e;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
_log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
|
|
2614
|
+
}
|
|
2615
|
+
if (lastError) {
|
|
2616
|
+
return { parsed: parsedAll, error: lastError };
|
|
2617
|
+
} else {
|
|
2618
|
+
return { parsed: parsedAll };
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function config2(options) {
|
|
2622
|
+
if (_dotenvKey(options).length === 0) {
|
|
2623
|
+
return DotenvModule.configDotenv(options);
|
|
2624
|
+
}
|
|
2625
|
+
const vaultPath = _vaultPath(options);
|
|
2626
|
+
if (!vaultPath) {
|
|
2627
|
+
_warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
|
|
2628
|
+
return DotenvModule.configDotenv(options);
|
|
2629
|
+
}
|
|
2630
|
+
return DotenvModule._configVault(options);
|
|
2631
|
+
}
|
|
2632
|
+
function decrypt(encrypted, keyStr) {
|
|
2633
|
+
const key = Buffer.from(keyStr.slice(-64), "hex");
|
|
2634
|
+
let ciphertext = Buffer.from(encrypted, "base64");
|
|
2635
|
+
const nonce = ciphertext.subarray(0, 12);
|
|
2636
|
+
const authTag = ciphertext.subarray(-16);
|
|
2637
|
+
ciphertext = ciphertext.subarray(12, -16);
|
|
2638
|
+
try {
|
|
2639
|
+
const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
2640
|
+
aesgcm.setAuthTag(authTag);
|
|
2641
|
+
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
const isRange = error instanceof RangeError;
|
|
2644
|
+
const invalidKeyLength = error.message === "Invalid key length";
|
|
2645
|
+
const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
|
|
2646
|
+
if (isRange || invalidKeyLength) {
|
|
2647
|
+
const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
|
|
2648
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
2649
|
+
throw err;
|
|
2650
|
+
} else if (decryptionFailed) {
|
|
2651
|
+
const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
|
|
2652
|
+
err.code = "DECRYPTION_FAILED";
|
|
2653
|
+
throw err;
|
|
2654
|
+
} else {
|
|
2655
|
+
throw error;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
function populate(processEnv, parsed, options = {}) {
|
|
2660
|
+
const debug = Boolean(options && options.debug);
|
|
2661
|
+
const override = Boolean(options && options.override);
|
|
2662
|
+
const populated = {};
|
|
2663
|
+
if (typeof parsed !== "object") {
|
|
2664
|
+
const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
|
|
2665
|
+
err.code = "OBJECT_REQUIRED";
|
|
2666
|
+
throw err;
|
|
2667
|
+
}
|
|
2668
|
+
for (const key of Object.keys(parsed)) {
|
|
2669
|
+
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
2670
|
+
if (override === true) {
|
|
2671
|
+
processEnv[key] = parsed[key];
|
|
2672
|
+
populated[key] = parsed[key];
|
|
2673
|
+
}
|
|
2674
|
+
if (debug) {
|
|
2675
|
+
if (override === true) {
|
|
2676
|
+
_debug(`"${key}" is already defined and WAS overwritten`);
|
|
2677
|
+
} else {
|
|
2678
|
+
_debug(`"${key}" is already defined and was NOT overwritten`);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
} else {
|
|
2682
|
+
processEnv[key] = parsed[key];
|
|
2683
|
+
populated[key] = parsed[key];
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return populated;
|
|
2687
|
+
}
|
|
2688
|
+
var DotenvModule = {
|
|
2689
|
+
configDotenv,
|
|
2690
|
+
_configVault,
|
|
2691
|
+
_parseVault,
|
|
2692
|
+
config: config2,
|
|
2693
|
+
decrypt,
|
|
2694
|
+
parse,
|
|
2695
|
+
populate
|
|
2696
|
+
};
|
|
2697
|
+
module.exports.configDotenv = DotenvModule.configDotenv;
|
|
2698
|
+
module.exports._configVault = DotenvModule._configVault;
|
|
2699
|
+
module.exports._parseVault = DotenvModule._parseVault;
|
|
2700
|
+
module.exports.config = DotenvModule.config;
|
|
2701
|
+
module.exports.decrypt = DotenvModule.decrypt;
|
|
2702
|
+
module.exports.parse = DotenvModule.parse;
|
|
2703
|
+
module.exports.populate = DotenvModule.populate;
|
|
2704
|
+
module.exports = DotenvModule;
|
|
2155
2705
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
// cli/auth/index.ts
|
|
2709
|
+
var init_auth = __esm({
|
|
2710
|
+
"cli/auth/index.ts"() {
|
|
2711
|
+
"use strict";
|
|
2712
|
+
init_stub();
|
|
2159
2713
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
chmodSync(GRADER_PRIMER_EDIT_PATH, 420);
|
|
2172
|
-
writeFileSync4(GRADER_PRIMER_BASH_PATH, GRADER_PRIMER_BASH, "utf-8");
|
|
2173
|
-
chmodSync(GRADER_PRIMER_BASH_PATH, 420);
|
|
2174
|
-
}
|
|
2175
|
-
function writeHookScripts() {
|
|
2176
|
-
const bashScriptPath = join4(HOOKS_DIR, "cc-bash-judge.sh");
|
|
2177
|
-
const bashFollowupScriptPath = join4(HOOKS_DIR, "cc-bash-followup.sh");
|
|
2178
|
-
const editCaptureScriptPath = join4(HOOKS_DIR, "cc-edit-capture.sh");
|
|
2179
|
-
const editPrecheckScriptPath = join4(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
2180
|
-
const stopSummaryScriptPath = join4(HOOKS_DIR, "cc-stop-summary.sh");
|
|
2181
|
-
const sessionStartScriptPath = join4(HOOKS_DIR, "cc-session-start.sh");
|
|
2182
|
-
writeFileSync4(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
|
|
2183
|
-
writeFileSync4(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
2184
|
-
writeFileSync4(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
2185
|
-
writeFileSync4(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
|
|
2186
|
-
writeFileSync4(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
|
|
2187
|
-
writeFileSync4(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
|
|
2188
|
-
chmodSync(bashScriptPath, 493);
|
|
2189
|
-
chmodSync(bashFollowupScriptPath, 493);
|
|
2190
|
-
chmodSync(editCaptureScriptPath, 493);
|
|
2191
|
-
chmodSync(editPrecheckScriptPath, 493);
|
|
2192
|
-
chmodSync(stopSummaryScriptPath, 493);
|
|
2193
|
-
chmodSync(sessionStartScriptPath, 493);
|
|
2194
|
-
return {
|
|
2195
|
-
bashScript: bashScriptPath,
|
|
2196
|
-
bashFollowupScript: bashFollowupScriptPath,
|
|
2197
|
-
editCaptureScript: editCaptureScriptPath,
|
|
2198
|
-
editPrecheckScript: editPrecheckScriptPath,
|
|
2199
|
-
stopSummaryScript: stopSummaryScriptPath,
|
|
2200
|
-
sessionStartScript: sessionStartScriptPath
|
|
2714
|
+
});
|
|
2715
|
+
|
|
2716
|
+
// cli/api/projects.ts
|
|
2717
|
+
async function callApi(method, endpoint, body) {
|
|
2718
|
+
if (!API_URL) {
|
|
2719
|
+
throw new Error("SYNKRO_CRUD_URL (or SYNKRO_API_URL) is not set. Add it to your .env file.");
|
|
2720
|
+
}
|
|
2721
|
+
const url = `${API_URL}${endpoint}`;
|
|
2722
|
+
const accessToken = getAccessToken();
|
|
2723
|
+
const headers = {
|
|
2724
|
+
"Content-Type": "application/json"
|
|
2201
2725
|
};
|
|
2726
|
+
if (accessToken) {
|
|
2727
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
2728
|
+
}
|
|
2729
|
+
const response = await fetch(url, {
|
|
2730
|
+
method,
|
|
2731
|
+
headers,
|
|
2732
|
+
body: body ? JSON.stringify(body) : void 0
|
|
2733
|
+
});
|
|
2734
|
+
if (!response.ok) {
|
|
2735
|
+
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
|
2736
|
+
throw new Error(error.detail || `API error: ${response.status}`);
|
|
2737
|
+
}
|
|
2738
|
+
return response.json();
|
|
2202
2739
|
}
|
|
2203
|
-
function
|
|
2204
|
-
|
|
2205
|
-
|
|
2740
|
+
async function createProject(name, repos) {
|
|
2741
|
+
const body = { name };
|
|
2742
|
+
if (repos && repos.length > 0) body.repos = repos;
|
|
2743
|
+
return callApi("POST", "/projects", body);
|
|
2206
2744
|
}
|
|
2207
|
-
function
|
|
2208
|
-
return
|
|
2745
|
+
async function listProjects() {
|
|
2746
|
+
return callApi("GET", "/projects");
|
|
2209
2747
|
}
|
|
2210
|
-
function
|
|
2211
|
-
|
|
2212
|
-
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
2213
|
-
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
2214
|
-
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
2215
|
-
const safeEmail = sanitizeConfigValue(opts.email);
|
|
2216
|
-
const safeTier = sanitizeConfigValue(opts.tier ?? "pro", 32);
|
|
2217
|
-
const lines = [
|
|
2218
|
-
"# Synkro CLI config (managed by synkro install)",
|
|
2219
|
-
"# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
|
|
2220
|
-
"# and send Authorization: Bearer <access_token> on every gateway call.",
|
|
2221
|
-
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2222
|
-
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2223
|
-
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2224
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.2.6")}`
|
|
2225
|
-
];
|
|
2226
|
-
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2227
|
-
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
2228
|
-
if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
|
|
2229
|
-
lines.push("");
|
|
2230
|
-
writeFileSync4(CONFIG_PATH, lines.join("\n"), "utf-8");
|
|
2231
|
-
chmodSync(CONFIG_PATH, 384);
|
|
2748
|
+
async function unlinkRepo(projectId, repoId) {
|
|
2749
|
+
return callApi("DELETE", `/projects/${projectId}/repos/${repoId}`);
|
|
2232
2750
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2751
|
+
var import_dotenv, API_URL;
|
|
2752
|
+
var init_projects = __esm({
|
|
2753
|
+
"cli/api/projects.ts"() {
|
|
2754
|
+
"use strict";
|
|
2755
|
+
import_dotenv = __toESM(require_main(), 1);
|
|
2756
|
+
init_auth();
|
|
2757
|
+
(0, import_dotenv.config)({ quiet: true });
|
|
2758
|
+
API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
|
|
2762
|
+
// cli/installer/workflowTemplate.ts
|
|
2763
|
+
var SYNKRO_WORKFLOW_YAML, WORKFLOW_PATH;
|
|
2764
|
+
var init_workflowTemplate = __esm({
|
|
2765
|
+
"cli/installer/workflowTemplate.ts"() {
|
|
2766
|
+
"use strict";
|
|
2767
|
+
SYNKRO_WORKFLOW_YAML = `name: Synkro Security Review
|
|
2768
|
+
on:
|
|
2769
|
+
pull_request:
|
|
2770
|
+
types: [opened, synchronize, reopened]
|
|
2771
|
+
|
|
2772
|
+
jobs:
|
|
2773
|
+
scan:
|
|
2774
|
+
runs-on: ubuntu-latest
|
|
2775
|
+
permissions:
|
|
2776
|
+
contents: read
|
|
2777
|
+
pull-requests: write
|
|
2778
|
+
checks: write
|
|
2779
|
+
steps:
|
|
2780
|
+
- uses: actions/checkout@v4
|
|
2781
|
+
with:
|
|
2782
|
+
fetch-depth: 0
|
|
2783
|
+
|
|
2784
|
+
- name: Cache npm globals
|
|
2785
|
+
id: cache-npm-global
|
|
2786
|
+
uses: actions/cache@v4
|
|
2787
|
+
with:
|
|
2788
|
+
path: ~/.npm-global
|
|
2789
|
+
key: synkro-cli-\${{ runner.os }}-v1
|
|
2790
|
+
|
|
2791
|
+
- name: Install Synkro CLI + Claude Code CLI
|
|
2792
|
+
run: |
|
|
2793
|
+
npm config set prefix ~/.npm-global
|
|
2794
|
+
npm install -g @synkro-sh/cli @anthropic-ai/claude-code
|
|
2795
|
+
echo "~/.npm-global/bin" >> $GITHUB_PATH
|
|
2796
|
+
|
|
2797
|
+
- name: Run Synkro PR scan
|
|
2798
|
+
run: synkro-cli scan-pr
|
|
2799
|
+
env:
|
|
2800
|
+
CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
2801
|
+
SYNKRO_API_KEY: \${{ secrets.SYNKRO_API_KEY }}
|
|
2802
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2803
|
+
SYNKRO_PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2804
|
+
SYNKRO_REPO: \${{ github.repository }}
|
|
2805
|
+
SYNKRO_SHA: \${{ github.event.pull_request.head.sha }}
|
|
2806
|
+
SYNKRO_GATEWAY_URL: \${{ vars.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh' }}
|
|
2807
|
+
`;
|
|
2808
|
+
WORKFLOW_PATH = ".github/workflows/synkro.yml";
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2812
|
+
// cli/installer/githubSetup.ts
|
|
2813
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2814
|
+
import { join as join4 } from "path";
|
|
2815
|
+
async function encryptSecret(publicKeyBase64, secret) {
|
|
2816
|
+
const sodium = await import("libsodium-wrappers").then((m) => m.default ?? m);
|
|
2817
|
+
await sodium.ready;
|
|
2818
|
+
const keyBytes = sodium.from_base64(publicKeyBase64, sodium.base64_variants.ORIGINAL);
|
|
2819
|
+
const messageBytes = sodium.from_string(secret);
|
|
2820
|
+
const encryptedBytes = sodium.crypto_box_seal(messageBytes, keyBytes);
|
|
2821
|
+
return sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
|
2822
|
+
}
|
|
2823
|
+
async function getRepoPublicKey(opts, owner, repo) {
|
|
2824
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/public-key`;
|
|
2825
|
+
const resp = await fetch(url, {
|
|
2826
|
+
headers: {
|
|
2827
|
+
Authorization: `Bearer ${opts.token}`,
|
|
2828
|
+
Accept: "application/vnd.github+json",
|
|
2829
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
if (!resp.ok) {
|
|
2833
|
+
const text = await resp.text().catch(() => "");
|
|
2834
|
+
throw new Error(`GitHub API ${resp.status} fetching public key for ${owner}/${repo}: ${text.slice(0, 200)}`);
|
|
2835
|
+
}
|
|
2836
|
+
return await resp.json();
|
|
2837
|
+
}
|
|
2838
|
+
async function putRepoSecret(opts, owner, repo, secretName, secretValue, publicKey) {
|
|
2839
|
+
const encryptedValue = await encryptSecret(publicKey.key, secretValue);
|
|
2840
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/${encodeURIComponent(secretName)}`;
|
|
2841
|
+
const resp = await fetch(url, {
|
|
2842
|
+
method: "PUT",
|
|
2843
|
+
headers: {
|
|
2844
|
+
Authorization: `Bearer ${opts.token}`,
|
|
2845
|
+
Accept: "application/vnd.github+json",
|
|
2846
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
2847
|
+
"Content-Type": "application/json"
|
|
2848
|
+
},
|
|
2849
|
+
body: JSON.stringify({
|
|
2850
|
+
encrypted_value: encryptedValue,
|
|
2851
|
+
key_id: publicKey.key_id
|
|
2852
|
+
})
|
|
2853
|
+
});
|
|
2854
|
+
if (!resp.ok) {
|
|
2855
|
+
const text = await resp.text().catch(() => "");
|
|
2856
|
+
throw new Error(`GitHub API ${resp.status} setting secret ${secretName}: ${text.slice(0, 200)}`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
async function listAccessibleRepos(opts) {
|
|
2860
|
+
const repos = [];
|
|
2861
|
+
let page = 1;
|
|
2862
|
+
while (page <= 5) {
|
|
2863
|
+
const url = `https://api.github.com/user/repos?per_page=100&page=${page}&affiliation=owner,collaborator`;
|
|
2864
|
+
const resp = await fetch(url, {
|
|
2865
|
+
headers: {
|
|
2866
|
+
Authorization: `Bearer ${opts.token}`,
|
|
2867
|
+
Accept: "application/vnd.github+json",
|
|
2868
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
if (!resp.ok) {
|
|
2872
|
+
throw new Error(`GitHub API ${resp.status} listing repos`);
|
|
2873
|
+
}
|
|
2874
|
+
const data = await resp.json();
|
|
2875
|
+
if (data.length === 0) break;
|
|
2876
|
+
for (const r of data) {
|
|
2877
|
+
repos.push({ owner: r.owner.login, repo: r.name, full_name: r.full_name });
|
|
2878
|
+
}
|
|
2879
|
+
if (data.length < 100) break;
|
|
2880
|
+
page++;
|
|
2881
|
+
}
|
|
2882
|
+
return repos;
|
|
2883
|
+
}
|
|
2884
|
+
async function pushSecretsToRepo(opts, owner, repo, secrets) {
|
|
2885
|
+
const pubkey = await getRepoPublicKey(opts, owner, repo);
|
|
2886
|
+
await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
|
|
2887
|
+
await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
|
|
2888
|
+
}
|
|
2889
|
+
function writeWorkflowFile(repoRootPath) {
|
|
2890
|
+
const workflowDir = join4(repoRootPath, ".github", "workflows");
|
|
2891
|
+
mkdirSync4(workflowDir, { recursive: true });
|
|
2892
|
+
const workflowFile = join4(workflowDir, "synkro.yml");
|
|
2893
|
+
writeFileSync4(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
|
|
2894
|
+
return workflowFile;
|
|
2895
|
+
}
|
|
2896
|
+
function findGitRoot(startCwd) {
|
|
2897
|
+
let cur = startCwd;
|
|
2898
|
+
while (cur && cur !== "/") {
|
|
2899
|
+
if (existsSync5(join4(cur, ".git"))) return cur;
|
|
2900
|
+
const parent = join4(cur, "..");
|
|
2901
|
+
if (parent === cur) break;
|
|
2902
|
+
cur = parent;
|
|
2903
|
+
}
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
var SECRET_NAMES, WORKFLOW_RELATIVE_PATH;
|
|
2907
|
+
var init_githubSetup = __esm({
|
|
2908
|
+
"cli/installer/githubSetup.ts"() {
|
|
2909
|
+
"use strict";
|
|
2910
|
+
init_workflowTemplate();
|
|
2911
|
+
SECRET_NAMES = {
|
|
2912
|
+
CLAUDE_OAUTH: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
2913
|
+
SYNKRO_API_KEY: "SYNKRO_API_KEY"
|
|
2914
|
+
};
|
|
2915
|
+
WORKFLOW_RELATIVE_PATH = WORKFLOW_PATH;
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
|
|
2919
|
+
// cli/commands/repoConnect.ts
|
|
2920
|
+
import { execSync as execSync2 } from "child_process";
|
|
2921
|
+
import { createServer as createServer2 } from "http";
|
|
2922
|
+
import { createInterface } from "readline";
|
|
2923
|
+
function detectGitRepo() {
|
|
2924
|
+
try {
|
|
2925
|
+
const remoteUrl = execSync2("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
2926
|
+
const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
|
|
2927
|
+
if (!match) return null;
|
|
2928
|
+
const fullName = match[1];
|
|
2929
|
+
return { fullName, shortName: fullName.split("/").pop() || fullName };
|
|
2930
|
+
} catch {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
function ask(rl, question) {
|
|
2935
|
+
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
2936
|
+
}
|
|
2937
|
+
function waitForGithubToken() {
|
|
2938
|
+
return new Promise((resolve2, reject) => {
|
|
2939
|
+
const server = createServer2((req, res) => {
|
|
2940
|
+
if (req.method === "OPTIONS") {
|
|
2941
|
+
res.writeHead(204, {
|
|
2942
|
+
"Access-Control-Allow-Origin": SYNKRO_WEB_AUTH_URL2,
|
|
2943
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
2944
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
2945
|
+
});
|
|
2946
|
+
res.end();
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (req.url !== "/auth" || req.method !== "POST") {
|
|
2950
|
+
res.writeHead(404);
|
|
2951
|
+
res.end();
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
let body = "";
|
|
2955
|
+
req.on("data", (chunk) => {
|
|
2956
|
+
body += chunk;
|
|
2957
|
+
});
|
|
2958
|
+
req.on("end", () => {
|
|
2959
|
+
try {
|
|
2960
|
+
const parsed = JSON.parse(body);
|
|
2961
|
+
if (!parsed.github_token) {
|
|
2962
|
+
res.writeHead(400, {
|
|
2963
|
+
"Content-Type": "application/json",
|
|
2964
|
+
"Access-Control-Allow-Origin": SYNKRO_WEB_AUTH_URL2
|
|
2965
|
+
});
|
|
2966
|
+
res.end(JSON.stringify({ error: "missing github_token" }));
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
res.writeHead(200, {
|
|
2970
|
+
"Content-Type": "application/json",
|
|
2971
|
+
"Access-Control-Allow-Origin": SYNKRO_WEB_AUTH_URL2
|
|
2972
|
+
});
|
|
2973
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2974
|
+
setTimeout(() => server.close(), 200);
|
|
2975
|
+
resolve2(parsed.github_token);
|
|
2976
|
+
} catch {
|
|
2977
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2978
|
+
res.end(JSON.stringify({ error: "invalid json" }));
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
});
|
|
2982
|
+
server.on("error", (err) => {
|
|
2983
|
+
if (err.code === "EADDRINUSE") {
|
|
2984
|
+
reject(new Error(`Port ${GITHUB_PORT} is in use. Close other processes and try again.`));
|
|
2985
|
+
} else {
|
|
2986
|
+
reject(err);
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2989
|
+
server.listen(GITHUB_PORT);
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
function openBrowser2(url) {
|
|
2993
|
+
const { execFile: execFile2 } = __require("child_process");
|
|
2994
|
+
const plat = process.platform;
|
|
2995
|
+
if (plat === "darwin") execFile2("open", [url]);
|
|
2996
|
+
else if (plat === "win32") execFile2("cmd", ["/c", "start", "", url]);
|
|
2997
|
+
else execFile2("xdg-open", [url]);
|
|
2998
|
+
}
|
|
2999
|
+
async function connectGithubAndSelectRepos() {
|
|
3000
|
+
const url = `${SYNKRO_WEB_AUTH_URL2}/cli-github?port=${GITHUB_PORT}`;
|
|
3001
|
+
console.log(" Opening browser for GitHub authorization...");
|
|
3002
|
+
openBrowser2(url);
|
|
3003
|
+
console.log(" Waiting for GitHub authorization...");
|
|
3004
|
+
const ghToken = await waitForGithubToken();
|
|
3005
|
+
console.log(" \u2713 GitHub connected\n");
|
|
3006
|
+
const repos = await listAccessibleRepos({ token: ghToken });
|
|
3007
|
+
if (repos.length === 0) {
|
|
3008
|
+
console.log(" No accessible repos found on GitHub.");
|
|
3009
|
+
return [];
|
|
3010
|
+
}
|
|
3011
|
+
console.log(` Found ${repos.length} repos:
|
|
3012
|
+
`);
|
|
3013
|
+
repos.forEach((r, i) => {
|
|
3014
|
+
console.log(` ${String(i + 1).padStart(3)}. ${r.full_name}`);
|
|
3015
|
+
});
|
|
3016
|
+
console.log();
|
|
3017
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3018
|
+
try {
|
|
3019
|
+
const selection = await ask(rl, " Select repos (comma-separated numbers, e.g. 1,3,5): ");
|
|
3020
|
+
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < repos.length);
|
|
3021
|
+
if (indices.length === 0) {
|
|
3022
|
+
console.log(" No repos selected.");
|
|
3023
|
+
return [];
|
|
3024
|
+
}
|
|
3025
|
+
return indices.map((i) => ({ full_name: repos[i].full_name }));
|
|
3026
|
+
} finally {
|
|
3027
|
+
rl.close();
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
async function promptRepoConnection() {
|
|
3031
|
+
const localRepo = detectGitRepo();
|
|
3032
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3033
|
+
try {
|
|
3034
|
+
console.log("Connect repos to Synkro:\n");
|
|
3035
|
+
const options = [];
|
|
3036
|
+
if (localRepo) {
|
|
3037
|
+
options.push(`Link this repo (${localRepo.fullName})`);
|
|
3038
|
+
}
|
|
3039
|
+
options.push("Connect GitHub to select repos");
|
|
3040
|
+
options.push("Skip for now");
|
|
3041
|
+
options.forEach((opt, i) => {
|
|
3042
|
+
console.log(` ${i + 1}. ${opt}`);
|
|
3043
|
+
});
|
|
3044
|
+
console.log();
|
|
3045
|
+
const choice = await ask(rl, " Choose (number): ");
|
|
3046
|
+
const choiceNum = parseInt(choice.trim(), 10);
|
|
3047
|
+
console.log();
|
|
3048
|
+
rl.close();
|
|
3049
|
+
const localIdx = localRepo ? 1 : -1;
|
|
3050
|
+
const githubIdx = localRepo ? 2 : 1;
|
|
3051
|
+
const skipIdx = localRepo ? 3 : 2;
|
|
3052
|
+
if (choiceNum === localIdx && localRepo) {
|
|
3053
|
+
try {
|
|
3054
|
+
const existing = await listProjects();
|
|
3055
|
+
const alreadyLinked = existing.some(
|
|
3056
|
+
(p) => p.repos?.some((r) => r.full_name === localRepo.fullName)
|
|
3057
|
+
);
|
|
3058
|
+
if (!alreadyLinked) {
|
|
3059
|
+
await createProject(localRepo.shortName, [{ full_name: localRepo.fullName }]);
|
|
3060
|
+
console.log(` \u2713 Created project "${localRepo.shortName}" linked to ${localRepo.fullName}`);
|
|
3061
|
+
} else {
|
|
3062
|
+
console.log(` \u2713 ${localRepo.fullName} is already linked to a Synkro project.`);
|
|
3063
|
+
}
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
console.warn(` \u26A0 Could not link repo: ${err.message}`);
|
|
3066
|
+
}
|
|
3067
|
+
} else if (choiceNum === githubIdx) {
|
|
3068
|
+
const selectedRepos = await connectGithubAndSelectRepos();
|
|
3069
|
+
if (selectedRepos.length > 0) {
|
|
3070
|
+
try {
|
|
3071
|
+
const existing = await listProjects();
|
|
3072
|
+
const existingFullNames = new Set(
|
|
3073
|
+
existing.flatMap((p) => (p.repos || []).map((r) => r.full_name))
|
|
3074
|
+
);
|
|
3075
|
+
const newRepos = selectedRepos.filter((r) => !existingFullNames.has(r.full_name));
|
|
3076
|
+
if (newRepos.length === 0) {
|
|
3077
|
+
console.log(" \u2713 All selected repos are already linked.");
|
|
3078
|
+
} else {
|
|
3079
|
+
const projectName = newRepos.length === 1 ? newRepos[0].full_name.split("/").pop() || "Project" : "Multi-Repo Project";
|
|
3080
|
+
await createProject(projectName, newRepos);
|
|
3081
|
+
console.log(` \u2713 Linked ${newRepos.length} repo(s) to project "${projectName}"`);
|
|
3082
|
+
}
|
|
3083
|
+
} catch (err) {
|
|
3084
|
+
console.warn(` \u26A0 Could not link repos: ${err.message}`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
} else if (choiceNum === skipIdx) {
|
|
3088
|
+
console.log(" Skipped. Run `synkro link` later to connect repos.");
|
|
3089
|
+
} else {
|
|
3090
|
+
console.log(" Invalid choice. Skipping repo connection.");
|
|
3091
|
+
}
|
|
3092
|
+
} catch {
|
|
3093
|
+
rl.close();
|
|
3094
|
+
}
|
|
3095
|
+
console.log();
|
|
3096
|
+
}
|
|
3097
|
+
var RAW_WEB_AUTH_URL2, SYNKRO_WEB_AUTH_URL2, GITHUB_PORT;
|
|
3098
|
+
var init_repoConnect = __esm({
|
|
3099
|
+
"cli/commands/repoConnect.ts"() {
|
|
3100
|
+
"use strict";
|
|
3101
|
+
init_projects();
|
|
3102
|
+
init_githubSetup();
|
|
3103
|
+
RAW_WEB_AUTH_URL2 = process.env.SYNKRO_WEB_AUTH_URL;
|
|
3104
|
+
SYNKRO_WEB_AUTH_URL2 = RAW_WEB_AUTH_URL2 && /^https?:\/\//.test(RAW_WEB_AUTH_URL2) ? RAW_WEB_AUTH_URL2 : "https://app.synkro.sh";
|
|
3105
|
+
GITHUB_PORT = 8101;
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
// cli/commands/install.ts
|
|
3110
|
+
var install_exports = {};
|
|
3111
|
+
__export(install_exports, {
|
|
3112
|
+
installCommand: () => installCommand,
|
|
3113
|
+
parseArgs: () => parseArgs
|
|
3114
|
+
});
|
|
3115
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, chmodSync, readFileSync as readFileSync4, readdirSync } from "fs";
|
|
3116
|
+
import { homedir as homedir4 } from "os";
|
|
3117
|
+
import { join as join5 } from "path";
|
|
3118
|
+
import { execSync as execSync3 } from "child_process";
|
|
3119
|
+
function sanitizeGatewayCandidate(raw) {
|
|
3120
|
+
if (!raw) return void 0;
|
|
3121
|
+
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
3122
|
+
}
|
|
3123
|
+
function parseArgs(argv) {
|
|
3124
|
+
const opts = {};
|
|
3125
|
+
for (const a of argv) {
|
|
3126
|
+
if (a.startsWith("--api-key=")) opts.apiKey = a.slice("--api-key=".length);
|
|
3127
|
+
else if (a.startsWith("--gateway=")) opts.gatewayUrl = a.slice("--gateway=".length);
|
|
3128
|
+
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
3129
|
+
else if (a === "--no-mcp") opts.noMcp = true;
|
|
3130
|
+
else if (a === "--force" || a === "-f") opts.force = true;
|
|
3131
|
+
}
|
|
3132
|
+
if (!opts.gatewayUrl) {
|
|
3133
|
+
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
3134
|
+
if (fromEnv) opts.gatewayUrl = fromEnv;
|
|
3135
|
+
}
|
|
3136
|
+
return opts;
|
|
3137
|
+
}
|
|
3138
|
+
function ensureSynkroDir() {
|
|
3139
|
+
mkdirSync5(SYNKRO_DIR, { recursive: true });
|
|
3140
|
+
mkdirSync5(HOOKS_DIR, { recursive: true });
|
|
3141
|
+
mkdirSync5(BIN_DIR, { recursive: true });
|
|
3142
|
+
}
|
|
3143
|
+
function writeGraderDaemon() {
|
|
3144
|
+
writeFileSync5(GRADER_DAEMON_PATH, GRADER_DAEMON_PY, "utf-8");
|
|
3145
|
+
chmodSync(GRADER_DAEMON_PATH, 493);
|
|
3146
|
+
writeFileSync5(GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_EDIT, "utf-8");
|
|
3147
|
+
chmodSync(GRADER_PRIMER_EDIT_PATH, 420);
|
|
3148
|
+
writeFileSync5(GRADER_PRIMER_BASH_PATH, GRADER_PRIMER_BASH, "utf-8");
|
|
3149
|
+
chmodSync(GRADER_PRIMER_BASH_PATH, 420);
|
|
3150
|
+
}
|
|
3151
|
+
function writeHookScripts() {
|
|
3152
|
+
const bashScriptPath = join5(HOOKS_DIR, "cc-bash-judge.sh");
|
|
3153
|
+
const bashFollowupScriptPath = join5(HOOKS_DIR, "cc-bash-followup.sh");
|
|
3154
|
+
const editCaptureScriptPath = join5(HOOKS_DIR, "cc-edit-capture.sh");
|
|
3155
|
+
const editPrecheckScriptPath = join5(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
3156
|
+
const stopSummaryScriptPath = join5(HOOKS_DIR, "cc-stop-summary.sh");
|
|
3157
|
+
const sessionStartScriptPath = join5(HOOKS_DIR, "cc-session-start.sh");
|
|
3158
|
+
writeFileSync5(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
|
|
3159
|
+
writeFileSync5(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
|
|
3160
|
+
writeFileSync5(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
3161
|
+
writeFileSync5(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
|
|
3162
|
+
writeFileSync5(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
|
|
3163
|
+
writeFileSync5(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
|
|
3164
|
+
chmodSync(bashScriptPath, 493);
|
|
3165
|
+
chmodSync(bashFollowupScriptPath, 493);
|
|
3166
|
+
chmodSync(editCaptureScriptPath, 493);
|
|
3167
|
+
chmodSync(editPrecheckScriptPath, 493);
|
|
3168
|
+
chmodSync(stopSummaryScriptPath, 493);
|
|
3169
|
+
chmodSync(sessionStartScriptPath, 493);
|
|
3170
|
+
return {
|
|
3171
|
+
bashScript: bashScriptPath,
|
|
3172
|
+
bashFollowupScript: bashFollowupScriptPath,
|
|
3173
|
+
editCaptureScript: editCaptureScriptPath,
|
|
3174
|
+
editPrecheckScript: editPrecheckScriptPath,
|
|
3175
|
+
stopSummaryScript: stopSummaryScriptPath,
|
|
3176
|
+
sessionStartScript: sessionStartScriptPath
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
function sanitizeConfigValue(raw, maxLen = 256) {
|
|
3180
|
+
if (!raw) return "";
|
|
3181
|
+
return raw.replace(/[^\x20-\x7E]/g, "").slice(0, maxLen);
|
|
3182
|
+
}
|
|
3183
|
+
function shellQuoteSingle(value) {
|
|
3184
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3185
|
+
}
|
|
3186
|
+
function writeConfigEnv(opts) {
|
|
3187
|
+
const credsPath = join5(SYNKRO_DIR, "credentials.json");
|
|
3188
|
+
const safeGateway = sanitizeConfigValue(opts.gatewayUrl);
|
|
3189
|
+
const safeUserId = sanitizeConfigValue(opts.userId);
|
|
3190
|
+
const safeOrgId = sanitizeConfigValue(opts.orgId);
|
|
3191
|
+
const safeEmail = sanitizeConfigValue(opts.email);
|
|
3192
|
+
const safeTier = sanitizeConfigValue(opts.tier ?? "pro", 32);
|
|
3193
|
+
const lines = [
|
|
3194
|
+
"# Synkro CLI config (managed by synkro install)",
|
|
3195
|
+
"# JWT auth \u2014 the hook scripts read SYNKRO_CREDENTIALS_PATH at runtime",
|
|
3196
|
+
"# and send Authorization: Bearer <access_token> on every gateway call.",
|
|
3197
|
+
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
3198
|
+
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3199
|
+
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3200
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.3")}`
|
|
3201
|
+
];
|
|
3202
|
+
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3203
|
+
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
3204
|
+
if (safeEmail) lines.push(`SYNKRO_EMAIL=${shellQuoteSingle(safeEmail)}`);
|
|
3205
|
+
lines.push("");
|
|
3206
|
+
writeFileSync5(CONFIG_PATH, lines.join("\n"), "utf-8");
|
|
3207
|
+
chmodSync(CONFIG_PATH, 384);
|
|
3208
|
+
}
|
|
3209
|
+
function assertGatewayAllowed(gatewayUrl) {
|
|
3210
|
+
let parsed;
|
|
3211
|
+
try {
|
|
3212
|
+
parsed = new URL(gatewayUrl);
|
|
2237
3213
|
} catch {
|
|
2238
3214
|
throw new Error(`Invalid gateway URL: ${gatewayUrl}`);
|
|
2239
3215
|
}
|
|
@@ -2253,17 +3229,17 @@ function assertGatewayAllowed(gatewayUrl) {
|
|
|
2253
3229
|
}
|
|
2254
3230
|
function isAlreadyInstalled() {
|
|
2255
3231
|
const requiredScripts = [
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
3232
|
+
join5(HOOKS_DIR, "cc-bash-judge.sh"),
|
|
3233
|
+
join5(HOOKS_DIR, "cc-bash-followup.sh"),
|
|
3234
|
+
join5(HOOKS_DIR, "cc-edit-precheck.sh"),
|
|
3235
|
+
join5(HOOKS_DIR, "cc-edit-capture.sh"),
|
|
3236
|
+
join5(HOOKS_DIR, "cc-stop-summary.sh"),
|
|
3237
|
+
join5(HOOKS_DIR, "cc-session-start.sh")
|
|
2262
3238
|
];
|
|
2263
|
-
if (!requiredScripts.every((p) =>
|
|
2264
|
-
if (!
|
|
2265
|
-
const settingsPath =
|
|
2266
|
-
if (!
|
|
3239
|
+
if (!requiredScripts.every((p) => existsSync6(p))) return false;
|
|
3240
|
+
if (!existsSync6(CONFIG_PATH)) return false;
|
|
3241
|
+
const settingsPath = join5(homedir4(), ".claude", "settings.json");
|
|
3242
|
+
if (!existsSync6(settingsPath)) return false;
|
|
2267
3243
|
try {
|
|
2268
3244
|
const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
2269
3245
|
const hooks = settings?.hooks;
|
|
@@ -2324,6 +3300,7 @@ async function installCommand(opts = {}) {
|
|
|
2324
3300
|
console.error("No access token available after auth.");
|
|
2325
3301
|
process.exit(1);
|
|
2326
3302
|
}
|
|
3303
|
+
await promptRepoConnection();
|
|
2327
3304
|
const agents = detectAgents();
|
|
2328
3305
|
if (agents.length === 0) {
|
|
2329
3306
|
console.error("No AI coding agents detected. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
@@ -2346,7 +3323,7 @@ async function installCommand(opts = {}) {
|
|
|
2346
3323
|
`);
|
|
2347
3324
|
writeGraderDaemon();
|
|
2348
3325
|
for (const mode of ["edit", "bash"]) {
|
|
2349
|
-
const pidFile =
|
|
3326
|
+
const pidFile = join5(SYNKRO_DIR, "daemon", mode, "daemon.pid");
|
|
2350
3327
|
try {
|
|
2351
3328
|
const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
|
|
2352
3329
|
if (pid > 0) {
|
|
@@ -2417,12 +3394,115 @@ async function installCommand(opts = {}) {
|
|
|
2417
3394
|
writeConfigEnv({ gatewayUrl, userId, orgId, email });
|
|
2418
3395
|
console.log(`Wrote config to ${CONFIG_PATH}
|
|
2419
3396
|
`);
|
|
3397
|
+
try {
|
|
3398
|
+
const repo = detectGitRepo2();
|
|
3399
|
+
if (repo) {
|
|
3400
|
+
const ingested = await ingestSessionTranscripts(gatewayUrl, token, repo);
|
|
3401
|
+
if (ingested > 0) {
|
|
3402
|
+
console.log(`Indexed ${ingested} session insights from Claude Code history for ${repo}.`);
|
|
3403
|
+
console.log(" This helps the safety judge understand your workflow.\n");
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
} catch (err) {
|
|
3407
|
+
console.warn(` \u26A0 Session indexing skipped: ${err.message}
|
|
3408
|
+
`);
|
|
3409
|
+
}
|
|
2420
3410
|
console.log("\u2713 Synkro installed.");
|
|
2421
3411
|
console.log();
|
|
2422
3412
|
console.log("Next steps:");
|
|
2423
3413
|
console.log(" \u2022 synkro-cli setup-github (enable PR scanning)");
|
|
2424
3414
|
console.log(" \u2022 synkro-cli status (check what is configured)");
|
|
2425
3415
|
}
|
|
3416
|
+
function detectGitRepo2() {
|
|
3417
|
+
try {
|
|
3418
|
+
const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
3419
|
+
const match = remoteUrl.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[:/](.+?)(?:\.git)?$/);
|
|
3420
|
+
return match ? match[1] : null;
|
|
3421
|
+
} catch {
|
|
3422
|
+
return null;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function getClaudeProjectsFolder() {
|
|
3426
|
+
const cwd = process.cwd();
|
|
3427
|
+
const sanitized = "-" + cwd.replace(/\//g, "-");
|
|
3428
|
+
const projectsDir = join5(homedir4(), ".claude", "projects", sanitized);
|
|
3429
|
+
return existsSync6(projectsDir) ? projectsDir : null;
|
|
3430
|
+
}
|
|
3431
|
+
function extractSessionInsights(projectsDir) {
|
|
3432
|
+
const insights = [];
|
|
3433
|
+
const files = readdirSync(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
3434
|
+
for (const file of files) {
|
|
3435
|
+
const sessionId = file.replace(".jsonl", "");
|
|
3436
|
+
const filePath = join5(projectsDir, file);
|
|
3437
|
+
try {
|
|
3438
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
3439
|
+
const lines = content.split("\n").filter(Boolean);
|
|
3440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3441
|
+
try {
|
|
3442
|
+
const entry = JSON.parse(lines[i]);
|
|
3443
|
+
if (entry.type === "user" && typeof entry.message?.content === "string" && entry.message.content.startsWith("This session is being continued")) {
|
|
3444
|
+
insights.push({
|
|
3445
|
+
session_id: sessionId,
|
|
3446
|
+
insight_type: "summary",
|
|
3447
|
+
content: entry.message.content.slice(0, 4e3),
|
|
3448
|
+
metadata: { source: "compaction_summary" }
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
const userMessages = [];
|
|
3455
|
+
for (let i = lines.length - 1; i >= 0 && userMessages.length < 20; i--) {
|
|
3456
|
+
try {
|
|
3457
|
+
const entry = JSON.parse(lines[i]);
|
|
3458
|
+
if (entry.type === "user") {
|
|
3459
|
+
const text = typeof entry.message?.content === "string" ? entry.message.content : Array.isArray(entry.message?.content) ? entry.message.content.map((b) => b.text ?? b).filter((t) => typeof t === "string").join(" ") : null;
|
|
3460
|
+
if (text && text.length > 10 && text.length < 2e3 && !text.startsWith("This session is being continued")) {
|
|
3461
|
+
userMessages.push(text);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
} catch {
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
for (const msg of userMessages.reverse()) {
|
|
3468
|
+
insights.push({
|
|
3469
|
+
session_id: sessionId,
|
|
3470
|
+
insight_type: "user_message",
|
|
3471
|
+
content: msg.slice(0, 2e3)
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
return insights;
|
|
3478
|
+
}
|
|
3479
|
+
async function ingestSessionTranscripts(gatewayUrl, token, repo) {
|
|
3480
|
+
const projectsDir = getClaudeProjectsFolder();
|
|
3481
|
+
if (!projectsDir) return 0;
|
|
3482
|
+
const insights = extractSessionInsights(projectsDir);
|
|
3483
|
+
if (insights.length === 0) return 0;
|
|
3484
|
+
console.log(`Found ${insights.length} session insights from Claude Code history...`);
|
|
3485
|
+
let total = 0;
|
|
3486
|
+
for (let i = 0; i < insights.length; i += 100) {
|
|
3487
|
+
const batch = insights.slice(i, i + 100);
|
|
3488
|
+
try {
|
|
3489
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ingest-sessions`, {
|
|
3490
|
+
method: "POST",
|
|
3491
|
+
headers: {
|
|
3492
|
+
"Authorization": `Bearer ${token}`,
|
|
3493
|
+
"Content-Type": "application/json"
|
|
3494
|
+
},
|
|
3495
|
+
body: JSON.stringify({ repo, sessions: batch })
|
|
3496
|
+
});
|
|
3497
|
+
if (resp.ok) {
|
|
3498
|
+
const result = await resp.json();
|
|
3499
|
+
total += result.ingested;
|
|
3500
|
+
}
|
|
3501
|
+
} catch {
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
return total;
|
|
3505
|
+
}
|
|
2426
3506
|
var SYNKRO_DIR, HOOKS_DIR, BIN_DIR, CONFIG_PATH, GRADER_DAEMON_PATH, GRADER_PRIMER_EDIT_PATH, GRADER_PRIMER_BASH_PATH;
|
|
2427
3507
|
var init_install = __esm({
|
|
2428
3508
|
"cli/commands/install.ts"() {
|
|
@@ -2433,13 +3513,14 @@ var init_install = __esm({
|
|
|
2433
3513
|
init_hookScripts();
|
|
2434
3514
|
init_graderDaemon();
|
|
2435
3515
|
init_stub();
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
3516
|
+
init_repoConnect();
|
|
3517
|
+
SYNKRO_DIR = join5(homedir4(), ".synkro");
|
|
3518
|
+
HOOKS_DIR = join5(SYNKRO_DIR, "hooks");
|
|
3519
|
+
BIN_DIR = join5(SYNKRO_DIR, "bin");
|
|
3520
|
+
CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
|
|
3521
|
+
GRADER_DAEMON_PATH = join5(BIN_DIR, "grader_daemon.py");
|
|
3522
|
+
GRADER_PRIMER_EDIT_PATH = join5(SYNKRO_DIR, "grader-primer-edit.txt");
|
|
3523
|
+
GRADER_PRIMER_BASH_PATH = join5(SYNKRO_DIR, "grader-primer-bash.txt");
|
|
2443
3524
|
}
|
|
2444
3525
|
});
|
|
2445
3526
|
|
|
@@ -2515,11 +3596,11 @@ var status_exports = {};
|
|
|
2515
3596
|
__export(status_exports, {
|
|
2516
3597
|
statusCommand: () => statusCommand
|
|
2517
3598
|
});
|
|
2518
|
-
import { existsSync as
|
|
3599
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
2519
3600
|
import { homedir as homedir5 } from "os";
|
|
2520
|
-
import { join as
|
|
3601
|
+
import { join as join6 } from "path";
|
|
2521
3602
|
function readConfigEnv() {
|
|
2522
|
-
if (!
|
|
3603
|
+
if (!existsSync7(CONFIG_PATH2)) return {};
|
|
2523
3604
|
const out = {};
|
|
2524
3605
|
const raw = readFileSync5(CONFIG_PATH2, "utf-8");
|
|
2525
3606
|
for (const line of raw.split("\n")) {
|
|
@@ -2545,21 +3626,21 @@ function statusCommand() {
|
|
|
2545
3626
|
console.log("Authentication: \u2717 not logged in (run: synkro-cli login)");
|
|
2546
3627
|
}
|
|
2547
3628
|
console.log();
|
|
2548
|
-
const
|
|
3629
|
+
const config2 = readConfigEnv();
|
|
2549
3630
|
console.log("Config:");
|
|
2550
|
-
console.log(` gateway: ${
|
|
2551
|
-
console.log(` credentials: ${
|
|
2552
|
-
console.log(` tier: ${
|
|
3631
|
+
console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL ?? "(unset)"}`);
|
|
3632
|
+
console.log(` credentials: ${config2.SYNKRO_CREDENTIALS_PATH ?? "(unset)"}`);
|
|
3633
|
+
console.log(` tier: ${config2.SYNKRO_TIER ?? "(unset)"}`);
|
|
2553
3634
|
const info2 = getUserInfo();
|
|
2554
|
-
const userId = info2?.id ??
|
|
2555
|
-
const tierCacheFile =
|
|
2556
|
-
let inferenceTier =
|
|
2557
|
-
if (!inferenceTier &&
|
|
3635
|
+
const userId = info2?.id ?? config2.SYNKRO_USER_ID ?? "default";
|
|
3636
|
+
const tierCacheFile = join6(SYNKRO_DIR2, `.tier-cache-${userId}`);
|
|
3637
|
+
let inferenceTier = config2.SYNKRO_INFERENCE_TIER || null;
|
|
3638
|
+
if (!inferenceTier && existsSync7(tierCacheFile)) {
|
|
2558
3639
|
inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
|
|
2559
3640
|
}
|
|
2560
3641
|
const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
|
|
2561
3642
|
console.log(` inference: ${tierLabel}`);
|
|
2562
|
-
console.log(` version: ${
|
|
3643
|
+
console.log(` version: ${config2.SYNKRO_VERSION ?? "(unset)"}`);
|
|
2563
3644
|
console.log();
|
|
2564
3645
|
const agents = detectAgents();
|
|
2565
3646
|
console.log("Detected agents:");
|
|
@@ -2582,19 +3663,19 @@ function statusCommand() {
|
|
|
2582
3663
|
}
|
|
2583
3664
|
}
|
|
2584
3665
|
console.log();
|
|
2585
|
-
const bashScript =
|
|
2586
|
-
const bashFollowupScript =
|
|
2587
|
-
const editPrecheckScript =
|
|
2588
|
-
const editCaptureScript =
|
|
2589
|
-
const stopSummaryScript =
|
|
2590
|
-
const sessionStartScript =
|
|
3666
|
+
const bashScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-judge.sh");
|
|
3667
|
+
const bashFollowupScript = join6(SYNKRO_DIR2, "hooks", "cc-bash-followup.sh");
|
|
3668
|
+
const editPrecheckScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-precheck.sh");
|
|
3669
|
+
const editCaptureScript = join6(SYNKRO_DIR2, "hooks", "cc-edit-capture.sh");
|
|
3670
|
+
const stopSummaryScript = join6(SYNKRO_DIR2, "hooks", "cc-stop-summary.sh");
|
|
3671
|
+
const sessionStartScript = join6(SYNKRO_DIR2, "hooks", "cc-session-start.sh");
|
|
2591
3672
|
console.log("Hook scripts:");
|
|
2592
|
-
console.log(` ${
|
|
2593
|
-
console.log(` ${
|
|
2594
|
-
console.log(` ${
|
|
2595
|
-
console.log(` ${
|
|
2596
|
-
console.log(` ${
|
|
2597
|
-
console.log(` ${
|
|
3673
|
+
console.log(` ${existsSync7(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
|
|
3674
|
+
console.log(` ${existsSync7(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
|
|
3675
|
+
console.log(` ${existsSync7(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
|
|
3676
|
+
console.log(` ${existsSync7(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
|
|
3677
|
+
console.log(` ${existsSync7(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
|
|
3678
|
+
console.log(` ${existsSync7(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
|
|
2598
3679
|
console.log();
|
|
2599
3680
|
const mcp = inspectMcpConfig();
|
|
2600
3681
|
console.log("Guardrails MCP server (Claude Code):");
|
|
@@ -2614,165 +3695,88 @@ var init_status = __esm({
|
|
|
2614
3695
|
init_agentDetect();
|
|
2615
3696
|
init_ccHookConfig();
|
|
2616
3697
|
init_mcpConfig();
|
|
2617
|
-
SYNKRO_DIR2 =
|
|
2618
|
-
CONFIG_PATH2 =
|
|
3698
|
+
SYNKRO_DIR2 = join6(homedir5(), ".synkro");
|
|
3699
|
+
CONFIG_PATH2 = join6(SYNKRO_DIR2, "config.env");
|
|
2619
3700
|
}
|
|
2620
3701
|
});
|
|
2621
3702
|
|
|
2622
|
-
// cli/
|
|
2623
|
-
var
|
|
2624
|
-
|
|
2625
|
-
|
|
3703
|
+
// cli/commands/link.ts
|
|
3704
|
+
var link_exports = {};
|
|
3705
|
+
__export(link_exports, {
|
|
3706
|
+
linkCommand: () => linkCommand
|
|
3707
|
+
});
|
|
3708
|
+
async function linkCommand() {
|
|
3709
|
+
if (!isAuthenticated()) {
|
|
3710
|
+
console.error("Not authenticated. Run `synkro install` or `synkro login` first.");
|
|
3711
|
+
process.exit(1);
|
|
3712
|
+
}
|
|
3713
|
+
await ensureValidToken();
|
|
3714
|
+
await promptRepoConnection();
|
|
3715
|
+
}
|
|
3716
|
+
var init_link = __esm({
|
|
3717
|
+
"cli/commands/link.ts"() {
|
|
2626
3718
|
"use strict";
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
pull_request:
|
|
2630
|
-
types: [opened, synchronize, reopened]
|
|
2631
|
-
|
|
2632
|
-
jobs:
|
|
2633
|
-
scan:
|
|
2634
|
-
runs-on: ubuntu-latest
|
|
2635
|
-
permissions:
|
|
2636
|
-
contents: read
|
|
2637
|
-
pull-requests: write
|
|
2638
|
-
checks: write
|
|
2639
|
-
steps:
|
|
2640
|
-
- uses: actions/checkout@v4
|
|
2641
|
-
with:
|
|
2642
|
-
fetch-depth: 0
|
|
2643
|
-
|
|
2644
|
-
- name: Cache npm globals
|
|
2645
|
-
id: cache-npm-global
|
|
2646
|
-
uses: actions/cache@v4
|
|
2647
|
-
with:
|
|
2648
|
-
path: ~/.npm-global
|
|
2649
|
-
key: synkro-cli-\${{ runner.os }}-v1
|
|
2650
|
-
|
|
2651
|
-
- name: Install Synkro CLI + Claude Code CLI
|
|
2652
|
-
run: |
|
|
2653
|
-
npm config set prefix ~/.npm-global
|
|
2654
|
-
npm install -g @synkro-sh/cli @anthropic-ai/claude-code
|
|
2655
|
-
echo "~/.npm-global/bin" >> $GITHUB_PATH
|
|
2656
|
-
|
|
2657
|
-
- name: Run Synkro PR scan
|
|
2658
|
-
run: synkro-cli scan-pr
|
|
2659
|
-
env:
|
|
2660
|
-
CLAUDE_CODE_OAUTH_TOKEN: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
2661
|
-
SYNKRO_API_KEY: \${{ secrets.SYNKRO_API_KEY }}
|
|
2662
|
-
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2663
|
-
SYNKRO_PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2664
|
-
SYNKRO_REPO: \${{ github.repository }}
|
|
2665
|
-
SYNKRO_SHA: \${{ github.event.pull_request.head.sha }}
|
|
2666
|
-
SYNKRO_GATEWAY_URL: \${{ vars.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh' }}
|
|
2667
|
-
`;
|
|
2668
|
-
WORKFLOW_PATH = ".github/workflows/synkro.yml";
|
|
3719
|
+
init_stub();
|
|
3720
|
+
init_repoConnect();
|
|
2669
3721
|
}
|
|
2670
3722
|
});
|
|
2671
3723
|
|
|
2672
|
-
// cli/
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
const encryptedBytes = sodium.crypto_box_seal(messageBytes, keyBytes);
|
|
2681
|
-
return sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
|
3724
|
+
// cli/commands/unlink.ts
|
|
3725
|
+
var unlink_exports = {};
|
|
3726
|
+
__export(unlink_exports, {
|
|
3727
|
+
unlinkCommand: () => unlinkCommand
|
|
3728
|
+
});
|
|
3729
|
+
import { createInterface as createInterface2 } from "readline";
|
|
3730
|
+
function ask2(rl, question) {
|
|
3731
|
+
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
2682
3732
|
}
|
|
2683
|
-
async function
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3733
|
+
async function unlinkCommand() {
|
|
3734
|
+
if (!isAuthenticated()) {
|
|
3735
|
+
console.error("Not authenticated. Run `synkro install` or `synkro login` first.");
|
|
3736
|
+
process.exit(1);
|
|
3737
|
+
}
|
|
3738
|
+
await ensureValidToken();
|
|
3739
|
+
const projects = await listProjects();
|
|
3740
|
+
const linked = [];
|
|
3741
|
+
for (const p of projects) {
|
|
3742
|
+
for (const r of p.repos || []) {
|
|
3743
|
+
if (r.full_name && r.id) {
|
|
3744
|
+
linked.push({ projectId: p.id, projectName: p.name, repoId: r.id, fullName: r.full_name });
|
|
3745
|
+
}
|
|
2690
3746
|
}
|
|
2691
|
-
});
|
|
2692
|
-
if (!resp.ok) {
|
|
2693
|
-
const text = await resp.text().catch(() => "");
|
|
2694
|
-
throw new Error(`GitHub API ${resp.status} fetching public key for ${owner}/${repo}: ${text.slice(0, 200)}`);
|
|
2695
3747
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
const encryptedValue = await encryptSecret(publicKey.key, secretValue);
|
|
2700
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/${encodeURIComponent(secretName)}`;
|
|
2701
|
-
const resp = await fetch(url, {
|
|
2702
|
-
method: "PUT",
|
|
2703
|
-
headers: {
|
|
2704
|
-
Authorization: `Bearer ${opts.token}`,
|
|
2705
|
-
Accept: "application/vnd.github+json",
|
|
2706
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
2707
|
-
"Content-Type": "application/json"
|
|
2708
|
-
},
|
|
2709
|
-
body: JSON.stringify({
|
|
2710
|
-
encrypted_value: encryptedValue,
|
|
2711
|
-
key_id: publicKey.key_id
|
|
2712
|
-
})
|
|
2713
|
-
});
|
|
2714
|
-
if (!resp.ok) {
|
|
2715
|
-
const text = await resp.text().catch(() => "");
|
|
2716
|
-
throw new Error(`GitHub API ${resp.status} setting secret ${secretName}: ${text.slice(0, 200)}`);
|
|
3748
|
+
if (linked.length === 0) {
|
|
3749
|
+
console.log("No linked repos found.");
|
|
3750
|
+
return;
|
|
2717
3751
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
});
|
|
2731
|
-
if (!resp.ok) {
|
|
2732
|
-
throw new Error(`GitHub API ${resp.status} listing repos`);
|
|
3752
|
+
console.log("\nLinked repos:\n");
|
|
3753
|
+
linked.forEach((r, i) => {
|
|
3754
|
+
console.log(` ${i + 1}. ${r.fullName} (${r.projectName})`);
|
|
3755
|
+
});
|
|
3756
|
+
console.log();
|
|
3757
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3758
|
+
try {
|
|
3759
|
+
const selection = await ask2(rl, " Select repos to unlink (comma-separated numbers): ");
|
|
3760
|
+
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((n) => !isNaN(n) && n >= 0 && n < linked.length);
|
|
3761
|
+
if (indices.length === 0) {
|
|
3762
|
+
console.log(" No repos selected.");
|
|
3763
|
+
return;
|
|
2733
3764
|
}
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3765
|
+
for (const idx of indices) {
|
|
3766
|
+
const r = linked[idx];
|
|
3767
|
+
await unlinkRepo(r.projectId, r.repoId);
|
|
3768
|
+
console.log(` \u2713 Unlinked ${r.fullName} from ${r.projectName}`);
|
|
2738
3769
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
}
|
|
2742
|
-
return repos;
|
|
2743
|
-
}
|
|
2744
|
-
async function pushSecretsToRepo(opts, owner, repo, secrets) {
|
|
2745
|
-
const pubkey = await getRepoPublicKey(opts, owner, repo);
|
|
2746
|
-
await putRepoSecret(opts, owner, repo, "CLAUDE_CODE_OAUTH_TOKEN", secrets.claudeCodeOauthToken, pubkey);
|
|
2747
|
-
await putRepoSecret(opts, owner, repo, "SYNKRO_API_KEY", secrets.synkroApiKey, pubkey);
|
|
2748
|
-
}
|
|
2749
|
-
function writeWorkflowFile(repoRootPath) {
|
|
2750
|
-
const workflowDir = join6(repoRootPath, ".github", "workflows");
|
|
2751
|
-
mkdirSync5(workflowDir, { recursive: true });
|
|
2752
|
-
const workflowFile = join6(workflowDir, "synkro.yml");
|
|
2753
|
-
writeFileSync5(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
|
|
2754
|
-
return workflowFile;
|
|
2755
|
-
}
|
|
2756
|
-
function findGitRoot(startCwd) {
|
|
2757
|
-
let cur = startCwd;
|
|
2758
|
-
while (cur && cur !== "/") {
|
|
2759
|
-
if (existsSync7(join6(cur, ".git"))) return cur;
|
|
2760
|
-
const parent = join6(cur, "..");
|
|
2761
|
-
if (parent === cur) break;
|
|
2762
|
-
cur = parent;
|
|
3770
|
+
} finally {
|
|
3771
|
+
rl.close();
|
|
2763
3772
|
}
|
|
2764
|
-
|
|
3773
|
+
console.log();
|
|
2765
3774
|
}
|
|
2766
|
-
var
|
|
2767
|
-
|
|
2768
|
-
"cli/installer/githubSetup.ts"() {
|
|
3775
|
+
var init_unlink = __esm({
|
|
3776
|
+
"cli/commands/unlink.ts"() {
|
|
2769
3777
|
"use strict";
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
CLAUDE_OAUTH: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
2773
|
-
SYNKRO_API_KEY: "SYNKRO_API_KEY"
|
|
2774
|
-
};
|
|
2775
|
-
WORKFLOW_RELATIVE_PATH = WORKFLOW_PATH;
|
|
3778
|
+
init_stub();
|
|
3779
|
+
init_projects();
|
|
2776
3780
|
}
|
|
2777
3781
|
});
|
|
2778
3782
|
|
|
@@ -2781,7 +3785,7 @@ var setupGithub_exports = {};
|
|
|
2781
3785
|
__export(setupGithub_exports, {
|
|
2782
3786
|
setupGithubCommand: () => setupGithubCommand
|
|
2783
3787
|
});
|
|
2784
|
-
import { createInterface } from "readline/promises";
|
|
3788
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
2785
3789
|
import { stdin as input, stdout as output } from "process";
|
|
2786
3790
|
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
2787
3791
|
import { homedir as homedir6 } from "os";
|
|
@@ -2832,8 +3836,8 @@ async function setupGithubCommand() {
|
|
|
2832
3836
|
console.error("Not authenticated. Run `synkro-cli login` first.");
|
|
2833
3837
|
process.exit(1);
|
|
2834
3838
|
}
|
|
2835
|
-
const
|
|
2836
|
-
const gatewayUrl = (
|
|
3839
|
+
const config2 = readConfig();
|
|
3840
|
+
const gatewayUrl = (config2.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
2837
3841
|
const jwt2 = getAccessToken();
|
|
2838
3842
|
if (!jwt2) {
|
|
2839
3843
|
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro-cli login`.");
|
|
@@ -2862,7 +3866,7 @@ async function setupGithubCommand() {
|
|
|
2862
3866
|
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
2863
3867
|
process.exit(1);
|
|
2864
3868
|
}
|
|
2865
|
-
const rl =
|
|
3869
|
+
const rl = createInterface3({ input, output });
|
|
2866
3870
|
console.log("Synkro PR scan setup\n");
|
|
2867
3871
|
console.log("Requirements:");
|
|
2868
3872
|
console.log(" \u2022 Claude Code Pro or Max subscription (for `claude setup-token`)");
|
|
@@ -2969,7 +3973,7 @@ var scanPr_exports = {};
|
|
|
2969
3973
|
__export(scanPr_exports, {
|
|
2970
3974
|
scanPrCommand: () => scanPrCommand
|
|
2971
3975
|
});
|
|
2972
|
-
import { execSync as
|
|
3976
|
+
import { execSync as execSync4, spawn } from "child_process";
|
|
2973
3977
|
function parseMatchSpec(condition) {
|
|
2974
3978
|
if (!condition.startsWith("match_spec:")) return null;
|
|
2975
3979
|
try {
|
|
@@ -3075,7 +4079,7 @@ function shouldSkipFile(filename) {
|
|
|
3075
4079
|
return SKIP_FILE_PATTERNS.some((p) => p.test(filename));
|
|
3076
4080
|
}
|
|
3077
4081
|
function ghJson(args2) {
|
|
3078
|
-
const out =
|
|
4082
|
+
const out = execSync4(`gh ${args2.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`, {
|
|
3079
4083
|
encoding: "utf-8",
|
|
3080
4084
|
maxBuffer: 16 * 1024 * 1024
|
|
3081
4085
|
});
|
|
@@ -3191,7 +4195,7 @@ ${finding.description}
|
|
|
3191
4195
|
|
|
3192
4196
|
**Fix:** ${finding.fix}`;
|
|
3193
4197
|
try {
|
|
3194
|
-
|
|
4198
|
+
execSync4(
|
|
3195
4199
|
`gh api -X POST /repos/${repo}/pulls/${prNumber}/comments -f body=${JSON.stringify(body)} -f commit_id=${sha} -f path=${JSON.stringify(finding.file)} -F line=${finding.line} -f side=RIGHT`,
|
|
3196
4200
|
{ encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"] }
|
|
3197
4201
|
);
|
|
@@ -3213,7 +4217,7 @@ function postCheckRun(repo, sha, conclusion, findings) {
|
|
|
3213
4217
|
}
|
|
3214
4218
|
});
|
|
3215
4219
|
try {
|
|
3216
|
-
|
|
4220
|
+
execSync4(`gh api -X POST /repos/${repo}/check-runs --input -`, {
|
|
3217
4221
|
encoding: "utf-8",
|
|
3218
4222
|
input: body,
|
|
3219
4223
|
stdio: ["pipe", "ignore", "pipe"]
|
|
@@ -3431,6 +4435,43 @@ var init_disconnect = __esm({
|
|
|
3431
4435
|
}
|
|
3432
4436
|
});
|
|
3433
4437
|
|
|
4438
|
+
// cli/commands/uninstall.ts
|
|
4439
|
+
var uninstall_exports = {};
|
|
4440
|
+
__export(uninstall_exports, {
|
|
4441
|
+
uninstallCommand: () => uninstallCommand
|
|
4442
|
+
});
|
|
4443
|
+
function uninstallCommand() {
|
|
4444
|
+
console.log("Uninstalling Synkro...\n");
|
|
4445
|
+
disconnectCommand(["--purge"]);
|
|
4446
|
+
console.log("\nTo reinstall later: synkro install");
|
|
4447
|
+
}
|
|
4448
|
+
var init_uninstall = __esm({
|
|
4449
|
+
"cli/commands/uninstall.ts"() {
|
|
4450
|
+
"use strict";
|
|
4451
|
+
init_disconnect();
|
|
4452
|
+
}
|
|
4453
|
+
});
|
|
4454
|
+
|
|
4455
|
+
// cli/commands/reinstall.ts
|
|
4456
|
+
var reinstall_exports = {};
|
|
4457
|
+
__export(reinstall_exports, {
|
|
4458
|
+
reinstallCommand: () => reinstallCommand
|
|
4459
|
+
});
|
|
4460
|
+
async function reinstallCommand() {
|
|
4461
|
+
console.log("Reinstalling Synkro...\n");
|
|
4462
|
+
disconnectCommand(["--purge"]);
|
|
4463
|
+
console.log("");
|
|
4464
|
+
await installCommand({ force: true });
|
|
4465
|
+
console.log("\n\u2713 Synkro reinstalled.");
|
|
4466
|
+
}
|
|
4467
|
+
var init_reinstall = __esm({
|
|
4468
|
+
"cli/commands/reinstall.ts"() {
|
|
4469
|
+
"use strict";
|
|
4470
|
+
init_disconnect();
|
|
4471
|
+
init_install();
|
|
4472
|
+
}
|
|
4473
|
+
});
|
|
4474
|
+
|
|
3434
4475
|
// cli/bootstrap.js
|
|
3435
4476
|
import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
|
|
3436
4477
|
import { resolve } from "path";
|
|
@@ -3466,10 +4507,14 @@ Commands:
|
|
|
3466
4507
|
login Authenticate with Synkro (browser OAuth via WorkOS)
|
|
3467
4508
|
logout Clear local credentials
|
|
3468
4509
|
status Show current setup state
|
|
4510
|
+
link Link repos to a Synkro project (local git or GitHub OAuth)
|
|
4511
|
+
unlink Remove repo links from Synkro projects
|
|
3469
4512
|
setup-github Configure GitHub PR scanning (push secrets + workflow file)
|
|
3470
4513
|
scan-pr Run a PR scan (used by GitHub Actions, not for direct invocation)
|
|
3471
4514
|
update Refresh hook configs and judge prompts
|
|
3472
4515
|
disconnect [--purge] Remove Synkro hooks from agents (--purge also removes ~/.synkro)
|
|
4516
|
+
uninstall Fully remove Synkro from this machine
|
|
4517
|
+
reinstall Clean uninstall + fresh install
|
|
3473
4518
|
help Show this message
|
|
3474
4519
|
|
|
3475
4520
|
Quick start:
|
|
@@ -3502,6 +4547,16 @@ async function main() {
|
|
|
3502
4547
|
statusCommand2();
|
|
3503
4548
|
break;
|
|
3504
4549
|
}
|
|
4550
|
+
case "link": {
|
|
4551
|
+
const { linkCommand: linkCommand2 } = await Promise.resolve().then(() => (init_link(), link_exports));
|
|
4552
|
+
await linkCommand2();
|
|
4553
|
+
break;
|
|
4554
|
+
}
|
|
4555
|
+
case "unlink": {
|
|
4556
|
+
const { unlinkCommand: unlinkCommand2 } = await Promise.resolve().then(() => (init_unlink(), unlink_exports));
|
|
4557
|
+
await unlinkCommand2();
|
|
4558
|
+
break;
|
|
4559
|
+
}
|
|
3505
4560
|
case "setup-github": {
|
|
3506
4561
|
const { setupGithubCommand: setupGithubCommand2 } = await Promise.resolve().then(() => (init_setupGithub(), setupGithub_exports));
|
|
3507
4562
|
await setupGithubCommand2();
|
|
@@ -3522,6 +4577,16 @@ async function main() {
|
|
|
3522
4577
|
disconnectCommand2(subArgs);
|
|
3523
4578
|
break;
|
|
3524
4579
|
}
|
|
4580
|
+
case "uninstall": {
|
|
4581
|
+
const { uninstallCommand: uninstallCommand2 } = await Promise.resolve().then(() => (init_uninstall(), uninstall_exports));
|
|
4582
|
+
uninstallCommand2();
|
|
4583
|
+
break;
|
|
4584
|
+
}
|
|
4585
|
+
case "reinstall": {
|
|
4586
|
+
const { reinstallCommand: reinstallCommand2 } = await Promise.resolve().then(() => (init_reinstall(), reinstall_exports));
|
|
4587
|
+
await reinstallCommand2();
|
|
4588
|
+
break;
|
|
4589
|
+
}
|
|
3525
4590
|
case "help":
|
|
3526
4591
|
case "--help":
|
|
3527
4592
|
case "-h":
|