@bitfab/sdk 0.30.0 → 0.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -320,6 +320,203 @@ var init_replayContext = __esm({
320
320
  }
321
321
  });
322
322
 
323
+ // src/codeChange.ts
324
+ async function resolveAutoCodeChange(label) {
325
+ if (typeof process === "undefined") {
326
+ return null;
327
+ }
328
+ if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
329
+ return null;
330
+ }
331
+ const fromEnv = await readCodeChangeFile();
332
+ if (fromEnv) {
333
+ return fromEnv;
334
+ }
335
+ return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
336
+ }
337
+ async function readCodeChangeFile() {
338
+ const path = process.env?.BITFAB_CODE_CHANGE_PATH;
339
+ if (!path) {
340
+ return null;
341
+ }
342
+ try {
343
+ const { readFile } = await import("fs/promises");
344
+ const parsed = JSON.parse(await readFile(path, "utf8"));
345
+ const files = Array.isArray(parsed?.files) && parsed.files.every(
346
+ (f) => typeof f === "object" && f !== null && !Array.isArray(f)
347
+ ) ? parsed.files : void 0;
348
+ const description = typeof parsed?.description === "string" ? parsed.description : void 0;
349
+ if (!files && description === void 0) {
350
+ return null;
351
+ }
352
+ return { description, files };
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+ async function captureCodeChangeFromGit(cwd, label) {
358
+ let execFile;
359
+ let readFile;
360
+ try {
361
+ ;
362
+ ({ execFile } = await import("child_process"));
363
+ ({ readFile } = await import("fs/promises"));
364
+ } catch {
365
+ return null;
366
+ }
367
+ const git = (dir, args) => new Promise((resolve) => {
368
+ execFile(
369
+ "git",
370
+ args,
371
+ // 30s timeout so a hung git (e.g. a network-touching ref op) can't
372
+ // block the whole replay indefinitely.
373
+ { cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
374
+ (err, stdout) => resolve(err ? null : stdout)
375
+ );
376
+ });
377
+ try {
378
+ const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
379
+ if (!root) {
380
+ return null;
381
+ }
382
+ const resolved = await resolveBase(git, root);
383
+ if (!resolved) {
384
+ return null;
385
+ }
386
+ const { base, fromTrunk } = resolved;
387
+ const blobBytes = async (ref, path) => {
388
+ const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
389
+ const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
390
+ return Number.isFinite(n) ? n : 0;
391
+ };
392
+ const workingBytes = async (path) => {
393
+ try {
394
+ const { stat } = await import("fs/promises");
395
+ const { join } = await import("path");
396
+ return (await stat(join(root, path))).size;
397
+ } catch {
398
+ return 0;
399
+ }
400
+ };
401
+ const tracked = await git(root, [
402
+ "diff",
403
+ "--name-status",
404
+ "--no-renames",
405
+ "-z",
406
+ base,
407
+ "--",
408
+ ":!.bitfab"
409
+ ]);
410
+ const untracked = await git(root, [
411
+ "ls-files",
412
+ "--others",
413
+ "--exclude-standard",
414
+ "-z",
415
+ "--",
416
+ ":!.bitfab"
417
+ ]);
418
+ const entries = [
419
+ ...parseNameStatusZ(tracked ?? ""),
420
+ ...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
421
+ ];
422
+ if (entries.length === 0) {
423
+ return null;
424
+ }
425
+ const files = [];
426
+ let totalBytes = 0;
427
+ for (const { status, path } of entries) {
428
+ if (files.length >= MAX_FILES) {
429
+ break;
430
+ }
431
+ const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
432
+ const afterBytes = status === "D" ? 0 : await workingBytes(path);
433
+ if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
434
+ continue;
435
+ }
436
+ const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
437
+ const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
438
+ if (before === after) {
439
+ continue;
440
+ }
441
+ const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
442
+ if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
443
+ continue;
444
+ }
445
+ totalBytes += size;
446
+ files.push({ path, before, after });
447
+ }
448
+ if (files.length === 0) {
449
+ return null;
450
+ }
451
+ const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
452
+ const fileWord = files.length === 1 ? "file" : "files";
453
+ const head = label?.trim() || subject || "Working-tree change";
454
+ const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
455
+ return {
456
+ description: `${head} (${files.length} ${fileWord} changed ${against})`,
457
+ files
458
+ };
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ async function resolveBase(git, root) {
464
+ const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
465
+ if (forced && await refExists(git, root, forced)) {
466
+ const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
467
+ return base ? { base, fromTrunk: true } : null;
468
+ }
469
+ for (const candidate of TRUNK_CANDIDATES) {
470
+ if (!await refExists(git, root, candidate)) {
471
+ continue;
472
+ }
473
+ const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
474
+ if (mb) {
475
+ return { base: mb, fromTrunk: true };
476
+ }
477
+ }
478
+ return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
479
+ }
480
+ async function refExists(git, root, ref) {
481
+ return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
482
+ }
483
+ async function readWorkingFile(readFile, root, path) {
484
+ try {
485
+ const { join } = await import("path");
486
+ return await readFile(join(root, path), "utf8");
487
+ } catch {
488
+ return "";
489
+ }
490
+ }
491
+ function parseNameStatusZ(raw) {
492
+ const parts = raw.split(NUL).filter((p) => p.length > 0);
493
+ const out = [];
494
+ for (let i = 0; i + 1 < parts.length; i += 2) {
495
+ out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
496
+ }
497
+ return out;
498
+ }
499
+ function looksBinary(s) {
500
+ return s.slice(0, 8e3).includes(NUL);
501
+ }
502
+ var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
503
+ var init_codeChange = __esm({
504
+ "src/codeChange.ts"() {
505
+ "use strict";
506
+ MAX_FILES = 60;
507
+ MAX_FILE_BYTES = 5e5;
508
+ MAX_TOTAL_BYTES = 2e6;
509
+ TRUNK_CANDIDATES = [
510
+ "origin/HEAD",
511
+ "origin/main",
512
+ "origin/master",
513
+ "main",
514
+ "master"
515
+ ];
516
+ NUL = String.fromCharCode(0);
517
+ }
518
+ });
519
+
323
520
  // src/replay.ts
324
521
  var replay_exports = {};
325
522
  __export(replay_exports, {
@@ -544,6 +741,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
544
741
  }
545
742
  }
546
743
  await replayContextReady;
744
+ let codeChangeDescription = options?.codeChangeDescription;
745
+ let codeChangeFiles = options?.codeChangeFiles;
746
+ if (codeChangeDescription === void 0 && codeChangeFiles === void 0) {
747
+ const captured = await resolveAutoCodeChange(options?.name);
748
+ if (captured) {
749
+ codeChangeDescription = captured.description;
750
+ codeChangeFiles = captured.files;
751
+ }
752
+ }
547
753
  const {
548
754
  testRunId,
549
755
  testRunUrl,
@@ -555,8 +761,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
555
761
  options?.traceIds ? void 0 : options?.limit ?? 5,
556
762
  options?.traceIds,
557
763
  options?.name,
558
- options?.codeChangeDescription,
559
- options?.codeChangeFiles,
764
+ codeChangeDescription,
765
+ codeChangeFiles,
560
766
  options?.environment !== void 0,
561
767
  // includeDbBranchLease
562
768
  options?.experimentGroupId,
@@ -590,7 +796,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
590
796
  const resultItems = await mapWithConcurrency(
591
797
  tasks,
592
798
  maxConcurrency,
593
- options?.onProgress ? (item, index) => {
799
+ options?.onProgress ? (item) => {
594
800
  completed += 1;
595
801
  if (item.error === null) {
596
802
  succeeded += 1;
@@ -712,6 +918,7 @@ var BITFAB_PROGRESS_PREFIX;
712
918
  var init_replay = __esm({
713
919
  "src/replay.ts"() {
714
920
  "use strict";
921
+ init_codeChange();
715
922
  init_errors();
716
923
  init_mockOverride();
717
924
  init_randomUuid();
@@ -747,7 +954,7 @@ __export(index_exports, {
747
954
  module.exports = __toCommonJS(index_exports);
748
955
 
749
956
  // src/version.generated.ts
750
- var __version__ = "0.30.0";
957
+ var __version__ = "0.30.2";
751
958
 
752
959
  // src/constants.ts
753
960
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";