@ai-development-environment/control-agent 0.0.30 → 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 +702 -222
  2. package/package.json +1 -1
@@ -2288,6 +2288,7 @@ var IOS_DEPLOY_JOB_KIND = "ios.build.deploy";
2288
2288
  var IOS_EXPORT_JOB_KIND = "ios.archive.export";
2289
2289
  var IOS_TEST_RESULTS_JOB_KIND = "ios.test-results.parse";
2290
2290
  var IOS_COVERAGE_REPORT_JOB_KIND = "ios.coverage.generate";
2291
+ var IOS_SIGNING_INSPECT_JOB_KIND = "ios.signing.inspect";
2291
2292
  var IOS_BUILD_JOB_KINDS = [
2292
2293
  IOS_SOURCE_DISCOVER_JOB_KIND,
2293
2294
  IOS_SOURCE_PARSE_JOB_KIND,
@@ -2299,7 +2300,8 @@ var IOS_BUILD_JOB_KINDS = [
2299
2300
  IOS_DEPLOY_JOB_KIND,
2300
2301
  IOS_EXPORT_JOB_KIND,
2301
2302
  IOS_TEST_RESULTS_JOB_KIND,
2302
- IOS_COVERAGE_REPORT_JOB_KIND
2303
+ IOS_COVERAGE_REPORT_JOB_KIND,
2304
+ IOS_SIGNING_INSPECT_JOB_KIND
2303
2305
  ];
2304
2306
  var BUILD_ACTIONS = [
2305
2307
  "BUILD",
@@ -2840,6 +2842,29 @@ function parseBuildExportPayload(value) {
2840
2842
  settings: parseBuildExportSettings(input.settings)
2841
2843
  };
2842
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
+ }
2843
2868
 
2844
2869
  // src/inventory.ts
2845
2870
  var AGENT_VERSION = "0.1.0";
@@ -2980,64 +3005,159 @@ function runCloudflared(payload, timeoutMs, signal, onLog) {
2980
3005
  });
2981
3006
  }
2982
3007
 
2983
- // src/handlers/ccusage.ts
2984
- import { Buffer as Buffer2 } from "node:buffer";
2985
- var MAX_CCUSAGE_STDOUT_BYTES = 16 * 1024 * 1024;
2986
- function validateCcusagePayload(payload) {
2987
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2988
- 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>`;
2989
3023
  }
2990
- const keys = Object.keys(payload);
2991
- if (keys.length > 0) {
2992
- 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>`;
2993
3026
  }
2994
- 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");
2995
3031
  }
2996
- async function runCcusage(payload, timeoutMs, signal, onLog) {
2997
- validateCcusagePayload(payload);
2998
- const stdout = [];
2999
- let stdoutBytes = 0;
3000
- let exceededLimit = false;
3001
- const result = await runProcess({
3002
- command: "ccusage",
3003
- args: ["--json"],
3004
- timeoutMs,
3005
- signal,
3006
- onLog: async (log) => {
3007
- if (log.stream !== "STDOUT") {
3008
- await onLog(log);
3009
- 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));
3010
3051
  }
3011
- const lineBytes = Buffer2.byteLength(log.message, "utf8") + 1;
3012
- if (stdoutBytes + lineBytes > MAX_CCUSAGE_STDOUT_BYTES) {
3013
- exceededLimit = true;
3014
- return;
3052
+ if (entity.startsWith("#")) {
3053
+ return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
3015
3054
  }
3016
- stdoutBytes += lineBytes;
3017
- 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];
3108
+ }
3109
+ }
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];
3018
3153
  }
3019
- });
3020
- if (result.exitCode !== 0 || result.cancelled || result.timedOut || result.signal !== null) {
3021
- return result;
3022
- }
3023
- if (exceededLimit) {
3024
- throw new Error(
3025
- `ccusage JSON exceeded the ${MAX_CCUSAGE_STDOUT_BYTES} byte limit`
3026
- );
3027
- }
3028
- let parsed;
3029
- try {
3030
- parsed = JSON.parse(stdout.join("\n"));
3031
- } catch {
3032
- throw new Error("ccusage returned malformed JSON");
3033
3154
  }
3034
- return { ...result, report: parseCcusageReport(parsed) };
3035
3155
  }
3036
-
3037
- // src/handlers/build-data.ts
3038
- import { lstat, opendir, readdir, realpath, rm } from "node:fs/promises";
3039
- import { homedir as homedir2 } from "node:os";
3040
- 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
+ }
3041
3161
 
3042
3162
  // src/capture-command.ts
3043
3163
  import { spawn as spawn2 } from "node:child_process";
@@ -3107,7 +3227,332 @@ function captureCommand(options) {
3107
3227
  });
3108
3228
  }
3109
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
+
3110
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";
3111
3556
  var successfulProcess = {
3112
3557
  exitCode: 0,
3113
3558
  signal: null,
@@ -3124,7 +3569,7 @@ function safeRelativePath3(value) {
3124
3569
  }
3125
3570
  async function configuredRoots(payload) {
3126
3571
  if (payload.mode === "DEFAULT") {
3127
- return [join2(homedir2(), "Library", "Developer", "Xcode", "DerivedData")];
3572
+ return [join3(homedir3(), "Library", "Developer", "Xcode", "DerivedData")];
3128
3573
  }
3129
3574
  if (payload.mode === "ABSOLUTE") {
3130
3575
  if (!payload.path || !isAbsolute(payload.path)) {
@@ -3173,7 +3618,7 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
3173
3618
  }
3174
3619
  let children;
3175
3620
  try {
3176
- children = await readdir(rootPath2, { withFileTypes: true });
3621
+ children = await readdir2(rootPath2, { withFileTypes: true });
3177
3622
  } catch (error) {
3178
3623
  return {
3179
3624
  entries: [],
@@ -3184,9 +3629,9 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
3184
3629
  for (const child of children) {
3185
3630
  signal.throwIfAborted();
3186
3631
  if (!child.isDirectory()) continue;
3187
- const path = join2(rootPath2, child.name);
3632
+ const path = join3(rootPath2, child.name);
3188
3633
  const projectPath = await workspacePath(
3189
- join2(path, "info.plist"),
3634
+ join3(path, "info.plist"),
3190
3635
  timeoutMs,
3191
3636
  signal
3192
3637
  );
@@ -3228,7 +3673,7 @@ function assertDirectChild(rootPath2, path) {
3228
3673
  }
3229
3674
  const root = resolve(rootPath2);
3230
3675
  const target2 = resolve(path);
3231
- if (target2 === root || dirname2(target2) !== root || basename(target2) !== basename(path)) {
3676
+ if (target2 === root || dirname2(target2) !== root || basename2(target2) !== basename2(path)) {
3232
3677
  throw new Error("Build Data target must be a direct child of its root");
3233
3678
  }
3234
3679
  }
@@ -3242,7 +3687,7 @@ async function allocatedBytes(path, signal) {
3242
3687
  try {
3243
3688
  for await (const entry of directory) {
3244
3689
  signal.throwIfAborted();
3245
- total += await allocatedBytes(join2(path, entry.name), signal);
3690
+ total += await allocatedBytes(join3(path, entry.name), signal);
3246
3691
  }
3247
3692
  } finally {
3248
3693
  await directory.close().catch(() => void 0);
@@ -3310,9 +3755,9 @@ var deleteBuildData = async (rawPayload, _timeoutMs, signal) => {
3310
3755
  };
3311
3756
 
3312
3757
  // src/handlers/codebases.ts
3313
- import { readdir as readdir2, realpath as realpath2, stat as stat2 } from "node:fs/promises";
3314
- import { homedir as homedir3 } from "node:os";
3315
- 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";
3316
3761
  var successfulProcess2 = {
3317
3762
  exitCode: 0,
3318
3763
  signal: null,
@@ -3389,7 +3834,7 @@ function baseSnapshot(folder) {
3389
3834
  }
3390
3835
  async function fetchedAt(commonDirectory) {
3391
3836
  try {
3392
- return (await stat2(join3(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
3837
+ return (await stat3(join4(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
3393
3838
  } catch {
3394
3839
  return null;
3395
3840
  }
@@ -3400,7 +3845,7 @@ async function inspectCodebaseProcess(selectedFolder, timeoutMs, signal, expecte
3400
3845
  let selected;
3401
3846
  try {
3402
3847
  selected = await realpath2(fallbackFolder);
3403
- if (!(await stat2(selected)).isDirectory())
3848
+ if (!(await stat3(selected)).isDirectory())
3404
3849
  throw new Error("Folder is not a directory");
3405
3850
  } catch (error) {
3406
3851
  return {
@@ -3568,23 +4013,23 @@ async function inspectCodebase(selectedFolder, timeoutMs, signal, expectedOrigin
3568
4013
  }
3569
4014
  var browseCodebaseDirectories = async (payload) => {
3570
4015
  const input = codebaseBrowsePayload(payload);
3571
- const homePath = await realpath2(homedir3());
4016
+ const homePath = await realpath2(homedir4());
3572
4017
  const path = await realpath2(input.path ? resolve2(input.path) : homePath);
3573
- if (!(await stat2(path)).isDirectory())
4018
+ if (!(await stat3(path)).isDirectory())
3574
4019
  throw new Error("Path is not a directory");
3575
- const candidates = await readdir2(path, { withFileTypes: true });
4020
+ const candidates = await readdir3(path, { withFileTypes: true });
3576
4021
  const entries = [];
3577
4022
  for (const candidate of candidates) {
3578
4023
  if (candidate.isDirectory()) {
3579
4024
  entries.push({
3580
4025
  name: candidate.name,
3581
- path: join3(path, candidate.name),
4026
+ path: join4(path, candidate.name),
3582
4027
  hidden: candidate.name.startsWith(".")
3583
4028
  });
3584
4029
  } else if (candidate.isSymbolicLink()) {
3585
4030
  try {
3586
- const target2 = join3(path, candidate.name);
3587
- if ((await stat2(target2)).isDirectory()) {
4031
+ const target2 = join4(path, candidate.name);
4032
+ if ((await stat3(target2)).isDirectory()) {
3588
4033
  entries.push({
3589
4034
  name: candidate.name,
3590
4035
  path: target2,
@@ -4154,14 +4599,14 @@ import {
4154
4599
  mkdir as mkdir2,
4155
4600
  readFile as readFile2,
4156
4601
  realpath as realpath3,
4157
- readdir as readdir3,
4602
+ readdir as readdir4,
4158
4603
  rename as rename2,
4159
4604
  rm as rm2,
4160
- stat as stat3,
4605
+ stat as stat4,
4161
4606
  writeFile as writeFile2
4162
4607
  } from "node:fs/promises";
4163
- import { homedir as homedir4 } from "node:os";
4164
- 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";
4165
4610
  import { promisify } from "node:util";
4166
4611
  var executeFile = promisify(execFile);
4167
4612
  var successfulProcess3 = {
@@ -4216,27 +4661,27 @@ var PROJECT_ROOTS = {
4216
4661
  };
4217
4662
  async function directoryExists(path) {
4218
4663
  try {
4219
- return (await stat3(path)).isDirectory();
4664
+ return (await stat4(path)).isDirectory();
4220
4665
  } catch {
4221
4666
  return false;
4222
4667
  }
4223
4668
  }
4224
4669
  async function configuredTools(tools) {
4225
- const homePath = homedir4();
4670
+ const homePath = homedir5();
4226
4671
  return Promise.all(
4227
4672
  AI_TOOLS.filter((tool) => tools.includes(tool)).map(async (tool) => ({
4228
4673
  tool,
4229
- configured: await directoryExists(join4(homePath, ...CONFIG_PATHS[tool])),
4674
+ configured: await directoryExists(join5(homePath, ...CONFIG_PATHS[tool])),
4230
4675
  homePath
4231
4676
  }))
4232
4677
  );
4233
4678
  }
4234
4679
  function rootPath(location) {
4235
- const base = location.scope === "GLOBAL" ? homedir4() : location.folder;
4680
+ const base = location.scope === "GLOBAL" ? homedir5() : location.folder;
4236
4681
  const parts = location.scope === "GLOBAL" ? GLOBAL_ROOTS[location.rootKind] : PROJECT_ROOTS[location.rootKind];
4237
4682
  const path = resolve3(base, ...parts);
4238
4683
  const normalizedBase = resolve3(base);
4239
- if (path !== normalizedBase && !path.startsWith(`${normalizedBase}${sep}`)) {
4684
+ if (path !== normalizedBase && !path.startsWith(`${normalizedBase}${sep2}`)) {
4240
4685
  throw new Error("Skill root escaped its configured base directory");
4241
4686
  }
4242
4687
  return path;
@@ -4248,12 +4693,12 @@ async function listPackageFiles(skillDirectory) {
4248
4693
  const files = [];
4249
4694
  let totalBytes = 0;
4250
4695
  const visit = async (directory) => {
4251
- for (const entry of await readdir3(directory, { withFileTypes: true })) {
4252
- const path = join4(directory, entry.name);
4696
+ for (const entry of await readdir4(directory, { withFileTypes: true })) {
4697
+ const path = join5(directory, entry.name);
4253
4698
  const metadata = await lstat2(path);
4254
4699
  if (metadata.isSymbolicLink()) {
4255
4700
  throw new Error(
4256
- `Nested symbolic link is not supported: ${relative(skillDirectory, path)}`
4701
+ `Nested symbolic link is not supported: ${relative2(skillDirectory, path)}`
4257
4702
  );
4258
4703
  }
4259
4704
  if (metadata.isDirectory()) {
@@ -4265,14 +4710,14 @@ async function listPackageFiles(skillDirectory) {
4265
4710
  throw new Error("Skill contains too many files");
4266
4711
  if (metadata.size > MAX_SKILL_FILE_BYTES) {
4267
4712
  throw new Error(
4268
- `${relative(skillDirectory, path)} exceeds the per-file limit`
4713
+ `${relative2(skillDirectory, path)} exceeds the per-file limit`
4269
4714
  );
4270
4715
  }
4271
4716
  totalBytes += metadata.size;
4272
4717
  if (totalBytes > MAX_SKILL_PACKAGE_BYTES)
4273
4718
  throw new Error("Skill exceeds the package size limit");
4274
4719
  files.push({
4275
- path: relative(skillDirectory, path).split(sep).join("/"),
4720
+ path: relative2(skillDirectory, path).split(sep2).join("/"),
4276
4721
  contentsBase64: (await readFile2(path)).toString("base64"),
4277
4722
  executable: (metadata.mode & 73) !== 0
4278
4723
  });
@@ -4295,7 +4740,7 @@ async function readPackage(directory) {
4295
4740
  const frontmatter = parseSkillMetadata(
4296
4741
  Buffer.from(definition.contentsBase64, "base64").toString("utf8")
4297
4742
  );
4298
- if (frontmatter.name !== directory.split(sep).at(-1)) {
4743
+ if (frontmatter.name !== directory.split(sep2).at(-1)) {
4299
4744
  throw new Error("Skill name must match its directory name");
4300
4745
  }
4301
4746
  return {
@@ -4336,12 +4781,12 @@ async function scanRoot2(location, configured, targets, warnings) {
4336
4781
  if (!consumers.length) return [];
4337
4782
  const target2 = targetForFolder(targets, location.folder);
4338
4783
  const installations = [];
4339
- for (const entry of await readdir3(root, { withFileTypes: true })) {
4784
+ for (const entry of await readdir4(root, { withFileTypes: true })) {
4340
4785
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
4341
- const directory = join4(root, entry.name);
4786
+ const directory = join5(root, entry.name);
4342
4787
  try {
4343
4788
  const skillPackage = await readPackage(directory);
4344
- const relativeDirectory = location.folder ? relative(location.folder, directory).split(sep).join("/") : "";
4789
+ const relativeDirectory = location.folder ? relative2(location.folder, directory).split(sep2).join("/") : "";
4345
4790
  installations.push({
4346
4791
  ...location,
4347
4792
  codebaseId: target2?.codebaseId ?? null,
@@ -4417,7 +4862,7 @@ var readSkills = async (payload) => {
4417
4862
  }
4418
4863
  packages.push({
4419
4864
  ...request,
4420
- package: await readPackage(join4(root, request.skillName))
4865
+ package: await readPackage(join5(root, request.skillName))
4421
4866
  });
4422
4867
  }
4423
4868
  return { ...successfulProcess3, packages };
@@ -4452,7 +4897,7 @@ async function updateManagedExclude(folder, relativeSkillPath, present) {
4452
4897
  if (line.trim()) managed.add(line.trim());
4453
4898
  }
4454
4899
  }
4455
- const pattern = `/${relativeSkillPath.split(sep).join("/").replace(/^\/+/, "")}/`;
4900
+ const pattern = `/${relativeSkillPath.split(sep2).join("/").replace(/^\/+/, "")}/`;
4456
4901
  if (present) managed.add(pattern);
4457
4902
  else managed.delete(pattern);
4458
4903
  const block = managed.size ? `${MANAGED_EXCLUDE_START}
@@ -4467,7 +4912,7 @@ ${MANAGED_EXCLUDE_END}` : "";
4467
4912
  }
4468
4913
  async function assertUntracked(location, skillDirectory) {
4469
4914
  if (location.scope !== "PROJECT") return;
4470
- const relativeDirectory = relative(location.folder, skillDirectory).split(sep).join("/");
4915
+ const relativeDirectory = relative2(location.folder, skillDirectory).split(sep2).join("/");
4471
4916
  if (await trackedProjectPath(location.folder, relativeDirectory)) {
4472
4917
  throw new Error(
4473
4918
  `${relativeDirectory} is tracked by Git and cannot be managed automatically`
@@ -4479,16 +4924,16 @@ async function writePackage(location, skillPackage) {
4479
4924
  throw new Error("New skill copies may only be written to shared roots");
4480
4925
  }
4481
4926
  const root = rootPath(location);
4482
- const destination = join4(root, skillPackage.name);
4927
+ const destination = join5(root, skillPackage.name);
4483
4928
  await assertUntracked(location, destination);
4484
4929
  await mkdir2(root, { recursive: true });
4485
- const temporary = join4(root, `.${skillPackage.name}.${randomUUID()}.tmp`);
4486
- 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`);
4487
4932
  await mkdir2(temporary, { recursive: true });
4488
4933
  try {
4489
4934
  for (const file of skillPackage.files) {
4490
4935
  const path = resolve3(temporary, ...file.path.split("/"));
4491
- if (!path.startsWith(`${temporary}${sep}`))
4936
+ if (!path.startsWith(`${temporary}${sep2}`))
4492
4937
  throw new Error("Skill file escaped temporary directory");
4493
4938
  await mkdir2(dirname4(path), { recursive: true });
4494
4939
  await writeFile2(path, Buffer.from(file.contentsBase64, "base64"));
@@ -4521,7 +4966,7 @@ var applySkills = async (payload) => {
4521
4966
  for (const operation of operations) {
4522
4967
  const root = rootPath(operation);
4523
4968
  const skillName = operation.kind === "WRITE" ? operation.package.name : operation.skillName;
4524
- const destination = join4(root, skillName);
4969
+ const destination = join5(root, skillName);
4525
4970
  await assertUntracked(operation, destination);
4526
4971
  if (operation.kind === "WRITE") {
4527
4972
  await writePackage(operation, operation.package);
@@ -4541,7 +4986,7 @@ var applySkills = async (payload) => {
4541
4986
  if (operation.scope === "PROJECT" && operation.manageGitExclude) {
4542
4987
  await updateManagedExclude(
4543
4988
  operation.folder,
4544
- relative(operation.folder, destination),
4989
+ relative2(operation.folder, destination),
4545
4990
  operation.kind === "WRITE"
4546
4991
  );
4547
4992
  }
@@ -4551,16 +4996,16 @@ var applySkills = async (payload) => {
4551
4996
 
4552
4997
  // src/handlers/worktrees.ts
4553
4998
  import { createWriteStream, watch } from "node:fs";
4554
- import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat4 } from "node:fs/promises";
4999
+ import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat5 } from "node:fs/promises";
4555
5000
  import { tmpdir } from "node:os";
4556
- import { basename as basename2, dirname as dirname5, join as join5, relative as relative3 } from "node:path";
5001
+ import { basename as basename3, dirname as dirname5, join as join6, relative as relative4 } from "node:path";
4557
5002
  import { spawn as spawn4 } from "node:child_process";
4558
5003
 
4559
5004
  // src/git-code-state.ts
4560
- import { createHash as createHash2 } from "node:crypto";
5005
+ import { createHash as createHash3 } from "node:crypto";
4561
5006
  import { createReadStream as createReadStream2 } from "node:fs";
4562
5007
  import { lstat as lstat3, readlink } from "node:fs/promises";
4563
- 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";
4564
5009
  import { spawn as spawn3 } from "node:child_process";
4565
5010
  function gitEnvironment() {
4566
5011
  return {
@@ -4574,8 +5019,8 @@ function remainingTimeoutMs(deadline) {
4574
5019
  }
4575
5020
  function containedPath(folder, path) {
4576
5021
  const absolutePath = resolve4(folder, path);
4577
- const difference = relative2(folder, absolutePath);
4578
- if (!difference || difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
5022
+ const difference = relative3(folder, absolutePath);
5023
+ if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute3(difference)) {
4579
5024
  return null;
4580
5025
  }
4581
5026
  return absolutePath;
@@ -4684,7 +5129,7 @@ async function worktreeCodeStateHash(folder, timeoutMs, signal) {
4684
5129
  if (head.exitCode !== 0 || untracked.exitCode !== 0 || submodules.exitCode !== 0 || operation.signal.aborted) {
4685
5130
  return null;
4686
5131
  }
4687
- const hash = createHash2("sha256");
5132
+ const hash = createHash3("sha256");
4688
5133
  hash.update("head\0");
4689
5134
  hash.update(head.stdout.trim());
4690
5135
  hash.update("\0diff\0");
@@ -4963,7 +5408,7 @@ async function origin(folder, timeoutMs, signal) {
4963
5408
  }
4964
5409
  async function validateWorktree(input, timeoutMs, signal) {
4965
5410
  const folder = await realpath4(input.folder);
4966
- if (!(await stat4(folder)).isDirectory())
5411
+ if (!(await stat5(folder)).isDirectory())
4967
5412
  throw new Error("Worktree is missing");
4968
5413
  const observedGitDirectory = await gitDirectory(folder, timeoutMs, signal);
4969
5414
  if (observedGitDirectory !== input.gitDirectory) {
@@ -5047,7 +5492,7 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
5047
5492
  return {
5048
5493
  gitDirectory: gitDir,
5049
5494
  folder,
5050
- relativePath: relative3(rootFolder, folder) || ".",
5495
+ relativePath: relative4(rootFolder, folder) || ".",
5051
5496
  primary,
5052
5497
  branch,
5053
5498
  headSha: headResult.exitCode === 0 ? headResult.stdout.trim() : null,
@@ -5074,7 +5519,7 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
5074
5519
  return {
5075
5520
  gitDirectory: folderValue2,
5076
5521
  folder: folderValue2,
5077
- relativePath: relative3(rootFolder, folderValue2) || ".",
5522
+ relativePath: relative4(rootFolder, folderValue2) || ".",
5078
5523
  primary,
5079
5524
  branch: null,
5080
5525
  headSha: null,
@@ -5190,12 +5635,12 @@ function parseNumstat(value) {
5190
5635
  }
5191
5636
  async function untrackedLines(folder, path) {
5192
5637
  try {
5193
- const file = await stat4(`${folder}/${path}`);
5638
+ const file = await stat5(`${folder}/${path}`);
5194
5639
  if (!file.isFile() || file.size > 1024 * 1024) return null;
5195
5640
  const contents = await readFile3(`${folder}/${path}`);
5196
5641
  if (contents.includes(0)) return null;
5197
- const text = contents.toString("utf8");
5198
- 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;
5199
5644
  } catch {
5200
5645
  return null;
5201
5646
  }
@@ -5401,7 +5846,7 @@ async function comparisonSides(input, folder, timeoutMs, signal) {
5401
5846
  if (!input.path) return { before: null, after: null };
5402
5847
  const path = input.path;
5403
5848
  const previousPath = input.previousPath ?? path;
5404
- const currentPath = join5(folder, path);
5849
+ const currentPath = join6(folder, path);
5405
5850
  if (input.scope === "UNTRACKED") {
5406
5851
  return { before: null, after: { kind: "FILE", path: currentPath } };
5407
5852
  }
@@ -5485,8 +5930,8 @@ async function availableSide(side, folder, timeoutMs, signal) {
5485
5930
  }
5486
5931
  try {
5487
5932
  const resolved = await realpath4(side.path);
5488
- const difference = relative3(folder, resolved);
5489
- return difference !== ".." && !difference.startsWith("../") && (await stat4(resolved)).isFile();
5933
+ const difference = relative4(folder, resolved);
5934
+ return difference !== ".." && !difference.startsWith("../") && (await stat5(resolved)).isFile();
5490
5935
  } catch {
5491
5936
  return false;
5492
5937
  }
@@ -5557,7 +6002,7 @@ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
5557
6002
  let patch;
5558
6003
  if (input.scope === "UNTRACKED") {
5559
6004
  const bounded = await readBoundedFile(
5560
- join5(folder, input.path),
6005
+ join6(folder, input.path),
5561
6006
  MAX_DIFF_BYTES
5562
6007
  );
5563
6008
  const contents = bounded.contents;
@@ -5728,7 +6173,7 @@ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, conte
5728
6173
  try {
5729
6174
  if (selected.kind === "FILE") {
5730
6175
  uploadPath = await realpath4(selected.path);
5731
- const difference = relative3(folder, uploadPath);
6176
+ const difference = relative4(folder, uploadPath);
5732
6177
  if (difference === ".." || difference.startsWith("../")) {
5733
6178
  throw new Error("Diff image resolves outside the worktree");
5734
6179
  }
@@ -5745,8 +6190,8 @@ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, conte
5745
6190
  if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
5746
6191
  throw new Error("Diff image exceeds the 20 MiB limit");
5747
6192
  }
5748
- temporaryDirectory = await mkdtemp(join5(tmpdir(), "ade-diff-image-"));
5749
- uploadPath = join5(temporaryDirectory, basename2(input.path));
6193
+ temporaryDirectory = await mkdtemp(join6(tmpdir(), "ade-diff-image-"));
6194
+ uploadPath = join6(temporaryDirectory, basename3(input.path));
5750
6195
  await writeGitObject(
5751
6196
  folder,
5752
6197
  selected.specification,
@@ -5755,14 +6200,14 @@ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, conte
5755
6200
  signal
5756
6201
  );
5757
6202
  }
5758
- const information = await stat4(uploadPath);
6203
+ const information = await stat5(uploadPath);
5759
6204
  if (!information.isFile() || information.size > 20 * 1024 * 1024) {
5760
6205
  throw new Error("Diff image exceeds the 20 MiB limit");
5761
6206
  }
5762
6207
  await context.uploadBuildArtifact({
5763
6208
  uploadId: input.uploadId,
5764
6209
  path: uploadPath,
5765
- filename: basename2(input.path),
6210
+ filename: basename3(input.path),
5766
6211
  contentType: imageContentType(input.path)
5767
6212
  });
5768
6213
  return successfulProcess4;
@@ -5906,7 +6351,7 @@ async function chooseNewBranch(folder, candidates, allowedCurrentBranch, timeout
5906
6351
  }
5907
6352
  async function validateBranchRoot(input, timeoutMs, signal) {
5908
6353
  const rootFolder = await realpath4(input.rootFolder);
5909
- if (!(await stat4(rootFolder)).isDirectory()) {
6354
+ if (!(await stat5(rootFolder)).isDirectory()) {
5910
6355
  throw new Error("Base repository is missing");
5911
6356
  }
5912
6357
  if (await origin(rootFolder, timeoutMs, signal) !== input.expectedOrigin) {
@@ -5969,13 +6414,13 @@ var branchWorktree = async (payload, timeoutMs, signal) => {
5969
6414
  timeoutMs,
5970
6415
  signal
5971
6416
  ) : input.candidates[0];
5972
- const targetFolder = input.action === "CREATE" ? join5(
6417
+ const targetFolder = input.action === "CREATE" ? join6(
5973
6418
  dirname5(rootFolder),
5974
- `${basename2(rootFolder)}-${branch.replaceAll("/", "-")}`
6419
+ `${basename3(rootFolder)}-${branch.replaceAll("/", "-")}`
5975
6420
  ) : changeFolder;
5976
6421
  if (input.action === "CREATE") {
5977
6422
  try {
5978
- await stat4(targetFolder);
6423
+ await stat5(targetFolder);
5979
6424
  throw new Error(`Worktree folder already exists: ${targetFolder}`);
5980
6425
  } catch (error) {
5981
6426
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
@@ -6209,12 +6654,12 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
6209
6654
  `Branch ${input.branch} is already checked out in ${occupiedFolder}`
6210
6655
  );
6211
6656
  }
6212
- targetFolder = join5(
6657
+ targetFolder = join6(
6213
6658
  dirname5(rootFolder),
6214
- `${basename2(rootFolder)}-${input.branch.replaceAll("/", "-")}`
6659
+ `${basename3(rootFolder)}-${input.branch.replaceAll("/", "-")}`
6215
6660
  );
6216
6661
  try {
6217
- await stat4(targetFolder);
6662
+ await stat5(targetFolder);
6218
6663
  throw new Error(`Worktree folder already exists: ${targetFolder}`);
6219
6664
  } catch (error) {
6220
6665
  if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
@@ -6375,7 +6820,7 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
6375
6820
  var deleteWorktree = async (payload, timeoutMs, signal) => {
6376
6821
  const input = worktreeDeleteJobPayload(payload);
6377
6822
  const rootFolder = await realpath4(input.rootFolder);
6378
- if (!(await stat4(rootFolder)).isDirectory()) {
6823
+ if (!(await stat5(rootFolder)).isDirectory()) {
6379
6824
  throw new Error("Base repository is missing");
6380
6825
  }
6381
6826
  if (await origin(rootFolder, timeoutMs, signal) !== input.expectedOrigin) {
@@ -6569,30 +7014,31 @@ var operateWorktree = async (payload, timeoutMs, signal) => {
6569
7014
  };
6570
7015
 
6571
7016
  // src/handlers/builds.ts
6572
- import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
7017
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
6573
7018
  import {
6574
7019
  cp,
6575
7020
  mkdir as mkdir3,
6576
7021
  mkdtemp as mkdtemp2,
6577
7022
  open as open2,
6578
- readdir as readdir4,
7023
+ readdir as readdir5,
6579
7024
  readFile as readFile4,
6580
7025
  realpath as realpath5,
6581
7026
  rename as rename3,
6582
7027
  rm as rm4,
6583
- stat as stat5,
7028
+ stat as stat6,
6584
7029
  writeFile as writeFile3
6585
7030
  } from "node:fs/promises";
6586
- import { createWriteStream as createWriteStream2 } from "node:fs";
7031
+ import { createReadStream as createReadStream3, createWriteStream as createWriteStream2 } from "node:fs";
6587
7032
  import { tmpdir as tmpdir2 } from "node:os";
7033
+ import { pipeline } from "node:stream/promises";
6588
7034
  import {
6589
- basename as basename3,
7035
+ basename as basename4,
6590
7036
  dirname as dirname6,
6591
7037
  isAbsolute as isAbsolute4,
6592
- join as join6,
6593
- relative as relative4,
7038
+ join as join7,
7039
+ relative as relative5,
6594
7040
  resolve as resolve5,
6595
- sep as sep3
7041
+ sep as sep4
6596
7042
  } from "node:path";
6597
7043
  import { spawn as spawn5 } from "node:child_process";
6598
7044
  var successfulProcess5 = {
@@ -6651,7 +7097,7 @@ function requireSuccess3(result, fallback) {
6651
7097
  }
6652
7098
  async function validateWorktree2(input, timeoutMs, signal) {
6653
7099
  const folder = await realpath5(input.folder);
6654
- if (!(await stat5(folder)).isDirectory())
7100
+ if (!(await stat6(folder)).isDirectory())
6655
7101
  throw new Error("Worktree is missing");
6656
7102
  const gitDirectory2 = requireSuccess3(
6657
7103
  await command2(
@@ -6685,8 +7131,8 @@ async function validateWorktree2(input, timeoutMs, signal) {
6685
7131
  }
6686
7132
  function containedPath2(root, relativePath) {
6687
7133
  const target2 = resolve5(root, relativePath);
6688
- const difference = relative4(root, target2);
6689
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7134
+ const difference = relative5(root, target2);
7135
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6690
7136
  throw new Error("Path must stay within the worktree");
6691
7137
  }
6692
7138
  return target2;
@@ -6694,11 +7140,11 @@ function containedPath2(root, relativePath) {
6694
7140
  async function validateSource(root, source) {
6695
7141
  const target2 = containedPath2(root, source.relativePath);
6696
7142
  const resolved = await realpath5(target2);
6697
- const difference = relative4(root, resolved);
6698
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7143
+ const difference = relative5(root, resolved);
7144
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6699
7145
  throw new Error("Build source resolves outside the worktree");
6700
7146
  }
6701
- const information = await stat5(resolved);
7147
+ const information = await stat6(resolved);
6702
7148
  if (source.kind === "PACKAGE" && !information.isFile()) {
6703
7149
  throw new Error("Package.swift is missing");
6704
7150
  }
@@ -6712,10 +7158,10 @@ async function discoverSourcesInFolder(folder) {
6712
7158
  const queue = [folder];
6713
7159
  while (queue.length && sources.length < DISCOVERY_LIMIT) {
6714
7160
  const current = queue.shift();
6715
- for (const entry of await readdir4(current, { withFileTypes: true })) {
7161
+ for (const entry of await readdir5(current, { withFileTypes: true })) {
6716
7162
  if (entry.isSymbolicLink()) continue;
6717
- const absolute = join6(current, entry.name);
6718
- const path = relative4(folder, absolute).split(sep3).join("/");
7163
+ const absolute = join7(current, entry.name);
7164
+ const path = relative5(folder, absolute).split(sep4).join("/");
6719
7165
  if (entry.isDirectory()) {
6720
7166
  if (entry.name.endsWith(".xcodeproj")) {
6721
7167
  sources.push({ kind: "PROJECT", relativePath: path });
@@ -6785,7 +7231,7 @@ function metadataFromList(value) {
6785
7231
  };
6786
7232
  }
6787
7233
  async function workspaceProjectPaths(workspacePath2, folder) {
6788
- const contentsPath = join6(workspacePath2, "contents.xcworkspacedata");
7234
+ const contentsPath = join7(workspacePath2, "contents.xcworkspacedata");
6789
7235
  let contents;
6790
7236
  try {
6791
7237
  contents = await readFile4(contentsPath, "utf8");
@@ -6807,14 +7253,14 @@ async function workspaceProjectPaths(workspacePath2, folder) {
6807
7253
  const projects = [];
6808
7254
  for (const location of locations) {
6809
7255
  const candidate = resolve5(dirname6(workspacePath2), location);
6810
- const difference = relative4(folder, candidate);
6811
- if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
7256
+ const difference = relative5(folder, candidate);
7257
+ if (difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
6812
7258
  continue;
6813
7259
  }
6814
7260
  try {
6815
7261
  const resolved = await realpath5(candidate);
6816
- const resolvedDifference = relative4(folder, resolved);
6817
- 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()) {
6818
7264
  continue;
6819
7265
  }
6820
7266
  projects.push(resolved);
@@ -7029,8 +7475,8 @@ function genericBuildDestinations(action) {
7029
7475
  ];
7030
7476
  }
7031
7477
  async function listPhysicalDevices(timeoutMs, signal) {
7032
- const directory = await mkdtemp2(join6(tmpdir2(), "ade-devices-"));
7033
- const output = join6(directory, "devices.json");
7478
+ const directory = await mkdtemp2(join7(tmpdir2(), "ade-devices-"));
7479
+ const output = join7(directory, "devices.json");
7034
7480
  try {
7035
7481
  const result = await command2(
7036
7482
  "xcrun",
@@ -7428,7 +7874,7 @@ function actionArgument(action) {
7428
7874
  }[action];
7429
7875
  }
7430
7876
  function xcodeBuildArguments(input) {
7431
- const resultBundle = join6(input.artifactDirectory, "result.xcresult");
7877
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
7432
7878
  const sourcePath = containedPath2(input.folder, input.source.relativePath);
7433
7879
  const usesCapturedTestProducts = input.action === "TEST_WITHOUT_BUILDING";
7434
7880
  const args = [
@@ -7450,13 +7896,13 @@ function xcodeBuildArguments(input) {
7450
7896
  if (input.action === "ARCHIVE") {
7451
7897
  args.push(
7452
7898
  "-archivePath",
7453
- join6(input.artifactDirectory, "archive.xcarchive")
7899
+ join7(input.artifactDirectory, "archive.xcarchive")
7454
7900
  );
7455
7901
  }
7456
7902
  if (input.action === "BUILD_FOR_TESTING") {
7457
7903
  args.push(
7458
7904
  "-testProductsPath",
7459
- join6(input.artifactDirectory, "test-products.xctestproducts")
7905
+ join7(input.artifactDirectory, "test-products.xctestproducts")
7460
7906
  );
7461
7907
  }
7462
7908
  if (input.action === "TEST_WITHOUT_BUILDING" && input.advancedSettings.priorTestProductsPath) {
@@ -7519,15 +7965,15 @@ function classifyFailure(output) {
7519
7965
  }
7520
7966
  async function runHook(options) {
7521
7967
  const phaseDirectory = options.phase === "PRE_BUILD" ? "pre" : "post";
7522
- const directory = join6(
7968
+ const directory = join7(
7523
7969
  options.input.artifactDirectory,
7524
7970
  "hooks",
7525
7971
  phaseDirectory
7526
7972
  );
7527
7973
  const prefix = `${String(options.script.position).padStart(3, "0")}-${options.script.id}`;
7528
- const file = join6(directory, `${prefix}.mjs`);
7529
- const runner = join6(directory, `${prefix}.runner.mjs`);
7530
- 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`);
7531
7977
  const started = Date.now();
7532
7978
  try {
7533
7979
  await mkdir3(directory, { recursive: true, mode: 448 });
@@ -7542,7 +7988,7 @@ async function runHook(options) {
7542
7988
  await writeFile3(
7543
7989
  runner,
7544
7990
  `import { readFile } from "node:fs/promises";
7545
- const hookModule = await import(${JSON.stringify(`./${basename3(file)}`)});
7991
+ const hookModule = await import(${JSON.stringify(`./${basename4(file)}`)});
7546
7992
  if (hookModule.default !== undefined && typeof hookModule.default !== "function") {
7547
7993
  throw new TypeError("The default build hook export must be a function");
7548
7994
  }
@@ -7579,7 +8025,7 @@ if (typeof hookModule.default === "function") {
7579
8025
  exitCode: result.exitCode,
7580
8026
  durationMs: Date.now() - started,
7581
8027
  causedBuildFailure: failed && options.script.failureBehavior === "FAIL_BUILD",
7582
- outputRelativePath: relative4(options.input.artifactDirectory, log),
8028
+ outputRelativePath: relative5(options.input.artifactDirectory, log),
7583
8029
  error: failed ? cleanError3(result.output || "Build hook failed") : null
7584
8030
  };
7585
8031
  } catch (hookError) {
@@ -7592,44 +8038,75 @@ if (typeof hookModule.default === "function") {
7592
8038
  exitCode: null,
7593
8039
  durationMs: Date.now() - started,
7594
8040
  causedBuildFailure: !options.signal.aborted && options.script.failureBehavior === "FAIL_BUILD",
7595
- outputRelativePath: relative4(options.input.artifactDirectory, log),
8041
+ outputRelativePath: relative5(options.input.artifactDirectory, log),
7596
8042
  error
7597
8043
  };
7598
8044
  }
7599
8045
  }
7600
8046
  async function pathExists(path) {
7601
8047
  try {
7602
- await stat5(path);
8048
+ await stat6(path);
7603
8049
  return true;
7604
8050
  } catch {
7605
8051
  return false;
7606
8052
  }
7607
8053
  }
7608
8054
  async function pathSize(path) {
7609
- const information = await stat5(path);
8055
+ const information = await stat6(path);
7610
8056
  if (information.isFile()) return information.size;
7611
8057
  if (!information.isDirectory()) return 0;
7612
8058
  let size = 0;
7613
- for (const entry of await readdir4(path, { withFileTypes: true })) {
8059
+ for (const entry of await readdir5(path, { withFileTypes: true })) {
7614
8060
  if (entry.isSymbolicLink()) continue;
7615
- size += await pathSize(join6(path, entry.name));
8061
+ size += await pathSize(join7(path, entry.name));
7616
8062
  }
7617
8063
  return size;
7618
8064
  }
7619
8065
  async function fileChecksum(path) {
7620
8066
  try {
7621
- const information = await stat5(path);
7622
- if (!information.isFile() || information.size > 256 * 1024 * 1024)
7623
- return null;
7624
- 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;
7625
8086
  } catch {
7626
8087
  return null;
7627
8088
  }
7628
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
+ }
7629
8106
  async function artifact(root, kind, path, metadata = {}) {
7630
8107
  return {
7631
8108
  kind,
7632
- relativePath: relative4(root, path).split(sep3).join("/"),
8109
+ relativePath: relative5(root, path).split(sep4).join("/"),
7633
8110
  sizeBytes: await pathSize(path),
7634
8111
  checksum: await fileChecksum(path),
7635
8112
  metadata
@@ -7657,7 +8134,7 @@ function testSourceFile(node, suite) {
7657
8134
  node.sourceFile
7658
8135
  ].find((value) => typeof value === "string" && !!value);
7659
8136
  if (explicit) {
7660
- return { file: basename3(explicit), filePath: explicit };
8137
+ return { file: basename4(explicit), filePath: explicit };
7661
8138
  }
7662
8139
  const identifier = typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : "";
7663
8140
  const identifierOwner = identifier.split("/")[0]?.trim();
@@ -7770,9 +8247,9 @@ function normalizeCoverage(value) {
7770
8247
  };
7771
8248
  }
7772
8249
  async function writeJsonReport(input, kind, timeoutMs, signal) {
7773
- const resultBundle = join6(input.artifactDirectory, "result.xcresult");
8250
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
7774
8251
  const filename = kind === "TEST_RESULTS" ? "test-results.json" : "code-coverage.json";
7775
- const destination = join6(input.artifactDirectory, filename);
8252
+ const destination = join7(input.artifactDirectory, filename);
7776
8253
  const temporary = `${destination}.tmp-${randomUUID2()}`;
7777
8254
  try {
7778
8255
  if (!await pathExists(resultBundle)) {
@@ -7907,7 +8384,7 @@ async function snapshotCoverageChanges(input, folder, timeoutMs, signal) {
7907
8384
  const types = changeTypesFromStatus(status2.stdout);
7908
8385
  for (const path of untracked.stdout.split("\0").filter(Boolean)) {
7909
8386
  try {
7910
- const contents = await readFile4(join6(folder, path));
8387
+ const contents = await readFile4(join7(folder, path));
7911
8388
  if (contents.includes(0)) continue;
7912
8389
  const lineCount = contents.length ? contents.toString("utf8").split("\n").length - (contents.toString("utf8").endsWith("\n") ? 1 : 0) : 0;
7913
8390
  lines.set(
@@ -7936,12 +8413,12 @@ async function addChangedCoverage(report, changes, input, folder, timeoutMs, sig
7936
8413
  let changedExecutableLines = 0;
7937
8414
  for (const change of changes) {
7938
8415
  const coveragePath = coveragePaths.find(
7939
- (candidate) => relative4(folder, candidate).split(sep3).join("/") === change.path
8416
+ (candidate) => relative5(folder, candidate).split(sep4).join("/") === change.path
7940
8417
  );
7941
8418
  let covered = 0;
7942
8419
  let executable = 0;
7943
8420
  if (coveragePath && change.lines.length) {
7944
- const temporary2 = join6(
8421
+ const temporary2 = join7(
7945
8422
  input.artifactDirectory,
7946
8423
  `.changed-coverage-${randomUUID2()}.json`
7947
8424
  );
@@ -8002,7 +8479,7 @@ async function addChangedCoverage(report, changes, input, folder, timeoutMs, sig
8002
8479
  changedLineCoverage: changedExecutableLines ? changedCoveredLines / changedExecutableLines : null
8003
8480
  };
8004
8481
  const data = { ...report.data, changedFiles };
8005
- const destination = join6(input.artifactDirectory, "worktree-coverage.json");
8482
+ const destination = join7(input.artifactDirectory, "worktree-coverage.json");
8006
8483
  const temporary = `${destination}.tmp-${randomUUID2()}`;
8007
8484
  await writeFile3(temporary, JSON.stringify({ summary, data }, null, 2), {
8008
8485
  mode: 384
@@ -8050,13 +8527,13 @@ async function findFiles(root, predicate, limit = 20) {
8050
8527
  const current = queue.shift();
8051
8528
  let entries;
8052
8529
  try {
8053
- entries = await readdir4(current, { withFileTypes: true });
8530
+ entries = await readdir5(current, { withFileTypes: true });
8054
8531
  } catch {
8055
8532
  continue;
8056
8533
  }
8057
8534
  for (const entry of entries) {
8058
8535
  if (entry.isSymbolicLink()) continue;
8059
- const path = join6(current, entry.name);
8536
+ const path = join7(current, entry.name);
8060
8537
  if (entry.isDirectory()) queue.push(path);
8061
8538
  else if (entry.isFile() && predicate(entry.name)) results.push(path);
8062
8539
  if (results.length >= limit) break;
@@ -8066,7 +8543,7 @@ async function findFiles(root, predicate, limit = 20) {
8066
8543
  }
8067
8544
  async function captureArtifacts(input, folder, signal, includeProducts = true) {
8068
8545
  const artifacts = [];
8069
- const resultBundle = join6(input.artifactDirectory, "result.xcresult");
8546
+ const resultBundle = join7(input.artifactDirectory, "result.xcresult");
8070
8547
  if (await pathExists(resultBundle)) {
8071
8548
  const coverageProbe = await command2(
8072
8549
  "xcrun",
@@ -8093,7 +8570,7 @@ async function captureArtifacts(input, folder, signal, includeProducts = true) {
8093
8570
  );
8094
8571
  }
8095
8572
  if (!includeProducts) return artifacts;
8096
- const archivePath = join6(input.artifactDirectory, "archive.xcarchive");
8573
+ const archivePath = join7(input.artifactDirectory, "archive.xcarchive");
8097
8574
  if (await pathExists(archivePath)) {
8098
8575
  artifacts.push(
8099
8576
  await artifact(input.artifactDirectory, "ARCHIVE", archivePath)
@@ -8116,10 +8593,10 @@ async function captureArtifacts(input, folder, signal, includeProducts = true) {
8116
8593
  if (apps.length === 1) {
8117
8594
  const settings = apps[0].buildSettings;
8118
8595
  const productName = settings.FULL_PRODUCT_NAME ?? settings.WRAPPER_NAME;
8119
- 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;
8120
8597
  if (source && await pathExists(source)) {
8121
- const productDirectory = join6(input.artifactDirectory, "products");
8122
- const destination = join6(productDirectory, basename3(source));
8598
+ const productDirectory = join7(input.artifactDirectory, "products");
8599
+ const destination = join7(productDirectory, basename4(source));
8123
8600
  await mkdir3(productDirectory, { recursive: true, mode: 448 });
8124
8601
  await rm4(destination, { recursive: true, force: true });
8125
8602
  await cp(source, destination, {
@@ -8136,7 +8613,7 @@ async function captureArtifacts(input, folder, signal, includeProducts = true) {
8136
8613
  }
8137
8614
  }
8138
8615
  if (input.action === "BUILD_FOR_TESTING") {
8139
- const testDirectory = join6(
8616
+ const testDirectory = join7(
8140
8617
  input.artifactDirectory,
8141
8618
  "test-products.xctestproducts"
8142
8619
  );
@@ -8151,7 +8628,7 @@ async function captureArtifacts(input, folder, signal, includeProducts = true) {
8151
8628
  )) {
8152
8629
  artifacts.push(
8153
8630
  await artifact(input.artifactDirectory, "XCTESTRUN", file, {
8154
- testPlan: basename3(file, ".xctestrun")
8631
+ testPlan: basename4(file, ".xctestrun")
8155
8632
  })
8156
8633
  );
8157
8634
  }
@@ -8171,14 +8648,14 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
8171
8648
  const testProductsPath = input.advancedSettings.priorTestProductsPath;
8172
8649
  const xctestrunPath = input.advancedSettings.priorXctestrunPath;
8173
8650
  if (testProductsPath) {
8174
- if (!testProductsPath.endsWith(".xctestproducts") || !await pathExists(testProductsPath) || !(await stat5(testProductsPath)).isDirectory()) {
8651
+ if (!testProductsPath.endsWith(".xctestproducts") || !await pathExists(testProductsPath) || !(await stat6(testProductsPath)).isDirectory()) {
8175
8652
  throw new Error("The captured test-products artifact is unavailable");
8176
8653
  }
8177
- } 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()) {
8178
8655
  throw new Error("The captured .xctestrun artifact is unavailable");
8179
8656
  }
8180
8657
  }
8181
- if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8658
+ if (!isAbsolute4(input.artifactDirectory) || basename4(input.artifactDirectory) !== input.buildId) {
8182
8659
  throw new Error("Build artifact directory is invalid");
8183
8660
  }
8184
8661
  const sourceStateHash = await worktreeCodeStateHash(
@@ -8187,11 +8664,11 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
8187
8664
  signal
8188
8665
  );
8189
8666
  await mkdir3(input.artifactDirectory, { recursive: true, mode: 448 });
8190
- const rawLog = join6(input.artifactDirectory, "build.log");
8667
+ const rawLog = join7(input.artifactDirectory, "build.log");
8191
8668
  const logger = new BuildLogger(input.buildId, context, rawLog, process.env);
8192
8669
  try {
8193
8670
  const scriptExecutions = [];
8194
- const contextPath = join6(input.artifactDirectory, "context.json");
8671
+ const contextPath = join7(input.artifactDirectory, "context.json");
8195
8672
  const baseHookContext = {
8196
8673
  buildId: input.buildId,
8197
8674
  branch: input.branch ?? null,
@@ -8424,7 +8901,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
8424
8901
  };
8425
8902
  var generateIosBuildReport = async (payload, timeoutMs, signal, onLog) => {
8426
8903
  const input = parseBuildReportPayload(payload);
8427
- if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
8904
+ if (!isAbsolute4(input.artifactDirectory) || basename4(input.artifactDirectory) !== input.buildId) {
8428
8905
  throw new Error("Build artifact directory is invalid");
8429
8906
  }
8430
8907
  if (signal.aborted) return { ...successfulProcess5, cancelled: true };
@@ -8467,25 +8944,25 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
8467
8944
  const target2 = await realpath5(
8468
8945
  containedPath2(root, input.artifactRelativePath)
8469
8946
  );
8470
- const difference = relative4(root, target2);
8471
- if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
8947
+ const difference = relative5(root, target2);
8948
+ if (!difference || difference === ".." || difference.startsWith(`..${sep4}`) || isAbsolute4(difference)) {
8472
8949
  throw new Error("Build artifact resolves outside the build folder");
8473
8950
  }
8474
- const information = await stat5(target2);
8951
+ const information = await stat6(target2);
8475
8952
  let uploadPath = target2;
8476
- let filename = basename3(target2);
8953
+ let filename = basename4(target2);
8477
8954
  let contentType = "application/octet-stream";
8478
8955
  let temporaryArchive = null;
8479
8956
  try {
8480
8957
  if (information.isDirectory()) {
8481
- temporaryArchive = join6(
8958
+ temporaryArchive = join7(
8482
8959
  tmpdir2(),
8483
8960
  `ade-build-artifact-${randomUUID2()}.tar.gz`
8484
8961
  );
8485
8962
  requireSuccess3(
8486
8963
  await command2(
8487
8964
  "tar",
8488
- ["-czf", temporaryArchive, "-C", dirname6(target2), basename3(target2)],
8965
+ ["-czf", temporaryArchive, "-C", dirname6(target2), basename4(target2)],
8489
8966
  timeoutMs,
8490
8967
  signal
8491
8968
  ),
@@ -8548,24 +9025,24 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
8548
9025
  if (!await pathExists(appPath) || !appPath.endsWith(".app")) {
8549
9026
  throw new Error("Runnable app artifact is missing");
8550
9027
  }
8551
- const deploymentsDirectory = join6(input.artifactDirectory, "deployments");
9028
+ const deploymentsDirectory = join7(input.artifactDirectory, "deployments");
8552
9029
  await mkdir3(deploymentsDirectory, { recursive: true, mode: 448 });
8553
9030
  const logger = new BuildLogger(
8554
9031
  input.buildId,
8555
9032
  context,
8556
- join6(deploymentsDirectory, "deployments.log"),
9033
+ join7(deploymentsDirectory, "deployments.log"),
8557
9034
  process.env
8558
9035
  );
8559
9036
  try {
8560
9037
  const outcomes = [];
8561
9038
  for (const deployment of input.deployments) {
8562
- const directory = join6(
9039
+ const directory = join7(
8563
9040
  input.artifactDirectory,
8564
9041
  "deployments",
8565
9042
  deployment.id
8566
9043
  );
8567
9044
  await mkdir3(directory, { recursive: true, mode: 448 });
8568
- const logPath = join6(directory, "deployment.log");
9045
+ const logPath = join7(directory, "deployment.log");
8569
9046
  const started = Date.now();
8570
9047
  let failure = null;
8571
9048
  try {
@@ -8719,7 +9196,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
8719
9196
  status: signal.aborted ? "CANCELLED" : failure ? "FAILED" : "SUCCEEDED",
8720
9197
  error: failure,
8721
9198
  durationMs: Date.now() - started,
8722
- outputRelativePath: relative4(input.artifactDirectory, logPath)
9199
+ outputRelativePath: relative5(input.artifactDirectory, logPath)
8723
9200
  });
8724
9201
  if (signal.aborted) break;
8725
9202
  }
@@ -8735,17 +9212,6 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
8735
9212
  await logger.close();
8736
9213
  }
8737
9214
  };
8738
- function xml(value) {
8739
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
8740
- }
8741
- function plistValue(value) {
8742
- if (typeof value === "boolean") return value ? "<true/>" : "<false/>";
8743
- if (typeof value === "string") return `<string>${xml(value)}</string>`;
8744
- if (value && typeof value === "object" && !Array.isArray(value)) {
8745
- return `<dict>${Object.entries(value).map(([key, entry]) => `<key>${xml(key)}</key>${plistValue(entry)}`).join("")}</dict>`;
8746
- }
8747
- throw new Error("Unsupported export plist value");
8748
- }
8749
9215
  function exportPlist(settings) {
8750
9216
  const method = {
8751
9217
  DEBUGGING: "debugging",
@@ -8767,10 +9233,7 @@ function exportPlist(settings) {
8767
9233
  if (Object.keys(settings.provisioningProfiles).length) {
8768
9234
  values.provisioningProfiles = settings.provisioningProfiles;
8769
9235
  }
8770
- return `<?xml version="1.0" encoding="UTF-8"?>
8771
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
8772
- <plist version="1.0">${plistValue(values)}</plist>
8773
- `;
9236
+ return plistDocument(values);
8774
9237
  }
8775
9238
  var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
8776
9239
  const input = parseBuildExportPayload(payload);
@@ -8786,14 +9249,14 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
8786
9249
  if (!await pathExists(archivePath) || !archivePath.endsWith(".xcarchive")) {
8787
9250
  throw new Error("Archive artifact is missing");
8788
9251
  }
8789
- const exportDirectory = join6(
9252
+ const exportDirectory = join7(
8790
9253
  input.artifactDirectory,
8791
9254
  "exports",
8792
9255
  input.exportId
8793
9256
  );
8794
9257
  await mkdir3(exportDirectory, { recursive: true, mode: 448 });
8795
- const plistPath = join6(exportDirectory, "ExportOptions.plist");
8796
- const logPath = join6(exportDirectory, "export.log");
9258
+ const plistPath = join7(exportDirectory, "ExportOptions.plist");
9259
+ const logPath = join7(exportDirectory, "export.log");
8797
9260
  await writeFile3(plistPath, exportPlist(input.settings), { mode: 384 });
8798
9261
  const logger = new BuildLogger(input.buildId, context, logPath, process.env);
8799
9262
  try {
@@ -8825,14 +9288,30 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
8825
9288
  signal: result.signal,
8826
9289
  timedOut: result.timedOut,
8827
9290
  cancelled: result.cancelled,
8828
- outputRelativePath: relative4(input.artifactDirectory, exportDirectory),
9291
+ outputRelativePath: relative5(input.artifactDirectory, exportDirectory),
8829
9292
  sizeBytes: result.exitCode === 0 ? await pathSize(exportDirectory) : null,
8830
- 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) : []
8831
9295
  };
8832
9296
  } finally {
8833
9297
  await logger.close();
8834
9298
  }
8835
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
+ }
8836
9315
 
8837
9316
  // src/handlers/index.ts
8838
9317
  var handlers = {
@@ -8869,7 +9348,8 @@ var handlers = {
8869
9348
  [IOS_DEPLOY_JOB_KIND]: deployIosBuild,
8870
9349
  [IOS_EXPORT_JOB_KIND]: exportIosArchive,
8871
9350
  [IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
8872
- [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport
9351
+ [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport,
9352
+ [IOS_SIGNING_INSPECT_JOB_KIND]: inspectIosSigning
8873
9353
  };
8874
9354
 
8875
9355
  // src/repository-coordinator.ts