hillclimb 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +20 -2
  2. package/dist/cli.js +667 -187
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs15 from "fs";
5
- import path18 from "path";
4
+ import fs16 from "fs";
5
+ import path19 from "path";
6
6
  import * as p6 from "@clack/prompts";
7
7
 
8
8
  // src/commands/init.ts
@@ -146,6 +146,97 @@ async function clearIdentity(apiBaseUrl) {
146
146
  return true;
147
147
  }
148
148
 
149
+ // src/platform/error-format.ts
150
+ function formatError(err) {
151
+ if (!(err instanceof Error)) return String(err);
152
+ const parts = [err.message || err.name];
153
+ const status = err.status;
154
+ const code = err.code;
155
+ if (typeof status === "number") parts.push(`status=${status}`);
156
+ if (typeof code === "string" && code) parts.push(`code=${code}`);
157
+ if (err.cause !== void 0 && err.cause !== null) {
158
+ const causeMsg = err.cause instanceof Error ? err.cause.message : String(err.cause);
159
+ if (causeMsg) parts.push(`cause=${causeMsg}`);
160
+ }
161
+ return parts.join(" ");
162
+ }
163
+
164
+ // src/platform/log.ts
165
+ import fs3 from "fs";
166
+ import path4 from "path";
167
+ var PT_TIME_ZONE = "America/Los_Angeles";
168
+ function pacificParts(date) {
169
+ const parts = new Intl.DateTimeFormat("en-US", {
170
+ timeZone: PT_TIME_ZONE,
171
+ year: "numeric",
172
+ month: "2-digit",
173
+ day: "2-digit",
174
+ hour: "2-digit",
175
+ minute: "2-digit",
176
+ second: "2-digit",
177
+ hour12: false
178
+ }).formatToParts(date);
179
+ const lookup = {};
180
+ for (const p7 of parts) if (p7.type !== "literal") lookup[p7.type] = p7.value;
181
+ if (lookup.hour === "24") lookup.hour = "00";
182
+ return {
183
+ year: lookup.year ?? "0000",
184
+ month: lookup.month ?? "00",
185
+ day: lookup.day ?? "00",
186
+ hour: lookup.hour ?? "00",
187
+ minute: lookup.minute ?? "00",
188
+ second: lookup.second ?? "00"
189
+ };
190
+ }
191
+ function pacificOffsetMinutes(date) {
192
+ const p7 = pacificParts(date);
193
+ const asIfUtc = Date.UTC(
194
+ Number(p7.year),
195
+ Number(p7.month) - 1,
196
+ Number(p7.day),
197
+ Number(p7.hour),
198
+ Number(p7.minute),
199
+ Number(p7.second)
200
+ );
201
+ return Math.round((asIfUtc - date.getTime()) / 6e4);
202
+ }
203
+ function pacificTimestamp(date) {
204
+ const p7 = pacificParts(date);
205
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
206
+ const off = pacificOffsetMinutes(date);
207
+ const sign = off >= 0 ? "+" : "-";
208
+ const abs = Math.abs(off);
209
+ const oh = String(Math.floor(abs / 60)).padStart(2, "0");
210
+ const om = String(abs % 60).padStart(2, "0");
211
+ return `${p7.year}-${p7.month}-${p7.day}T${p7.hour}:${p7.minute}:${p7.second}.${ms}${sign}${oh}:${om}`;
212
+ }
213
+ function pacificDateString(date) {
214
+ const p7 = pacificParts(date);
215
+ return `${p7.year}-${p7.month}-${p7.day}`;
216
+ }
217
+ function logsDir() {
218
+ return path4.join(configDir(), "logs");
219
+ }
220
+ function todayLogPath() {
221
+ return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
222
+ }
223
+ var logPrefix = "";
224
+ function setLogPrefix(prefix) {
225
+ logPrefix = prefix;
226
+ }
227
+ function appendLog(level, message) {
228
+ const tag = logPrefix ? ` ${logPrefix}` : "";
229
+ const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
230
+ `;
231
+ try {
232
+ fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
233
+ fs3.appendFileSync(todayLogPath(), line);
234
+ } catch {
235
+ process.stderr.write(`hillclimb:${tag} ${level}: ${message}
236
+ `);
237
+ }
238
+ }
239
+
149
240
  // src/platform/client.ts
150
241
  var PlatformError = class extends Error {
151
242
  constructor(message, status, code) {
@@ -229,9 +320,21 @@ var PlatformClient = class {
229
320
  if (body !== void 0) {
230
321
  init.body = JSON.stringify(body);
231
322
  }
232
- const res = await fetch(this.url(relative), init);
323
+ const start = Date.now();
324
+ let res;
325
+ try {
326
+ res = await fetch(this.url(relative), init);
327
+ } catch (err) {
328
+ appendLog(
329
+ "error",
330
+ `[${method} ${relative}] FAILED ${Date.now() - start}ms ${formatError(err)}`
331
+ );
332
+ throw err;
333
+ }
334
+ const elapsedMs = Date.now() - start;
233
335
  this.mergeSetCookies(res);
234
336
  if (!res.ok) {
337
+ appendLog("warn", `[${method} ${relative}] ${res.status} ${elapsedMs}ms`);
235
338
  let code;
236
339
  let message = `${method} ${relative} failed: HTTP ${res.status}`;
237
340
  try {
@@ -253,6 +356,7 @@ var PlatformClient = class {
253
356
  }
254
357
  throw new PlatformError(message, res.status, code);
255
358
  }
359
+ appendLog("info", `[${method} ${relative}] ${res.status} ${elapsedMs}ms`);
256
360
  if (res.status === 204) {
257
361
  return void 0;
258
362
  }
@@ -337,12 +441,31 @@ var PlatformClient = class {
337
441
  async uploadToPresignedUrl(presignedUrl, headers, body) {
338
442
  const ab = new ArrayBuffer(body.byteLength);
339
443
  new Uint8Array(ab).set(body);
340
- const res = await fetch(presignedUrl, {
341
- method: "PUT",
342
- headers,
343
- body: new Blob([ab])
344
- });
444
+ const host = (() => {
445
+ try {
446
+ return new URL(presignedUrl).host;
447
+ } catch {
448
+ return "presigned-url";
449
+ }
450
+ })();
451
+ const start = Date.now();
452
+ let res;
453
+ try {
454
+ res = await fetch(presignedUrl, {
455
+ method: "PUT",
456
+ headers,
457
+ body: new Blob([ab])
458
+ });
459
+ } catch (err) {
460
+ appendLog(
461
+ "error",
462
+ `[PUT ${host}] FAILED ${Date.now() - start}ms ${formatError(err)}`
463
+ );
464
+ throw err;
465
+ }
466
+ const elapsedMs = Date.now() - start;
345
467
  if (!res.ok) {
468
+ appendLog("warn", `[PUT ${host}] ${res.status} ${elapsedMs}ms`);
346
469
  let detail = "";
347
470
  try {
348
471
  detail = await res.text();
@@ -353,6 +476,10 @@ var PlatformClient = class {
353
476
  res.status
354
477
  );
355
478
  }
479
+ appendLog(
480
+ "info",
481
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms (${body.byteLength} bytes)`
482
+ );
356
483
  }
357
484
  async submitContribution(contributionId) {
358
485
  const data = await this.request(
@@ -377,9 +504,9 @@ var PlatformClient = class {
377
504
  };
378
505
 
379
506
  // src/platform/hooks.ts
380
- import fs3 from "fs";
507
+ import fs4 from "fs";
381
508
  import os2 from "os";
382
- import path4 from "path";
509
+ import path5 from "path";
383
510
  var HOOK_CMD = (sub) => `npx hillclimb ${sub}`;
384
511
  var GIT_TRACES_CMD = (tool) => `npx hillclimb git-traces --tool=${tool}`;
385
512
  var TOOLS = [
@@ -394,7 +521,7 @@ var TOOLS = [
394
521
  { eventName: "Stop", command: GIT_TRACES_CMD("claude") },
395
522
  { eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
396
523
  ],
397
- detect: () => isDir(path4.join(os2.homedir(), ".claude"))
524
+ detect: () => isDir(path5.join(os2.homedir(), ".claude"))
398
525
  },
399
526
  {
400
527
  tool: "cursor",
@@ -407,7 +534,7 @@ var TOOLS = [
407
534
  { eventName: "stop", command: GIT_TRACES_CMD("cursor") },
408
535
  { eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
409
536
  ],
410
- detect: () => isDir(path4.join(os2.homedir(), ".cursor"))
537
+ detect: () => isDir(path5.join(os2.homedir(), ".cursor"))
411
538
  },
412
539
  {
413
540
  // opencode has no settings-file hook system (the `experimental.hook` block
@@ -422,7 +549,28 @@ var TOOLS = [
422
549
  format: "opencode",
423
550
  events: [],
424
551
  // Events are wired inside the plugin itself; see OPENCODE_PLUGIN_CONTENT.
425
- detect: () => isDir(path4.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path4.join(os2.homedir(), ".config", "opencode"))
552
+ detect: () => isDir(path5.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path5.join(os2.homedir(), ".config", "opencode"))
553
+ },
554
+ {
555
+ // VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
556
+ // event is documented but never fires — VS Code issue microsoft/vscode#300650).
557
+ // Stale-state cleanup happens on next SessionStart, like Codex.
558
+ // Hook config lives at .github/hooks/hooks.json — distinct from
559
+ // .claude/settings.local.json so Claude Code and Copilot Chat don't
560
+ // fire each other's hooks even though VS Code's docs claim they share.
561
+ tool: "copilot-chat",
562
+ label: "GitHub Copilot Chat",
563
+ settingsFile: ".github/hooks/hooks.json",
564
+ format: "copilot",
565
+ events: [
566
+ { eventName: "SessionStart", command: GIT_TRACES_CMD("copilot-chat") },
567
+ { eventName: "Stop", command: GIT_TRACES_CMD("copilot-chat") },
568
+ {
569
+ eventName: "Stop",
570
+ command: `${HOOK_CMD("upload")} --tool=copilot-chat`
571
+ }
572
+ ],
573
+ detect: copilotChatDetect
426
574
  },
427
575
  {
428
576
  // Codex has no SessionEnd hook (only SessionStart, PreToolUse, PostToolUse,
@@ -441,22 +589,36 @@ var TOOLS = [
441
589
  { eventName: "SessionStart", command: GIT_TRACES_CMD("codex") },
442
590
  { eventName: "Stop", command: GIT_TRACES_CMD("codex") }
443
591
  ],
444
- detect: () => isDir(path4.join(os2.homedir(), ".codex"))
592
+ detect: () => isDir(path5.join(os2.homedir(), ".codex"))
445
593
  }
446
594
  ];
447
595
  function isDir(p7) {
448
596
  try {
449
- return fs3.statSync(p7).isDirectory();
597
+ return fs4.statSync(p7).isDirectory();
450
598
  } catch {
451
599
  return false;
452
600
  }
453
601
  }
602
+ function copilotChatDetect() {
603
+ const home = os2.homedir();
604
+ const suffix = path5.join(
605
+ "User",
606
+ "globalStorage",
607
+ "github.copilot-chat"
608
+ );
609
+ const candidates = [
610
+ path5.join(home, "Library", "Application Support", "Code", suffix),
611
+ path5.join(home, ".config", "Code", suffix),
612
+ process.env.APPDATA ? path5.join(process.env.APPDATA, "Code", suffix) : null
613
+ ].filter((p7) => p7 !== null);
614
+ return candidates.some(isDir);
615
+ }
454
616
  function settingsPath(repoRoot, def) {
455
- return path4.join(repoRoot, def.settingsFile);
617
+ return path5.join(repoRoot, def.settingsFile);
456
618
  }
457
619
  async function readJson(file) {
458
620
  try {
459
- const raw = await fs3.promises.readFile(file, "utf-8");
621
+ const raw = await fs4.promises.readFile(file, "utf-8");
460
622
  const parsed = JSON.parse(raw);
461
623
  if (parsed && typeof parsed === "object") return parsed;
462
624
  return {};
@@ -466,8 +628,8 @@ async function readJson(file) {
466
628
  }
467
629
  }
468
630
  async function writeJson(file, obj) {
469
- await fs3.promises.mkdir(path4.dirname(file), { recursive: true });
470
- await fs3.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
631
+ await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
632
+ await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
471
633
  `);
472
634
  }
473
635
  function claudeHookPresent(matchers, command) {
@@ -545,11 +707,41 @@ function cursorUninstall(settings, eventName, command) {
545
707
  }
546
708
  return true;
547
709
  }
710
+ function copilotHookPresent(entries, command) {
711
+ return entries.some((e) => e.type === "command" && e.command === command);
712
+ }
713
+ function copilotInstall(settings, eventName, command) {
714
+ if (!settings.hooks) settings.hooks = {};
715
+ if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
716
+ const entries = settings.hooks[eventName];
717
+ if (copilotHookPresent(entries, command)) return false;
718
+ entries.push({ type: "command", command });
719
+ return true;
720
+ }
721
+ function copilotCheck(settings, eventName, command) {
722
+ const entries = settings.hooks?.[eventName] ?? [];
723
+ return copilotHookPresent(entries, command);
724
+ }
725
+ function copilotUninstall(settings, eventName, command) {
726
+ const entries = settings.hooks?.[eventName];
727
+ if (!entries || entries.length === 0) return false;
728
+ const next = entries.filter(
729
+ (e) => !(e.type === "command" && e.command === command)
730
+ );
731
+ if (next.length === entries.length) return false;
732
+ if (next.length > 0) {
733
+ settings.hooks[eventName] = next;
734
+ } else {
735
+ delete settings.hooks[eventName];
736
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
737
+ }
738
+ return true;
739
+ }
548
740
  var OPENCODE_PLUGIN_VERSION = 2;
549
741
  var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
550
742
  var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
551
- // Auto-installed by \`npx hillclimb init\`. Do not edit manually \u2014 re-running
552
- // init will overwrite this file. See https://github.com/hillclimbai for context.
743
+ // Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
744
+ // hillclimb will overwrite this file. See https://github.com/hillclimbai for context.
553
745
 
554
746
  import { spawn } from "node:child_process";
555
747
  import fs from "node:fs";
@@ -695,32 +887,53 @@ export default HillclimbPlugin;
695
887
  `;
696
888
  async function opencodeInstall(file) {
697
889
  try {
698
- const existing = await fs3.promises.readFile(file, "utf-8");
890
+ const existing = await fs4.promises.readFile(file, "utf-8");
699
891
  if (existing === OPENCODE_PLUGIN_CONTENT) {
700
892
  return { installed: 0, alreadyPresent: 1 };
701
893
  }
702
894
  } catch {
703
895
  }
704
- await fs3.promises.mkdir(path4.dirname(file), { recursive: true });
705
- await fs3.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
896
+ await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
897
+ await fs4.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
706
898
  return { installed: 1, alreadyPresent: 0 };
707
899
  }
708
900
  async function opencodeCheck(file) {
709
901
  try {
710
- const content = await fs3.promises.readFile(file, "utf-8");
902
+ const content = await fs4.promises.readFile(file, "utf-8");
711
903
  return content.includes(OPENCODE_PLUGIN_MARKER);
712
904
  } catch {
713
905
  return false;
714
906
  }
715
907
  }
716
908
  function install(settings, format, eventName, command) {
717
- return format === "cursor" ? cursorInstall(settings, eventName, command) : claudeInstall(settings, eventName, command);
909
+ switch (format) {
910
+ case "cursor":
911
+ return cursorInstall(settings, eventName, command);
912
+ case "copilot":
913
+ return copilotInstall(settings, eventName, command);
914
+ default:
915
+ return claudeInstall(settings, eventName, command);
916
+ }
718
917
  }
719
918
  function check(settings, format, eventName, command) {
720
- return format === "cursor" ? cursorCheck(settings, eventName, command) : claudeCheck(settings, eventName, command);
919
+ switch (format) {
920
+ case "cursor":
921
+ return cursorCheck(settings, eventName, command);
922
+ case "copilot":
923
+ return copilotCheck(settings, eventName, command);
924
+ default:
925
+ return claudeCheck(settings, eventName, command);
926
+ }
721
927
  }
722
928
  function uninstall(settings, format, eventName, command) {
723
- return format === "cursor" ? cursorUninstall(settings, eventName, command) : claudeUninstall(settings, eventName, command);
929
+ switch (format) {
930
+ case "cursor":
931
+ return cursorUninstall(settings, eventName, command);
932
+ case "copilot":
933
+ return copilotUninstall(settings, eventName, command);
934
+ default:
935
+ return claudeUninstall(settings, eventName, command);
936
+ }
724
937
  }
725
938
  function legacyCommandsFor(command) {
726
939
  const prefix = "npx hillclimb ";
@@ -814,17 +1027,17 @@ async function checkAllHooks(repoRoot) {
814
1027
  }
815
1028
  return results;
816
1029
  }
817
- var CODEX_CONFIG_PATH = path4.join(os2.homedir(), ".codex", "config.toml");
1030
+ var CODEX_CONFIG_PATH = path5.join(os2.homedir(), ".codex", "config.toml");
818
1031
  async function ensureCodexHooksEnabled() {
819
1032
  let content;
820
1033
  try {
821
- content = await fs3.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
1034
+ content = await fs4.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
822
1035
  } catch (err) {
823
1036
  if (err.code === "ENOENT") {
824
- await fs3.promises.mkdir(path4.dirname(CODEX_CONFIG_PATH), {
1037
+ await fs4.promises.mkdir(path5.dirname(CODEX_CONFIG_PATH), {
825
1038
  recursive: true
826
1039
  });
827
- await fs3.promises.writeFile(
1040
+ await fs4.promises.writeFile(
828
1041
  CODEX_CONFIG_PATH,
829
1042
  "[features]\ncodex_hooks = true\n"
830
1043
  );
@@ -840,7 +1053,7 @@ async function ensureCodexHooksEnabled() {
840
1053
  } else {
841
1054
  content = content.trimEnd() + "\n\n[features]\ncodex_hooks = true\n";
842
1055
  }
843
- await fs3.promises.writeFile(CODEX_CONFIG_PATH, content);
1056
+ await fs4.promises.writeFile(CODEX_CONFIG_PATH, content);
844
1057
  return true;
845
1058
  }
846
1059
  var CLAUDE_DEF = TOOLS[0];
@@ -896,8 +1109,10 @@ function openBrowser(url) {
896
1109
  }
897
1110
  async function webAuth(apiBaseUrl) {
898
1111
  if (!IS_TTY) {
1112
+ appendLog("info", "web-auth: skipped (not a TTY)");
899
1113
  return null;
900
1114
  }
1115
+ appendLog("info", `web-auth: started (apiBaseUrl=${apiBaseUrl})`);
901
1116
  const client = new PlatformClient(apiBaseUrl);
902
1117
  const codeResponse = await withSpinner(
903
1118
  "Preparing web authentication...",
@@ -905,6 +1120,10 @@ async function webAuth(apiBaseUrl) {
905
1120
  "Could not start web authentication.",
906
1121
  () => client.createCliAuthCode()
907
1122
  );
1123
+ appendLog(
1124
+ "info",
1125
+ `web-auth: code prepared (expiresIn=${codeResponse.expiresIn}s, pollInterval=${codeResponse.interval}s)`
1126
+ );
908
1127
  const url = codeResponse.verificationUrl;
909
1128
  openBrowser(url);
910
1129
  p2.log.info(`Opening browser to: ${cyan(url)}`);
@@ -918,6 +1137,7 @@ async function webAuth(apiBaseUrl) {
918
1137
  try {
919
1138
  const poll = await client.pollCliAuthCode(codeResponse.code);
920
1139
  if (poll.status === "approved" && poll.sessionToken) {
1140
+ appendLog("info", "web-auth: authorized");
921
1141
  spinner5.stop("Authorized.");
922
1142
  const secure = apiBaseUrl.startsWith("https://");
923
1143
  const cookieName = secure ? "__Secure-better-auth.session_token" : "better-auth.session_token";
@@ -933,6 +1153,10 @@ async function webAuth(apiBaseUrl) {
933
1153
  return data;
934
1154
  }
935
1155
  );
1156
+ appendLog(
1157
+ "info",
1158
+ `web-auth: completed (email=${bootstrap.session.email})`
1159
+ );
936
1160
  return {
937
1161
  client,
938
1162
  sessionCookie,
@@ -941,13 +1165,16 @@ async function webAuth(apiBaseUrl) {
941
1165
  };
942
1166
  }
943
1167
  if (poll.status === "expired") {
1168
+ appendLog("warn", "web-auth: code expired before authorization");
944
1169
  spinner5.stop("Authorization expired.");
945
1170
  p2.log.warn("The authorization code expired. Please try again.");
946
1171
  return null;
947
1172
  }
948
- } catch {
1173
+ } catch (err) {
1174
+ appendLog("warn", `web-auth: poll attempt failed: ${formatError(err)}`);
949
1175
  }
950
1176
  }
1177
+ appendLog("warn", "web-auth: timed out waiting for authorization");
951
1178
  spinner5.stop("Authorization timed out.");
952
1179
  p2.log.warn("Timed out waiting for browser authorization.");
953
1180
  return null;
@@ -1001,16 +1228,20 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
1001
1228
  async function runInit(args = []) {
1002
1229
  const forceLogin = args.includes("--login");
1003
1230
  const workspaceSlugFlag = parseStringFlag(args, "workspace");
1004
- p3.intro("hillclimb init");
1231
+ appendLog(
1232
+ "info",
1233
+ `init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, workspaceSlug=${workspaceSlugFlag ?? "<none>"})`
1234
+ );
1235
+ p3.intro("hillclimb");
1005
1236
  const repo = detectRepoRoot();
1006
1237
  if (!repo) {
1007
- p3.log.error(
1008
- "Not a git repository. Run `npx hillclimb init` inside a repo."
1009
- );
1238
+ appendLog("error", "init: not a git repository, aborting");
1239
+ p3.log.error("Not a git repository. Run `npx hillclimb` inside a repo.");
1010
1240
  p3.outro("Aborted.");
1011
1241
  process.exit(1);
1012
1242
  }
1013
1243
  const { root: repoRoot, name: repoName } = repo;
1244
+ appendLog("info", `init: repo detected (root=${repoRoot}, name=${repoName})`);
1014
1245
  p3.log.info(`Repo: ${repoName} (${repoRoot})`);
1015
1246
  const apiBaseUrl = resolveDefaultApiUrl();
1016
1247
  const saved = forceLogin ? null : await loadIdentity(apiBaseUrl);
@@ -1040,9 +1271,11 @@ async function runInit(args = []) {
1040
1271
  }
1041
1272
  method = selected;
1042
1273
  }
1274
+ appendLog("info", `init: auth method selected (${method})`);
1043
1275
  if (method === "web") {
1044
1276
  const result = await webAuth(apiBaseUrl);
1045
1277
  if (!result) {
1278
+ appendLog("error", "init: web authentication failed");
1046
1279
  p3.log.error("Web authentication failed.");
1047
1280
  p3.outro("Aborted.");
1048
1281
  process.exit(1);
@@ -1107,6 +1340,10 @@ async function runInit(args = []) {
1107
1340
  }
1108
1341
  pickedId = resolved.organizationId;
1109
1342
  workspace = resolved;
1343
+ appendLog(
1344
+ "info",
1345
+ `init: workspace pre-scoped (slug=${workspaceSlugFlag}, id=${resolved.id}, orgId=${pickedId})`
1346
+ );
1110
1347
  const belongs = orgs.some((o) => o.id === pickedId);
1111
1348
  if (!belongs && !isAdmin) {
1112
1349
  p3.log.error(
@@ -1123,6 +1360,7 @@ async function runInit(args = []) {
1123
1360
  await client.setActiveOrganization(pickedId);
1124
1361
  }
1125
1362
  } catch (err) {
1363
+ appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
1126
1364
  p3.log.error(err instanceof Error ? err.message : String(err));
1127
1365
  p3.outro("Aborted.");
1128
1366
  process.exit(1);
@@ -1163,6 +1401,7 @@ async function runInit(args = []) {
1163
1401
  await client.setActiveOrganization(pickedId);
1164
1402
  }
1165
1403
  } catch (err) {
1404
+ appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
1166
1405
  p3.log.error(err instanceof Error ? err.message : String(err));
1167
1406
  p3.outro("Aborted.");
1168
1407
  process.exit(1);
@@ -1203,6 +1442,10 @@ async function runInit(args = []) {
1203
1442
  process.exit(1);
1204
1443
  }
1205
1444
  workspace = picked;
1445
+ appendLog(
1446
+ "info",
1447
+ `init: workspace selected (slug=${workspace.slug}, id=${workspace.id}, orgId=${pickedId})`
1448
+ );
1206
1449
  }
1207
1450
  const types = await withSpinner(
1208
1451
  "Loading contribution types...",
@@ -1253,6 +1496,10 @@ async function runInit(args = []) {
1253
1496
  p3.outro("Aborted.");
1254
1497
  process.exit(1);
1255
1498
  }
1499
+ appendLog(
1500
+ "info",
1501
+ `init: contribution type selected (slug=${type.slug}, name=${type.name})`
1502
+ );
1256
1503
  const pickedOrg = orgs.find((o) => o.id === activeOrgId);
1257
1504
  await upsertProject(repoRoot, {
1258
1505
  apiBaseUrl,
@@ -1265,6 +1512,7 @@ async function runInit(args = []) {
1265
1512
  contributionTypeName: type.name,
1266
1513
  autoSubmit: true
1267
1514
  });
1515
+ appendLog("info", `init: project config saved (repoRoot=${repoRoot})`);
1268
1516
  p3.log.success(`Saved config for ${repoRoot}`);
1269
1517
  await saveIdentity({
1270
1518
  apiBaseUrl,
@@ -1276,10 +1524,19 @@ async function runInit(args = []) {
1276
1524
  try {
1277
1525
  const hookResults = await installDetectedHooks(repoRoot);
1278
1526
  if (hookResults.length === 0) {
1527
+ appendLog(
1528
+ "warn",
1529
+ "init: no supported tools detected, hooks not installed"
1530
+ );
1279
1531
  p3.log.warn(
1280
- "No supported tools detected (.claude/, .cursor/, .codex/). Hook not installed."
1532
+ "No supported tools detected (Claude Code, Cursor, Codex, opencode, GitHub Copilot Chat). Hook not installed."
1281
1533
  );
1282
1534
  } else {
1535
+ const tools = hookResults.map((r) => `${r.tool}${r.alreadyPresent ? "(existing)" : ""}`).join(",");
1536
+ appendLog(
1537
+ "info",
1538
+ `init: hooks installed (count=${hookResults.length}, tools=${tools})`
1539
+ );
1283
1540
  for (const r of hookResults) {
1284
1541
  if (r.alreadyPresent) {
1285
1542
  p3.log.info(`${r.label} hook already present`);
@@ -1291,9 +1548,17 @@ async function runInit(args = []) {
1291
1548
  try {
1292
1549
  const changed = await ensureCodexHooksEnabled();
1293
1550
  if (changed) {
1551
+ appendLog(
1552
+ "info",
1553
+ "init: enabled codex_hooks in ~/.codex/config.toml"
1554
+ );
1294
1555
  p3.log.success("Enabled codex_hooks in ~/.codex/config.toml");
1295
1556
  }
1296
1557
  } catch (err) {
1558
+ appendLog(
1559
+ "warn",
1560
+ `init: could not enable codex_hooks: ${formatError(err)}`
1561
+ );
1297
1562
  p3.log.warn(
1298
1563
  `Could not enable codex_hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
1299
1564
  );
@@ -1301,12 +1566,17 @@ async function runInit(args = []) {
1301
1566
  }
1302
1567
  }
1303
1568
  } catch (err) {
1569
+ appendLog("error", `init: failed to install hooks: ${formatError(err)}`);
1304
1570
  p3.log.error(
1305
1571
  `Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`
1306
1572
  );
1307
1573
  p3.outro("Partially configured. Re-run after fixing the error.");
1308
1574
  process.exit(1);
1309
1575
  }
1576
+ appendLog(
1577
+ "info",
1578
+ `init: completed (workspace=${workspace.slug}, contributionType=${type.slug})`
1579
+ );
1310
1580
  p3.outro(
1311
1581
  `Done. Your next coding session in this repo will upload automatically to ${workspace.name}.`
1312
1582
  );
@@ -1347,6 +1617,7 @@ function resolveDefaultApiUrl2() {
1347
1617
  async function runLogin(_args = []) {
1348
1618
  const apiBaseUrl = resolveDefaultApiUrl2();
1349
1619
  header("login");
1620
+ appendLog("info", `login: started (apiBaseUrl=${apiBaseUrl})`);
1350
1621
  const saved = await loadIdentity(apiBaseUrl);
1351
1622
  if (saved) {
1352
1623
  if (IS_TTY) {
@@ -1377,6 +1648,10 @@ async function runLogin(_args = []) {
1377
1648
  process.stdout.write("\r\x1B[K");
1378
1649
  }
1379
1650
  if (verified) {
1651
+ appendLog(
1652
+ "info",
1653
+ `login: reused saved login (email=${verified.email})`
1654
+ );
1380
1655
  row(CHECK, "Signed in", cyan(verified.email));
1381
1656
  const fresh = probe.getSessionCookie();
1382
1657
  if (fresh) {
@@ -1395,10 +1670,15 @@ async function runLogin(_args = []) {
1395
1670
  return;
1396
1671
  }
1397
1672
  if (expired) {
1673
+ appendLog("info", "login: saved login expired, falling through");
1398
1674
  row(CROSS, "Signed in", dim("expired \u2014 please sign in again"));
1399
1675
  println();
1400
1676
  await clearIdentity(apiBaseUrl);
1401
1677
  } else if (verifyError) {
1678
+ appendLog(
1679
+ "warn",
1680
+ `login: could not verify saved login: ${formatError(verifyError)}`
1681
+ );
1402
1682
  row(CROSS, "Signed in", dim("could not verify saved login"));
1403
1683
  println(` ${dim(verifyError.message)}`);
1404
1684
  println();
@@ -1424,9 +1704,11 @@ async function runLogin(_args = []) {
1424
1704
  }
1425
1705
  method = selected;
1426
1706
  }
1707
+ appendLog("info", `login: auth method selected (${method})`);
1427
1708
  if (method === "web") {
1428
1709
  const result = await webAuth(apiBaseUrl);
1429
1710
  if (!result) {
1711
+ appendLog("error", "login: web authentication failed");
1430
1712
  p4.log.error("Web authentication failed.");
1431
1713
  p4.outro("Aborted.");
1432
1714
  process.exit(1);
@@ -1438,9 +1720,10 @@ async function runLogin(_args = []) {
1438
1720
  userId: result.userId,
1439
1721
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
1440
1722
  });
1723
+ appendLog("info", `login: completed via web auth (email=${result.email})`);
1441
1724
  println();
1442
1725
  row(CHECK, "Signed in", cyan(result.email));
1443
- footer("Run `npx hillclimb init` in a repo to wire up auto-upload.");
1726
+ footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
1444
1727
  return;
1445
1728
  }
1446
1729
  const email = await requireText("Email");
@@ -1464,6 +1747,7 @@ async function runLogin(_args = []) {
1464
1747
  );
1465
1748
  const cookie = client.getSessionCookie();
1466
1749
  if (!cookie) {
1750
+ appendLog("error", "login: session cookie missing after sign-in");
1467
1751
  p4.log.error("Session cookie was lost during sign-in.");
1468
1752
  p4.outro("Aborted.");
1469
1753
  process.exit(1);
@@ -1475,9 +1759,13 @@ async function runLogin(_args = []) {
1475
1759
  userId: bootstrap.session.userId,
1476
1760
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
1477
1761
  });
1762
+ appendLog(
1763
+ "info",
1764
+ `login: completed via email/password (email=${bootstrap.session.email})`
1765
+ );
1478
1766
  println();
1479
1767
  row(CHECK, "Signed in", cyan(bootstrap.session.email));
1480
- footer("Run `npx hillclimb init` in a repo to wire up auto-upload.");
1768
+ footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
1481
1769
  }
1482
1770
 
1483
1771
  // src/commands/logout.ts
@@ -1509,39 +1797,12 @@ async function runLogout(args = []) {
1509
1797
  );
1510
1798
  footer(
1511
1799
  "Per-repo upload hooks still active.",
1512
- "Re-run `npx hillclimb init` to re-authenticate a repo."
1800
+ "Re-run `npx hillclimb` to re-authenticate a repo."
1513
1801
  );
1514
1802
  }
1515
1803
 
1516
1804
  // src/commands/status.ts
1517
1805
  import path6 from "path";
1518
-
1519
- // src/platform/log.ts
1520
- import fs4 from "fs";
1521
- import path5 from "path";
1522
- function today() {
1523
- const d = /* @__PURE__ */ new Date();
1524
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1525
- }
1526
- function logsDir() {
1527
- return path5.join(configDir(), "logs");
1528
- }
1529
- function todayLogPath() {
1530
- return path5.join(logsDir(), `${today()}.log`);
1531
- }
1532
- function appendLog(level, message) {
1533
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${message}
1534
- `;
1535
- try {
1536
- fs4.mkdirSync(logsDir(), { recursive: true, mode: 448 });
1537
- fs4.appendFileSync(todayLogPath(), line);
1538
- } catch {
1539
- process.stderr.write(`hillclimb: ${level}: ${message}
1540
- `);
1541
- }
1542
- }
1543
-
1544
- // src/commands/status.ts
1545
1806
  async function runStatus(args = []) {
1546
1807
  const debug = args.includes("--debug");
1547
1808
  header("status");
@@ -1555,7 +1816,7 @@ async function runStatus(args = []) {
1555
1816
  return;
1556
1817
  }
1557
1818
  row(CROSS, "Repo", `${bold(repo.name)} ${dim("(not configured)")}`);
1558
- footer(`Run \`npx hillclimb init\` in ${repo.name} to connect it.`);
1819
+ footer(`Run \`npx hillclimb\` in ${repo.name} to connect it.`);
1559
1820
  return;
1560
1821
  }
1561
1822
  const { repoRoot, config } = match;
@@ -1617,11 +1878,11 @@ async function runStatus(args = []) {
1617
1878
  row(CROSS, "Auto-upload", dim("error checking hooks"));
1618
1879
  }
1619
1880
  if (loginError === "expired") {
1620
- footer("Run `npx hillclimb init` to sign in again.");
1881
+ footer("Run `npx hillclimb` to sign in again.");
1621
1882
  } else if (loginError === "unreachable") {
1622
1883
  footer(`Couldn't reach ${config.apiBaseUrl}. Check your connection.`);
1623
1884
  } else if (!anyHookInstalled) {
1624
- footer("Run `npx hillclimb init` to install the upload hook.");
1885
+ footer("Run `npx hillclimb` to install the upload hook.");
1625
1886
  } else {
1626
1887
  footer("All set.");
1627
1888
  }
@@ -11464,6 +11725,9 @@ function formatTitleTimestamp(date) {
11464
11725
  const pad = (n) => String(n).padStart(2, "0");
11465
11726
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
11466
11727
  }
11728
+ function lineHasAssistant(line) {
11729
+ return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
11730
+ }
11467
11731
  async function hasAssistantMessage(transcriptPath) {
11468
11732
  const stream = fs8.createReadStream(transcriptPath, { encoding: "utf-8" });
11469
11733
  let buffer = "";
@@ -11474,15 +11738,14 @@ async function hasAssistantMessage(transcriptPath) {
11474
11738
  while (newlineIdx !== -1) {
11475
11739
  const line = buffer.slice(0, newlineIdx);
11476
11740
  buffer = buffer.slice(newlineIdx + 1);
11477
- if (line.includes('"type":"assistant"') || line.includes('"role":"assistant"')) {
11741
+ if (lineHasAssistant(line)) {
11478
11742
  stream.destroy();
11479
11743
  return true;
11480
11744
  }
11481
11745
  newlineIdx = buffer.indexOf("\n");
11482
11746
  }
11483
11747
  }
11484
- if (buffer.includes('"type":"assistant"') || buffer.includes('"role":"assistant"'))
11485
- return true;
11748
+ if (lineHasAssistant(buffer)) return true;
11486
11749
  } catch {
11487
11750
  return true;
11488
11751
  }
@@ -11546,7 +11809,7 @@ async function runUploadInner(payload) {
11546
11809
  if (!match) {
11547
11810
  appendLog(
11548
11811
  "warn",
11549
- `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb init\` in the repo.`
11812
+ `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
11550
11813
  );
11551
11814
  return;
11552
11815
  }
@@ -11600,7 +11863,13 @@ async function uploadSession(args) {
11600
11863
  }
11601
11864
  const client = new PlatformClient(config.apiBaseUrl, identity.sessionCookie);
11602
11865
  const shortId = sessionId.slice(0, 12);
11603
- const toolLabels = { cursor: "Cursor", codex: "Codex", claude: "Claude", opencode: "opencode" };
11866
+ const toolLabels = {
11867
+ cursor: "Cursor",
11868
+ codex: "Codex",
11869
+ claude: "Claude",
11870
+ "copilot-chat": "GitHub Copilot Chat",
11871
+ opencode: "opencode"
11872
+ };
11604
11873
  const toolLabel = toolLabels[sourceTool] ?? "Claude";
11605
11874
  const title = `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp(now)}`;
11606
11875
  const body = `Session ID: ${sessionId}
@@ -11630,7 +11899,7 @@ Uploaded: ${now.toISOString()}`;
11630
11899
  if (err instanceof PlatformError && err.status === 401) {
11631
11900
  appendLog(
11632
11901
  "error",
11633
- `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb init\` in ${repoRoot}.`
11902
+ `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
11634
11903
  );
11635
11904
  return;
11636
11905
  }
@@ -11641,6 +11910,15 @@ Uploaded: ${now.toISOString()}`;
11641
11910
  }
11642
11911
  }
11643
11912
  var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
11913
+ var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
11914
+ function parseToolArg(argv) {
11915
+ for (let i = 0; i < argv.length; i++) {
11916
+ const a = argv[i];
11917
+ if (a.startsWith("--tool=")) return a.slice("--tool=".length);
11918
+ if (a === "--tool" && i + 1 < argv.length) return argv[i + 1];
11919
+ }
11920
+ return null;
11921
+ }
11644
11922
  async function runUpload() {
11645
11923
  if (process.env[WORKER_ENV_FLAG] === "1") {
11646
11924
  await runUploadWorker();
@@ -11669,11 +11947,17 @@ async function runUpload() {
11669
11947
  appendLog("error", "Cannot detach worker: process.argv[1] is empty");
11670
11948
  return;
11671
11949
  }
11950
+ const toolArg = parseToolArg(process.argv.slice(2));
11951
+ const workerEnv = {
11952
+ ...process.env,
11953
+ [WORKER_ENV_FLAG]: "1"
11954
+ };
11955
+ if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
11672
11956
  try {
11673
11957
  const child = spawn2(process.execPath, [entrypoint, "upload"], {
11674
11958
  detached: true,
11675
11959
  stdio: ["pipe", "ignore", "ignore"],
11676
- env: { ...process.env, [WORKER_ENV_FLAG]: "1" }
11960
+ env: workerEnv
11677
11961
  });
11678
11962
  child.on("error", (err) => {
11679
11963
  appendLog("error", `Failed to spawn worker: ${err.message}`);
@@ -11719,6 +12003,8 @@ async function runUploadWorker() {
11719
12003
  );
11720
12004
  return;
11721
12005
  }
12006
+ const toolOverride = process.env[TOOL_ENV_FLAG];
12007
+ if (toolOverride) payload.tool = toolOverride;
11722
12008
  try {
11723
12009
  await runUploadInner(payload);
11724
12010
  } catch (err) {
@@ -11731,6 +12017,7 @@ async function runUploadWorker() {
11731
12017
 
11732
12018
  // src/git-traces/index.ts
11733
12019
  import { spawn as spawn3 } from "child_process";
12020
+ import crypto2 from "crypto";
11734
12021
 
11735
12022
  // src/git-traces/handlers.ts
11736
12023
  import { execFileSync as execFileSync2 } from "child_process";
@@ -11741,23 +12028,74 @@ import fs9 from "fs";
11741
12028
  import os6 from "os";
11742
12029
  import path12 from "path";
11743
12030
  import { gzipSync } from "zlib";
11744
- var EXEC_OPTS = { timeout: 3e4, maxBuffer: 50 * 1024 * 1024 };
12031
+ var GIT_COMMAND_TIMEOUT_MS = 12e4;
12032
+ var EXEC_OPTS = {
12033
+ timeout: GIT_COMMAND_TIMEOUT_MS,
12034
+ maxBuffer: 50 * 1024 * 1024
12035
+ };
12036
+ var MAX_ERROR_OUTPUT_CHARS = 2e3;
11745
12037
  var MAX_UNTRACKED_FILE_BYTES = 10 * 1024 * 1024;
11746
12038
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
12039
+ function quoteGitArg(arg) {
12040
+ if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
12041
+ return JSON.stringify(arg);
12042
+ }
12043
+ function formatGitCommand(args) {
12044
+ return ["git", ...args.map(quoteGitArg)].join(" ");
12045
+ }
12046
+ function outputToString(output) {
12047
+ if (Buffer.isBuffer(output)) return output.toString("utf-8").trim();
12048
+ if (typeof output === "string") return output.trim();
12049
+ return null;
12050
+ }
12051
+ function truncateOutput(output) {
12052
+ if (output.length <= MAX_ERROR_OUTPUT_CHARS) return output;
12053
+ return `${output.slice(0, MAX_ERROR_OUTPUT_CHARS)}...`;
12054
+ }
12055
+ function formatGitFailure(command, failure, err) {
12056
+ const details = [];
12057
+ if (typeof failure.status === "number")
12058
+ details.push(`exit=${failure.status}`);
12059
+ if (failure.signal) details.push(`signal=${failure.signal}`);
12060
+ const stderr = outputToString(failure.stderr);
12061
+ if (stderr) details.push(`stderr=${truncateOutput(stderr)}`);
12062
+ const stdout = outputToString(failure.stdout);
12063
+ if (!stderr && stdout) details.push(`stdout=${truncateOutput(stdout)}`);
12064
+ if (details.length === 0 && err instanceof Error && err.message) {
12065
+ details.push(err.message);
12066
+ }
12067
+ return `${command} failed${details.length ? ` (${details.join("; ")})` : ""}`;
12068
+ }
12069
+ function createGitError(args, err) {
12070
+ const failure = err;
12071
+ const command = formatGitCommand(args);
12072
+ const isTimeout = failure.code === "ETIMEDOUT";
12073
+ const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : formatGitFailure(command, failure, err);
12074
+ const wrapped = new Error(message);
12075
+ wrapped.isGitTimeout = isTimeout;
12076
+ return wrapped;
12077
+ }
12078
+ function isGitTimeoutError(err) {
12079
+ return err instanceof Error && err.isGitTimeout === true;
12080
+ }
12081
+ function gitBuffer(repoRoot, args, options = {}) {
12082
+ try {
12083
+ return execFileSync("git", args, {
12084
+ cwd: repoRoot,
12085
+ ...options.env ? { env: options.env } : {},
12086
+ ...options.input !== void 0 ? { input: options.input } : {},
12087
+ stdio: ["pipe", "pipe", "pipe"],
12088
+ ...EXEC_OPTS
12089
+ });
12090
+ } catch (err) {
12091
+ throw createGitError(args, err);
12092
+ }
12093
+ }
11747
12094
  function git(repoRoot, args) {
11748
- return execFileSync("git", args, {
11749
- cwd: repoRoot,
11750
- stdio: ["pipe", "pipe", "pipe"],
11751
- ...EXEC_OPTS
11752
- }).toString("utf-8").trim();
12095
+ return gitBuffer(repoRoot, args).toString("utf-8").trim();
11753
12096
  }
11754
12097
  function gitWithEnv(repoRoot, args, env) {
11755
- return execFileSync("git", args, {
11756
- cwd: repoRoot,
11757
- env,
11758
- stdio: ["pipe", "pipe", "pipe"],
11759
- ...EXEC_OPTS
11760
- }).toString("utf-8").trim();
12098
+ return gitBuffer(repoRoot, args, { env }).toString("utf-8").trim();
11761
12099
  }
11762
12100
  function isGitRepo(dir) {
11763
12101
  try {
@@ -11771,11 +12109,13 @@ function captureBaselineSha(repoRoot) {
11771
12109
  try {
11772
12110
  const sha = git(repoRoot, ["stash", "create"]);
11773
12111
  if (sha) return sha;
11774
- } catch {
12112
+ } catch (err) {
12113
+ if (isGitTimeoutError(err)) throw err;
11775
12114
  }
11776
12115
  try {
11777
12116
  return git(repoRoot, ["rev-parse", "HEAD"]);
11778
- } catch {
12117
+ } catch (err) {
12118
+ if (isGitTimeoutError(err)) throw err;
11779
12119
  const tree = git(repoRoot, ["write-tree"]);
11780
12120
  return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
11781
12121
  }
@@ -11793,7 +12133,8 @@ function captureSnapshotSha(repoRoot) {
11793
12133
  try {
11794
12134
  const sha = git(repoRoot, ["stash", "create"]);
11795
12135
  if (sha) return sha;
11796
- } catch {
12136
+ } catch (err) {
12137
+ if (isGitTimeoutError(err)) throw err;
11797
12138
  }
11798
12139
  return git(repoRoot, ["rev-parse", "HEAD"]);
11799
12140
  }
@@ -11828,12 +12169,9 @@ function buildUntrackedTree(repoRoot) {
11828
12169
  );
11829
12170
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
11830
12171
  try {
11831
- execFileSync("git", ["update-index", "--add", "-z", "--stdin"], {
11832
- cwd: repoRoot,
11833
- env,
11834
- input: kept.join("\0") + "\0",
11835
- stdio: ["pipe", "pipe", "pipe"],
11836
- ...EXEC_OPTS
12172
+ gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
12173
+ input: `${kept.join("\0")}\0`,
12174
+ env
11837
12175
  });
11838
12176
  return gitWithEnv(repoRoot, ["write-tree"], env);
11839
12177
  } finally {
@@ -11856,10 +12194,12 @@ function buildSnapshotTree(repoRoot, stashSha) {
11856
12194
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
11857
12195
  try {
11858
12196
  gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
11859
- const untrackedList = execFileSync(
11860
- "git",
12197
+ const untrackedList = gitBuffer(
12198
+ repoRoot,
11861
12199
  ["ls-tree", "-r", untrackedTree],
11862
- { cwd: repoRoot, env, stdio: ["pipe", "pipe", "pipe"], ...EXEC_OPTS }
12200
+ {
12201
+ env
12202
+ }
11863
12203
  ).toString("utf-8");
11864
12204
  const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
11865
12205
  const [meta, filePath] = line.split(" ");
@@ -11867,12 +12207,9 @@ function buildSnapshotTree(repoRoot, stashSha) {
11867
12207
  return `${mode} ${sha} ${filePath}`;
11868
12208
  }).join("\n");
11869
12209
  if (indexInfo) {
11870
- execFileSync("git", ["update-index", "--index-info"], {
11871
- cwd: repoRoot,
11872
- env,
12210
+ gitBuffer(repoRoot, ["update-index", "--index-info"], {
11873
12211
  input: indexInfo,
11874
- stdio: ["pipe", "pipe", "pipe"],
11875
- ...EXEC_OPTS
12212
+ env
11876
12213
  });
11877
12214
  }
11878
12215
  return gitWithEnv(repoRoot, ["write-tree"], env);
@@ -11909,15 +12246,12 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
11909
12246
  }
11910
12247
  function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
11911
12248
  if (fromTreeSha === toTreeSha) return null;
11912
- const diff = execFileSync(
11913
- "git",
11914
- ["diff", "--binary", fromTreeSha, toTreeSha],
11915
- {
11916
- cwd: repoRoot,
11917
- stdio: ["pipe", "pipe", "pipe"],
11918
- ...EXEC_OPTS
11919
- }
11920
- );
12249
+ const diff = gitBuffer(repoRoot, [
12250
+ "diff",
12251
+ "--binary",
12252
+ fromTreeSha,
12253
+ toTreeSha
12254
+ ]);
11921
12255
  if (diff.length === 0) return null;
11922
12256
  return Buffer.from(gzipSync(diff));
11923
12257
  }
@@ -11976,7 +12310,11 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
11976
12310
  function enumerateCommits(repoRoot, fromSha, toSha) {
11977
12311
  let shas;
11978
12312
  try {
11979
- const out = git(repoRoot, ["rev-list", "--reverse", `${fromSha}..${toSha}`]);
12313
+ const out = git(repoRoot, [
12314
+ "rev-list",
12315
+ "--reverse",
12316
+ `${fromSha}..${toSha}`
12317
+ ]);
11980
12318
  if (!out) return [];
11981
12319
  shas = out.split("\n").filter(Boolean);
11982
12320
  } catch (err) {
@@ -12028,12 +12366,7 @@ function parseCommitFiles(repoRoot, sha) {
12028
12366
  let nameStatus = "";
12029
12367
  let numstat = "";
12030
12368
  try {
12031
- nameStatus = git(repoRoot, [
12032
- "show",
12033
- "--name-status",
12034
- "--format=",
12035
- sha
12036
- ]);
12369
+ nameStatus = git(repoRoot, ["show", "--name-status", "--format=", sha]);
12037
12370
  numstat = git(repoRoot, ["show", "--numstat", "--format=", sha]);
12038
12371
  } catch {
12039
12372
  return [];
@@ -12058,9 +12391,9 @@ function parseCommitFiles(repoRoot, sha) {
12058
12391
  oldPath
12059
12392
  });
12060
12393
  } else {
12061
- const path19 = parts[parts.length - 1];
12062
- indexByPath.set(path19, files.length);
12063
- files.push({ path: path19, status, additions: 0, deletions: 0 });
12394
+ const path20 = parts[parts.length - 1];
12395
+ indexByPath.set(path20, files.length);
12396
+ files.push({ path: path20, status, additions: 0, deletions: 0 });
12064
12397
  }
12065
12398
  }
12066
12399
  for (const line of numstat.split("\n")) {
@@ -12081,7 +12414,13 @@ function parseCommitFiles(repoRoot, sha) {
12081
12414
  function safeGit(repoRoot, args) {
12082
12415
  try {
12083
12416
  return git(repoRoot, args);
12084
- } catch {
12417
+ } catch (err) {
12418
+ if (isGitTimeoutError(err)) {
12419
+ appendLog(
12420
+ "warn",
12421
+ `git-traces: optional metadata command skipped: ${err instanceof Error ? err.message : String(err)}`
12422
+ );
12423
+ }
12085
12424
  return null;
12086
12425
  }
12087
12426
  }
@@ -12188,6 +12527,7 @@ var TOOL_LABELS = {
12188
12527
  cursor: "Cursor",
12189
12528
  codex: "Codex",
12190
12529
  claude: "Claude",
12530
+ "copilot-chat": "GitHub Copilot Chat",
12191
12531
  opencode: "opencode"
12192
12532
  };
12193
12533
  function resolveCwd(payload) {
@@ -12235,7 +12575,10 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
12235
12575
  presigned.headers,
12236
12576
  buffer
12237
12577
  );
12238
- appendLog("info", `git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`);
12578
+ appendLog(
12579
+ "info",
12580
+ `git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`
12581
+ );
12239
12582
  }
12240
12583
  function pinEpochBaseline(repoRoot, sessionId, epoch) {
12241
12584
  const baselineSha = captureBaselineSha(repoRoot);
@@ -12246,11 +12589,9 @@ function pinEpochBaseline(repoRoot, sessionId, epoch) {
12246
12589
  );
12247
12590
  return { baselineSha, headSha: captureHeadSha(repoRoot) };
12248
12591
  }
12249
- async function uploadEpochBaseline(params) {
12592
+ function buildEpochBaselineArtifacts(params) {
12250
12593
  const {
12251
12594
  repoRoot,
12252
- client,
12253
- contributionId,
12254
12595
  sessionId,
12255
12596
  tool,
12256
12597
  epoch,
@@ -12260,50 +12601,63 @@ async function uploadEpochBaseline(params) {
12260
12601
  startedAt
12261
12602
  } = params;
12262
12603
  const prefix = epochPrefix(epoch);
12263
- const metadata = buildBaselineMetadata(
12264
- repoRoot,
12265
- sessionId,
12266
- tool,
12267
- baselineSha,
12268
- CLI_VERSION,
12269
- epoch,
12270
- prevHeadSha,
12271
- transitionKind,
12272
- startedAt
12273
- );
12274
- let baselineTreeSha;
12275
- let bundleBuffer;
12276
12604
  try {
12277
- baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
12278
- bundleBuffer = createBundleFromTree(
12605
+ const metadata = buildBaselineMetadata(
12606
+ repoRoot,
12607
+ sessionId,
12608
+ tool,
12609
+ baselineSha,
12610
+ CLI_VERSION,
12611
+ epoch,
12612
+ prevHeadSha,
12613
+ transitionKind,
12614
+ startedAt
12615
+ );
12616
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
12617
+ const bundleBuffer = createBundleFromTree(
12279
12618
  repoRoot,
12280
12619
  baselineTreeSha,
12281
12620
  `${sessionId}-${prefix}`,
12282
12621
  `baseline ${prefix}`
12283
12622
  );
12623
+ const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2));
12624
+ return { baselineTreeSha, metadataBuffer, bundleBuffer };
12284
12625
  } catch (err) {
12285
12626
  appendLog(
12286
12627
  "error",
12287
- `git-traces: failed to create baseline bundle for ${prefix}: ${err instanceof Error ? err.message : String(err)}`
12628
+ `git-traces: failed to build baseline artifacts for ${prefix}: ${formatError(err)}`
12288
12629
  );
12289
12630
  return null;
12290
12631
  }
12291
- const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2));
12632
+ }
12633
+ async function uploadEpochBaselineArtifacts(params) {
12634
+ const { client, contributionId, epoch, artifacts } = params;
12635
+ const prefix = epochPrefix(epoch);
12292
12636
  await uploadFile(
12293
12637
  client,
12294
12638
  contributionId,
12295
12639
  `${prefix}-baseline.json`,
12296
12640
  "application/json",
12297
- metadataBuffer
12641
+ artifacts.metadataBuffer
12298
12642
  );
12299
12643
  await uploadFile(
12300
12644
  client,
12301
12645
  contributionId,
12302
12646
  `${prefix}-baseline.bundle`,
12303
12647
  "application/x-git-bundle",
12304
- bundleBuffer
12648
+ artifacts.bundleBuffer
12305
12649
  );
12306
- return { baselineTreeSha };
12650
+ }
12651
+ async function uploadEpochBaseline(params) {
12652
+ const artifacts = buildEpochBaselineArtifacts(params);
12653
+ if (!artifacts) return null;
12654
+ await uploadEpochBaselineArtifacts({
12655
+ client: params.client,
12656
+ contributionId: params.contributionId,
12657
+ epoch: params.epoch,
12658
+ artifacts
12659
+ });
12660
+ return { baselineTreeSha: artifacts.baselineTreeSha };
12307
12661
  }
12308
12662
  async function openEpoch(params) {
12309
12663
  const { baselineSha, headSha } = pinEpochBaseline(
@@ -12318,7 +12672,10 @@ async function openEpoch(params) {
12318
12672
  async function initializeSession(cwd, tool, sessionId) {
12319
12673
  const project = await findProjectForCwd(cwd);
12320
12674
  if (!project) {
12321
- appendLog("warn", "git-traces: no hillclimb project config found, skipping");
12675
+ appendLog(
12676
+ "warn",
12677
+ "git-traces: no hillclimb project config found, skipping"
12678
+ );
12322
12679
  return null;
12323
12680
  }
12324
12681
  const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
@@ -12398,6 +12755,17 @@ async function handleStop(payload, tool) {
12398
12755
  identity.sessionCookie
12399
12756
  );
12400
12757
  if (state.contributionId === null) {
12758
+ const artifacts = buildEpochBaselineArtifacts({
12759
+ repoRoot: cwd,
12760
+ sessionId: state.sessionId,
12761
+ tool,
12762
+ epoch: 1,
12763
+ baselineSha: state.baselineSha,
12764
+ prevHeadSha: null,
12765
+ transitionKind: "initial",
12766
+ startedAt: state.startedAt
12767
+ });
12768
+ if (!artifacts) return;
12401
12769
  const toolLabel = TOOL_LABELS[tool] ?? "Claude";
12402
12770
  const now = /* @__PURE__ */ new Date();
12403
12771
  const shortId = state.sessionId.slice(0, 12);
@@ -12412,23 +12780,16 @@ Repo: ${cwd}
12412
12780
  Uploaded: ${now.toISOString()}`
12413
12781
  }
12414
12782
  );
12415
- await client.submitContribution(contribution.id);
12416
- const uploaded = await uploadEpochBaseline({
12417
- repoRoot: cwd,
12783
+ await uploadEpochBaselineArtifacts({
12418
12784
  client,
12419
12785
  contributionId: contribution.id,
12420
- sessionId: state.sessionId,
12421
- tool,
12422
12786
  epoch: 1,
12423
- baselineSha: state.baselineSha,
12424
- prevHeadSha: null,
12425
- transitionKind: "initial",
12426
- startedAt: state.startedAt
12787
+ artifacts
12427
12788
  });
12428
- if (!uploaded) return;
12789
+ await client.submitContribution(contribution.id);
12429
12790
  state.contributionId = contribution.id;
12430
- state.baselineTreeSha = uploaded.baselineTreeSha;
12431
- state.lastSnapshotTreeSha = uploaded.baselineTreeSha;
12791
+ state.baselineTreeSha = artifacts.baselineTreeSha;
12792
+ state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
12432
12793
  await writeSessionState(state, tool);
12433
12794
  appendLog(
12434
12795
  "info",
@@ -12542,9 +12903,19 @@ async function handleSessionEnd(payload, tool) {
12542
12903
 
12543
12904
  // src/git-traces/index.ts
12544
12905
  var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
12545
- var TOOL_ENV_FLAG = "HILLCLIMB_GIT_TRACES_TOOL";
12546
- var KNOWN_TOOLS = /* @__PURE__ */ new Set(["claude", "codex", "cursor", "opencode"]);
12547
- function parseToolArg(argv) {
12906
+ var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
12907
+ var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
12908
+ function newFlowId() {
12909
+ return crypto2.randomBytes(3).toString("hex");
12910
+ }
12911
+ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
12912
+ "claude",
12913
+ "codex",
12914
+ "copilot-chat",
12915
+ "cursor",
12916
+ "opencode"
12917
+ ]);
12918
+ function parseToolArg2(argv) {
12548
12919
  for (let i = 0; i < argv.length; i++) {
12549
12920
  const a = argv[i];
12550
12921
  if (a.startsWith("--tool=")) return a.slice("--tool=".length);
@@ -12565,7 +12936,9 @@ async function runGitTraces() {
12565
12936
  await runGitTracesWorker();
12566
12937
  return;
12567
12938
  }
12568
- const toolArg = parseToolArg(process.argv.slice(2));
12939
+ const flowId = newFlowId();
12940
+ setLogPrefix(`[${flowId}]`);
12941
+ const toolArg = parseToolArg2(process.argv.slice(2));
12569
12942
  appendLog(
12570
12943
  "info",
12571
12944
  `git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
@@ -12581,10 +12954,7 @@ async function runGitTraces() {
12581
12954
  try {
12582
12955
  raw = await readStdin2();
12583
12956
  } catch (err) {
12584
- appendLog(
12585
- "error",
12586
- `git-traces: failed to read stdin: ${err instanceof Error ? err.message : String(err)}`
12587
- );
12957
+ appendLog("error", `git-traces: failed to read stdin: ${formatError(err)}`);
12588
12958
  return;
12589
12959
  }
12590
12960
  if (!raw.trim()) {
@@ -12606,7 +12976,8 @@ async function runGitTraces() {
12606
12976
  env: {
12607
12977
  ...process.env,
12608
12978
  [WORKER_ENV_FLAG2]: "1",
12609
- [TOOL_ENV_FLAG]: toolArg
12979
+ [TOOL_ENV_FLAG2]: toolArg,
12980
+ [FLOW_ID_ENV]: flowId
12610
12981
  }
12611
12982
  }
12612
12983
  );
@@ -12623,12 +12994,13 @@ async function runGitTraces() {
12623
12994
  } catch (err) {
12624
12995
  appendLog(
12625
12996
  "error",
12626
- `git-traces: failed to spawn worker: ${err instanceof Error ? err.message : String(err)}`
12997
+ `git-traces: failed to spawn worker: ${formatError(err)}`
12627
12998
  );
12628
12999
  }
12629
13000
  }
12630
13001
  async function runGitTracesWorker() {
12631
- const tool = process.env[TOOL_ENV_FLAG] ?? parseToolArg(process.argv.slice(2)) ?? null;
13002
+ setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
13003
+ const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
12632
13004
  appendLog(
12633
13005
  "info",
12634
13006
  `git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"})`
@@ -12646,7 +13018,7 @@ async function runGitTracesWorker() {
12646
13018
  } catch (err) {
12647
13019
  appendLog(
12648
13020
  "error",
12649
- `git-traces worker: failed to read stdin: ${err instanceof Error ? err.message : String(err)}`
13021
+ `git-traces worker: failed to read stdin: ${formatError(err)}`
12650
13022
  );
12651
13023
  return;
12652
13024
  }
@@ -12660,7 +13032,7 @@ async function runGitTracesWorker() {
12660
13032
  } catch (err) {
12661
13033
  appendLog(
12662
13034
  "error",
12663
- `git-traces worker: failed to parse payload: ${err instanceof Error ? err.message : String(err)}`
13035
+ `git-traces worker: failed to parse payload: ${formatError(err)}`
12664
13036
  );
12665
13037
  return;
12666
13038
  }
@@ -12699,9 +13071,12 @@ async function runGitTracesWorker() {
12699
13071
  appendLog("warn", `git-traces worker: unknown event ${event}`);
12700
13072
  }
12701
13073
  } catch (err) {
13074
+ const detail = formatError(err);
13075
+ const stack = err instanceof Error ? err.stack : void 0;
12702
13076
  appendLog(
12703
13077
  "error",
12704
- `git-traces worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13078
+ `git-traces worker: unexpected error: ${detail}${stack ? `
13079
+ ${stack}` : ""}`
12705
13080
  );
12706
13081
  }
12707
13082
  }
@@ -13202,8 +13577,106 @@ var CodexSource = class {
13202
13577
  }
13203
13578
  };
13204
13579
 
13580
+ // src/sources/copilotChat.ts
13581
+ import fs15 from "fs";
13582
+ import os11 from "os";
13583
+ import path18 from "path";
13584
+ import { fileURLToPath } from "url";
13585
+ function vsCodeUserDirs() {
13586
+ const home = os11.homedir();
13587
+ const dirs = [
13588
+ path18.join(home, "Library", "Application Support", "Code", "User"),
13589
+ path18.join(home, ".config", "Code", "User")
13590
+ ];
13591
+ if (process.env.APPDATA) {
13592
+ dirs.push(path18.join(process.env.APPDATA, "Code", "User"));
13593
+ }
13594
+ return dirs;
13595
+ }
13596
+ function uriToFsPath(uri) {
13597
+ if (!uri.startsWith("file://")) return null;
13598
+ try {
13599
+ return fileURLToPath(uri);
13600
+ } catch {
13601
+ return null;
13602
+ }
13603
+ }
13604
+ async function readWorkspaceFolder(workspaceJsonPath) {
13605
+ let raw;
13606
+ try {
13607
+ raw = await fs15.promises.readFile(workspaceJsonPath, "utf-8");
13608
+ } catch {
13609
+ return null;
13610
+ }
13611
+ let data;
13612
+ try {
13613
+ data = JSON.parse(raw);
13614
+ } catch {
13615
+ return null;
13616
+ }
13617
+ if (!data || typeof data !== "object") return null;
13618
+ const obj = data;
13619
+ if (typeof obj.folder === "string") {
13620
+ return uriToFsPath(obj.folder) ?? obj.folder;
13621
+ }
13622
+ return null;
13623
+ }
13624
+ var CopilotChatSource = class {
13625
+ name = "copilot-chat";
13626
+ async scan() {
13627
+ const results = [];
13628
+ for (const userDir of vsCodeUserDirs()) {
13629
+ const workspaceStorage = path18.join(userDir, "workspaceStorage");
13630
+ let hashDirs;
13631
+ try {
13632
+ hashDirs = await fs15.promises.readdir(workspaceStorage, {
13633
+ withFileTypes: true
13634
+ });
13635
+ } catch {
13636
+ continue;
13637
+ }
13638
+ for (const hash of hashDirs) {
13639
+ if (!hash.isDirectory()) continue;
13640
+ const wsRoot = path18.join(workspaceStorage, hash.name);
13641
+ const transcriptsDir = path18.join(
13642
+ wsRoot,
13643
+ "GitHub.copilot-chat",
13644
+ "transcripts"
13645
+ );
13646
+ let transcriptEntries;
13647
+ try {
13648
+ transcriptEntries = await fs15.promises.readdir(transcriptsDir, {
13649
+ withFileTypes: true
13650
+ });
13651
+ } catch {
13652
+ continue;
13653
+ }
13654
+ const repoPath = await readWorkspaceFolder(
13655
+ path18.join(wsRoot, "workspace.json")
13656
+ );
13657
+ if (!repoPath) continue;
13658
+ for (const entry of transcriptEntries) {
13659
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
13660
+ const sessionId = entry.name.slice(0, -".jsonl".length);
13661
+ results.push({
13662
+ sourceName: this.name,
13663
+ absolutePath: path18.join(transcriptsDir, entry.name),
13664
+ repoPath,
13665
+ metadata: { sessionId }
13666
+ });
13667
+ }
13668
+ }
13669
+ }
13670
+ return results;
13671
+ }
13672
+ };
13673
+
13205
13674
  // src/sources/index.ts
13206
- var sources = [new ClaudeSource(), new CodexSource()];
13675
+ var sources = [
13676
+ new ClaudeSource(),
13677
+ new CodexSource(),
13678
+ new CopilotChatSource()
13679
+ ];
13207
13680
 
13208
13681
  // src/cli.ts
13209
13682
  function reportRedactionStats(noun, stats) {
@@ -13227,7 +13700,7 @@ function reportRedactionStats(noun, stats) {
13227
13700
  async function filterByTimeRange(group, range) {
13228
13701
  const results = await Promise.all(
13229
13702
  group.files.map(
13230
- (f) => fs15.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
13703
+ (f) => fs16.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
13231
13704
  )
13232
13705
  );
13233
13706
  const filtered = [];
@@ -13254,10 +13727,10 @@ async function runInteractive() {
13254
13727
  s.start(`Scanning ${source.name} logs...`);
13255
13728
  const allFiles = await source.scan();
13256
13729
  const allGroups = await mergeByRepo(allFiles);
13257
- const repoRoot = path18.resolve(repo.root);
13730
+ const repoRoot = path19.resolve(repo.root);
13258
13731
  const matching = allGroups.filter((g) => {
13259
- const resolved = path18.resolve(g.repoPath);
13260
- return resolved === repoRoot || resolved.startsWith(repoRoot + path18.sep);
13732
+ const resolved = path19.resolve(g.repoPath);
13733
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path19.sep);
13261
13734
  });
13262
13735
  if (matching.length === 0) {
13263
13736
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -13288,7 +13761,7 @@ async function runInteractive() {
13288
13761
  }
13289
13762
  }
13290
13763
  const envFileNames = await discoverEnvFiles(repoRoot);
13291
- const envFilePaths = envFileNames.map((n) => path18.join(repoRoot, n));
13764
+ const envFilePaths = envFileNames.map((n) => path19.join(repoRoot, n));
13292
13765
  const additionalFiles = await promptSecretFiles(envFileNames);
13293
13766
  const secretResult = await collectSecrets(
13294
13767
  repoRoot,
@@ -13332,11 +13805,14 @@ async function main() {
13332
13805
  const [, , subcommand, ...rest] = process.argv;
13333
13806
  switch (subcommand) {
13334
13807
  case void 0:
13335
- await runInteractive();
13808
+ await runInit([]);
13336
13809
  return;
13337
13810
  case "init":
13338
13811
  await runInit(rest);
13339
13812
  return;
13813
+ case "export":
13814
+ await runInteractive();
13815
+ return;
13340
13816
  case "login":
13341
13817
  await runLogin(rest);
13342
13818
  return;
@@ -13356,11 +13832,15 @@ async function main() {
13356
13832
  case "-h":
13357
13833
  case "help": {
13358
13834
  process.stdout.write(
13359
- "Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) 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 init Configure this repo for automatic upload (--login forces fresh sign-in)\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"
13835
+ "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"
13360
13836
  );
13361
13837
  return;
13362
13838
  }
13363
13839
  default:
13840
+ if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
13841
+ await runInit([subcommand, ...rest]);
13842
+ return;
13843
+ }
13364
13844
  process.stderr.write(
13365
13845
  `Unknown subcommand: ${subcommand}${rest.length > 0 ? ` ${rest.join(" ")}` : ""}
13366
13846
  `