@ai-development-environment/control-agent 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control-agent.js +1525 -189
- package/package.json +1 -1
package/dist/control-agent.js
CHANGED
|
@@ -269,8 +269,8 @@ function createClient(options) {
|
|
|
269
269
|
retryWait = async function randomisedExponentialBackoff(retries2) {
|
|
270
270
|
const retryDelaySeconds = Math.pow(2, retries2);
|
|
271
271
|
await new Promise(
|
|
272
|
-
(
|
|
273
|
-
|
|
272
|
+
(resolve6) => setTimeout(
|
|
273
|
+
resolve6,
|
|
274
274
|
retryDelaySeconds * 1e3 + // add random timeout from 300ms to 3s
|
|
275
275
|
Math.floor(Math.random() * (3e3 - 300) + 300)
|
|
276
276
|
)
|
|
@@ -506,7 +506,7 @@ function createClient(options) {
|
|
|
506
506
|
if (socket.readyState === WebSocketImpl.CLOSING) await throwOnClose;
|
|
507
507
|
let release2 = () => {
|
|
508
508
|
};
|
|
509
|
-
const released = new Promise((
|
|
509
|
+
const released = new Promise((resolve6) => release2 = resolve6);
|
|
510
510
|
return [
|
|
511
511
|
socket,
|
|
512
512
|
release2,
|
|
@@ -669,7 +669,7 @@ function createClient(options) {
|
|
|
669
669
|
const iterator = (async function* iterator2() {
|
|
670
670
|
for (; ; ) {
|
|
671
671
|
if (!pending.length) {
|
|
672
|
-
await new Promise((
|
|
672
|
+
await new Promise((resolve6) => deferred.resolve = resolve6);
|
|
673
673
|
}
|
|
674
674
|
while (pending.length) {
|
|
675
675
|
yield pending.shift();
|
|
@@ -960,7 +960,7 @@ function createAgentSubscriptionClient(config) {
|
|
|
960
960
|
shouldRetry: () => true,
|
|
961
961
|
retryWait: async (retries) => {
|
|
962
962
|
const delay = Math.min(3e4, 1e3 * 2 ** Math.min(retries, 5));
|
|
963
|
-
await new Promise((
|
|
963
|
+
await new Promise((resolve6) => setTimeout(resolve6, delay));
|
|
964
964
|
},
|
|
965
965
|
on: {
|
|
966
966
|
connected: () => console.log("Connected to control-plane WebSocket"),
|
|
@@ -1506,6 +1506,8 @@ var WORKTREE_BRANCH_JOB_KIND = "worktree.branch";
|
|
|
1506
1506
|
var WORKTREE_MOVE_PUSH_JOB_KIND = "worktree.move.push";
|
|
1507
1507
|
var WORKTREE_MOVE_CHECKOUT_JOB_KIND = "worktree.move.checkout";
|
|
1508
1508
|
var WORKTREE_DELETE_JOB_KIND = "worktree.delete";
|
|
1509
|
+
var WORKTREE_DIFF_JOB_KIND = "worktree.diff.inspect";
|
|
1510
|
+
var WORKTREE_DIFF_ASSET_JOB_KIND = "worktree.diff.asset";
|
|
1509
1511
|
var WORKTREE_JOB_KINDS = [
|
|
1510
1512
|
WORKTREE_INSPECT_JOB_KIND,
|
|
1511
1513
|
WORKTREE_OPERATION_JOB_KIND,
|
|
@@ -1513,7 +1515,9 @@ var WORKTREE_JOB_KINDS = [
|
|
|
1513
1515
|
WORKTREE_BRANCH_JOB_KIND,
|
|
1514
1516
|
WORKTREE_MOVE_PUSH_JOB_KIND,
|
|
1515
1517
|
WORKTREE_MOVE_CHECKOUT_JOB_KIND,
|
|
1516
|
-
WORKTREE_DELETE_JOB_KIND
|
|
1518
|
+
WORKTREE_DELETE_JOB_KIND,
|
|
1519
|
+
WORKTREE_DIFF_JOB_KIND,
|
|
1520
|
+
WORKTREE_DIFF_ASSET_JOB_KIND
|
|
1517
1521
|
];
|
|
1518
1522
|
var DEFAULT_JIRA_BRANCH_REGEX = String.raw`\b([A-Z][A-Z0-9_]*-\d+)\b`;
|
|
1519
1523
|
var WORKTREE_OPERATIONS = [
|
|
@@ -1534,6 +1538,13 @@ function validGitBranchName(value) {
|
|
|
1534
1538
|
}
|
|
1535
1539
|
return value.split("/").every((part) => part && !part.startsWith(".") && !part.endsWith(".lock"));
|
|
1536
1540
|
}
|
|
1541
|
+
var WORKTREE_DIFF_SCOPES = [
|
|
1542
|
+
"STAGED",
|
|
1543
|
+
"UNSTAGED",
|
|
1544
|
+
"UNTRACKED",
|
|
1545
|
+
"COMMIT",
|
|
1546
|
+
"BRANCH"
|
|
1547
|
+
];
|
|
1537
1548
|
function objectValue4(value, name) {
|
|
1538
1549
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1539
1550
|
throw new Error(`${name} must be an object`);
|
|
@@ -1550,6 +1561,13 @@ function nullableString3(value, name) {
|
|
|
1550
1561
|
if (value === null || value === void 0) return null;
|
|
1551
1562
|
return stringValue4(value, name);
|
|
1552
1563
|
}
|
|
1564
|
+
function safeRelativePath(value, name) {
|
|
1565
|
+
const path = stringValue4(value, name).replaceAll("\\", "/");
|
|
1566
|
+
if (path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.split("/").includes("..") || path.includes("\0")) {
|
|
1567
|
+
throw new Error(`${name} must stay within the worktree`);
|
|
1568
|
+
}
|
|
1569
|
+
return path;
|
|
1570
|
+
}
|
|
1553
1571
|
function worktreeBranchJobPayload(value) {
|
|
1554
1572
|
const payload = objectValue4(value, "worktree branch payload");
|
|
1555
1573
|
const allowed = /* @__PURE__ */ new Set([
|
|
@@ -1858,6 +1876,73 @@ function worktreeJobPayload(value) {
|
|
|
1858
1876
|
...editorVariant === void 0 ? {} : { editorVariant }
|
|
1859
1877
|
};
|
|
1860
1878
|
}
|
|
1879
|
+
function worktreeDiffPayload(value) {
|
|
1880
|
+
const payload = objectValue4(value, "worktree diff payload");
|
|
1881
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1882
|
+
"codebaseId",
|
|
1883
|
+
"folder",
|
|
1884
|
+
"gitDirectory",
|
|
1885
|
+
"expectedOrigin",
|
|
1886
|
+
"baseBranch",
|
|
1887
|
+
"scope",
|
|
1888
|
+
"path",
|
|
1889
|
+
"previousPath",
|
|
1890
|
+
"commitSha",
|
|
1891
|
+
"uploadId",
|
|
1892
|
+
"side"
|
|
1893
|
+
]);
|
|
1894
|
+
const unexpected = Object.keys(payload).find((key) => !allowed.has(key));
|
|
1895
|
+
if (unexpected) {
|
|
1896
|
+
throw new Error(`Unexpected worktree diff payload field: ${unexpected}`);
|
|
1897
|
+
}
|
|
1898
|
+
if (!WORKTREE_DIFF_SCOPES.includes(payload.scope)) {
|
|
1899
|
+
throw new Error("worktree diff payload.scope is invalid");
|
|
1900
|
+
}
|
|
1901
|
+
if (payload.side !== null && payload.side !== void 0 && payload.side !== "BEFORE" && payload.side !== "AFTER") {
|
|
1902
|
+
throw new Error("worktree diff payload.side is invalid");
|
|
1903
|
+
}
|
|
1904
|
+
const path = payload.path === null || payload.path === void 0 ? null : safeRelativePath(payload.path, "worktree diff payload.path");
|
|
1905
|
+
const previousPath = payload.previousPath === null || payload.previousPath === void 0 ? null : safeRelativePath(
|
|
1906
|
+
payload.previousPath,
|
|
1907
|
+
"worktree diff payload.previousPath"
|
|
1908
|
+
);
|
|
1909
|
+
const scope = payload.scope;
|
|
1910
|
+
const commitSha = nullableString3(
|
|
1911
|
+
payload.commitSha,
|
|
1912
|
+
"worktree diff payload.commitSha"
|
|
1913
|
+
);
|
|
1914
|
+
if (scope === "COMMIT" && !commitSha) {
|
|
1915
|
+
throw new Error("Commit diffs require a commit SHA");
|
|
1916
|
+
}
|
|
1917
|
+
return {
|
|
1918
|
+
codebaseId: stringValue4(
|
|
1919
|
+
payload.codebaseId,
|
|
1920
|
+
"worktree diff payload.codebaseId"
|
|
1921
|
+
),
|
|
1922
|
+
folder: stringValue4(payload.folder, "worktree diff payload.folder"),
|
|
1923
|
+
gitDirectory: stringValue4(
|
|
1924
|
+
payload.gitDirectory,
|
|
1925
|
+
"worktree diff payload.gitDirectory"
|
|
1926
|
+
),
|
|
1927
|
+
expectedOrigin: stringValue4(
|
|
1928
|
+
payload.expectedOrigin,
|
|
1929
|
+
"worktree diff payload.expectedOrigin"
|
|
1930
|
+
),
|
|
1931
|
+
baseBranch: stringValue4(
|
|
1932
|
+
payload.baseBranch,
|
|
1933
|
+
"worktree diff payload.baseBranch"
|
|
1934
|
+
),
|
|
1935
|
+
scope,
|
|
1936
|
+
path,
|
|
1937
|
+
previousPath,
|
|
1938
|
+
commitSha,
|
|
1939
|
+
uploadId: nullableString3(
|
|
1940
|
+
payload.uploadId,
|
|
1941
|
+
"worktree diff payload.uploadId"
|
|
1942
|
+
),
|
|
1943
|
+
side: payload.side ?? null
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1861
1946
|
function worktreeWatchJobPayload(value) {
|
|
1862
1947
|
const payload = objectValue4(value, "worktree watch payload");
|
|
1863
1948
|
const allowed = /* @__PURE__ */ new Set([
|
|
@@ -2201,6 +2286,8 @@ var IOS_BUILD_DELETE_JOB_KIND = "ios.build.delete";
|
|
|
2201
2286
|
var IOS_ARTIFACT_DOWNLOAD_JOB_KIND = "ios.artifact.download";
|
|
2202
2287
|
var IOS_DEPLOY_JOB_KIND = "ios.build.deploy";
|
|
2203
2288
|
var IOS_EXPORT_JOB_KIND = "ios.archive.export";
|
|
2289
|
+
var IOS_TEST_RESULTS_JOB_KIND = "ios.test-results.parse";
|
|
2290
|
+
var IOS_COVERAGE_REPORT_JOB_KIND = "ios.coverage.generate";
|
|
2204
2291
|
var IOS_BUILD_JOB_KINDS = [
|
|
2205
2292
|
IOS_SOURCE_DISCOVER_JOB_KIND,
|
|
2206
2293
|
IOS_SOURCE_PARSE_JOB_KIND,
|
|
@@ -2210,7 +2297,9 @@ var IOS_BUILD_JOB_KINDS = [
|
|
|
2210
2297
|
IOS_BUILD_DELETE_JOB_KIND,
|
|
2211
2298
|
IOS_ARTIFACT_DOWNLOAD_JOB_KIND,
|
|
2212
2299
|
IOS_DEPLOY_JOB_KIND,
|
|
2213
|
-
IOS_EXPORT_JOB_KIND
|
|
2300
|
+
IOS_EXPORT_JOB_KIND,
|
|
2301
|
+
IOS_TEST_RESULTS_JOB_KIND,
|
|
2302
|
+
IOS_COVERAGE_REPORT_JOB_KIND
|
|
2214
2303
|
];
|
|
2215
2304
|
var BUILD_ACTIONS = [
|
|
2216
2305
|
"BUILD",
|
|
@@ -2259,6 +2348,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
|
|
|
2259
2348
|
allowProvisioningDeviceRegistration: false,
|
|
2260
2349
|
testPlan: null,
|
|
2261
2350
|
codeCoverage: false,
|
|
2351
|
+
parseTestResults: true,
|
|
2262
2352
|
parallelTesting: null,
|
|
2263
2353
|
parallelTestingWorkers: null,
|
|
2264
2354
|
onlyTesting: [],
|
|
@@ -2268,6 +2358,7 @@ var DEFAULT_BUILD_ADVANCED_SETTINGS = {
|
|
|
2268
2358
|
priorTestProductsPath: null,
|
|
2269
2359
|
priorXctestrunPath: null
|
|
2270
2360
|
};
|
|
2361
|
+
var BUILD_REPORT_KINDS = ["TEST_RESULTS", "CODE_COVERAGE"];
|
|
2271
2362
|
function objectValue6(value, name) {
|
|
2272
2363
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2273
2364
|
throw new Error(`${name} must be an object`);
|
|
@@ -2293,7 +2384,7 @@ function enumValue3(value, values, name) {
|
|
|
2293
2384
|
}
|
|
2294
2385
|
return value;
|
|
2295
2386
|
}
|
|
2296
|
-
function
|
|
2387
|
+
function safeRelativePath2(value, name) {
|
|
2297
2388
|
const path = stringValue6(value, name);
|
|
2298
2389
|
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path) || path.split(/[\\/]/).includes("..") || path.includes("\0")) {
|
|
2299
2390
|
throw new Error(`${name} must stay within the worktree`);
|
|
@@ -2318,13 +2409,14 @@ function worktreeIdentity(value) {
|
|
|
2318
2409
|
value.expectedOrigin,
|
|
2319
2410
|
"build payload.expectedOrigin"
|
|
2320
2411
|
),
|
|
2321
|
-
headSha: nullableString4(value.headSha, "build payload.headSha")
|
|
2412
|
+
headSha: nullableString4(value.headSha, "build payload.headSha"),
|
|
2413
|
+
baseBranch: nullableString4(value.baseBranch, "build payload.baseBranch")
|
|
2322
2414
|
};
|
|
2323
2415
|
}
|
|
2324
2416
|
function parseBuildSource(value) {
|
|
2325
2417
|
const source = objectValue6(value, "build source");
|
|
2326
2418
|
const kind = enumValue3(source.kind, BUILD_SOURCE_KINDS, "build source.kind");
|
|
2327
|
-
const relativePath =
|
|
2419
|
+
const relativePath = safeRelativePath2(
|
|
2328
2420
|
source.relativePath,
|
|
2329
2421
|
"build source.relativePath"
|
|
2330
2422
|
);
|
|
@@ -2443,6 +2535,10 @@ function parseBuildAdvancedSettings(value) {
|
|
|
2443
2535
|
input.codeCoverage,
|
|
2444
2536
|
"advanced settings.codeCoverage"
|
|
2445
2537
|
),
|
|
2538
|
+
parseTestResults: booleanValue2(
|
|
2539
|
+
input.parseTestResults,
|
|
2540
|
+
"advanced settings.parseTestResults"
|
|
2541
|
+
),
|
|
2446
2542
|
parallelTesting: parallel,
|
|
2447
2543
|
parallelTestingWorkers: workers ?? null,
|
|
2448
2544
|
onlyTesting: stringArray2(
|
|
@@ -2580,7 +2676,27 @@ function parseBuildJobPayload(value) {
|
|
|
2580
2676
|
action,
|
|
2581
2677
|
destination,
|
|
2582
2678
|
advancedSettings,
|
|
2583
|
-
scripts
|
|
2679
|
+
scripts,
|
|
2680
|
+
worktreeCoverage: input.worktreeCoverage === void 0 ? false : booleanValue2(
|
|
2681
|
+
input.worktreeCoverage,
|
|
2682
|
+
"build job payload.worktreeCoverage"
|
|
2683
|
+
)
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
function parseBuildReportPayload(value) {
|
|
2687
|
+
const input = objectValue6(value, "build report payload");
|
|
2688
|
+
return {
|
|
2689
|
+
...parseBuildDeletePayload(input),
|
|
2690
|
+
reportKind: enumValue3(
|
|
2691
|
+
input.reportKind,
|
|
2692
|
+
BUILD_REPORT_KINDS,
|
|
2693
|
+
"build report payload.reportKind"
|
|
2694
|
+
),
|
|
2695
|
+
source: enumValue3(
|
|
2696
|
+
input.source,
|
|
2697
|
+
["MANUAL", "WORKTREE"],
|
|
2698
|
+
"build report payload.source"
|
|
2699
|
+
)
|
|
2584
2700
|
};
|
|
2585
2701
|
}
|
|
2586
2702
|
function parseBuildDeletePayload(value) {
|
|
@@ -2607,7 +2723,7 @@ function parseBuildArtifactDownloadPayload(value) {
|
|
|
2607
2723
|
const identity = parseBuildDeletePayload(input);
|
|
2608
2724
|
return {
|
|
2609
2725
|
...identity,
|
|
2610
|
-
artifactRelativePath:
|
|
2726
|
+
artifactRelativePath: safeRelativePath2(
|
|
2611
2727
|
input.artifactRelativePath,
|
|
2612
2728
|
"build artifact download payload.artifactRelativePath"
|
|
2613
2729
|
),
|
|
@@ -2649,7 +2765,7 @@ function parseBuildDeploymentPayload(value) {
|
|
|
2649
2765
|
input.artifactDirectory,
|
|
2650
2766
|
"build deployment payload.artifactDirectory"
|
|
2651
2767
|
),
|
|
2652
|
-
artifactRelativePath:
|
|
2768
|
+
artifactRelativePath: safeRelativePath2(
|
|
2653
2769
|
input.artifactRelativePath,
|
|
2654
2770
|
"build deployment payload.artifactRelativePath"
|
|
2655
2771
|
),
|
|
@@ -2717,7 +2833,7 @@ function parseBuildExportPayload(value) {
|
|
|
2717
2833
|
input.artifactDirectory,
|
|
2718
2834
|
"build export payload.artifactDirectory"
|
|
2719
2835
|
),
|
|
2720
|
-
archiveRelativePath:
|
|
2836
|
+
archiveRelativePath: safeRelativePath2(
|
|
2721
2837
|
input.archiveRelativePath,
|
|
2722
2838
|
"build export payload.archiveRelativePath"
|
|
2723
2839
|
),
|
|
@@ -2756,7 +2872,7 @@ function collectInventory() {
|
|
|
2756
2872
|
// src/process-runner.ts
|
|
2757
2873
|
import { spawn } from "node:child_process";
|
|
2758
2874
|
async function runProcess(options) {
|
|
2759
|
-
return new Promise((
|
|
2875
|
+
return new Promise((resolve6, reject) => {
|
|
2760
2876
|
let sequence = 0;
|
|
2761
2877
|
let timedOut = false;
|
|
2762
2878
|
let cancelled = false;
|
|
@@ -2833,7 +2949,7 @@ async function runProcess(options) {
|
|
|
2833
2949
|
clearTimeout(timeout);
|
|
2834
2950
|
options.signal.removeEventListener("abort", abort);
|
|
2835
2951
|
void logChain.then(
|
|
2836
|
-
() =>
|
|
2952
|
+
() => resolve6({ exitCode, signal, timedOut, cancelled })
|
|
2837
2953
|
);
|
|
2838
2954
|
});
|
|
2839
2955
|
});
|
|
@@ -2925,9 +3041,9 @@ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } fro
|
|
|
2925
3041
|
|
|
2926
3042
|
// src/capture-command.ts
|
|
2927
3043
|
import { spawn as spawn2 } from "node:child_process";
|
|
2928
|
-
var MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
3044
|
+
var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
2929
3045
|
function captureCommand(options) {
|
|
2930
|
-
return new Promise((
|
|
3046
|
+
return new Promise((resolve6, reject) => {
|
|
2931
3047
|
let stdout = "";
|
|
2932
3048
|
let stderr = "";
|
|
2933
3049
|
let timedOut = false;
|
|
@@ -2935,15 +3051,19 @@ function captureCommand(options) {
|
|
|
2935
3051
|
let settled = false;
|
|
2936
3052
|
const child = spawn2(options.command, options.args, {
|
|
2937
3053
|
shell: false,
|
|
2938
|
-
stdio: [
|
|
3054
|
+
stdio: [
|
|
3055
|
+
"ignore",
|
|
3056
|
+
options.stdoutFileDescriptor === void 0 ? "pipe" : options.stdoutFileDescriptor,
|
|
3057
|
+
"pipe"
|
|
3058
|
+
],
|
|
2939
3059
|
env: options.env ?? process.env,
|
|
2940
3060
|
cwd: options.cwd
|
|
2941
3061
|
});
|
|
2942
3062
|
const append = (current, chunk) => `${current}${String(chunk)}`.slice(0, MAX_OUTPUT_BYTES);
|
|
2943
|
-
child.stdout
|
|
3063
|
+
child.stdout?.on("data", (chunk) => {
|
|
2944
3064
|
stdout = append(stdout, chunk);
|
|
2945
3065
|
});
|
|
2946
|
-
child.stderr
|
|
3066
|
+
child.stderr?.on("data", (chunk) => {
|
|
2947
3067
|
stderr = append(stderr, chunk);
|
|
2948
3068
|
});
|
|
2949
3069
|
const terminate = () => {
|
|
@@ -2975,7 +3095,7 @@ function captureCommand(options) {
|
|
|
2975
3095
|
settled = true;
|
|
2976
3096
|
clearTimeout(timeout);
|
|
2977
3097
|
options.signal.removeEventListener("abort", abort);
|
|
2978
|
-
|
|
3098
|
+
resolve6({
|
|
2979
3099
|
exitCode,
|
|
2980
3100
|
signal,
|
|
2981
3101
|
timedOut,
|
|
@@ -2997,7 +3117,7 @@ var successfulProcess = {
|
|
|
2997
3117
|
function errorMessage(error) {
|
|
2998
3118
|
return error instanceof Error ? error.message : String(error);
|
|
2999
3119
|
}
|
|
3000
|
-
function
|
|
3120
|
+
function safeRelativePath3(value) {
|
|
3001
3121
|
if (!value || value.includes("\0") || isAbsolute(value)) return false;
|
|
3002
3122
|
const normalized = value.replaceAll("\\", "/");
|
|
3003
3123
|
return normalized !== "." && normalized.split("/").every((part) => part && part !== ".." && part !== ".");
|
|
@@ -3012,7 +3132,7 @@ async function configuredRoots(payload) {
|
|
|
3012
3132
|
}
|
|
3013
3133
|
return [resolve(payload.path)];
|
|
3014
3134
|
}
|
|
3015
|
-
if (!payload.path || !
|
|
3135
|
+
if (!payload.path || !safeRelativePath3(payload.path)) {
|
|
3016
3136
|
throw new Error("Relative Derived Data mode requires a safe relative path");
|
|
3017
3137
|
}
|
|
3018
3138
|
return payload.worktrees.map(
|
|
@@ -4430,9 +4550,204 @@ var applySkills = async (payload) => {
|
|
|
4430
4550
|
};
|
|
4431
4551
|
|
|
4432
4552
|
// src/handlers/worktrees.ts
|
|
4433
|
-
import { watch } from "node:fs";
|
|
4434
|
-
import { readFile as readFile3, realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
4435
|
-
import {
|
|
4553
|
+
import { createWriteStream, watch } from "node:fs";
|
|
4554
|
+
import { mkdtemp, open, readFile as readFile3, realpath as realpath4, rm as rm3, stat as stat4 } from "node:fs/promises";
|
|
4555
|
+
import { tmpdir } from "node:os";
|
|
4556
|
+
import { basename as basename2, dirname as dirname5, join as join5, relative as relative3 } from "node:path";
|
|
4557
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
4558
|
+
|
|
4559
|
+
// src/git-code-state.ts
|
|
4560
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4561
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
4562
|
+
import { lstat as lstat3, readlink } from "node:fs/promises";
|
|
4563
|
+
import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
4564
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4565
|
+
function gitEnvironment() {
|
|
4566
|
+
return {
|
|
4567
|
+
...process.env,
|
|
4568
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
4569
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
function remainingTimeoutMs(deadline) {
|
|
4573
|
+
return Math.max(1, deadline - Date.now());
|
|
4574
|
+
}
|
|
4575
|
+
function containedPath(folder, path) {
|
|
4576
|
+
const absolutePath = resolve4(folder, path);
|
|
4577
|
+
const difference = relative2(folder, absolutePath);
|
|
4578
|
+
if (!difference || difference === ".." || difference.startsWith(`..${sep2}`) || isAbsolute3(difference)) {
|
|
4579
|
+
return null;
|
|
4580
|
+
}
|
|
4581
|
+
return absolutePath;
|
|
4582
|
+
}
|
|
4583
|
+
function hashGitDiff(hash, folder, timeoutMs, signal) {
|
|
4584
|
+
return new Promise((resolveResult, reject) => {
|
|
4585
|
+
let settled = false;
|
|
4586
|
+
let timedOut = false;
|
|
4587
|
+
const child = spawn3(
|
|
4588
|
+
"git",
|
|
4589
|
+
[
|
|
4590
|
+
"-C",
|
|
4591
|
+
folder,
|
|
4592
|
+
"--no-optional-locks",
|
|
4593
|
+
"diff",
|
|
4594
|
+
"--binary",
|
|
4595
|
+
"--no-ext-diff",
|
|
4596
|
+
"--no-textconv",
|
|
4597
|
+
"HEAD",
|
|
4598
|
+
"--"
|
|
4599
|
+
],
|
|
4600
|
+
{
|
|
4601
|
+
env: gitEnvironment(),
|
|
4602
|
+
shell: false,
|
|
4603
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4604
|
+
}
|
|
4605
|
+
);
|
|
4606
|
+
child.stdout.on("data", (chunk) => hash.update(chunk));
|
|
4607
|
+
child.stderr.resume();
|
|
4608
|
+
const terminate = () => {
|
|
4609
|
+
if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
|
|
4610
|
+
};
|
|
4611
|
+
const timeout = setTimeout(() => {
|
|
4612
|
+
timedOut = true;
|
|
4613
|
+
terminate();
|
|
4614
|
+
}, timeoutMs);
|
|
4615
|
+
timeout.unref();
|
|
4616
|
+
const abort = () => terminate();
|
|
4617
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4618
|
+
if (signal.aborted) abort();
|
|
4619
|
+
child.once("error", (error) => {
|
|
4620
|
+
if (settled) return;
|
|
4621
|
+
settled = true;
|
|
4622
|
+
clearTimeout(timeout);
|
|
4623
|
+
signal.removeEventListener("abort", abort);
|
|
4624
|
+
reject(error);
|
|
4625
|
+
});
|
|
4626
|
+
child.once("close", (exitCode) => {
|
|
4627
|
+
if (settled) return;
|
|
4628
|
+
settled = true;
|
|
4629
|
+
clearTimeout(timeout);
|
|
4630
|
+
signal.removeEventListener("abort", abort);
|
|
4631
|
+
resolveResult(!timedOut && !signal.aborted && exitCode === 0);
|
|
4632
|
+
});
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
async function worktreeCodeStateHash(folder, timeoutMs, signal) {
|
|
4636
|
+
const deadline = Date.now() + timeoutMs;
|
|
4637
|
+
const operation = new AbortController();
|
|
4638
|
+
const abort = () => operation.abort(signal.reason);
|
|
4639
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4640
|
+
if (signal.aborted) abort();
|
|
4641
|
+
const timeout = setTimeout(() => operation.abort(), Math.max(0, timeoutMs));
|
|
4642
|
+
timeout.unref();
|
|
4643
|
+
try {
|
|
4644
|
+
operation.signal.throwIfAborted();
|
|
4645
|
+
const [head, untracked, submodules] = await Promise.all([
|
|
4646
|
+
captureCommand({
|
|
4647
|
+
command: "git",
|
|
4648
|
+
args: ["-C", folder, "rev-parse", "HEAD"],
|
|
4649
|
+
timeoutMs: remainingTimeoutMs(deadline),
|
|
4650
|
+
signal: operation.signal,
|
|
4651
|
+
env: gitEnvironment()
|
|
4652
|
+
}),
|
|
4653
|
+
captureCommand({
|
|
4654
|
+
command: "git",
|
|
4655
|
+
args: [
|
|
4656
|
+
"-C",
|
|
4657
|
+
folder,
|
|
4658
|
+
"--no-optional-locks",
|
|
4659
|
+
"ls-files",
|
|
4660
|
+
"--others",
|
|
4661
|
+
"--exclude-standard",
|
|
4662
|
+
"-z"
|
|
4663
|
+
],
|
|
4664
|
+
timeoutMs: remainingTimeoutMs(deadline),
|
|
4665
|
+
signal: operation.signal,
|
|
4666
|
+
env: gitEnvironment()
|
|
4667
|
+
}),
|
|
4668
|
+
captureCommand({
|
|
4669
|
+
command: "git",
|
|
4670
|
+
args: [
|
|
4671
|
+
"-C",
|
|
4672
|
+
folder,
|
|
4673
|
+
"--no-optional-locks",
|
|
4674
|
+
"submodule",
|
|
4675
|
+
"foreach",
|
|
4676
|
+
"--quiet",
|
|
4677
|
+
'printf "%s\\0" "$sm_path"'
|
|
4678
|
+
],
|
|
4679
|
+
timeoutMs: remainingTimeoutMs(deadline),
|
|
4680
|
+
signal: operation.signal,
|
|
4681
|
+
env: gitEnvironment()
|
|
4682
|
+
})
|
|
4683
|
+
]);
|
|
4684
|
+
if (head.exitCode !== 0 || untracked.exitCode !== 0 || submodules.exitCode !== 0 || operation.signal.aborted) {
|
|
4685
|
+
return null;
|
|
4686
|
+
}
|
|
4687
|
+
const hash = createHash2("sha256");
|
|
4688
|
+
hash.update("head\0");
|
|
4689
|
+
hash.update(head.stdout.trim());
|
|
4690
|
+
hash.update("\0diff\0");
|
|
4691
|
+
if (!await hashGitDiff(
|
|
4692
|
+
hash,
|
|
4693
|
+
folder,
|
|
4694
|
+
remainingTimeoutMs(deadline),
|
|
4695
|
+
operation.signal
|
|
4696
|
+
)) {
|
|
4697
|
+
return null;
|
|
4698
|
+
}
|
|
4699
|
+
hash.update("\0untracked\0");
|
|
4700
|
+
const paths = untracked.stdout.split("\0").filter(Boolean).sort();
|
|
4701
|
+
for (const path of paths) {
|
|
4702
|
+
operation.signal.throwIfAborted();
|
|
4703
|
+
const absolutePath = containedPath(folder, path);
|
|
4704
|
+
if (!absolutePath) return null;
|
|
4705
|
+
hash.update(path);
|
|
4706
|
+
hash.update("\0");
|
|
4707
|
+
const information = await lstat3(absolutePath);
|
|
4708
|
+
operation.signal.throwIfAborted();
|
|
4709
|
+
if (information.isSymbolicLink()) {
|
|
4710
|
+
hash.update("symlink\0");
|
|
4711
|
+
hash.update(await readlink(absolutePath));
|
|
4712
|
+
} else if (information.isFile()) {
|
|
4713
|
+
hash.update("file\0");
|
|
4714
|
+
hash.update((information.mode & 511).toString(8).padStart(3, "0"));
|
|
4715
|
+
hash.update("\0");
|
|
4716
|
+
for await (const chunk of createReadStream2(absolutePath, {
|
|
4717
|
+
signal: operation.signal
|
|
4718
|
+
})) {
|
|
4719
|
+
hash.update(chunk);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
hash.update("\0");
|
|
4723
|
+
}
|
|
4724
|
+
hash.update("submodules\0");
|
|
4725
|
+
const submodulePaths = submodules.stdout.split("\0").filter(Boolean).sort();
|
|
4726
|
+
for (const path of submodulePaths) {
|
|
4727
|
+
operation.signal.throwIfAborted();
|
|
4728
|
+
const absolutePath = containedPath(folder, path);
|
|
4729
|
+
if (!absolutePath) return null;
|
|
4730
|
+
const submoduleHash = await worktreeCodeStateHash(
|
|
4731
|
+
absolutePath,
|
|
4732
|
+
remainingTimeoutMs(deadline),
|
|
4733
|
+
operation.signal
|
|
4734
|
+
);
|
|
4735
|
+
if (!submoduleHash) return null;
|
|
4736
|
+
hash.update(path);
|
|
4737
|
+
hash.update("\0");
|
|
4738
|
+
hash.update(submoduleHash);
|
|
4739
|
+
hash.update("\0");
|
|
4740
|
+
}
|
|
4741
|
+
return hash.digest("hex");
|
|
4742
|
+
} catch {
|
|
4743
|
+
return null;
|
|
4744
|
+
} finally {
|
|
4745
|
+
clearTimeout(timeout);
|
|
4746
|
+
signal.removeEventListener("abort", abort);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
// src/handlers/worktrees.ts
|
|
4436
4751
|
var successfulProcess4 = {
|
|
4437
4752
|
exitCode: 0,
|
|
4438
4753
|
signal: null,
|
|
@@ -4440,6 +4755,16 @@ var successfulProcess4 = {
|
|
|
4440
4755
|
cancelled: false
|
|
4441
4756
|
};
|
|
4442
4757
|
var WATCH_DEBOUNCE_MS = 500;
|
|
4758
|
+
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4759
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4760
|
+
".png",
|
|
4761
|
+
".jpg",
|
|
4762
|
+
".jpeg",
|
|
4763
|
+
".gif",
|
|
4764
|
+
".webp",
|
|
4765
|
+
".avif",
|
|
4766
|
+
".bmp"
|
|
4767
|
+
]);
|
|
4443
4768
|
var activeWorktreeWatches = /* @__PURE__ */ new Map();
|
|
4444
4769
|
function statusChangeState(value) {
|
|
4445
4770
|
const entries = value.split("\0").filter(Boolean);
|
|
@@ -4482,27 +4807,30 @@ async function flushWorktreeActivity(entry) {
|
|
|
4482
4807
|
entry.pending = false;
|
|
4483
4808
|
try {
|
|
4484
4809
|
const signal = new AbortController().signal;
|
|
4485
|
-
const [status2, branchResult, headResult] = await Promise.all(
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4810
|
+
const [status2, branchResult, headResult, codeStateHash] = await Promise.all(
|
|
4811
|
+
[
|
|
4812
|
+
git2(
|
|
4813
|
+
entry.folder,
|
|
4814
|
+
[
|
|
4815
|
+
"--no-optional-locks",
|
|
4816
|
+
"status",
|
|
4817
|
+
"--porcelain=v1",
|
|
4818
|
+
"-z",
|
|
4819
|
+
"--untracked-files=all"
|
|
4820
|
+
],
|
|
4821
|
+
entry.timeoutMs,
|
|
4822
|
+
signal
|
|
4823
|
+
),
|
|
4824
|
+
git2(
|
|
4825
|
+
entry.folder,
|
|
4826
|
+
["symbolic-ref", "--short", "-q", "HEAD"],
|
|
4827
|
+
entry.timeoutMs,
|
|
4828
|
+
signal
|
|
4829
|
+
),
|
|
4830
|
+
git2(entry.folder, ["rev-parse", "HEAD"], entry.timeoutMs, signal),
|
|
4831
|
+
worktreeCodeStateHash(entry.folder, entry.timeoutMs, signal)
|
|
4832
|
+
]
|
|
4833
|
+
);
|
|
4506
4834
|
const branch = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
|
|
4507
4835
|
const headSha = headResult.exitCode === 0 ? headResult.stdout.trim() : null;
|
|
4508
4836
|
const headIdentity = headSha ? `${branch ?? ""}\0${headSha}` : null;
|
|
@@ -4510,6 +4838,7 @@ async function flushWorktreeActivity(entry) {
|
|
|
4510
4838
|
const report = {
|
|
4511
4839
|
codebaseId: entry.codebaseId,
|
|
4512
4840
|
gitDirectory: entry.gitDirectory,
|
|
4841
|
+
codeStateHash,
|
|
4513
4842
|
...changes,
|
|
4514
4843
|
pushStatus: await worktreePushStatus(
|
|
4515
4844
|
entry.folder,
|
|
@@ -4718,10 +5047,11 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
|
|
|
4718
5047
|
return {
|
|
4719
5048
|
gitDirectory: gitDir,
|
|
4720
5049
|
folder,
|
|
4721
|
-
relativePath:
|
|
5050
|
+
relativePath: relative3(rootFolder, folder) || ".",
|
|
4722
5051
|
primary,
|
|
4723
5052
|
branch,
|
|
4724
5053
|
headSha: headResult.exitCode === 0 ? headResult.stdout.trim() : null,
|
|
5054
|
+
codeStateHash: await worktreeCodeStateHash(folder, timeoutMs, signal),
|
|
4725
5055
|
upstream,
|
|
4726
5056
|
ahead: upstreamCounts.ahead,
|
|
4727
5057
|
behind: upstreamCounts.behind,
|
|
@@ -4744,10 +5074,11 @@ async function inspectWorktreeItem(folderValue2, rootFolder, baseBranch, primary
|
|
|
4744
5074
|
return {
|
|
4745
5075
|
gitDirectory: folderValue2,
|
|
4746
5076
|
folder: folderValue2,
|
|
4747
|
-
relativePath:
|
|
5077
|
+
relativePath: relative3(rootFolder, folderValue2) || ".",
|
|
4748
5078
|
primary,
|
|
4749
5079
|
branch: null,
|
|
4750
5080
|
headSha: null,
|
|
5081
|
+
codeStateHash: null,
|
|
4751
5082
|
upstream: null,
|
|
4752
5083
|
ahead: null,
|
|
4753
5084
|
behind: null,
|
|
@@ -4889,7 +5220,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
|
|
|
4889
5220
|
const value = values[index];
|
|
4890
5221
|
const code = value.slice(0, 2);
|
|
4891
5222
|
const path = value.slice(3);
|
|
4892
|
-
|
|
5223
|
+
const previousPath = (code[0] === "R" || code[0] === "C") && values[index + 1] ? values[index + 1] : null;
|
|
5224
|
+
if (previousPath) index += 1;
|
|
4893
5225
|
const untracked = code === "??";
|
|
4894
5226
|
const conflicted = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(
|
|
4895
5227
|
code
|
|
@@ -4901,6 +5233,8 @@ async function inspectChanges(folder, timeoutMs, signal) {
|
|
|
4901
5233
|
const untrackedCount = untracked ? await untrackedLines(folder, path) : null;
|
|
4902
5234
|
changes.push({
|
|
4903
5235
|
path,
|
|
5236
|
+
previousPath,
|
|
5237
|
+
changeType: untracked ? "ADDED" : conflicted ? "CONFLICTED" : code,
|
|
4904
5238
|
staged,
|
|
4905
5239
|
unstaged,
|
|
4906
5240
|
untracked,
|
|
@@ -4913,6 +5247,65 @@ async function inspectChanges(folder, timeoutMs, signal) {
|
|
|
4913
5247
|
}
|
|
4914
5248
|
return { changes: changes.slice(0, 500), truncated: changes.length > 500 };
|
|
4915
5249
|
}
|
|
5250
|
+
function imagePath(path) {
|
|
5251
|
+
const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
|
|
5252
|
+
return IMAGE_EXTENSIONS.has(extension);
|
|
5253
|
+
}
|
|
5254
|
+
function parseNameStatus(value) {
|
|
5255
|
+
const entries = value.split("\0").filter(Boolean);
|
|
5256
|
+
const files = [];
|
|
5257
|
+
for (let index = 0; index < entries.length; ) {
|
|
5258
|
+
const status2 = entries[index++] ?? "M";
|
|
5259
|
+
const renamed = status2.startsWith("R") || status2.startsWith("C");
|
|
5260
|
+
const first = entries[index++] ?? "";
|
|
5261
|
+
const second = renamed ? entries[index++] ?? "" : null;
|
|
5262
|
+
const path = second ?? first;
|
|
5263
|
+
if (!path) continue;
|
|
5264
|
+
files.push({
|
|
5265
|
+
path,
|
|
5266
|
+
previousPath: renamed ? first : null,
|
|
5267
|
+
changeType: status2[0] ?? "M"
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
5270
|
+
return files;
|
|
5271
|
+
}
|
|
5272
|
+
async function diffFiles(folder, range, timeoutMs, signal) {
|
|
5273
|
+
const [status2, counts2] = await Promise.all([
|
|
5274
|
+
git2(folder, ["diff", "--name-status", "-z", range], timeoutMs, signal),
|
|
5275
|
+
git2(folder, ["diff", "--numstat", "-z", range], timeoutMs, signal)
|
|
5276
|
+
]);
|
|
5277
|
+
requireSuccess2(status2, "Could not inspect changed files");
|
|
5278
|
+
requireSuccess2(counts2, "Could not inspect changed line counts");
|
|
5279
|
+
const numstat = parseNumstat(counts2.stdout);
|
|
5280
|
+
return parseNameStatus(status2.stdout).map((file) => {
|
|
5281
|
+
const [additions, deletions] = numstat.get(file.path) ?? [null, null];
|
|
5282
|
+
return {
|
|
5283
|
+
...file,
|
|
5284
|
+
additions,
|
|
5285
|
+
deletions,
|
|
5286
|
+
binary: additions === null && deletions === null,
|
|
5287
|
+
image: imagePath(file.path)
|
|
5288
|
+
};
|
|
5289
|
+
});
|
|
5290
|
+
}
|
|
5291
|
+
async function inspectBranchChanges(folder, baseBranch, timeoutMs, signal) {
|
|
5292
|
+
const mergeBase = requireSuccess2(
|
|
5293
|
+
await git2(
|
|
5294
|
+
folder,
|
|
5295
|
+
["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
|
|
5296
|
+
timeoutMs,
|
|
5297
|
+
signal
|
|
5298
|
+
),
|
|
5299
|
+
"Could not determine the base-branch merge base"
|
|
5300
|
+
).stdout.trim();
|
|
5301
|
+
const files = await diffFiles(
|
|
5302
|
+
folder,
|
|
5303
|
+
`${mergeBase}..HEAD`,
|
|
5304
|
+
timeoutMs,
|
|
5305
|
+
signal
|
|
5306
|
+
);
|
|
5307
|
+
return { changes: files.slice(0, 500), truncated: files.length > 500 };
|
|
5308
|
+
}
|
|
4916
5309
|
async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
|
|
4917
5310
|
const result = requireSuccess2(
|
|
4918
5311
|
await git2(
|
|
@@ -4944,89 +5337,513 @@ async function inspectCommits(folder, baseBranch, timeoutMs, signal) {
|
|
|
4944
5337
|
return { commits: commits.slice(0, 100), truncated: commits.length > 100 };
|
|
4945
5338
|
}
|
|
4946
5339
|
async function inspectWorktreeDetail(folder, baseBranch, timeoutMs, signal) {
|
|
4947
|
-
const [commitResult, changeResult] = await Promise.all([
|
|
5340
|
+
const [commitResult, changeResult, branchResult] = await Promise.all([
|
|
4948
5341
|
inspectCommits(folder, baseBranch, timeoutMs, signal),
|
|
4949
|
-
inspectChanges(folder, timeoutMs, signal)
|
|
5342
|
+
inspectChanges(folder, timeoutMs, signal),
|
|
5343
|
+
inspectBranchChanges(folder, baseBranch, timeoutMs, signal)
|
|
4950
5344
|
]);
|
|
4951
5345
|
return {
|
|
4952
5346
|
commits: commitResult.commits,
|
|
4953
5347
|
changes: changeResult.changes,
|
|
5348
|
+
branchChanges: branchResult.changes,
|
|
4954
5349
|
commitsTruncated: commitResult.truncated,
|
|
4955
|
-
changesTruncated: changeResult.truncated
|
|
5350
|
+
changesTruncated: changeResult.truncated,
|
|
5351
|
+
branchChangesTruncated: branchResult.truncated
|
|
4956
5352
|
};
|
|
4957
5353
|
}
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
if (
|
|
4971
|
-
|
|
4972
|
-
closeWorktreeWatch(current);
|
|
4973
|
-
activeWorktreeWatches.delete(input.gitDirectory);
|
|
4974
|
-
}
|
|
4975
|
-
const entry = {
|
|
4976
|
-
watchId: input.watchId,
|
|
4977
|
-
codebaseId: input.codebaseId,
|
|
4978
|
-
gitDirectory: input.gitDirectory,
|
|
4979
|
-
folder,
|
|
4980
|
-
baseBranch: input.baseBranch,
|
|
4981
|
-
timeoutMs: Math.min(timeoutMs, 3e4),
|
|
4982
|
-
watchers: [],
|
|
4983
|
-
reporter: context.reportWorktreeActivity,
|
|
4984
|
-
timer: null,
|
|
4985
|
-
reporting: false,
|
|
4986
|
-
pending: false,
|
|
4987
|
-
headIdentity: null
|
|
4988
|
-
};
|
|
4989
|
-
activeWorktreeWatches.set(input.gitDirectory, entry);
|
|
4990
|
-
try {
|
|
4991
|
-
for (const target2 of /* @__PURE__ */ new Set([folder, input.gitDirectory])) {
|
|
4992
|
-
const watcher = watch(
|
|
4993
|
-
target2,
|
|
4994
|
-
{ recursive: true },
|
|
4995
|
-
() => scheduleWorktreeActivity(entry)
|
|
4996
|
-
);
|
|
4997
|
-
watcher.on("error", (error) => {
|
|
4998
|
-
console.error(
|
|
4999
|
-
`Worktree watcher failed for ${target2}:`,
|
|
5000
|
-
error instanceof Error ? error.message : error
|
|
5001
|
-
);
|
|
5002
|
-
});
|
|
5003
|
-
watcher.unref();
|
|
5004
|
-
entry.watchers.push(watcher);
|
|
5005
|
-
}
|
|
5006
|
-
scheduleWorktreeActivity(entry);
|
|
5007
|
-
} catch (error) {
|
|
5008
|
-
closeWorktreeWatch(entry);
|
|
5009
|
-
activeWorktreeWatches.delete(input.gitDirectory);
|
|
5010
|
-
throw error;
|
|
5354
|
+
async function baseMergeCommit(folder, baseBranch, timeoutMs, signal) {
|
|
5355
|
+
return requireSuccess2(
|
|
5356
|
+
await git2(
|
|
5357
|
+
folder,
|
|
5358
|
+
["merge-base", `refs/remotes/origin/${baseBranch}`, "HEAD"],
|
|
5359
|
+
timeoutMs,
|
|
5360
|
+
signal
|
|
5361
|
+
),
|
|
5362
|
+
"Could not determine the base-branch merge base"
|
|
5363
|
+
).stdout.trim();
|
|
5364
|
+
}
|
|
5365
|
+
async function validateDisplayedCommit(folder, baseBranch, commitSha, timeoutMs, signal) {
|
|
5366
|
+
if (!/^[0-9a-f]{7,64}$/i.test(commitSha)) {
|
|
5367
|
+
throw new Error("Commit SHA is invalid");
|
|
5011
5368
|
}
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
const input = worktreeJobPayload(payload);
|
|
5016
|
-
const folder = await validateWorktree(input, timeoutMs, signal);
|
|
5017
|
-
if (!input.baseBranch) throw new Error("A base branch is required");
|
|
5018
|
-
return {
|
|
5019
|
-
...successfulProcess4,
|
|
5020
|
-
detail: await inspectWorktreeDetail(
|
|
5369
|
+
const base = await baseMergeCommit(folder, baseBranch, timeoutMs, signal);
|
|
5370
|
+
const [afterBase, beforeHead] = await Promise.all([
|
|
5371
|
+
git2(
|
|
5021
5372
|
folder,
|
|
5022
|
-
|
|
5373
|
+
["merge-base", "--is-ancestor", base, commitSha],
|
|
5374
|
+
timeoutMs,
|
|
5375
|
+
signal
|
|
5376
|
+
),
|
|
5377
|
+
git2(
|
|
5378
|
+
folder,
|
|
5379
|
+
["merge-base", "--is-ancestor", commitSha, "HEAD"],
|
|
5023
5380
|
timeoutMs,
|
|
5024
5381
|
signal
|
|
5025
5382
|
)
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5383
|
+
]);
|
|
5384
|
+
if (afterBase.exitCode !== 0 || beforeHead.exitCode !== 0) {
|
|
5385
|
+
throw new Error("Commit is outside the displayed branch history");
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
async function parentCommit(folder, commitSha, timeoutMs, signal) {
|
|
5389
|
+
const result = await git2(
|
|
5390
|
+
folder,
|
|
5391
|
+
["rev-parse", `${commitSha}^`],
|
|
5392
|
+
timeoutMs,
|
|
5393
|
+
signal
|
|
5394
|
+
);
|
|
5395
|
+
return result.exitCode === 0 ? result.stdout.trim() : null;
|
|
5396
|
+
}
|
|
5397
|
+
async function gitObjectExists(folder, specification, timeoutMs, signal) {
|
|
5398
|
+
return (await git2(folder, ["cat-file", "-e", specification], timeoutMs, signal)).exitCode === 0;
|
|
5399
|
+
}
|
|
5400
|
+
async function comparisonSides(input, folder, timeoutMs, signal) {
|
|
5401
|
+
if (!input.path) return { before: null, after: null };
|
|
5402
|
+
const path = input.path;
|
|
5403
|
+
const previousPath = input.previousPath ?? path;
|
|
5404
|
+
const currentPath = join5(folder, path);
|
|
5405
|
+
if (input.scope === "UNTRACKED") {
|
|
5406
|
+
return { before: null, after: { kind: "FILE", path: currentPath } };
|
|
5407
|
+
}
|
|
5408
|
+
if (input.scope === "STAGED") {
|
|
5409
|
+
return {
|
|
5410
|
+
before: { kind: "GIT", specification: `HEAD:${previousPath}` },
|
|
5411
|
+
after: { kind: "GIT", specification: `:${path}` }
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
if (input.scope === "UNSTAGED") {
|
|
5415
|
+
return {
|
|
5416
|
+
before: { kind: "GIT", specification: `:${path}` },
|
|
5417
|
+
after: { kind: "FILE", path: currentPath }
|
|
5418
|
+
};
|
|
5419
|
+
}
|
|
5420
|
+
if (input.scope === "COMMIT") {
|
|
5421
|
+
const commitSha = input.commitSha;
|
|
5422
|
+
await validateDisplayedCommit(
|
|
5423
|
+
folder,
|
|
5424
|
+
input.baseBranch,
|
|
5425
|
+
commitSha,
|
|
5426
|
+
timeoutMs,
|
|
5427
|
+
signal
|
|
5428
|
+
);
|
|
5429
|
+
const parent = await parentCommit(folder, commitSha, timeoutMs, signal);
|
|
5430
|
+
return {
|
|
5431
|
+
before: parent ? { kind: "GIT", specification: `${parent}:${previousPath}` } : null,
|
|
5432
|
+
after: { kind: "GIT", specification: `${commitSha}:${path}` }
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
const base = await baseMergeCommit(
|
|
5436
|
+
folder,
|
|
5437
|
+
input.baseBranch,
|
|
5438
|
+
timeoutMs,
|
|
5439
|
+
signal
|
|
5440
|
+
);
|
|
5441
|
+
return {
|
|
5442
|
+
before: { kind: "GIT", specification: `${base}:${previousPath}` },
|
|
5443
|
+
after: { kind: "GIT", specification: `HEAD:${path}` }
|
|
5444
|
+
};
|
|
5445
|
+
}
|
|
5446
|
+
function requestedDiffPaths(input) {
|
|
5447
|
+
if (!input.path) return [];
|
|
5448
|
+
if (input.previousPath && ["STAGED", "COMMIT", "BRANCH"].includes(input.scope)) {
|
|
5449
|
+
return [input.previousPath, input.path];
|
|
5450
|
+
}
|
|
5451
|
+
return [input.path];
|
|
5452
|
+
}
|
|
5453
|
+
async function readBoundedFile(path, limit) {
|
|
5454
|
+
const file = await open(path, "r");
|
|
5455
|
+
try {
|
|
5456
|
+
const metadata = await file.stat();
|
|
5457
|
+
if (!metadata.isFile()) throw new Error("Changed path is not a file");
|
|
5458
|
+
if (metadata.size > limit) {
|
|
5459
|
+
return { contents: Buffer.alloc(0), oversized: true };
|
|
5460
|
+
}
|
|
5461
|
+
const buffer = Buffer.allocUnsafe(limit + 1);
|
|
5462
|
+
let length = 0;
|
|
5463
|
+
while (length < buffer.length) {
|
|
5464
|
+
const { bytesRead } = await file.read(
|
|
5465
|
+
buffer,
|
|
5466
|
+
length,
|
|
5467
|
+
buffer.length - length,
|
|
5468
|
+
length
|
|
5469
|
+
);
|
|
5470
|
+
if (bytesRead === 0) break;
|
|
5471
|
+
length += bytesRead;
|
|
5472
|
+
}
|
|
5473
|
+
return {
|
|
5474
|
+
contents: buffer.subarray(0, Math.min(length, limit)),
|
|
5475
|
+
oversized: length > limit
|
|
5476
|
+
};
|
|
5477
|
+
} finally {
|
|
5478
|
+
await file.close();
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
async function availableSide(side, folder, timeoutMs, signal) {
|
|
5482
|
+
if (!side) return false;
|
|
5483
|
+
if (side.kind === "GIT") {
|
|
5484
|
+
return gitObjectExists(folder, side.specification, timeoutMs, signal);
|
|
5485
|
+
}
|
|
5486
|
+
try {
|
|
5487
|
+
const resolved = await realpath4(side.path);
|
|
5488
|
+
const difference = relative3(folder, resolved);
|
|
5489
|
+
return difference !== ".." && !difference.startsWith("../") && (await stat4(resolved)).isFile();
|
|
5490
|
+
} catch {
|
|
5491
|
+
return false;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
|
|
5495
|
+
if (!input.path) {
|
|
5496
|
+
if (input.scope === "COMMIT") {
|
|
5497
|
+
await validateDisplayedCommit(
|
|
5498
|
+
folder,
|
|
5499
|
+
input.baseBranch,
|
|
5500
|
+
input.commitSha,
|
|
5501
|
+
timeoutMs,
|
|
5502
|
+
signal
|
|
5503
|
+
);
|
|
5504
|
+
const parent = await parentCommit(
|
|
5505
|
+
folder,
|
|
5506
|
+
input.commitSha,
|
|
5507
|
+
timeoutMs,
|
|
5508
|
+
signal
|
|
5509
|
+
);
|
|
5510
|
+
const files = await diffFiles(
|
|
5511
|
+
folder,
|
|
5512
|
+
parent ? `${parent}..${input.commitSha}` : input.commitSha,
|
|
5513
|
+
timeoutMs,
|
|
5514
|
+
signal
|
|
5515
|
+
);
|
|
5516
|
+
return { files: files.slice(0, 500), truncated: files.length > 500 };
|
|
5517
|
+
}
|
|
5518
|
+
if (input.scope === "BRANCH") {
|
|
5519
|
+
const result = await inspectBranchChanges(
|
|
5520
|
+
folder,
|
|
5521
|
+
input.baseBranch,
|
|
5522
|
+
timeoutMs,
|
|
5523
|
+
signal
|
|
5524
|
+
);
|
|
5525
|
+
return { files: result.changes, truncated: result.truncated };
|
|
5526
|
+
}
|
|
5527
|
+
throw new Error("A file path is required for this diff scope");
|
|
5528
|
+
}
|
|
5529
|
+
if (input.scope === "COMMIT") {
|
|
5530
|
+
await validateDisplayedCommit(
|
|
5531
|
+
folder,
|
|
5532
|
+
input.baseBranch,
|
|
5533
|
+
input.commitSha,
|
|
5534
|
+
timeoutMs,
|
|
5535
|
+
signal
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
await validateChangedPath(input, folder, timeoutMs, signal);
|
|
5539
|
+
const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
|
|
5540
|
+
let oversized = false;
|
|
5541
|
+
if (input.scope === "STAGED") args.push("--cached");
|
|
5542
|
+
if (input.scope === "COMMIT") {
|
|
5543
|
+
const parent = await parentCommit(
|
|
5544
|
+
folder,
|
|
5545
|
+
input.commitSha,
|
|
5546
|
+
timeoutMs,
|
|
5547
|
+
signal
|
|
5548
|
+
);
|
|
5549
|
+
args.push(parent ?? `${input.commitSha}^`, input.commitSha);
|
|
5550
|
+
} else if (input.scope === "BRANCH") {
|
|
5551
|
+
args.push(
|
|
5552
|
+
await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
|
|
5553
|
+
"HEAD"
|
|
5554
|
+
);
|
|
5555
|
+
}
|
|
5556
|
+
args.push("--", ...requestedDiffPaths(input));
|
|
5557
|
+
let patch;
|
|
5558
|
+
if (input.scope === "UNTRACKED") {
|
|
5559
|
+
const bounded = await readBoundedFile(
|
|
5560
|
+
join5(folder, input.path),
|
|
5561
|
+
MAX_DIFF_BYTES
|
|
5562
|
+
);
|
|
5563
|
+
const contents = bounded.contents;
|
|
5564
|
+
if (bounded.oversized || contents.includes(0)) {
|
|
5565
|
+
oversized = bounded.oversized;
|
|
5566
|
+
patch = "";
|
|
5567
|
+
} else {
|
|
5568
|
+
patch = [
|
|
5569
|
+
`diff --git a/${input.path} b/${input.path}`,
|
|
5570
|
+
"new file mode 100644",
|
|
5571
|
+
"--- /dev/null",
|
|
5572
|
+
`+++ b/${input.path}`,
|
|
5573
|
+
...contents.toString("utf8").split("\n").map((line) => `+${line}`)
|
|
5574
|
+
].join("\n");
|
|
5575
|
+
}
|
|
5576
|
+
} else {
|
|
5577
|
+
patch = requireSuccess2(
|
|
5578
|
+
await git2(folder, args, timeoutMs, signal),
|
|
5579
|
+
"Could not load the file diff"
|
|
5580
|
+
).stdout;
|
|
5581
|
+
}
|
|
5582
|
+
const sides = await comparisonSides(input, folder, timeoutMs, signal);
|
|
5583
|
+
const [beforeAvailable, afterAvailable] = await Promise.all([
|
|
5584
|
+
availableSide(sides.before, folder, timeoutMs, signal),
|
|
5585
|
+
availableSide(sides.after, folder, timeoutMs, signal)
|
|
5586
|
+
]);
|
|
5587
|
+
return {
|
|
5588
|
+
files: [],
|
|
5589
|
+
patch,
|
|
5590
|
+
image: imagePath(input.path),
|
|
5591
|
+
binary: !patch && !imagePath(input.path) && !oversized,
|
|
5592
|
+
truncated: oversized || Buffer.byteLength(patch) >= MAX_DIFF_BYTES,
|
|
5593
|
+
beforeAvailable,
|
|
5594
|
+
afterAvailable
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5597
|
+
async function validateChangedPath(input, folder, timeoutMs, signal) {
|
|
5598
|
+
if (!input.path) throw new Error("A changed file path is required");
|
|
5599
|
+
let result;
|
|
5600
|
+
if (input.scope === "UNTRACKED") {
|
|
5601
|
+
result = await git2(
|
|
5602
|
+
folder,
|
|
5603
|
+
["ls-files", "--others", "--exclude-standard", "-z", "--", input.path],
|
|
5604
|
+
timeoutMs,
|
|
5605
|
+
signal
|
|
5606
|
+
);
|
|
5607
|
+
} else {
|
|
5608
|
+
const args = ["diff", "--name-status", "-z"];
|
|
5609
|
+
if (input.scope === "STAGED") args.push("--cached");
|
|
5610
|
+
if (input.scope === "COMMIT") {
|
|
5611
|
+
await validateDisplayedCommit(
|
|
5612
|
+
folder,
|
|
5613
|
+
input.baseBranch,
|
|
5614
|
+
input.commitSha,
|
|
5615
|
+
timeoutMs,
|
|
5616
|
+
signal
|
|
5617
|
+
);
|
|
5618
|
+
const parent = await parentCommit(
|
|
5619
|
+
folder,
|
|
5620
|
+
input.commitSha,
|
|
5621
|
+
timeoutMs,
|
|
5622
|
+
signal
|
|
5623
|
+
);
|
|
5624
|
+
args.push(parent ?? `${input.commitSha}^`, input.commitSha);
|
|
5625
|
+
} else if (input.scope === "BRANCH") {
|
|
5626
|
+
args.push(
|
|
5627
|
+
await baseMergeCommit(folder, input.baseBranch, timeoutMs, signal),
|
|
5628
|
+
"HEAD"
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
args.push("--", ...requestedDiffPaths(input));
|
|
5632
|
+
result = await git2(folder, args, timeoutMs, signal);
|
|
5633
|
+
}
|
|
5634
|
+
requireSuccess2(result, "Could not validate the changed file");
|
|
5635
|
+
const requested = input.scope === "UNTRACKED" ? result.stdout.split("\0").filter(Boolean).includes(input.path) : parseNameStatus(result.stdout).some(
|
|
5636
|
+
(file) => file.path === input.path && (!input.previousPath || file.previousPath === input.previousPath)
|
|
5637
|
+
);
|
|
5638
|
+
if (!requested) {
|
|
5639
|
+
throw new Error("The requested file is outside the displayed diff");
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
var inspectWorktreeDiff = async (payload, timeoutMs, signal) => {
|
|
5643
|
+
const input = worktreeDiffPayload(payload);
|
|
5644
|
+
const folder = await validateWorktree(input, timeoutMs, signal);
|
|
5645
|
+
return {
|
|
5646
|
+
...successfulProcess4,
|
|
5647
|
+
diff: await inspectRequestedDiff(input, folder, timeoutMs, signal)
|
|
5648
|
+
};
|
|
5649
|
+
};
|
|
5650
|
+
function imageContentType(path) {
|
|
5651
|
+
const extension = path.slice(path.lastIndexOf(".")).toLocaleLowerCase();
|
|
5652
|
+
return {
|
|
5653
|
+
".png": "image/png",
|
|
5654
|
+
".jpg": "image/jpeg",
|
|
5655
|
+
".jpeg": "image/jpeg",
|
|
5656
|
+
".gif": "image/gif",
|
|
5657
|
+
".webp": "image/webp",
|
|
5658
|
+
".avif": "image/avif",
|
|
5659
|
+
".bmp": "image/bmp"
|
|
5660
|
+
}[extension] ?? "application/octet-stream";
|
|
5661
|
+
}
|
|
5662
|
+
async function writeGitObject(folder, specification, destination, timeoutMs, signal) {
|
|
5663
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
5664
|
+
const child = spawn4(
|
|
5665
|
+
"git",
|
|
5666
|
+
["-C", folder, "cat-file", "blob", specification],
|
|
5667
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
5668
|
+
);
|
|
5669
|
+
const output = createWriteStream(destination, { mode: 384 });
|
|
5670
|
+
let stderr = "";
|
|
5671
|
+
let settled = false;
|
|
5672
|
+
const fail = (error) => {
|
|
5673
|
+
if (settled) return;
|
|
5674
|
+
settled = true;
|
|
5675
|
+
rejectPromise(error instanceof Error ? error : new Error(String(error)));
|
|
5676
|
+
};
|
|
5677
|
+
child.stderr.on("data", (chunk) => {
|
|
5678
|
+
stderr = `${stderr}${String(chunk)}`.slice(0, 4e3);
|
|
5679
|
+
});
|
|
5680
|
+
child.stdout.pipe(output);
|
|
5681
|
+
child.once("error", fail);
|
|
5682
|
+
output.once("error", fail);
|
|
5683
|
+
const terminate = () => child.kill("SIGTERM");
|
|
5684
|
+
const timer = setTimeout(() => {
|
|
5685
|
+
terminate();
|
|
5686
|
+
fail(new Error("Image extraction timed out"));
|
|
5687
|
+
}, timeoutMs);
|
|
5688
|
+
timer.unref();
|
|
5689
|
+
const abort = () => {
|
|
5690
|
+
terminate();
|
|
5691
|
+
fail(new Error("Image extraction was cancelled"));
|
|
5692
|
+
};
|
|
5693
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5694
|
+
child.once("close", (code) => {
|
|
5695
|
+
clearTimeout(timer);
|
|
5696
|
+
signal.removeEventListener("abort", abort);
|
|
5697
|
+
if (settled) return;
|
|
5698
|
+
if (code !== 0) {
|
|
5699
|
+
fail(new Error(cleanError2(stderr || "Could not extract image")));
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
output.end(() => {
|
|
5703
|
+
if (settled) return;
|
|
5704
|
+
settled = true;
|
|
5705
|
+
resolvePromise();
|
|
5706
|
+
});
|
|
5707
|
+
});
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5710
|
+
var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
5711
|
+
const input = worktreeDiffPayload(payload);
|
|
5712
|
+
if (!input.path || !input.uploadId || !input.side) {
|
|
5713
|
+
throw new Error("Diff image path, side, and upload ID are required");
|
|
5714
|
+
}
|
|
5715
|
+
if (!imagePath(input.path)) throw new Error("Diff asset is not an image");
|
|
5716
|
+
if (!context?.uploadBuildArtifact) {
|
|
5717
|
+
throw new Error("This agent cannot upload diff images");
|
|
5718
|
+
}
|
|
5719
|
+
const folder = await validateWorktree(input, timeoutMs, signal);
|
|
5720
|
+
await validateChangedPath(input, folder, timeoutMs, signal);
|
|
5721
|
+
const sides = await comparisonSides(input, folder, timeoutMs, signal);
|
|
5722
|
+
const selected = input.side === "BEFORE" ? sides.before : sides.after;
|
|
5723
|
+
if (!selected || !await availableSide(selected, folder, timeoutMs, signal)) {
|
|
5724
|
+
throw new Error("The requested image side is unavailable");
|
|
5725
|
+
}
|
|
5726
|
+
let uploadPath;
|
|
5727
|
+
let temporaryDirectory = null;
|
|
5728
|
+
try {
|
|
5729
|
+
if (selected.kind === "FILE") {
|
|
5730
|
+
uploadPath = await realpath4(selected.path);
|
|
5731
|
+
const difference = relative3(folder, uploadPath);
|
|
5732
|
+
if (difference === ".." || difference.startsWith("../")) {
|
|
5733
|
+
throw new Error("Diff image resolves outside the worktree");
|
|
5734
|
+
}
|
|
5735
|
+
} else {
|
|
5736
|
+
const size = requireSuccess2(
|
|
5737
|
+
await git2(
|
|
5738
|
+
folder,
|
|
5739
|
+
["cat-file", "-s", selected.specification],
|
|
5740
|
+
timeoutMs,
|
|
5741
|
+
signal
|
|
5742
|
+
),
|
|
5743
|
+
"Could not inspect diff image"
|
|
5744
|
+
).stdout.trim();
|
|
5745
|
+
if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
|
|
5746
|
+
throw new Error("Diff image exceeds the 20 MiB limit");
|
|
5747
|
+
}
|
|
5748
|
+
temporaryDirectory = await mkdtemp(join5(tmpdir(), "ade-diff-image-"));
|
|
5749
|
+
uploadPath = join5(temporaryDirectory, basename2(input.path));
|
|
5750
|
+
await writeGitObject(
|
|
5751
|
+
folder,
|
|
5752
|
+
selected.specification,
|
|
5753
|
+
uploadPath,
|
|
5754
|
+
timeoutMs,
|
|
5755
|
+
signal
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
const information = await stat4(uploadPath);
|
|
5759
|
+
if (!information.isFile() || information.size > 20 * 1024 * 1024) {
|
|
5760
|
+
throw new Error("Diff image exceeds the 20 MiB limit");
|
|
5761
|
+
}
|
|
5762
|
+
await context.uploadBuildArtifact({
|
|
5763
|
+
uploadId: input.uploadId,
|
|
5764
|
+
path: uploadPath,
|
|
5765
|
+
filename: basename2(input.path),
|
|
5766
|
+
contentType: imageContentType(input.path)
|
|
5767
|
+
});
|
|
5768
|
+
return successfulProcess4;
|
|
5769
|
+
} finally {
|
|
5770
|
+
if (temporaryDirectory) {
|
|
5771
|
+
await rm3(temporaryDirectory, { recursive: true, force: true });
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
};
|
|
5775
|
+
var watchWorktree = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
5776
|
+
const input = worktreeWatchJobPayload(payload);
|
|
5777
|
+
const current = activeWorktreeWatches.get(input.gitDirectory);
|
|
5778
|
+
if (input.action === "STOP") {
|
|
5779
|
+
if (current?.watchId === input.watchId) {
|
|
5780
|
+
closeWorktreeWatch(current);
|
|
5781
|
+
activeWorktreeWatches.delete(input.gitDirectory);
|
|
5782
|
+
}
|
|
5783
|
+
return successfulProcess4;
|
|
5784
|
+
}
|
|
5785
|
+
if (!context) throw new Error("Worktree activity reporting is unavailable");
|
|
5786
|
+
const folder = await validateWorktree(input, timeoutMs, signal);
|
|
5787
|
+
if (current?.watchId === input.watchId) return successfulProcess4;
|
|
5788
|
+
if (current) {
|
|
5789
|
+
closeWorktreeWatch(current);
|
|
5790
|
+
activeWorktreeWatches.delete(input.gitDirectory);
|
|
5791
|
+
}
|
|
5792
|
+
const entry = {
|
|
5793
|
+
watchId: input.watchId,
|
|
5794
|
+
codebaseId: input.codebaseId,
|
|
5795
|
+
gitDirectory: input.gitDirectory,
|
|
5796
|
+
folder,
|
|
5797
|
+
baseBranch: input.baseBranch,
|
|
5798
|
+
timeoutMs: Math.min(timeoutMs, 3e4),
|
|
5799
|
+
watchers: [],
|
|
5800
|
+
reporter: context.reportWorktreeActivity,
|
|
5801
|
+
timer: null,
|
|
5802
|
+
reporting: false,
|
|
5803
|
+
pending: false,
|
|
5804
|
+
headIdentity: null
|
|
5805
|
+
};
|
|
5806
|
+
activeWorktreeWatches.set(input.gitDirectory, entry);
|
|
5807
|
+
try {
|
|
5808
|
+
for (const target2 of /* @__PURE__ */ new Set([folder, input.gitDirectory])) {
|
|
5809
|
+
const watcher = watch(
|
|
5810
|
+
target2,
|
|
5811
|
+
{ recursive: true },
|
|
5812
|
+
() => scheduleWorktreeActivity(entry)
|
|
5813
|
+
);
|
|
5814
|
+
watcher.on("error", (error) => {
|
|
5815
|
+
console.error(
|
|
5816
|
+
`Worktree watcher failed for ${target2}:`,
|
|
5817
|
+
error instanceof Error ? error.message : error
|
|
5818
|
+
);
|
|
5819
|
+
});
|
|
5820
|
+
watcher.unref();
|
|
5821
|
+
entry.watchers.push(watcher);
|
|
5822
|
+
}
|
|
5823
|
+
scheduleWorktreeActivity(entry);
|
|
5824
|
+
} catch (error) {
|
|
5825
|
+
closeWorktreeWatch(entry);
|
|
5826
|
+
activeWorktreeWatches.delete(input.gitDirectory);
|
|
5827
|
+
throw error;
|
|
5828
|
+
}
|
|
5829
|
+
return successfulProcess4;
|
|
5830
|
+
};
|
|
5831
|
+
var inspectWorktree = async (payload, timeoutMs, signal) => {
|
|
5832
|
+
const input = worktreeJobPayload(payload);
|
|
5833
|
+
const folder = await validateWorktree(input, timeoutMs, signal);
|
|
5834
|
+
if (!input.baseBranch) throw new Error("A base branch is required");
|
|
5835
|
+
return {
|
|
5836
|
+
...successfulProcess4,
|
|
5837
|
+
detail: await inspectWorktreeDetail(
|
|
5838
|
+
folder,
|
|
5839
|
+
input.baseBranch,
|
|
5840
|
+
timeoutMs,
|
|
5841
|
+
signal
|
|
5842
|
+
)
|
|
5843
|
+
};
|
|
5844
|
+
};
|
|
5845
|
+
async function refExists(folder, ref, timeoutMs, signal) {
|
|
5846
|
+
const result = await git2(
|
|
5030
5847
|
folder,
|
|
5031
5848
|
["show-ref", "--verify", "--quiet", ref],
|
|
5032
5849
|
timeoutMs,
|
|
@@ -5752,30 +6569,32 @@ var operateWorktree = async (payload, timeoutMs, signal) => {
|
|
|
5752
6569
|
};
|
|
5753
6570
|
|
|
5754
6571
|
// src/handlers/builds.ts
|
|
5755
|
-
import { createHash as
|
|
6572
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
5756
6573
|
import {
|
|
5757
6574
|
cp,
|
|
5758
6575
|
mkdir as mkdir3,
|
|
5759
|
-
mkdtemp,
|
|
6576
|
+
mkdtemp as mkdtemp2,
|
|
6577
|
+
open as open2,
|
|
5760
6578
|
readdir as readdir4,
|
|
5761
6579
|
readFile as readFile4,
|
|
5762
6580
|
realpath as realpath5,
|
|
5763
|
-
|
|
6581
|
+
rename as rename3,
|
|
6582
|
+
rm as rm4,
|
|
5764
6583
|
stat as stat5,
|
|
5765
6584
|
writeFile as writeFile3
|
|
5766
6585
|
} from "node:fs/promises";
|
|
5767
|
-
import { createWriteStream } from "node:fs";
|
|
5768
|
-
import { tmpdir } from "node:os";
|
|
6586
|
+
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
6587
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
5769
6588
|
import {
|
|
5770
6589
|
basename as basename3,
|
|
5771
6590
|
dirname as dirname6,
|
|
5772
|
-
isAbsolute as
|
|
6591
|
+
isAbsolute as isAbsolute4,
|
|
5773
6592
|
join as join6,
|
|
5774
|
-
relative as
|
|
5775
|
-
resolve as
|
|
5776
|
-
sep as
|
|
6593
|
+
relative as relative4,
|
|
6594
|
+
resolve as resolve5,
|
|
6595
|
+
sep as sep3
|
|
5777
6596
|
} from "node:path";
|
|
5778
|
-
import { spawn as
|
|
6597
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
5779
6598
|
var successfulProcess5 = {
|
|
5780
6599
|
exitCode: 0,
|
|
5781
6600
|
signal: null,
|
|
@@ -5806,6 +6625,22 @@ function command2(executable, args, timeoutMs, signal, cwd, env = process.env) {
|
|
|
5806
6625
|
env
|
|
5807
6626
|
});
|
|
5808
6627
|
}
|
|
6628
|
+
async function commandToFile(executable, args, timeoutMs, signal, cwd, env, destination) {
|
|
6629
|
+
const output = await open2(destination, "wx", 384);
|
|
6630
|
+
try {
|
|
6631
|
+
return await captureCommand({
|
|
6632
|
+
command: executable,
|
|
6633
|
+
args,
|
|
6634
|
+
timeoutMs,
|
|
6635
|
+
signal,
|
|
6636
|
+
cwd,
|
|
6637
|
+
env,
|
|
6638
|
+
stdoutFileDescriptor: output.fd
|
|
6639
|
+
});
|
|
6640
|
+
} finally {
|
|
6641
|
+
await output.close();
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
5809
6644
|
function requireSuccess3(result, fallback) {
|
|
5810
6645
|
if (result.cancelled) throw new Error("Operation was cancelled");
|
|
5811
6646
|
if (result.timedOut) throw new Error("Operation timed out");
|
|
@@ -5848,19 +6683,19 @@ async function validateWorktree2(input, timeoutMs, signal) {
|
|
|
5848
6683
|
}
|
|
5849
6684
|
return folder;
|
|
5850
6685
|
}
|
|
5851
|
-
function
|
|
5852
|
-
const target2 =
|
|
5853
|
-
const difference =
|
|
5854
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
6686
|
+
function containedPath2(root, relativePath) {
|
|
6687
|
+
const target2 = resolve5(root, relativePath);
|
|
6688
|
+
const difference = relative4(root, target2);
|
|
6689
|
+
if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
|
|
5855
6690
|
throw new Error("Path must stay within the worktree");
|
|
5856
6691
|
}
|
|
5857
6692
|
return target2;
|
|
5858
6693
|
}
|
|
5859
6694
|
async function validateSource(root, source) {
|
|
5860
|
-
const target2 =
|
|
6695
|
+
const target2 = containedPath2(root, source.relativePath);
|
|
5861
6696
|
const resolved = await realpath5(target2);
|
|
5862
|
-
const difference =
|
|
5863
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
6697
|
+
const difference = relative4(root, resolved);
|
|
6698
|
+
if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
|
|
5864
6699
|
throw new Error("Build source resolves outside the worktree");
|
|
5865
6700
|
}
|
|
5866
6701
|
const information = await stat5(resolved);
|
|
@@ -5880,7 +6715,7 @@ async function discoverSourcesInFolder(folder) {
|
|
|
5880
6715
|
for (const entry of await readdir4(current, { withFileTypes: true })) {
|
|
5881
6716
|
if (entry.isSymbolicLink()) continue;
|
|
5882
6717
|
const absolute = join6(current, entry.name);
|
|
5883
|
-
const path =
|
|
6718
|
+
const path = relative4(folder, absolute).split(sep3).join("/");
|
|
5884
6719
|
if (entry.isDirectory()) {
|
|
5885
6720
|
if (entry.name.endsWith(".xcodeproj")) {
|
|
5886
6721
|
sources.push({ kind: "PROJECT", relativePath: path });
|
|
@@ -5971,15 +6806,15 @@ async function workspaceProjectPaths(workspacePath2, folder) {
|
|
|
5971
6806
|
}).filter((path) => path.endsWith(".xcodeproj"));
|
|
5972
6807
|
const projects = [];
|
|
5973
6808
|
for (const location of locations) {
|
|
5974
|
-
const candidate =
|
|
5975
|
-
const difference =
|
|
5976
|
-
if (difference === ".." || difference.startsWith(`..${
|
|
6809
|
+
const candidate = resolve5(dirname6(workspacePath2), location);
|
|
6810
|
+
const difference = relative4(folder, candidate);
|
|
6811
|
+
if (difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
|
|
5977
6812
|
continue;
|
|
5978
6813
|
}
|
|
5979
6814
|
try {
|
|
5980
6815
|
const resolved = await realpath5(candidate);
|
|
5981
|
-
const resolvedDifference =
|
|
5982
|
-
if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${
|
|
6816
|
+
const resolvedDifference = relative4(folder, resolved);
|
|
6817
|
+
if (resolvedDifference === ".." || resolvedDifference.startsWith(`..${sep3}`) || isAbsolute4(resolvedDifference) || !(await stat5(resolved)).isDirectory()) {
|
|
5983
6818
|
continue;
|
|
5984
6819
|
}
|
|
5985
6820
|
projects.push(resolved);
|
|
@@ -6194,7 +7029,7 @@ function genericBuildDestinations(action) {
|
|
|
6194
7029
|
];
|
|
6195
7030
|
}
|
|
6196
7031
|
async function listPhysicalDevices(timeoutMs, signal) {
|
|
6197
|
-
const directory = await
|
|
7032
|
+
const directory = await mkdtemp2(join6(tmpdir2(), "ade-devices-"));
|
|
6198
7033
|
const output = join6(directory, "devices.json");
|
|
6199
7034
|
try {
|
|
6200
7035
|
const result = await command2(
|
|
@@ -6215,7 +7050,7 @@ async function listPhysicalDevices(timeoutMs, signal) {
|
|
|
6215
7050
|
if (result.exitCode !== 0) return [];
|
|
6216
7051
|
return physicalDestinations(JSON.parse(await readFile4(output, "utf8")));
|
|
6217
7052
|
} finally {
|
|
6218
|
-
await
|
|
7053
|
+
await rm4(directory, { recursive: true, force: true });
|
|
6219
7054
|
}
|
|
6220
7055
|
}
|
|
6221
7056
|
var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
|
|
@@ -6239,12 +7074,6 @@ var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
|
|
|
6239
7074
|
folder
|
|
6240
7075
|
);
|
|
6241
7076
|
requireSuccess3(preflight, "The saved scheme or configuration is unavailable");
|
|
6242
|
-
if (GENERIC_BUILD_DESTINATION_ACTIONS.includes(input.action)) {
|
|
6243
|
-
return {
|
|
6244
|
-
...successfulProcess5,
|
|
6245
|
-
destinations: genericBuildDestinations(input.action)
|
|
6246
|
-
};
|
|
6247
|
-
}
|
|
6248
7077
|
const [simulators, physical] = await Promise.all([
|
|
6249
7078
|
command2(
|
|
6250
7079
|
"xcrun",
|
|
@@ -6258,6 +7087,7 @@ var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
|
|
|
6258
7087
|
return {
|
|
6259
7088
|
...successfulProcess5,
|
|
6260
7089
|
destinations: [
|
|
7090
|
+
...genericBuildDestinations(input.action),
|
|
6261
7091
|
...simulators.exitCode === 0 ? simulatorDestinations(JSON.parse(simulators.stdout)) : [],
|
|
6262
7092
|
...physical
|
|
6263
7093
|
]
|
|
@@ -6311,7 +7141,7 @@ var BuildLogger = class {
|
|
|
6311
7141
|
constructor(buildId, context, rawLogPath, env) {
|
|
6312
7142
|
this.buildId = buildId;
|
|
6313
7143
|
this.context = context;
|
|
6314
|
-
this.stream =
|
|
7144
|
+
this.stream = createWriteStream2(rawLogPath, { flags: "a", mode: 384 });
|
|
6315
7145
|
this.redact = createRedactor(env);
|
|
6316
7146
|
}
|
|
6317
7147
|
buildId;
|
|
@@ -6401,12 +7231,12 @@ function runLoggedCommand(options) {
|
|
|
6401
7231
|
let killTimer = null;
|
|
6402
7232
|
let extraStream;
|
|
6403
7233
|
if (options.additionalLogPath) {
|
|
6404
|
-
extraStream =
|
|
7234
|
+
extraStream = createWriteStream2(options.additionalLogPath, {
|
|
6405
7235
|
flags: "a",
|
|
6406
7236
|
mode: 384
|
|
6407
7237
|
});
|
|
6408
7238
|
}
|
|
6409
|
-
const child =
|
|
7239
|
+
const child = spawn5(options.command, options.args, {
|
|
6410
7240
|
cwd: options.cwd,
|
|
6411
7241
|
env: options.env,
|
|
6412
7242
|
shell: false,
|
|
@@ -6599,7 +7429,7 @@ function actionArgument(action) {
|
|
|
6599
7429
|
}
|
|
6600
7430
|
function xcodeBuildArguments(input) {
|
|
6601
7431
|
const resultBundle = join6(input.artifactDirectory, "result.xcresult");
|
|
6602
|
-
const sourcePath =
|
|
7432
|
+
const sourcePath = containedPath2(input.folder, input.source.relativePath);
|
|
6603
7433
|
const usesCapturedTestProducts = input.action === "TEST_WITHOUT_BUILDING";
|
|
6604
7434
|
const args = [
|
|
6605
7435
|
"xcodebuild",
|
|
@@ -6646,7 +7476,7 @@ function xcodeBuildSettingsArguments(input, folder = input.folder) {
|
|
|
6646
7476
|
"xcodebuild",
|
|
6647
7477
|
...sourceArguments(
|
|
6648
7478
|
input.source,
|
|
6649
|
-
|
|
7479
|
+
containedPath2(folder, input.source.relativePath)
|
|
6650
7480
|
),
|
|
6651
7481
|
"-scheme",
|
|
6652
7482
|
input.scheme,
|
|
@@ -6749,7 +7579,7 @@ if (typeof hookModule.default === "function") {
|
|
|
6749
7579
|
exitCode: result.exitCode,
|
|
6750
7580
|
durationMs: Date.now() - started,
|
|
6751
7581
|
causedBuildFailure: failed && options.script.failureBehavior === "FAIL_BUILD",
|
|
6752
|
-
outputRelativePath:
|
|
7582
|
+
outputRelativePath: relative4(options.input.artifactDirectory, log),
|
|
6753
7583
|
error: failed ? cleanError3(result.output || "Build hook failed") : null
|
|
6754
7584
|
};
|
|
6755
7585
|
} catch (hookError) {
|
|
@@ -6762,7 +7592,7 @@ if (typeof hookModule.default === "function") {
|
|
|
6762
7592
|
exitCode: null,
|
|
6763
7593
|
durationMs: Date.now() - started,
|
|
6764
7594
|
causedBuildFailure: !options.signal.aborted && options.script.failureBehavior === "FAIL_BUILD",
|
|
6765
|
-
outputRelativePath:
|
|
7595
|
+
outputRelativePath: relative4(options.input.artifactDirectory, log),
|
|
6766
7596
|
error
|
|
6767
7597
|
};
|
|
6768
7598
|
}
|
|
@@ -6791,7 +7621,7 @@ async function fileChecksum(path) {
|
|
|
6791
7621
|
const information = await stat5(path);
|
|
6792
7622
|
if (!information.isFile() || information.size > 256 * 1024 * 1024)
|
|
6793
7623
|
return null;
|
|
6794
|
-
return
|
|
7624
|
+
return createHash3("sha256").update(await readFile4(path)).digest("hex");
|
|
6795
7625
|
} catch {
|
|
6796
7626
|
return null;
|
|
6797
7627
|
}
|
|
@@ -6799,12 +7629,398 @@ async function fileChecksum(path) {
|
|
|
6799
7629
|
async function artifact(root, kind, path, metadata = {}) {
|
|
6800
7630
|
return {
|
|
6801
7631
|
kind,
|
|
6802
|
-
relativePath:
|
|
7632
|
+
relativePath: relative4(root, path).split(sep3).join("/"),
|
|
6803
7633
|
sizeBytes: await pathSize(path),
|
|
6804
7634
|
checksum: await fileChecksum(path),
|
|
6805
7635
|
metadata
|
|
6806
7636
|
};
|
|
6807
7637
|
}
|
|
7638
|
+
function jsonObject(value) {
|
|
7639
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7640
|
+
}
|
|
7641
|
+
function testDetails(node) {
|
|
7642
|
+
const own = typeof node.details === "string" && node.details.trim() ? [node.details.trim()] : typeof node.name === "string" && ["Failure Message", "Runtime Warning"].includes(String(node.nodeType)) ? [node.name] : [];
|
|
7643
|
+
const children = Array.isArray(node.children) ? node.children : [];
|
|
7644
|
+
return [
|
|
7645
|
+
...own,
|
|
7646
|
+
...children.flatMap((child) => {
|
|
7647
|
+
const object = jsonObject(child);
|
|
7648
|
+
return object ? testDetails(object) : [];
|
|
7649
|
+
})
|
|
7650
|
+
];
|
|
7651
|
+
}
|
|
7652
|
+
function testSourceFile(node, suite) {
|
|
7653
|
+
const explicit = [
|
|
7654
|
+
node.filePath,
|
|
7655
|
+
node.sourceFilePath,
|
|
7656
|
+
node.fileName,
|
|
7657
|
+
node.sourceFile
|
|
7658
|
+
].find((value) => typeof value === "string" && !!value);
|
|
7659
|
+
if (explicit) {
|
|
7660
|
+
return { file: basename3(explicit), filePath: explicit };
|
|
7661
|
+
}
|
|
7662
|
+
const identifier = typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : "";
|
|
7663
|
+
const identifierOwner = identifier.split("/")[0]?.trim();
|
|
7664
|
+
return {
|
|
7665
|
+
file: identifierOwner || suite,
|
|
7666
|
+
filePath: null
|
|
7667
|
+
};
|
|
7668
|
+
}
|
|
7669
|
+
function normalizeTestResults(value) {
|
|
7670
|
+
const root = jsonObject(value);
|
|
7671
|
+
if (!root || !Array.isArray(root.testNodes)) {
|
|
7672
|
+
throw new Error("xcresulttool returned invalid test results JSON");
|
|
7673
|
+
}
|
|
7674
|
+
const tests = [];
|
|
7675
|
+
const visit = (raw, context) => {
|
|
7676
|
+
const node = jsonObject(raw);
|
|
7677
|
+
if (!node) return;
|
|
7678
|
+
const nodeType = typeof node.nodeType === "string" ? node.nodeType : "";
|
|
7679
|
+
const name = typeof node.name === "string" ? node.name : "";
|
|
7680
|
+
const next = {
|
|
7681
|
+
plan: nodeType === "Test Plan" ? name : context.plan,
|
|
7682
|
+
configuration: nodeType === "Test Plan Configuration" ? name : context.configuration,
|
|
7683
|
+
bundle: /test bundle$/.test(nodeType) ? name : context.bundle,
|
|
7684
|
+
suite: nodeType === "Test Suite" ? name : context.suite
|
|
7685
|
+
};
|
|
7686
|
+
if (nodeType === "Test Case") {
|
|
7687
|
+
const result = typeof node.result === "string" ? node.result : "unknown";
|
|
7688
|
+
const sourceFile = testSourceFile(node, next.suite);
|
|
7689
|
+
tests.push({
|
|
7690
|
+
identifier: typeof node.nodeIdentifier === "string" ? node.nodeIdentifier : name,
|
|
7691
|
+
name,
|
|
7692
|
+
...next,
|
|
7693
|
+
...sourceFile,
|
|
7694
|
+
result,
|
|
7695
|
+
durationSeconds: typeof node.durationInSeconds === "number" ? node.durationInSeconds : null,
|
|
7696
|
+
tags: Array.isArray(node.tags) ? node.tags.filter((tag) => typeof tag === "string") : [],
|
|
7697
|
+
details: [...new Set(testDetails(node))]
|
|
7698
|
+
});
|
|
7699
|
+
}
|
|
7700
|
+
if (Array.isArray(node.children)) {
|
|
7701
|
+
for (const child of node.children) visit(child, next);
|
|
7702
|
+
}
|
|
7703
|
+
};
|
|
7704
|
+
for (const node of root.testNodes) {
|
|
7705
|
+
visit(node, {
|
|
7706
|
+
plan: null,
|
|
7707
|
+
configuration: null,
|
|
7708
|
+
bundle: null,
|
|
7709
|
+
suite: null
|
|
7710
|
+
});
|
|
7711
|
+
}
|
|
7712
|
+
const count = (result) => tests.filter((test) => test.result === result).length;
|
|
7713
|
+
return {
|
|
7714
|
+
summary: {
|
|
7715
|
+
total: tests.length,
|
|
7716
|
+
passed: count("Passed"),
|
|
7717
|
+
failed: count("Failed"),
|
|
7718
|
+
skipped: count("Skipped"),
|
|
7719
|
+
expectedFailures: count("Expected Failure"),
|
|
7720
|
+
unknown: tests.filter(
|
|
7721
|
+
(test) => !["Passed", "Failed", "Skipped", "Expected Failure"].includes(
|
|
7722
|
+
String(test.result)
|
|
7723
|
+
)
|
|
7724
|
+
).length,
|
|
7725
|
+
durationSeconds: tests.reduce(
|
|
7726
|
+
(total, test) => total + Number(test.durationSeconds ?? 0),
|
|
7727
|
+
0
|
|
7728
|
+
)
|
|
7729
|
+
},
|
|
7730
|
+
data: {
|
|
7731
|
+
devices: Array.isArray(root.devices) ? root.devices : [],
|
|
7732
|
+
configurations: Array.isArray(root.testPlanConfigurations) ? root.testPlanConfigurations : [],
|
|
7733
|
+
tests
|
|
7734
|
+
}
|
|
7735
|
+
};
|
|
7736
|
+
}
|
|
7737
|
+
function normalizeCoverage(value) {
|
|
7738
|
+
const root = jsonObject(value);
|
|
7739
|
+
if (!root || !Array.isArray(root.targets)) {
|
|
7740
|
+
throw new Error("xccov returned invalid coverage JSON");
|
|
7741
|
+
}
|
|
7742
|
+
const files = root.targets.flatMap((rawTarget) => {
|
|
7743
|
+
const target2 = jsonObject(rawTarget);
|
|
7744
|
+
if (!target2 || !Array.isArray(target2.files)) return [];
|
|
7745
|
+
const targetName = typeof target2.name === "string" ? target2.name : "";
|
|
7746
|
+
return target2.files.flatMap((rawFile) => {
|
|
7747
|
+
const file = jsonObject(rawFile);
|
|
7748
|
+
if (!file) return [];
|
|
7749
|
+
return [
|
|
7750
|
+
{
|
|
7751
|
+
target: targetName,
|
|
7752
|
+
name: typeof file.name === "string" ? file.name : "",
|
|
7753
|
+
path: typeof file.path === "string" ? file.path : "",
|
|
7754
|
+
coveredLines: typeof file.coveredLines === "number" ? file.coveredLines : 0,
|
|
7755
|
+
executableLines: typeof file.executableLines === "number" ? file.executableLines : 0,
|
|
7756
|
+
lineCoverage: typeof file.lineCoverage === "number" ? file.lineCoverage : 0
|
|
7757
|
+
}
|
|
7758
|
+
];
|
|
7759
|
+
});
|
|
7760
|
+
});
|
|
7761
|
+
return {
|
|
7762
|
+
summary: {
|
|
7763
|
+
coveredLines: typeof root.coveredLines === "number" ? root.coveredLines : 0,
|
|
7764
|
+
executableLines: typeof root.executableLines === "number" ? root.executableLines : 0,
|
|
7765
|
+
lineCoverage: typeof root.lineCoverage === "number" ? root.lineCoverage : 0,
|
|
7766
|
+
targetCount: root.targets.length,
|
|
7767
|
+
fileCount: files.length
|
|
7768
|
+
},
|
|
7769
|
+
data: { files }
|
|
7770
|
+
};
|
|
7771
|
+
}
|
|
7772
|
+
async function writeJsonReport(input, kind, timeoutMs, signal) {
|
|
7773
|
+
const resultBundle = join6(input.artifactDirectory, "result.xcresult");
|
|
7774
|
+
const filename = kind === "TEST_RESULTS" ? "test-results.json" : "code-coverage.json";
|
|
7775
|
+
const destination = join6(input.artifactDirectory, filename);
|
|
7776
|
+
const temporary = `${destination}.tmp-${randomUUID2()}`;
|
|
7777
|
+
try {
|
|
7778
|
+
if (!await pathExists(resultBundle)) {
|
|
7779
|
+
throw new Error("The build result bundle is unavailable");
|
|
7780
|
+
}
|
|
7781
|
+
const args = kind === "TEST_RESULTS" ? [
|
|
7782
|
+
"xcresulttool",
|
|
7783
|
+
"get",
|
|
7784
|
+
"test-results",
|
|
7785
|
+
"tests",
|
|
7786
|
+
"--path",
|
|
7787
|
+
"result.xcresult",
|
|
7788
|
+
"--format",
|
|
7789
|
+
"json"
|
|
7790
|
+
] : ["xccov", "view", "--report", "--json", "result.xcresult"];
|
|
7791
|
+
requireSuccess3(
|
|
7792
|
+
await commandToFile(
|
|
7793
|
+
"xcrun",
|
|
7794
|
+
args,
|
|
7795
|
+
timeoutMs,
|
|
7796
|
+
signal,
|
|
7797
|
+
input.artifactDirectory,
|
|
7798
|
+
xcodeEnvironment(),
|
|
7799
|
+
temporary
|
|
7800
|
+
),
|
|
7801
|
+
`Could not generate ${filename}`
|
|
7802
|
+
);
|
|
7803
|
+
const serialized = await readFile4(temporary, "utf8");
|
|
7804
|
+
const parsed = JSON.parse(serialized);
|
|
7805
|
+
const normalized = kind === "TEST_RESULTS" ? normalizeTestResults(parsed) : normalizeCoverage(parsed);
|
|
7806
|
+
await rename3(temporary, destination);
|
|
7807
|
+
return {
|
|
7808
|
+
kind,
|
|
7809
|
+
status: "READY",
|
|
7810
|
+
artifact: await artifact(
|
|
7811
|
+
input.artifactDirectory,
|
|
7812
|
+
kind === "TEST_RESULTS" ? "TEST_RESULTS_JSON" : "CODE_COVERAGE_JSON",
|
|
7813
|
+
destination
|
|
7814
|
+
),
|
|
7815
|
+
...normalized,
|
|
7816
|
+
error: null
|
|
7817
|
+
};
|
|
7818
|
+
} catch (error) {
|
|
7819
|
+
await rm4(temporary, { force: true });
|
|
7820
|
+
return {
|
|
7821
|
+
kind,
|
|
7822
|
+
status: "FAILED",
|
|
7823
|
+
artifact: null,
|
|
7824
|
+
summary: {},
|
|
7825
|
+
data: {},
|
|
7826
|
+
error: cleanError3(error)
|
|
7827
|
+
};
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
async function gitCommand(folder, args, timeoutMs, signal) {
|
|
7831
|
+
return command2("git", ["-C", folder, ...args], timeoutMs, signal, void 0, {
|
|
7832
|
+
...process.env,
|
|
7833
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
7834
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
function changedLinesFromPatch(value) {
|
|
7838
|
+
const result = /* @__PURE__ */ new Map();
|
|
7839
|
+
let path = null;
|
|
7840
|
+
for (const line of value.split("\n")) {
|
|
7841
|
+
if (line.startsWith("+++ ")) {
|
|
7842
|
+
const raw = line.slice(4);
|
|
7843
|
+
path = raw === "/dev/null" ? null : raw.replace(/^b\//, "");
|
|
7844
|
+
if (path && !result.has(path)) result.set(path, /* @__PURE__ */ new Set());
|
|
7845
|
+
continue;
|
|
7846
|
+
}
|
|
7847
|
+
if (!path || !line.startsWith("@@")) continue;
|
|
7848
|
+
const match = /\+(\d+)(?:,(\d+))?/.exec(line);
|
|
7849
|
+
if (!match) continue;
|
|
7850
|
+
const start = Number(match[1]);
|
|
7851
|
+
const count = match[2] === void 0 ? 1 : Number(match[2]);
|
|
7852
|
+
const lines = result.get(path);
|
|
7853
|
+
for (let number = start; number < start + count; number += 1) {
|
|
7854
|
+
lines.add(number);
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
return result;
|
|
7858
|
+
}
|
|
7859
|
+
function changeTypesFromStatus(value) {
|
|
7860
|
+
const entries = value.split("\0").filter(Boolean);
|
|
7861
|
+
const result = /* @__PURE__ */ new Map();
|
|
7862
|
+
for (let index = 0; index < entries.length; ) {
|
|
7863
|
+
const status2 = entries[index++] ?? "M";
|
|
7864
|
+
const renamed = status2.startsWith("R") || status2.startsWith("C");
|
|
7865
|
+
const first = entries[index++] ?? "";
|
|
7866
|
+
const second = renamed ? entries[index++] ?? "" : null;
|
|
7867
|
+
const path = second ?? first;
|
|
7868
|
+
if (path) result.set(path, status2[0] ?? "M");
|
|
7869
|
+
}
|
|
7870
|
+
return result;
|
|
7871
|
+
}
|
|
7872
|
+
async function snapshotCoverageChanges(input, folder, timeoutMs, signal) {
|
|
7873
|
+
if (!input.baseBranch) throw new Error("A base branch is required");
|
|
7874
|
+
const base = requireSuccess3(
|
|
7875
|
+
await gitCommand(
|
|
7876
|
+
folder,
|
|
7877
|
+
["merge-base", `refs/remotes/origin/${input.baseBranch}`, "HEAD"],
|
|
7878
|
+
timeoutMs,
|
|
7879
|
+
signal
|
|
7880
|
+
),
|
|
7881
|
+
"Could not determine the coverage merge base"
|
|
7882
|
+
).stdout.trim();
|
|
7883
|
+
const [patch, status2, untracked] = await Promise.all([
|
|
7884
|
+
gitCommand(
|
|
7885
|
+
folder,
|
|
7886
|
+
["diff", "--no-color", "--unified=0", base, "--"],
|
|
7887
|
+
timeoutMs,
|
|
7888
|
+
signal
|
|
7889
|
+
),
|
|
7890
|
+
gitCommand(
|
|
7891
|
+
folder,
|
|
7892
|
+
["diff", "--name-status", "-z", base, "--"],
|
|
7893
|
+
timeoutMs,
|
|
7894
|
+
signal
|
|
7895
|
+
),
|
|
7896
|
+
gitCommand(
|
|
7897
|
+
folder,
|
|
7898
|
+
["ls-files", "--others", "--exclude-standard", "-z"],
|
|
7899
|
+
timeoutMs,
|
|
7900
|
+
signal
|
|
7901
|
+
)
|
|
7902
|
+
]);
|
|
7903
|
+
requireSuccess3(patch, "Could not inspect changed coverage lines");
|
|
7904
|
+
requireSuccess3(status2, "Could not inspect coverage change types");
|
|
7905
|
+
requireSuccess3(untracked, "Could not inspect untracked coverage files");
|
|
7906
|
+
const lines = changedLinesFromPatch(patch.stdout);
|
|
7907
|
+
const types = changeTypesFromStatus(status2.stdout);
|
|
7908
|
+
for (const path of untracked.stdout.split("\0").filter(Boolean)) {
|
|
7909
|
+
try {
|
|
7910
|
+
const contents = await readFile4(join6(folder, path));
|
|
7911
|
+
if (contents.includes(0)) continue;
|
|
7912
|
+
const lineCount = contents.length ? contents.toString("utf8").split("\n").length - (contents.toString("utf8").endsWith("\n") ? 1 : 0) : 0;
|
|
7913
|
+
lines.set(
|
|
7914
|
+
path,
|
|
7915
|
+
new Set(Array.from({ length: lineCount }, (_, index) => index + 1))
|
|
7916
|
+
);
|
|
7917
|
+
types.set(path, "A");
|
|
7918
|
+
} catch {
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
return [.../* @__PURE__ */ new Set([...lines.keys(), ...types.keys()])].map((path) => ({
|
|
7922
|
+
path,
|
|
7923
|
+
changeType: types.get(path) ?? "M",
|
|
7924
|
+
lines: [...lines.get(path) ?? []].sort((a, b) => a - b)
|
|
7925
|
+
}));
|
|
7926
|
+
}
|
|
7927
|
+
async function addChangedCoverage(report, changes, input, folder, timeoutMs, signal) {
|
|
7928
|
+
if (report.status !== "READY") return report;
|
|
7929
|
+
const rawFiles = Array.isArray(report.data.files) ? report.data.files : [];
|
|
7930
|
+
const coveragePaths = rawFiles.flatMap((raw) => {
|
|
7931
|
+
const file = jsonObject(raw);
|
|
7932
|
+
return file && typeof file.path === "string" ? [file.path] : [];
|
|
7933
|
+
});
|
|
7934
|
+
const changedFiles = [];
|
|
7935
|
+
let changedCoveredLines = 0;
|
|
7936
|
+
let changedExecutableLines = 0;
|
|
7937
|
+
for (const change of changes) {
|
|
7938
|
+
const coveragePath = coveragePaths.find(
|
|
7939
|
+
(candidate) => relative4(folder, candidate).split(sep3).join("/") === change.path
|
|
7940
|
+
);
|
|
7941
|
+
let covered = 0;
|
|
7942
|
+
let executable = 0;
|
|
7943
|
+
if (coveragePath && change.lines.length) {
|
|
7944
|
+
const temporary2 = join6(
|
|
7945
|
+
input.artifactDirectory,
|
|
7946
|
+
`.changed-coverage-${randomUUID2()}.json`
|
|
7947
|
+
);
|
|
7948
|
+
try {
|
|
7949
|
+
const result = await commandToFile(
|
|
7950
|
+
"xcrun",
|
|
7951
|
+
[
|
|
7952
|
+
"xccov",
|
|
7953
|
+
"view",
|
|
7954
|
+
"--archive",
|
|
7955
|
+
"--file",
|
|
7956
|
+
coveragePath,
|
|
7957
|
+
"--json",
|
|
7958
|
+
"result.xcresult"
|
|
7959
|
+
],
|
|
7960
|
+
timeoutMs,
|
|
7961
|
+
signal,
|
|
7962
|
+
input.artifactDirectory,
|
|
7963
|
+
xcodeEnvironment(),
|
|
7964
|
+
temporary2
|
|
7965
|
+
);
|
|
7966
|
+
if (result.exitCode === 0 && !result.cancelled && !result.timedOut) {
|
|
7967
|
+
const parsed = jsonObject(
|
|
7968
|
+
JSON.parse(await readFile4(temporary2, "utf8"))
|
|
7969
|
+
);
|
|
7970
|
+
const lineData = parsed?.[coveragePath];
|
|
7971
|
+
if (Array.isArray(lineData)) {
|
|
7972
|
+
const changed = new Set(change.lines);
|
|
7973
|
+
for (const rawLine of lineData) {
|
|
7974
|
+
const line = jsonObject(rawLine);
|
|
7975
|
+
if (line?.isExecutable === true && typeof line.line === "number" && changed.has(line.line)) {
|
|
7976
|
+
executable += 1;
|
|
7977
|
+
if (typeof line.executionCount === "number" && line.executionCount > 0) {
|
|
7978
|
+
covered += 1;
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
} finally {
|
|
7985
|
+
await rm4(temporary2, { force: true });
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
changedCoveredLines += covered;
|
|
7989
|
+
changedExecutableLines += executable;
|
|
7990
|
+
changedFiles.push({
|
|
7991
|
+
path: change.path,
|
|
7992
|
+
changeType: change.changeType,
|
|
7993
|
+
changedCoveredLines: covered,
|
|
7994
|
+
changedExecutableLines: executable,
|
|
7995
|
+
changedLineCoverage: executable ? covered / executable : null
|
|
7996
|
+
});
|
|
7997
|
+
}
|
|
7998
|
+
const summary = {
|
|
7999
|
+
...report.summary,
|
|
8000
|
+
changedCoveredLines,
|
|
8001
|
+
changedExecutableLines,
|
|
8002
|
+
changedLineCoverage: changedExecutableLines ? changedCoveredLines / changedExecutableLines : null
|
|
8003
|
+
};
|
|
8004
|
+
const data = { ...report.data, changedFiles };
|
|
8005
|
+
const destination = join6(input.artifactDirectory, "worktree-coverage.json");
|
|
8006
|
+
const temporary = `${destination}.tmp-${randomUUID2()}`;
|
|
8007
|
+
await writeFile3(temporary, JSON.stringify({ summary, data }, null, 2), {
|
|
8008
|
+
mode: 384
|
|
8009
|
+
});
|
|
8010
|
+
await rename3(temporary, destination);
|
|
8011
|
+
return {
|
|
8012
|
+
...report,
|
|
8013
|
+
summary,
|
|
8014
|
+
data,
|
|
8015
|
+
additionalArtifacts: [
|
|
8016
|
+
await artifact(
|
|
8017
|
+
input.artifactDirectory,
|
|
8018
|
+
"WORKTREE_COVERAGE_JSON",
|
|
8019
|
+
destination
|
|
8020
|
+
)
|
|
8021
|
+
]
|
|
8022
|
+
};
|
|
8023
|
+
}
|
|
6808
8024
|
function parseBuildSettings(value) {
|
|
6809
8025
|
const parsed = JSON.parse(value);
|
|
6810
8026
|
if (!Array.isArray(parsed)) return [];
|
|
@@ -6848,14 +8064,35 @@ async function findFiles(root, predicate, limit = 20) {
|
|
|
6848
8064
|
}
|
|
6849
8065
|
return results;
|
|
6850
8066
|
}
|
|
6851
|
-
async function captureArtifacts(input, folder, signal) {
|
|
8067
|
+
async function captureArtifacts(input, folder, signal, includeProducts = true) {
|
|
6852
8068
|
const artifacts = [];
|
|
6853
8069
|
const resultBundle = join6(input.artifactDirectory, "result.xcresult");
|
|
6854
8070
|
if (await pathExists(resultBundle)) {
|
|
8071
|
+
const coverageProbe = await command2(
|
|
8072
|
+
"xcrun",
|
|
8073
|
+
[
|
|
8074
|
+
"xccov",
|
|
8075
|
+
"view",
|
|
8076
|
+
"--report",
|
|
8077
|
+
"--only-targets",
|
|
8078
|
+
"--json",
|
|
8079
|
+
"result.xcresult"
|
|
8080
|
+
],
|
|
8081
|
+
3e4,
|
|
8082
|
+
new AbortController().signal,
|
|
8083
|
+
input.artifactDirectory,
|
|
8084
|
+
xcodeEnvironment()
|
|
8085
|
+
);
|
|
6855
8086
|
artifacts.push(
|
|
6856
|
-
await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle
|
|
8087
|
+
await artifact(input.artifactDirectory, "RESULT_BUNDLE", resultBundle, {
|
|
8088
|
+
testResultsAvailable: ["TEST", "TEST_WITHOUT_BUILDING"].includes(
|
|
8089
|
+
input.action
|
|
8090
|
+
),
|
|
8091
|
+
coverageAvailable: coverageProbe.exitCode === 0
|
|
8092
|
+
})
|
|
6857
8093
|
);
|
|
6858
8094
|
}
|
|
8095
|
+
if (!includeProducts) return artifacts;
|
|
6859
8096
|
const archivePath = join6(input.artifactDirectory, "archive.xcarchive");
|
|
6860
8097
|
if (await pathExists(archivePath)) {
|
|
6861
8098
|
artifacts.push(
|
|
@@ -6884,7 +8121,7 @@ async function captureArtifacts(input, folder, signal) {
|
|
|
6884
8121
|
const productDirectory = join6(input.artifactDirectory, "products");
|
|
6885
8122
|
const destination = join6(productDirectory, basename3(source));
|
|
6886
8123
|
await mkdir3(productDirectory, { recursive: true, mode: 448 });
|
|
6887
|
-
await
|
|
8124
|
+
await rm4(destination, { recursive: true, force: true });
|
|
6888
8125
|
await cp(source, destination, {
|
|
6889
8126
|
recursive: true,
|
|
6890
8127
|
preserveTimestamps: true
|
|
@@ -6941,9 +8178,14 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
6941
8178
|
throw new Error("The captured .xctestrun artifact is unavailable");
|
|
6942
8179
|
}
|
|
6943
8180
|
}
|
|
6944
|
-
if (!
|
|
8181
|
+
if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
|
|
6945
8182
|
throw new Error("Build artifact directory is invalid");
|
|
6946
8183
|
}
|
|
8184
|
+
const sourceStateHash = await worktreeCodeStateHash(
|
|
8185
|
+
folder,
|
|
8186
|
+
Math.min(timeoutMs, 6e4),
|
|
8187
|
+
signal
|
|
8188
|
+
);
|
|
6947
8189
|
await mkdir3(input.artifactDirectory, { recursive: true, mode: 448 });
|
|
6948
8190
|
const rawLog = join6(input.artifactDirectory, "build.log");
|
|
6949
8191
|
const logger = new BuildLogger(input.buildId, context, rawLog, process.env);
|
|
@@ -6966,6 +8208,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
6966
8208
|
configuration: input.configuration
|
|
6967
8209
|
};
|
|
6968
8210
|
let buildResult = null;
|
|
8211
|
+
let coverageChanges = [];
|
|
6969
8212
|
let errorCode = null;
|
|
6970
8213
|
let error = null;
|
|
6971
8214
|
let failBuild = false;
|
|
@@ -7023,6 +8266,18 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
7023
8266
|
} catch (progressError) {
|
|
7024
8267
|
logger.emit("RUNNING", "SYSTEM", cleanError3(progressError));
|
|
7025
8268
|
}
|
|
8269
|
+
if (input.worktreeCoverage) {
|
|
8270
|
+
try {
|
|
8271
|
+
coverageChanges = await snapshotCoverageChanges(
|
|
8272
|
+
input,
|
|
8273
|
+
folder,
|
|
8274
|
+
Math.min(timeoutMs, 6e4),
|
|
8275
|
+
signal
|
|
8276
|
+
);
|
|
8277
|
+
} catch (coverageError) {
|
|
8278
|
+
logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
7026
8281
|
const args2 = xcodeBuildArguments(input);
|
|
7027
8282
|
buildResult = await runLoggedCommand({
|
|
7028
8283
|
command: "xcrun",
|
|
@@ -7082,12 +8337,55 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
7082
8337
|
}
|
|
7083
8338
|
}
|
|
7084
8339
|
let artifacts = [];
|
|
7085
|
-
|
|
8340
|
+
try {
|
|
8341
|
+
artifacts = await captureArtifacts(
|
|
8342
|
+
input,
|
|
8343
|
+
folder,
|
|
8344
|
+
new AbortController().signal,
|
|
8345
|
+
!failBuild && !signal.aborted && buildResult?.exitCode === 0
|
|
8346
|
+
);
|
|
8347
|
+
} catch (artifactError) {
|
|
8348
|
+
logger.emit("ARTIFACTS", "STDERR", cleanError3(artifactError));
|
|
8349
|
+
}
|
|
8350
|
+
const reports = [];
|
|
8351
|
+
const testAction = ["TEST", "TEST_WITHOUT_BUILDING"].includes(input.action);
|
|
8352
|
+
const canGenerateReports = () => !signal.aborted && buildResult?.cancelled !== true && buildResult?.timedOut !== true;
|
|
8353
|
+
if (canGenerateReports() && testAction && input.advancedSettings.parseTestResults) {
|
|
8354
|
+
const report = await writeJsonReport(
|
|
8355
|
+
input,
|
|
8356
|
+
"TEST_RESULTS",
|
|
8357
|
+
Math.min(timeoutMs, 12e4),
|
|
8358
|
+
signal
|
|
8359
|
+
);
|
|
8360
|
+
reports.push(report);
|
|
8361
|
+
if (report.artifact) artifacts.push(report.artifact);
|
|
8362
|
+
if (report.error) logger.emit("REPORTS", "STDERR", report.error);
|
|
8363
|
+
}
|
|
8364
|
+
if (canGenerateReports() && testAction && input.worktreeCoverage) {
|
|
8365
|
+
let report = await writeJsonReport(
|
|
8366
|
+
input,
|
|
8367
|
+
"CODE_COVERAGE",
|
|
8368
|
+
Math.min(timeoutMs, 12e4),
|
|
8369
|
+
signal
|
|
8370
|
+
);
|
|
7086
8371
|
try {
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
8372
|
+
report = await addChangedCoverage(
|
|
8373
|
+
report,
|
|
8374
|
+
coverageChanges,
|
|
8375
|
+
input,
|
|
8376
|
+
folder,
|
|
8377
|
+
Math.min(timeoutMs, 12e4),
|
|
8378
|
+
signal
|
|
8379
|
+
);
|
|
8380
|
+
} catch (coverageError) {
|
|
8381
|
+
logger.emit("COVERAGE", "STDERR", cleanError3(coverageError));
|
|
8382
|
+
}
|
|
8383
|
+
reports.push(report);
|
|
8384
|
+
if (report.artifact) artifacts.push(report.artifact);
|
|
8385
|
+
if (report.additionalArtifacts) {
|
|
8386
|
+
artifacts.push(...report.additionalArtifacts);
|
|
7090
8387
|
}
|
|
8388
|
+
if (report.error) logger.emit("REPORTS", "STDERR", report.error);
|
|
7091
8389
|
}
|
|
7092
8390
|
await logger.close();
|
|
7093
8391
|
if (await pathExists(rawLog)) {
|
|
@@ -7099,6 +8397,12 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
7099
8397
|
const timedOut = buildResult?.timedOut === true;
|
|
7100
8398
|
const exitCode = cancelled ? null : failBuild || !buildResult ? 1 : buildResult.exitCode;
|
|
7101
8399
|
const args = xcodeBuildArguments(input);
|
|
8400
|
+
const finalStateHash = await worktreeCodeStateHash(
|
|
8401
|
+
folder,
|
|
8402
|
+
Math.min(timeoutMs, 6e4),
|
|
8403
|
+
new AbortController().signal
|
|
8404
|
+
);
|
|
8405
|
+
const codeStateObservedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7102
8406
|
return {
|
|
7103
8407
|
exitCode,
|
|
7104
8408
|
signal: buildResult?.signal ?? null,
|
|
@@ -7108,16 +8412,44 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
|
|
|
7108
8412
|
error,
|
|
7109
8413
|
commandSummary: commandSummary("xcrun", args),
|
|
7110
8414
|
artifacts,
|
|
7111
|
-
|
|
8415
|
+
reports,
|
|
8416
|
+
scriptExecutions,
|
|
8417
|
+
sourceStateHash,
|
|
8418
|
+
finalStateHash,
|
|
8419
|
+
codeStateObservedAt
|
|
7112
8420
|
};
|
|
7113
8421
|
} finally {
|
|
7114
8422
|
await logger.close();
|
|
7115
8423
|
}
|
|
7116
8424
|
};
|
|
8425
|
+
var generateIosBuildReport = async (payload, timeoutMs, signal, onLog) => {
|
|
8426
|
+
const input = parseBuildReportPayload(payload);
|
|
8427
|
+
if (!isAbsolute4(input.artifactDirectory) || basename3(input.artifactDirectory) !== input.buildId) {
|
|
8428
|
+
throw new Error("Build artifact directory is invalid");
|
|
8429
|
+
}
|
|
8430
|
+
if (signal.aborted) return { ...successfulProcess5, cancelled: true };
|
|
8431
|
+
const report = await writeJsonReport(
|
|
8432
|
+
input,
|
|
8433
|
+
input.reportKind,
|
|
8434
|
+
Math.min(timeoutMs, 18e4),
|
|
8435
|
+
signal
|
|
8436
|
+
);
|
|
8437
|
+
await onLog({
|
|
8438
|
+
sequence: 0,
|
|
8439
|
+
stream: report.status === "READY" ? "SYSTEM" : "STDERR",
|
|
8440
|
+
message: report.status === "READY" ? `Generated ${report.kind.toLocaleLowerCase()} report` : report.error || "Report generation failed",
|
|
8441
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8442
|
+
});
|
|
8443
|
+
return {
|
|
8444
|
+
...successfulProcess5,
|
|
8445
|
+
report,
|
|
8446
|
+
artifacts: report.artifact ? [report.artifact] : []
|
|
8447
|
+
};
|
|
8448
|
+
};
|
|
7117
8449
|
var deleteIosBuild = async (payload, _timeoutMs, signal, onLog) => {
|
|
7118
8450
|
const input = parseBuildDeletePayload(payload);
|
|
7119
8451
|
if (signal.aborted) return { ...successfulProcess5, cancelled: true };
|
|
7120
|
-
await
|
|
8452
|
+
await rm4(input.artifactDirectory, { recursive: true, force: true });
|
|
7121
8453
|
await onLog({
|
|
7122
8454
|
sequence: 0,
|
|
7123
8455
|
stream: "SYSTEM",
|
|
@@ -7133,10 +8465,10 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
|
|
|
7133
8465
|
}
|
|
7134
8466
|
const root = await realpath5(input.artifactDirectory);
|
|
7135
8467
|
const target2 = await realpath5(
|
|
7136
|
-
|
|
8468
|
+
containedPath2(root, input.artifactRelativePath)
|
|
7137
8469
|
);
|
|
7138
|
-
const difference =
|
|
7139
|
-
if (!difference || difference === ".." || difference.startsWith(`..${
|
|
8470
|
+
const difference = relative4(root, target2);
|
|
8471
|
+
if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute4(difference)) {
|
|
7140
8472
|
throw new Error("Build artifact resolves outside the build folder");
|
|
7141
8473
|
}
|
|
7142
8474
|
const information = await stat5(target2);
|
|
@@ -7147,7 +8479,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
|
|
|
7147
8479
|
try {
|
|
7148
8480
|
if (information.isDirectory()) {
|
|
7149
8481
|
temporaryArchive = join6(
|
|
7150
|
-
|
|
8482
|
+
tmpdir2(),
|
|
7151
8483
|
`ade-build-artifact-${randomUUID2()}.tar.gz`
|
|
7152
8484
|
);
|
|
7153
8485
|
requireSuccess3(
|
|
@@ -7180,7 +8512,7 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
|
|
|
7180
8512
|
return successfulProcess5;
|
|
7181
8513
|
} finally {
|
|
7182
8514
|
if (temporaryArchive) {
|
|
7183
|
-
await
|
|
8515
|
+
await rm4(temporaryArchive, { force: true });
|
|
7184
8516
|
}
|
|
7185
8517
|
}
|
|
7186
8518
|
};
|
|
@@ -7209,7 +8541,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
|
7209
8541
|
Math.min(timeoutMs, 6e4),
|
|
7210
8542
|
signal
|
|
7211
8543
|
);
|
|
7212
|
-
const appPath =
|
|
8544
|
+
const appPath = containedPath2(
|
|
7213
8545
|
input.artifactDirectory,
|
|
7214
8546
|
input.artifactRelativePath
|
|
7215
8547
|
);
|
|
@@ -7387,7 +8719,7 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
|
7387
8719
|
status: signal.aborted ? "CANCELLED" : failure ? "FAILED" : "SUCCEEDED",
|
|
7388
8720
|
error: failure,
|
|
7389
8721
|
durationMs: Date.now() - started,
|
|
7390
|
-
outputRelativePath:
|
|
8722
|
+
outputRelativePath: relative4(input.artifactDirectory, logPath)
|
|
7391
8723
|
});
|
|
7392
8724
|
if (signal.aborted) break;
|
|
7393
8725
|
}
|
|
@@ -7447,7 +8779,7 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
|
7447
8779
|
Math.min(timeoutMs, 6e4),
|
|
7448
8780
|
signal
|
|
7449
8781
|
);
|
|
7450
|
-
const archivePath =
|
|
8782
|
+
const archivePath = containedPath2(
|
|
7451
8783
|
input.artifactDirectory,
|
|
7452
8784
|
input.archiveRelativePath
|
|
7453
8785
|
);
|
|
@@ -7493,7 +8825,7 @@ var exportIosArchive = async (payload, timeoutMs, signal, _onLog, context) => {
|
|
|
7493
8825
|
signal: result.signal,
|
|
7494
8826
|
timedOut: result.timedOut,
|
|
7495
8827
|
cancelled: result.cancelled,
|
|
7496
|
-
outputRelativePath:
|
|
8828
|
+
outputRelativePath: relative4(input.artifactDirectory, exportDirectory),
|
|
7497
8829
|
sizeBytes: result.exitCode === 0 ? await pathSize(exportDirectory) : null,
|
|
7498
8830
|
error: result.exitCode === 0 ? null : cleanError3(result.output)
|
|
7499
8831
|
};
|
|
@@ -7522,6 +8854,8 @@ var handlers = {
|
|
|
7522
8854
|
[WORKTREE_DELETE_JOB_KIND]: deleteWorktree,
|
|
7523
8855
|
[WORKTREE_OPERATION_JOB_KIND]: operateWorktree,
|
|
7524
8856
|
[WORKTREE_WATCH_JOB_KIND]: watchWorktree,
|
|
8857
|
+
[WORKTREE_DIFF_JOB_KIND]: inspectWorktreeDiff,
|
|
8858
|
+
[WORKTREE_DIFF_ASSET_JOB_KIND]: downloadWorktreeDiffAsset,
|
|
7525
8859
|
[SKILL_SCAN_JOB_KIND]: scanSkills,
|
|
7526
8860
|
[SKILL_READ_JOB_KIND]: readSkills,
|
|
7527
8861
|
[SKILL_APPLY_JOB_KIND]: applySkills,
|
|
@@ -7533,7 +8867,9 @@ var handlers = {
|
|
|
7533
8867
|
[IOS_BUILD_DELETE_JOB_KIND]: deleteIosBuild,
|
|
7534
8868
|
[IOS_ARTIFACT_DOWNLOAD_JOB_KIND]: downloadIosBuildArtifact,
|
|
7535
8869
|
[IOS_DEPLOY_JOB_KIND]: deployIosBuild,
|
|
7536
|
-
[IOS_EXPORT_JOB_KIND]: exportIosArchive
|
|
8870
|
+
[IOS_EXPORT_JOB_KIND]: exportIosArchive,
|
|
8871
|
+
[IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
|
|
8872
|
+
[IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport
|
|
7537
8873
|
};
|
|
7538
8874
|
|
|
7539
8875
|
// src/repository-coordinator.ts
|
|
@@ -7542,8 +8878,8 @@ var RepositoryCoordinator = class {
|
|
|
7542
8878
|
async run(key, task) {
|
|
7543
8879
|
const previous = this.tails.get(key) ?? Promise.resolve();
|
|
7544
8880
|
let release2;
|
|
7545
|
-
const current = new Promise((
|
|
7546
|
-
release2 =
|
|
8881
|
+
const current = new Promise((resolve6) => {
|
|
8882
|
+
release2 = resolve6;
|
|
7547
8883
|
});
|
|
7548
8884
|
const tail = previous.then(() => current);
|
|
7549
8885
|
this.tails.set(key, tail);
|
|
@@ -7641,7 +8977,7 @@ var JobExecutor = class {
|
|
|
7641
8977
|
);
|
|
7642
8978
|
if (this.stopping) return;
|
|
7643
8979
|
const delay = Math.min(3e4, 1e3 * 2 ** Math.min(retry++, 5));
|
|
7644
|
-
await new Promise((
|
|
8980
|
+
await new Promise((resolve6) => setTimeout(resolve6, delay));
|
|
7645
8981
|
}
|
|
7646
8982
|
}
|
|
7647
8983
|
}
|
|
@@ -7912,9 +9248,9 @@ async function runAgent(config, signal) {
|
|
|
7912
9248
|
() => void heartbeat(),
|
|
7913
9249
|
HEARTBEAT_INTERVAL_MS
|
|
7914
9250
|
);
|
|
7915
|
-
await new Promise((
|
|
7916
|
-
if (signal.aborted)
|
|
7917
|
-
else signal.addEventListener("abort", () =>
|
|
9251
|
+
await new Promise((resolve6) => {
|
|
9252
|
+
if (signal.aborted) resolve6();
|
|
9253
|
+
else signal.addEventListener("abort", () => resolve6(), { once: true });
|
|
7918
9254
|
});
|
|
7919
9255
|
clearInterval(heartbeatTimer);
|
|
7920
9256
|
if (codebaseTimer) clearTimeout(codebaseTimer);
|
|
@@ -7931,7 +9267,7 @@ var defaultDependencies = {
|
|
|
7931
9267
|
inventory: collectInventory,
|
|
7932
9268
|
load: loadConfig,
|
|
7933
9269
|
save: saveConfig,
|
|
7934
|
-
wait: (milliseconds, signal) => new Promise((
|
|
9270
|
+
wait: (milliseconds, signal) => new Promise((resolve6, reject) => {
|
|
7935
9271
|
if (signal.aborted) {
|
|
7936
9272
|
reject(new Error("Development agent startup was cancelled"));
|
|
7937
9273
|
return;
|
|
@@ -7942,7 +9278,7 @@ var defaultDependencies = {
|
|
|
7942
9278
|
};
|
|
7943
9279
|
const timer = setTimeout(() => {
|
|
7944
9280
|
signal.removeEventListener("abort", onAbort);
|
|
7945
|
-
|
|
9281
|
+
resolve6();
|
|
7946
9282
|
}, milliseconds);
|
|
7947
9283
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
7948
9284
|
})
|