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

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 +1849 -241
  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,9 @@ 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";
2291
+ var IOS_SIGNING_INSPECT_JOB_KIND = "ios.signing.inspect";
2204
2292
  var IOS_BUILD_JOB_KINDS = [
2205
2293
  IOS_SOURCE_DISCOVER_JOB_KIND,
2206
2294
  IOS_SOURCE_PARSE_JOB_KIND,
@@ -2210,7 +2298,10 @@ var IOS_BUILD_JOB_KINDS = [
2210
2298
  IOS_BUILD_DELETE_JOB_KIND,
2211
2299
  IOS_ARTIFACT_DOWNLOAD_JOB_KIND,
2212
2300
  IOS_DEPLOY_JOB_KIND,
2213
- IOS_EXPORT_JOB_KIND
2301
+ IOS_EXPORT_JOB_KIND,
2302
+ IOS_TEST_RESULTS_JOB_KIND,
2303
+ IOS_COVERAGE_REPORT_JOB_KIND,
2304
+ IOS_SIGNING_INSPECT_JOB_KIND
2214
2305
  ];
2215
2306
  var BUILD_ACTIONS = [
2216
2307
  "BUILD",
@@ -2259,6 +2350,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2259
2350
  allowProvisioningDeviceRegistration: false,
2260
2351
  testPlan: null,
2261
2352
  codeCoverage: false,
2353
+ parseTestResults: true,
2262
2354
  parallelTesting: null,
2263
2355
  parallelTestingWorkers: null,
2264
2356
  onlyTesting: [],
@@ -2268,6 +2360,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
2268
2360
  priorTestProductsPath: null,
2269
2361
  priorXctestrunPath: null
2270
2362
  };
2363
+ var BUILD_REPORT_KINDS = ["TEST_RESULTS", "CODE_COVERAGE"];
2271
2364
  function objectValue6(value, name) {
2272
2365
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2273
2366
  throw new Error(`${name} must be an object`);
@@ -2293,7 +2386,7 @@ function enumValue3(value, values, name) {
2293
2386
  }
2294
2387
  return value;
2295
2388
  }
2296
- function safeRelativePath(value, name) {
2389
+ function safeRelativePath2(value, name) {
2297
2390
  const path = stringValue6(value, name);
2298
2391
  if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.split(/[\\/]/).includes("..") || path.includes("\0")) {
2299
2392
  throw new Error(`${name} must stay within the worktree`);
@@ -2318,13 +2411,14 @@ function worktreeIdentity(value) {
2318
2411
  value.expectedOrigin,
2319
2412
  "build payload.expectedOrigin"
2320
2413
  ),
2321
- headSha: nullableString4(value.headSha, "build payload.headSha")
2414
+ headSha: nullableString4(value.headSha, "build payload.headSha"),
2415
+ baseBranch: nullableString4(value.baseBranch, "build payload.baseBranch")
2322
2416
  };
2323
2417
  }
2324
2418
  function parseBuildSource(value) {
2325
2419
  const source = objectValue6(value, "build source");
2326
2420
  const kind = enumValue3(source.kind, BUILD_SOURCE_KINDS, "build source.kind");
2327
- const relativePath = safeRelativePath(
2421
+ const relativePath = safeRelativePath2(
2328
2422
  source.relativePath,
2329
2423
  "build source.relativePath"
2330
2424
  );
@@ -2443,6 +2537,10 @@ function parseBuildAdvancedSettings(value) {
2443
2537
  input.codeCoverage,
2444
2538
  "advanced settings.codeCoverage"
2445
2539
  ),
2540
+ parseTestResults: booleanValue2(
2541
+ input.parseTestResults,
2542
+ "advanced settings.parseTestResults"
2543
+ ),
2446
2544
  parallelTesting: parallel,
2447
2545
  parallelTestingWorkers: workers ?? null,
2448
2546
  onlyTesting: stringArray2(
@@ -2580,7 +2678,27 @@ function parseBuildJobPayload(value) {
2580
2678
  action,
2581
2679
  destination,
2582
2680
  advancedSettings,
2583
- scripts
2681
+ scripts,
2682
+ worktreeCoverage: input.worktreeCoverage === void 0 ? false : booleanValue2(
2683
+ input.worktreeCoverage,
2684
+ "build job payload.worktreeCoverage"
2685
+ )
2686
+ };
2687
+ }
2688
+ function parseBuildReportPayload(value) {
2689
+ const input = objectValue6(value, "build report payload");
2690
+ return {
2691
+ ...parseBuildDeletePayload(input),
2692
+ reportKind: enumValue3(
2693
+ input.reportKind,
2694
+ BUILD_REPORT_KINDS,
2695
+ "build report payload.reportKind"
2696
+ ),
2697
+ source: enumValue3(
2698
+ input.source,
2699
+ ["MANUAL", "WORKTREE"],
2700
+ "build report payload.source"
2701
+ )
2584
2702
  };
2585
2703
  }
2586
2704
  function parseBuildDeletePayload(value) {
@@ -2607,7 +2725,7 @@ function parseBuildArtifactDownloadPayload(value) {
2607
2725
  const identity = parseBuildDeletePayload(input);
2608
2726
  return {
2609
2727
  ...identity,
2610
- artifactRelativePath: safeRelativePath(
2728
+ artifactRelativePath: safeRelativePath2(
2611
2729
  input.artifactRelativePath,
2612
2730
  "build artifact download payload.artifactRelativePath"
2613
2731
  ),
@@ -2649,7 +2767,7 @@ function parseBuildDeploymentPayload(value) {
2649
2767
  input.artifactDirectory,
2650
2768
  "build deployment payload.artifactDirectory"
2651
2769
  ),
2652
- artifactRelativePath: safeRelativePath(
2770
+ artifactRelativePath: safeRelativePath2(
2653
2771
  input.artifactRelativePath,
2654
2772
  "build deployment payload.artifactRelativePath"
2655
2773
  ),
@@ -2717,13 +2835,36 @@ function parseBuildExportPayload(value) {
2717
2835
  input.artifactDirectory,
2718
2836
  "build export payload.artifactDirectory"
2719
2837
  ),
2720
- archiveRelativePath: safeRelativePath(
2838
+ archiveRelativePath: safeRelativePath2(
2721
2839
  input.archiveRelativePath,
2722
2840
  "build export payload.archiveRelativePath"
2723
2841
  ),
2724
2842
  settings: parseBuildExportSettings(input.settings)
2725
2843
  };
2726
2844
  }
2845
+ function provisioningProfileType(profile) {
2846
+ if (profile.getTaskAllow) return "DEVELOPMENT";
2847
+ if (profile.provisionsAllDevices) return "ENTERPRISE";
2848
+ return profile.hasProvisionedDevices ? "AD_HOC" : "APP_STORE";
2849
+ }
2850
+ function parseBuildSigningInspectPayload(value) {
2851
+ const input = objectValue6(value, "build signing payload");
2852
+ return {
2853
+ buildId: stringValue6(input.buildId, "build signing payload.buildId"),
2854
+ codebaseId: stringValue6(
2855
+ input.codebaseId,
2856
+ "build signing payload.codebaseId"
2857
+ ),
2858
+ artifactDirectory: stringValue6(
2859
+ input.artifactDirectory,
2860
+ "build signing payload.artifactDirectory"
2861
+ ),
2862
+ archiveRelativePath: safeRelativePath2(
2863
+ input.archiveRelativePath,
2864
+ "build signing payload.archiveRelativePath"
2865
+ )
2866
+ };
2867
+ }
2727
2868
 
2728
2869
  // src/inventory.ts
2729
2870
  var AGENT_VERSION = "0.1.0";
@@ -2864,68 +3005,163 @@ function runCloudflared(payload, timeoutMs, signal, onLog) {
2864
3005
  });
2865
3006
  }
2866
3007
 
2867
- // src/handlers/ccusage.ts
2868
- import { Buffer as Buffer2 } from "node:buffer";
2869
- var MAX_CCUSAGE_STDOUT_BYTES = 16 * 1024 * 1024;
2870
- function validateCcusagePayload(payload) {
2871
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2872
- throw new Error("ccusage.report payload must be an object");
3008
+ // src/handlers/signing.ts
3009
+ import { createHash as createHash2 } from "node:crypto";
3010
+ import { readdir, stat as stat2 } from "node:fs/promises";
3011
+ import { homedir as homedir2 } from "node:os";
3012
+ import { basename, join as join2, relative, sep } from "node:path";
3013
+
3014
+ // ../agent-contract/src/plist.ts
3015
+ function xmlEscape(value) {
3016
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
3017
+ }
3018
+ function plistValue(value) {
3019
+ if (typeof value === "boolean") return value ? "<true/>" : "<false/>";
3020
+ if (typeof value === "string") return `<string>${xmlEscape(value)}</string>`;
3021
+ if (typeof value === "number" && Number.isInteger(value)) {
3022
+ return `<integer>${value}</integer>`;
2873
3023
  }
2874
- const keys = Object.keys(payload);
2875
- if (keys.length > 0) {
2876
- throw new Error(`Unexpected ccusage.report payload field: ${keys[0]}`);
3024
+ if (Array.isArray(value)) {
3025
+ return `<array>${value.map((entry) => plistValue(entry)).join("")}</array>`;
2877
3026
  }
2878
- return {};
3027
+ if (value && typeof value === "object") {
3028
+ return `<dict>${Object.entries(value).map(([key, entry]) => `<key>${xmlEscape(key)}</key>${plistValue(entry)}`).join("")}</dict>`;
3029
+ }
3030
+ throw new Error("Unsupported plist value");
2879
3031
  }
2880
- async function runCcusage(payload, timeoutMs, signal, onLog) {
2881
- validateCcusagePayload(payload);
2882
- const stdout = [];
2883
- let stdoutBytes = 0;
2884
- let exceededLimit = false;
2885
- const result = await runProcess({
2886
- command: "ccusage",
2887
- args: ["--json"],
2888
- timeoutMs,
2889
- signal,
2890
- onLog: async (log) => {
2891
- if (log.stream !== "STDOUT") {
2892
- await onLog(log);
2893
- return;
3032
+ function plistDocument(value) {
3033
+ return `<?xml version="1.0" encoding="UTF-8"?>
3034
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3035
+ <plist version="1.0">${plistValue(value)}</plist>
3036
+ `;
3037
+ }
3038
+ var ENTITIES = {
3039
+ amp: "&",
3040
+ lt: "<",
3041
+ gt: ">",
3042
+ quot: '"',
3043
+ apos: "'"
3044
+ };
3045
+ function decodeText(value) {
3046
+ return value.replace(
3047
+ /&(#x?[0-9a-fA-F]+|[a-z]+);/g,
3048
+ (match, entity) => {
3049
+ if (entity.startsWith("#x") || entity.startsWith("#X")) {
3050
+ return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
2894
3051
  }
2895
- const lineBytes = Buffer2.byteLength(log.message, "utf8") + 1;
2896
- if (stdoutBytes + lineBytes > MAX_CCUSAGE_STDOUT_BYTES) {
2897
- exceededLimit = true;
2898
- return;
3052
+ if (entity.startsWith("#")) {
3053
+ return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
2899
3054
  }
2900
- stdoutBytes += lineBytes;
2901
- stdout.push(log.message);
3055
+ return ENTITIES[entity] ?? match;
3056
+ }
3057
+ );
3058
+ }
3059
+ function nextTag(xml, from) {
3060
+ let index = from;
3061
+ for (; ; ) {
3062
+ const start = xml.indexOf("<", index);
3063
+ if (start < 0) return null;
3064
+ if (xml.startsWith("<!--", start)) {
3065
+ const close2 = xml.indexOf("-->", start);
3066
+ if (close2 < 0) return null;
3067
+ index = close2 + 3;
3068
+ continue;
3069
+ }
3070
+ if (xml.startsWith("<?", start) || xml.startsWith("<!", start)) {
3071
+ const close2 = xml.indexOf(">", start);
3072
+ if (close2 < 0) return null;
3073
+ index = close2 + 1;
3074
+ continue;
3075
+ }
3076
+ const close = xml.indexOf(">", start);
3077
+ if (close < 0) return null;
3078
+ const raw = xml.slice(start + 1, close);
3079
+ const closing = raw.startsWith("/");
3080
+ const selfClosing = raw.endsWith("/");
3081
+ const name = raw.slice(closing ? 1 : 0, selfClosing ? raw.length - 1 : raw.length).trim().split(/\s/)[0];
3082
+ return { name, closing, selfClosing, end: close + 1 };
3083
+ }
3084
+ }
3085
+ function textUntilClose(xml, from, name) {
3086
+ const close = xml.indexOf(`</${name}`, from);
3087
+ if (close < 0) throw new Error(`Unterminated <${name}> in plist`);
3088
+ const end = xml.indexOf(">", close);
3089
+ return [decodeText(xml.slice(from, close)), end + 1];
3090
+ }
3091
+ var MAX_DEPTH = 64;
3092
+ function parseValue(xml, from, depth) {
3093
+ if (depth > MAX_DEPTH) throw new Error("plist nesting is too deep");
3094
+ const tag = nextTag(xml, from);
3095
+ if (!tag || tag.closing) throw new Error("Expected a plist value");
3096
+ if (tag.selfClosing) {
3097
+ switch (tag.name) {
3098
+ case "true":
3099
+ return [true, tag.end];
3100
+ case "false":
3101
+ return [false, tag.end];
3102
+ case "dict":
3103
+ return [{}, tag.end];
3104
+ case "array":
3105
+ return [[], tag.end];
3106
+ default:
3107
+ return ["", tag.end];
2902
3108
  }
2903
- });
2904
- if (result.exitCode !== 0 || result.cancelled || result.timedOut || result.signal !== null) {
2905
- return result;
2906
- }
2907
- if (exceededLimit) {
2908
- throw new Error(
2909
- `ccusage JSON exceeded the ${MAX_CCUSAGE_STDOUT_BYTES} byte limit`
2910
- );
2911
3109
  }
2912
- let parsed;
2913
- try {
2914
- parsed = JSON.parse(stdout.join("\n"));
2915
- } catch {
2916
- throw new Error("ccusage returned malformed JSON");
3110
+ switch (tag.name) {
3111
+ case "dict": {
3112
+ const result = {};
3113
+ let cursor = tag.end;
3114
+ for (; ; ) {
3115
+ const next = nextTag(xml, cursor);
3116
+ if (!next) throw new Error("Unterminated <dict> in plist");
3117
+ if (next.closing && next.name === "dict") return [result, next.end];
3118
+ if (next.name !== "key") throw new Error("Expected <key> in <dict>");
3119
+ const [key, afterKey] = textUntilClose(xml, next.end, "key");
3120
+ const [value, afterValue] = parseValue(xml, afterKey, depth + 1);
3121
+ result[key] = value;
3122
+ cursor = afterValue;
3123
+ }
3124
+ }
3125
+ case "array": {
3126
+ const result = [];
3127
+ let cursor = tag.end;
3128
+ for (; ; ) {
3129
+ const next = nextTag(xml, cursor);
3130
+ if (!next) throw new Error("Unterminated <array> in plist");
3131
+ if (next.closing && next.name === "array") return [result, next.end];
3132
+ const [value, afterValue] = parseValue(xml, cursor, depth + 1);
3133
+ result.push(value);
3134
+ cursor = afterValue;
3135
+ }
3136
+ }
3137
+ case "integer":
3138
+ case "real": {
3139
+ const [text2, end] = textUntilClose(xml, tag.end, tag.name);
3140
+ return [Number(text2.trim()), end];
3141
+ }
3142
+ // Dates and data are returned verbatim: callers need the timestamp text and
3143
+ // never the certificate bytes, so decoding them would only cost memory.
3144
+ case "string":
3145
+ case "date":
3146
+ case "data": {
3147
+ const [text2, end] = textUntilClose(xml, tag.end, tag.name);
3148
+ return [tag.name === "string" ? text2 : text2.trim(), end];
3149
+ }
3150
+ default: {
3151
+ const [, end] = textUntilClose(xml, tag.end, tag.name);
3152
+ return [null, end];
3153
+ }
2917
3154
  }
2918
- return { ...result, report: parseCcusageReport(parsed) };
2919
3155
  }
2920
-
2921
- // src/handlers/build-data.ts
2922
- import { lstat, opendir, readdir, realpath, rm } from "node:fs/promises";
2923
- import { homedir as homedir2 } from "node:os";
2924
- import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
3156
+ function parsePlist(xml) {
3157
+ const start = xml.indexOf("<plist");
3158
+ const from = start < 0 ? 0 : xml.indexOf(">", start) + 1;
3159
+ return parseValue(xml, from, 0)[0];
3160
+ }
2925
3161
 
2926
3162
  // src/capture-command.ts
2927
3163
  import { spawn as spawn2 } from "node:child_process";
2928
- var MAX_OUTPUT_BYTES = 1024 * 1024;
3164
+ var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
2929
3165
  function captureCommand(options) {
2930
3166
  return new Promise((resolve6, reject) => {
2931
3167
  let stdout = "";
@@ -2935,15 +3171,19 @@ function captureCommand(options) {
2935
3171
  let settled = false;
2936
3172
  const child = spawn2(options.command, options.args, {
2937
3173
  shell: false,
2938
- stdio: ["ignore", "pipe", "pipe"],
3174
+ stdio: [
3175
+ "ignore",
3176
+ options.stdoutFileDescriptor === void 0 ? "pipe" : options.stdoutFileDescriptor,
3177
+ "pipe"
3178
+ ],
2939
3179
  env: options.env ?? process.env,
2940
3180
  cwd: options.cwd
2941
3181
  });
2942
3182
  const append = (current, chunk) => `${current}${String(chunk)}`.slice(0, MAX_OUTPUT_BYTES);
2943
- child.stdout.on("data", (chunk) => {
3183
+ child.stdout?.on("data", (chunk) => {
2944
3184
  stdout = append(stdout, chunk);
2945
3185
  });
2946
- child.stderr.on("data", (chunk) => {
3186
+ child.stderr?.on("data", (chunk) => {
2947
3187
  stderr = append(stderr, chunk);
2948
3188
  });
2949
3189
  const terminate = () => {
@@ -2987,7 +3227,332 @@ function captureCommand(options) {
2987
3227
  });
2988
3228
  }
2989
3229
 
3230
+ // src/handlers/signing.ts
3231
+ var PROFILE_DIRECTORIES = [
3232
+ join2(
3233
+ homedir2(),
3234
+ "Library",
3235
+ "Developer",
3236
+ "Xcode",
3237
+ "UserData",
3238
+ "Provisioning Profiles"
3239
+ ),
3240
+ join2(homedir2(), "Library", "MobileDevice", "Provisioning Profiles")
3241
+ ];
3242
+ var PROFILE_EXTENSIONS = [".mobileprovision", ".provisionprofile"];
3243
+ function text(value) {
3244
+ return typeof value === "string" && value.trim() ? value : null;
3245
+ }
3246
+ function stringList(value) {
3247
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
3248
+ }
3249
+ async function decodeProfile(path, timeoutMs, signal) {
3250
+ try {
3251
+ const result = await captureCommand({
3252
+ command: "/usr/bin/security",
3253
+ args: ["cms", "-D", "-i", path],
3254
+ timeoutMs: Math.min(timeoutMs, 1e4),
3255
+ signal
3256
+ });
3257
+ if (result.exitCode !== 0 || !result.stdout.trim()) return null;
3258
+ const parsed = parsePlist(result.stdout);
3259
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3260
+ return null;
3261
+ }
3262
+ return parsed;
3263
+ } catch {
3264
+ return null;
3265
+ }
3266
+ }
3267
+ function toProfile(uuid, raw, now) {
3268
+ const name = text(raw.Name);
3269
+ if (!name) return null;
3270
+ const entitlements = raw.Entitlements && typeof raw.Entitlements === "object" ? raw.Entitlements : {};
3271
+ const applicationIdentifier = text(entitlements["application-identifier"]);
3272
+ const teamId = stringList(raw.TeamIdentifier)[0] ?? text(entitlements["com.apple.developer.team-identifier"]);
3273
+ const bundleId = applicationIdentifier ? applicationIdentifier.replace(/^[A-Z0-9]+\./, "") : "*";
3274
+ const expiresAt = text(raw.ExpirationDate);
3275
+ const expiry = expiresAt ? Date.parse(expiresAt) : Number.NaN;
3276
+ return {
3277
+ uuid,
3278
+ name,
3279
+ teamId: teamId ?? null,
3280
+ teamName: text(raw.TeamName),
3281
+ bundleId,
3282
+ type: provisioningProfileType({
3283
+ getTaskAllow: entitlements["get-task-allow"] === true,
3284
+ hasProvisionedDevices: stringList(raw.ProvisionedDevices).length > 0,
3285
+ provisionsAllDevices: raw.ProvisionsAllDevices === true
3286
+ }),
3287
+ platforms: stringList(raw.Platform),
3288
+ expiresAt,
3289
+ expired: Number.isFinite(expiry) ? expiry < now : false,
3290
+ xcodeManaged: raw.IsXcodeManaged === true,
3291
+ certificateSha1s: certificateFingerprints(raw.DeveloperCertificates)
3292
+ };
3293
+ }
3294
+ function certificateFingerprints(value) {
3295
+ return stringList(value).flatMap((encoded) => {
3296
+ try {
3297
+ const der = Buffer.from(encoded.replace(/\s+/g, ""), "base64");
3298
+ if (!der.length) return [];
3299
+ return [createHash2("sha1").update(der).digest("hex").toUpperCase()];
3300
+ } catch {
3301
+ return [];
3302
+ }
3303
+ });
3304
+ }
3305
+ async function readProfiles(timeoutMs, signal) {
3306
+ const profiles = /* @__PURE__ */ new Map();
3307
+ const now = Date.now();
3308
+ for (const directory of PROFILE_DIRECTORIES) {
3309
+ let entries;
3310
+ try {
3311
+ entries = await readdir(directory);
3312
+ } catch {
3313
+ continue;
3314
+ }
3315
+ for (const entry of entries) {
3316
+ if (!PROFILE_EXTENSIONS.some((suffix) => entry.endsWith(suffix)))
3317
+ continue;
3318
+ signal.throwIfAborted();
3319
+ const raw = await decodeProfile(
3320
+ join2(directory, entry),
3321
+ timeoutMs,
3322
+ signal
3323
+ );
3324
+ if (!raw) continue;
3325
+ const uuid = text(raw.UUID) ?? basename(entry).replace(/\.[^.]+$/, "");
3326
+ const profile = toProfile(uuid, raw, now);
3327
+ if (profile && !profiles.has(profile.uuid)) {
3328
+ profiles.set(profile.uuid, profile);
3329
+ }
3330
+ }
3331
+ }
3332
+ return dedupe([...profiles.values()]).sort(
3333
+ (left, right) => left.name.localeCompare(right.name)
3334
+ );
3335
+ }
3336
+ function dedupe(profiles) {
3337
+ const best = /* @__PURE__ */ new Map();
3338
+ for (const profile of profiles) {
3339
+ const key = [
3340
+ profile.name,
3341
+ profile.type,
3342
+ profile.bundleId,
3343
+ profile.teamId ?? "",
3344
+ [...profile.platforms].sort().join(",")
3345
+ ].join("\0");
3346
+ const existing = best.get(key);
3347
+ if (!existing || (profile.expiresAt ?? "") > (existing.expiresAt ?? "") || // Stable tie-break so repeated inspections return the same profile.
3348
+ profile.expiresAt === existing.expiresAt && profile.uuid < existing.uuid) {
3349
+ best.set(key, profile);
3350
+ }
3351
+ }
3352
+ return [...best.values()];
3353
+ }
3354
+ var IDENTITY_PATTERN = /^\s*\d+\)\s+([0-9A-F]{40})\s+"(.+)"\s*$/i;
3355
+ async function readIdentities(timeoutMs, signal) {
3356
+ try {
3357
+ const result = await captureCommand({
3358
+ command: "/usr/bin/security",
3359
+ args: ["find-identity", "-v", "-p", "codesigning"],
3360
+ timeoutMs: Math.min(timeoutMs, 1e4),
3361
+ signal
3362
+ });
3363
+ if (result.exitCode !== 0) return [];
3364
+ const identities = /* @__PURE__ */ new Map();
3365
+ for (const line of result.stdout.split("\n")) {
3366
+ const match = IDENTITY_PATTERN.exec(line);
3367
+ if (!match) continue;
3368
+ const [, sha1, name] = match;
3369
+ const team = /\(([A-Z0-9]{10})\)\s*$/.exec(name);
3370
+ identities.set(sha1, { sha1, name, teamId: team?.[1] ?? null });
3371
+ }
3372
+ return [...identities.values()];
3373
+ } catch {
3374
+ return [];
3375
+ }
3376
+ }
3377
+ async function readArchiveBundles(archivePath, timeoutMs, signal) {
3378
+ const products = join2(archivePath, "Products");
3379
+ const bundles = [];
3380
+ const queue = [products];
3381
+ while (queue.length) {
3382
+ signal.throwIfAborted();
3383
+ const current = queue.shift();
3384
+ let entries;
3385
+ try {
3386
+ entries = await readdir(current, { withFileTypes: true });
3387
+ } catch {
3388
+ continue;
3389
+ }
3390
+ for (const entry of entries) {
3391
+ if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
3392
+ const path = join2(current, entry.name);
3393
+ const signable = /\.(app|appex)$/.test(entry.name);
3394
+ if (!signable) {
3395
+ if (!/\.[A-Za-z0-9]+$/.test(entry.name)) queue.push(path);
3396
+ continue;
3397
+ }
3398
+ const bundleId = await bundleIdentifier(path, timeoutMs, signal);
3399
+ if (bundleId) {
3400
+ bundles.push({
3401
+ bundleId,
3402
+ name: basename(entry.name).replace(/\.(app|appex)$/, ""),
3403
+ relativePath: relative(archivePath, path).split(sep).join("/"),
3404
+ ...await embeddedProfile(path, timeoutMs, signal)
3405
+ });
3406
+ }
3407
+ queue.push(path);
3408
+ }
3409
+ }
3410
+ return bundles.sort(
3411
+ (left, right) => left.relativePath.localeCompare(right.relativePath)
3412
+ );
3413
+ }
3414
+ async function bundleIdentifier(bundlePath, timeoutMs, signal) {
3415
+ try {
3416
+ const result = await captureCommand({
3417
+ command: "/usr/bin/plutil",
3418
+ args: [
3419
+ "-extract",
3420
+ "CFBundleIdentifier",
3421
+ "raw",
3422
+ "-o",
3423
+ "-",
3424
+ join2(bundlePath, "Info.plist")
3425
+ ],
3426
+ timeoutMs: Math.min(timeoutMs, 5e3),
3427
+ signal
3428
+ });
3429
+ const value = result.stdout.trim();
3430
+ return result.exitCode === 0 && value ? value : null;
3431
+ } catch {
3432
+ return null;
3433
+ }
3434
+ }
3435
+ async function embeddedProfile(bundlePath, timeoutMs, signal) {
3436
+ for (const name of [
3437
+ "embedded.mobileprovision",
3438
+ "embedded.provisionprofile"
3439
+ ]) {
3440
+ const path = join2(bundlePath, name);
3441
+ try {
3442
+ await stat2(path);
3443
+ } catch {
3444
+ continue;
3445
+ }
3446
+ const raw = await decodeProfile(path, timeoutMs, signal);
3447
+ if (raw) {
3448
+ return {
3449
+ embeddedProfileUuid: text(raw.UUID),
3450
+ embeddedProfileName: text(raw.Name)
3451
+ };
3452
+ }
3453
+ }
3454
+ return { embeddedProfileUuid: null, embeddedProfileName: null };
3455
+ }
3456
+ function teamsFrom(profiles, identities) {
3457
+ const teams = /* @__PURE__ */ new Map();
3458
+ for (const profile of profiles) {
3459
+ if (!profile.teamId) continue;
3460
+ const existing = teams.get(profile.teamId);
3461
+ if (!existing && profile.teamName)
3462
+ teams.set(profile.teamId, profile.teamName);
3463
+ else if (!existing) teams.set(profile.teamId, profile.teamId);
3464
+ }
3465
+ for (const identity of identities) {
3466
+ if (!identity.teamId || teams.has(identity.teamId)) continue;
3467
+ const name = /^[^:]+:\s*(.+?)\s*\([A-Z0-9]{10}\)\s*$/.exec(identity.name);
3468
+ teams.set(identity.teamId, name?.[1] ?? identity.teamId);
3469
+ }
3470
+ return [...teams.entries()].map(([id, name]) => ({ id, name })).sort((left, right) => left.name.localeCompare(right.name));
3471
+ }
3472
+ async function inspectSigningAssets(archivePath, timeoutMs, signal) {
3473
+ const [profiles, identities, bundles] = await Promise.all([
3474
+ readProfiles(timeoutMs, signal),
3475
+ readIdentities(timeoutMs, signal),
3476
+ readArchiveBundles(archivePath, timeoutMs, signal)
3477
+ ]);
3478
+ return {
3479
+ teams: teamsFrom(profiles, identities),
3480
+ identities,
3481
+ profiles,
3482
+ bundles
3483
+ };
3484
+ }
3485
+ var inspectIosSigning = async (payload, timeoutMs, signal) => {
3486
+ const input = parseBuildSigningInspectPayload(payload);
3487
+ const archivePath = join2(input.artifactDirectory, input.archiveRelativePath);
3488
+ const inspection = await inspectSigningAssets(archivePath, timeoutMs, signal);
3489
+ return {
3490
+ exitCode: 0,
3491
+ signal: null,
3492
+ timedOut: false,
3493
+ cancelled: false,
3494
+ ...inspection
3495
+ };
3496
+ };
3497
+
3498
+ // src/handlers/ccusage.ts
3499
+ import { Buffer as Buffer2 } from "node:buffer";
3500
+ var MAX_CCUSAGE_STDOUT_BYTES = 16 * 1024 * 1024;
3501
+ function validateCcusagePayload(payload) {
3502
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
3503
+ throw new Error("ccusage.report payload must be an object");
3504
+ }
3505
+ const keys = Object.keys(payload);
3506
+ if (keys.length > 0) {
3507
+ throw new Error(`Unexpected ccusage.report payload field: ${keys[0]}`);
3508
+ }
3509
+ return {};
3510
+ }
3511
+ async function runCcusage(payload, timeoutMs, signal, onLog) {
3512
+ validateCcusagePayload(payload);
3513
+ const stdout = [];
3514
+ let stdoutBytes = 0;
3515
+ let exceededLimit = false;
3516
+ const result = await runProcess({
3517
+ command: "ccusage",
3518
+ args: ["--json"],
3519
+ timeoutMs,
3520
+ signal,
3521
+ onLog: async (log) => {
3522
+ if (log.stream !== "STDOUT") {
3523
+ await onLog(log);
3524
+ return;
3525
+ }
3526
+ const lineBytes = Buffer2.byteLength(log.message, "utf8") + 1;
3527
+ if (stdoutBytes + lineBytes > MAX_CCUSAGE_STDOUT_BYTES) {
3528
+ exceededLimit = true;
3529
+ return;
3530
+ }
3531
+ stdoutBytes += lineBytes;
3532
+ stdout.push(log.message);
3533
+ }
3534
+ });
3535
+ if (result.exitCode !== 0 || result.cancelled || result.timedOut || result.signal !== null) {
3536
+ return result;
3537
+ }
3538
+ if (exceededLimit) {
3539
+ throw new Error(
3540
+ `ccusage JSON exceeded the ${MAX_CCUSAGE_STDOUT_BYTES} byte limit`
3541
+ );
3542
+ }
3543
+ let parsed;
3544
+ try {
3545
+ parsed = JSON.parse(stdout.join("\n"));
3546
+ } catch {
3547
+ throw new Error("ccusage returned malformed JSON");
3548
+ }
3549
+ return { ...result, report: parseCcusageReport(parsed) };
3550
+ }
3551
+
2990
3552
  // src/handlers/build-data.ts
3553
+ import { lstat, opendir, readdir as readdir2, realpath, rm } from "node:fs/promises";
3554
+ import { homedir as homedir3 } from "node:os";
3555
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, resolve } from "node:path";
2991
3556
  var successfulProcess = {
2992
3557
  exitCode: 0,
2993
3558
  signal: null,
@@ -2997,14 +3562,14 @@ var successfulProcess = {
2997
3562
  function errorMessage(error) {
2998
3563
  return error instanceof Error ? error.message : String(error);
2999
3564
  }
3000
- function safeRelativePath2(value) {
3565
+ function safeRelativePath3(value) {
3001
3566
  if (!value || value.includes("\0") || isAbsolute(value)) return false;
3002
3567
  const normalized = value.replaceAll("\\", "/");
3003
3568
  return normalized !== "." && normalized.split("/").every((part) => part && part !== ".." && part !== ".");
3004
3569
  }
3005
3570
  async function configuredRoots(payload) {
3006
3571
  if (payload.mode === "DEFAULT") {
3007
- return [join2(homedir2(), "Library", "Developer", "Xcode", "DerivedData")];
3572
+ return [join3(homedir3(), "Library", "Developer", "Xcode", "DerivedData")];
3008
3573
  }
3009
3574
  if (payload.mode === "ABSOLUTE") {
3010
3575
  if (!payload.path || !isAbsolute(payload.path)) {
@@ -3012,7 +3577,7 @@ async function configuredRoots(payload) {
3012
3577
  }
3013
3578
  return [resolve(payload.path)];
3014
3579
  }
3015
- if (!payload.path || !safeRelativePath2(payload.path)) {
3580
+ if (!payload.path || !safeRelativePath3(payload.path)) {
3016
3581
  throw new Error("Relative Derived Data mode requires a safe relative path");
3017
3582
  }
3018
3583
  return payload.worktrees.map(
@@ -3053,7 +3618,7 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
3053
3618
  }
3054
3619
  let children;
3055
3620
  try {
3056
- children = await readdir(rootPath2, { withFileTypes: true });
3621
+ children = await readdir2(rootPath2, { withFileTypes: true });
3057
3622
  } catch (error) {
3058
3623
  return {
3059
3624
  entries: [],
@@ -3064,9 +3629,9 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
3064
3629
  for (const child of children) {
3065
3630
  signal.throwIfAborted();
3066
3631
  if (!child.isDirectory()) continue;
3067
- const path = join2(rootPath2, child.name);
3632
+ const path = join3(rootPath2, child.name);
3068
3633
  const projectPath = await workspacePath(
3069
- join2(path, "info.plist"),
3634
+ join3(path, "info.plist"),
3070
3635
  timeoutMs,
3071
3636
  signal
3072
3637
  );
@@ -3108,7 +3673,7 @@ function assertDirectChild(rootPath2, path) {
3108
3673
  }
3109
3674
  const root = resolve(rootPath2);
3110
3675
  const target2 = resolve(path);
3111
- if (target2 === root || dirname2(target2) !== root || basename(target2) !== basename(path)) {
3676
+ if (target2 === root || dirname2(target2) !== root || basename2(target2) !== basename2(path)) {
3112
3677
  throw new Error("Build Data target must be a direct child of its root");
3113
3678
  }
3114
3679
  }
@@ -3122,7 +3687,7 @@ async function allocatedBytes(path, signal) {
3122
3687
  try {
3123
3688
  for await (const entry of directory) {
3124
3689
  signal.throwIfAborted();
3125
- total += await allocatedBytes(join2(path, entry.name), signal);
3690
+ total += await allocatedBytes(join3(path, entry.name), signal);
3126
3691
  }
3127
3692
  } finally {
3128
3693
  await directory.close().catch(() => void 0);
@@ -3190,9 +3755,9 @@ var deleteBuildData = async (rawPayload, _timeoutMs, signal) => {
3190
3755
  };
3191
3756
 
3192
3757
  // src/handlers/codebases.ts
3193
- import { readdir as readdir2, realpath as realpath2, stat as stat2 } from "node:fs/promises";
3194
- import { homedir as homedir3 } from "node:os";
3195
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
3758
+ import { readdir as readdir3, realpath as realpath2, stat as stat3 } from "node:fs/promises";
3759
+ import { homedir as homedir4 } from "node:os";
3760
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
3196
3761
  var successfulProcess2 = {
3197
3762
  exitCode: 0,
3198
3763
  signal: null,
@@ -3269,7 +3834,7 @@ function baseSnapshot(folder) {
3269
3834
  }
3270
3835
  async function fetchedAt(commonDirectory) {
3271
3836
  try {
3272
- return (await stat2(join3(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
3837
+ return (await stat3(join4(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
3273
3838
  } catch {
3274
3839
  return null;
3275
3840
  }
@@ -3280,7 +3845,7 @@ async function inspectCodebaseProcess(selectedFolder, timeoutMs, signal, expecte
3280
3845
  let selected;
3281
3846
  try {
3282
3847
  selected = await realpath2(fallbackFolder);
3283
- if (!(await stat2(selected)).isDirectory())
3848
+ if (!(await stat3(selected)).isDirectory())
3284
3849
  throw new Error("Folder is not a directory");
3285
3850
  } catch (error) {
3286
3851
  return {
@@ -3448,23 +4013,23 @@ async function inspectCodebase(selectedFolder, timeoutMs, signal, expectedOrigin
3448
4013
  }
3449
4014
  var browseCodebaseDirectories = async (payload) => {
3450
4015
  const input = codebaseBrowsePayload(payload);
3451
- const homePath = await realpath2(homedir3());
4016
+ const homePath = await realpath2(homedir4());
3452
4017
  const path = await realpath2(input.path ? resolve2(input.path) : homePath);
3453
- if (!(await stat2(path)).isDirectory())
4018
+ if (!(await stat3(path)).isDirectory())
3454
4019
  throw new Error("Path is not a directory");
3455
- const candidates = await readdir2(path, { withFileTypes: true });
4020
+ const candidates = await readdir3(path, { withFileTypes: true });
3456
4021
  const entries = [];
3457
4022
  for (const candidate of candidates) {
3458
4023
  if (candidate.isDirectory()) {
3459
4024
  entries.push({
3460
4025
  name: candidate.name,
3461
- path: join3(path, candidate.name),
4026
+ path: join4(path, candidate.name),
3462
4027
  hidden: candidate.name.startsWith(".")
3463
4028
  });
3464
4029
  } else if (candidate.isSymbolicLink()) {
3465
4030
  try {
3466
- const target2 = join3(path, candidate.name);
3467
- if ((await stat2(target2)).isDirectory()) {
4031
+ const target2 = join4(path, candidate.name);
4032
+ if ((await stat3(target2)).isDirectory()) {
3468
4033
  entries.push({
3469
4034
  name: candidate.name,
3470
4035
  path: target2,
@@ -4034,14 +4599,14 @@ import {
4034
4599
  mkdir as mkdir2,
4035
4600
  readFile as readFile2,
4036
4601
  realpath as realpath3,
4037
- readdir as readdir3,
4602
+ readdir as readdir4,
4038
4603
  rename as rename2,
4039
4604
  rm as rm2,
4040
- stat as stat3,
4605
+ stat as stat4,
4041
4606
  writeFile as writeFile2
4042
4607
  } from "node:fs/promises";
4043
- import { homedir as homedir4 } from "node:os";
4044
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve3, sep } from "node:path";
4608
+ import { homedir as homedir5 } from "node:os";
4609
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
4045
4610
  import { promisify } from "node:util";
4046
4611
  var executeFile = promisify(execFile);
4047
4612
  var successfulProcess3 = {
@@ -4096,27 +4661,27 @@ var PROJECT_ROOTS = {
4096
4661
  };
4097
4662
  async function directoryExists(path) {
4098
4663
  try {
4099
- return (await stat3(path)).isDirectory();
4664
+ return (await stat4(path)).isDirectory();
4100
4665
  } catch {
4101
4666
  return false;
4102
4667
  }
4103
4668
  }
4104
4669
  async function configuredTools(tools) {
4105
- const homePath = homedir4();
4670
+ const homePath = homedir5();
4106
4671
  return Promise.all(
4107
4672
  AI_TOOLS.filter((tool) => tools.includes(tool)).map(async (tool) => ({
4108
4673
  tool,
4109
- configured: await directoryExists(join4(homePath, ...CONFIG_PATHS[tool])),
4674
+ configured: await directoryExists(join5(homePath, ...CONFIG_PATHS[tool])),
4110
4675
  homePath
4111
4676
  }))
4112
4677
  );
4113
4678
  }
4114
4679
  function rootPath(location) {
4115
- const base = location.scope === "GLOBAL" ? homedir4() : location.folder;
4680
+ const base = location.scope === "GLOBAL" ? homedir5() : location.folder;
4116
4681
  const parts = location.scope === "GLOBAL" ? GLOBAL_ROOTS[location.rootKind] : PROJECT_ROOTS[location.rootKind];
4117
4682
  const path = resolve3(base, ...parts);
4118
4683
  const normalizedBase = resolve3(base);
4119
- if (path !== normalizedBase && !path.startsWith(`${normalizedBase}${sep}`)) {
4684
+ if (path !== normalizedBase && !path.startsWith(`${normalizedBase}${sep2}`)) {
4120
4685
  throw new Error("Skill root escaped its configured base directory");
4121
4686
  }
4122
4687
  return path;
@@ -4128,12 +4693,12 @@ async function listPackageFiles(skillDirectory) {
4128
4693
  const files = [];
4129
4694
  let totalBytes = 0;
4130
4695
  const visit = async (directory) => {
4131
- for (const entry of await readdir3(directory, { withFileTypes: true })) {
4132
- const path = join4(directory, entry.name);
4696
+ for (const entry of await readdir4(directory, { withFileTypes: true })) {
4697
+ const path = join5(directory, entry.name);
4133
4698
  const metadata = await lstat2(path);
4134
4699
  if (metadata.isSymbolicLink()) {
4135
4700
  throw new Error(
4136
- `Nested symbolic link is not supported: ${relative(skillDirectory, path)}`
4701
+ `Nested symbolic link is not supported: ${relative2(skillDirectory, path)}`
4137
4702
  );
4138
4703
  }
4139
4704
  if (metadata.isDirectory()) {
@@ -4145,14 +4710,14 @@ async function listPackageFiles(skillDirectory) {
4145
4710
  throw new Error("Skill contains too many files");
4146
4711
  if (metadata.size > MAX_SKILL_FILE_BYTES) {
4147
4712
  throw new Error(
4148
- `${relative(skillDirectory, path)} exceeds the per-file limit`
4713
+ `${relative2(skillDirectory, path)} exceeds the per-file limit`
4149
4714
  );
4150
4715
  }
4151
4716
  totalBytes += metadata.size;
4152
4717
  if (totalBytes > MAX_SKILL_PACKAGE_BYTES)
4153
4718
  throw new Error("Skill exceeds the package size limit");
4154
4719
  files.push({
4155
- path: relative(skillDirectory, path).split(sep).join("/"),
4720
+ path: relative2(skillDirectory, path).split(sep2).join("/"),
4156
4721
  contentsBase64: (await readFile2(path)).toString("base64"),
4157
4722
  executable: (metadata.mode & 73) !== 0
4158
4723
  });
@@ -4175,7 +4740,7 @@ async function readPackage(directory) {
4175
4740
  const frontmatter = parseSkillMetadata(
4176
4741
  Buffer.from(definition.contentsBase64, "base64").toString("utf8")
4177
4742
  );
4178
- if (frontmatter.name !== directory.split(sep).at(-1)) {
4743
+ if (frontmatter.name !== directory.split(sep2).at(-1)) {
4179
4744
  throw new Error("Skill name must match its directory name");
4180
4745
  }
4181
4746
  return {
@@ -4216,12 +4781,12 @@ async function scanRoot2(location, configured, targets, warnings) {
4216
4781
  if (!consumers.length) return [];
4217
4782
  const target2 = targetForFolder(targets, location.folder);
4218
4783
  const installations = [];
4219
- for (const entry of await readdir3(root, { withFileTypes: true })) {
4784
+ for (const entry of await readdir4(root, { withFileTypes: true })) {
4220
4785
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
4221
- const directory = join4(root, entry.name);
4786
+ const directory = join5(root, entry.name);
4222
4787
  try {
4223
4788
  const skillPackage = await readPackage(directory);
4224
- const relativeDirectory = location.folder ? relative(location.folder, directory).split(sep).join("/") : "";
4789
+ const relativeDirectory = location.folder ? relative2(location.folder, directory).split(sep2).join("/") : "";
4225
4790
  installations.push({
4226
4791
  ...location,
4227
4792
  codebaseId: target2?.codebaseId ?? null,
@@ -4297,7 +4862,7 @@ var readSkills = async (payload) => {
4297
4862
  }
4298
4863
  packages.push({
4299
4864
  ...request,
4300
- package: await readPackage(join4(root, request.skillName))
4865
+ package: await readPackage(join5(root, request.skillName))
4301
4866
  });
4302
4867
  }
4303
4868
  return { ...successfulProcess3, packages };
@@ -4332,7 +4897,7 @@ async function updateManagedExclude(folder, relativeSkillPath, present) {
4332
4897
  if (line.trim()) managed.add(line.trim());
4333
4898
  }
4334
4899
  }
4335
- const pattern = `/${relativeSkillPath.split(sep).join("/").replace(/^\/+/, "")}/`;
4900
+ const pattern = `/${relativeSkillPath.split(sep2).join("/").replace(/^\/+/, "")}/`;
4336
4901
  if (present) managed.add(pattern);
4337
4902
  else managed.delete(pattern);
4338
4903
  const block = managed.size ? `${MANAGED_EXCLUDE_START}
@@ -4347,7 +4912,7 @@ ${MANAGED_EXCLUDE_END}` : "";
4347
4912
  }
4348
4913
  async function assertUntracked(location, skillDirectory) {
4349
4914
  if (location.scope !== "PROJECT") return;
4350
- const relativeDirectory = relative(location.folder, skillDirectory).split(sep).join("/");
4915
+ const relativeDirectory = relative2(location.folder, skillDirectory).split(sep2).join("/");
4351
4916
  if (await trackedProjectPath(location.folder, relativeDirectory)) {
4352
4917
  throw new Error(
4353
4918
  `${relativeDirectory} is tracked by Git and cannot be managed automatically`
@@ -4359,16 +4924,16 @@ async function writePackage(location, skillPackage) {
4359
4924
  throw new Error("New skill copies may only be written to shared roots");
4360
4925
  }
4361
4926
  const root = rootPath(location);
4362
- const destination = join4(root, skillPackage.name);
4927
+ const destination = join5(root, skillPackage.name);
4363
4928
  await assertUntracked(location, destination);
4364
4929
  await mkdir2(root, { recursive: true });
4365
- const temporary = join4(root, `.${skillPackage.name}.${randomUUID()}.tmp`);
4366
- const backup = join4(root, `.${skillPackage.name}.${randomUUID()}.bak`);
4930
+ const temporary = join5(root, `.${skillPackage.name}.${randomUUID()}.tmp`);
4931
+ const backup = join5(root, `.${skillPackage.name}.${randomUUID()}.bak`);
4367
4932
  await mkdir2(temporary, { recursive: true });
4368
4933
  try {
4369
4934
  for (const file of skillPackage.files) {
4370
4935
  const path = resolve3(temporary, ...file.path.split("/"));
4371
- if (!path.startsWith(`${temporary}${sep}`))
4936
+ if (!path.startsWith(`${temporary}${sep2}`))
4372
4937
  throw new Error("Skill file escaped temporary directory");
4373
4938
  await mkdir2(dirname4(path), { recursive: true });
4374
4939
  await writeFile2(path, Buffer.from(file.contentsBase64, "base64"));
@@ -4401,7 +4966,7 @@ var applySkills = async (payload) => {
4401
4966
  for (const operation of operations) {
4402
4967
  const root = rootPath(operation);
4403
4968
  const skillName = operation.kind === "WRITE" ? operation.package.name : operation.skillName;
4404
- const destination = join4(root, skillName);
4969
+ const destination = join5(root, skillName);
4405
4970
  await assertUntracked(operation, destination);
4406
4971
  if (operation.kind === "WRITE") {
4407
4972
  await writePackage(operation, operation.package);
@@ -4421,7 +4986,7 @@ var applySkills = async (payload) => {
4421
4986
  if (operation.scope === "PROJECT" && operation.manageGitExclude) {
4422
4987
  await updateManagedExclude(
4423
4988
  operation.folder,
4424
- relative(operation.folder, destination),
4989
+ relative2(operation.folder, destination),
4425
4990
  operation.kind === "WRITE"
4426
4991
  );
4427
4992
  }
@@ -4430,15 +4995,17 @@ var applySkills = async (payload) => {
4430
4995
  };
4431
4996
 
4432
4997
  // 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";
4435
- import { basename as basename2, dirname as dirname5, join as join5, relative as relative3 } from "node:path";
4998
+ import { createWriteStream, watch } from "node:fs";
4999
+ import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat5 } from "node:fs/promises";
5000
+ import { tmpdir } from "node:os";
5001
+ import { basename as basename3, dirname as dirname5, join as join6, relative as relative4 } from "node:path";
5002
+ import { spawn as spawn4 } from "node:child_process";
4436
5003
 
4437
5004
  // src/git-code-state.ts
4438
- import { createHash as createHash2 } from "node:crypto";
5005
+ import { createHash as createHash3 } from "node:crypto";
4439
5006
  import { createReadStream as createReadStream2 } from "node:fs";
4440
5007
  import { lstat as lstat3, readlink } from "node:fs/promises";
4441
- import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
5008
+ import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
4442
5009
  import { spawn as spawn3 } from "node:child_process";
4443
5010
  function gitEnvironment() {
4444
5011
  return {
@@ -4452,8 +5019,8 @@ function remainingTimeoutMs(deadline) {
4452
5019
  }
4453
5020
  function containedPath(folder, path) {
4454
5021
  const absolutePath = resolve4(folder, path);
4455
- const difference = relative2(folder, absolutePath);
4456
- if (!difference || difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
5022
+ const difference = relative3(folder, absolutePath);
5023
+ if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute3(difference)) {
4457
5024
  return null;
4458
5025
  }
4459
5026
  return absolutePath;
@@ -4562,7 +5129,7 @@ async function worktreeCodeStateHash(folder, timeoutMs, signal) {
4562
5129
  if (head.exitCode !== 0 || untracked.exitCode !== 0 || submodules.exitCode !== 0 || operation.signal.aborted) {
4563
5130
  return null;
4564
5131
  }
4565
- const hash = createHash2("sha256");
5132
+ const hash = createHash3("sha256");
4566
5133
  hash.update("head\0");
4567
5134
  hash.update(head.stdout.trim());
4568
5135
  hash.update("\0diff\0");
@@ -4633,6 +5200,16 @@ var successfulProcess4 = {
4633
5200
  cancelled: false
4634
5201
  };
4635
5202
  var WATCH_DEBOUNCE_MS = 500;
5203
+ var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5204
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
5205
+ ".png",
5206
+ ".jpg",
5207
+ ".jpeg",
5208
+ ".gif",
5209
+ ".webp",
5210
+ ".avif",
5211
+ ".bmp"
5212
+ ]);
4636
5213
  var activeWorktreeWatches = /* @__PURE__ */ new Map();
4637
5214
  function statusChangeState(value) {
4638
5215
  const entries = value.split("\0").filter(Boolean);
@@ -4831,7 +5408,7 @@ async function origin(folder, timeoutMs, signal) {
4831
5408
  }
4832
5409
  async function validateWorktree(input, timeoutMs, signal) {
4833
5410
  const folder = await realpath4(input.folder);
4834
- if (!(await stat4(folder)).isDirectory())
5411
+ if (!(await stat5(folder)).isDirectory())
4835
5412
  throw new Error("Worktree is missing");
4836
5413
  const observedGitDirectory = await gitDirectory(folder, timeoutMs, signal);
4837
5414
  if (observedGitDirectory !== input.gitDirectory) {
@@ -4915,7 +5492,7 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
4915
5492
  return {
4916
5493
  gitDirectory: gitDir,
4917
5494
  folder,
4918
- relativePath: relative3(rootFolder, folder) || ".",
5495
+ relativePath: relative4(rootFolder, folder) || ".",
4919
5496
  primary,
4920
5497
  branch,
4921
5498
  headSha: headResult.exitCode === 0 ? headResult.stdout.trim() : null,
@@ -4942,7 +5519,7 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
4942
5519
  return {
4943
5520
  gitDirectory: folderValue2,
4944
5521
  folder: folderValue2,
4945
- relativePath: relative3(rootFolder, folderValue2) || ".",
5522
+ relativePath: relative4(rootFolder, folderValue2) || ".",
4946
5523
  primary,
4947
5524
  branch: null,
4948
5525
  headSha: null,
@@ -5058,12 +5635,12 @@ function parseNumstat(value) {
5058
5635
  }
5059
5636
  async function untrackedLines(folder, path) {
5060
5637
  try {
5061
- const file = await stat4(`${folder}/${path}`);
5638
+ const file = await stat5(`${folder}/${path}`);
5062
5639
  if (!file.isFile() || file.size > 1024 * 1024) return null;
5063
5640
  const contents = await readFile3(`${folder}/${path}`);
5064
5641
  if (contents.includes(0)) return null;
5065
- const text = contents.toString("utf8");
5066
- return text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0;
5642
+ const text2 = contents.toString("utf8");
5643
+ return text2 ? text2.split("\n").length - (text2.endsWith("\n") ? 1 : 0) : 0;
5067
5644
  } catch {
5068
5645
  return null;
5069
5646
  }
@@ -5088,7 +5665,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
5088
5665
  const value = values[index];
5089
5666
  const code = value.slice(0, 2);
5090
5667
  const path = value.slice(3);
5091
- if ((code[0] === "R" || code[0] === "C") && values[index + 1]) index += 1;
5668
+ const previousPath = (code[0] === "R" || code[0] === "C") && values[index + 1] ? values[index + 1] : null;
5669
+ if (previousPath) index += 1;
5092
5670
  const untracked = code === "??";
5093
5671
  const conflicted = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(
5094
5672
  code
@@ -5100,6 +5678,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
5100
5678
  const untrackedCount = untracked ? await untrackedLines(folder, path) : null;
5101
5679
  changes.push({
5102
5680
  path,
5681
+ previousPath,
5682
+ changeType: untracked ? "ADDED" : conflicted ? "CONFLICTED" : code,
5103
5683
  staged,
5104
5684
  unstaged,
5105
5685
  untracked,
@@ -5112,6 +5692,65 @@ async function inspectChanges(folder, timeoutMs, signal) {
5112
5692
  }
5113
5693
  return { changes: changes.slice(0, 500), truncated: changes.length > 500 };
5114
5694
  }
5695
+ function imagePath(path) {
5696
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
5697
+ return IMAGE_EXTENSIONS.has(extension);
5698
+ }
5699
+ function parseNameStatus(value) {
5700
+ const entries = value.split("\0").filter(Boolean);
5701
+ const files = [];
5702
+ for (let index = 0; index < entries.length; ) {
5703
+ const status2 = entries[index++] ?? "M";
5704
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
5705
+ const first = entries[index++] ?? "";
5706
+ const second = renamed ? entries[index++] ?? "" : null;
5707
+ const path = second ?? first;
5708
+ if (!path) continue;
5709
+ files.push({
5710
+ path,
5711
+ previousPath: renamed ? first : null,
5712
+ changeType: status2[0] ?? "M"
5713
+ });
5714
+ }
5715
+ return files;
5716
+ }
5717
+ async function diffFiles(folder, range, timeoutMs, signal) {
5718
+ const [status2, counts2] = await Promise.all([
5719
+ git2(folder, ["diff", "--name-status", "-z", range], timeoutMs, signal),
5720
+ git2(folder, ["diff", "--numstat", "-z", range], timeoutMs, signal)
5721
+ ]);
5722
+ requireSuccess2(status2, "Could not inspect changed files");
5723
+ requireSuccess2(counts2, "Could not inspect changed line counts");
5724
+ const numstat = parseNumstat(counts2.stdout);
5725
+ return parseNameStatus(status2.stdout).map((file) => {
5726
+ const [additions, deletions] = numstat.get(file.path) ?? [null, null];
5727
+ return {
5728
+ ...file,
5729
+ additions,
5730
+ deletions,
5731
+ binary: additions === null && deletions === null,
5732
+ image: imagePath(file.path)
5733
+ };
5734
+ });
5735
+ }
5736
+ async function inspectBranchChanges(folder, baseBranch, timeoutMs, signal) {
5737
+ const mergeBase = requireSuccess2(
5738
+ await git2(
5739
+ folder,
5740
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5741
+ timeoutMs,
5742
+ signal
5743
+ ),
5744
+ "Could not determine the base-branch merge base"
5745
+ ).stdout.trim();
5746
+ const files = await diffFiles(
5747
+ folder,
5748
+ `${mergeBase}..HEAD`,
5749
+ timeoutMs,
5750
+ signal
5751
+ );
5752
+ return { changes: files.slice(0, 500), truncated: files.length > 500 };
5753
+ }
5115
5754
  async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
5116
5755
  const result = requireSuccess2(
5117
5756
  await git2(
@@ -5143,17 +5782,441 @@ async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
5143
5782
  return { commits: commits.slice(0, 100), truncated: commits.length > 100 };
5144
5783
  }
5145
5784
  async function inspectWorktreeDetail(folder, baseBranch, timeoutMs, signal) {
5146
- const [commitResult, changeResult] = await Promise.all([
5785
+ const [commitResult, changeResult, branchResult] = await Promise.all([
5147
5786
  inspectCommits(folder, baseBranch, timeoutMs, signal),
5148
- inspectChanges(folder, timeoutMs, signal)
5787
+ inspectChanges(folder, timeoutMs, signal),
5788
+ inspectBranchChanges(folder, baseBranch, timeoutMs, signal)
5149
5789
  ]);
5150
5790
  return {
5151
5791
  commits: commitResult.commits,
5152
5792
  changes: changeResult.changes,
5793
+ branchChanges: branchResult.changes,
5153
5794
  commitsTruncated: commitResult.truncated,
5154
- changesTruncated: changeResult.truncated
5795
+ changesTruncated: changeResult.truncated,
5796
+ branchChangesTruncated: branchResult.truncated
5797
+ };
5798
+ }
5799
+ async function baseMergeCommit(folder, baseBranch, timeoutMs, signal) {
5800
+ return requireSuccess2(
5801
+ await git2(
5802
+ folder,
5803
+ ["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
5804
+ timeoutMs,
5805
+ signal
5806
+ ),
5807
+ "Could not determine the base-branch merge base"
5808
+ ).stdout.trim();
5809
+ }
5810
+ async function validateDisplayedCommit(folder, baseBranch, commitSha, timeoutMs, signal) {
5811
+ if (!/^[0-9a-f]{7,64}$/i.test(commitSha)) {
5812
+ throw new Error("Commit SHA is invalid");
5813
+ }
5814
+ const base = await baseMergeCommit(folder, baseBranch, timeoutMs, signal);
5815
+ const [afterBase, beforeHead] = await Promise.all([
5816
+ git2(
5817
+ folder,
5818
+ ["merge-base", "--is-ancestor", base, commitSha],
5819
+ timeoutMs,
5820
+ signal
5821
+ ),
5822
+ git2(
5823
+ folder,
5824
+ ["merge-base", "--is-ancestor", commitSha, "HEAD"],
5825
+ timeoutMs,
5826
+ signal
5827
+ )
5828
+ ]);
5829
+ if (afterBase.exitCode !== 0 || beforeHead.exitCode !== 0) {
5830
+ throw new Error("Commit is outside the displayed branch history");
5831
+ }
5832
+ }
5833
+ async function parentCommit(folder, commitSha, timeoutMs, signal) {
5834
+ const result = await git2(
5835
+ folder,
5836
+ ["rev-parse", `${commitSha}^`],
5837
+ timeoutMs,
5838
+ signal
5839
+ );
5840
+ return result.exitCode === 0 ? result.stdout.trim() : null;
5841
+ }
5842
+ async function gitObjectExists(folder, specification, timeoutMs, signal) {
5843
+ return (await git2(folder, ["cat-file", "-e", specification], timeoutMs, signal)).exitCode === 0;
5844
+ }
5845
+ async function comparisonSides(input, folder, timeoutMs, signal) {
5846
+ if (!input.path) return { before: null, after: null };
5847
+ const path = input.path;
5848
+ const previousPath = input.previousPath ?? path;
5849
+ const currentPath = join6(folder, path);
5850
+ if (input.scope === "UNTRACKED") {
5851
+ return { before: null, after: { kind: "FILE", path: currentPath } };
5852
+ }
5853
+ if (input.scope === "STAGED") {
5854
+ return {
5855
+ before: { kind: "GIT", specification: `HEAD:${previousPath}` },
5856
+ after: { kind: "GIT", specification: `:${path}` }
5857
+ };
5858
+ }
5859
+ if (input.scope === "UNSTAGED") {
5860
+ return {
5861
+ before: { kind: "GIT", specification: `:${path}` },
5862
+ after: { kind: "FILE", path: currentPath }
5863
+ };
5864
+ }
5865
+ if (input.scope === "COMMIT") {
5866
+ const commitSha = input.commitSha;
5867
+ await validateDisplayedCommit(
5868
+ folder,
5869
+ input.baseBranch,
5870
+ commitSha,
5871
+ timeoutMs,
5872
+ signal
5873
+ );
5874
+ const parent = await parentCommit(folder, commitSha, timeoutMs, signal);
5875
+ return {
5876
+ before: parent ? { kind: "GIT", specification: `${parent}:${previousPath}` } : null,
5877
+ after: { kind: "GIT", specification: `${commitSha}:${path}` }
5878
+ };
5879
+ }
5880
+ const base = await baseMergeCommit(
5881
+ folder,
5882
+ input.baseBranch,
5883
+ timeoutMs,
5884
+ signal
5885
+ );
5886
+ return {
5887
+ before: { kind: "GIT", specification: `${base}:${previousPath}` },
5888
+ after: { kind: "GIT", specification: `HEAD:${path}` }
5889
+ };
5890
+ }
5891
+ function requestedDiffPaths(input) {
5892
+ if (!input.path) return [];
5893
+ if (input.previousPath && ["STAGED", "COMMIT", "BRANCH"].includes(input.scope)) {
5894
+ return [input.previousPath, input.path];
5895
+ }
5896
+ return [input.path];
5897
+ }
5898
+ async function readBoundedFile(path, limit) {
5899
+ const file = await open(path, "r");
5900
+ try {
5901
+ const metadata = await file.stat();
5902
+ if (!metadata.isFile()) throw new Error("Changed path is not a file");
5903
+ if (metadata.size > limit) {
5904
+ return { contents: Buffer.alloc(0), oversized: true };
5905
+ }
5906
+ const buffer = Buffer.allocUnsafe(limit + 1);
5907
+ let length = 0;
5908
+ while (length < buffer.length) {
5909
+ const { bytesRead } = await file.read(
5910
+ buffer,
5911
+ length,
5912
+ buffer.length - length,
5913
+ length
5914
+ );
5915
+ if (bytesRead === 0) break;
5916
+ length += bytesRead;
5917
+ }
5918
+ return {
5919
+ contents: buffer.subarray(0, Math.min(length, limit)),
5920
+ oversized: length > limit
5921
+ };
5922
+ } finally {
5923
+ await file.close();
5924
+ }
5925
+ }
5926
+ async function availableSide(side, folder, timeoutMs, signal) {
5927
+ if (!side) return false;
5928
+ if (side.kind === "GIT") {
5929
+ return gitObjectExists(folder, side.specification, timeoutMs, signal);
5930
+ }
5931
+ try {
5932
+ const resolved = await realpath4(side.path);
5933
+ const difference = relative4(folder, resolved);
5934
+ return difference !== ".." && !difference.startsWith("../") && (await stat5(resolved)).isFile();
5935
+ } catch {
5936
+ return false;
5937
+ }
5938
+ }
5939
+ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
5940
+ if (!input.path) {
5941
+ if (input.scope === "COMMIT") {
5942
+ await validateDisplayedCommit(
5943
+ folder,
5944
+ input.baseBranch,
5945
+ input.commitSha,
5946
+ timeoutMs,
5947
+ signal
5948
+ );
5949
+ const parent = await parentCommit(
5950
+ folder,
5951
+ input.commitSha,
5952
+ timeoutMs,
5953
+ signal
5954
+ );
5955
+ const files = await diffFiles(
5956
+ folder,
5957
+ parent ? `${parent}..${input.commitSha}` : input.commitSha,
5958
+ timeoutMs,
5959
+ signal
5960
+ );
5961
+ return { files: files.slice(0, 500), truncated: files.length > 500 };
5962
+ }
5963
+ if (input.scope === "BRANCH") {
5964
+ const result = await inspectBranchChanges(
5965
+ folder,
5966
+ input.baseBranch,
5967
+ timeoutMs,
5968
+ signal
5969
+ );
5970
+ return { files: result.changes, truncated: result.truncated };
5971
+ }
5972
+ throw new Error("A file path is required for this diff scope");
5973
+ }
5974
+ if (input.scope === "COMMIT") {
5975
+ await validateDisplayedCommit(
5976
+ folder,
5977
+ input.baseBranch,
5978
+ input.commitSha,
5979
+ timeoutMs,
5980
+ signal
5981
+ );
5982
+ }
5983
+ await validateChangedPath(input, folder, timeoutMs, signal);
5984
+ const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
5985
+ let oversized = false;
5986
+ if (input.scope === "STAGED") args.push("--cached");
5987
+ if (input.scope === "COMMIT") {
5988
+ const parent = await parentCommit(
5989
+ folder,
5990
+ input.commitSha,
5991
+ timeoutMs,
5992
+ signal
5993
+ );
5994
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
5995
+ } else if (input.scope === "BRANCH") {
5996
+ args.push(
5997
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
5998
+ "HEAD"
5999
+ );
6000
+ }
6001
+ args.push("--", ...requestedDiffPaths(input));
6002
+ let patch;
6003
+ if (input.scope === "UNTRACKED") {
6004
+ const bounded = await readBoundedFile(
6005
+ join6(folder, input.path),
6006
+ MAX_DIFF_BYTES
6007
+ );
6008
+ const contents = bounded.contents;
6009
+ if (bounded.oversized || contents.includes(0)) {
6010
+ oversized = bounded.oversized;
6011
+ patch = "";
6012
+ } else {
6013
+ patch = [
6014
+ `diff --git a/${input.path} b/${input.path}`,
6015
+ "new file mode 100644",
6016
+ "--- /dev/null",
6017
+ `+++ b/${input.path}`,
6018
+ ...contents.toString("utf8").split("\n").map((line) => `+${line}`)
6019
+ ].join("\n");
6020
+ }
6021
+ } else {
6022
+ patch = requireSuccess2(
6023
+ await git2(folder, args, timeoutMs, signal),
6024
+ "Could not load the file diff"
6025
+ ).stdout;
6026
+ }
6027
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
6028
+ const [beforeAvailable, afterAvailable] = await Promise.all([
6029
+ availableSide(sides.before, folder, timeoutMs, signal),
6030
+ availableSide(sides.after, folder, timeoutMs, signal)
6031
+ ]);
6032
+ return {
6033
+ files: [],
6034
+ patch,
6035
+ image: imagePath(input.path),
6036
+ binary: !patch && !imagePath(input.path) && !oversized,
6037
+ truncated: oversized || Buffer.byteLength(patch) >= MAX_DIFF_BYTES,
6038
+ beforeAvailable,
6039
+ afterAvailable
6040
+ };
6041
+ }
6042
+ async function validateChangedPath(input, folder, timeoutMs, signal) {
6043
+ if (!input.path) throw new Error("A changed file path is required");
6044
+ let result;
6045
+ if (input.scope === "UNTRACKED") {
6046
+ result = await git2(
6047
+ folder,
6048
+ ["ls-files", "--others", "--exclude-standard", "-z", "--", input.path],
6049
+ timeoutMs,
6050
+ signal
6051
+ );
6052
+ } else {
6053
+ const args = ["diff", "--name-status", "-z"];
6054
+ if (input.scope === "STAGED") args.push("--cached");
6055
+ if (input.scope === "COMMIT") {
6056
+ await validateDisplayedCommit(
6057
+ folder,
6058
+ input.baseBranch,
6059
+ input.commitSha,
6060
+ timeoutMs,
6061
+ signal
6062
+ );
6063
+ const parent = await parentCommit(
6064
+ folder,
6065
+ input.commitSha,
6066
+ timeoutMs,
6067
+ signal
6068
+ );
6069
+ args.push(parent ?? `${input.commitSha}^`, input.commitSha);
6070
+ } else if (input.scope === "BRANCH") {
6071
+ args.push(
6072
+ await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
6073
+ "HEAD"
6074
+ );
6075
+ }
6076
+ args.push("--", ...requestedDiffPaths(input));
6077
+ result = await git2(folder, args, timeoutMs, signal);
6078
+ }
6079
+ requireSuccess2(result, "Could not validate the changed file");
6080
+ const requested = input.scope === "UNTRACKED" ? result.stdout.split("\0").filter(Boolean).includes(input.path) : parseNameStatus(result.stdout).some(
6081
+ (file) => file.path === input.path && (!input.previousPath || file.previousPath === input.previousPath)
6082
+ );
6083
+ if (!requested) {
6084
+ throw new Error("The requested file is outside the displayed diff");
6085
+ }
6086
+ }
6087
+ var inspectWorktreeDiff = async (payload, timeoutMs, signal) => {
6088
+ const input = worktreeDiffPayload(payload);
6089
+ const folder = await validateWorktree(input, timeoutMs, signal);
6090
+ return {
6091
+ ...successfulProcess4,
6092
+ diff: await inspectRequestedDiff(input, folder, timeoutMs, signal)
5155
6093
  };
6094
+ };
6095
+ function imageContentType(path) {
6096
+ const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
6097
+ return {
6098
+ ".png": "image/png",
6099
+ ".jpg": "image/jpeg",
6100
+ ".jpeg": "image/jpeg",
6101
+ ".gif": "image/gif",
6102
+ ".webp": "image/webp",
6103
+ ".avif": "image/avif",
6104
+ ".bmp": "image/bmp"
6105
+ }[extension] ?? "application/octet-stream";
6106
+ }
6107
+ async function writeGitObject(folder, specification, destination, timeoutMs, signal) {
6108
+ await new Promise((resolvePromise, rejectPromise) => {
6109
+ const child = spawn4(
6110
+ "git",
6111
+ ["-C", folder, "cat-file", "blob", specification],
6112
+ { stdio: ["ignore", "pipe", "pipe"] }
6113
+ );
6114
+ const output = createWriteStream(destination, { mode: 384 });
6115
+ let stderr = "";
6116
+ let settled = false;
6117
+ const fail = (error) => {
6118
+ if (settled) return;
6119
+ settled = true;
6120
+ rejectPromise(error instanceof Error ? error : new Error(String(error)));
6121
+ };
6122
+ child.stderr.on("data", (chunk) => {
6123
+ stderr = `${stderr}${String(chunk)}`.slice(0, 4e3);
6124
+ });
6125
+ child.stdout.pipe(output);
6126
+ child.once("error", fail);
6127
+ output.once("error", fail);
6128
+ const terminate = () => child.kill("SIGTERM");
6129
+ const timer = setTimeout(() => {
6130
+ terminate();
6131
+ fail(new Error("Image extraction timed out"));
6132
+ }, timeoutMs);
6133
+ timer.unref();
6134
+ const abort = () => {
6135
+ terminate();
6136
+ fail(new Error("Image extraction was cancelled"));
6137
+ };
6138
+ signal.addEventListener("abort", abort, { once: true });
6139
+ child.once("close", (code) => {
6140
+ clearTimeout(timer);
6141
+ signal.removeEventListener("abort", abort);
6142
+ if (settled) return;
6143
+ if (code !== 0) {
6144
+ fail(new Error(cleanError2(stderr || "Could not extract image")));
6145
+ return;
6146
+ }
6147
+ output.end(() => {
6148
+ if (settled) return;
6149
+ settled = true;
6150
+ resolvePromise();
6151
+ });
6152
+ });
6153
+ });
5156
6154
  }
6155
+ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, context) => {
6156
+ const input = worktreeDiffPayload(payload);
6157
+ if (!input.path || !input.uploadId || !input.side) {
6158
+ throw new Error("Diff image path, side, and upload ID are required");
6159
+ }
6160
+ if (!imagePath(input.path)) throw new Error("Diff asset is not an image");
6161
+ if (!context?.uploadBuildArtifact) {
6162
+ throw new Error("This agent cannot upload diff images");
6163
+ }
6164
+ const folder = await validateWorktree(input, timeoutMs, signal);
6165
+ await validateChangedPath(input, folder, timeoutMs, signal);
6166
+ const sides = await comparisonSides(input, folder, timeoutMs, signal);
6167
+ const selected = input.side === "BEFORE" ? sides.before : sides.after;
6168
+ if (!selected || !await availableSide(selected, folder, timeoutMs, signal)) {
6169
+ throw new Error("The requested image side is unavailable");
6170
+ }
6171
+ let uploadPath;
6172
+ let temporaryDirectory = null;
6173
+ try {
6174
+ if (selected.kind === "FILE") {
6175
+ uploadPath = await realpath4(selected.path);
6176
+ const difference = relative4(folder, uploadPath);
6177
+ if (difference === ".." || difference.startsWith("../")) {
6178
+ throw new Error("Diff image resolves outside the worktree");
6179
+ }
6180
+ } else {
6181
+ const size = requireSuccess2(
6182
+ await git2(
6183
+ folder,
6184
+ ["cat-file", "-s", selected.specification],
6185
+ timeoutMs,
6186
+ signal
6187
+ ),
6188
+ "Could not inspect diff image"
6189
+ ).stdout.trim();
6190
+ if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
6191
+ throw new Error("Diff image exceeds the 20 MiB limit");
6192
+ }
6193
+ temporaryDirectory = await mkdtemp(join6(tmpdir(), "ade-diff-image-"));
6194
+ uploadPath = join6(temporaryDirectory, basename3(input.path));
6195
+ await writeGitObject(
6196
+ folder,
6197
+ selected.specification,
6198
+ uploadPath,
6199
+ timeoutMs,
6200
+ signal
6201
+ );
6202
+ }
6203
+ const information = await stat5(uploadPath);
6204
+ if (!information.isFile() || information.size > 20 * 1024 * 1024) {
6205
+ throw new Error("Diff image exceeds the 20 MiB limit");
6206
+ }
6207
+ await context.uploadBuildArtifact({
6208
+ uploadId: input.uploadId,
6209
+ path: uploadPath,
6210
+ filename: basename3(input.path),
6211
+ contentType: imageContentType(input.path)
6212
+ });
6213
+ return successfulProcess4;
6214
+ } finally {
6215
+ if (temporaryDirectory) {
6216
+ await rm3(temporaryDirectory, { recursive: true, force: true });
6217
+ }
6218
+ }
6219
+ };
5157
6220
  var watchWorktree = async (payload, timeoutMs, signal, _onLog, context) => {
5158
6221
  const input = worktreeWatchJobPayload(payload);
5159
6222
  const current = activeWorktreeWatches.get(input.gitDirectory);
@@ -5288,7 +6351,7 @@ async function chooseNewBranch(folder, candidates, allowedCurrentBranch, timeout
5288
6351
  }
5289
6352
  async function validateBranchRoot(input, timeoutMs, signal) {
5290
6353
  const rootFolder = await realpath4(input.rootFolder);
5291
- if (!(await stat4(rootFolder)).isDirectory()) {
6354
+ if (!(await stat5(rootFolder)).isDirectory()) {
5292
6355
  throw new Error("Base repository is missing");
5293
6356
  }
5294
6357
  if (await origin(rootFolder, timeoutMs, signal) !== input.expectedOrigin) {
@@ -5351,13 +6414,13 @@ var branchWorktree = async (payload, timeoutMs, signal) => {
5351
6414
  timeoutMs,
5352
6415
  signal
5353
6416
  ) : input.candidates[0];
5354
- const targetFolder = input.action === "CREATE" ? join5(
6417
+ const targetFolder = input.action === "CREATE" ? join6(
5355
6418
  dirname5(rootFolder),
5356
- `${basename2(rootFolder)}-${branch.replaceAll("/", "-")}`
6419
+ `${basename3(rootFolder)}-${branch.replaceAll("/", "-")}`
5357
6420
  ) : changeFolder;
5358
6421
  if (input.action === "CREATE") {
5359
6422
  try {
5360
- await stat4(targetFolder);
6423
+ await stat5(targetFolder);
5361
6424
  throw new Error(`Worktree folder already exists: ${targetFolder}`);
5362
6425
  } catch (error) {
5363
6426
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
@@ -5591,12 +6654,12 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
5591
6654
  `Branch ${input.branch} is already checked out in ${occupiedFolder}`
5592
6655
  );
5593
6656
  }
5594
- targetFolder = join5(
6657
+ targetFolder = join6(
5595
6658
  dirname5(rootFolder),
5596
- `${basename2(rootFolder)}-${input.branch.replaceAll("/", "-")}`
6659
+ `${basename3(rootFolder)}-${input.branch.replaceAll("/", "-")}`
5597
6660
  );
5598
6661
  try {
5599
- await stat4(targetFolder);
6662
+ await stat5(targetFolder);
5600
6663
  throw new Error(`Worktree folder already exists: ${targetFolder}`);
5601
6664
  } catch (error) {
5602
6665
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
@@ -5757,7 +6820,7 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
5757
6820
  var deleteWorktree = async (payload, timeoutMs, signal) => {
5758
6821
  const input = worktreeDeleteJobPayload(payload);
5759
6822
  const rootFolder = await realpath4(input.rootFolder);
5760
- if (!(await stat4(rootFolder)).isDirectory()) {
6823
+ if (!(await stat5(rootFolder)).isDirectory()) {
5761
6824
  throw new Error("Base repository is missing");
5762
6825
  }
5763
6826
  if (await origin(rootFolder, timeoutMs, signal) !== input.expectedOrigin) {
@@ -5951,30 +7014,33 @@ var operateWorktree = async (payload, timeoutMs, signal) => {
5951
7014
  };
5952
7015
 
5953
7016
  // src/handlers/builds.ts
5954
- import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
7017
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
5955
7018
  import {
5956
7019
  cp,
5957
7020
  mkdir as mkdir3,
5958
- mkdtemp,
5959
- readdir as readdir4,
7021
+ mkdtemp as mkdtemp2,
7022
+ open as open2,
7023
+ readdir as readdir5,
5960
7024
  readFile as readFile4,
5961
7025
  realpath as realpath5,
5962
- rm as rm3,
5963
- stat as stat5,
7026
+ rename as rename3,
7027
+ rm as rm4,
7028
+ stat as stat6,
5964
7029
  writeFile as writeFile3
5965
7030
  } from "node:fs/promises";
5966
- import { createWriteStream } from "node:fs";
5967
- import { tmpdir } from "node:os";
7031
+ import { createReadStream as createReadStream3, createWriteStream as createWriteStream2 } from "node:fs";
7032
+ import { tmpdir as tmpdir2 } from "node:os";
7033
+ import { pipeline } from "node:stream/promises";
5968
7034
  import {
5969
- basename as basename3,
7035
+ basename as basename4,
5970
7036
  dirname as dirname6,
5971
7037
  isAbsolute as isAbsolute4,
5972
- join as join6,
5973
- relative as relative4,
7038
+ join as join7,
7039
+ relative as relative5,
5974
7040
  resolve as resolve5,
5975
- sep as sep3
7041
+ sep as sep4
5976
7042
  } from "node:path";
5977
- import { spawn as spawn4 } from "node:child_process";
7043
+ import { spawn as spawn5 } from "node:child_process";
5978
7044
  var successfulProcess5 = {
5979
7045
  exitCode: 0,
5980
7046
  signal: null,
@@ -6005,6 +7071,22 @@ function command2(executable, args, timeoutMs, signal, cwd, env = process.env) {
6005
7071
  env
6006
7072
  });
6007
7073
  }
7074
+ async function commandToFile(executable, args, timeoutMs, signal, cwd, env, destination) {
7075
+ const output = await open2(destination, "wx", 384);
7076
+ try {
7077
+ return await captureCommand({
7078
+ command: executable,
7079
+ args,
7080
+ timeoutMs,
7081
+ signal,
7082
+ cwd,
7083
+ env,
7084
+ stdoutFileDescriptor: output.fd
7085
+ });
7086
+ } finally {
7087
+ await output.close();
7088
+ }
7089
+ }
6008
7090
  function requireSuccess3(result, fallback) {
6009
7091
  if (result.cancelled) throw new Error("Operation was cancelled");
6010
7092
  if (result.timedOut) throw new Error("Operation timed out");
@@ -6015,7 +7097,7 @@ function requireSuccess3(result, fallback) {
6015
7097
  }
6016
7098
  async function validateWorktree2(input, timeoutMs, signal) {
6017
7099
  const folder = await realpath5(input.folder);
6018
- if (!(await stat5(folder)).isDirectory())
7100
+ if (!(await stat6(folder)).isDirectory())
6019
7101
  throw new Error("Worktree is missing");
6020
7102
  const gitDirectory2 = requireSuccess3(
6021
7103
  await command2(
@@ -6049,8 +7131,8 @@ async function validateWorktree2(input, timeoutMs, signal) {
6049
7131
  }
6050
7132
  function containedPath2(root, relativePath) {
6051
7133
  const target2 = resolve5(root, relativePath);
6052
- const difference = relative4(root, target2);
6053
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7134
+ const difference = relative5(root, target2);
7135
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6054
7136
  throw new Error("Path must stay within the worktree");
6055
7137
  }
6056
7138
  return target2;
@@ -6058,11 +7140,11 @@ function containedPath2(root, relativePath) {
6058
7140
  async function validateSource(root, source) {
6059
7141
  const target2 = containedPath2(root, source.relativePath);
6060
7142
  const resolved = await realpath5(target2);
6061
- const difference = relative4(root, resolved);
6062
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7143
+ const difference = relative5(root, resolved);
7144
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6063
7145
  throw new Error("Build source resolves outside the worktree");
6064
7146
  }
6065
- const information = await stat5(resolved);
7147
+ const information = await stat6(resolved);
6066
7148
  if (source.kind === "PACKAGE" && !information.isFile()) {
6067
7149
  throw new Error("Package.swift is missing");
6068
7150
  }
@@ -6076,10 +7158,10 @@ async function discoverSourcesInFolder(folder) {
6076
7158
  const queue = [folder];
6077
7159
  while (queue.length && sources.length < DISCOVERY_LIMIT) {
6078
7160
  const current = queue.shift();
6079
- for (const entry of await readdir4(current, { withFileTypes: true })) {
7161
+ for (const entry of await readdir5(current, { withFileTypes: true })) {
6080
7162
  if (entry.isSymbolicLink()) continue;
6081
- const absolute = join6(current, entry.name);
6082
- const path = relative4(folder, absolute).split(sep3).join("/");
7163
+ const absolute = join7(current, entry.name);
7164
+ const path = relative5(folder, absolute).split(sep4).join("/");
6083
7165
  if (entry.isDirectory()) {
6084
7166
  if (entry.name.endsWith(".xcodeproj")) {
6085
7167
  sources.push({ kind: "PROJECT", relativePath: path });
@@ -6149,7 +7231,7 @@ function metadataFromList(value) {
6149
7231
  };
6150
7232
  }
6151
7233
  async function workspaceProjectPaths(workspacePath2, folder) {
6152
- const contentsPath = join6(workspacePath2, "contents.xcworkspacedata");
7234
+ const contentsPath = join7(workspacePath2, "contents.xcworkspacedata");
6153
7235
  let contents;
6154
7236
  try {
6155
7237
  contents = await readFile4(contentsPath, "utf8");
@@ -6171,14 +7253,14 @@ async function workspaceProjectPaths(workspacePath2, folder) {
6171
7253
  const projects = [];
6172
7254
  for (const location of locations) {
6173
7255
  const candidate = resolve5(dirname6(workspacePath2), location);
6174
- const difference = relative4(folder, candidate);
6175
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7256
+ const difference = relative5(folder, candidate);
7257
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6176
7258
  continue;
6177
7259
  }
6178
7260
  try {
6179
7261
  const resolved = await realpath5(candidate);
6180
- const resolvedDifference = relative4(folder, resolved);
6181
- if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${sep3}`) || isAbsolute4(resolvedDifference) || !(await stat5(resolved)).isDirectory()) {
7262
+ const resolvedDifference = relative5(folder, resolved);
7263
+ if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${sep4}`) || isAbsolute4(resolvedDifference) || !(await stat6(resolved)).isDirectory()) {
6182
7264
  continue;
6183
7265
  }
6184
7266
  projects.push(resolved);
@@ -6393,8 +7475,8 @@ function genericBuildDestinations(action) {
6393
7475
  ];
6394
7476
  }
6395
7477
  async function listPhysicalDevices(timeoutMs, signal) {
6396
- const directory = await mkdtemp(join6(tmpdir(), "ade-devices-"));
6397
- const output = join6(directory, "devices.json");
7478
+ const directory = await mkdtemp2(join7(tmpdir2(), "ade-devices-"));
7479
+ const output = join7(directory, "devices.json");
6398
7480
  try {
6399
7481
  const result = await command2(
6400
7482
  "xcrun",
@@ -6414,7 +7496,7 @@ async function listPhysicalDevices(timeoutMs, signal) {
6414
7496
  if (result.exitCode !== 0) return [];
6415
7497
  return physicalDestinations(JSON.parse(await readFile4(output, "utf8")));
6416
7498
  } finally {
6417
- await rm3(directory, { recursive: true, force: true });
7499
+ await rm4(directory, { recursive: true, force: true });
6418
7500
  }
6419
7501
  }
6420
7502
  var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
@@ -6505,7 +7587,7 @@ var BuildLogger = class {
6505
7587
  constructor(buildId, context, rawLogPath, env) {
6506
7588
  this.buildId = buildId;
6507
7589
  this.context = context;
6508
- this.stream = createWriteStream(rawLogPath, { flags: "a", mode: 384 });
7590
+ this.stream = createWriteStream2(rawLogPath, { flags: "a", mode: 384 });
6509
7591
  this.redact = createRedactor(env);
6510
7592
  }
6511
7593
  buildId;
@@ -6595,12 +7677,12 @@ function runLoggedCommand(options) {
6595
7677
  let killTimer = null;
6596
7678
  let extraStream;
6597
7679
  if (options.additionalLogPath) {
6598
- extraStream = createWriteStream(options.additionalLogPath, {
7680
+ extraStream = createWriteStream2(options.additionalLogPath, {
6599
7681
  flags: "a",
6600
7682
  mode: 384
6601
7683
  });
6602
7684
  }
6603
- const child = spawn4(options.command, options.args, {
7685
+ const child = spawn5(options.command, options.args, {
6604
7686
  cwd: options.cwd,
6605
7687
  env: options.env,
6606
7688
  shell: false,
@@ -6792,7 +7874,7 @@ function actionArgument(action) {
6792
7874
  }[action];
6793
7875
  }
6794
7876
  function xcodeBuildArguments(input) {
6795
- const resultBundle = join6(input.artifactDirectory, "result.xcresult");
7877
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
6796
7878
  const sourcePath = containedPath2(input.folder, input.source.relativePath);
6797
7879
  const usesCapturedTestProducts = input.action === "TEST_WITHOUT_BUILDING";
6798
7880
  const args = [
@@ -6814,13 +7896,13 @@ function xcodeBuildArguments(input) {
6814
7896
  if (input.action === "ARCHIVE") {
6815
7897
  args.push(
6816
7898
  "-archivePath",
6817
- join6(input.artifactDirectory, "archive.xcarchive")
7899
+ join7(input.artifactDirectory, "archive.xcarchive")
6818
7900
  );
6819
7901
  }
6820
7902
  if (input.action === "BUILD_FOR_TESTING") {
6821
7903
  args.push(
6822
7904
  "-testProductsPath",
6823
- join6(input.artifactDirectory, "test-products.xctestproducts")
7905
+ join7(input.artifactDirectory, "test-products.xctestproducts")
6824
7906
  );
6825
7907
  }
6826
7908
  if (input.action === "TEST_WITHOUT_BUILDING" && input.advancedSettings.priorTestProductsPath) {
@@ -6883,15 +7965,15 @@ function classifyFailure(output) {
6883
7965
  }
6884
7966
  async function runHook(options) {
6885
7967
  const phaseDirectory = options.phase === "PRE_BUILD" ? "pre" : "post";
6886
- const directory = join6(
7968
+ const directory = join7(
6887
7969
  options.input.artifactDirectory,
6888
7970
  "hooks",
6889
7971
  phaseDirectory
6890
7972
  );
6891
7973
  const prefix = `${String(options.script.position).padStart(3, "0")}-${options.script.id}`;
6892
- const file = join6(directory, `${prefix}.mjs`);
6893
- const runner = join6(directory, `${prefix}.runner.mjs`);
6894
- const log = join6(directory, `${prefix}.log`);
7974
+ const file = join7(directory, `${prefix}.mjs`);
7975
+ const runner = join7(directory, `${prefix}.runner.mjs`);
7976
+ const log = join7(directory, `${prefix}.log`);
6895
7977
  const started = Date.now();
6896
7978
  try {
6897
7979
  await mkdir3(directory, { recursive: true, mode: 448 });
@@ -6906,7 +7988,7 @@ async function runHook(options) {
6906
7988
  await writeFile3(
6907
7989
  runner,
6908
7990
  `import { readFile } from "node:fs/promises";
6909
- const hookModule = await import(${JSON.stringify(`./${basename3(file)}`)});
7991
+ const hookModule = await import(${JSON.stringify(`./${basename4(file)}`)});
6910
7992
  if (hookModule.default !== undefined && typeof hookModule.default !== "function") {
6911
7993
  throw new TypeError("The default build hook export must be a function");
6912
7994
  }
@@ -6943,7 +8025,7 @@ if (typeof hookModule.default === "function") {
6943
8025
  exitCode: result.exitCode,
6944
8026
  durationMs: Date.now() - started,
6945
8027
  causedBuildFailure: failed && options.script.failureBehavior === "FAIL_BUILD",
6946
- outputRelativePath: relative4(options.input.artifactDirectory, log),
8028
+ outputRelativePath: relative5(options.input.artifactDirectory, log),
6947
8029
  error: failed ? cleanError3(result.output || "Build hook failed") : null
6948
8030
  };
6949
8031
  } catch (hookError) {
@@ -6956,49 +8038,466 @@ if (typeof hookModule.default === "function") {
6956
8038
  exitCode: null,
6957
8039
  durationMs: Date.now() - started,
6958
8040
  causedBuildFailure: !options.signal.aborted && options.script.failureBehavior === "FAIL_BUILD",
6959
- outputRelativePath: relative4(options.input.artifactDirectory, log),
8041
+ outputRelativePath: relative5(options.input.artifactDirectory, log),
6960
8042
  error
6961
8043
  };
6962
8044
  }
6963
8045
  }
6964
8046
  async function pathExists(path) {
6965
8047
  try {
6966
- await stat5(path);
8048
+ await stat6(path);
6967
8049
  return true;
6968
8050
  } catch {
6969
8051
  return false;
6970
8052
  }
6971
8053
  }
6972
8054
  async function pathSize(path) {
6973
- const information = await stat5(path);
8055
+ const information = await stat6(path);
6974
8056
  if (information.isFile()) return information.size;
6975
8057
  if (!information.isDirectory()) return 0;
6976
8058
  let size = 0;
6977
- for (const entry of await readdir4(path, { withFileTypes: true })) {
8059
+ for (const entry of await readdir5(path, { withFileTypes: true })) {
6978
8060
  if (entry.isSymbolicLink()) continue;
6979
- size += await pathSize(join6(path, entry.name));
8061
+ size += await pathSize(join7(path, entry.name));
6980
8062
  }
6981
8063
  return size;
6982
8064
  }
6983
8065
  async function fileChecksum(path) {
6984
8066
  try {
6985
- const information = await stat5(path);
6986
- if (!information.isFile() || information.size > 256 * 1024 * 1024)
6987
- return null;
6988
- return createHash3("sha256").update(await readFile4(path)).digest("hex");
8067
+ const information = await stat6(path);
8068
+ if (!information.isFile()) return null;
8069
+ const hash = createHash4("sha256");
8070
+ await pipeline(createReadStream3(path), hash);
8071
+ return hash.digest("hex");
8072
+ } catch {
8073
+ return null;
8074
+ }
8075
+ }
8076
+ async function plistString(plistPath, keyPath, timeoutMs, signal) {
8077
+ try {
8078
+ const result = await command2(
8079
+ "/usr/bin/plutil",
8080
+ ["-extract", keyPath, "raw", "-o", "-", plistPath],
8081
+ Math.min(timeoutMs, 5e3),
8082
+ signal
8083
+ );
8084
+ const value = result.stdout.trim();
8085
+ return result.exitCode === 0 && value ? value : null;
6989
8086
  } catch {
6990
8087
  return null;
6991
8088
  }
6992
8089
  }
8090
+ async function archiveApplicationProperties(archivePath, timeoutMs, signal) {
8091
+ const plistPath = join7(archivePath, "Info.plist");
8092
+ const read = (key) => plistString(plistPath, `ApplicationProperties.${key}`, timeoutMs, signal);
8093
+ const [bundleIdentifier2, bundleShortVersion, bundleVersion, applicationPath] = await Promise.all([
8094
+ read("CFBundleIdentifier"),
8095
+ read("CFBundleShortVersionString"),
8096
+ read("CFBundleVersion"),
8097
+ read("ApplicationPath")
8098
+ ]);
8099
+ return {
8100
+ bundleIdentifier: bundleIdentifier2,
8101
+ bundleShortVersion,
8102
+ bundleVersion,
8103
+ applicationName: applicationPath ? basename4(applicationPath).replace(/\.app$/, "") : null
8104
+ };
8105
+ }
6993
8106
  async function artifact(root, kind, path, metadata = {}) {
6994
8107
  return {
6995
8108
  kind,
6996
- relativePath: relative4(root, path).split(sep3).join("/"),
8109
+ relativePath: relative5(root, path).split(sep4).join("/"),
6997
8110
  sizeBytes: await pathSize(path),
6998
8111
  checksum: await fileChecksum(path),
6999
8112
  metadata
7000
8113
  };
7001
8114
  }
8115
+ function jsonObject(value) {
8116
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
8117
+ }
8118
+ function testDetails(node) {
8119
+ 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] : [];
8120
+ const children = Array.isArray(node.children) ? node.children : [];
8121
+ return [
8122
+ ...own,
8123
+ ...children.flatMap((child) => {
8124
+ const object = jsonObject(child);
8125
+ return object ? testDetails(object) : [];
8126
+ })
8127
+ ];
8128
+ }
8129
+ function testSourceFile(node, suite) {
8130
+ const explicit = [
8131
+ node.filePath,
8132
+ node.sourceFilePath,
8133
+ node.fileName,
8134
+ node.sourceFile
8135
+ ].find((value) => typeof value === "string" && !!value);
8136
+ if (explicit) {
8137
+ return { file: basename4(explicit), filePath: explicit };
8138
+ }
8139
+ const identifier = typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : "";
8140
+ const identifierOwner = identifier.split("/")[0]?.trim();
8141
+ return {
8142
+ file: identifierOwner || suite,
8143
+ filePath: null
8144
+ };
8145
+ }
8146
+ function normalizeTestResults(value) {
8147
+ const root = jsonObject(value);
8148
+ if (!root || !Array.isArray(root.testNodes)) {
8149
+ throw new Error("xcresulttool returned invalid test results JSON");
8150
+ }
8151
+ const tests = [];
8152
+ const visit = (raw, context) => {
8153
+ const node = jsonObject(raw);
8154
+ if (!node) return;
8155
+ const nodeType = typeof node.nodeType === "string" ? node.nodeType : "";
8156
+ const name = typeof node.name === "string" ? node.name : "";
8157
+ const next = {
8158
+ plan: nodeType === "Test Plan" ? name : context.plan,
8159
+ configuration: nodeType === "Test Plan Configuration" ? name : context.configuration,
8160
+ bundle: /test bundle$/.test(nodeType) ? name : context.bundle,
8161
+ suite: nodeType === "Test Suite" ? name : context.suite
8162
+ };
8163
+ if (nodeType === "Test Case") {
8164
+ const result = typeof node.result === "string" ? node.result : "unknown";
8165
+ const sourceFile = testSourceFile(node, next.suite);
8166
+ tests.push({
8167
+ identifier: typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : name,
8168
+ name,
8169
+ ...next,
8170
+ ...sourceFile,
8171
+ result,
8172
+ durationSeconds: typeof node.durationInSeconds === "number" ? node.durationInSeconds : null,
8173
+ tags: Array.isArray(node.tags) ? node.tags.filter((tag) => typeof tag === "string") : [],
8174
+ details: [...new Set(testDetails(node))]
8175
+ });
8176
+ }
8177
+ if (Array.isArray(node.children)) {
8178
+ for (const child of node.children) visit(child, next);
8179
+ }
8180
+ };
8181
+ for (const node of root.testNodes) {
8182
+ visit(node, {
8183
+ plan: null,
8184
+ configuration: null,
8185
+ bundle: null,
8186
+ suite: null
8187
+ });
8188
+ }
8189
+ const count = (result) => tests.filter((test) => test.result === result).length;
8190
+ return {
8191
+ summary: {
8192
+ total: tests.length,
8193
+ passed: count("Passed"),
8194
+ failed: count("Failed"),
8195
+ skipped: count("Skipped"),
8196
+ expectedFailures: count("Expected Failure"),
8197
+ unknown: tests.filter(
8198
+ (test) => !["Passed", "Failed", "Skipped", "Expected Failure"].includes(
8199
+ String(test.result)
8200
+ )
8201
+ ).length,
8202
+ durationSeconds: tests.reduce(
8203
+ (total, test) => total + Number(test.durationSeconds ?? 0),
8204
+ 0
8205
+ )
8206
+ },
8207
+ data: {
8208
+ devices: Array.isArray(root.devices) ? root.devices : [],
8209
+ configurations: Array.isArray(root.testPlanConfigurations) ? root.testPlanConfigurations : [],
8210
+ tests
8211
+ }
8212
+ };
8213
+ }
8214
+ function normalizeCoverage(value) {
8215
+ const root = jsonObject(value);
8216
+ if (!root || !Array.isArray(root.targets)) {
8217
+ throw new Error("xccov returned invalid coverage JSON");
8218
+ }
8219
+ const files = root.targets.flatMap((rawTarget) => {
8220
+ const target2 = jsonObject(rawTarget);
8221
+ if (!target2 || !Array.isArray(target2.files)) return [];
8222
+ const targetName = typeof target2.name === "string" ? target2.name : "";
8223
+ return target2.files.flatMap((rawFile) => {
8224
+ const file = jsonObject(rawFile);
8225
+ if (!file) return [];
8226
+ return [
8227
+ {
8228
+ target: targetName,
8229
+ name: typeof file.name === "string" ? file.name : "",
8230
+ path: typeof file.path === "string" ? file.path : "",
8231
+ coveredLines: typeof file.coveredLines === "number" ? file.coveredLines : 0,
8232
+ executableLines: typeof file.executableLines === "number" ? file.executableLines : 0,
8233
+ lineCoverage: typeof file.lineCoverage === "number" ? file.lineCoverage : 0
8234
+ }
8235
+ ];
8236
+ });
8237
+ });
8238
+ return {
8239
+ summary: {
8240
+ coveredLines: typeof root.coveredLines === "number" ? root.coveredLines : 0,
8241
+ executableLines: typeof root.executableLines === "number" ? root.executableLines : 0,
8242
+ lineCoverage: typeof root.lineCoverage === "number" ? root.lineCoverage : 0,
8243
+ targetCount: root.targets.length,
8244
+ fileCount: files.length
8245
+ },
8246
+ data: { files }
8247
+ };
8248
+ }
8249
+ async function writeJsonReport(input, kind, timeoutMs, signal) {
8250
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
8251
+ const filename = kind === "TEST_RESULTS" ? "test-results.json" : "code-coverage.json";
8252
+ const destination = join7(input.artifactDirectory, filename);
8253
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
8254
+ try {
8255
+ if (!await pathExists(resultBundle)) {
8256
+ throw new Error("The build result bundle is unavailable");
8257
+ }
8258
+ const args = kind === "TEST_RESULTS" ? [
8259
+ "xcresulttool",
8260
+ "get",
8261
+ "test-results",
8262
+ "tests",
8263
+ "--path",
8264
+ "result.xcresult",
8265
+ "--format",
8266
+ "json"
8267
+ ] : ["xccov", "view", "--report", "--json", "result.xcresult"];
8268
+ requireSuccess3(
8269
+ await commandToFile(
8270
+ "xcrun",
8271
+ args,
8272
+ timeoutMs,
8273
+ signal,
8274
+ input.artifactDirectory,
8275
+ xcodeEnvironment(),
8276
+ temporary
8277
+ ),
8278
+ `Could not generate ${filename}`
8279
+ );
8280
+ const serialized = await readFile4(temporary, "utf8");
8281
+ const parsed = JSON.parse(serialized);
8282
+ const normalized = kind === "TEST_RESULTS" ? normalizeTestResults(parsed) : normalizeCoverage(parsed);
8283
+ await rename3(temporary, destination);
8284
+ return {
8285
+ kind,
8286
+ status: "READY",
8287
+ artifact: await artifact(
8288
+ input.artifactDirectory,
8289
+ kind === "TEST_RESULTS" ? "TEST_RESULTS_JSON" : "CODE_COVERAGE_JSON",
8290
+ destination
8291
+ ),
8292
+ ...normalized,
8293
+ error: null
8294
+ };
8295
+ } catch (error) {
8296
+ await rm4(temporary, { force: true });
8297
+ return {
8298
+ kind,
8299
+ status: "FAILED",
8300
+ artifact: null,
8301
+ summary: {},
8302
+ data: {},
8303
+ error: cleanError3(error)
8304
+ };
8305
+ }
8306
+ }
8307
+ async function gitCommand(folder, args, timeoutMs, signal) {
8308
+ return command2("git", ["-C", folder, ...args], timeoutMs, signal, void 0, {
8309
+ ...process.env,
8310
+ GIT_TERMINAL_PROMPT: "0",
8311
+ GIT_OPTIONAL_LOCKS: "0"
8312
+ });
8313
+ }
8314
+ function changedLinesFromPatch(value) {
8315
+ const result = /* @__PURE__ */ new Map();
8316
+ let path = null;
8317
+ for (const line of value.split("\n")) {
8318
+ if (line.startsWith("+++ ")) {
8319
+ const raw = line.slice(4);
8320
+ path = raw === "/dev/null" ? null : raw.replace(/^b\//, "");
8321
+ if (path && !result.has(path)) result.set(path, /* @__PURE__ */ new Set());
8322
+ continue;
8323
+ }
8324
+ if (!path || !line.startsWith("@@")) continue;
8325
+ const match = /\+(\d+)(?:,(\d+))?/.exec(line);
8326
+ if (!match) continue;
8327
+ const start = Number(match[1]);
8328
+ const count = match[2] === void 0 ? 1 : Number(match[2]);
8329
+ const lines = result.get(path);
8330
+ for (let number = start; number < start + count; number += 1) {
8331
+ lines.add(number);
8332
+ }
8333
+ }
8334
+ return result;
8335
+ }
8336
+ function changeTypesFromStatus(value) {
8337
+ const entries = value.split("\0").filter(Boolean);
8338
+ const result = /* @__PURE__ */ new Map();
8339
+ for (let index = 0; index < entries.length; ) {
8340
+ const status2 = entries[index++] ?? "M";
8341
+ const renamed = status2.startsWith("R") || status2.startsWith("C");
8342
+ const first = entries[index++] ?? "";
8343
+ const second = renamed ? entries[index++] ?? "" : null;
8344
+ const path = second ?? first;
8345
+ if (path) result.set(path, status2[0] ?? "M");
8346
+ }
8347
+ return result;
8348
+ }
8349
+ async function snapshotCoverageChanges(input, folder, timeoutMs, signal) {
8350
+ if (!input.baseBranch) throw new Error("A base branch is required");
8351
+ const base = requireSuccess3(
8352
+ await gitCommand(
8353
+ folder,
8354
+ ["merge-base", `refs/remotes/origin/${input.baseBranch}`, "HEAD"],
8355
+ timeoutMs,
8356
+ signal
8357
+ ),
8358
+ "Could not determine the coverage merge base"
8359
+ ).stdout.trim();
8360
+ const [patch, status2, untracked] = await Promise.all([
8361
+ gitCommand(
8362
+ folder,
8363
+ ["diff", "--no-color", "--unified=0", base, "--"],
8364
+ timeoutMs,
8365
+ signal
8366
+ ),
8367
+ gitCommand(
8368
+ folder,
8369
+ ["diff", "--name-status", "-z", base, "--"],
8370
+ timeoutMs,
8371
+ signal
8372
+ ),
8373
+ gitCommand(
8374
+ folder,
8375
+ ["ls-files", "--others", "--exclude-standard", "-z"],
8376
+ timeoutMs,
8377
+ signal
8378
+ )
8379
+ ]);
8380
+ requireSuccess3(patch, "Could not inspect changed coverage lines");
8381
+ requireSuccess3(status2, "Could not inspect coverage change types");
8382
+ requireSuccess3(untracked, "Could not inspect untracked coverage files");
8383
+ const lines = changedLinesFromPatch(patch.stdout);
8384
+ const types = changeTypesFromStatus(status2.stdout);
8385
+ for (const path of untracked.stdout.split("\0").filter(Boolean)) {
8386
+ try {
8387
+ const contents = await readFile4(join7(folder, path));
8388
+ if (contents.includes(0)) continue;
8389
+ const lineCount = contents.length ? contents.toString("utf8").split("\n").length - (contents.toString("utf8").endsWith("\n") ? 1 : 0) : 0;
8390
+ lines.set(
8391
+ path,
8392
+ new Set(Array.from({ length: lineCount }, (_, index) => index + 1))
8393
+ );
8394
+ types.set(path, "A");
8395
+ } catch {
8396
+ }
8397
+ }
8398
+ return [.../* @__PURE__ */ new Set([...lines.keys(), ...types.keys()])].map((path) => ({
8399
+ path,
8400
+ changeType: types.get(path) ?? "M",
8401
+ lines: [...lines.get(path) ?? []].sort((a, b) => a - b)
8402
+ }));
8403
+ }
8404
+ async function addChangedCoverage(report, changes, input, folder, timeoutMs, signal) {
8405
+ if (report.status !== "READY") return report;
8406
+ const rawFiles = Array.isArray(report.data.files) ? report.data.files : [];
8407
+ const coveragePaths = rawFiles.flatMap((raw) => {
8408
+ const file = jsonObject(raw);
8409
+ return file && typeof file.path === "string" ? [file.path] : [];
8410
+ });
8411
+ const changedFiles = [];
8412
+ let changedCoveredLines = 0;
8413
+ let changedExecutableLines = 0;
8414
+ for (const change of changes) {
8415
+ const coveragePath = coveragePaths.find(
8416
+ (candidate) => relative5(folder, candidate).split(sep4).join("/") === change.path
8417
+ );
8418
+ let covered = 0;
8419
+ let executable = 0;
8420
+ if (coveragePath && change.lines.length) {
8421
+ const temporary2 = join7(
8422
+ input.artifactDirectory,
8423
+ `.changed-coverage-${randomUUID2()}.json`
8424
+ );
8425
+ try {
8426
+ const result = await commandToFile(
8427
+ "xcrun",
8428
+ [
8429
+ "xccov",
8430
+ "view",
8431
+ "--archive",
8432
+ "--file",
8433
+ coveragePath,
8434
+ "--json",
8435
+ "result.xcresult"
8436
+ ],
8437
+ timeoutMs,
8438
+ signal,
8439
+ input.artifactDirectory,
8440
+ xcodeEnvironment(),
8441
+ temporary2
8442
+ );
8443
+ if (result.exitCode === 0 && !result.cancelled && !result.timedOut) {
8444
+ const parsed = jsonObject(
8445
+ JSON.parse(await readFile4(temporary2, "utf8"))
8446
+ );
8447
+ const lineData = parsed?.[coveragePath];
8448
+ if (Array.isArray(lineData)) {
8449
+ const changed = new Set(change.lines);
8450
+ for (const rawLine of lineData) {
8451
+ const line = jsonObject(rawLine);
8452
+ if (line?.isExecutable === true && typeof line.line === "number" && changed.has(line.line)) {
8453
+ executable += 1;
8454
+ if (typeof line.executionCount === "number" && line.executionCount > 0) {
8455
+ covered += 1;
8456
+ }
8457
+ }
8458
+ }
8459
+ }
8460
+ }
8461
+ } finally {
8462
+ await rm4(temporary2, { force: true });
8463
+ }
8464
+ }
8465
+ changedCoveredLines += covered;
8466
+ changedExecutableLines += executable;
8467
+ changedFiles.push({
8468
+ path: change.path,
8469
+ changeType: change.changeType,
8470
+ changedCoveredLines: covered,
8471
+ changedExecutableLines: executable,
8472
+ changedLineCoverage: executable ? covered / executable : null
8473
+ });
8474
+ }
8475
+ const summary = {
8476
+ ...report.summary,
8477
+ changedCoveredLines,
8478
+ changedExecutableLines,
8479
+ changedLineCoverage: changedExecutableLines ? changedCoveredLines / changedExecutableLines : null
8480
+ };
8481
+ const data = { ...report.data, changedFiles };
8482
+ const destination = join7(input.artifactDirectory, "worktree-coverage.json");
8483
+ const temporary = `${destination}.tmp-${randomUUID2()}`;
8484
+ await writeFile3(temporary, JSON.stringify({ summary, data }, null, 2), {
8485
+ mode: 384
8486
+ });
8487
+ await rename3(temporary, destination);
8488
+ return {
8489
+ ...report,
8490
+ summary,
8491
+ data,
8492
+ additionalArtifacts: [
8493
+ await artifact(
8494
+ input.artifactDirectory,
8495
+ "WORKTREE_COVERAGE_JSON",
8496
+ destination
8497
+ )
8498
+ ]
8499
+ };
8500
+ }
7002
8501
  function parseBuildSettings(value) {
7003
8502
  const parsed = JSON.parse(value);
7004
8503
  if (!Array.isArray(parsed)) return [];
@@ -7028,13 +8527,13 @@ async function findFiles(root, predicate, limit = 20) {
7028
8527
  const current = queue.shift();
7029
8528
  let entries;
7030
8529
  try {
7031
- entries = await readdir4(current, { withFileTypes: true });
8530
+ entries = await readdir5(current, { withFileTypes: true });
7032
8531
  } catch {
7033
8532
  continue;
7034
8533
  }
7035
8534
  for (const entry of entries) {
7036
8535
  if (entry.isSymbolicLink()) continue;
7037
- const path = join6(current, entry.name);
8536
+ const path = join7(current, entry.name);
7038
8537
  if (entry.isDirectory()) queue.push(path);
7039
8538
  else if (entry.isFile() && predicate(entry.name)) results.push(path);
7040
8539
  if (results.length >= limit) break;
@@ -7042,15 +8541,36 @@ async function findFiles(root, predicate, limit = 20) {
7042
8541
  }
7043
8542
  return results;
7044
8543
  }
7045
- async function captureArtifacts(input, folder, signal) {
8544
+ async function captureArtifacts(input, folder, signal, includeProducts = true) {
7046
8545
  const artifacts = [];
7047
- const resultBundle = join6(input.artifactDirectory, "result.xcresult");
8546
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
7048
8547
  if (await pathExists(resultBundle)) {
8548
+ const coverageProbe = await command2(
8549
+ "xcrun",
8550
+ [
8551
+ "xccov",
8552
+ "view",
8553
+ "--report",
8554
+ "--only-targets",
8555
+ "--json",
8556
+ "result.xcresult"
8557
+ ],
8558
+ 3e4,
8559
+ new AbortController().signal,
8560
+ input.artifactDirectory,
8561
+ xcodeEnvironment()
8562
+ );
7049
8563
  artifacts.push(
7050
- await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle)
8564
+ await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle, {
8565
+ testResultsAvailable: ["TEST", "TEST_WITHOUT_BUILDING"].includes(
8566
+ input.action
8567
+ ),
8568
+ coverageAvailable: coverageProbe.exitCode === 0
8569
+ })
7051
8570
  );
7052
8571
  }
7053
- const archivePath = join6(input.artifactDirectory, "archive.xcarchive");
8572
+ if (!includeProducts) return artifacts;
8573
+ const archivePath = join7(input.artifactDirectory, "archive.xcarchive");
7054
8574
  if (await pathExists(archivePath)) {
7055
8575
  artifacts.push(
7056
8576
  await artifact(input.artifactDirectory, "ARCHIVE", archivePath)
@@ -7073,12 +8593,12 @@ async function captureArtifacts(input, folder, signal) {
7073
8593
  if (apps.length === 1) {
7074
8594
  const settings = apps[0].buildSettings;
7075
8595
  const productName = settings.FULL_PRODUCT_NAME ?? settings.WRAPPER_NAME;
7076
- const source = settings.TARGET_BUILD_DIR && productName ? join6(settings.TARGET_BUILD_DIR, productName) : null;
8596
+ const source = settings.TARGET_BUILD_DIR && productName ? join7(settings.TARGET_BUILD_DIR, productName) : null;
7077
8597
  if (source && await pathExists(source)) {
7078
- const productDirectory = join6(input.artifactDirectory, "products");
7079
- const destination = join6(productDirectory, basename3(source));
8598
+ const productDirectory = join7(input.artifactDirectory, "products");
8599
+ const destination = join7(productDirectory, basename4(source));
7080
8600
  await mkdir3(productDirectory, { recursive: true, mode: 448 });
7081
- await rm3(destination, { recursive: true, force: true });
8601
+ await rm4(destination, { recursive: true, force: true });
7082
8602
  await cp(source, destination, {
7083
8603
  recursive: true,
7084
8604
  preserveTimestamps: true
@@ -7093,7 +8613,7 @@ async function captureArtifacts(input, folder, signal) {
7093
8613
  }
7094
8614
  }
7095
8615
  if (input.action === "BUILD_FOR_TESTING") {
7096
- const testDirectory = join6(
8616
+ const testDirectory = join7(
7097
8617
  input.artifactDirectory,
7098
8618
  "test-products.xctestproducts"
7099
8619
  );
@@ -7108,7 +8628,7 @@ async function captureArtifacts(input, folder, signal) {
7108
8628
  )) {
7109
8629
  artifacts.push(
7110
8630
  await artifact(input.artifactDirectory, "XCTESTRUN", file, {
7111
- testPlan: basename3(file, ".xctestrun")
8631
+ testPlan: basename4(file, ".xctestrun")
7112
8632
  })
7113
8633
  );
7114
8634
  }
@@ -7128,14 +8648,14 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7128
8648
  const testProductsPath = input.advancedSettings.priorTestProductsPath;
7129
8649
  const xctestrunPath = input.advancedSettings.priorXctestrunPath;
7130
8650
  if (testProductsPath) {
7131
- if (!testProductsPath.endsWith(".xctestproducts") || !await pathExists(testProductsPath) || !(await stat5(testProductsPath)).isDirectory()) {
8651
+ if (!testProductsPath.endsWith(".xctestproducts") || !await pathExists(testProductsPath) || !(await stat6(testProductsPath)).isDirectory()) {
7132
8652
  throw new Error("The captured test-products artifact is unavailable");
7133
8653
  }
7134
- } else if (!xctestrunPath?.endsWith(".xctestrun") || !await pathExists(xctestrunPath) || !(await stat5(xctestrunPath)).isFile()) {
8654
+ } else if (!xctestrunPath?.endsWith(".xctestrun") || !await pathExists(xctestrunPath) || !(await stat6(xctestrunPath)).isFile()) {
7135
8655
  throw new Error("The captured .xctestrun artifact is unavailable");
7136
8656
  }
7137
8657
  }
7138
- if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8658
+ if (!isAbsolute4(input.artifactDirectory) || basename4(input.artifactDirectory) !== input.buildId) {
7139
8659
  throw new Error("Build artifact directory is invalid");
7140
8660
  }
7141
8661
  const sourceStateHash = await worktreeCodeStateHash(
@@ -7144,11 +8664,11 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7144
8664
  signal
7145
8665
  );
7146
8666
  await mkdir3(input.artifactDirectory, { recursive: true, mode: 448 });
7147
- const rawLog = join6(input.artifactDirectory, "build.log");
8667
+ const rawLog = join7(input.artifactDirectory, "build.log");
7148
8668
  const logger = new BuildLogger(input.buildId, context, rawLog, process.env);
7149
8669
  try {
7150
8670
  const scriptExecutions = [];
7151
- const contextPath = join6(input.artifactDirectory, "context.json");
8671
+ const contextPath = join7(input.artifactDirectory, "context.json");
7152
8672
  const baseHookContext = {
7153
8673
  buildId: input.buildId,
7154
8674
  branch: input.branch ?? null,
@@ -7165,6 +8685,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7165
8685
  configuration: input.configuration
7166
8686
  };
7167
8687
  let buildResult = null;
8688
+ let coverageChanges = [];
7168
8689
  let errorCode = null;
7169
8690
  let error = null;
7170
8691
  let failBuild = false;
@@ -7222,6 +8743,18 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7222
8743
  } catch (progressError) {
7223
8744
  logger.emit("RUNNING", "SYSTEM", cleanError3(progressError));
7224
8745
  }
8746
+ if (input.worktreeCoverage) {
8747
+ try {
8748
+ coverageChanges = await snapshotCoverageChanges(
8749
+ input,
8750
+ folder,
8751
+ Math.min(timeoutMs, 6e4),
8752
+ signal
8753
+ );
8754
+ } catch (coverageError) {
8755
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
8756
+ }
8757
+ }
7225
8758
  const args2 = xcodeBuildArguments(input);
7226
8759
  buildResult = await runLoggedCommand({
7227
8760
  command: "xcrun",
@@ -7281,12 +8814,55 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7281
8814
  }
7282
8815
  }
7283
8816
  let artifacts = [];
7284
- if (!failBuild && !signal.aborted && buildResult?.exitCode === 0) {
8817
+ try {
8818
+ artifacts = await captureArtifacts(
8819
+ input,
8820
+ folder,
8821
+ new AbortController().signal,
8822
+ !failBuild && !signal.aborted && buildResult?.exitCode === 0
8823
+ );
8824
+ } catch (artifactError) {
8825
+ logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8826
+ }
8827
+ const reports = [];
8828
+ const testAction = ["TEST", "TEST_WITHOUT_BUILDING"].includes(input.action);
8829
+ const canGenerateReports = () => !signal.aborted && buildResult?.cancelled !== true && buildResult?.timedOut !== true;
8830
+ if (canGenerateReports() && testAction && input.advancedSettings.parseTestResults) {
8831
+ const report = await writeJsonReport(
8832
+ input,
8833
+ "TEST_RESULTS",
8834
+ Math.min(timeoutMs, 12e4),
8835
+ signal
8836
+ );
8837
+ reports.push(report);
8838
+ if (report.artifact) artifacts.push(report.artifact);
8839
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
8840
+ }
8841
+ if (canGenerateReports() && testAction && input.worktreeCoverage) {
8842
+ let report = await writeJsonReport(
8843
+ input,
8844
+ "CODE_COVERAGE",
8845
+ Math.min(timeoutMs, 12e4),
8846
+ signal
8847
+ );
7285
8848
  try {
7286
- artifacts = await captureArtifacts(input, folder, signal);
7287
- } catch (artifactError) {
7288
- logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
8849
+ report = await addChangedCoverage(
8850
+ report,
8851
+ coverageChanges,
8852
+ input,
8853
+ folder,
8854
+ Math.min(timeoutMs, 12e4),
8855
+ signal
8856
+ );
8857
+ } catch (coverageError) {
8858
+ logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
8859
+ }
8860
+ reports.push(report);
8861
+ if (report.artifact) artifacts.push(report.artifact);
8862
+ if (report.additionalArtifacts) {
8863
+ artifacts.push(...report.additionalArtifacts);
7289
8864
  }
8865
+ if (report.error) logger.emit("REPORTS", "STDERR", report.error);
7290
8866
  }
7291
8867
  await logger.close();
7292
8868
  if (await pathExists(rawLog)) {
@@ -7313,6 +8889,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7313
8889
  error,
7314
8890
  commandSummary: commandSummary("xcrun", args),
7315
8891
  artifacts,
8892
+ reports,
7316
8893
  scriptExecutions,
7317
8894
  sourceStateHash,
7318
8895
  finalStateHash,
@@ -7322,10 +8899,34 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
7322
8899
  await logger.close();
7323
8900
  }
7324
8901
  };
8902
+ var generateIosBuildReport = async (payload, timeoutMs, signal, onLog) => {
8903
+ const input = parseBuildReportPayload(payload);
8904
+ if (!isAbsolute4(input.artifactDirectory) || basename4(input.artifactDirectory) !== input.buildId) {
8905
+ throw new Error("Build artifact directory is invalid");
8906
+ }
8907
+ if (signal.aborted) return { ...successfulProcess5, cancelled: true };
8908
+ const report = await writeJsonReport(
8909
+ input,
8910
+ input.reportKind,
8911
+ Math.min(timeoutMs, 18e4),
8912
+ signal
8913
+ );
8914
+ await onLog({
8915
+ sequence: 0,
8916
+ stream: report.status === "READY" ? "SYSTEM" : "STDERR",
8917
+ message: report.status === "READY" ? `Generated ${report.kind.toLocaleLowerCase()} report` : report.error || "Report generation failed",
8918
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
8919
+ });
8920
+ return {
8921
+ ...successfulProcess5,
8922
+ report,
8923
+ artifacts: report.artifact ? [report.artifact] : []
8924
+ };
8925
+ };
7325
8926
  var deleteIosBuild = async (payload, _timeoutMs, signal, onLog) => {
7326
8927
  const input = parseBuildDeletePayload(payload);
7327
8928
  if (signal.aborted) return { ...successfulProcess5, cancelled: true };
7328
- await rm3(input.artifactDirectory, { recursive: true, force: true });
8929
+ await rm4(input.artifactDirectory, { recursive: true, force: true });
7329
8930
  await onLog({
7330
8931
  sequence: 0,
7331
8932
  stream: "SYSTEM",
@@ -7343,25 +8944,25 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7343
8944
  const target2 = await realpath5(
7344
8945
  containedPath2(root, input.artifactRelativePath)
7345
8946
  );
7346
- const difference = relative4(root, target2);
7347
- if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
8947
+ const difference = relative5(root, target2);
8948
+ if (!difference || difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
7348
8949
  throw new Error("Build artifact resolves outside the build folder");
7349
8950
  }
7350
- const information = await stat5(target2);
8951
+ const information = await stat6(target2);
7351
8952
  let uploadPath = target2;
7352
- let filename = basename3(target2);
8953
+ let filename = basename4(target2);
7353
8954
  let contentType = "application/octet-stream";
7354
8955
  let temporaryArchive = null;
7355
8956
  try {
7356
8957
  if (information.isDirectory()) {
7357
- temporaryArchive = join6(
7358
- tmpdir(),
8958
+ temporaryArchive = join7(
8959
+ tmpdir2(),
7359
8960
  `ade-build-artifact-${randomUUID2()}.tar.gz`
7360
8961
  );
7361
8962
  requireSuccess3(
7362
8963
  await command2(
7363
8964
  "tar",
7364
- ["-czf", temporaryArchive, "-C", dirname6(target2), basename3(target2)],
8965
+ ["-czf", temporaryArchive, "-C", dirname6(target2), basename4(target2)],
7365
8966
  timeoutMs,
7366
8967
  signal
7367
8968
  ),
@@ -7388,7 +8989,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
7388
8989
  return successfulProcess5;
7389
8990
  } finally {
7390
8991
  if (temporaryArchive) {
7391
- await rm3(temporaryArchive, { force: true });
8992
+ await rm4(temporaryArchive, { force: true });
7392
8993
  }
7393
8994
  }
7394
8995
  };
@@ -7424,24 +9025,24 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
7424
9025
  if (!await pathExists(appPath) || !appPath.endsWith(".app")) {
7425
9026
  throw new Error("Runnable app artifact is missing");
7426
9027
  }
7427
- const deploymentsDirectory = join6(input.artifactDirectory, "deployments");
9028
+ const deploymentsDirectory = join7(input.artifactDirectory, "deployments");
7428
9029
  await mkdir3(deploymentsDirectory, { recursive: true, mode: 448 });
7429
9030
  const logger = new BuildLogger(
7430
9031
  input.buildId,
7431
9032
  context,
7432
- join6(deploymentsDirectory, "deployments.log"),
9033
+ join7(deploymentsDirectory, "deployments.log"),
7433
9034
  process.env
7434
9035
  );
7435
9036
  try {
7436
9037
  const outcomes = [];
7437
9038
  for (const deployment of input.deployments) {
7438
- const directory = join6(
9039
+ const directory = join7(
7439
9040
  input.artifactDirectory,
7440
9041
  "deployments",
7441
9042
  deployment.id
7442
9043
  );
7443
9044
  await mkdir3(directory, { recursive: true, mode: 448 });
7444
- const logPath = join6(directory, "deployment.log");
9045
+ const logPath = join7(directory, "deployment.log");
7445
9046
  const started = Date.now();
7446
9047
  let failure = null;
7447
9048
  try {
@@ -7595,7 +9196,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
7595
9196
  status: signal.aborted ? "CANCELLED" : failure ? "FAILED" : "SUCCEEDED",
7596
9197
  error: failure,
7597
9198
  durationMs: Date.now() - started,
7598
- outputRelativePath: relative4(input.artifactDirectory, logPath)
9199
+ outputRelativePath: relative5(input.artifactDirectory, logPath)
7599
9200
  });
7600
9201
  if (signal.aborted) break;
7601
9202
  }
@@ -7611,17 +9212,6 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
7611
9212
  await logger.close();
7612
9213
  }
7613
9214
  };
7614
- function xml(value) {
7615
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
7616
- }
7617
- function plistValue(value) {
7618
- if (typeof value === "boolean") return value ? "<true/>" : "<false/>";
7619
- if (typeof value === "string") return `<string>${xml(value)}</string>`;
7620
- if (value && typeof value === "object" && !Array.isArray(value)) {
7621
- return `<dict>${Object.entries(value).map(([key, entry]) => `<key>${xml(key)}</key>${plistValue(entry)}`).join("")}</dict>`;
7622
- }
7623
- throw new Error("Unsupported export plist value");
7624
- }
7625
9215
  function exportPlist(settings) {
7626
9216
  const method = {
7627
9217
  DEBUGGING: "debugging",
@@ -7643,10 +9233,7 @@ function exportPlist(settings) {
7643
9233
  if (Object.keys(settings.provisioningProfiles).length) {
7644
9234
  values.provisioningProfiles = settings.provisioningProfiles;
7645
9235
  }
7646
- return `<?xml version="1.0" encoding="UTF-8"?>
7647
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7648
- <plist version="1.0">${plistValue(values)}</plist>
7649
- `;
9236
+ return plistDocument(values);
7650
9237
  }
7651
9238
  var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
7652
9239
  const input = parseBuildExportPayload(payload);
@@ -7662,14 +9249,14 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
7662
9249
  if (!await pathExists(archivePath) || !archivePath.endsWith(".xcarchive")) {
7663
9250
  throw new Error("Archive artifact is missing");
7664
9251
  }
7665
- const exportDirectory = join6(
9252
+ const exportDirectory = join7(
7666
9253
  input.artifactDirectory,
7667
9254
  "exports",
7668
9255
  input.exportId
7669
9256
  );
7670
9257
  await mkdir3(exportDirectory, { recursive: true, mode: 448 });
7671
- const plistPath = join6(exportDirectory, "ExportOptions.plist");
7672
- const logPath = join6(exportDirectory, "export.log");
9258
+ const plistPath = join7(exportDirectory, "ExportOptions.plist");
9259
+ const logPath = join7(exportDirectory, "export.log");
7673
9260
  await writeFile3(plistPath, exportPlist(input.settings), { mode: 384 });
7674
9261
  const logger = new BuildLogger(input.buildId, context, logPath, process.env);
7675
9262
  try {
@@ -7701,14 +9288,30 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
7701
9288
  signal: result.signal,
7702
9289
  timedOut: result.timedOut,
7703
9290
  cancelled: result.cancelled,
7704
- outputRelativePath: relative4(input.artifactDirectory, exportDirectory),
9291
+ outputRelativePath: relative5(input.artifactDirectory, exportDirectory),
7705
9292
  sizeBytes: result.exitCode === 0 ? await pathSize(exportDirectory) : null,
7706
- error: result.exitCode === 0 ? null : cleanError3(result.output)
9293
+ error: result.exitCode === 0 ? null : cleanError3(result.output),
9294
+ artifacts: result.exitCode === 0 ? await exportedArtifacts(input, exportDirectory, archivePath, signal) : []
7707
9295
  };
7708
9296
  } finally {
7709
9297
  await logger.close();
7710
9298
  }
7711
9299
  };
9300
+ async function exportedArtifacts(input, exportDirectory, archivePath, signal) {
9301
+ const [ipaPath] = await findFiles(
9302
+ exportDirectory,
9303
+ (name) => name.endsWith(".ipa"),
9304
+ 5
9305
+ );
9306
+ if (!ipaPath) return [];
9307
+ return [
9308
+ await artifact(input.artifactDirectory, "IPA", ipaPath, {
9309
+ exportId: input.exportId,
9310
+ exportMethod: input.settings.method,
9311
+ ...await archiveApplicationProperties(archivePath, 1e4, signal)
9312
+ })
9313
+ ];
9314
+ }
7712
9315
 
7713
9316
  // src/handlers/index.ts
7714
9317
  var handlers = {
@@ -7730,6 +9333,8 @@ var handlers = {
7730
9333
  [WORKTREE_DELETE_JOB_KIND]: deleteWorktree,
7731
9334
  [WORKTREE_OPERATION_JOB_KIND]: operateWorktree,
7732
9335
  [WORKTREE_WATCH_JOB_KIND]: watchWorktree,
9336
+ [WORKTREE_DIFF_JOB_KIND]: inspectWorktreeDiff,
9337
+ [WORKTREE_DIFF_ASSET_JOB_KIND]: downloadWorktreeDiffAsset,
7733
9338
  [SKILL_SCAN_JOB_KIND]: scanSkills,
7734
9339
  [SKILL_READ_JOB_KIND]: readSkills,
7735
9340
  [SKILL_APPLY_JOB_KIND]: applySkills,
@@ -7741,7 +9346,10 @@ var handlers = {
7741
9346
  [IOS_BUILD_DELETE_JOB_KIND]: deleteIosBuild,
7742
9347
  [IOS_ARTIFACT_DOWNLOAD_JOB_KIND]: downloadIosBuildArtifact,
7743
9348
  [IOS_DEPLOY_JOB_KIND]: deployIosBuild,
7744
- [IOS_EXPORT_JOB_KIND]: exportIosArchive
9349
+ [IOS_EXPORT_JOB_KIND]: exportIosArchive,
9350
+ [IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
9351
+ [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport,
9352
+ [IOS_SIGNING_INSPECT_JOB_KIND]: inspectIosSigning
7745
9353
  };
7746
9354
 
7747
9355
  // src/repository-coordinator.ts