@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.
- package/dist/control-agent.js +1849 -241
- package/package.json +1 -1
package/dist/control-agent.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
2868
|
-
import {
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
-
|
|
2875
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
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
|
-
|
|
2896
|
-
|
|
2897
|
-
exceededLimit = true;
|
|
2898
|
-
return;
|
|
3052
|
+
if (entity.startsWith("#")) {
|
|
3053
|
+
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
2899
3054
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
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
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
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
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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: [
|
|
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
|
|
3183
|
+
child.stdout?.on("data", (chunk) => {
|
|
2944
3184
|
stdout = append(stdout, chunk);
|
|
2945
3185
|
});
|
|
2946
|
-
child.stderr
|
|
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
|
|
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 [
|
|
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 || !
|
|
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
|
|
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 =
|
|
3632
|
+
const path = join3(rootPath2, child.name);
|
|
3068
3633
|
const projectPath = await workspacePath(
|
|
3069
|
-
|
|
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 ||
|
|
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(
|
|
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
|
|
3194
|
-
import { homedir as
|
|
3195
|
-
import { dirname as dirname3, join as
|
|
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
|
|
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
|
|
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(
|
|
4016
|
+
const homePath = await realpath2(homedir4());
|
|
3452
4017
|
const path = await realpath2(input.path ? resolve2(input.path) : homePath);
|
|
3453
|
-
if (!(await
|
|
4018
|
+
if (!(await stat3(path)).isDirectory())
|
|
3454
4019
|
throw new Error("Path is not a directory");
|
|
3455
|
-
const candidates = await
|
|
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:
|
|
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 =
|
|
3467
|
-
if ((await
|
|
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
|
|
4602
|
+
readdir as readdir4,
|
|
4038
4603
|
rename as rename2,
|
|
4039
4604
|
rm as rm2,
|
|
4040
|
-
stat as
|
|
4605
|
+
stat as stat4,
|
|
4041
4606
|
writeFile as writeFile2
|
|
4042
4607
|
} from "node:fs/promises";
|
|
4043
|
-
import { homedir as
|
|
4044
|
-
import { dirname as dirname4, isAbsolute as isAbsolute2, join as
|
|
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
|
|
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 =
|
|
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(
|
|
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" ?
|
|
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}${
|
|
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
|
|
4132
|
-
const path =
|
|
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: ${
|
|
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
|
-
`${
|
|
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:
|
|
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(
|
|
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
|
|
4784
|
+
for (const entry of await readdir4(root, { withFileTypes: true })) {
|
|
4220
4785
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
4221
|
-
const directory =
|
|
4786
|
+
const directory = join5(root, entry.name);
|
|
4222
4787
|
try {
|
|
4223
4788
|
const skillPackage = await readPackage(directory);
|
|
4224
|
-
const relativeDirectory = location.folder ?
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
4927
|
+
const destination = join5(root, skillPackage.name);
|
|
4363
4928
|
await assertUntracked(location, destination);
|
|
4364
4929
|
await mkdir2(root, { recursive: true });
|
|
4365
|
-
const temporary =
|
|
4366
|
-
const backup =
|
|
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}${
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4435
|
-
import {
|
|
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
|
|
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
|
|
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 =
|
|
4456
|
-
if (!difference || difference === ".." || difference.startsWith(`..${
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
5066
|
-
return
|
|
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
|
-
|
|
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
|
|
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" ?
|
|
6417
|
+
const targetFolder = input.action === "CREATE" ? join6(
|
|
5355
6418
|
dirname5(rootFolder),
|
|
5356
|
-
`${
|
|
6419
|
+
`${basename3(rootFolder)}-${branch.replaceAll("/", "-")}`
|
|
5357
6420
|
) : changeFolder;
|
|
5358
6421
|
if (input.action === "CREATE") {
|
|
5359
6422
|
try {
|
|
5360
|
-
await
|
|
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 =
|
|
6657
|
+
targetFolder = join6(
|
|
5595
6658
|
dirname5(rootFolder),
|
|
5596
|
-
`${
|
|
6659
|
+
`${basename3(rootFolder)}-${input.branch.replaceAll("/", "-")}`
|
|
5597
6660
|
);
|
|
5598
6661
|
try {
|
|
5599
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
7021
|
+
mkdtemp as mkdtemp2,
|
|
7022
|
+
open as open2,
|
|
7023
|
+
readdir as readdir5,
|
|
5960
7024
|
readFile as readFile4,
|
|
5961
7025
|
realpath as realpath5,
|
|
5962
|
-
|
|
5963
|
-
|
|
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
|
|
7035
|
+
basename as basename4,
|
|
5970
7036
|
dirname as dirname6,
|
|
5971
7037
|
isAbsolute as isAbsolute4,
|
|
5972
|
-
join as
|
|
5973
|
-
relative as
|
|
7038
|
+
join as join7,
|
|
7039
|
+
relative as relative5,
|
|
5974
7040
|
resolve as resolve5,
|
|
5975
|
-
sep as
|
|
7041
|
+
sep as sep4
|
|
5976
7042
|
} from "node:path";
|
|
5977
|
-
import { spawn as
|
|
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
|
|
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 =
|
|
6053
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
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 =
|
|
6062
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
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
|
|
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
|
|
7161
|
+
for (const entry of await readdir5(current, { withFileTypes: true })) {
|
|
6080
7162
|
if (entry.isSymbolicLink()) continue;
|
|
6081
|
-
const absolute =
|
|
6082
|
-
const path =
|
|
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 =
|
|
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 =
|
|
6175
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
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 =
|
|
6181
|
-
if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${
|
|
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
|
|
6397
|
-
const output =
|
|
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
|
|
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 =
|
|
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 =
|
|
7680
|
+
extraStream = createWriteStream2(options.additionalLogPath, {
|
|
6599
7681
|
flags: "a",
|
|
6600
7682
|
mode: 384
|
|
6601
7683
|
});
|
|
6602
7684
|
}
|
|
6603
|
-
const child =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
6893
|
-
const runner =
|
|
6894
|
-
const 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(`./${
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
8059
|
+
for (const entry of await readdir5(path, { withFileTypes: true })) {
|
|
6978
8060
|
if (entry.isSymbolicLink()) continue;
|
|
6979
|
-
size += await pathSize(
|
|
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
|
|
6986
|
-
if (!information.isFile()
|
|
6987
|
-
|
|
6988
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
7079
|
-
const destination =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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) ||
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
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
|
|
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 =
|
|
7347
|
-
if (!difference || difference === ".." || difference.startsWith(`..${
|
|
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
|
|
8951
|
+
const information = await stat6(target2);
|
|
7351
8952
|
let uploadPath = target2;
|
|
7352
|
-
let filename =
|
|
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 =
|
|
7358
|
-
|
|
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),
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
|
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 =
|
|
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 =
|
|
7672
|
-
const logPath =
|
|
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:
|
|
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
|