hillclimb 0.1.6 → 0.1.8
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/README.md +1 -1
- package/dist/cli.js +412 -336
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,29 @@ import os from "os";
|
|
|
30
30
|
import path from "path";
|
|
31
31
|
var CONFIG_DIR = path.join(os.homedir(), ".hillclimb");
|
|
32
32
|
var CONFIG_PATH = path.join(CONFIG_DIR, "projects.json");
|
|
33
|
+
function normalizeProjectConfig(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object") return null;
|
|
35
|
+
const config = raw;
|
|
36
|
+
const projectId = config.projectId ?? config.workspaceId;
|
|
37
|
+
const projectSlug = config.projectSlug ?? config.workspaceSlug;
|
|
38
|
+
const projectName = config.projectName ?? config.workspaceName;
|
|
39
|
+
if (!config.apiBaseUrl || !projectId || !projectSlug || !projectName || !config.contributionTypeSlug || !config.contributionTypeName || typeof config.autoSubmit !== "boolean") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
45
|
+
projectId,
|
|
46
|
+
projectSlug,
|
|
47
|
+
projectName,
|
|
48
|
+
workspaceId: config.workspaceId ?? projectId,
|
|
49
|
+
workspaceSlug: config.workspaceSlug ?? projectSlug,
|
|
50
|
+
workspaceName: config.workspaceName ?? projectName,
|
|
51
|
+
contributionTypeSlug: config.contributionTypeSlug,
|
|
52
|
+
contributionTypeName: config.contributionTypeName,
|
|
53
|
+
autoSubmit: config.autoSubmit
|
|
54
|
+
};
|
|
55
|
+
}
|
|
33
56
|
function configDir() {
|
|
34
57
|
return CONFIG_DIR;
|
|
35
58
|
}
|
|
@@ -43,7 +66,12 @@ async function loadProjects() {
|
|
|
43
66
|
if (!parsed.projects || typeof parsed.projects !== "object") {
|
|
44
67
|
return { projects: {} };
|
|
45
68
|
}
|
|
46
|
-
|
|
69
|
+
const projects = {};
|
|
70
|
+
for (const [repoRoot, config] of Object.entries(parsed.projects)) {
|
|
71
|
+
const normalized = normalizeProjectConfig(config);
|
|
72
|
+
if (normalized) projects[repoRoot] = normalized;
|
|
73
|
+
}
|
|
74
|
+
return { projects };
|
|
47
75
|
} catch {
|
|
48
76
|
return { projects: {} };
|
|
49
77
|
}
|
|
@@ -58,7 +86,9 @@ async function saveProjects(file) {
|
|
|
58
86
|
}
|
|
59
87
|
async function upsertProject(repoRoot, config) {
|
|
60
88
|
const file = await loadProjects();
|
|
61
|
-
|
|
89
|
+
const normalized = normalizeProjectConfig(config);
|
|
90
|
+
if (!normalized) throw new Error("Invalid project config.");
|
|
91
|
+
file.projects[path.resolve(repoRoot)] = normalized;
|
|
62
92
|
await saveProjects(file);
|
|
63
93
|
}
|
|
64
94
|
async function findProjectForCwd(cwd) {
|
|
@@ -398,30 +428,51 @@ var PlatformClient = class {
|
|
|
398
428
|
};
|
|
399
429
|
}
|
|
400
430
|
async listProjects() {
|
|
401
|
-
const data = await this.request(
|
|
402
|
-
|
|
403
|
-
"/api/v1/projects"
|
|
404
|
-
);
|
|
405
|
-
return data.workspaces ?? [];
|
|
431
|
+
const data = await this.request("GET", "/api/v1/projects");
|
|
432
|
+
return data.projects ?? data.workspaces ?? [];
|
|
406
433
|
}
|
|
407
|
-
async
|
|
408
|
-
|
|
409
|
-
"GET",
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
434
|
+
async getProjectBySlug(slug) {
|
|
435
|
+
try {
|
|
436
|
+
const data = await this.request("GET", `/api/v1/projects/by-slug/${encodeURIComponent(slug)}`);
|
|
437
|
+
const project = data.project ?? data.workspace;
|
|
438
|
+
if (!project) throw new PlatformError("Project response was empty.");
|
|
439
|
+
return project;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (!(err instanceof PlatformError) || err.status !== 404) throw err;
|
|
442
|
+
const data = await this.request(
|
|
443
|
+
"GET",
|
|
444
|
+
`/api/v1/workspaces/${encodeURIComponent(slug)}`
|
|
445
|
+
);
|
|
446
|
+
return data.workspace;
|
|
447
|
+
}
|
|
413
448
|
}
|
|
414
|
-
async listContributionTypes(
|
|
415
|
-
|
|
416
|
-
|
|
449
|
+
async listContributionTypes(projectId) {
|
|
450
|
+
try {
|
|
451
|
+
const data = await this.request("GET", `/api/v1/projects/${projectId}/contribution-types`);
|
|
452
|
+
return data.contributionTypes ?? [];
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (!(err instanceof PlatformError) || err.status !== 404) throw err;
|
|
455
|
+
const data = await this.request("GET", `/api/v1/workspaces/${projectId}/contribution-types`);
|
|
456
|
+
return data.contributionTypes ?? [];
|
|
457
|
+
}
|
|
417
458
|
}
|
|
418
|
-
async createContribution(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
459
|
+
async createContribution(projectId, input) {
|
|
460
|
+
try {
|
|
461
|
+
const data = await this.request(
|
|
462
|
+
"POST",
|
|
463
|
+
`/api/v1/projects/${projectId}/contributions`,
|
|
464
|
+
input
|
|
465
|
+
);
|
|
466
|
+
return data.contribution;
|
|
467
|
+
} catch (err) {
|
|
468
|
+
if (!(err instanceof PlatformError) || err.status !== 404) throw err;
|
|
469
|
+
const data = await this.request(
|
|
470
|
+
"POST",
|
|
471
|
+
`/api/v1/workspaces/${projectId}/contributions`,
|
|
472
|
+
input
|
|
473
|
+
);
|
|
474
|
+
return data.contribution;
|
|
475
|
+
}
|
|
425
476
|
}
|
|
426
477
|
async createUpload(contributionId, input) {
|
|
427
478
|
return this.request(
|
|
@@ -501,6 +552,7 @@ import os2 from "os";
|
|
|
501
552
|
import path5 from "path";
|
|
502
553
|
var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
|
|
503
554
|
var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
|
|
555
|
+
var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
|
|
504
556
|
var TOOLS = [
|
|
505
557
|
{
|
|
506
558
|
tool: "claude",
|
|
@@ -508,7 +560,11 @@ var TOOLS = [
|
|
|
508
560
|
settingsFile: ".claude/settings.local.json",
|
|
509
561
|
format: "claude",
|
|
510
562
|
events: [
|
|
511
|
-
{
|
|
563
|
+
{
|
|
564
|
+
eventName: "SessionEnd",
|
|
565
|
+
command: HOOK_CMD("upload"),
|
|
566
|
+
timeout: CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS
|
|
567
|
+
},
|
|
512
568
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("claude") },
|
|
513
569
|
{ eventName: "Stop", command: GIT_TRACES_CMD("claude") },
|
|
514
570
|
{ eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
|
|
@@ -593,11 +649,7 @@ function isDir(p7) {
|
|
|
593
649
|
}
|
|
594
650
|
function copilotChatDetect() {
|
|
595
651
|
const home = os2.homedir();
|
|
596
|
-
const suffix = path5.join(
|
|
597
|
-
"User",
|
|
598
|
-
"globalStorage",
|
|
599
|
-
"github.copilot-chat"
|
|
600
|
-
);
|
|
652
|
+
const suffix = path5.join("User", "globalStorage", "github.copilot-chat");
|
|
601
653
|
const candidates = [
|
|
602
654
|
path5.join(home, "Library", "Application Support", "Code", suffix),
|
|
603
655
|
path5.join(home, ".config", "Code", suffix),
|
|
@@ -605,6 +657,10 @@ function copilotChatDetect() {
|
|
|
605
657
|
].filter((p7) => p7 !== null);
|
|
606
658
|
return candidates.some(isDir);
|
|
607
659
|
}
|
|
660
|
+
function ensureHooks(settings) {
|
|
661
|
+
if (!settings.hooks) settings.hooks = {};
|
|
662
|
+
return settings.hooks;
|
|
663
|
+
}
|
|
608
664
|
function settingsPath(repoRoot, def) {
|
|
609
665
|
return path5.join(repoRoot, def.settingsFile);
|
|
610
666
|
}
|
|
@@ -624,31 +680,45 @@ async function writeJson(file, obj) {
|
|
|
624
680
|
await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
|
|
625
681
|
`);
|
|
626
682
|
}
|
|
627
|
-
function claudeHookPresent(matchers, command) {
|
|
683
|
+
function claudeHookPresent(matchers, command, options = {}) {
|
|
628
684
|
return matchers.some(
|
|
629
|
-
(m) => (m.hooks ?? []).some(
|
|
630
|
-
(h
|
|
631
|
-
|
|
685
|
+
(m) => (m.hooks ?? []).some((h) => {
|
|
686
|
+
if (h.type !== "command" || h.command !== command) return false;
|
|
687
|
+
if (options.timeout !== void 0 && h.timeout !== options.timeout) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
return true;
|
|
691
|
+
})
|
|
632
692
|
);
|
|
633
693
|
}
|
|
634
|
-
function claudeInstall(settings, eventName, command) {
|
|
635
|
-
|
|
636
|
-
if (!
|
|
637
|
-
const matchers =
|
|
638
|
-
|
|
694
|
+
function claudeInstall(settings, eventName, command, options = {}) {
|
|
695
|
+
const hooks = ensureHooks(settings);
|
|
696
|
+
if (!hooks[eventName]) hooks[eventName] = [];
|
|
697
|
+
const matchers = hooks[eventName];
|
|
698
|
+
for (const matcher of matchers) {
|
|
699
|
+
for (const hook of matcher.hooks ?? []) {
|
|
700
|
+
if (hook.type !== "command" || hook.command !== command) continue;
|
|
701
|
+
if (options.timeout === void 0 || hook.timeout === options.timeout) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
hook.timeout = options.timeout;
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
639
708
|
matchers.push({
|
|
640
709
|
matcher: "",
|
|
641
|
-
hooks: [{ type: "command", command }]
|
|
710
|
+
hooks: [{ type: "command", command, ...options }]
|
|
642
711
|
});
|
|
643
712
|
return true;
|
|
644
713
|
}
|
|
645
|
-
function claudeCheck(settings, eventName, command) {
|
|
714
|
+
function claudeCheck(settings, eventName, command, options = {}) {
|
|
646
715
|
const matchers = settings.hooks?.[eventName] ?? [];
|
|
647
|
-
return claudeHookPresent(matchers, command);
|
|
716
|
+
return claudeHookPresent(matchers, command, options);
|
|
648
717
|
}
|
|
649
718
|
function claudeUninstall(settings, eventName, command) {
|
|
650
|
-
const
|
|
651
|
-
|
|
719
|
+
const hooks = settings.hooks;
|
|
720
|
+
const matchers = hooks?.[eventName];
|
|
721
|
+
if (!hooks || !matchers || matchers.length === 0) return false;
|
|
652
722
|
let removedAny = false;
|
|
653
723
|
const next = [];
|
|
654
724
|
for (const m of matchers) {
|
|
@@ -660,10 +730,10 @@ function claudeUninstall(settings, eventName, command) {
|
|
|
660
730
|
}
|
|
661
731
|
if (!removedAny) return false;
|
|
662
732
|
if (next.length > 0) {
|
|
663
|
-
|
|
733
|
+
hooks[eventName] = next;
|
|
664
734
|
} else {
|
|
665
|
-
delete
|
|
666
|
-
if (Object.keys(
|
|
735
|
+
delete hooks[eventName];
|
|
736
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
667
737
|
}
|
|
668
738
|
return true;
|
|
669
739
|
}
|
|
@@ -672,9 +742,9 @@ function cursorHookPresent(entries, command) {
|
|
|
672
742
|
}
|
|
673
743
|
function cursorInstall(settings, eventName, command) {
|
|
674
744
|
if (!settings.version) settings.version = 1;
|
|
675
|
-
|
|
676
|
-
if (!
|
|
677
|
-
const entries =
|
|
745
|
+
const hooks = ensureHooks(settings);
|
|
746
|
+
if (!hooks[eventName]) hooks[eventName] = [];
|
|
747
|
+
const entries = hooks[eventName];
|
|
678
748
|
if (cursorHookPresent(entries, command)) return false;
|
|
679
749
|
entries.push({ command });
|
|
680
750
|
return true;
|
|
@@ -684,15 +754,16 @@ function cursorCheck(settings, eventName, command) {
|
|
|
684
754
|
return cursorHookPresent(entries, command);
|
|
685
755
|
}
|
|
686
756
|
function cursorUninstall(settings, eventName, command) {
|
|
687
|
-
const
|
|
688
|
-
|
|
757
|
+
const hooks = settings.hooks;
|
|
758
|
+
const entries = hooks?.[eventName];
|
|
759
|
+
if (!hooks || !entries || entries.length === 0) return false;
|
|
689
760
|
const next = entries.filter((e) => e.command !== command);
|
|
690
761
|
if (next.length === entries.length) return false;
|
|
691
762
|
if (next.length > 0) {
|
|
692
|
-
|
|
763
|
+
hooks[eventName] = next;
|
|
693
764
|
} else {
|
|
694
|
-
delete
|
|
695
|
-
if (Object.keys(
|
|
765
|
+
delete hooks[eventName];
|
|
766
|
+
if (Object.keys(hooks).length === 0) {
|
|
696
767
|
delete settings.hooks;
|
|
697
768
|
delete settings.version;
|
|
698
769
|
}
|
|
@@ -703,9 +774,9 @@ function copilotHookPresent(entries, command) {
|
|
|
703
774
|
return entries.some((e) => e.type === "command" && e.command === command);
|
|
704
775
|
}
|
|
705
776
|
function copilotInstall(settings, eventName, command) {
|
|
706
|
-
|
|
707
|
-
if (!
|
|
708
|
-
const entries =
|
|
777
|
+
const hooks = ensureHooks(settings);
|
|
778
|
+
if (!hooks[eventName]) hooks[eventName] = [];
|
|
779
|
+
const entries = hooks[eventName];
|
|
709
780
|
if (copilotHookPresent(entries, command)) return false;
|
|
710
781
|
entries.push({ type: "command", command });
|
|
711
782
|
return true;
|
|
@@ -715,17 +786,18 @@ function copilotCheck(settings, eventName, command) {
|
|
|
715
786
|
return copilotHookPresent(entries, command);
|
|
716
787
|
}
|
|
717
788
|
function copilotUninstall(settings, eventName, command) {
|
|
718
|
-
const
|
|
719
|
-
|
|
789
|
+
const hooks = settings.hooks;
|
|
790
|
+
const entries = hooks?.[eventName];
|
|
791
|
+
if (!hooks || !entries || entries.length === 0) return false;
|
|
720
792
|
const next = entries.filter(
|
|
721
793
|
(e) => !(e.type === "command" && e.command === command)
|
|
722
794
|
);
|
|
723
795
|
if (next.length === entries.length) return false;
|
|
724
796
|
if (next.length > 0) {
|
|
725
|
-
|
|
797
|
+
hooks[eventName] = next;
|
|
726
798
|
} else {
|
|
727
|
-
delete
|
|
728
|
-
if (Object.keys(
|
|
799
|
+
delete hooks[eventName];
|
|
800
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
729
801
|
}
|
|
730
802
|
return true;
|
|
731
803
|
}
|
|
@@ -897,24 +969,24 @@ async function opencodeCheck(file) {
|
|
|
897
969
|
return false;
|
|
898
970
|
}
|
|
899
971
|
}
|
|
900
|
-
function install(settings, format, eventName, command) {
|
|
972
|
+
function install(settings, format, eventName, command, options = {}) {
|
|
901
973
|
switch (format) {
|
|
902
974
|
case "cursor":
|
|
903
975
|
return cursorInstall(settings, eventName, command);
|
|
904
976
|
case "copilot":
|
|
905
977
|
return copilotInstall(settings, eventName, command);
|
|
906
978
|
default:
|
|
907
|
-
return claudeInstall(settings, eventName, command);
|
|
979
|
+
return claudeInstall(settings, eventName, command, options);
|
|
908
980
|
}
|
|
909
981
|
}
|
|
910
|
-
function check(settings, format, eventName, command) {
|
|
982
|
+
function check(settings, format, eventName, command, options = {}) {
|
|
911
983
|
switch (format) {
|
|
912
984
|
case "cursor":
|
|
913
985
|
return cursorCheck(settings, eventName, command);
|
|
914
986
|
case "copilot":
|
|
915
987
|
return copilotCheck(settings, eventName, command);
|
|
916
988
|
default:
|
|
917
|
-
return claudeCheck(settings, eventName, command);
|
|
989
|
+
return claudeCheck(settings, eventName, command, options);
|
|
918
990
|
}
|
|
919
991
|
}
|
|
920
992
|
function uninstall(settings, format, eventName, command) {
|
|
@@ -961,7 +1033,9 @@ async function installHooksForTool(repoRoot, def) {
|
|
|
961
1033
|
mutated = true;
|
|
962
1034
|
}
|
|
963
1035
|
}
|
|
964
|
-
const added = install(settings, def.format, evt.eventName, evt.command
|
|
1036
|
+
const added = install(settings, def.format, evt.eventName, evt.command, {
|
|
1037
|
+
timeout: evt.timeout
|
|
1038
|
+
});
|
|
965
1039
|
if (added) {
|
|
966
1040
|
installed++;
|
|
967
1041
|
mutated = true;
|
|
@@ -985,7 +1059,9 @@ async function checkHooksForTool(repoRoot, def) {
|
|
|
985
1059
|
const settings = await readJson(file);
|
|
986
1060
|
const missing = [];
|
|
987
1061
|
for (const evt of def.events) {
|
|
988
|
-
if (!check(settings, def.format, evt.eventName, evt.command
|
|
1062
|
+
if (!check(settings, def.format, evt.eventName, evt.command, {
|
|
1063
|
+
timeout: evt.timeout
|
|
1064
|
+
})) {
|
|
989
1065
|
missing.push(`${evt.eventName}:${evt.command}`);
|
|
990
1066
|
}
|
|
991
1067
|
}
|
|
@@ -1246,10 +1322,10 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
|
|
|
1246
1322
|
}
|
|
1247
1323
|
async function runInit(args = []) {
|
|
1248
1324
|
const forceLogin = args.includes("--login");
|
|
1249
|
-
const
|
|
1325
|
+
const projectSlugFlag = parseStringFlag(args, "project") ?? parseStringFlag(args, "workspace");
|
|
1250
1326
|
appendLog(
|
|
1251
1327
|
"info",
|
|
1252
|
-
`init: started (cwd=${process.cwd()}, forceLogin=${forceLogin},
|
|
1328
|
+
`init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, projectSlug=${projectSlugFlag ?? "<none>"})`
|
|
1253
1329
|
);
|
|
1254
1330
|
p3.intro("hillclimb");
|
|
1255
1331
|
const repo = detectRepoRoot();
|
|
@@ -1270,8 +1346,8 @@ async function runInit(args = []) {
|
|
|
1270
1346
|
bootstrap = await tryReuseIdentity(apiBaseUrl, client, saved);
|
|
1271
1347
|
}
|
|
1272
1348
|
if (!bootstrap) {
|
|
1273
|
-
let method =
|
|
1274
|
-
if (IS_TTY && !
|
|
1349
|
+
let method = projectSlugFlag ? "web" : "email";
|
|
1350
|
+
if (IS_TTY && !projectSlugFlag) {
|
|
1275
1351
|
const selected = await p3.select({
|
|
1276
1352
|
message: "How would you like to sign in?",
|
|
1277
1353
|
options: [
|
|
@@ -1331,13 +1407,13 @@ async function runInit(args = []) {
|
|
|
1331
1407
|
);
|
|
1332
1408
|
}
|
|
1333
1409
|
}
|
|
1334
|
-
let
|
|
1335
|
-
if (
|
|
1410
|
+
let project;
|
|
1411
|
+
if (projectSlugFlag) {
|
|
1336
1412
|
const resolved = await withSpinner(
|
|
1337
|
-
`Looking up project "${
|
|
1413
|
+
`Looking up project "${projectSlugFlag}"...`,
|
|
1338
1414
|
(w) => `Found project: ${w.name}`,
|
|
1339
|
-
`Could not find project "${
|
|
1340
|
-
() => client.
|
|
1415
|
+
`Could not find project "${projectSlugFlag}".`,
|
|
1416
|
+
() => client.getProjectBySlug(projectSlugFlag)
|
|
1341
1417
|
);
|
|
1342
1418
|
if (resolved.status !== "active") {
|
|
1343
1419
|
p3.log.error(
|
|
@@ -1346,21 +1422,21 @@ async function runInit(args = []) {
|
|
|
1346
1422
|
p3.outro("Aborted.");
|
|
1347
1423
|
process.exit(1);
|
|
1348
1424
|
}
|
|
1349
|
-
|
|
1425
|
+
project = resolved;
|
|
1350
1426
|
appendLog(
|
|
1351
1427
|
"info",
|
|
1352
|
-
`init: project pre-scoped (slug=${
|
|
1428
|
+
`init: project pre-scoped (slug=${projectSlugFlag}, id=${resolved.id})`
|
|
1353
1429
|
);
|
|
1354
1430
|
} else {
|
|
1355
|
-
const
|
|
1431
|
+
const projects = await withSpinner(
|
|
1356
1432
|
"Loading projects...",
|
|
1357
1433
|
(ws) => `Found ${ws.length} project(s).`,
|
|
1358
1434
|
"Could not list projects.",
|
|
1359
1435
|
() => client.listProjects()
|
|
1360
1436
|
);
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1363
|
-
if (
|
|
1437
|
+
const activeProjects = projects.filter((w) => w.status === "active");
|
|
1438
|
+
const projectChoices = activeProjects.length > 0 ? activeProjects : projects;
|
|
1439
|
+
if (projectChoices.length === 0) {
|
|
1364
1440
|
p3.log.error(
|
|
1365
1441
|
"You do not have access to any projects. Create one in the web UI first."
|
|
1366
1442
|
);
|
|
@@ -1368,44 +1444,44 @@ async function runInit(args = []) {
|
|
|
1368
1444
|
process.exit(1);
|
|
1369
1445
|
}
|
|
1370
1446
|
let picked;
|
|
1371
|
-
if (
|
|
1372
|
-
picked =
|
|
1447
|
+
if (projectChoices.length === 1) {
|
|
1448
|
+
picked = projectChoices[0];
|
|
1373
1449
|
p3.log.info(`Using project: ${picked.name}`);
|
|
1374
1450
|
} else {
|
|
1375
|
-
const
|
|
1451
|
+
const selectedProjectId = await p3.select({
|
|
1376
1452
|
message: "Select a project",
|
|
1377
|
-
options:
|
|
1453
|
+
options: projectChoices.map((w) => ({
|
|
1378
1454
|
value: w.id,
|
|
1379
1455
|
label: w.name,
|
|
1380
1456
|
hint: `${w.slug} (${w.status})`
|
|
1381
1457
|
}))
|
|
1382
1458
|
});
|
|
1383
|
-
if (p3.isCancel(
|
|
1459
|
+
if (p3.isCancel(selectedProjectId)) {
|
|
1384
1460
|
p3.cancel("Cancelled.");
|
|
1385
1461
|
process.exit(0);
|
|
1386
1462
|
}
|
|
1387
|
-
picked =
|
|
1463
|
+
picked = projectChoices.find((w) => w.id === selectedProjectId);
|
|
1388
1464
|
}
|
|
1389
1465
|
if (!picked) {
|
|
1390
|
-
p3.log.error("Invalid
|
|
1466
|
+
p3.log.error("Invalid project selection.");
|
|
1391
1467
|
p3.outro("Aborted.");
|
|
1392
1468
|
process.exit(1);
|
|
1393
1469
|
}
|
|
1394
|
-
|
|
1470
|
+
project = picked;
|
|
1395
1471
|
appendLog(
|
|
1396
1472
|
"info",
|
|
1397
|
-
`init: project selected (slug=${
|
|
1473
|
+
`init: project selected (slug=${project.slug}, id=${project.id})`
|
|
1398
1474
|
);
|
|
1399
1475
|
}
|
|
1400
1476
|
const types = await withSpinner(
|
|
1401
1477
|
"Loading contribution types...",
|
|
1402
1478
|
(t) => `Found ${t.length} contribution type(s).`,
|
|
1403
1479
|
"Could not list contribution types.",
|
|
1404
|
-
() => client.listContributionTypes(
|
|
1480
|
+
() => client.listContributionTypes(project.id)
|
|
1405
1481
|
);
|
|
1406
1482
|
if (types.length === 0) {
|
|
1407
1483
|
p3.log.error(
|
|
1408
|
-
`Project "${
|
|
1484
|
+
`Project "${project.name}" has no contribution types. Create one in the web UI first.`
|
|
1409
1485
|
);
|
|
1410
1486
|
p3.outro("Aborted.");
|
|
1411
1487
|
process.exit(1);
|
|
@@ -1452,10 +1528,13 @@ async function runInit(args = []) {
|
|
|
1452
1528
|
);
|
|
1453
1529
|
await upsertProject(repoRoot, {
|
|
1454
1530
|
apiBaseUrl,
|
|
1455
|
-
organizationId:
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1531
|
+
organizationId: project.organizationId,
|
|
1532
|
+
projectId: project.id,
|
|
1533
|
+
projectSlug: project.slug,
|
|
1534
|
+
projectName: project.name,
|
|
1535
|
+
workspaceId: project.id,
|
|
1536
|
+
workspaceSlug: project.slug,
|
|
1537
|
+
workspaceName: project.name,
|
|
1459
1538
|
contributionTypeSlug: type.slug,
|
|
1460
1539
|
contributionTypeName: type.name,
|
|
1461
1540
|
autoSubmit: true
|
|
@@ -1520,10 +1599,10 @@ async function runInit(args = []) {
|
|
|
1520
1599
|
}
|
|
1521
1600
|
appendLog(
|
|
1522
1601
|
"info",
|
|
1523
|
-
`init: completed (project=${
|
|
1602
|
+
`init: completed (project=${project.slug}, contributionType=${type.slug})`
|
|
1524
1603
|
);
|
|
1525
1604
|
p3.outro(
|
|
1526
|
-
`Done. Your next coding session in this repo will upload automatically to ${
|
|
1605
|
+
`Done. Your next coding session in this repo will upload automatically to ${project.name}.`
|
|
1527
1606
|
);
|
|
1528
1607
|
}
|
|
1529
1608
|
|
|
@@ -1593,10 +1672,7 @@ async function runLogin(_args = []) {
|
|
|
1593
1672
|
process.stdout.write("\r\x1B[K");
|
|
1594
1673
|
}
|
|
1595
1674
|
if (verified) {
|
|
1596
|
-
appendLog(
|
|
1597
|
-
"info",
|
|
1598
|
-
`login: reused saved login (email=${verified.email})`
|
|
1599
|
-
);
|
|
1675
|
+
appendLog("info", `login: reused saved login (email=${verified.email})`);
|
|
1600
1676
|
row(CHECK, "Signed in", cyan(verified.email));
|
|
1601
1677
|
const fresh = probe.getSessionCookie();
|
|
1602
1678
|
if (fresh) {
|
|
@@ -1737,9 +1813,7 @@ async function runLogout(args = []) {
|
|
|
1737
1813
|
footer("Nothing to do.");
|
|
1738
1814
|
return;
|
|
1739
1815
|
}
|
|
1740
|
-
println(
|
|
1741
|
-
` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`
|
|
1742
|
-
);
|
|
1816
|
+
println(` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`);
|
|
1743
1817
|
footer(
|
|
1744
1818
|
"Per-repo upload hooks still active.",
|
|
1745
1819
|
"Re-run `npx hillclimb` to re-authenticate a repo."
|
|
@@ -1804,7 +1878,7 @@ async function runStatus(args = []) {
|
|
|
1804
1878
|
} else {
|
|
1805
1879
|
row(CROSS, "Login", dim(`couldn't reach ${config.apiBaseUrl}`));
|
|
1806
1880
|
}
|
|
1807
|
-
row(CHECK, "Project", bold(config.
|
|
1881
|
+
row(CHECK, "Project", bold(config.projectName));
|
|
1808
1882
|
row(CHECK, "Contribution", bold(config.contributionTypeName));
|
|
1809
1883
|
row(CHECK, "Repo", bold(repoName));
|
|
1810
1884
|
let anyHookInstalled = false;
|
|
@@ -1845,8 +1919,8 @@ async function runStatus(args = []) {
|
|
|
1845
1919
|
if (config.organizationId) {
|
|
1846
1920
|
debugRow("Legacy org ID:", config.organizationId);
|
|
1847
1921
|
}
|
|
1848
|
-
const
|
|
1849
|
-
debugRow("Project:", `${config.
|
|
1922
|
+
const projectIdent = config.projectSlug === config.projectId ? config.projectId : `${config.projectSlug} / ${config.projectId}`;
|
|
1923
|
+
debugRow("Project:", `${config.projectName} (${projectIdent})`);
|
|
1850
1924
|
debugRow(
|
|
1851
1925
|
"Contribution type:",
|
|
1852
1926
|
`${config.contributionTypeName} (${config.contributionTypeSlug})`
|
|
@@ -10365,8 +10439,214 @@ var RedactMiddleware = class {
|
|
|
10365
10439
|
// src/middleware/index.ts
|
|
10366
10440
|
var middleware = [];
|
|
10367
10441
|
|
|
10368
|
-
// src/
|
|
10442
|
+
// src/middleware/secrets.ts
|
|
10443
|
+
import fs6 from "fs";
|
|
10369
10444
|
import path7 from "path";
|
|
10445
|
+
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
10446
|
+
"true",
|
|
10447
|
+
"false",
|
|
10448
|
+
"null",
|
|
10449
|
+
"undefined",
|
|
10450
|
+
"yes",
|
|
10451
|
+
"no",
|
|
10452
|
+
"on",
|
|
10453
|
+
"off",
|
|
10454
|
+
"localhost",
|
|
10455
|
+
"127.0.0.1",
|
|
10456
|
+
"0.0.0.0",
|
|
10457
|
+
"::1",
|
|
10458
|
+
"development",
|
|
10459
|
+
"production",
|
|
10460
|
+
"staging",
|
|
10461
|
+
"test"
|
|
10462
|
+
]);
|
|
10463
|
+
var STRUCTURAL_VARS = /* @__PURE__ */ new Set([
|
|
10464
|
+
"PATH",
|
|
10465
|
+
"HOME",
|
|
10466
|
+
"SHELL",
|
|
10467
|
+
"USER",
|
|
10468
|
+
"LOGNAME",
|
|
10469
|
+
"LANG",
|
|
10470
|
+
"TERM",
|
|
10471
|
+
"PWD",
|
|
10472
|
+
"OLDPWD",
|
|
10473
|
+
"HOSTNAME",
|
|
10474
|
+
"DISPLAY",
|
|
10475
|
+
"EDITOR",
|
|
10476
|
+
"VISUAL",
|
|
10477
|
+
"PAGER",
|
|
10478
|
+
"SHLVL",
|
|
10479
|
+
"_",
|
|
10480
|
+
"NODE_ENV"
|
|
10481
|
+
]);
|
|
10482
|
+
var SENSITIVE_PATTERNS = [
|
|
10483
|
+
/_TOKEN$/,
|
|
10484
|
+
/_SECRET$/,
|
|
10485
|
+
/_KEY$/,
|
|
10486
|
+
/_PASSWORD$/,
|
|
10487
|
+
/_API$/,
|
|
10488
|
+
/_AUTH$/,
|
|
10489
|
+
/_CREDENTIAL$/,
|
|
10490
|
+
/_PASS$/,
|
|
10491
|
+
/SECRET/,
|
|
10492
|
+
/PASSWORD/,
|
|
10493
|
+
/PRIVATE/
|
|
10494
|
+
];
|
|
10495
|
+
var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
|
|
10496
|
+
"DATABASE_URL",
|
|
10497
|
+
"REDIS_URL",
|
|
10498
|
+
"MONGO_URI",
|
|
10499
|
+
"AWS_ACCESS_KEY_ID",
|
|
10500
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
10501
|
+
"SENTRY_DSN",
|
|
10502
|
+
"SLACK_WEBHOOK_URL",
|
|
10503
|
+
"STRIPE_SK"
|
|
10504
|
+
]);
|
|
10505
|
+
function isSensitiveKey(key) {
|
|
10506
|
+
if (SENSITIVE_EXACT.has(key)) return true;
|
|
10507
|
+
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
10508
|
+
}
|
|
10509
|
+
function isStructuralVar(key) {
|
|
10510
|
+
if (STRUCTURAL_VARS.has(key)) return true;
|
|
10511
|
+
if (key.startsWith("XDG_")) return true;
|
|
10512
|
+
return false;
|
|
10513
|
+
}
|
|
10514
|
+
function isUsableValue(value) {
|
|
10515
|
+
if (value.length < 4) return false;
|
|
10516
|
+
if (KNOWN_NON_SECRETS.has(value.toLowerCase())) return false;
|
|
10517
|
+
if (/^\d+$/.test(value)) return false;
|
|
10518
|
+
return true;
|
|
10519
|
+
}
|
|
10520
|
+
async function parseEnvFile(filePath) {
|
|
10521
|
+
const values = [];
|
|
10522
|
+
let content;
|
|
10523
|
+
try {
|
|
10524
|
+
content = await fs6.promises.readFile(filePath, "utf-8");
|
|
10525
|
+
} catch {
|
|
10526
|
+
return values;
|
|
10527
|
+
}
|
|
10528
|
+
for (const line of content.split("\n")) {
|
|
10529
|
+
const trimmed = line.trim();
|
|
10530
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
10531
|
+
const eqIndex = trimmed.indexOf("=");
|
|
10532
|
+
if (eqIndex === -1) continue;
|
|
10533
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
10534
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
10535
|
+
const quote = value[0];
|
|
10536
|
+
let end = -1;
|
|
10537
|
+
for (let i = 1; i < value.length; i++) {
|
|
10538
|
+
if (value[i] === "\\" && i + 1 < value.length) {
|
|
10539
|
+
i++;
|
|
10540
|
+
continue;
|
|
10541
|
+
}
|
|
10542
|
+
if (value[i] === quote) {
|
|
10543
|
+
end = i;
|
|
10544
|
+
break;
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
if (end !== -1) {
|
|
10548
|
+
value = value.slice(1, end).replace(/\\(.)/g, "$1");
|
|
10549
|
+
} else {
|
|
10550
|
+
value = value.slice(1);
|
|
10551
|
+
}
|
|
10552
|
+
} else {
|
|
10553
|
+
const commentIndex = value.indexOf(" #");
|
|
10554
|
+
if (commentIndex !== -1) {
|
|
10555
|
+
value = value.slice(0, commentIndex);
|
|
10556
|
+
}
|
|
10557
|
+
value = value.trim();
|
|
10558
|
+
}
|
|
10559
|
+
if (!value) continue;
|
|
10560
|
+
values.push(value);
|
|
10561
|
+
}
|
|
10562
|
+
return values;
|
|
10563
|
+
}
|
|
10564
|
+
function addWithVariants(set, value) {
|
|
10565
|
+
set.add(value);
|
|
10566
|
+
const encoded = encodeURIComponent(value);
|
|
10567
|
+
if (encoded !== value) {
|
|
10568
|
+
set.add(encoded);
|
|
10569
|
+
}
|
|
10570
|
+
if (value.includes("://")) {
|
|
10571
|
+
try {
|
|
10572
|
+
const url = new URL(value);
|
|
10573
|
+
if (url.password) {
|
|
10574
|
+
const rawPassword = url.password;
|
|
10575
|
+
set.add(rawPassword);
|
|
10576
|
+
const decodedPassword = decodeURIComponent(rawPassword);
|
|
10577
|
+
if (decodedPassword !== rawPassword) {
|
|
10578
|
+
set.add(decodedPassword);
|
|
10579
|
+
}
|
|
10580
|
+
const encodedPw = encodeURIComponent(decodedPassword);
|
|
10581
|
+
if (encodedPw !== decodedPassword && encodedPw !== rawPassword) {
|
|
10582
|
+
set.add(encodedPw);
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
} catch {
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10588
|
+
}
|
|
10589
|
+
async function discoverEnvFiles(repoRoot) {
|
|
10590
|
+
let entries;
|
|
10591
|
+
try {
|
|
10592
|
+
entries = await fs6.promises.readdir(repoRoot);
|
|
10593
|
+
} catch {
|
|
10594
|
+
return [];
|
|
10595
|
+
}
|
|
10596
|
+
const envFiles = [];
|
|
10597
|
+
for (const name of entries) {
|
|
10598
|
+
if (!name.startsWith(".env")) continue;
|
|
10599
|
+
const filePath = path7.join(repoRoot, name);
|
|
10600
|
+
try {
|
|
10601
|
+
const stat = await fs6.promises.stat(filePath);
|
|
10602
|
+
if (stat.isFile()) envFiles.push(name);
|
|
10603
|
+
} catch {
|
|
10604
|
+
}
|
|
10605
|
+
}
|
|
10606
|
+
return envFiles;
|
|
10607
|
+
}
|
|
10608
|
+
async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
10609
|
+
const values = /* @__PURE__ */ new Set();
|
|
10610
|
+
const sourceFiles = [];
|
|
10611
|
+
let processEnvCount = 0;
|
|
10612
|
+
let skippedCount = 0;
|
|
10613
|
+
for (const filePath of envFiles) {
|
|
10614
|
+
sourceFiles.push(filePath);
|
|
10615
|
+
for (const value of await parseEnvFile(filePath)) {
|
|
10616
|
+
if (isUsableValue(value)) {
|
|
10617
|
+
addWithVariants(values, value);
|
|
10618
|
+
} else {
|
|
10619
|
+
skippedCount++;
|
|
10620
|
+
}
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
for (const filePath of additionalFiles) {
|
|
10624
|
+
const resolved = path7.resolve(repoRoot, filePath);
|
|
10625
|
+
sourceFiles.push(resolved);
|
|
10626
|
+
for (const value of await parseEnvFile(resolved)) {
|
|
10627
|
+
if (isUsableValue(value)) {
|
|
10628
|
+
addWithVariants(values, value);
|
|
10629
|
+
} else {
|
|
10630
|
+
skippedCount++;
|
|
10631
|
+
}
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
10635
|
+
if (!value) continue;
|
|
10636
|
+
if (isStructuralVar(key)) continue;
|
|
10637
|
+
if (!isSensitiveKey(key)) continue;
|
|
10638
|
+
if (isUsableValue(value)) {
|
|
10639
|
+
addWithVariants(values, value);
|
|
10640
|
+
processEnvCount++;
|
|
10641
|
+
} else {
|
|
10642
|
+
skippedCount++;
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
10645
|
+
return { values, sourceFiles, processEnvCount, skippedCount };
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10648
|
+
// src/normalizer/index.ts
|
|
10649
|
+
import path8 from "path";
|
|
10370
10650
|
|
|
10371
10651
|
// src/normalizer/claude.ts
|
|
10372
10652
|
function stringify(value) {
|
|
@@ -10771,8 +11051,7 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
|
|
|
10771
11051
|
finalExtra.service_tiers = [...serviceTiers].sort();
|
|
10772
11052
|
if (cacheCreationSeen)
|
|
10773
11053
|
finalExtra.total_cache_creation_input_tokens = cacheCreationTotal;
|
|
10774
|
-
if (cacheReadSeen)
|
|
10775
|
-
finalExtra.total_cache_read_input_tokens = cacheReadTotal;
|
|
11054
|
+
if (cacheReadSeen) finalExtra.total_cache_read_input_tokens = cacheReadTotal;
|
|
10776
11055
|
const finalMetrics = {
|
|
10777
11056
|
total_prompt_tokens: promptValues.length > 0 ? promptValues.reduce((a, b) => a + b, 0) : void 0,
|
|
10778
11057
|
total_completion_tokens: completionValues.length > 0 ? completionValues.reduce((a, b) => a + b, 0) : void 0,
|
|
@@ -10830,7 +11109,8 @@ function convertEventToStep(event, stepId, defaultModelName) {
|
|
|
10830
11109
|
};
|
|
10831
11110
|
const observation = event.output !== void 0 ? { results: [observationResult] } : void 0;
|
|
10832
11111
|
const extra = { ...event.extra ?? {} };
|
|
10833
|
-
if (event.metadata !== void 0)
|
|
11112
|
+
if (event.metadata !== void 0)
|
|
11113
|
+
extra.metadata = extra.metadata ?? event.metadata;
|
|
10834
11114
|
if (event.raw_arguments !== void 0)
|
|
10835
11115
|
extra.raw_arguments = extra.raw_arguments ?? event.raw_arguments;
|
|
10836
11116
|
if (event.status !== void 0) extra.status = extra.status ?? event.status;
|
|
@@ -10902,7 +11182,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
10902
11182
|
const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
|
|
10903
11183
|
const metaPayload = sessionMeta?.payload ?? {};
|
|
10904
11184
|
const sid = sessionId ?? metaPayload.id ?? "";
|
|
10905
|
-
|
|
11185
|
+
const agentVersion = metaPayload.cli_version ?? "unknown";
|
|
10906
11186
|
const agentExtra = {};
|
|
10907
11187
|
for (const key of ["originator", "cwd", "git", "instructions"]) {
|
|
10908
11188
|
const value = metaPayload[key];
|
|
@@ -11020,7 +11300,6 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
|
|
|
11020
11300
|
callInfo.timestamp = callInfo.timestamp ?? timestamp;
|
|
11021
11301
|
normalizedEvents.push(callInfo);
|
|
11022
11302
|
pendingReasoning = void 0;
|
|
11023
|
-
continue;
|
|
11024
11303
|
}
|
|
11025
11304
|
}
|
|
11026
11305
|
const steps = [];
|
|
@@ -11235,7 +11514,7 @@ var NormalizeMiddleware = class {
|
|
|
11235
11514
|
if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
|
|
11236
11515
|
const content = file.content ? file.content.toString("utf-8") : null;
|
|
11237
11516
|
if (!content) continue;
|
|
11238
|
-
const sessionId = file.metadata?.sessionId ??
|
|
11517
|
+
const sessionId = file.metadata?.sessionId ?? path8.basename(file.absolutePath, ".jsonl");
|
|
11239
11518
|
try {
|
|
11240
11519
|
const trajectory = normalizeContent(
|
|
11241
11520
|
file.sourceName,
|
|
@@ -11248,10 +11527,7 @@ var NormalizeMiddleware = class {
|
|
|
11248
11527
|
null,
|
|
11249
11528
|
2
|
|
11250
11529
|
);
|
|
11251
|
-
const atifPath = file.absolutePath.replace(
|
|
11252
|
-
/\.jsonl$/,
|
|
11253
|
-
".atif.json"
|
|
11254
|
-
);
|
|
11530
|
+
const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
|
|
11255
11531
|
newFiles.push({
|
|
11256
11532
|
sourceName: file.sourceName,
|
|
11257
11533
|
absolutePath: atifPath,
|
|
@@ -11266,212 +11542,6 @@ var NormalizeMiddleware = class {
|
|
|
11266
11542
|
}
|
|
11267
11543
|
};
|
|
11268
11544
|
|
|
11269
|
-
// src/middleware/secrets.ts
|
|
11270
|
-
import fs6 from "fs";
|
|
11271
|
-
import path8 from "path";
|
|
11272
|
-
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
11273
|
-
"true",
|
|
11274
|
-
"false",
|
|
11275
|
-
"null",
|
|
11276
|
-
"undefined",
|
|
11277
|
-
"yes",
|
|
11278
|
-
"no",
|
|
11279
|
-
"on",
|
|
11280
|
-
"off",
|
|
11281
|
-
"localhost",
|
|
11282
|
-
"127.0.0.1",
|
|
11283
|
-
"0.0.0.0",
|
|
11284
|
-
"::1",
|
|
11285
|
-
"development",
|
|
11286
|
-
"production",
|
|
11287
|
-
"staging",
|
|
11288
|
-
"test"
|
|
11289
|
-
]);
|
|
11290
|
-
var STRUCTURAL_VARS = /* @__PURE__ */ new Set([
|
|
11291
|
-
"PATH",
|
|
11292
|
-
"HOME",
|
|
11293
|
-
"SHELL",
|
|
11294
|
-
"USER",
|
|
11295
|
-
"LOGNAME",
|
|
11296
|
-
"LANG",
|
|
11297
|
-
"TERM",
|
|
11298
|
-
"PWD",
|
|
11299
|
-
"OLDPWD",
|
|
11300
|
-
"HOSTNAME",
|
|
11301
|
-
"DISPLAY",
|
|
11302
|
-
"EDITOR",
|
|
11303
|
-
"VISUAL",
|
|
11304
|
-
"PAGER",
|
|
11305
|
-
"SHLVL",
|
|
11306
|
-
"_",
|
|
11307
|
-
"NODE_ENV"
|
|
11308
|
-
]);
|
|
11309
|
-
var SENSITIVE_PATTERNS = [
|
|
11310
|
-
/_TOKEN$/,
|
|
11311
|
-
/_SECRET$/,
|
|
11312
|
-
/_KEY$/,
|
|
11313
|
-
/_PASSWORD$/,
|
|
11314
|
-
/_API$/,
|
|
11315
|
-
/_AUTH$/,
|
|
11316
|
-
/_CREDENTIAL$/,
|
|
11317
|
-
/_PASS$/,
|
|
11318
|
-
/SECRET/,
|
|
11319
|
-
/PASSWORD/,
|
|
11320
|
-
/PRIVATE/
|
|
11321
|
-
];
|
|
11322
|
-
var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
|
|
11323
|
-
"DATABASE_URL",
|
|
11324
|
-
"REDIS_URL",
|
|
11325
|
-
"MONGO_URI",
|
|
11326
|
-
"AWS_ACCESS_KEY_ID",
|
|
11327
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
11328
|
-
"SENTRY_DSN",
|
|
11329
|
-
"SLACK_WEBHOOK_URL",
|
|
11330
|
-
"STRIPE_SK"
|
|
11331
|
-
]);
|
|
11332
|
-
function isSensitiveKey(key) {
|
|
11333
|
-
if (SENSITIVE_EXACT.has(key)) return true;
|
|
11334
|
-
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
|
|
11335
|
-
}
|
|
11336
|
-
function isStructuralVar(key) {
|
|
11337
|
-
if (STRUCTURAL_VARS.has(key)) return true;
|
|
11338
|
-
if (key.startsWith("XDG_")) return true;
|
|
11339
|
-
return false;
|
|
11340
|
-
}
|
|
11341
|
-
function isUsableValue(value) {
|
|
11342
|
-
if (value.length < 4) return false;
|
|
11343
|
-
if (KNOWN_NON_SECRETS.has(value.toLowerCase())) return false;
|
|
11344
|
-
if (/^\d+$/.test(value)) return false;
|
|
11345
|
-
return true;
|
|
11346
|
-
}
|
|
11347
|
-
async function parseEnvFile(filePath) {
|
|
11348
|
-
const values = [];
|
|
11349
|
-
let content;
|
|
11350
|
-
try {
|
|
11351
|
-
content = await fs6.promises.readFile(filePath, "utf-8");
|
|
11352
|
-
} catch {
|
|
11353
|
-
return values;
|
|
11354
|
-
}
|
|
11355
|
-
for (const line of content.split("\n")) {
|
|
11356
|
-
const trimmed = line.trim();
|
|
11357
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11358
|
-
const eqIndex = trimmed.indexOf("=");
|
|
11359
|
-
if (eqIndex === -1) continue;
|
|
11360
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
11361
|
-
if (value.startsWith('"') || value.startsWith("'")) {
|
|
11362
|
-
const quote = value[0];
|
|
11363
|
-
let end = -1;
|
|
11364
|
-
for (let i = 1; i < value.length; i++) {
|
|
11365
|
-
if (value[i] === "\\" && i + 1 < value.length) {
|
|
11366
|
-
i++;
|
|
11367
|
-
continue;
|
|
11368
|
-
}
|
|
11369
|
-
if (value[i] === quote) {
|
|
11370
|
-
end = i;
|
|
11371
|
-
break;
|
|
11372
|
-
}
|
|
11373
|
-
}
|
|
11374
|
-
if (end !== -1) {
|
|
11375
|
-
value = value.slice(1, end).replace(/\\(.)/g, "$1");
|
|
11376
|
-
} else {
|
|
11377
|
-
value = value.slice(1);
|
|
11378
|
-
}
|
|
11379
|
-
} else {
|
|
11380
|
-
const commentIndex = value.indexOf(" #");
|
|
11381
|
-
if (commentIndex !== -1) {
|
|
11382
|
-
value = value.slice(0, commentIndex);
|
|
11383
|
-
}
|
|
11384
|
-
value = value.trim();
|
|
11385
|
-
}
|
|
11386
|
-
if (!value) continue;
|
|
11387
|
-
values.push(value);
|
|
11388
|
-
}
|
|
11389
|
-
return values;
|
|
11390
|
-
}
|
|
11391
|
-
function addWithVariants(set, value) {
|
|
11392
|
-
set.add(value);
|
|
11393
|
-
const encoded = encodeURIComponent(value);
|
|
11394
|
-
if (encoded !== value) {
|
|
11395
|
-
set.add(encoded);
|
|
11396
|
-
}
|
|
11397
|
-
if (value.includes("://")) {
|
|
11398
|
-
try {
|
|
11399
|
-
const url = new URL(value);
|
|
11400
|
-
if (url.password) {
|
|
11401
|
-
const rawPassword = url.password;
|
|
11402
|
-
set.add(rawPassword);
|
|
11403
|
-
const decodedPassword = decodeURIComponent(rawPassword);
|
|
11404
|
-
if (decodedPassword !== rawPassword) {
|
|
11405
|
-
set.add(decodedPassword);
|
|
11406
|
-
}
|
|
11407
|
-
const encodedPw = encodeURIComponent(decodedPassword);
|
|
11408
|
-
if (encodedPw !== decodedPassword && encodedPw !== rawPassword) {
|
|
11409
|
-
set.add(encodedPw);
|
|
11410
|
-
}
|
|
11411
|
-
}
|
|
11412
|
-
} catch {
|
|
11413
|
-
}
|
|
11414
|
-
}
|
|
11415
|
-
}
|
|
11416
|
-
async function discoverEnvFiles(repoRoot) {
|
|
11417
|
-
let entries;
|
|
11418
|
-
try {
|
|
11419
|
-
entries = await fs6.promises.readdir(repoRoot);
|
|
11420
|
-
} catch {
|
|
11421
|
-
return [];
|
|
11422
|
-
}
|
|
11423
|
-
const envFiles = [];
|
|
11424
|
-
for (const name of entries) {
|
|
11425
|
-
if (!name.startsWith(".env")) continue;
|
|
11426
|
-
const filePath = path8.join(repoRoot, name);
|
|
11427
|
-
try {
|
|
11428
|
-
const stat = await fs6.promises.stat(filePath);
|
|
11429
|
-
if (stat.isFile()) envFiles.push(name);
|
|
11430
|
-
} catch {
|
|
11431
|
-
}
|
|
11432
|
-
}
|
|
11433
|
-
return envFiles;
|
|
11434
|
-
}
|
|
11435
|
-
async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
11436
|
-
const values = /* @__PURE__ */ new Set();
|
|
11437
|
-
const sourceFiles = [];
|
|
11438
|
-
let processEnvCount = 0;
|
|
11439
|
-
let skippedCount = 0;
|
|
11440
|
-
for (const filePath of envFiles) {
|
|
11441
|
-
sourceFiles.push(filePath);
|
|
11442
|
-
for (const value of await parseEnvFile(filePath)) {
|
|
11443
|
-
if (isUsableValue(value)) {
|
|
11444
|
-
addWithVariants(values, value);
|
|
11445
|
-
} else {
|
|
11446
|
-
skippedCount++;
|
|
11447
|
-
}
|
|
11448
|
-
}
|
|
11449
|
-
}
|
|
11450
|
-
for (const filePath of additionalFiles) {
|
|
11451
|
-
const resolved = path8.resolve(repoRoot, filePath);
|
|
11452
|
-
sourceFiles.push(resolved);
|
|
11453
|
-
for (const value of await parseEnvFile(resolved)) {
|
|
11454
|
-
if (isUsableValue(value)) {
|
|
11455
|
-
addWithVariants(values, value);
|
|
11456
|
-
} else {
|
|
11457
|
-
skippedCount++;
|
|
11458
|
-
}
|
|
11459
|
-
}
|
|
11460
|
-
}
|
|
11461
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
11462
|
-
if (!value) continue;
|
|
11463
|
-
if (isStructuralVar(key)) continue;
|
|
11464
|
-
if (!isSensitiveKey(key)) continue;
|
|
11465
|
-
if (isUsableValue(value)) {
|
|
11466
|
-
addWithVariants(values, value);
|
|
11467
|
-
processEnvCount++;
|
|
11468
|
-
} else {
|
|
11469
|
-
skippedCount++;
|
|
11470
|
-
}
|
|
11471
|
-
}
|
|
11472
|
-
return { values, sourceFiles, processEnvCount, skippedCount };
|
|
11473
|
-
}
|
|
11474
|
-
|
|
11475
11545
|
// src/outputs/platform.ts
|
|
11476
11546
|
import { PassThrough } from "stream";
|
|
11477
11547
|
import archiver from "archiver";
|
|
@@ -11532,14 +11602,14 @@ var PlatformUploadOutput = class {
|
|
|
11532
11602
|
const buffer = await buildZipBuffer(group, selectedSources);
|
|
11533
11603
|
const {
|
|
11534
11604
|
client,
|
|
11535
|
-
|
|
11605
|
+
projectId,
|
|
11536
11606
|
contributionTypeSlug,
|
|
11537
11607
|
contributionTitle,
|
|
11538
11608
|
contributionBody,
|
|
11539
11609
|
zipFilename,
|
|
11540
11610
|
autoSubmit
|
|
11541
11611
|
} = this.opts;
|
|
11542
|
-
const contribution = await client.createContribution(
|
|
11612
|
+
const contribution = await client.createContribution(projectId, {
|
|
11543
11613
|
contributionTypeSlug,
|
|
11544
11614
|
title: contributionTitle,
|
|
11545
11615
|
body: contributionBody
|
|
@@ -11549,7 +11619,10 @@ var PlatformUploadOutput = class {
|
|
|
11549
11619
|
mimeType: "application/zip",
|
|
11550
11620
|
sizeBytes: buffer.byteLength
|
|
11551
11621
|
});
|
|
11552
|
-
appendLog(
|
|
11622
|
+
appendLog(
|
|
11623
|
+
"info",
|
|
11624
|
+
`uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
|
|
11625
|
+
);
|
|
11553
11626
|
await client.uploadToPresignedUrl(
|
|
11554
11627
|
presigned.presignedUrl,
|
|
11555
11628
|
presigned.headers,
|
|
@@ -11821,7 +11894,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11821
11894
|
const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
|
|
11822
11895
|
const output = new PlatformUploadOutput({
|
|
11823
11896
|
client,
|
|
11824
|
-
|
|
11897
|
+
projectId: config.projectId,
|
|
11825
11898
|
contributionTypeSlug: config.contributionTypeSlug,
|
|
11826
11899
|
contributionTitle: title,
|
|
11827
11900
|
contributionBody: body,
|
|
@@ -11835,7 +11908,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11835
11908
|
});
|
|
11836
11909
|
appendLog(
|
|
11837
11910
|
"info",
|
|
11838
|
-
`Uploaded session ${sessionId} to
|
|
11911
|
+
`Uploaded session ${sessionId} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId}`
|
|
11839
11912
|
);
|
|
11840
11913
|
} catch (err) {
|
|
11841
11914
|
if (err instanceof PlatformError && err.status === 401) {
|
|
@@ -12436,7 +12509,10 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
|
|
|
12436
12509
|
await fs10.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
|
|
12437
12510
|
for (let i = 0; i < retries; i++) {
|
|
12438
12511
|
try {
|
|
12439
|
-
const fd = await fs10.promises.open(
|
|
12512
|
+
const fd = await fs10.promises.open(
|
|
12513
|
+
lockPath,
|
|
12514
|
+
fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
|
|
12515
|
+
);
|
|
12440
12516
|
await fd.write(String(process.pid));
|
|
12441
12517
|
await fd.close();
|
|
12442
12518
|
return fd.fd;
|
|
@@ -12712,7 +12788,7 @@ async function handleStop(payload, tool) {
|
|
|
12712
12788
|
const epochSeconds = formatEpochSeconds2(now);
|
|
12713
12789
|
const shortId = state.sessionId.slice(0, 12);
|
|
12714
12790
|
const contribution = await client.createContribution(
|
|
12715
|
-
project.config.
|
|
12791
|
+
project.config.projectId,
|
|
12716
12792
|
{
|
|
12717
12793
|
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
12718
12794
|
title: `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`,
|
|
@@ -13773,12 +13849,12 @@ async function main() {
|
|
|
13773
13849
|
case "-h":
|
|
13774
13850
|
case "help": {
|
|
13775
13851
|
process.stdout.write(
|
|
13776
|
-
"Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Configure this repo for automatic upload\n init Configure this repo for automatic upload (--login forces fresh sign-in)\n export Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n status Show whether you're signed in and this repo is connected (pass --debug for details)\n upload Hook entry point \u2014 reads JSON payload from stdin and uploads one session\n help Show this message\n"
|
|
13852
|
+
"Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Configure this repo for automatic upload\n init Configure this repo for automatic upload (--project scopes setup, --login forces fresh sign-in)\n export Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n status Show whether you're signed in and this repo is connected (pass --debug for details)\n upload Hook entry point \u2014 reads JSON payload from stdin and uploads one session\n help Show this message\n"
|
|
13777
13853
|
);
|
|
13778
13854
|
return;
|
|
13779
13855
|
}
|
|
13780
13856
|
default:
|
|
13781
|
-
if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
|
|
13857
|
+
if (subcommand === "--login" || subcommand === "--project" || subcommand.startsWith("--project=") || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
|
|
13782
13858
|
await runInit([subcommand, ...rest]);
|
|
13783
13859
|
return;
|
|
13784
13860
|
}
|