hillclimb 0.1.3 → 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.
- package/README.md +20 -2
- package/dist/cli.js +430 -166
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode) and uploa
|
|
|
5
5
|
## Quickstart
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx hillclimb
|
|
8
|
+
npx hillclimb
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Run in any git repo. Sessions auto-upload when they end.
|
|
@@ -18,7 +18,25 @@ Shows login and hook status for the current repo.
|
|
|
18
18
|
|
|
19
19
|
## Hooks
|
|
20
20
|
|
|
21
|
-
`hillclimb
|
|
21
|
+
`hillclimb` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`) and installs upload + git-trace hooks for each. You might want to gitignore the respective directories for the tools you use in your repo.
|
|
22
|
+
|
|
23
|
+
To manually export historical logs instead of configuring auto-upload, run:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx hillclimb export
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Logs
|
|
30
|
+
|
|
31
|
+
Hillclimb writes one log file per day to `~/.hillclimb/logs/YYYY-MM-DD.log`. Timestamps and the daily filename are in Pacific Time, so logs line up with Hillclimb's internal clock regardless of your timezone.
|
|
32
|
+
|
|
33
|
+
Each hook firing tags every line it writes (parent + worker) with a short correlation ID, e.g. `[a1b2c3]`. To follow one flow across interleaved hook runs:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
grep '\[a1b2c3\]' ~/.hillclimb/logs/$(TZ=America/Los_Angeles date +%F).log
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
HTTP calls log method, path, status, and latency. Auth codes and presigned-URL signatures are deliberately omitted.
|
|
22
40
|
|
|
23
41
|
## License
|
|
24
42
|
|
package/dist/cli.js
CHANGED
|
@@ -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
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
507
|
+
import fs4 from "fs";
|
|
381
508
|
import os2 from "os";
|
|
382
|
-
import
|
|
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(
|
|
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(
|
|
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,7 @@ 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(
|
|
552
|
+
detect: () => isDir(path5.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path5.join(os2.homedir(), ".config", "opencode"))
|
|
426
553
|
},
|
|
427
554
|
{
|
|
428
555
|
// VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
|
|
@@ -462,36 +589,36 @@ var TOOLS = [
|
|
|
462
589
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("codex") },
|
|
463
590
|
{ eventName: "Stop", command: GIT_TRACES_CMD("codex") }
|
|
464
591
|
],
|
|
465
|
-
detect: () => isDir(
|
|
592
|
+
detect: () => isDir(path5.join(os2.homedir(), ".codex"))
|
|
466
593
|
}
|
|
467
594
|
];
|
|
468
595
|
function isDir(p7) {
|
|
469
596
|
try {
|
|
470
|
-
return
|
|
597
|
+
return fs4.statSync(p7).isDirectory();
|
|
471
598
|
} catch {
|
|
472
599
|
return false;
|
|
473
600
|
}
|
|
474
601
|
}
|
|
475
602
|
function copilotChatDetect() {
|
|
476
603
|
const home = os2.homedir();
|
|
477
|
-
const suffix =
|
|
604
|
+
const suffix = path5.join(
|
|
478
605
|
"User",
|
|
479
606
|
"globalStorage",
|
|
480
607
|
"github.copilot-chat"
|
|
481
608
|
);
|
|
482
609
|
const candidates = [
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
process.env.APPDATA ?
|
|
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
|
|
486
613
|
].filter((p7) => p7 !== null);
|
|
487
614
|
return candidates.some(isDir);
|
|
488
615
|
}
|
|
489
616
|
function settingsPath(repoRoot, def) {
|
|
490
|
-
return
|
|
617
|
+
return path5.join(repoRoot, def.settingsFile);
|
|
491
618
|
}
|
|
492
619
|
async function readJson(file) {
|
|
493
620
|
try {
|
|
494
|
-
const raw = await
|
|
621
|
+
const raw = await fs4.promises.readFile(file, "utf-8");
|
|
495
622
|
const parsed = JSON.parse(raw);
|
|
496
623
|
if (parsed && typeof parsed === "object") return parsed;
|
|
497
624
|
return {};
|
|
@@ -501,8 +628,8 @@ async function readJson(file) {
|
|
|
501
628
|
}
|
|
502
629
|
}
|
|
503
630
|
async function writeJson(file, obj) {
|
|
504
|
-
await
|
|
505
|
-
await
|
|
631
|
+
await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
|
|
632
|
+
await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
|
|
506
633
|
`);
|
|
507
634
|
}
|
|
508
635
|
function claudeHookPresent(matchers, command) {
|
|
@@ -613,8 +740,8 @@ function copilotUninstall(settings, eventName, command) {
|
|
|
613
740
|
var OPENCODE_PLUGIN_VERSION = 2;
|
|
614
741
|
var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
|
|
615
742
|
var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
|
|
616
|
-
// Auto-installed by \`npx hillclimb
|
|
617
|
-
//
|
|
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.
|
|
618
745
|
|
|
619
746
|
import { spawn } from "node:child_process";
|
|
620
747
|
import fs from "node:fs";
|
|
@@ -760,19 +887,19 @@ export default HillclimbPlugin;
|
|
|
760
887
|
`;
|
|
761
888
|
async function opencodeInstall(file) {
|
|
762
889
|
try {
|
|
763
|
-
const existing = await
|
|
890
|
+
const existing = await fs4.promises.readFile(file, "utf-8");
|
|
764
891
|
if (existing === OPENCODE_PLUGIN_CONTENT) {
|
|
765
892
|
return { installed: 0, alreadyPresent: 1 };
|
|
766
893
|
}
|
|
767
894
|
} catch {
|
|
768
895
|
}
|
|
769
|
-
await
|
|
770
|
-
await
|
|
896
|
+
await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
|
|
897
|
+
await fs4.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
|
|
771
898
|
return { installed: 1, alreadyPresent: 0 };
|
|
772
899
|
}
|
|
773
900
|
async function opencodeCheck(file) {
|
|
774
901
|
try {
|
|
775
|
-
const content = await
|
|
902
|
+
const content = await fs4.promises.readFile(file, "utf-8");
|
|
776
903
|
return content.includes(OPENCODE_PLUGIN_MARKER);
|
|
777
904
|
} catch {
|
|
778
905
|
return false;
|
|
@@ -900,17 +1027,17 @@ async function checkAllHooks(repoRoot) {
|
|
|
900
1027
|
}
|
|
901
1028
|
return results;
|
|
902
1029
|
}
|
|
903
|
-
var CODEX_CONFIG_PATH =
|
|
1030
|
+
var CODEX_CONFIG_PATH = path5.join(os2.homedir(), ".codex", "config.toml");
|
|
904
1031
|
async function ensureCodexHooksEnabled() {
|
|
905
1032
|
let content;
|
|
906
1033
|
try {
|
|
907
|
-
content = await
|
|
1034
|
+
content = await fs4.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
|
|
908
1035
|
} catch (err) {
|
|
909
1036
|
if (err.code === "ENOENT") {
|
|
910
|
-
await
|
|
1037
|
+
await fs4.promises.mkdir(path5.dirname(CODEX_CONFIG_PATH), {
|
|
911
1038
|
recursive: true
|
|
912
1039
|
});
|
|
913
|
-
await
|
|
1040
|
+
await fs4.promises.writeFile(
|
|
914
1041
|
CODEX_CONFIG_PATH,
|
|
915
1042
|
"[features]\ncodex_hooks = true\n"
|
|
916
1043
|
);
|
|
@@ -926,7 +1053,7 @@ async function ensureCodexHooksEnabled() {
|
|
|
926
1053
|
} else {
|
|
927
1054
|
content = content.trimEnd() + "\n\n[features]\ncodex_hooks = true\n";
|
|
928
1055
|
}
|
|
929
|
-
await
|
|
1056
|
+
await fs4.promises.writeFile(CODEX_CONFIG_PATH, content);
|
|
930
1057
|
return true;
|
|
931
1058
|
}
|
|
932
1059
|
var CLAUDE_DEF = TOOLS[0];
|
|
@@ -982,8 +1109,10 @@ function openBrowser(url) {
|
|
|
982
1109
|
}
|
|
983
1110
|
async function webAuth(apiBaseUrl) {
|
|
984
1111
|
if (!IS_TTY) {
|
|
1112
|
+
appendLog("info", "web-auth: skipped (not a TTY)");
|
|
985
1113
|
return null;
|
|
986
1114
|
}
|
|
1115
|
+
appendLog("info", `web-auth: started (apiBaseUrl=${apiBaseUrl})`);
|
|
987
1116
|
const client = new PlatformClient(apiBaseUrl);
|
|
988
1117
|
const codeResponse = await withSpinner(
|
|
989
1118
|
"Preparing web authentication...",
|
|
@@ -991,6 +1120,10 @@ async function webAuth(apiBaseUrl) {
|
|
|
991
1120
|
"Could not start web authentication.",
|
|
992
1121
|
() => client.createCliAuthCode()
|
|
993
1122
|
);
|
|
1123
|
+
appendLog(
|
|
1124
|
+
"info",
|
|
1125
|
+
`web-auth: code prepared (expiresIn=${codeResponse.expiresIn}s, pollInterval=${codeResponse.interval}s)`
|
|
1126
|
+
);
|
|
994
1127
|
const url = codeResponse.verificationUrl;
|
|
995
1128
|
openBrowser(url);
|
|
996
1129
|
p2.log.info(`Opening browser to: ${cyan(url)}`);
|
|
@@ -1004,6 +1137,7 @@ async function webAuth(apiBaseUrl) {
|
|
|
1004
1137
|
try {
|
|
1005
1138
|
const poll = await client.pollCliAuthCode(codeResponse.code);
|
|
1006
1139
|
if (poll.status === "approved" && poll.sessionToken) {
|
|
1140
|
+
appendLog("info", "web-auth: authorized");
|
|
1007
1141
|
spinner5.stop("Authorized.");
|
|
1008
1142
|
const secure = apiBaseUrl.startsWith("https://");
|
|
1009
1143
|
const cookieName = secure ? "__Secure-better-auth.session_token" : "better-auth.session_token";
|
|
@@ -1019,6 +1153,10 @@ async function webAuth(apiBaseUrl) {
|
|
|
1019
1153
|
return data;
|
|
1020
1154
|
}
|
|
1021
1155
|
);
|
|
1156
|
+
appendLog(
|
|
1157
|
+
"info",
|
|
1158
|
+
`web-auth: completed (email=${bootstrap.session.email})`
|
|
1159
|
+
);
|
|
1022
1160
|
return {
|
|
1023
1161
|
client,
|
|
1024
1162
|
sessionCookie,
|
|
@@ -1027,13 +1165,16 @@ async function webAuth(apiBaseUrl) {
|
|
|
1027
1165
|
};
|
|
1028
1166
|
}
|
|
1029
1167
|
if (poll.status === "expired") {
|
|
1168
|
+
appendLog("warn", "web-auth: code expired before authorization");
|
|
1030
1169
|
spinner5.stop("Authorization expired.");
|
|
1031
1170
|
p2.log.warn("The authorization code expired. Please try again.");
|
|
1032
1171
|
return null;
|
|
1033
1172
|
}
|
|
1034
|
-
} catch {
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
appendLog("warn", `web-auth: poll attempt failed: ${formatError(err)}`);
|
|
1035
1175
|
}
|
|
1036
1176
|
}
|
|
1177
|
+
appendLog("warn", "web-auth: timed out waiting for authorization");
|
|
1037
1178
|
spinner5.stop("Authorization timed out.");
|
|
1038
1179
|
p2.log.warn("Timed out waiting for browser authorization.");
|
|
1039
1180
|
return null;
|
|
@@ -1087,16 +1228,20 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
|
|
|
1087
1228
|
async function runInit(args = []) {
|
|
1088
1229
|
const forceLogin = args.includes("--login");
|
|
1089
1230
|
const workspaceSlugFlag = parseStringFlag(args, "workspace");
|
|
1090
|
-
|
|
1231
|
+
appendLog(
|
|
1232
|
+
"info",
|
|
1233
|
+
`init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, workspaceSlug=${workspaceSlugFlag ?? "<none>"})`
|
|
1234
|
+
);
|
|
1235
|
+
p3.intro("hillclimb");
|
|
1091
1236
|
const repo = detectRepoRoot();
|
|
1092
1237
|
if (!repo) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
);
|
|
1238
|
+
appendLog("error", "init: not a git repository, aborting");
|
|
1239
|
+
p3.log.error("Not a git repository. Run `npx hillclimb` inside a repo.");
|
|
1096
1240
|
p3.outro("Aborted.");
|
|
1097
1241
|
process.exit(1);
|
|
1098
1242
|
}
|
|
1099
1243
|
const { root: repoRoot, name: repoName } = repo;
|
|
1244
|
+
appendLog("info", `init: repo detected (root=${repoRoot}, name=${repoName})`);
|
|
1100
1245
|
p3.log.info(`Repo: ${repoName} (${repoRoot})`);
|
|
1101
1246
|
const apiBaseUrl = resolveDefaultApiUrl();
|
|
1102
1247
|
const saved = forceLogin ? null : await loadIdentity(apiBaseUrl);
|
|
@@ -1126,9 +1271,11 @@ async function runInit(args = []) {
|
|
|
1126
1271
|
}
|
|
1127
1272
|
method = selected;
|
|
1128
1273
|
}
|
|
1274
|
+
appendLog("info", `init: auth method selected (${method})`);
|
|
1129
1275
|
if (method === "web") {
|
|
1130
1276
|
const result = await webAuth(apiBaseUrl);
|
|
1131
1277
|
if (!result) {
|
|
1278
|
+
appendLog("error", "init: web authentication failed");
|
|
1132
1279
|
p3.log.error("Web authentication failed.");
|
|
1133
1280
|
p3.outro("Aborted.");
|
|
1134
1281
|
process.exit(1);
|
|
@@ -1193,6 +1340,10 @@ async function runInit(args = []) {
|
|
|
1193
1340
|
}
|
|
1194
1341
|
pickedId = resolved.organizationId;
|
|
1195
1342
|
workspace = resolved;
|
|
1343
|
+
appendLog(
|
|
1344
|
+
"info",
|
|
1345
|
+
`init: workspace pre-scoped (slug=${workspaceSlugFlag}, id=${resolved.id}, orgId=${pickedId})`
|
|
1346
|
+
);
|
|
1196
1347
|
const belongs = orgs.some((o) => o.id === pickedId);
|
|
1197
1348
|
if (!belongs && !isAdmin) {
|
|
1198
1349
|
p3.log.error(
|
|
@@ -1209,6 +1360,7 @@ async function runInit(args = []) {
|
|
|
1209
1360
|
await client.setActiveOrganization(pickedId);
|
|
1210
1361
|
}
|
|
1211
1362
|
} catch (err) {
|
|
1363
|
+
appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
|
|
1212
1364
|
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1213
1365
|
p3.outro("Aborted.");
|
|
1214
1366
|
process.exit(1);
|
|
@@ -1249,6 +1401,7 @@ async function runInit(args = []) {
|
|
|
1249
1401
|
await client.setActiveOrganization(pickedId);
|
|
1250
1402
|
}
|
|
1251
1403
|
} catch (err) {
|
|
1404
|
+
appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
|
|
1252
1405
|
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1253
1406
|
p3.outro("Aborted.");
|
|
1254
1407
|
process.exit(1);
|
|
@@ -1289,6 +1442,10 @@ async function runInit(args = []) {
|
|
|
1289
1442
|
process.exit(1);
|
|
1290
1443
|
}
|
|
1291
1444
|
workspace = picked;
|
|
1445
|
+
appendLog(
|
|
1446
|
+
"info",
|
|
1447
|
+
`init: workspace selected (slug=${workspace.slug}, id=${workspace.id}, orgId=${pickedId})`
|
|
1448
|
+
);
|
|
1292
1449
|
}
|
|
1293
1450
|
const types = await withSpinner(
|
|
1294
1451
|
"Loading contribution types...",
|
|
@@ -1339,6 +1496,10 @@ async function runInit(args = []) {
|
|
|
1339
1496
|
p3.outro("Aborted.");
|
|
1340
1497
|
process.exit(1);
|
|
1341
1498
|
}
|
|
1499
|
+
appendLog(
|
|
1500
|
+
"info",
|
|
1501
|
+
`init: contribution type selected (slug=${type.slug}, name=${type.name})`
|
|
1502
|
+
);
|
|
1342
1503
|
const pickedOrg = orgs.find((o) => o.id === activeOrgId);
|
|
1343
1504
|
await upsertProject(repoRoot, {
|
|
1344
1505
|
apiBaseUrl,
|
|
@@ -1351,6 +1512,7 @@ async function runInit(args = []) {
|
|
|
1351
1512
|
contributionTypeName: type.name,
|
|
1352
1513
|
autoSubmit: true
|
|
1353
1514
|
});
|
|
1515
|
+
appendLog("info", `init: project config saved (repoRoot=${repoRoot})`);
|
|
1354
1516
|
p3.log.success(`Saved config for ${repoRoot}`);
|
|
1355
1517
|
await saveIdentity({
|
|
1356
1518
|
apiBaseUrl,
|
|
@@ -1362,10 +1524,19 @@ async function runInit(args = []) {
|
|
|
1362
1524
|
try {
|
|
1363
1525
|
const hookResults = await installDetectedHooks(repoRoot);
|
|
1364
1526
|
if (hookResults.length === 0) {
|
|
1527
|
+
appendLog(
|
|
1528
|
+
"warn",
|
|
1529
|
+
"init: no supported tools detected, hooks not installed"
|
|
1530
|
+
);
|
|
1365
1531
|
p3.log.warn(
|
|
1366
1532
|
"No supported tools detected (Claude Code, Cursor, Codex, opencode, GitHub Copilot Chat). Hook not installed."
|
|
1367
1533
|
);
|
|
1368
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
|
+
);
|
|
1369
1540
|
for (const r of hookResults) {
|
|
1370
1541
|
if (r.alreadyPresent) {
|
|
1371
1542
|
p3.log.info(`${r.label} hook already present`);
|
|
@@ -1377,9 +1548,17 @@ async function runInit(args = []) {
|
|
|
1377
1548
|
try {
|
|
1378
1549
|
const changed = await ensureCodexHooksEnabled();
|
|
1379
1550
|
if (changed) {
|
|
1551
|
+
appendLog(
|
|
1552
|
+
"info",
|
|
1553
|
+
"init: enabled codex_hooks in ~/.codex/config.toml"
|
|
1554
|
+
);
|
|
1380
1555
|
p3.log.success("Enabled codex_hooks in ~/.codex/config.toml");
|
|
1381
1556
|
}
|
|
1382
1557
|
} catch (err) {
|
|
1558
|
+
appendLog(
|
|
1559
|
+
"warn",
|
|
1560
|
+
`init: could not enable codex_hooks: ${formatError(err)}`
|
|
1561
|
+
);
|
|
1383
1562
|
p3.log.warn(
|
|
1384
1563
|
`Could not enable codex_hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
|
|
1385
1564
|
);
|
|
@@ -1387,12 +1566,17 @@ async function runInit(args = []) {
|
|
|
1387
1566
|
}
|
|
1388
1567
|
}
|
|
1389
1568
|
} catch (err) {
|
|
1569
|
+
appendLog("error", `init: failed to install hooks: ${formatError(err)}`);
|
|
1390
1570
|
p3.log.error(
|
|
1391
1571
|
`Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
1392
1572
|
);
|
|
1393
1573
|
p3.outro("Partially configured. Re-run after fixing the error.");
|
|
1394
1574
|
process.exit(1);
|
|
1395
1575
|
}
|
|
1576
|
+
appendLog(
|
|
1577
|
+
"info",
|
|
1578
|
+
`init: completed (workspace=${workspace.slug}, contributionType=${type.slug})`
|
|
1579
|
+
);
|
|
1396
1580
|
p3.outro(
|
|
1397
1581
|
`Done. Your next coding session in this repo will upload automatically to ${workspace.name}.`
|
|
1398
1582
|
);
|
|
@@ -1433,6 +1617,7 @@ function resolveDefaultApiUrl2() {
|
|
|
1433
1617
|
async function runLogin(_args = []) {
|
|
1434
1618
|
const apiBaseUrl = resolveDefaultApiUrl2();
|
|
1435
1619
|
header("login");
|
|
1620
|
+
appendLog("info", `login: started (apiBaseUrl=${apiBaseUrl})`);
|
|
1436
1621
|
const saved = await loadIdentity(apiBaseUrl);
|
|
1437
1622
|
if (saved) {
|
|
1438
1623
|
if (IS_TTY) {
|
|
@@ -1463,6 +1648,10 @@ async function runLogin(_args = []) {
|
|
|
1463
1648
|
process.stdout.write("\r\x1B[K");
|
|
1464
1649
|
}
|
|
1465
1650
|
if (verified) {
|
|
1651
|
+
appendLog(
|
|
1652
|
+
"info",
|
|
1653
|
+
`login: reused saved login (email=${verified.email})`
|
|
1654
|
+
);
|
|
1466
1655
|
row(CHECK, "Signed in", cyan(verified.email));
|
|
1467
1656
|
const fresh = probe.getSessionCookie();
|
|
1468
1657
|
if (fresh) {
|
|
@@ -1481,10 +1670,15 @@ async function runLogin(_args = []) {
|
|
|
1481
1670
|
return;
|
|
1482
1671
|
}
|
|
1483
1672
|
if (expired) {
|
|
1673
|
+
appendLog("info", "login: saved login expired, falling through");
|
|
1484
1674
|
row(CROSS, "Signed in", dim("expired \u2014 please sign in again"));
|
|
1485
1675
|
println();
|
|
1486
1676
|
await clearIdentity(apiBaseUrl);
|
|
1487
1677
|
} else if (verifyError) {
|
|
1678
|
+
appendLog(
|
|
1679
|
+
"warn",
|
|
1680
|
+
`login: could not verify saved login: ${formatError(verifyError)}`
|
|
1681
|
+
);
|
|
1488
1682
|
row(CROSS, "Signed in", dim("could not verify saved login"));
|
|
1489
1683
|
println(` ${dim(verifyError.message)}`);
|
|
1490
1684
|
println();
|
|
@@ -1510,9 +1704,11 @@ async function runLogin(_args = []) {
|
|
|
1510
1704
|
}
|
|
1511
1705
|
method = selected;
|
|
1512
1706
|
}
|
|
1707
|
+
appendLog("info", `login: auth method selected (${method})`);
|
|
1513
1708
|
if (method === "web") {
|
|
1514
1709
|
const result = await webAuth(apiBaseUrl);
|
|
1515
1710
|
if (!result) {
|
|
1711
|
+
appendLog("error", "login: web authentication failed");
|
|
1516
1712
|
p4.log.error("Web authentication failed.");
|
|
1517
1713
|
p4.outro("Aborted.");
|
|
1518
1714
|
process.exit(1);
|
|
@@ -1524,9 +1720,10 @@ async function runLogin(_args = []) {
|
|
|
1524
1720
|
userId: result.userId,
|
|
1525
1721
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1526
1722
|
});
|
|
1723
|
+
appendLog("info", `login: completed via web auth (email=${result.email})`);
|
|
1527
1724
|
println();
|
|
1528
1725
|
row(CHECK, "Signed in", cyan(result.email));
|
|
1529
|
-
footer("Run `npx hillclimb
|
|
1726
|
+
footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
|
|
1530
1727
|
return;
|
|
1531
1728
|
}
|
|
1532
1729
|
const email = await requireText("Email");
|
|
@@ -1550,6 +1747,7 @@ async function runLogin(_args = []) {
|
|
|
1550
1747
|
);
|
|
1551
1748
|
const cookie = client.getSessionCookie();
|
|
1552
1749
|
if (!cookie) {
|
|
1750
|
+
appendLog("error", "login: session cookie missing after sign-in");
|
|
1553
1751
|
p4.log.error("Session cookie was lost during sign-in.");
|
|
1554
1752
|
p4.outro("Aborted.");
|
|
1555
1753
|
process.exit(1);
|
|
@@ -1561,9 +1759,13 @@ async function runLogin(_args = []) {
|
|
|
1561
1759
|
userId: bootstrap.session.userId,
|
|
1562
1760
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1563
1761
|
});
|
|
1762
|
+
appendLog(
|
|
1763
|
+
"info",
|
|
1764
|
+
`login: completed via email/password (email=${bootstrap.session.email})`
|
|
1765
|
+
);
|
|
1564
1766
|
println();
|
|
1565
1767
|
row(CHECK, "Signed in", cyan(bootstrap.session.email));
|
|
1566
|
-
footer("Run `npx hillclimb
|
|
1768
|
+
footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
|
|
1567
1769
|
}
|
|
1568
1770
|
|
|
1569
1771
|
// src/commands/logout.ts
|
|
@@ -1595,39 +1797,12 @@ async function runLogout(args = []) {
|
|
|
1595
1797
|
);
|
|
1596
1798
|
footer(
|
|
1597
1799
|
"Per-repo upload hooks still active.",
|
|
1598
|
-
"Re-run `npx hillclimb
|
|
1800
|
+
"Re-run `npx hillclimb` to re-authenticate a repo."
|
|
1599
1801
|
);
|
|
1600
1802
|
}
|
|
1601
1803
|
|
|
1602
1804
|
// src/commands/status.ts
|
|
1603
1805
|
import path6 from "path";
|
|
1604
|
-
|
|
1605
|
-
// src/platform/log.ts
|
|
1606
|
-
import fs4 from "fs";
|
|
1607
|
-
import path5 from "path";
|
|
1608
|
-
function today() {
|
|
1609
|
-
const d = /* @__PURE__ */ new Date();
|
|
1610
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1611
|
-
}
|
|
1612
|
-
function logsDir() {
|
|
1613
|
-
return path5.join(configDir(), "logs");
|
|
1614
|
-
}
|
|
1615
|
-
function todayLogPath() {
|
|
1616
|
-
return path5.join(logsDir(), `${today()}.log`);
|
|
1617
|
-
}
|
|
1618
|
-
function appendLog(level, message) {
|
|
1619
|
-
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${message}
|
|
1620
|
-
`;
|
|
1621
|
-
try {
|
|
1622
|
-
fs4.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
1623
|
-
fs4.appendFileSync(todayLogPath(), line);
|
|
1624
|
-
} catch {
|
|
1625
|
-
process.stderr.write(`hillclimb: ${level}: ${message}
|
|
1626
|
-
`);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// src/commands/status.ts
|
|
1631
1806
|
async function runStatus(args = []) {
|
|
1632
1807
|
const debug = args.includes("--debug");
|
|
1633
1808
|
header("status");
|
|
@@ -1641,7 +1816,7 @@ async function runStatus(args = []) {
|
|
|
1641
1816
|
return;
|
|
1642
1817
|
}
|
|
1643
1818
|
row(CROSS, "Repo", `${bold(repo.name)} ${dim("(not configured)")}`);
|
|
1644
|
-
footer(`Run \`npx hillclimb
|
|
1819
|
+
footer(`Run \`npx hillclimb\` in ${repo.name} to connect it.`);
|
|
1645
1820
|
return;
|
|
1646
1821
|
}
|
|
1647
1822
|
const { repoRoot, config } = match;
|
|
@@ -1703,11 +1878,11 @@ async function runStatus(args = []) {
|
|
|
1703
1878
|
row(CROSS, "Auto-upload", dim("error checking hooks"));
|
|
1704
1879
|
}
|
|
1705
1880
|
if (loginError === "expired") {
|
|
1706
|
-
footer("Run `npx hillclimb
|
|
1881
|
+
footer("Run `npx hillclimb` to sign in again.");
|
|
1707
1882
|
} else if (loginError === "unreachable") {
|
|
1708
1883
|
footer(`Couldn't reach ${config.apiBaseUrl}. Check your connection.`);
|
|
1709
1884
|
} else if (!anyHookInstalled) {
|
|
1710
|
-
footer("Run `npx hillclimb
|
|
1885
|
+
footer("Run `npx hillclimb` to install the upload hook.");
|
|
1711
1886
|
} else {
|
|
1712
1887
|
footer("All set.");
|
|
1713
1888
|
}
|
|
@@ -11634,7 +11809,7 @@ async function runUploadInner(payload) {
|
|
|
11634
11809
|
if (!match) {
|
|
11635
11810
|
appendLog(
|
|
11636
11811
|
"warn",
|
|
11637
|
-
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb
|
|
11812
|
+
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
|
|
11638
11813
|
);
|
|
11639
11814
|
return;
|
|
11640
11815
|
}
|
|
@@ -11724,7 +11899,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11724
11899
|
if (err instanceof PlatformError && err.status === 401) {
|
|
11725
11900
|
appendLog(
|
|
11726
11901
|
"error",
|
|
11727
|
-
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb
|
|
11902
|
+
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
|
|
11728
11903
|
);
|
|
11729
11904
|
return;
|
|
11730
11905
|
}
|
|
@@ -11842,6 +12017,7 @@ async function runUploadWorker() {
|
|
|
11842
12017
|
|
|
11843
12018
|
// src/git-traces/index.ts
|
|
11844
12019
|
import { spawn as spawn3 } from "child_process";
|
|
12020
|
+
import crypto2 from "crypto";
|
|
11845
12021
|
|
|
11846
12022
|
// src/git-traces/handlers.ts
|
|
11847
12023
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -11852,23 +12028,74 @@ import fs9 from "fs";
|
|
|
11852
12028
|
import os6 from "os";
|
|
11853
12029
|
import path12 from "path";
|
|
11854
12030
|
import { gzipSync } from "zlib";
|
|
11855
|
-
var
|
|
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;
|
|
11856
12037
|
var MAX_UNTRACKED_FILE_BYTES = 10 * 1024 * 1024;
|
|
11857
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
|
+
}
|
|
11858
12094
|
function git(repoRoot, args) {
|
|
11859
|
-
return
|
|
11860
|
-
cwd: repoRoot,
|
|
11861
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11862
|
-
...EXEC_OPTS
|
|
11863
|
-
}).toString("utf-8").trim();
|
|
12095
|
+
return gitBuffer(repoRoot, args).toString("utf-8").trim();
|
|
11864
12096
|
}
|
|
11865
12097
|
function gitWithEnv(repoRoot, args, env) {
|
|
11866
|
-
return
|
|
11867
|
-
cwd: repoRoot,
|
|
11868
|
-
env,
|
|
11869
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11870
|
-
...EXEC_OPTS
|
|
11871
|
-
}).toString("utf-8").trim();
|
|
12098
|
+
return gitBuffer(repoRoot, args, { env }).toString("utf-8").trim();
|
|
11872
12099
|
}
|
|
11873
12100
|
function isGitRepo(dir) {
|
|
11874
12101
|
try {
|
|
@@ -11882,11 +12109,13 @@ function captureBaselineSha(repoRoot) {
|
|
|
11882
12109
|
try {
|
|
11883
12110
|
const sha = git(repoRoot, ["stash", "create"]);
|
|
11884
12111
|
if (sha) return sha;
|
|
11885
|
-
} catch {
|
|
12112
|
+
} catch (err) {
|
|
12113
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11886
12114
|
}
|
|
11887
12115
|
try {
|
|
11888
12116
|
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
11889
|
-
} catch {
|
|
12117
|
+
} catch (err) {
|
|
12118
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11890
12119
|
const tree = git(repoRoot, ["write-tree"]);
|
|
11891
12120
|
return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
|
|
11892
12121
|
}
|
|
@@ -11904,7 +12133,8 @@ function captureSnapshotSha(repoRoot) {
|
|
|
11904
12133
|
try {
|
|
11905
12134
|
const sha = git(repoRoot, ["stash", "create"]);
|
|
11906
12135
|
if (sha) return sha;
|
|
11907
|
-
} catch {
|
|
12136
|
+
} catch (err) {
|
|
12137
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11908
12138
|
}
|
|
11909
12139
|
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
11910
12140
|
}
|
|
@@ -11939,12 +12169,9 @@ function buildUntrackedTree(repoRoot) {
|
|
|
11939
12169
|
);
|
|
11940
12170
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
11941
12171
|
try {
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
env
|
|
11945
|
-
input: kept.join("\0") + "\0",
|
|
11946
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11947
|
-
...EXEC_OPTS
|
|
12172
|
+
gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
|
|
12173
|
+
input: `${kept.join("\0")}\0`,
|
|
12174
|
+
env
|
|
11948
12175
|
});
|
|
11949
12176
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
11950
12177
|
} finally {
|
|
@@ -11967,10 +12194,12 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
11967
12194
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
11968
12195
|
try {
|
|
11969
12196
|
gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
|
|
11970
|
-
const untrackedList =
|
|
11971
|
-
|
|
12197
|
+
const untrackedList = gitBuffer(
|
|
12198
|
+
repoRoot,
|
|
11972
12199
|
["ls-tree", "-r", untrackedTree],
|
|
11973
|
-
{
|
|
12200
|
+
{
|
|
12201
|
+
env
|
|
12202
|
+
}
|
|
11974
12203
|
).toString("utf-8");
|
|
11975
12204
|
const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
|
|
11976
12205
|
const [meta, filePath] = line.split(" ");
|
|
@@ -11978,12 +12207,9 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
11978
12207
|
return `${mode} ${sha} ${filePath}`;
|
|
11979
12208
|
}).join("\n");
|
|
11980
12209
|
if (indexInfo) {
|
|
11981
|
-
|
|
11982
|
-
cwd: repoRoot,
|
|
11983
|
-
env,
|
|
12210
|
+
gitBuffer(repoRoot, ["update-index", "--index-info"], {
|
|
11984
12211
|
input: indexInfo,
|
|
11985
|
-
|
|
11986
|
-
...EXEC_OPTS
|
|
12212
|
+
env
|
|
11987
12213
|
});
|
|
11988
12214
|
}
|
|
11989
12215
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
@@ -12020,15 +12246,12 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
12020
12246
|
}
|
|
12021
12247
|
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
|
|
12022
12248
|
if (fromTreeSha === toTreeSha) return null;
|
|
12023
|
-
const diff =
|
|
12024
|
-
"
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
...EXEC_OPTS
|
|
12030
|
-
}
|
|
12031
|
-
);
|
|
12249
|
+
const diff = gitBuffer(repoRoot, [
|
|
12250
|
+
"diff",
|
|
12251
|
+
"--binary",
|
|
12252
|
+
fromTreeSha,
|
|
12253
|
+
toTreeSha
|
|
12254
|
+
]);
|
|
12032
12255
|
if (diff.length === 0) return null;
|
|
12033
12256
|
return Buffer.from(gzipSync(diff));
|
|
12034
12257
|
}
|
|
@@ -12087,7 +12310,11 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
12087
12310
|
function enumerateCommits(repoRoot, fromSha, toSha) {
|
|
12088
12311
|
let shas;
|
|
12089
12312
|
try {
|
|
12090
|
-
const out = git(repoRoot, [
|
|
12313
|
+
const out = git(repoRoot, [
|
|
12314
|
+
"rev-list",
|
|
12315
|
+
"--reverse",
|
|
12316
|
+
`${fromSha}..${toSha}`
|
|
12317
|
+
]);
|
|
12091
12318
|
if (!out) return [];
|
|
12092
12319
|
shas = out.split("\n").filter(Boolean);
|
|
12093
12320
|
} catch (err) {
|
|
@@ -12139,12 +12366,7 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12139
12366
|
let nameStatus = "";
|
|
12140
12367
|
let numstat = "";
|
|
12141
12368
|
try {
|
|
12142
|
-
nameStatus = git(repoRoot, [
|
|
12143
|
-
"show",
|
|
12144
|
-
"--name-status",
|
|
12145
|
-
"--format=",
|
|
12146
|
-
sha
|
|
12147
|
-
]);
|
|
12369
|
+
nameStatus = git(repoRoot, ["show", "--name-status", "--format=", sha]);
|
|
12148
12370
|
numstat = git(repoRoot, ["show", "--numstat", "--format=", sha]);
|
|
12149
12371
|
} catch {
|
|
12150
12372
|
return [];
|
|
@@ -12192,7 +12414,13 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12192
12414
|
function safeGit(repoRoot, args) {
|
|
12193
12415
|
try {
|
|
12194
12416
|
return git(repoRoot, args);
|
|
12195
|
-
} 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
|
+
}
|
|
12196
12424
|
return null;
|
|
12197
12425
|
}
|
|
12198
12426
|
}
|
|
@@ -12347,7 +12575,10 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
12347
12575
|
presigned.headers,
|
|
12348
12576
|
buffer
|
|
12349
12577
|
);
|
|
12350
|
-
appendLog(
|
|
12578
|
+
appendLog(
|
|
12579
|
+
"info",
|
|
12580
|
+
`git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`
|
|
12581
|
+
);
|
|
12351
12582
|
}
|
|
12352
12583
|
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
12353
12584
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
@@ -12358,11 +12589,9 @@ function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
|
12358
12589
|
);
|
|
12359
12590
|
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
12360
12591
|
}
|
|
12361
|
-
|
|
12592
|
+
function buildEpochBaselineArtifacts(params) {
|
|
12362
12593
|
const {
|
|
12363
12594
|
repoRoot,
|
|
12364
|
-
client,
|
|
12365
|
-
contributionId,
|
|
12366
12595
|
sessionId,
|
|
12367
12596
|
tool,
|
|
12368
12597
|
epoch,
|
|
@@ -12372,50 +12601,63 @@ async function uploadEpochBaseline(params) {
|
|
|
12372
12601
|
startedAt
|
|
12373
12602
|
} = params;
|
|
12374
12603
|
const prefix = epochPrefix(epoch);
|
|
12375
|
-
const metadata = buildBaselineMetadata(
|
|
12376
|
-
repoRoot,
|
|
12377
|
-
sessionId,
|
|
12378
|
-
tool,
|
|
12379
|
-
baselineSha,
|
|
12380
|
-
CLI_VERSION,
|
|
12381
|
-
epoch,
|
|
12382
|
-
prevHeadSha,
|
|
12383
|
-
transitionKind,
|
|
12384
|
-
startedAt
|
|
12385
|
-
);
|
|
12386
|
-
let baselineTreeSha;
|
|
12387
|
-
let bundleBuffer;
|
|
12388
12604
|
try {
|
|
12389
|
-
|
|
12390
|
-
|
|
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(
|
|
12391
12618
|
repoRoot,
|
|
12392
12619
|
baselineTreeSha,
|
|
12393
12620
|
`${sessionId}-${prefix}`,
|
|
12394
12621
|
`baseline ${prefix}`
|
|
12395
12622
|
);
|
|
12623
|
+
const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2));
|
|
12624
|
+
return { baselineTreeSha, metadataBuffer, bundleBuffer };
|
|
12396
12625
|
} catch (err) {
|
|
12397
12626
|
appendLog(
|
|
12398
12627
|
"error",
|
|
12399
|
-
`git-traces: failed to
|
|
12628
|
+
`git-traces: failed to build baseline artifacts for ${prefix}: ${formatError(err)}`
|
|
12400
12629
|
);
|
|
12401
12630
|
return null;
|
|
12402
12631
|
}
|
|
12403
|
-
|
|
12632
|
+
}
|
|
12633
|
+
async function uploadEpochBaselineArtifacts(params) {
|
|
12634
|
+
const { client, contributionId, epoch, artifacts } = params;
|
|
12635
|
+
const prefix = epochPrefix(epoch);
|
|
12404
12636
|
await uploadFile(
|
|
12405
12637
|
client,
|
|
12406
12638
|
contributionId,
|
|
12407
12639
|
`${prefix}-baseline.json`,
|
|
12408
12640
|
"application/json",
|
|
12409
|
-
metadataBuffer
|
|
12641
|
+
artifacts.metadataBuffer
|
|
12410
12642
|
);
|
|
12411
12643
|
await uploadFile(
|
|
12412
12644
|
client,
|
|
12413
12645
|
contributionId,
|
|
12414
12646
|
`${prefix}-baseline.bundle`,
|
|
12415
12647
|
"application/x-git-bundle",
|
|
12416
|
-
bundleBuffer
|
|
12648
|
+
artifacts.bundleBuffer
|
|
12417
12649
|
);
|
|
12418
|
-
|
|
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 };
|
|
12419
12661
|
}
|
|
12420
12662
|
async function openEpoch(params) {
|
|
12421
12663
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
@@ -12430,7 +12672,10 @@ async function openEpoch(params) {
|
|
|
12430
12672
|
async function initializeSession(cwd, tool, sessionId) {
|
|
12431
12673
|
const project = await findProjectForCwd(cwd);
|
|
12432
12674
|
if (!project) {
|
|
12433
|
-
appendLog(
|
|
12675
|
+
appendLog(
|
|
12676
|
+
"warn",
|
|
12677
|
+
"git-traces: no hillclimb project config found, skipping"
|
|
12678
|
+
);
|
|
12434
12679
|
return null;
|
|
12435
12680
|
}
|
|
12436
12681
|
const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
|
|
@@ -12510,6 +12755,17 @@ async function handleStop(payload, tool) {
|
|
|
12510
12755
|
identity.sessionCookie
|
|
12511
12756
|
);
|
|
12512
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;
|
|
12513
12769
|
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
12514
12770
|
const now = /* @__PURE__ */ new Date();
|
|
12515
12771
|
const shortId = state.sessionId.slice(0, 12);
|
|
@@ -12524,23 +12780,16 @@ Repo: ${cwd}
|
|
|
12524
12780
|
Uploaded: ${now.toISOString()}`
|
|
12525
12781
|
}
|
|
12526
12782
|
);
|
|
12527
|
-
await
|
|
12528
|
-
const uploaded = await uploadEpochBaseline({
|
|
12529
|
-
repoRoot: cwd,
|
|
12783
|
+
await uploadEpochBaselineArtifacts({
|
|
12530
12784
|
client,
|
|
12531
12785
|
contributionId: contribution.id,
|
|
12532
|
-
sessionId: state.sessionId,
|
|
12533
|
-
tool,
|
|
12534
12786
|
epoch: 1,
|
|
12535
|
-
|
|
12536
|
-
prevHeadSha: null,
|
|
12537
|
-
transitionKind: "initial",
|
|
12538
|
-
startedAt: state.startedAt
|
|
12787
|
+
artifacts
|
|
12539
12788
|
});
|
|
12540
|
-
|
|
12789
|
+
await client.submitContribution(contribution.id);
|
|
12541
12790
|
state.contributionId = contribution.id;
|
|
12542
|
-
state.baselineTreeSha =
|
|
12543
|
-
state.lastSnapshotTreeSha =
|
|
12791
|
+
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
12792
|
+
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
12544
12793
|
await writeSessionState(state, tool);
|
|
12545
12794
|
appendLog(
|
|
12546
12795
|
"info",
|
|
@@ -12655,6 +12904,10 @@ async function handleSessionEnd(payload, tool) {
|
|
|
12655
12904
|
// src/git-traces/index.ts
|
|
12656
12905
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
12657
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
|
+
}
|
|
12658
12911
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
12659
12912
|
"claude",
|
|
12660
12913
|
"codex",
|
|
@@ -12683,6 +12936,8 @@ async function runGitTraces() {
|
|
|
12683
12936
|
await runGitTracesWorker();
|
|
12684
12937
|
return;
|
|
12685
12938
|
}
|
|
12939
|
+
const flowId = newFlowId();
|
|
12940
|
+
setLogPrefix(`[${flowId}]`);
|
|
12686
12941
|
const toolArg = parseToolArg2(process.argv.slice(2));
|
|
12687
12942
|
appendLog(
|
|
12688
12943
|
"info",
|
|
@@ -12699,10 +12954,7 @@ async function runGitTraces() {
|
|
|
12699
12954
|
try {
|
|
12700
12955
|
raw = await readStdin2();
|
|
12701
12956
|
} catch (err) {
|
|
12702
|
-
appendLog(
|
|
12703
|
-
"error",
|
|
12704
|
-
`git-traces: failed to read stdin: ${err instanceof Error ? err.message : String(err)}`
|
|
12705
|
-
);
|
|
12957
|
+
appendLog("error", `git-traces: failed to read stdin: ${formatError(err)}`);
|
|
12706
12958
|
return;
|
|
12707
12959
|
}
|
|
12708
12960
|
if (!raw.trim()) {
|
|
@@ -12724,7 +12976,8 @@ async function runGitTraces() {
|
|
|
12724
12976
|
env: {
|
|
12725
12977
|
...process.env,
|
|
12726
12978
|
[WORKER_ENV_FLAG2]: "1",
|
|
12727
|
-
[TOOL_ENV_FLAG2]: toolArg
|
|
12979
|
+
[TOOL_ENV_FLAG2]: toolArg,
|
|
12980
|
+
[FLOW_ID_ENV]: flowId
|
|
12728
12981
|
}
|
|
12729
12982
|
}
|
|
12730
12983
|
);
|
|
@@ -12741,11 +12994,12 @@ async function runGitTraces() {
|
|
|
12741
12994
|
} catch (err) {
|
|
12742
12995
|
appendLog(
|
|
12743
12996
|
"error",
|
|
12744
|
-
`git-traces: failed to spawn worker: ${
|
|
12997
|
+
`git-traces: failed to spawn worker: ${formatError(err)}`
|
|
12745
12998
|
);
|
|
12746
12999
|
}
|
|
12747
13000
|
}
|
|
12748
13001
|
async function runGitTracesWorker() {
|
|
13002
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
|
|
12749
13003
|
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
12750
13004
|
appendLog(
|
|
12751
13005
|
"info",
|
|
@@ -12764,7 +13018,7 @@ async function runGitTracesWorker() {
|
|
|
12764
13018
|
} catch (err) {
|
|
12765
13019
|
appendLog(
|
|
12766
13020
|
"error",
|
|
12767
|
-
`git-traces worker: failed to read stdin: ${
|
|
13021
|
+
`git-traces worker: failed to read stdin: ${formatError(err)}`
|
|
12768
13022
|
);
|
|
12769
13023
|
return;
|
|
12770
13024
|
}
|
|
@@ -12778,7 +13032,7 @@ async function runGitTracesWorker() {
|
|
|
12778
13032
|
} catch (err) {
|
|
12779
13033
|
appendLog(
|
|
12780
13034
|
"error",
|
|
12781
|
-
`git-traces worker: failed to parse payload: ${
|
|
13035
|
+
`git-traces worker: failed to parse payload: ${formatError(err)}`
|
|
12782
13036
|
);
|
|
12783
13037
|
return;
|
|
12784
13038
|
}
|
|
@@ -12817,9 +13071,12 @@ async function runGitTracesWorker() {
|
|
|
12817
13071
|
appendLog("warn", `git-traces worker: unknown event ${event}`);
|
|
12818
13072
|
}
|
|
12819
13073
|
} catch (err) {
|
|
13074
|
+
const detail = formatError(err);
|
|
13075
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
12820
13076
|
appendLog(
|
|
12821
13077
|
"error",
|
|
12822
|
-
`git-traces worker: unexpected error: ${
|
|
13078
|
+
`git-traces worker: unexpected error: ${detail}${stack ? `
|
|
13079
|
+
${stack}` : ""}`
|
|
12823
13080
|
);
|
|
12824
13081
|
}
|
|
12825
13082
|
}
|
|
@@ -13548,11 +13805,14 @@ async function main() {
|
|
|
13548
13805
|
const [, , subcommand, ...rest] = process.argv;
|
|
13549
13806
|
switch (subcommand) {
|
|
13550
13807
|
case void 0:
|
|
13551
|
-
await
|
|
13808
|
+
await runInit([]);
|
|
13552
13809
|
return;
|
|
13553
13810
|
case "init":
|
|
13554
13811
|
await runInit(rest);
|
|
13555
13812
|
return;
|
|
13813
|
+
case "export":
|
|
13814
|
+
await runInteractive();
|
|
13815
|
+
return;
|
|
13556
13816
|
case "login":
|
|
13557
13817
|
await runLogin(rest);
|
|
13558
13818
|
return;
|
|
@@ -13572,11 +13832,15 @@ async function main() {
|
|
|
13572
13832
|
case "-h":
|
|
13573
13833
|
case "help": {
|
|
13574
13834
|
process.stdout.write(
|
|
13575
|
-
"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
|
|
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"
|
|
13576
13836
|
);
|
|
13577
13837
|
return;
|
|
13578
13838
|
}
|
|
13579
13839
|
default:
|
|
13840
|
+
if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
|
|
13841
|
+
await runInit([subcommand, ...rest]);
|
|
13842
|
+
return;
|
|
13843
|
+
}
|
|
13580
13844
|
process.stderr.write(
|
|
13581
13845
|
`Unknown subcommand: ${subcommand}${rest.length > 0 ? ` ${rest.join(" ")}` : ""}
|
|
13582
13846
|
`
|