@ai-development-environment/control-agent 0.0.29 → 0.0.30

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 (2) hide show
  1. package/dist/control-agent.js +1170 -42
  2. package/package.json +1 -1
@@ -1506,6 +1506,8 @@ var WORKTREE_BRANCH_JOB_KIND = "worktree.branch";
1506
1506
  var WORKTREE_MOVE_PUSH_JOB_KIND = "worktree.move.push";
1507
1507
  var WORKTREE_MOVE_CHECKOUT_JOB_KIND = "worktree.move.checkout";
1508
1508
  var WORKTREE_DELETE_JOB_KIND = "worktree.delete";
1509
+ var WORKTREE_DIFF_JOB_KIND = "worktree.diff.inspect";
1510
+ var WORKTREE_DIFF_ASSET_JOB_KIND = "worktree.diff.asset";
1509
1511
  var WORKTREE_JOB_KINDS = [
1510
1512
  WORKTREE_INSPECT_JOB_KIND,
1511
1513
  WORKTREE_OPERATION_JOB_KIND,
@@ -1513,7 +1515,9 @@ var WORKTREE_JOB_KINDS = [
1513
1515
  WORKTREE_BRANCH_JOB_KIND,
1514
1516
  WORKTREE_MOVE_PUSH_JOB_KIND,
1515
1517
  WORKTREE_MOVE_CHECKOUT_JOB_KIND,
1516
- WORKTREE_DELETE_JOB_KIND
1518
+ WORKTREE_DELETE_JOB_KIND,
1519
+ WORKTREE_DIFF_JOB_KIND,
1520
+ WORKTREE_DIFF_ASSET_JOB_KIND
1517
1521
  ];
1518
1522
  var DEFAULT_JIRA_BRANCH_REGEX = String.raw`\b([A-Z][A-Z0-9_]*-\d+)\b`;
1519
1523
  var WORKTREE_OPERATIONS = [
@@ -1534,6 +1538,13 @@ function validGitBranchName(value) {
1534
1538
  }
1535
1539
  return value.split("/").every((part) => part && !part.startsWith(".") && !part.endsWith(".lock"));
1536
1540
  }
1541
+ var WORKTREE_DIFF_SCOPES = [
1542
+ "STAGED",
1543
+ "UNSTAGED",
1544
+ "UNTRACKED",
1545
+ "COMMIT",
1546
+ "BRANCH"
1547
+ ];
1537
1548
  function objectValue4(value, name) {
1538
1549
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1539
1550
  throw new Error(`${name} must be an object`);
@@ -1550,6 +1561,13 @@ function nullableString3(value, name) {
1550
1561
  if (value === null || value === void 0) return null;
1551
1562
  return stringValue4(value, name);
1552
1563
  }
1564
+ function safeRelativePath(value, name) {
1565
+ const path = stringValue4(value, name).replaceAll("\\", "/");
1566
+ if (path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.split("/").includes("..") || path.includes("\0")) {
1567
+ throw new Error(`${name} must stay within the worktree`);
1568
+ }
1569
+ return path;
1570
+ }
1553
1571
  function worktreeBranchJobPayload(value) {
1554
1572
  const payload = objectValue4(value, "worktree branch payload");
1555
1573
  const allowed = /* @__PURE__ */ new Set([
@@ -1858,6 +1876,73 @@ function worktreeJobPayload(value) {
1858
1876
  ...editorVariant === void 0 ? {} : { editorVariant }
1859
1877
  };
1860
1878
  }
1879
+ function worktreeDiffPayload(value) {
1880
+ const payload = objectValue4(value, "worktree diff payload");
1881
+ const allowed = /* @__PURE__ */ new Set([
1882
+ "codebaseId",
1883
+ "folder",
1884
+ "gitDirectory",
1885
+ "expectedOrigin",
1886
+ "baseBranch",
1887
+ "scope",
1888
+ "path",
1889
+ "previousPath",
1890
+ "commitSha",
1891
+ "uploadId",
1892
+ "side"
1893
+ ]);
1894
+ const unexpected = Object.keys(payload).find((key) => !allowed.has(key));
1895
+ if (unexpected) {
1896
+ throw new Error(`Unexpected worktree diff payload field: ${unexpected}`);
1897
+ }
1898
+ if (!WORKTREE_DIFF_SCOPES.includes(payload.scope)) {
1899
+ throw new Error("worktree diff payload.scope is invalid");
1900
+ }
1901
+ if (payload.side !== null && payload.side !== void 0 && payload.side !== "BEFORE" && payload.side !== "AFTER") {
1902
+ throw new Error("worktree diff payload.side is invalid");
1903
+ }
1904
+ const path = payload.path === null || payload.path === void 0 ? null : safeRelativePath(payload.path, "worktree diff payload.path");
1905
+ const previousPath = payload.previousPath === null || payload.previousPath === void 0 ? null : safeRelativePath(
1906
+ payload.previousPath,
1907
+ "worktree diff payload.previousPath"
1908
+ );
1909
+ const scope = payload.scope;
1910
+ const commitSha = nullableString3(
1911
+ payload.commitSha,
1912
+ "worktree diff payload.commitSha"
1913
+ );
1914
+ if (scope === "COMMIT" && !commitSha) {
1915
+ throw new Error("Commit diffs require a commit SHA");
1916
+ }
1917
+ return {
1918
+ codebaseId: stringValue4(
1919
+ payload.codebaseId,
1920
+ "worktree diff payload.codebaseId"
1921
+ ),
1922
+ folder: stringValue4(payload.folder, "worktree diff payload.folder"),
1923
+ gitDirectory: stringValue4(
1924
+ payload.gitDirectory,
1925
+ "worktree diff payload.gitDirectory"
1926
+ ),
1927
+ expectedOrigin: stringValue4(
1928
+ payload.expectedOrigin,
1929
+ "worktree diff payload.expectedOrigin"
1930
+ ),
1931
+ baseBranch: stringValue4(
1932
+ payload.baseBranch,
1933
+ "worktree diff payload.baseBranch"
1934
+ ),
1935
+ scope,
1936
+ path,
1937
+ previousPath,
1938
+ commitSha,
1939
+ uploadId: nullableString3(
1940
+ payload.uploadId,
1941
+ "worktree diff payload.uploadId"
1942
+ ),
1943
+ side: payload.side ?? null
1944
+ };
1945
+ }
1861
1946
  function worktreeWatchJobPayload(value) {
1862
1947
  const payload = objectValue4(value, "worktree watch payload");
1863
1948
  const allowed = /* @__PURE__ */ new Set([
@@ -2201,6 +2286,8 @@ var IOS_BUILD_DELETE_JOB_KIND = "ios.build.delete";
2201
2286
  var IOS_ARTIFACT_DOWNLOAD_JOB_KIND = "ios.artifact.download";
2202
2287
  var IOS_DEPLOY_JOB_KIND = "ios.build.deploy";
2203
2288
  var IOS_EXPORT_JOB_KIND = "ios.archive.export";
2289
+ var IOS_TEST_RESULTS_JOB_KIND = "ios.test-results.parse";
2290
+ var IOS_COVERAGE_REPORT_JOB_KIND = "ios.coverage.generate";
2204
2291
  var IOS_BUILD_JOB_KINDS = [
2205
2292
  IOS_SOURCE_DISCOVER_JOB_KIND,
2206
2293
  IOS_SOURCE_PARSE_JOB_KIND,
@@ -2210,7 +2297,9 @@ var IOS_BUILD_JOB_KINDS = [
2210
2297
  IOS_BUILD_DELETE_JOB_KIND,
2211
2298
  IOS_ARTIFACT_DOWNLOAD_JOB_KIND,
2212
2299
  IOS_DEPLOY_JOB_KIND,
2213
- IOS_EXPORT_JOB_KIND
2300
+ IOS_EXPORT_JOB_KIND,
2301
+ IOS_TEST_RESULTS_JOB_KIND,
2302
+ IOS_COVERAGE_REPORT_JOB_KIND
2214
2303
  ];
2215
2304
  var BUILD_ACTIONS = [
2216
2305
  "BUILD",
@@ -2259,6 +2348,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2259
2348
  allowProvisioningDeviceRegistration: false,
2260
2349
  testPlan: null,
2261
2350
  codeCoverage: false,
2351
+ parseTestResults: true,
2262
2352
  parallelTesting: null,
2263
2353
  parallelTestingWorkers: null,
2264
2354
  onlyTesting: [],
@@ -2268,6 +2358,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2268
2358
  priorTestProductsPath: null,
2269
2359
  priorXctestrunPath: null
2270
2360
  };
2361
+ var BUILD_REPORT_KINDS = ["TEST_RESULTS", "CODE_COVERAGE"];
2271
2362
  function objectValue6(value, name) {
2272
2363
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2273
2364
  throw new Error(`${name} must be an object`);
@@ -2293,7 +2384,7 @@ function enumValue3(value, values, name) {
2293
2384
  }
2294
2385
  return value;
2295
2386
  }
2296
- function safeRelativePath(value, name) {
2387
+ function safeRelativePath2(value, name) {
2297
2388
  const path = stringValue6(value, name);
2298
2389
  if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.split(/[\\/]/).includes("..") || path.includes("\0")) {
2299
2390
  throw new Error(`${name} must stay within the worktree`);
@@ -2318,13 +2409,14 @@ function worktreeIdentity(value) {
2318
2409
  value.expectedOrigin,
2319
2410
  "build payload.expectedOrigin"
2320
2411
  ),
2321
- headSha: nullableString4(value.headSha, "build payload.headSha")
2412
+ headSha: nullableString4(value.headSha, "build payload.headSha"),
2413
+ baseBranch: nullableString4(value.baseBranch, "build payload.baseBranch")
2322
2414
  };
2323
2415
  }
2324
2416
  function parseBuildSource(value) {
2325
2417
  const source = objectValue6(value, "build source");
2326
2418
  const kind = enumValue3(source.kind, BUILD_SOURCE_KINDS, "build source.kind");
2327
- const relativePath = safeRelativePath(
2419
+ const relativePath = safeRelativePath2(
2328
2420
  source.relativePath,
2329
2421
  "build source.relativePath"
2330
2422
  );
@@ -2443,6 +2535,10 @@ function parseBuildAdvancedSettings(value) {
2443
2535
  input.codeCoverage,
2444
2536
  "advanced settings.codeCoverage"
2445
2537
  ),
2538
+ parseTestResults: booleanValue2(
2539
+ input.parseTestResults,
2540
+ "advanced settings.parseTestResults"
2541
+ ),
2446
2542
  parallelTesting: parallel,
2447
2543
  parallelTestingWorkers: workers ?? null,
2448
2544
  onlyTesting: stringArray2(
@@ -2580,7 +2676,27 @@ function parseBuildJobPayload(value) {
2580
2676
  action,
2581
2677
  destination,
2582
2678
  advancedSettings,
2583
- scripts
2679
+ scripts,
2680
+ worktreeCoverage: input.worktreeCoverage === void 0 ? false : booleanValue2(
2681
+ input.worktreeCoverage,
2682
+ "build job payload.worktreeCoverage"
2683
+ )
2684
+ };
2685
+ }
2686
+ function parseBuildReportPayload(value) {
2687
+ const input = objectValue6(value, "build report payload");
2688
+ return {
2689
+ ...parseBuildDeletePayload(input),
2690
+ reportKind: enumValue3(
2691
+ input.reportKind,
2692
+ BUILD_REPORT_KINDS,
2693
+ "build report payload.reportKind"
2694
+ ),
2695
+ source: enumValue3(
2696
+ input.source,
2697
+ ["MANUAL", "WORKTREE"],
2698
+ "build report payload.source"
2699
+ )
2584
2700
  };
2585
2701
  }
2586
2702
  function parseBuildDeletePayload(value) {
@@ -2607,7 +2723,7 @@ function parseBuildArtifactDownloadPayload(value) {
2607
2723
  const identity = parseBuildDeletePayload(input);
2608
2724
  return {
2609
2725
  ...identity,
2610
- artifactRelativePath: safeRelativePath(
2726
+ artifactRelativePath: safeRelativePath2(
2611
2727
  input.artifactRelativePath,
2612
2728
  "build artifact download payload.artifactRelativePath"
2613
2729
  ),
@@ -2649,7 +2765,7 @@ function parseBuildDeploymentPayload(value) {
2649
2765
  input.artifactDirectory,
2650
2766
  "build deployment payload.artifactDirectory"
2651
2767
  ),
2652
- artifactRelativePath: safeRelativePath(
2768
+ artifactRelativePath: safeRelativePath2(
2653
2769
  input.artifactRelativePath,
2654
2770
  "build deployment payload.artifactRelativePath"
2655
2771
  ),
@@ -2717,7 +2833,7 @@ function parseBuildExportPayload(value) {
2717
2833
  input.artifactDirectory,
2718
2834
  "build export payload.artifactDirectory"
2719
2835
  ),
2720
- archiveRelativePath: safeRelativePath(
2836
+ archiveRelativePath: safeRelativePath2(
2721
2837
  input.archiveRelativePath,
2722
2838
  "build export payload.archiveRelativePath"
2723
2839
  ),
@@ -2925,7 +3041,7 @@ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } fro
2925
3041
 
2926
3042
  // src/capture-command.ts
2927
3043
  import { spawn as spawn2 } from "node:child_process";
2928
- var MAX_OUTPUT_BYTES = 1024 * 1024;
3044
+ var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
2929
3045
  function captureCommand(options) {
2930
3046
  return new Promise((resolve6, reject) => {
2931
3047
  let stdout = "";
@@ -2935,15 +3051,19 @@ function captureCommand(options) {
2935
3051
  let settled = false;
2936
3052
  const child = spawn2(options.command, options.args, {
2937
3053
  shell: false,
2938
- stdio: ["ignore", "pipe", "pipe"],
3054
+ stdio: [
3055
+ "ignore",
3056
+ options.stdoutFileDescriptor === void 0 ? "pipe" : options.stdoutFileDescriptor,
3057
+ "pipe"
3058
+ ],
2939
3059
  env: options.env ?? process.env,
2940
3060
  cwd: options.cwd
2941
3061
  });
2942
3062
  const append = (current, chunk) => `${current}${String(chunk)}`.slice(0, MAX_OUTPUT_BYTES);
2943
- child.stdout.on("data", (chunk) => {
3063
+ child.stdout?.on("data", (chunk) => {
2944
3064
  stdout = append(stdout, chunk);
2945
3065
  });
2946
- child.stderr.on("data", (chunk) => {
3066
+ child.stderr?.on("data", (chunk) => {
2947
3067
  stderr = append(stderr, chunk);
2948
3068
  });
2949
3069
  const terminate = () => {
@@ -2997,7 +3117,7 @@ var successfulProcess = {
2997
3117
  function errorMessage(error) {
2998
3118
  return error instanceof Error ? error.message : String(error);
2999
3119
  }
3000
- function safeRelativePath2(value) {
3120
+ function safeRelativePath3(value) {
3001
3121
  if (!value || value.includes("\0") || isAbsolute(value)) return false;
3002
3122
  const normalized = value.replaceAll("\\", "/");
3003
3123
  return normalized !== "." && normalized.split("/").every((part) => part && part !== ".." && part !== ".");
@@ -3012,7 +3132,7 @@ async function configuredRoots(payload) {
3012
3132
  }
3013
3133
  return [resolve(payload.path)];
3014
3134
  }
3015
- if (!payload.path || !safeRelativePath2(payload.path)) {
3135
+ if (!payload.path || !safeRelativePath3(payload.path)) {
3016
3136
  throw new Error("Relative Derived Data mode requires a safe relative path");
3017
3137
  }
3018
3138
  return payload.worktrees.map(
@@ -4430,9 +4550,11 @@ var applySkills = async (payload) => {
4430
4550
  };
4431
4551
 
4432
4552
  // src/handlers/worktrees.ts
4433
- import { watch } from "node:fs";
4434
- import { readFile as readFile3, realpath as realpath4, stat as stat4 } from "node:fs/promises";
4553
+ import { createWriteStream, watch } from "node:fs";
4554
+ import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat4 } from "node:fs/promises";
4555
+ import { tmpdir } from "node:os";
4435
4556
  import { basename as basename2, dirname as dirname5, join as join5, relative as relative3 } from "node:path";
4557
+ import { spawn as spawn4 } from "node:child_process";
4436
4558
 
4437
4559
  // src/git-code-state.ts
4438
4560
  import { createHash as createHash2 } from "node:crypto";
@@ -4633,6 +4755,16 @@ var successfulProcess4 = {
4633
4755
  cancelled: false
4634
4756
  };
4635
4757
  var WATCH_DEBOUNCE_MS = 500;
4758
+ var MAX_DIFF_BYTES = 2 * 1024 * 1024;
4759
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
4760
+ ".png",
4761
+ ".jpg",
4762
+ ".jpeg",
4763
+ ".gif",
4764
+ ".webp",
4765
+ ".avif",
4766
+ ".bmp"
4767
+ ]);
4636
4768
  var activeWorktreeWatches = /* @__PURE__ */ new Map();
4637
4769
  function statusChangeState(value) {
4638
4770
  const entries = value.split("\0").filter(Boolean);
@@ -5088,7 +5220,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
5088
5220
  const value = values[index];
5089
5221
  const code = value.slice(0, 2);
5090
5222
  const path = value.slice(3);
5091
- if ((code[0] === "R" || code[0] === "C") && values[index + 1]) index += 1;
5223
+ const previousPath = (code[0] === "R" || code[0] === "C") && values[index + 1] ? values[index + 1] : null;
5224
+ if (previousPath) index += 1;
5092
5225
  const untracked = code === "??";
5093
5226
  const conflicted = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(
5094
5227
  code
@@ -5100,6 +5233,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
5100
5233
  const untrackedCount = untracked ? await untrackedLines(folder, path) : null;
5101
5234
  changes.push({
5102
5235
  path,
5236
+ previousPath,
5237
+ changeType: untracked ? "ADDED" : conflicted ? "CONFLICTED" : code,
5103
5238
  staged,
5104
5239
  unstaged,
5105
5240
  untracked,
@@ -5112,6 +5247,65 @@ async function inspectChanges(folder, timeoutMs, signal) {
5112
5247
  }
5113
5248
  return { changes: changes.slice(0, 500), truncated: changes.length > 500 };
5114
5249
  }
5250
+ function imagePath(path) {
5251
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
5252
+ return IMAGE_EXTENSIONS.has(extension);
5253
+ }
5254
+ function parseNameStatus(value) {
5255
+ const entries = value.split("\0").filter(Boolean);
5256
+ const files = [];
5257
+ for (let index = 0; index < entries.length; ) {
5258
+ const status2 = entries[index++] ?? "M";
5259
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
5260
+ const first = entries[index++] ?? "";
5261
+ const second = renamed ? entries[index++] ?? "" : null;
5262
+ const path = second ?? first;
5263
+ if (!path) continue;
5264
+ files.push({
5265
+ path,
5266
+ previousPath: renamed ? first : null,
5267
+ changeType: status2[0] ?? "M"
5268
+ });
5269
+ }
5270
+ return files;
5271
+ }
5272
+ async function diffFiles(folder, range, timeoutMs, signal) {
5273
+ const [status2, counts2] = await Promise.all([
5274
+ git2(folder, ["diff", "--name-status", "-z", range], timeoutMs, signal),
5275
+ git2(folder, ["diff", "--numstat", "-z", range], timeoutMs, signal)
5276
+ ]);
5277
+ requireSuccess2(status2, "Could not inspect changed files");
5278
+ requireSuccess2(counts2, "Could not inspect changed line counts");
5279
+ const numstat = parseNumstat(counts2.stdout);
5280
+ return parseNameStatus(status2.stdout).map((file) => {
5281
+ const [additions, deletions] = numstat.get(file.path) ?? [null, null];
5282
+ return {
5283
+ ...file,
5284
+ additions,
5285
+ deletions,
5286
+ binary: additions === null && deletions === null,
5287
+ image: imagePath(file.path)
5288
+ };
5289
+ });
5290
+ }
5291
+ async function inspectBranchChanges(folder, baseBranch, timeoutMs, signal) {
5292
+ const mergeBase = requireSuccess2(
5293
+ await git2(
5294
+ folder,
5295
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5296
+ timeoutMs,
5297
+ signal
5298
+ ),
5299
+ "Could not determine the base-branch merge base"
5300
+ ).stdout.trim();
5301
+ const files = await diffFiles(
5302
+ folder,
5303
+ `${mergeBase}..HEAD`,
5304
+ timeoutMs,
5305
+ signal
5306
+ );
5307
+ return { changes: files.slice(0, 500), truncated: files.length > 500 };
5308
+ }
5115
5309
  async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
5116
5310
  const result = requireSuccess2(
5117
5311
  await git2(
@@ -5143,17 +5337,441 @@ async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
5143
5337
  return { commits: commits.slice(0, 100), truncated: commits.length > 100 };
5144
5338
  }
5145
5339
  async function inspectWorktreeDetail(folder, baseBranch, timeoutMs, signal) {
5146
- const [commitResult, changeResult] = await Promise.all([
5340
+ const [commitResult, changeResult, branchResult] = await Promise.all([
5147
5341
  inspectCommits(folder, baseBranch, timeoutMs, signal),
5148
- inspectChanges(folder, timeoutMs, signal)
5342
+ inspectChanges(folder, timeoutMs, signal),
5343
+ inspectBranchChanges(folder, baseBranch, timeoutMs, signal)
5149
5344
  ]);
5150
5345
  return {
5151
5346
  commits: commitResult.commits,
5152
5347
  changes: changeResult.changes,
5348
+ branchChanges: branchResult.changes,
5153
5349
  commitsTruncated: commitResult.truncated,
5154
- changesTruncated: changeResult.truncated
5350
+ changesTruncated: changeResult.truncated,
5351
+ branchChangesTruncated: branchResult.truncated
5155
5352
  };
5156
5353
  }
5354
+ async function baseMergeCommit(folder, baseBranch, timeoutMs, signal) {
5355
+ return requireSuccess2(
5356
+ await git2(
5357
+ folder,
5358
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5359
+ timeoutMs,
5360
+ signal
5361
+ ),
5362
+ "Could not determine the base-branch merge base"
5363
+ ).stdout.trim();
5364
+ }
5365
+ async function validateDisplayedCommit(folder, baseBranch, commitSha, timeoutMs, signal) {
5366
+ if (!/^[0-9a-f]{7,64}$/i.test(commitSha)) {
5367
+ throw new Error("Commit SHA is invalid");
5368
+ }
5369
+ const base = await baseMergeCommit(folder, baseBranch, timeoutMs, signal);
5370
+ const [afterBase, beforeHead] = await Promise.all([
5371
+ git2(
5372
+ folder,
5373
+ ["merge-base", "--is-ancestor", base, commitSha],
5374
+ timeoutMs,
5375
+ signal
5376
+ ),
5377
+ git2(
5378
+ folder,
5379
+ ["merge-base", "--is-ancestor", commitSha, "HEAD"],
5380
+ timeoutMs,
5381
+ signal
5382
+ )
5383
+ ]);
5384
+ if (afterBase.exitCode !== 0 || beforeHead.exitCode !== 0) {
5385
+ throw new Error("Commit is outside the displayed branch history");
5386
+ }
5387
+ }
5388
+ async function parentCommit(folder, commitSha, timeoutMs, signal) {
5389
+ const result = await git2(
5390
+ folder,
5391
+ ["rev-parse", `${commitSha}^`],
5392
+ timeoutMs,
5393
+ signal
5394
+ );
5395
+ return result.exitCode === 0 ? result.stdout.trim() : null;
5396
+ }
5397
+ async function gitObjectExists(folder, specification, timeoutMs, signal) {
5398
+ return (await git2(folder, ["cat-file", "-e", specification], timeoutMs, signal)).exitCode === 0;
5399
+ }
5400
+ async function comparisonSides(input, folder, timeoutMs, signal) {
5401
+ if (!input.path) return { before: null, after: null };
5402
+ const path = input.path;
5403
+ const previousPath = input.previousPath ?? path;
5404
+ const currentPath = join5(folder, path);
5405
+ if (input.scope === "UNTRACKED") {
5406
+ return { before: null, after: { kind: "FILE", path: currentPath } };
5407
+ }
5408
+ if (input.scope === "STAGED") {
5409
+ return {
5410
+ before: { kind: "GIT", specification: `HEAD:${previousPath}` },
5411
+ after: { kind: "GIT", specification: `:${path}` }
5412
+ };
5413
+ }
5414
+ if (input.scope === "UNSTAGED") {
5415
+ return {
5416
+ before: { kind: "GIT", specification: `:${path}` },
5417
+ after: { kind: "FILE", path: currentPath }
5418
+ };
5419
+ }
5420
+ if (input.scope === "COMMIT") {
5421
+ const commitSha = input.commitSha;
5422
+ await validateDisplayedCommit(
5423
+ folder,
5424
+ input.baseBranch,
5425
+ commitSha,
5426
+ timeoutMs,
5427
+ signal
5428
+ );
5429
+ const parent = await parentCommit(folder, commitSha, timeoutMs, signal);
5430
+ return {
5431
+ before: parent ? { kind: "GIT", specification: `${parent}:${previousPath}` } : null,
5432
+ after: { kind: "GIT", specification: `${commitSha}:${path}` }
5433
+ };
5434
+ }
5435
+ const base = await baseMergeCommit(
5436
+ folder,
5437
+ input.baseBranch,
5438
+ timeoutMs,
5439
+ signal
5440
+ );
5441
+ return {
5442
+ before: { kind: "GIT", specification: `${base}:${previousPath}` },
5443
+ after: { kind: "GIT", specification: `HEAD:${path}` }
5444
+ };
5445
+ }
5446
+ function requestedDiffPaths(input) {
5447
+ if (!input.path) return [];
5448
+ if (input.previousPath && ["STAGED", "COMMIT", "BRANCH"].includes(input.scope)) {
5449
+ return [input.previousPath, input.path];
5450
+ }
5451
+ return [input.path];
5452
+ }
5453
+ async function readBoundedFile(path, limit) {
5454
+ const file = await open(path, "r");
5455
+ try {
5456
+ const metadata = await file.stat();
5457
+ if (!metadata.isFile()) throw new Error("Changed path is not a file");
5458
+ if (metadata.size > limit) {
5459
+ return { contents: Buffer.alloc(0), oversized: true };
5460
+ }
5461
+ const buffer = Buffer.allocUnsafe(limit + 1);
5462
+ let length = 0;
5463
+ while (length < buffer.length) {
5464
+ const { bytesRead } = await file.read(
5465
+ buffer,
5466
+ length,
5467
+ buffer.length - length,
5468
+ length
5469
+ );
5470
+ if (bytesRead === 0) break;
5471
+ length += bytesRead;
5472
+ }
5473
+ return {
5474
+ contents: buffer.subarray(0, Math.min(length, limit)),
5475
+ oversized: length > limit
5476
+ };
5477
+ } finally {
5478
+ await file.close();
5479
+ }
5480
+ }
5481
+ async function availableSide(side, folder, timeoutMs, signal) {
5482
+ if (!side) return false;
5483
+ if (side.kind === "GIT") {
5484
+ return gitObjectExists(folder, side.specification, timeoutMs, signal);
5485
+ }
5486
+ try {
5487
+ const resolved = await realpath4(side.path);
5488
+ const difference = relative3(folder, resolved);
5489
+ return difference !== ".." && !difference.startsWith("../") && (await stat4(resolved)).isFile();
5490
+ } catch {
5491
+ return false;
5492
+ }
5493
+ }
5494
+ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
5495
+ if (!input.path) {
5496
+ if (input.scope === "COMMIT") {
5497
+ await validateDisplayedCommit(
5498
+ folder,
5499
+ input.baseBranch,
5500
+ input.commitSha,
5501
+ timeoutMs,
5502
+ signal
5503
+ );
5504
+ const parent = await parentCommit(
5505
+ folder,
5506
+ input.commitSha,
5507
+ timeoutMs,
5508
+ signal
5509
+ );
5510
+ const files = await diffFiles(
5511
+ folder,
5512
+ parent ? `${parent}..${input.commitSha}` : input.commitSha,
5513
+ timeoutMs,
5514
+ signal
5515
+ );
5516
+ return { files: files.slice(0, 500), truncated: files.length > 500 };
5517
+ }
5518
+ if (input.scope === "BRANCH") {
5519
+ const result = await inspectBranchChanges(
5520
+ folder,
5521
+ input.baseBranch,
5522
+ timeoutMs,
5523
+ signal
5524
+ );
5525
+ return { files: result.changes, truncated: result.truncated };
5526
+ }
5527
+ throw new Error("A file path is required for this diff scope");
5528
+ }
5529
+ if (input.scope === "COMMIT") {
5530
+ await validateDisplayedCommit(
5531
+ folder,
5532
+ input.baseBranch,
5533
+ input.commitSha,
5534
+ timeoutMs,
5535
+ signal
5536
+ );
5537
+ }
5538
+ await validateChangedPath(input, folder, timeoutMs, signal);
5539
+ const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
5540
+ let oversized = false;
5541
+ if (input.scope === "STAGED") args.push("--cached");
5542
+ if (input.scope === "COMMIT") {
5543
+ const parent = await parentCommit(
5544
+ folder,
5545
+ input.commitSha,
5546
+ timeoutMs,
5547
+ signal
5548
+ );
5549
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
5550
+ } else if (input.scope === "BRANCH") {
5551
+ args.push(
5552
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
5553
+ "HEAD"
5554
+ );
5555
+ }
5556
+ args.push("--", ...requestedDiffPaths(input));
5557
+ let patch;
5558
+ if (input.scope === "UNTRACKED") {
5559
+ const bounded = await readBoundedFile(
5560
+ join5(folder, input.path),
5561
+ MAX_DIFF_BYTES
5562
+ );
5563
+ const contents = bounded.contents;
5564
+ if (bounded.oversized || contents.includes(0)) {
5565
+ oversized = bounded.oversized;
5566
+ patch = "";
5567
+ } else {
5568
+ patch = [
5569
+ `diff --git a/${input.path} b/${input.path}`,
5570
+ "new file mode 100644",
5571
+ "--- /dev/null",
5572
+ `+++ b/${input.path}`,
5573
+ ...contents.toString("utf8").split("\n").map((line) => `+${line}`)
5574
+ ].join("\n");
5575
+ }
5576
+ } else {
5577
+ patch = requireSuccess2(
5578
+ await git2(folder, args, timeoutMs, signal),
5579
+ "Could not load the file diff"
5580
+ ).stdout;
5581
+ }
5582
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
5583
+ const [beforeAvailable, afterAvailable] = await Promise.all([
5584
+ availableSide(sides.before, folder, timeoutMs, signal),
5585
+ availableSide(sides.after, folder, timeoutMs, signal)
5586
+ ]);
5587
+ return {
5588
+ files: [],
5589
+ patch,
5590
+ image: imagePath(input.path),
5591
+ binary: !patch && !imagePath(input.path) && !oversized,
5592
+ truncated: oversized || Buffer.byteLength(patch) >= MAX_DIFF_BYTES,
5593
+ beforeAvailable,
5594
+ afterAvailable
5595
+ };
5596
+ }
5597
+ async function validateChangedPath(input, folder, timeoutMs, signal) {
5598
+ if (!input.path) throw new Error("A changed file path is required");
5599
+ let result;
5600
+ if (input.scope === "UNTRACKED") {
5601
+ result = await git2(
5602
+ folder,
5603
+ ["ls-files", "--others", "--exclude-standard", "-z", "--", input.path],
5604
+ timeoutMs,
5605
+ signal
5606
+ );
5607
+ } else {
5608
+ const args = ["diff", "--name-status", "-z"];
5609
+ if (input.scope === "STAGED") args.push("--cached");
5610
+ if (input.scope === "COMMIT") {
5611
+ await validateDisplayedCommit(
5612
+ folder,
5613
+ input.baseBranch,
5614
+ input.commitSha,
5615
+ timeoutMs,
5616
+ signal
5617
+ );
5618
+ const parent = await parentCommit(
5619
+ folder,
5620
+ input.commitSha,
5621
+ timeoutMs,
5622
+ signal
5623
+ );
5624
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
5625
+ } else if (input.scope === "BRANCH") {
5626
+ args.push(
5627
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
5628
+ "HEAD"
5629
+ );
5630
+ }
5631
+ args.push("--", ...requestedDiffPaths(input));
5632
+ result = await git2(folder, args, timeoutMs, signal);
5633
+ }
5634
+ requireSuccess2(result, "Could not validate the changed file");
5635
+ const requested = input.scope === "UNTRACKED" ? result.stdout.split("\0").filter(Boolean).includes(input.path) : parseNameStatus(result.stdout).some(
5636
+ (file) => file.path === input.path && (!input.previousPath || file.previousPath === input.previousPath)
5637
+ );
5638
+ if (!requested) {
5639
+ throw new Error("The requested file is outside the displayed diff");
5640
+ }
5641
+ }
5642
+ var inspectWorktreeDiff = async (payload, timeoutMs, signal) => {
5643
+ const input = worktreeDiffPayload(payload);
5644
+ const folder = await validateWorktree(input, timeoutMs, signal);
5645
+ return {
5646
+ ...successfulProcess4,
5647
+ diff: await inspectRequestedDiff(input, folder, timeoutMs, signal)
5648
+ };
5649
+ };
5650
+ function imageContentType(path) {
5651
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
5652
+ return {
5653
+ ".png": "image/png",
5654
+ ".jpg": "image/jpeg",
5655
+ ".jpeg": "image/jpeg",
5656
+ ".gif": "image/gif",
5657
+ ".webp": "image/webp",
5658
+ ".avif": "image/avif",
5659
+ ".bmp": "image/bmp"
5660
+ }[extension] ?? "application/octet-stream";
5661
+ }
5662
+ async function writeGitObject(folder, specification, destination, timeoutMs, signal) {
5663
+ await new Promise((resolvePromise, rejectPromise) => {
5664
+ const child = spawn4(
5665
+ "git",
5666
+ ["-C", folder, "cat-file", "blob", specification],
5667
+ { stdio: ["ignore", "pipe", "pipe"] }
5668
+ );
5669
+ const output = createWriteStream(destination, { mode: 384 });
5670
+ let stderr = "";
5671
+ let settled = false;
5672
+ const fail = (error) => {
5673
+ if (settled) return;
5674
+ settled = true;
5675
+ rejectPromise(error instanceof Error ? error : new Error(String(error)));
5676
+ };
5677
+ child.stderr.on("data", (chunk) => {
5678
+ stderr = `${stderr}${String(chunk)}`.slice(0, 4e3);
5679
+ });
5680
+ child.stdout.pipe(output);
5681
+ child.once("error", fail);
5682
+ output.once("error", fail);
5683
+ const terminate = () => child.kill("SIGTERM");
5684
+ const timer = setTimeout(() => {
5685
+ terminate();
5686
+ fail(new Error("Image extraction timed out"));
5687
+ }, timeoutMs);
5688
+ timer.unref();
5689
+ const abort = () => {
5690
+ terminate();
5691
+ fail(new Error("Image extraction was cancelled"));
5692
+ };
5693
+ signal.addEventListener("abort", abort, { once: true });
5694
+ child.once("close", (code) => {
5695
+ clearTimeout(timer);
5696
+ signal.removeEventListener("abort", abort);
5697
+ if (settled) return;
5698
+ if (code !== 0) {
5699
+ fail(new Error(cleanError2(stderr || "Could not extract image")));
5700
+ return;
5701
+ }
5702
+ output.end(() => {
5703
+ if (settled) return;
5704
+ settled = true;
5705
+ resolvePromise();
5706
+ });
5707
+ });
5708
+ });
5709
+ }
5710
+ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, context) => {
5711
+ const input = worktreeDiffPayload(payload);
5712
+ if (!input.path || !input.uploadId || !input.side) {
5713
+ throw new Error("Diff image path, side, and upload ID are required");
5714
+ }
5715
+ if (!imagePath(input.path)) throw new Error("Diff asset is not an image");
5716
+ if (!context?.uploadBuildArtifact) {
5717
+ throw new Error("This agent cannot upload diff images");
5718
+ }
5719
+ const folder = await validateWorktree(input, timeoutMs, signal);
5720
+ await validateChangedPath(input, folder, timeoutMs, signal);
5721
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
5722
+ const selected = input.side === "BEFORE" ? sides.before : sides.after;
5723
+ if (!selected || !await availableSide(selected, folder, timeoutMs, signal)) {
5724
+ throw new Error("The requested image side is unavailable");
5725
+ }
5726
+ let uploadPath;
5727
+ let temporaryDirectory = null;
5728
+ try {
5729
+ if (selected.kind === "FILE") {
5730
+ uploadPath = await realpath4(selected.path);
5731
+ const difference = relative3(folder, uploadPath);
5732
+ if (difference === ".." || difference.startsWith("../")) {
5733
+ throw new Error("Diff image resolves outside the worktree");
5734
+ }
5735
+ } else {
5736
+ const size = requireSuccess2(
5737
+ await git2(
5738
+ folder,
5739
+ ["cat-file", "-s", selected.specification],
5740
+ timeoutMs,
5741
+ signal
5742
+ ),
5743
+ "Could not inspect diff image"
5744
+ ).stdout.trim();
5745
+ if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
5746
+ throw new Error("Diff image exceeds the 20 MiB limit");
5747
+ }
5748
+ temporaryDirectory = await mkdtemp(join5(tmpdir(), "ade-diff-image-"));
5749
+ uploadPath = join5(temporaryDirectory, basename2(input.path));
5750
+ await writeGitObject(
5751
+ folder,
5752
+ selected.specification,
5753
+ uploadPath,
5754
+ timeoutMs,
5755
+ signal
5756
+ );
5757
+ }
5758
+ const information = await stat4(uploadPath);
5759
+ if (!information.isFile() || information.size > 20 * 1024 * 1024) {
5760
+ throw new Error("Diff image exceeds the 20 MiB limit");
5761
+ }
5762
+ await context.uploadBuildArtifact({
5763
+ uploadId: input.uploadId,
5764
+ path: uploadPath,
5765
+ filename: basename2(input.path),
5766
+ contentType: imageContentType(input.path)
5767
+ });
5768
+ return successfulProcess4;
5769
+ } finally {
5770
+ if (temporaryDirectory) {
5771
+ await rm3(temporaryDirectory, { recursive: true, force: true });
5772
+ }
5773
+ }
5774
+ };
5157
5775
  var watchWorktree = async (payload, timeoutMs, signal, _onLog, context) => {
5158
5776
  const input = worktreeWatchJobPayload(payload);
5159
5777
  const current = activeWorktreeWatches.get(input.gitDirectory);
@@ -5955,16 +6573,18 @@ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypt
5955
6573
  import {
5956
6574
  cp,
5957
6575
  mkdir as mkdir3,
5958
- mkdtemp,
6576
+ mkdtemp as mkdtemp2,
6577
+ open as open2,
5959
6578
  readdir as readdir4,
5960
6579
  readFile as readFile4,
5961
6580
  realpath as realpath5,
5962
- rm as rm3,
6581
+ rename as rename3,
6582
+ rm as rm4,
5963
6583
  stat as stat5,
5964
6584
  writeFile as writeFile3
5965
6585
  } from "node:fs/promises";
5966
- import { createWriteStream } from "node:fs";
5967
- import { tmpdir } from "node:os";
6586
+ import { createWriteStream as createWriteStream2 } from "node:fs";
6587
+ import { tmpdir as tmpdir2 } from "node:os";
5968
6588
  import {
5969
6589
  basename as basename3,
5970
6590
  dirname as dirname6,
@@ -5974,7 +6594,7 @@ import {
5974
6594
  resolve as resolve5,
5975
6595
  sep as sep3
5976
6596
  } from "node:path";
5977
- import { spawn as spawn4 } from "node:child_process";
6597
+ import { spawn as spawn5 } from "node:child_process";
5978
6598
  var successfulProcess5 = {
5979
6599
  exitCode: 0,
5980
6600
  signal: null,
@@ -6005,6 +6625,22 @@ function command2(executable, args, timeoutMs, signal, cwd, env = process.env) {
6005
6625
  env
6006
6626
  });
6007
6627
  }
6628
+ async function commandToFile(executable, args, timeoutMs, signal, cwd, env, destination) {
6629
+ const output = await open2(destination, "wx", 384);
6630
+ try {
6631
+ return await captureCommand({
6632
+ command: executable,
6633
+ args,
6634
+ timeoutMs,
6635
+ signal,
6636
+ cwd,
6637
+ env,
6638
+ stdoutFileDescriptor: output.fd
6639
+ });
6640
+ } finally {
6641
+ await output.close();
6642
+ }
6643
+ }
6008
6644
  function requireSuccess3(result, fallback) {
6009
6645
  if (result.cancelled) throw new Error("Operation was cancelled");
6010
6646
  if (result.timedOut) throw new Error("Operation timed out");
@@ -6393,7 +7029,7 @@ function genericBuildDestinations(action) {
6393
7029
  ];
6394
7030
  }
6395
7031
  async function listPhysicalDevices(timeoutMs, signal) {
6396
- const directory = await mkdtemp(join6(tmpdir(), "ade-devices-"));
7032
+ const directory = await mkdtemp2(join6(tmpdir2(), "ade-devices-"));
6397
7033
  const output = join6(directory, "devices.json");
6398
7034
  try {
6399
7035
  const result = await command2(
@@ -6414,7 +7050,7 @@ async function listPhysicalDevices(timeoutMs, signal) {
6414
7050
  if (result.exitCode !== 0) return [];
6415
7051
  return physicalDestinations(JSON.parse(await readFile4(output, "utf8")));
6416
7052
  } finally {
6417
- await rm3(directory, { recursive: true, force: true });
7053
+ await rm4(directory, { recursive: true, force: true });
6418
7054
  }
6419
7055
  }
6420
7056
  var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
@@ -6505,7 +7141,7 @@ var BuildLogger = class {
6505
7141
  constructor(buildId, context, rawLogPath, env) {
6506
7142
  this.buildId = buildId;
6507
7143
  this.context = context;
6508
- this.stream = createWriteStream(rawLogPath, { flags: "a", mode: 384 });
7144
+ this.stream = createWriteStream2(rawLogPath, { flags: "a", mode: 384 });
6509
7145
  this.redact = createRedactor(env);
6510
7146
  }
6511
7147
  buildId;
@@ -6595,12 +7231,12 @@ function runLoggedCommand(options) {
6595
7231
  let killTimer = null;
6596
7232
  let extraStream;
6597
7233
  if (options.additionalLogPath) {
6598
- extraStream = createWriteStream(options.additionalLogPath, {
7234
+ extraStream = createWriteStream2(options.additionalLogPath, {
6599
7235
  flags: "a",
6600
7236
  mode: 384
6601
7237
  });
6602
7238
  }
6603
- const child = spawn4(options.command, options.args, {
7239
+ const child = spawn5(options.command, options.args, {
6604
7240
  cwd: options.cwd,
6605
7241
  env: options.env,
6606
7242
  shell: false,
@@ -6999,6 +7635,392 @@ async function artifact(root, kind, path, metadata = {}) {
6999
7635
  metadata
7000
7636
  };
7001
7637
  }
7638
+ function jsonObject(value) {
7639
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
7640
+ }
7641
+ function testDetails(node) {
7642
+ const own = typeof node.details === "string" && node.details.trim() ? [node.details.trim()] : typeof node.name === "string" && ["Failure Message", "Runtime Warning"].includes(String(node.nodeType)) ? [node.name] : [];
7643
+ const children = Array.isArray(node.children) ? node.children : [];
7644
+ return [
7645
+ ...own,
7646
+ ...children.flatMap((child) => {
7647
+ const object = jsonObject(child);
7648
+ return object ? testDetails(object) : [];
7649
+ })
7650
+ ];
7651
+ }
7652
+ function testSourceFile(node, suite) {
7653
+ const explicit = [
7654
+ node.filePath,
7655
+ node.sourceFilePath,
7656
+ node.fileName,
7657
+ node.sourceFile
7658
+ ].find((value) => typeof value === "string" && !!value);
7659
+ if (explicit) {
7660
+ return { file: basename3(explicit), filePath: explicit };
7661
+ }
7662
+ const identifier = typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : "";
7663
+ const identifierOwner = identifier.split("/")[0]?.trim();
7664
+ return {
7665
+ file: identifierOwner || suite,
7666
+ filePath: null
7667
+ };
7668
+ }
7669
+ function normalizeTestResults(value) {
7670
+ const root = jsonObject(value);
7671
+ if (!root || !Array.isArray(root.testNodes)) {
7672
+ throw new Error("xcresulttool returned invalid test results JSON");
7673
+ }
7674
+ const tests = [];
7675
+ const visit = (raw, context) => {
7676
+ const node = jsonObject(raw);
7677
+ if (!node) return;
7678
+ const nodeType = typeof node.nodeType === "string" ? node.nodeType : "";
7679
+ const name = typeof node.name === "string" ? node.name : "";
7680
+ const next = {
7681
+ plan: nodeType === "Test Plan" ? name : context.plan,
7682
+ configuration: nodeType === "Test Plan Configuration" ? name : context.configuration,
7683
+ bundle: /test bundle$/.test(nodeType) ? name : context.bundle,
7684
+ suite: nodeType === "Test Suite" ? name : context.suite
7685
+ };
7686
+ if (nodeType === "Test Case") {
7687
+ const result = typeof node.result === "string" ? node.result : "unknown";
7688
+ const sourceFile = testSourceFile(node, next.suite);
7689
+ tests.push({
7690
+ identifier: typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : name,
7691
+ name,
7692
+ ...next,
7693
+ ...sourceFile,
7694
+ result,
7695
+ durationSeconds: typeof node.durationInSeconds === "number" ? node.durationInSeconds : null,
7696
+ tags: Array.isArray(node.tags) ? node.tags.filter((tag) => typeof tag === "string") : [],
7697
+ details: [...new Set(testDetails(node))]
7698
+ });
7699
+ }
7700
+ if (Array.isArray(node.children)) {
7701
+ for (const child of node.children) visit(child, next);
7702
+ }
7703
+ };
7704
+ for (const node of root.testNodes) {
7705
+ visit(node, {
7706
+ plan: null,
7707
+ configuration: null,
7708
+ bundle: null,
7709
+ suite: null
7710
+ });
7711
+ }
7712
+ const count = (result) => tests.filter((test) => test.result === result).length;
7713
+ return {
7714
+ summary: {
7715
+ total: tests.length,
7716
+ passed: count("Passed"),
7717
+ failed: count("Failed"),
7718
+ skipped: count("Skipped"),
7719
+ expectedFailures: count("Expected Failure"),
7720
+ unknown: tests.filter(
7721
+ (test) => !["Passed", "Failed", "Skipped", "Expected Failure"].includes(
7722
+ String(test.result)
7723
+ )
7724
+ ).length,
7725
+ durationSeconds: tests.reduce(
7726
+ (total, test) => total + Number(test.durationSeconds ?? 0),
7727
+ 0
7728
+ )
7729
+ },
7730
+ data: {
7731
+ devices: Array.isArray(root.devices) ? root.devices : [],
7732
+ configurations: Array.isArray(root.testPlanConfigurations) ? root.testPlanConfigurations : [],
7733
+ tests
7734
+ }
7735
+ };
7736
+ }
7737
+ function normalizeCoverage(value) {
7738
+ const root = jsonObject(value);
7739
+ if (!root || !Array.isArray(root.targets)) {
7740
+ throw new Error("xccov returned invalid coverage JSON");
7741
+ }
7742
+ const files = root.targets.flatMap((rawTarget) => {
7743
+ const target2 = jsonObject(rawTarget);
7744
+ if (!target2 || !Array.isArray(target2.files)) return [];
7745
+ const targetName = typeof target2.name === "string" ? target2.name : "";
7746
+ return target2.files.flatMap((rawFile) => {
7747
+ const file = jsonObject(rawFile);
7748
+ if (!file) return [];
7749
+ return [
7750
+ {
7751
+ target: targetName,
7752
+ name: typeof file.name === "string" ? file.name : "",
7753
+ path: typeof file.path === "string" ? file.path : "",
7754
+ coveredLines: typeof file.coveredLines === "number" ? file.coveredLines : 0,
7755
+ executableLines: typeof file.executableLines === "number" ? file.executableLines : 0,
7756
+ lineCoverage: typeof file.lineCoverage === "number" ? file.lineCoverage : 0
7757
+ }
7758
+ ];
7759
+ });
7760
+ });
7761
+ return {
7762
+ summary: {
7763
+ coveredLines: typeof root.coveredLines === "number" ? root.coveredLines : 0,
7764
+ executableLines: typeof root.executableLines === "number" ? root.executableLines : 0,
7765
+ lineCoverage: typeof root.lineCoverage === "number" ? root.lineCoverage : 0,
7766
+ targetCount: root.targets.length,
7767
+ fileCount: files.length
7768
+ },
7769
+ data: { files }
7770
+ };
7771
+ }
7772
+ async function writeJsonReport(input, kind, timeoutMs, signal) {
7773
+ const resultBundle = join6(input.artifactDirectory, "result.xcresult");
7774
+ const filename = kind === "TEST_RESULTS" ? "test-results.json" : "code-coverage.json";
7775
+ const destination = join6(input.artifactDirectory, filename);
7776
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
7777
+ try {
7778
+ if (!await pathExists(resultBundle)) {
7779
+ throw new Error("The build result bundle is unavailable");
7780
+ }
7781
+ const args = kind === "TEST_RESULTS" ? [
7782
+ "xcresulttool",
7783
+ "get",
7784
+ "test-results",
7785
+ "tests",
7786
+ "--path",
7787
+ "result.xcresult",
7788
+ "--format",
7789
+ "json"
7790
+ ] : ["xccov", "view", "--report", "--json", "result.xcresult"];
7791
+ requireSuccess3(
7792
+ await commandToFile(
7793
+ "xcrun",
7794
+ args,
7795
+ timeoutMs,
7796
+ signal,
7797
+ input.artifactDirectory,
7798
+ xcodeEnvironment(),
7799
+ temporary
7800
+ ),
7801
+ `Could not generate ${filename}`
7802
+ );
7803
+ const serialized = await readFile4(temporary, "utf8");
7804
+ const parsed = JSON.parse(serialized);
7805
+ const normalized = kind === "TEST_RESULTS" ? normalizeTestResults(parsed) : normalizeCoverage(parsed);
7806
+ await rename3(temporary, destination);
7807
+ return {
7808
+ kind,
7809
+ status: "READY",
7810
+ artifact: await artifact(
7811
+ input.artifactDirectory,
7812
+ kind === "TEST_RESULTS" ? "TEST_RESULTS_JSON" : "CODE_COVERAGE_JSON",
7813
+ destination
7814
+ ),
7815
+ ...normalized,
7816
+ error: null
7817
+ };
7818
+ } catch (error) {
7819
+ await rm4(temporary, { force: true });
7820
+ return {
7821
+ kind,
7822
+ status: "FAILED",
7823
+ artifact: null,
7824
+ summary: {},
7825
+ data: {},
7826
+ error: cleanError3(error)
7827
+ };
7828
+ }
7829
+ }
7830
+ async function gitCommand(folder, args, timeoutMs, signal) {
7831
+ return command2("git", ["-C", folder, ...args], timeoutMs, signal, void 0, {
7832
+ ...process.env,
7833
+ GIT_TERMINAL_PROMPT: "0",
7834
+ GIT_OPTIONAL_LOCKS: "0"
7835
+ });
7836
+ }
7837
+ function changedLinesFromPatch(value) {
7838
+ const result = /* @__PURE__ */ new Map();
7839
+ let path = null;
7840
+ for (const line of value.split("\n")) {
7841
+ if (line.startsWith("+++ ")) {
7842
+ const raw = line.slice(4);
7843
+ path = raw === "/dev/null" ? null : raw.replace(/^b\//, "");
7844
+ if (path && !result.has(path)) result.set(path, /* @__PURE__ */ new Set());
7845
+ continue;
7846
+ }
7847
+ if (!path || !line.startsWith("@@")) continue;
7848
+ const match = /\+(\d+)(?:,(\d+))?/.exec(line);
7849
+ if (!match) continue;
7850
+ const start = Number(match[1]);
7851
+ const count = match[2] === void 0 ? 1 : Number(match[2]);
7852
+ const lines = result.get(path);
7853
+ for (let number = start; number < start + count; number += 1) {
7854
+ lines.add(number);
7855
+ }
7856
+ }
7857
+ return result;
7858
+ }
7859
+ function changeTypesFromStatus(value) {
7860
+ const entries = value.split("\0").filter(Boolean);
7861
+ const result = /* @__PURE__ */ new Map();
7862
+ for (let index = 0; index < entries.length; ) {
7863
+ const status2 = entries[index++] ?? "M";
7864
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
7865
+ const first = entries[index++] ?? "";
7866
+ const second = renamed ? entries[index++] ?? "" : null;
7867
+ const path = second ?? first;
7868
+ if (path) result.set(path, status2[0] ?? "M");
7869
+ }
7870
+ return result;
7871
+ }
7872
+ async function snapshotCoverageChanges(input, folder, timeoutMs, signal) {
7873
+ if (!input.baseBranch) throw new Error("A base branch is required");
7874
+ const base = requireSuccess3(
7875
+ await gitCommand(
7876
+ folder,
7877
+ ["merge-base", `refs/remotes/origin/${input.baseBranch}`, "HEAD"],
7878
+ timeoutMs,
7879
+ signal
7880
+ ),
7881
+ "Could not determine the coverage merge base"
7882
+ ).stdout.trim();
7883
+ const [patch, status2, untracked] = await Promise.all([
7884
+ gitCommand(
7885
+ folder,
7886
+ ["diff", "--no-color", "--unified=0", base, "--"],
7887
+ timeoutMs,
7888
+ signal
7889
+ ),
7890
+ gitCommand(
7891
+ folder,
7892
+ ["diff", "--name-status", "-z", base, "--"],
7893
+ timeoutMs,
7894
+ signal
7895
+ ),
7896
+ gitCommand(
7897
+ folder,
7898
+ ["ls-files", "--others", "--exclude-standard", "-z"],
7899
+ timeoutMs,
7900
+ signal
7901
+ )
7902
+ ]);
7903
+ requireSuccess3(patch, "Could not inspect changed coverage lines");
7904
+ requireSuccess3(status2, "Could not inspect coverage change types");
7905
+ requireSuccess3(untracked, "Could not inspect untracked coverage files");
7906
+ const lines = changedLinesFromPatch(patch.stdout);
7907
+ const types = changeTypesFromStatus(status2.stdout);
7908
+ for (const path of untracked.stdout.split("\0").filter(Boolean)) {
7909
+ try {
7910
+ const contents = await readFile4(join6(folder, path));
7911
+ if (contents.includes(0)) continue;
7912
+ const lineCount = contents.length ? contents.toString("utf8").split("\n").length - (contents.toString("utf8").endsWith("\n") ? 1 : 0) : 0;
7913
+ lines.set(
7914
+ path,
7915
+ new Set(Array.from({ length: lineCount }, (_, index) => index + 1))
7916
+ );
7917
+ types.set(path, "A");
7918
+ } catch {
7919
+ }
7920
+ }
7921
+ return [.../* @__PURE__ */ new Set([...lines.keys(), ...types.keys()])].map((path) => ({
7922
+ path,
7923
+ changeType: types.get(path) ?? "M",
7924
+ lines: [...lines.get(path) ?? []].sort((a, b) => a - b)
7925
+ }));
7926
+ }
7927
+ async function addChangedCoverage(report, changes, input, folder, timeoutMs, signal) {
7928
+ if (report.status !== "READY") return report;
7929
+ const rawFiles = Array.isArray(report.data.files) ? report.data.files : [];
7930
+ const coveragePaths = rawFiles.flatMap((raw) => {
7931
+ const file = jsonObject(raw);
7932
+ return file && typeof file.path === "string" ? [file.path] : [];
7933
+ });
7934
+ const changedFiles = [];
7935
+ let changedCoveredLines = 0;
7936
+ let changedExecutableLines = 0;
7937
+ for (const change of changes) {
7938
+ const coveragePath = coveragePaths.find(
7939
+ (candidate) => relative4(folder, candidate).split(sep3).join("/") === change.path
7940
+ );
7941
+ let covered = 0;
7942
+ let executable = 0;
7943
+ if (coveragePath && change.lines.length) {
7944
+ const temporary2 = join6(
7945
+ input.artifactDirectory,
7946
+ `.changed-coverage-${randomUUID2()}.json`
7947
+ );
7948
+ try {
7949
+ const result = await commandToFile(
7950
+ "xcrun",
7951
+ [
7952
+ "xccov",
7953
+ "view",
7954
+ "--archive",
7955
+ "--file",
7956
+ coveragePath,
7957
+ "--json",
7958
+ "result.xcresult"
7959
+ ],
7960
+ timeoutMs,
7961
+ signal,
7962
+ input.artifactDirectory,
7963
+ xcodeEnvironment(),
7964
+ temporary2
7965
+ );
7966
+ if (result.exitCode === 0 && !result.cancelled && !result.timedOut) {
7967
+ const parsed = jsonObject(
7968
+ JSON.parse(await readFile4(temporary2, "utf8"))
7969
+ );
7970
+ const lineData = parsed?.[coveragePath];
7971
+ if (Array.isArray(lineData)) {
7972
+ const changed = new Set(change.lines);
7973
+ for (const rawLine of lineData) {
7974
+ const line = jsonObject(rawLine);
7975
+ if (line?.isExecutable === true && typeof line.line === "number" && changed.has(line.line)) {
7976
+ executable += 1;
7977
+ if (typeof line.executionCount === "number" && line.executionCount > 0) {
7978
+ covered += 1;
7979
+ }
7980
+ }
7981
+ }
7982
+ }
7983
+ }
7984
+ } finally {
7985
+ await rm4(temporary2, { force: true });
7986
+ }
7987
+ }
7988
+ changedCoveredLines += covered;
7989
+ changedExecutableLines += executable;
7990
+ changedFiles.push({
7991
+ path: change.path,
7992
+ changeType: change.changeType,
7993
+ changedCoveredLines: covered,
7994
+ changedExecutableLines: executable,
7995
+ changedLineCoverage: executable ? covered / executable : null
7996
+ });
7997
+ }
7998
+ const summary = {
7999
+ ...report.summary,
8000
+ changedCoveredLines,
8001
+ changedExecutableLines,
8002
+ changedLineCoverage: changedExecutableLines ? changedCoveredLines / changedExecutableLines : null
8003
+ };
8004
+ const data = { ...report.data, changedFiles };
8005
+ const destination = join6(input.artifactDirectory, "worktree-coverage.json");
8006
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
8007
+ await writeFile3(temporary, JSON.stringify({ summary, data }, null, 2), {
8008
+ mode: 384
8009
+ });
8010
+ await rename3(temporary, destination);
8011
+ return {
8012
+ ...report,
8013
+ summary,
8014
+ data,
8015
+ additionalArtifacts: [
8016
+ await artifact(
8017
+ input.artifactDirectory,
8018
+ "WORKTREE_COVERAGE_JSON",
8019
+ destination
8020
+ )
8021
+ ]
8022
+ };
8023
+ }
7002
8024
  function parseBuildSettings(value) {
7003
8025
  const parsed = JSON.parse(value);
7004
8026
  if (!Array.isArray(parsed)) return [];
@@ -7042,14 +8064,35 @@ async function findFiles(root, predicate, limit = 20) {
7042
8064
  }
7043
8065
  return results;
7044
8066
  }
7045
- async function captureArtifacts(input, folder, signal) {
8067
+ async function captureArtifacts(input, folder, signal, includeProducts = true) {
7046
8068
  const artifacts = [];
7047
8069
  const resultBundle = join6(input.artifactDirectory, "result.xcresult");
7048
8070
  if (await pathExists(resultBundle)) {
8071
+ const coverageProbe = await command2(
8072
+ "xcrun",
8073
+ [
8074
+ "xccov",
8075
+ "view",
8076
+ "--report",
8077
+ "--only-targets",
8078
+ "--json",
8079
+ "result.xcresult"
8080
+ ],
8081
+ 3e4,
8082
+ new AbortController().signal,
8083
+ input.artifactDirectory,
8084
+ xcodeEnvironment()
8085
+ );
7049
8086
  artifacts.push(
7050
- await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle)
8087
+ await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle, {
8088
+ testResultsAvailable: ["TEST", "TEST_WITHOUT_BUILDING"].includes(
8089
+ input.action
8090
+ ),
8091
+ coverageAvailable: coverageProbe.exitCode === 0
8092
+ })
7051
8093
  );
7052
8094
  }
8095
+ if (!includeProducts) return artifacts;
7053
8096
  const archivePath = join6(input.artifactDirectory, "archive.xcarchive");
7054
8097
  if (await pathExists(archivePath)) {
7055
8098
  artifacts.push(
@@ -7078,7 +8121,7 @@ async function captureArtifacts(input, folder, signal) {
7078
8121
  const productDirectory = join6(input.artifactDirectory, "products");
7079
8122
  const destination = join6(productDirectory, basename3(source));
7080
8123
  await mkdir3(productDirectory, { recursive: true, mode: 448 });
7081
- await rm3(destination, { recursive: true, force: true });
8124
+ await rm4(destination, { recursive: true, force: true });
7082
8125
  await cp(source, destination, {
7083
8126
  recursive: true,
7084
8127
  preserveTimestamps: true
@@ -7165,6 +8208,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7165
8208
  configuration: input.configuration
7166
8209
  };
7167
8210
  let buildResult = null;
8211
+ let coverageChanges = [];
7168
8212
  let errorCode = null;
7169
8213
  let error = null;
7170
8214
  let failBuild = false;
@@ -7222,6 +8266,18 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7222
8266
  } catch (progressError) {
7223
8267
  logger.emit("RUNNING", "SYSTEM", cleanError3(progressError));
7224
8268
  }
8269
+ if (input.worktreeCoverage) {
8270
+ try {
8271
+ coverageChanges = await snapshotCoverageChanges(
8272
+ input,
8273
+ folder,
8274
+ Math.min(timeoutMs, 6e4),
8275
+ signal
8276
+ );
8277
+ } catch (coverageError) {
8278
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
8279
+ }
8280
+ }
7225
8281
  const args2 = xcodeBuildArguments(input);
7226
8282
  buildResult = await runLoggedCommand({
7227
8283
  command: "xcrun",
@@ -7281,12 +8337,55 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7281
8337
  }
7282
8338
  }
7283
8339
  let artifacts = [];
7284
- if (!failBuild && !signal.aborted && buildResult?.exitCode === 0) {
8340
+ try {
8341
+ artifacts = await captureArtifacts(
8342
+ input,
8343
+ folder,
8344
+ new AbortController().signal,
8345
+ !failBuild && !signal.aborted && buildResult?.exitCode === 0
8346
+ );
8347
+ } catch (artifactError) {
8348
+ logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8349
+ }
8350
+ const reports = [];
8351
+ const testAction = ["TEST", "TEST_WITHOUT_BUILDING"].includes(input.action);
8352
+ const canGenerateReports = () => !signal.aborted && buildResult?.cancelled !== true && buildResult?.timedOut !== true;
8353
+ if (canGenerateReports() && testAction && input.advancedSettings.parseTestResults) {
8354
+ const report = await writeJsonReport(
8355
+ input,
8356
+ "TEST_RESULTS",
8357
+ Math.min(timeoutMs, 12e4),
8358
+ signal
8359
+ );
8360
+ reports.push(report);
8361
+ if (report.artifact) artifacts.push(report.artifact);
8362
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
8363
+ }
8364
+ if (canGenerateReports() && testAction && input.worktreeCoverage) {
8365
+ let report = await writeJsonReport(
8366
+ input,
8367
+ "CODE_COVERAGE",
8368
+ Math.min(timeoutMs, 12e4),
8369
+ signal
8370
+ );
7285
8371
  try {
7286
- artifacts = await captureArtifacts(input, folder, signal);
7287
- } catch (artifactError) {
7288
- logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8372
+ report = await addChangedCoverage(
8373
+ report,
8374
+ coverageChanges,
8375
+ input,
8376
+ folder,
8377
+ Math.min(timeoutMs, 12e4),
8378
+ signal
8379
+ );
8380
+ } catch (coverageError) {
8381
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
7289
8382
  }
8383
+ reports.push(report);
8384
+ if (report.artifact) artifacts.push(report.artifact);
8385
+ if (report.additionalArtifacts) {
8386
+ artifacts.push(...report.additionalArtifacts);
8387
+ }
8388
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
7290
8389
  }
7291
8390
  await logger.close();
7292
8391
  if (await pathExists(rawLog)) {
@@ -7313,6 +8412,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7313
8412
  error,
7314
8413
  commandSummary: commandSummary("xcrun", args),
7315
8414
  artifacts,
8415
+ reports,
7316
8416
  scriptExecutions,
7317
8417
  sourceStateHash,
7318
8418
  finalStateHash,
@@ -7322,10 +8422,34 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7322
8422
  await logger.close();
7323
8423
  }
7324
8424
  };
8425
+ var generateIosBuildReport = async (payload, timeoutMs, signal, onLog) => {
8426
+ const input = parseBuildReportPayload(payload);
8427
+ if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8428
+ throw new Error("Build artifact directory is invalid");
8429
+ }
8430
+ if (signal.aborted) return { ...successfulProcess5, cancelled: true };
8431
+ const report = await writeJsonReport(
8432
+ input,
8433
+ input.reportKind,
8434
+ Math.min(timeoutMs, 18e4),
8435
+ signal
8436
+ );
8437
+ await onLog({
8438
+ sequence: 0,
8439
+ stream: report.status === "READY" ? "SYSTEM" : "STDERR",
8440
+ message: report.status === "READY" ? `Generated ${report.kind.toLocaleLowerCase()} report` : report.error || "Report generation failed",
8441
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
8442
+ });
8443
+ return {
8444
+ ...successfulProcess5,
8445
+ report,
8446
+ artifacts: report.artifact ? [report.artifact] : []
8447
+ };
8448
+ };
7325
8449
  var deleteIosBuild = async (payload, _timeoutMs, signal, onLog) => {
7326
8450
  const input = parseBuildDeletePayload(payload);
7327
8451
  if (signal.aborted) return { ...successfulProcess5, cancelled: true };
7328
- await rm3(input.artifactDirectory, { recursive: true, force: true });
8452
+ await rm4(input.artifactDirectory, { recursive: true, force: true });
7329
8453
  await onLog({
7330
8454
  sequence: 0,
7331
8455
  stream: "SYSTEM",
@@ -7355,7 +8479,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7355
8479
  try {
7356
8480
  if (information.isDirectory()) {
7357
8481
  temporaryArchive = join6(
7358
- tmpdir(),
8482
+ tmpdir2(),
7359
8483
  `ade-build-artifact-${randomUUID2()}.tar.gz`
7360
8484
  );
7361
8485
  requireSuccess3(
@@ -7388,7 +8512,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7388
8512
  return successfulProcess5;
7389
8513
  } finally {
7390
8514
  if (temporaryArchive) {
7391
- await rm3(temporaryArchive, { force: true });
8515
+ await rm4(temporaryArchive, { force: true });
7392
8516
  }
7393
8517
  }
7394
8518
  };
@@ -7730,6 +8854,8 @@ var handlers = {
7730
8854
  [WORKTREE_DELETE_JOB_KIND]: deleteWorktree,
7731
8855
  [WORKTREE_OPERATION_JOB_KIND]: operateWorktree,
7732
8856
  [WORKTREE_WATCH_JOB_KIND]: watchWorktree,
8857
+ [WORKTREE_DIFF_JOB_KIND]: inspectWorktreeDiff,
8858
+ [WORKTREE_DIFF_ASSET_JOB_KIND]: downloadWorktreeDiffAsset,
7733
8859
  [SKILL_SCAN_JOB_KIND]: scanSkills,
7734
8860
  [SKILL_READ_JOB_KIND]: readSkills,
7735
8861
  [SKILL_APPLY_JOB_KIND]: applySkills,
@@ -7741,7 +8867,9 @@ var handlers = {
7741
8867
  [IOS_BUILD_DELETE_JOB_KIND]: deleteIosBuild,
7742
8868
  [IOS_ARTIFACT_DOWNLOAD_JOB_KIND]: downloadIosBuildArtifact,
7743
8869
  [IOS_DEPLOY_JOB_KIND]: deployIosBuild,
7744
- [IOS_EXPORT_JOB_KIND]: exportIosArchive
8870
+ [IOS_EXPORT_JOB_KIND]: exportIosArchive,
8871
+ [IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
8872
+ [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport
7745
8873
  };
7746
8874
 
7747
8875
  // src/repository-coordinator.ts