@ai-development-environment/control-agent 0.0.97 → 0.0.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/control-agent.js +594 -113
  2. package/package.json +1 -1
@@ -3225,7 +3225,7 @@ var require_stream = __commonJS({
3225
3225
  };
3226
3226
  duplex._final = function(callback) {
3227
3227
  if (ws.readyState === ws.CONNECTING) {
3228
- ws.once("open", function open5() {
3228
+ ws.once("open", function open6() {
3229
3229
  duplex._final(callback);
3230
3230
  });
3231
3231
  return;
@@ -3246,7 +3246,7 @@ var require_stream = __commonJS({
3246
3246
  };
3247
3247
  duplex._write = function(chunk, encoding, callback) {
3248
3248
  if (ws.readyState === ws.CONNECTING) {
3249
- ws.once("open", function open5() {
3249
+ ws.once("open", function open6() {
3250
3250
  duplex._write(chunk, encoding, callback);
3251
3251
  });
3252
3252
  return;
@@ -4280,7 +4280,7 @@ async function saveConfig(config, path = configPath()) {
4280
4280
 
4281
4281
  // src/graphql-client.ts
4282
4282
  import { createReadStream } from "node:fs";
4283
- import { rm, stat } from "node:fs/promises";
4283
+ import { open, rm, stat } from "node:fs/promises";
4284
4284
  import { createHash } from "node:crypto";
4285
4285
  import { Readable } from "node:stream";
4286
4286
 
@@ -4988,6 +4988,39 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
4988
4988
  var wrapper_default = import_websocket.default;
4989
4989
 
4990
4990
  // src/graphql-client.ts
4991
+ async function writeArtifactTransferBytes(writer, bytes, position) {
4992
+ let written = 0;
4993
+ while (written < bytes.byteLength) {
4994
+ const remaining = bytes.byteLength - written;
4995
+ const { bytesWritten } = await writer.write(
4996
+ bytes,
4997
+ written,
4998
+ remaining,
4999
+ position + written
5000
+ );
5001
+ if (!Number.isSafeInteger(bytesWritten) || bytesWritten <= 0 || bytesWritten > remaining) {
5002
+ throw new Error("Artifact transfer file write made no progress");
5003
+ }
5004
+ written += bytesWritten;
5005
+ }
5006
+ }
5007
+ async function artifactTransferRetryDelay(attempt, signal) {
5008
+ await new Promise((resolve12, reject) => {
5009
+ const timeout = setTimeout(
5010
+ () => {
5011
+ signal.removeEventListener("abort", abort);
5012
+ resolve12();
5013
+ },
5014
+ Math.min(4e3, 250 * 2 ** attempt)
5015
+ );
5016
+ const abort = () => {
5017
+ clearTimeout(timeout);
5018
+ reject(signal.reason ?? new Error("Artifact transfer was cancelled"));
5019
+ };
5020
+ if (signal.aborted) abort();
5021
+ else signal.addEventListener("abort", abort, { once: true });
5022
+ });
5023
+ }
4991
5024
  var RUN_COMMAND_FIELDS = `
4992
5025
  id runId agentId sequence type payload status error
4993
5026
  run {
@@ -5257,6 +5290,205 @@ var AgentGraphQLClient = class {
5257
5290
  );
5258
5291
  }
5259
5292
  }
5293
+ artifactTransferHeaders(values = {}) {
5294
+ return mergeHeaders(this.headers, {
5295
+ ...this.credential ? { authorization: `Bearer ${this.credential}` } : {},
5296
+ ...values
5297
+ });
5298
+ }
5299
+ async uploadBuildArtifactTransfer(input) {
5300
+ const information = await stat(input.path);
5301
+ const base = `${this.server}/api/agent/build-artifact-transfers/${encodeURIComponent(input.transferId)}`;
5302
+ const metadata = await fetch(base, {
5303
+ method: "POST",
5304
+ headers: this.artifactTransferHeaders({
5305
+ "content-type": "application/json"
5306
+ }),
5307
+ body: JSON.stringify({
5308
+ uploadLength: information.size,
5309
+ checksum: input.checksum,
5310
+ filename: input.filename,
5311
+ contentType: input.contentType
5312
+ }),
5313
+ signal: input.signal
5314
+ });
5315
+ if (!metadata.ok) {
5316
+ throw new Error(
5317
+ `Artifact transfer initialization failed: HTTP ${metadata.status} ${await metadata.text()}`
5318
+ );
5319
+ }
5320
+ const confirmedOffset = async () => {
5321
+ const response = await fetch(`${base}/upload`, {
5322
+ method: "HEAD",
5323
+ headers: this.artifactTransferHeaders(),
5324
+ signal: input.signal
5325
+ });
5326
+ if (!response.ok) {
5327
+ throw new Error(
5328
+ `Could not inspect artifact upload offset: HTTP ${response.status}`
5329
+ );
5330
+ }
5331
+ const offset = Number(response.headers.get("upload-offset"));
5332
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > information.size) {
5333
+ throw new Error("Artifact relay returned an invalid upload offset");
5334
+ }
5335
+ return offset;
5336
+ };
5337
+ const handle = await open(input.path, "r");
5338
+ try {
5339
+ let offset = await confirmedOffset();
5340
+ const buffer = Buffer.allocUnsafe(16 * 1024 * 1024);
5341
+ while (offset < information.size) {
5342
+ if (input.signal.aborted) throw input.signal.reason;
5343
+ const length = Math.min(buffer.length, information.size - offset);
5344
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
5345
+ if (bytesRead !== length)
5346
+ throw new Error("Artifact changed while uploading");
5347
+ let attempt = 0;
5348
+ while (true) {
5349
+ let failure;
5350
+ try {
5351
+ const response = await fetch(`${base}/upload`, {
5352
+ method: "PATCH",
5353
+ headers: this.artifactTransferHeaders({
5354
+ "content-length": String(bytesRead),
5355
+ "content-type": "application/offset+octet-stream",
5356
+ "upload-offset": String(offset)
5357
+ }),
5358
+ body: buffer.subarray(0, bytesRead),
5359
+ signal: input.signal
5360
+ });
5361
+ if (response.ok) {
5362
+ const nextOffset = Number(response.headers.get("upload-offset"));
5363
+ if (Number.isSafeInteger(nextOffset) && nextOffset > offset && nextOffset <= information.size) {
5364
+ offset = nextOffset;
5365
+ break;
5366
+ }
5367
+ failure = new Error(
5368
+ "Artifact relay returned an invalid upload offset"
5369
+ );
5370
+ } else {
5371
+ failure = new Error(
5372
+ `Artifact chunk upload failed: HTTP ${response.status} ${await response.text()}`
5373
+ );
5374
+ }
5375
+ } catch (error) {
5376
+ if (input.signal.aborted) throw error;
5377
+ failure = error instanceof Error ? error : new Error(String(error));
5378
+ }
5379
+ if (++attempt >= 5) throw failure;
5380
+ await artifactTransferRetryDelay(attempt, input.signal);
5381
+ const resumedOffset = await confirmedOffset();
5382
+ if (resumedOffset !== offset) {
5383
+ offset = resumedOffset;
5384
+ break;
5385
+ }
5386
+ }
5387
+ }
5388
+ } finally {
5389
+ await handle.close();
5390
+ }
5391
+ const completion = await fetch(`${base}/complete`, {
5392
+ method: "POST",
5393
+ headers: this.artifactTransferHeaders(),
5394
+ signal: input.signal
5395
+ });
5396
+ if (!completion.ok) {
5397
+ throw new Error(
5398
+ `Artifact transfer completion failed: HTTP ${completion.status} ${await completion.text()}`
5399
+ );
5400
+ }
5401
+ }
5402
+ async downloadBuildArtifactTransfer(input) {
5403
+ const base = `${this.server}/api/agent/build-artifact-transfers/${encodeURIComponent(input.transferId)}`;
5404
+ let metadata;
5405
+ while (true) {
5406
+ const response = await fetch(base, {
5407
+ headers: this.artifactTransferHeaders(),
5408
+ signal: input.signal
5409
+ });
5410
+ if (!response.ok) {
5411
+ throw new Error(
5412
+ `Could not inspect artifact transfer: HTTP ${response.status} ${await response.text()}`
5413
+ );
5414
+ }
5415
+ metadata = await response.json();
5416
+ if (["READY", "DOWNLOADING", "SUCCEEDED"].includes(metadata.status))
5417
+ break;
5418
+ if (["FAILED", "CANCELLED", "EXPIRED"].includes(metadata.status)) {
5419
+ throw new Error(
5420
+ metadata.error || `Artifact transfer ${metadata.status.toLowerCase()}`
5421
+ );
5422
+ }
5423
+ await new Promise((resolve12) => setTimeout(resolve12, 1e3));
5424
+ }
5425
+ if (!Number.isSafeInteger(metadata.uploadLength) || !metadata.uploadLength || !metadata.checksum || !metadata.filename) {
5426
+ throw new Error("Artifact transfer metadata is incomplete");
5427
+ }
5428
+ let offset = 0;
5429
+ try {
5430
+ offset = (await stat(input.path)).size;
5431
+ } catch {
5432
+ }
5433
+ if (offset > metadata.uploadLength) {
5434
+ await rm(input.path, { force: true });
5435
+ offset = 0;
5436
+ }
5437
+ const handle = await open(input.path, offset === 0 ? "w" : "r+");
5438
+ try {
5439
+ while (offset < metadata.uploadLength) {
5440
+ const end = Math.min(
5441
+ metadata.uploadLength - 1,
5442
+ offset + 16 * 1024 * 1024 - 1
5443
+ );
5444
+ let bytes = null;
5445
+ let attempt = 0;
5446
+ while (!bytes) {
5447
+ let failure = new Error("Artifact range download failed");
5448
+ try {
5449
+ const response = await fetch(`${base}/download`, {
5450
+ headers: this.artifactTransferHeaders({
5451
+ range: `bytes=${offset}-${end}`
5452
+ }),
5453
+ signal: input.signal
5454
+ });
5455
+ if (response.status !== 206) {
5456
+ failure = new Error(
5457
+ `Artifact range download failed: HTTP ${response.status} ${await response.text()}`
5458
+ );
5459
+ } else {
5460
+ const received = Buffer.from(await response.arrayBuffer());
5461
+ if (received.length === end - offset + 1) bytes = received;
5462
+ else failure = new Error("Artifact range length did not match");
5463
+ }
5464
+ } catch (error) {
5465
+ if (input.signal.aborted) throw error;
5466
+ failure = error instanceof Error ? error : new Error(String(error));
5467
+ }
5468
+ if (bytes) break;
5469
+ if (++attempt >= 5) throw failure;
5470
+ await artifactTransferRetryDelay(attempt, input.signal);
5471
+ }
5472
+ await writeArtifactTransferBytes(handle, bytes, offset);
5473
+ offset += bytes.length;
5474
+ }
5475
+ await handle.sync();
5476
+ } finally {
5477
+ await handle.close();
5478
+ }
5479
+ const digest = createHash("sha256");
5480
+ for await (const chunk of createReadStream(input.path))
5481
+ digest.update(chunk);
5482
+ if (digest.digest("hex") !== metadata.checksum) {
5483
+ await rm(input.path, { force: true });
5484
+ throw new Error("Downloaded artifact checksum did not match");
5485
+ }
5486
+ return {
5487
+ filename: metadata.filename,
5488
+ contentType: metadata.contentType ?? "application/octet-stream",
5489
+ checksum: metadata.checksum
5490
+ };
5491
+ }
5260
5492
  completeJob(jobId, status2, result, error) {
5261
5493
  return this.request(
5262
5494
  `mutation CompleteJob($jobId: ID!, $status: AgentJobStatus!, $result: JSON, $error: String) {
@@ -7423,6 +7655,9 @@ var IOS_BUILD_JOB_KIND = "ios.build.run";
7423
7655
  var IOS_BUILD_DELETE_JOB_KIND = "ios.build.delete";
7424
7656
  var IOS_ARTIFACT_DOWNLOAD_JOB_KIND = "ios.artifact.download";
7425
7657
  var IOS_DEPLOY_JOB_KIND = "ios.build.deploy";
7658
+ var IOS_AGENT_RUN_DESTINATIONS_JOB_KIND = "ios.agentRunDestinations.inspect";
7659
+ var IOS_ARTIFACT_TRANSFER_UPLOAD_JOB_KIND = "ios.artifact.transfer.upload";
7660
+ var IOS_REMOTE_DEPLOY_JOB_KIND = "ios.build.remoteDeploy";
7426
7661
  var IOS_EXPORT_JOB_KIND = "ios.archive.export";
7427
7662
  var IOS_TEST_RESULTS_JOB_KIND = "ios.test-results.parse";
7428
7663
  var IOS_COVERAGE_REPORT_JOB_KIND = "ios.coverage.generate";
@@ -7436,6 +7671,9 @@ var IOS_BUILD_JOB_KINDS = [
7436
7671
  IOS_BUILD_DELETE_JOB_KIND,
7437
7672
  IOS_ARTIFACT_DOWNLOAD_JOB_KIND,
7438
7673
  IOS_DEPLOY_JOB_KIND,
7674
+ IOS_AGENT_RUN_DESTINATIONS_JOB_KIND,
7675
+ IOS_ARTIFACT_TRANSFER_UPLOAD_JOB_KIND,
7676
+ IOS_REMOTE_DEPLOY_JOB_KIND,
7439
7677
  IOS_EXPORT_JOB_KIND,
7440
7678
  IOS_TEST_RESULTS_JOB_KIND,
7441
7679
  IOS_COVERAGE_REPORT_JOB_KIND,
@@ -7476,6 +7714,31 @@ var APPROVED_BUILD_SETTING_OVERRIDES = [
7476
7714
  "MARKETING_VERSION",
7477
7715
  "CURRENT_PROJECT_VERSION"
7478
7716
  ];
7717
+ function genericBuildDestinations(action) {
7718
+ if (!GENERIC_BUILD_DESTINATION_ACTIONS.includes(action)) return [];
7719
+ const physical = {
7720
+ type: "PHYSICAL_DEVICE",
7721
+ id: "generic-ios",
7722
+ name: "Any Physical iOS Device",
7723
+ platform: "iOS",
7724
+ osVersion: null,
7725
+ state: null,
7726
+ generic: true
7727
+ };
7728
+ if (action === "ARCHIVE") return [physical];
7729
+ return [
7730
+ {
7731
+ type: "SIMULATOR",
7732
+ id: "generic-ios-simulator",
7733
+ name: "Any iOS Simulator",
7734
+ platform: "iOS Simulator",
7735
+ osVersion: null,
7736
+ state: null,
7737
+ generic: true
7738
+ },
7739
+ physical
7740
+ ];
7741
+ }
7479
7742
  var DEFAULT_BUILD_ADVANCED_SETTINGS = {
7480
7743
  packageResolution: "DEFAULT",
7481
7744
  disablePackageRepositoryCache: false,
@@ -7592,6 +7855,12 @@ function parseBuildDestination(value) {
7592
7855
  destination.generic,
7593
7856
  "build destination.generic"
7594
7857
  )
7858
+ },
7859
+ ...destination.available === void 0 ? {} : {
7860
+ available: booleanValue2(
7861
+ destination.available,
7862
+ "build destination.available"
7863
+ )
7595
7864
  }
7596
7865
  };
7597
7866
  }
@@ -7925,13 +8194,50 @@ function parseBuildArtifactDownloadPayload(value) {
7925
8194
  )
7926
8195
  };
7927
8196
  }
7928
- function parseBuildDeploymentPayload(value) {
7929
- const input = objectValue7(value, "build deployment payload");
8197
+ function parseBuildArtifactTransferUploadPayload(value) {
8198
+ const input = objectValue7(value, "build artifact transfer upload payload");
8199
+ const buildId = stringValue7(
8200
+ input.buildId,
8201
+ "build artifact transfer upload payload.buildId"
8202
+ );
8203
+ const artifactDirectory = safeAbsolutePath(
8204
+ input.artifactDirectory,
8205
+ "build artifact transfer upload payload.artifactDirectory"
8206
+ );
8207
+ if (artifactDirectory.split("/").at(-1) !== buildId) {
8208
+ throw new Error(
8209
+ "Build artifact transfer folder must end with the build ID"
8210
+ );
8211
+ }
8212
+ return {
8213
+ transferId: stringValue7(
8214
+ input.transferId,
8215
+ "build artifact transfer upload payload.transferId"
8216
+ ),
8217
+ buildId,
8218
+ artifactDirectory,
8219
+ artifactRelativePath: safeRelativePath2(
8220
+ input.artifactRelativePath,
8221
+ "build artifact transfer upload payload.artifactRelativePath"
8222
+ )
8223
+ };
8224
+ }
8225
+ function parseBuildAgentRunDestinationsPayload(value) {
8226
+ const input = objectValue7(value, "agent run destinations payload");
8227
+ return {
8228
+ destinationType: enumValue3(
8229
+ input.destinationType,
8230
+ BUILD_DESTINATION_TYPES,
8231
+ "agent run destinations payload.destinationType"
8232
+ )
8233
+ };
8234
+ }
8235
+ function parseDeployments(input) {
7930
8236
  if (!Array.isArray(input.deployments) || !input.deployments.length) {
7931
8237
  throw new Error("build deployment payload.deployments must not be empty");
7932
8238
  }
7933
- const deployments = input.deployments.map((value2, index) => {
7934
- const deployment = objectValue7(value2, `deployments[${index}]`);
8239
+ const deployments = input.deployments.map((value, index) => {
8240
+ const deployment = objectValue7(value, `deployments[${index}]`);
7935
8241
  return {
7936
8242
  id: stringValue7(deployment.id, `deployments[${index}].id`),
7937
8243
  destination: parseBuildDestination(deployment.destination)
@@ -7950,6 +8256,11 @@ function parseBuildDeploymentPayload(value) {
7950
8256
  if (new Set(deployments.map(({ destination }) => destination.type)).size > 1) {
7951
8257
  throw new Error("deployment destinations must use one destination type");
7952
8258
  }
8259
+ return deployments;
8260
+ }
8261
+ function parseBuildDeploymentPayload(value) {
8262
+ const input = objectValue7(value, "build deployment payload");
8263
+ const deployments = parseDeployments(input);
7953
8264
  return {
7954
8265
  ...worktreeIdentity(input),
7955
8266
  buildId: stringValue7(input.buildId, "build deployment payload.buildId"),
@@ -7968,6 +8279,24 @@ function parseBuildDeploymentPayload(value) {
7968
8279
  deployments
7969
8280
  };
7970
8281
  }
8282
+ function parseBuildRemoteDeploymentPayload(value) {
8283
+ const input = objectValue7(value, "remote build deployment payload");
8284
+ return {
8285
+ transferId: stringValue7(
8286
+ input.transferId,
8287
+ "remote build deployment payload.transferId"
8288
+ ),
8289
+ buildId: stringValue7(
8290
+ input.buildId,
8291
+ "remote build deployment payload.buildId"
8292
+ ),
8293
+ bundleIdentifier: stringValue7(
8294
+ input.bundleIdentifier,
8295
+ "remote build deployment payload.bundleIdentifier"
8296
+ ),
8297
+ deployments: parseDeployments(input)
8298
+ };
8299
+ }
7971
8300
  function parseBuildExportSettings(value) {
7972
8301
  const input = objectValue7(value, "export settings");
7973
8302
  const profiles = objectValue7(
@@ -9242,7 +9571,7 @@ import {
9242
9571
  cp,
9243
9572
  mkdir as mkdir2,
9244
9573
  mkdtemp as mkdtemp2,
9245
- open,
9574
+ open as open2,
9246
9575
  readdir,
9247
9576
  readFile as readFile2,
9248
9577
  realpath as realpath2,
@@ -9637,7 +9966,7 @@ function command(executable, args, timeoutMs, signal, cwd, env = process.env) {
9637
9966
  });
9638
9967
  }
9639
9968
  async function commandToFile(executable, args, timeoutMs, signal, cwd, env, destination) {
9640
- const output = await open(destination, "wx", 384);
9969
+ const output = await open2(destination, "wx", 384);
9641
9970
  try {
9642
9971
  return await captureCommand({
9643
9972
  command: executable,
@@ -10147,7 +10476,7 @@ var parseBuildSourceMetadata = async (payload, timeoutMs, signal) => {
10147
10476
  headSha: input.headSha
10148
10477
  };
10149
10478
  };
10150
- function simulatorDestinations(value) {
10479
+ function simulatorDestinations(value, includeUnavailable = false) {
10151
10480
  if (!value || typeof value !== "object" || Array.isArray(value)) return [];
10152
10481
  const devices = value.devices;
10153
10482
  if (!devices || typeof devices !== "object" || Array.isArray(devices))
@@ -10160,7 +10489,8 @@ function simulatorDestinations(value) {
10160
10489
  for (const raw of entries) {
10161
10490
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
10162
10491
  const device = raw;
10163
- if (device.isAvailable !== true || typeof device.udid !== "string")
10492
+ const available = device.isAvailable === true;
10493
+ if (!available && !includeUnavailable || typeof device.udid !== "string")
10164
10494
  continue;
10165
10495
  result.push({
10166
10496
  type: "SIMULATOR",
@@ -10168,7 +10498,8 @@ function simulatorDestinations(value) {
10168
10498
  name: typeof device.name === "string" ? device.name : device.udid,
10169
10499
  platform: "iOS Simulator",
10170
10500
  osVersion,
10171
- state: typeof device.state === "string" ? device.state : null
10501
+ state: typeof device.state === "string" ? device.state : available ? null : "Unavailable",
10502
+ available
10172
10503
  });
10173
10504
  }
10174
10505
  }
@@ -10188,7 +10519,7 @@ function nestedString(value, paths) {
10188
10519
  }
10189
10520
  return null;
10190
10521
  }
10191
- function physicalDestinations(value) {
10522
+ function physicalDestinations(value, includeUnavailable = false) {
10192
10523
  if (!value || typeof value !== "object" || Array.isArray(value)) return [];
10193
10524
  const result = value.result;
10194
10525
  const devices = result && typeof result === "object" && !Array.isArray(result) ? result.devices : null;
@@ -10209,7 +10540,8 @@ function physicalDestinations(value) {
10209
10540
  ["connectionProperties", "tunnelState"],
10210
10541
  ["deviceProperties", "connectionState"]
10211
10542
  ]);
10212
- if (state && /(disconnected|unavailable|not.?connected)/i.test(state)) {
10543
+ const available = !(state && /(disconnected|unavailable|not.?connected|offline)/i.test(state));
10544
+ if (!available && !includeUnavailable) {
10213
10545
  return [];
10214
10546
  }
10215
10547
  return [
@@ -10222,37 +10554,13 @@ function physicalDestinations(value) {
10222
10554
  ["deviceProperties", "osVersionNumber"],
10223
10555
  ["deviceProperties", "osVersion"]
10224
10556
  ]),
10225
- state
10557
+ state,
10558
+ available
10226
10559
  }
10227
10560
  ];
10228
10561
  });
10229
10562
  }
10230
- function genericBuildDestinations(action) {
10231
- if (!GENERIC_BUILD_DESTINATION_ACTIONS.includes(action)) return [];
10232
- const physical = {
10233
- type: "PHYSICAL_DEVICE",
10234
- id: "generic-ios",
10235
- name: "Any Physical iOS Device",
10236
- platform: "iOS",
10237
- osVersion: null,
10238
- state: null,
10239
- generic: true
10240
- };
10241
- if (action === "ARCHIVE") return [physical];
10242
- return [
10243
- {
10244
- type: "SIMULATOR",
10245
- id: "generic-ios-simulator",
10246
- name: "Any iOS Simulator",
10247
- platform: "iOS Simulator",
10248
- osVersion: null,
10249
- state: null,
10250
- generic: true
10251
- },
10252
- physical
10253
- ];
10254
- }
10255
- async function listPhysicalDevices(timeoutMs, signal) {
10563
+ async function listPhysicalDevices(timeoutMs, signal, includeUnavailable = false) {
10256
10564
  const directory = await mkdtemp2(join3(tmpdir2(), "ade-devices-"));
10257
10565
  const output = join3(directory, "devices.json");
10258
10566
  try {
@@ -10272,7 +10580,10 @@ async function listPhysicalDevices(timeoutMs, signal) {
10272
10580
  signal
10273
10581
  );
10274
10582
  if (result.exitCode !== 0) return [];
10275
- return physicalDestinations(JSON.parse(await readFile2(output, "utf8")));
10583
+ return physicalDestinations(
10584
+ JSON.parse(await readFile2(output, "utf8")),
10585
+ includeUnavailable
10586
+ );
10276
10587
  } finally {
10277
10588
  await rm3(directory, { recursive: true, force: true });
10278
10589
  }
@@ -10280,24 +10591,7 @@ async function listPhysicalDevices(timeoutMs, signal) {
10280
10591
  var inspectBuildDestinations = async (payload, timeoutMs, signal) => {
10281
10592
  const input = parseBuildDestinationsPayload(payload);
10282
10593
  const folder = await validateWorktree(input, timeoutMs, signal);
10283
- const absolutePath = await validateSource(folder, input.source);
10284
- const preflight = await command(
10285
- "xcrun",
10286
- [
10287
- "xcodebuild",
10288
- ...sourceArguments(input.source, absolutePath),
10289
- "-scheme",
10290
- input.scheme,
10291
- "-configuration",
10292
- input.configuration,
10293
- "-showBuildSettings",
10294
- "-json"
10295
- ],
10296
- Math.min(timeoutMs, 6e4),
10297
- signal,
10298
- folder
10299
- );
10300
- requireSuccess(preflight, "The saved scheme or configuration is unavailable");
10594
+ await validateSource(folder, input.source);
10301
10595
  const [simulators, physical] = await Promise.all([
10302
10596
  command(
10303
10597
  "xcrun",
@@ -10323,7 +10617,7 @@ var inspectBuildRunDestinations = async (payload, timeoutMs, signal) => {
10323
10617
  if (input.destinationType === "SIMULATOR") {
10324
10618
  const simulators = await command(
10325
10619
  "xcrun",
10326
- ["simctl", "list", "devices", "available", "-j"],
10620
+ ["simctl", "list", "devices", "-j"],
10327
10621
  Math.min(timeoutMs, 3e4),
10328
10622
  signal,
10329
10623
  folder
@@ -10331,12 +10625,33 @@ var inspectBuildRunDestinations = async (payload, timeoutMs, signal) => {
10331
10625
  requireSuccess(simulators, "Could not inspect available simulators");
10332
10626
  return {
10333
10627
  ...successfulProcess,
10334
- destinations: simulatorDestinations(JSON.parse(simulators.stdout))
10628
+ destinations: simulatorDestinations(JSON.parse(simulators.stdout), true)
10335
10629
  };
10336
10630
  }
10337
10631
  return {
10338
10632
  ...successfulProcess,
10339
- destinations: await listPhysicalDevices(timeoutMs, signal)
10633
+ destinations: await listPhysicalDevices(timeoutMs, signal, true)
10634
+ };
10635
+ };
10636
+ var inspectAgentBuildRunDestinations = async (payload, timeoutMs, signal) => {
10637
+ const input = parseBuildAgentRunDestinationsPayload(payload);
10638
+ if (input.destinationType === "SIMULATOR") {
10639
+ const simulators = await command(
10640
+ "xcrun",
10641
+ ["simctl", "list", "devices", "-j"],
10642
+ Math.min(timeoutMs, 3e4),
10643
+ signal,
10644
+ process.cwd()
10645
+ );
10646
+ requireSuccess(simulators, "Could not inspect available simulators");
10647
+ return {
10648
+ ...successfulProcess,
10649
+ destinations: simulatorDestinations(JSON.parse(simulators.stdout), true)
10650
+ };
10651
+ }
10652
+ return {
10653
+ ...successfulProcess,
10654
+ destinations: await listPhysicalDevices(timeoutMs, signal, true)
10340
10655
  };
10341
10656
  };
10342
10657
  function sensitiveValues(env) {
@@ -11457,7 +11772,10 @@ async function captureArtifacts(input, folder, signal, includeProducts = true) {
11457
11772
  await artifact(input.artifactDirectory, "RUNNABLE_APP", destination, {
11458
11773
  bundleIdentifier: settings.PRODUCT_BUNDLE_IDENTIFIER ?? null,
11459
11774
  target: apps[0].target,
11460
- destinationType: input.destination.type
11775
+ destinationType: input.destination.type,
11776
+ architectures: (settings.ARCHS ?? settings.VALID_ARCHS ?? "").split(/\s+/).filter(Boolean),
11777
+ minimumOSVersion: settings.IPHONEOS_DEPLOYMENT_TARGET ?? null,
11778
+ sdkName: settings.SDK_NAME ?? null
11461
11779
  })
11462
11780
  );
11463
11781
  }
@@ -11536,6 +11854,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11536
11854
  telemetry: input.telemetry ?? null
11537
11855
  };
11538
11856
  let buildResult = null;
11857
+ let preflightResult = null;
11539
11858
  let coverageChanges = [];
11540
11859
  let errorCode = null;
11541
11860
  let error = null;
@@ -11561,29 +11880,50 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11561
11880
  } catch (progressError) {
11562
11881
  logger.emit("PREPARING", "SYSTEM", cleanError(progressError));
11563
11882
  }
11564
- for (const script of [...input.scripts].sort(
11565
- (a, b) => a.position - b.position
11566
- )) {
11567
- if (!script.preBuildScript) continue;
11568
- const execution = await runHook({
11569
- input,
11570
- folder,
11571
- script,
11572
- phase: "PRE_BUILD",
11573
- source: script.preBuildScript,
11574
- contextPath,
11575
- hookContext: baseHookContext,
11883
+ if (!signal.aborted) {
11884
+ preflightResult = await runLoggedCommand({
11885
+ command: "xcrun",
11886
+ args: xcodeBuildSettingsArguments(input, folder),
11887
+ cwd: folder,
11888
+ env: xcodeEnvironment(),
11889
+ timeoutMs: Math.min(timeoutMs, 6e4),
11890
+ signal,
11576
11891
  logger,
11577
- signal
11892
+ phase: "PREFLIGHT"
11578
11893
  });
11579
- scriptExecutions.push(execution);
11580
- if (execution.causedBuildFailure) {
11894
+ if (preflightResult.exitCode !== 0 && !preflightResult.cancelled) {
11581
11895
  failBuild = true;
11582
- errorCode = "SCRIPT_FAILED";
11583
- error = execution.error;
11584
- break;
11896
+ errorCode = preflightResult.timedOut ? "TIMEOUT" : classifyFailure(preflightResult.output);
11897
+ error = cleanError(
11898
+ preflightResult.output || "The saved scheme, configuration, or destination is unavailable"
11899
+ );
11900
+ }
11901
+ }
11902
+ if (!failBuild && !signal.aborted) {
11903
+ for (const script of [...input.scripts].sort(
11904
+ (a, b) => a.position - b.position
11905
+ )) {
11906
+ if (!script.preBuildScript) continue;
11907
+ const execution = await runHook({
11908
+ input,
11909
+ folder,
11910
+ script,
11911
+ phase: "PRE_BUILD",
11912
+ source: script.preBuildScript,
11913
+ contextPath,
11914
+ hookContext: baseHookContext,
11915
+ logger,
11916
+ signal
11917
+ });
11918
+ scriptExecutions.push(execution);
11919
+ if (execution.causedBuildFailure) {
11920
+ failBuild = true;
11921
+ errorCode = "SCRIPT_FAILED";
11922
+ error = execution.error;
11923
+ break;
11924
+ }
11925
+ if (signal.aborted) break;
11585
11926
  }
11586
- if (signal.aborted) break;
11587
11927
  }
11588
11928
  if (!failBuild && !signal.aborted) {
11589
11929
  try {
@@ -11641,7 +11981,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11641
11981
  ...baseHookContext,
11642
11982
  buildFolder: input.artifactDirectory,
11643
11983
  failed: failBuild || !signal.aborted && buildResult !== null && buildResult.exitCode !== 0,
11644
- cancelled: signal.aborted || buildResult?.cancelled === true,
11984
+ cancelled: signal.aborted || preflightResult?.cancelled === true || buildResult?.cancelled === true,
11645
11985
  errorCode,
11646
11986
  error
11647
11987
  },
@@ -11677,7 +12017,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11677
12017
  }
11678
12018
  const reports = [];
11679
12019
  const testAction = ["TEST", "TEST_WITHOUT_BUILDING"].includes(input.action);
11680
- const canGenerateReports = () => !signal.aborted && buildResult?.cancelled !== true && buildResult?.timedOut !== true;
12020
+ const canGenerateReports = () => !signal.aborted && buildResult !== null && !buildResult.cancelled && !buildResult.timedOut;
11681
12021
  if (canGenerateReports() && testAction && input.advancedSettings.parseTestResults) {
11682
12022
  const report = await writeJsonReport(
11683
12023
  input,
@@ -11721,8 +12061,8 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11721
12061
  await artifact(input.artifactDirectory, "RAW_LOG", rawLog)
11722
12062
  );
11723
12063
  }
11724
- const cancelled = signal.aborted || buildResult?.cancelled === true;
11725
- const timedOut = buildResult?.timedOut === true;
12064
+ const cancelled = signal.aborted || preflightResult?.cancelled === true || buildResult?.cancelled === true;
12065
+ const timedOut = preflightResult?.timedOut === true || buildResult?.timedOut === true;
11726
12066
  const exitCode = cancelled ? null : failBuild || !buildResult ? 1 : buildResult.exitCode;
11727
12067
  const args = xcodeBuildArguments(input);
11728
12068
  const finalStateHash = await worktreeCodeStateHash(
@@ -11733,7 +12073,7 @@ var runIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
11733
12073
  const codeStateObservedAt = (/* @__PURE__ */ new Date()).toISOString();
11734
12074
  return {
11735
12075
  exitCode,
11736
- signal: buildResult?.signal ?? null,
12076
+ signal: buildResult?.signal ?? preflightResult?.signal ?? null,
11737
12077
  timedOut,
11738
12078
  cancelled,
11739
12079
  errorCode,
@@ -11844,6 +12184,67 @@ var downloadIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context
11844
12184
  }
11845
12185
  }
11846
12186
  };
12187
+ var uploadTransferredIosBuildArtifact = async (payload, timeoutMs, signal, onLog, context) => {
12188
+ const input = parseBuildArtifactTransferUploadPayload(payload);
12189
+ if (!context?.uploadBuildArtifactTransfer) {
12190
+ throw new Error("This agent cannot upload transferred build artifacts");
12191
+ }
12192
+ const root = await realpath2(input.artifactDirectory);
12193
+ const target2 = await realpath2(
12194
+ containedPath2(root, input.artifactRelativePath)
12195
+ );
12196
+ const difference = relative3(root, target2);
12197
+ if (!difference || difference === ".." || difference.startsWith(`..${sep3}`) || isAbsolute3(difference)) {
12198
+ throw new Error("Build artifact resolves outside the build folder");
12199
+ }
12200
+ const information = await stat2(target2);
12201
+ let uploadPath = target2;
12202
+ let filename = basename2(target2);
12203
+ let contentType = "application/octet-stream";
12204
+ let temporaryArchive = null;
12205
+ try {
12206
+ if (information.isDirectory()) {
12207
+ temporaryArchive = join3(
12208
+ tmpdir2(),
12209
+ `ade-build-transfer-${input.transferId}-${randomUUID()}.tar.gz`
12210
+ );
12211
+ requireSuccess(
12212
+ await command(
12213
+ "tar",
12214
+ ["-czf", temporaryArchive, "-C", dirname2(target2), basename2(target2)],
12215
+ timeoutMs,
12216
+ signal
12217
+ ),
12218
+ "Could not package the runnable app"
12219
+ );
12220
+ uploadPath = temporaryArchive;
12221
+ filename = `${filename}.tar.gz`;
12222
+ contentType = "application/gzip";
12223
+ } else if (!information.isFile()) {
12224
+ throw new Error("Build artifact is not transferable");
12225
+ }
12226
+ const checksum = await fileChecksum(uploadPath);
12227
+ if (!checksum)
12228
+ throw new Error("Could not checksum the runnable app archive");
12229
+ await onLog({
12230
+ sequence: 0,
12231
+ stream: "SYSTEM",
12232
+ message: `Uploading ${filename} in resumable 16 MiB chunks`,
12233
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12234
+ });
12235
+ await context.uploadBuildArtifactTransfer({
12236
+ transferId: input.transferId,
12237
+ path: uploadPath,
12238
+ filename,
12239
+ contentType,
12240
+ checksum,
12241
+ signal
12242
+ });
12243
+ return successfulProcess;
12244
+ } finally {
12245
+ if (temporaryArchive) await rm3(temporaryArchive, { force: true });
12246
+ }
12247
+ };
11847
12248
  async function runSimpleLogged(logger, options) {
11848
12249
  return runLoggedCommand({
11849
12250
  command: "xcrun",
@@ -11862,17 +12263,7 @@ async function runSimpleLogged(logger, options) {
11862
12263
  function simulatorAppArguments(destinationId) {
11863
12264
  return ["-a", "Simulator", "--args", "-CurrentDeviceUDID", destinationId];
11864
12265
  }
11865
- var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
11866
- const input = parseBuildDeploymentPayload(payload);
11867
- const folder = await validateWorktree(
11868
- input,
11869
- Math.min(timeoutMs, 6e4),
11870
- signal
11871
- );
11872
- const appPath = containedPath2(
11873
- input.artifactDirectory,
11874
- input.artifactRelativePath
11875
- );
12266
+ async function deployPreparedIosBuild(input, folder, appPath, timeoutMs, signal, context) {
11876
12267
  if (!await pathExists(appPath) || !appPath.endsWith(".app")) {
11877
12268
  throw new Error("Runnable app artifact is missing");
11878
12269
  }
@@ -12062,6 +12453,99 @@ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
12062
12453
  } finally {
12063
12454
  await logger.close();
12064
12455
  }
12456
+ }
12457
+ var deployIosBuild = async (payload, timeoutMs, signal, _onLog, context) => {
12458
+ const input = parseBuildDeploymentPayload(payload);
12459
+ const folder = await validateWorktree(
12460
+ input,
12461
+ Math.min(timeoutMs, 6e4),
12462
+ signal
12463
+ );
12464
+ const appPath = containedPath2(
12465
+ input.artifactDirectory,
12466
+ input.artifactRelativePath
12467
+ );
12468
+ return deployPreparedIosBuild(
12469
+ input,
12470
+ folder,
12471
+ appPath,
12472
+ timeoutMs,
12473
+ signal,
12474
+ context
12475
+ );
12476
+ };
12477
+ async function createRemoteBuildStagingDirectory(buildId) {
12478
+ return realpath2(
12479
+ await mkdtemp2(join3(tmpdir2(), `ade-remote-build-${buildId}-`))
12480
+ );
12481
+ }
12482
+ var deployTransferredIosBuild = async (payload, timeoutMs, signal, onLog, context) => {
12483
+ const input = parseBuildRemoteDeploymentPayload(payload);
12484
+ if (!context?.downloadBuildArtifactTransfer) {
12485
+ throw new Error("This agent cannot download transferred build artifacts");
12486
+ }
12487
+ const staging = await createRemoteBuildStagingDirectory(input.buildId);
12488
+ const archive = join3(staging, "artifact.tar.gz");
12489
+ try {
12490
+ await onLog({
12491
+ sequence: 0,
12492
+ stream: "SYSTEM",
12493
+ message: "Waiting for the relayed build artifact",
12494
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
12495
+ });
12496
+ await context.downloadBuildArtifactTransfer({
12497
+ transferId: input.transferId,
12498
+ path: archive,
12499
+ signal
12500
+ });
12501
+ const listing = await command(
12502
+ "tar",
12503
+ ["-tzf", archive],
12504
+ Math.min(timeoutMs, 12e4),
12505
+ signal,
12506
+ staging
12507
+ );
12508
+ requireSuccess(listing, "Transferred build archive is malformed");
12509
+ const entries = listing.stdout.split("\n").map((entry) => entry.trim().replace(/^\.\//, "")).filter(Boolean);
12510
+ if (!entries.length || entries.some(
12511
+ (entry) => entry.startsWith("/") || entry.split("/").some((component) => component === "..")
12512
+ )) {
12513
+ throw new Error("Transferred build archive contains an unsafe path");
12514
+ }
12515
+ const roots = new Set(entries.map((entry) => entry.split("/")[0]));
12516
+ const root = [...roots][0];
12517
+ if (roots.size !== 1 || !root?.endsWith(".app")) {
12518
+ throw new Error("Transferred build archive must contain one app bundle");
12519
+ }
12520
+ requireSuccess(
12521
+ await command(
12522
+ "tar",
12523
+ ["-xzf", archive, "-C", staging],
12524
+ Math.min(timeoutMs, 12e4),
12525
+ signal,
12526
+ staging
12527
+ ),
12528
+ "Could not extract the transferred app"
12529
+ );
12530
+ const appPath = await realpath2(join3(staging, root));
12531
+ const pathDifference = relative3(staging, appPath);
12532
+ if (pathDifference === ".." || pathDifference.startsWith(`..${sep3}`) || isAbsolute3(pathDifference)) {
12533
+ throw new Error("Transferred app resolves outside its staging directory");
12534
+ }
12535
+ return await deployPreparedIosBuild(
12536
+ {
12537
+ ...input,
12538
+ artifactDirectory: staging
12539
+ },
12540
+ staging,
12541
+ appPath,
12542
+ timeoutMs,
12543
+ signal,
12544
+ context
12545
+ );
12546
+ } finally {
12547
+ await rm3(staging, { recursive: true, force: true });
12548
+ }
12065
12549
  };
12066
12550
  function exportPlist(settings) {
12067
12551
  const method = {
@@ -15385,7 +15869,7 @@ var runWorkflowGitCheckpoint = async (payloadValue, _timeoutMs, signal) => {
15385
15869
 
15386
15870
  // src/handlers/worktrees.ts
15387
15871
  import { createWriteStream as createWriteStream2, watch } from "node:fs";
15388
- import { mkdtemp as mkdtemp6, open as open2, readFile as readFile9, realpath as realpath9, rm as rm10, stat as stat10 } from "node:fs/promises";
15872
+ import { mkdtemp as mkdtemp6, open as open3, readFile as readFile9, realpath as realpath9, rm as rm10, stat as stat10 } from "node:fs/promises";
15389
15873
  import { tmpdir as tmpdir6 } from "node:os";
15390
15874
  import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute7, join as join14, relative as relative8 } from "node:path";
15391
15875
  import { spawn as spawn7 } from "node:child_process";
@@ -16649,7 +17133,7 @@ function requestedDiffPaths(input) {
16649
17133
  return [input.path];
16650
17134
  }
16651
17135
  async function readBoundedFile(path, limit) {
16652
- const file = await open2(path, "r");
17136
+ const file = await open3(path, "r");
16653
17137
  try {
16654
17138
  const metadata = await file.stat();
16655
17139
  if (!metadata.isFile()) throw new Error("Changed path is not a file");
@@ -18908,10 +19392,13 @@ var handlers = {
18908
19392
  [IOS_SOURCE_PARSE_JOB_KIND]: parseBuildSourceMetadata,
18909
19393
  [IOS_DESTINATIONS_JOB_KIND]: inspectBuildDestinations,
18910
19394
  [IOS_RUN_DESTINATIONS_JOB_KIND]: inspectBuildRunDestinations,
19395
+ [IOS_AGENT_RUN_DESTINATIONS_JOB_KIND]: inspectAgentBuildRunDestinations,
18911
19396
  [IOS_BUILD_JOB_KIND]: runIosBuild,
18912
19397
  [IOS_BUILD_DELETE_JOB_KIND]: deleteIosBuild,
18913
19398
  [IOS_ARTIFACT_DOWNLOAD_JOB_KIND]: downloadIosBuildArtifact,
19399
+ [IOS_ARTIFACT_TRANSFER_UPLOAD_JOB_KIND]: uploadTransferredIosBuildArtifact,
18914
19400
  [IOS_DEPLOY_JOB_KIND]: deployIosBuild,
19401
+ [IOS_REMOTE_DEPLOY_JOB_KIND]: deployTransferredIosBuild,
18915
19402
  [IOS_EXPORT_JOB_KIND]: exportIosArchive,
18916
19403
  [IOS_TEST_RESULTS_JOB_KIND]: generateIosBuildReport,
18917
19404
  [IOS_COVERAGE_REPORT_JOB_KIND]: generateIosBuildReport,
@@ -19008,6 +19495,8 @@ var JobExecutor = class {
19008
19495
  reportBuildProgress: (input) => this.client.reportBuildProgress(input),
19009
19496
  appendBuildLogChunks: (buildId, chunks) => this.client.appendBuildLogChunks(buildId, chunks),
19010
19497
  uploadBuildArtifact: (input) => this.client.uploadBuildArtifact(input),
19498
+ uploadBuildArtifactTransfer: (input) => this.client.uploadBuildArtifactTransfer(input),
19499
+ downloadBuildArtifactTransfer: (input) => this.client.downloadBuildArtifactTransfer(input),
19011
19500
  claimSigningSecretTransfer: (transferId) => this.client.claimSigningSecretTransfer(transferId),
19012
19501
  claimWorkflowJobSecrets: () => this.client.claimWorkflowJobSecrets(claimed.id),
19013
19502
  appendCommandOutput: (attemptId, chunks) => this.client.appendCommandOutput(claimed.id, attemptId, chunks)
@@ -60864,15 +61353,6 @@ var defaultDependencies = {
60864
61353
  signal.addEventListener("abort", onAbort, { once: true });
60865
61354
  })
60866
61355
  };
60867
- function assertLoopbackServer(server) {
60868
- const hostname2 = new URL(server).hostname.toLowerCase();
60869
- const isIpv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(hostname2);
60870
- if (hostname2 !== "localhost" && hostname2 !== "[::1]" && !isIpv4Loopback) {
60871
- throw new Error(
60872
- `Development auto-enrollment is restricted to loopback servers; received ${hostname2}`
60873
- );
60874
- }
60875
- }
60876
61356
  async function waitForServer(client2, timeoutMs, signal, dependencies) {
60877
61357
  const deadline = Date.now() + timeoutMs;
60878
61358
  let lastError;
@@ -60886,17 +61366,18 @@ async function waitForServer(client2, timeoutMs, signal, dependencies) {
60886
61366
  }
60887
61367
  }
60888
61368
  const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
60889
- throw new Error(`Timed out waiting for the local control plane${detail}`);
61369
+ throw new Error(
61370
+ `Timed out waiting for the development control plane${detail}`
61371
+ );
60890
61372
  }
60891
61373
  async function prepareDevelopmentAgent(options, signal, dependencies = defaultDependencies) {
60892
61374
  const server = normalizeServer(options.server);
60893
- assertLoopbackServer(server);
60894
61375
  const websocketServer = options.websocketServer ?? defaultWebSocketServer(server);
60895
61376
  const configFile = options.configFile ?? developmentConfigPath();
60896
61377
  const inventory = dependencies.inventory();
60897
61378
  const name = options.name ?? `${inventory.hostname}-dev`;
60898
61379
  const anonymousClient = dependencies.createClient(server);
60899
- console.log(`Waiting for local control plane at ${server} ...`);
61380
+ console.log(`Waiting for development control plane at ${server} ...`);
60900
61381
  await waitForServer(
60901
61382
  anonymousClient,
60902
61383
  options.waitTimeoutMs ?? DEFAULT_SERVER_WAIT_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.97",
3
+ "version": "0.0.98",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {