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