runcloud 0.2.156 → 0.2.158
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/commands/run-cloud-audio.js +84 -0
- package/dist/commands/run-cloud.js +26 -1
- package/dist/commands/xcode.js +46 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.2.158.tgz +0 -0
- package/runcloud-0.2.156.tgz +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
export async function recordSimulatorAudio(session, durationMs, signal, fetchImpl = fetch) {
|
|
3
|
+
if (!Number.isInteger(durationMs) || durationMs < 100 || durationMs > 60000)
|
|
4
|
+
throw new Error('Audio duration must be from 100ms to 60s');
|
|
5
|
+
if (session.status !== 'active' || !session.url || !session.device)
|
|
6
|
+
throw new Error('An active simulator session is required for audio');
|
|
7
|
+
const url = new URL(session.url);
|
|
8
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
9
|
+
throw new Error('Invalid simulator audio URL');
|
|
10
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/helper/${encodeURIComponent(session.device)}/audio.pcm`;
|
|
11
|
+
url.hash = '';
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const cancel = () => controller.abort(signal?.reason);
|
|
14
|
+
if (signal?.aborted)
|
|
15
|
+
cancel();
|
|
16
|
+
else
|
|
17
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
18
|
+
let timer = setTimeout(() => controller.abort(new Error('Simulator audio did not connect within 15 seconds')), 15000);
|
|
19
|
+
let elapsed = false;
|
|
20
|
+
let reader;
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetchImpl(url.toString(), {
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
headers: { Accept: 'application/x.run-cloud.pcm', traceparent: `00-${randomBytes(16).toString('hex')}-${randomBytes(8).toString('hex')}-01` },
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok || !response.body || !response.headers.get('content-type')?.startsWith('application/x.run-cloud.pcm')) {
|
|
27
|
+
await response.body?.cancel();
|
|
28
|
+
throw new Error('Audio is unavailable for this simulator. Reconnect and try again.');
|
|
29
|
+
}
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
const bytes = Math.floor(durationMs * 44100 / 1000) * 4;
|
|
32
|
+
const wav = Buffer.alloc(44 + bytes);
|
|
33
|
+
wav.write('RIFF', 0);
|
|
34
|
+
wav.writeUInt32LE(36 + bytes, 4);
|
|
35
|
+
wav.write('WAVEfmt ', 8);
|
|
36
|
+
wav.writeUInt32LE(16, 16);
|
|
37
|
+
wav.writeUInt16LE(1, 20);
|
|
38
|
+
wav.writeUInt16LE(2, 22);
|
|
39
|
+
wav.writeUInt32LE(44100, 24);
|
|
40
|
+
wav.writeUInt32LE(44100 * 4, 28);
|
|
41
|
+
wav.writeUInt16LE(4, 32);
|
|
42
|
+
wav.writeUInt16LE(16, 34);
|
|
43
|
+
wav.write('data', 36);
|
|
44
|
+
wav.writeUInt32LE(bytes, 40);
|
|
45
|
+
const started = performance.now();
|
|
46
|
+
let writeOffset = 0;
|
|
47
|
+
let pending = Buffer.alloc(0);
|
|
48
|
+
reader = response.body.getReader();
|
|
49
|
+
timer = setTimeout(() => { elapsed = true; controller.abort(); }, durationMs);
|
|
50
|
+
try {
|
|
51
|
+
while (true) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done)
|
|
54
|
+
throw new Error('Simulator audio disconnected before recording finished');
|
|
55
|
+
pending = Buffer.concat([pending, value]);
|
|
56
|
+
while (pending.length >= 4) {
|
|
57
|
+
const size = pending.readUInt32BE();
|
|
58
|
+
if (!size || size > 65536 || size % 4)
|
|
59
|
+
throw new Error('Invalid simulator audio frame');
|
|
60
|
+
if (pending.length < size + 4)
|
|
61
|
+
break;
|
|
62
|
+
const arrivalOffset = Math.max(0, Math.floor((performance.now() - started) * 44100 / 1000) * 4 - size);
|
|
63
|
+
writeOffset = Math.max(writeOffset, arrivalOffset);
|
|
64
|
+
if (writeOffset < bytes)
|
|
65
|
+
pending.copy(wav, 44 + writeOffset, 4, 4 + Math.min(size, bytes - writeOffset));
|
|
66
|
+
writeOffset += size;
|
|
67
|
+
pending = pending.subarray(size + 4);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!elapsed || signal?.aborted)
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return wav;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
controller.abort();
|
|
80
|
+
signal?.removeEventListener('abort', cancel);
|
|
81
|
+
await reader?.cancel().catch(() => undefined);
|
|
82
|
+
reader?.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import { ApiClient, ApiError, ApiTransportError, friendlyApiError } from '../api.js';
|
|
8
8
|
import { requireCredentials } from '../config.js';
|
|
9
|
+
import { recordSimulatorAudio } from './run-cloud-audio.js';
|
|
9
10
|
export const SIMULATOR_KEYS = [
|
|
10
11
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
11
12
|
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
@@ -1040,6 +1041,30 @@ function registerSimulatorCommands(program, platform) {
|
|
|
1040
1041
|
.option('--rm', 'release the session when this command exits', false)
|
|
1041
1042
|
.option('--json', 'output JSON', false)
|
|
1042
1043
|
.addOption(new Option('--dev-host <host>', 'dev-only: target a specific simulator fleet host').hideHelp());
|
|
1044
|
+
simulator.command('audio')
|
|
1045
|
+
.description('Capture simulator speaker output as a WAV file')
|
|
1046
|
+
.argument('<id>', 'session id')
|
|
1047
|
+
.requiredOption('--output <file>', 'destination WAV file')
|
|
1048
|
+
.option('--duration <seconds>', 'recording duration in seconds, from 0.1 to 60', '10')
|
|
1049
|
+
.option('--json', 'output JSON', false)
|
|
1050
|
+
.action((id, opts) => action(async () => {
|
|
1051
|
+
const durationMs = Number(opts.duration) * 1000;
|
|
1052
|
+
if (!Number.isInteger(durationMs) || durationMs < 100 || durationMs > 60000)
|
|
1053
|
+
throw new Error('--duration must be a number of seconds from 0.1 to 60');
|
|
1054
|
+
const session = await client().get(`/run-cloud/${platform}/${encodeURIComponent(id)}`);
|
|
1055
|
+
const controller = new AbortController();
|
|
1056
|
+
const cancel = () => controller.abort(new Error('Audio recording cancelled'));
|
|
1057
|
+
process.once('SIGINT', cancel);
|
|
1058
|
+
try {
|
|
1059
|
+
const wav = await recordSimulatorAudio(session, durationMs, controller.signal);
|
|
1060
|
+
const output = resolve(opts.output);
|
|
1061
|
+
writeFileSync(output, wav, { flag: 'wx' });
|
|
1062
|
+
print({ output, durationMs, sampleRate: 44100, channels: 2, byteSize: wav.length }, opts);
|
|
1063
|
+
}
|
|
1064
|
+
finally {
|
|
1065
|
+
process.removeListener('SIGINT', cancel);
|
|
1066
|
+
}
|
|
1067
|
+
}, opts));
|
|
1043
1068
|
createCommand
|
|
1044
1069
|
.action((opts) => action(async () => {
|
|
1045
1070
|
print(await createSimulatorSession(platform, opts), opts);
|
|
@@ -1666,7 +1691,7 @@ export function registerRunCloud(program) {
|
|
|
1666
1691
|
.command('launch')
|
|
1667
1692
|
.description('Create one session and install one reusable output')
|
|
1668
1693
|
.option('--asset <id>', 'Asset ID to install')
|
|
1669
|
-
.option('--build <id>', '
|
|
1694
|
+
.option('--build <id>', 'installable Xcode simulator build to install')
|
|
1670
1695
|
.requiredOption('--idempotency-key <key>', 'stable retry key owned by this launch intent')
|
|
1671
1696
|
.option('--platform <ios|android>', 'mobile platform', 'ios')
|
|
1672
1697
|
.option('--org-id <id>', 'organization ID')
|
package/dist/commands/xcode.js
CHANGED
|
@@ -252,12 +252,16 @@ export async function requestFor(flags) {
|
|
|
252
252
|
throw new Error('Cloud signing requires --sdk iphoneos');
|
|
253
253
|
if (flags.uploadToAppstore && sdk !== 'iphoneos')
|
|
254
254
|
throw new Error('--upload-to-appstore requires --sdk iphoneos');
|
|
255
|
+
if (flags.applicationId !== undefined && !/^[A-Za-z0-9_.:-]{1,200}$/.test(flags.applicationId)) {
|
|
256
|
+
throw new Error('--application-id must contain 1–200 letters, digits, dots, underscores, colons, or hyphens');
|
|
257
|
+
}
|
|
255
258
|
const ascPrivateKey = flags.ascKey ? await base64File(flags.ascKey, '--asc-key') : undefined;
|
|
256
259
|
const waitTimeoutSeconds = integerFlag(flags.ascWaitTimeout, '--asc-wait-timeout', 0, 1800, 0);
|
|
257
260
|
const timeoutSeconds = integerFlag(flags.timeoutSeconds, '--timeout-seconds', 1, 21600, 3600);
|
|
258
261
|
const parsedBuildSettings = buildSettings(flags.buildSetting);
|
|
259
262
|
return {
|
|
260
263
|
...(flags.orgId ? { orgId: flags.orgId } : {}),
|
|
264
|
+
...(flags.applicationId ? { applicationId: flags.applicationId } : {}),
|
|
261
265
|
...(flags.workspace ? { workspace: flags.workspace } : {}),
|
|
262
266
|
...(flags.project ? { project: flags.project } : {}),
|
|
263
267
|
...(flags.scheme ? { scheme: flags.scheme } : {}),
|
|
@@ -313,13 +317,18 @@ async function logChunk(client, id, offset, source) {
|
|
|
313
317
|
return await retryXcodeRequest(async () => await client.get(`/run-cloud/xcode/builds/${encodeURIComponent(id)}/logs?offset=${offset}${source ? `&source=${source}` : ''}`));
|
|
314
318
|
}
|
|
315
319
|
async function waitForBuild(client, id, flags) {
|
|
320
|
+
validateWaitFlags(flags);
|
|
316
321
|
const timeout = integerFlag(flags.waitTimeout, '--wait-timeout', 1, 23400, 22500);
|
|
317
322
|
const deadline = Date.now() + timeout * 1000;
|
|
318
323
|
let offset = 0;
|
|
319
324
|
let source;
|
|
320
325
|
for (;;) {
|
|
321
326
|
const build = await retryXcodeRequest(async () => await client.get(`/run-cloud/xcode/builds/${encodeURIComponent(id)}`));
|
|
322
|
-
if (
|
|
327
|
+
if (flags.until === 'installable' && build.artifact.kind === 'simulator-app'
|
|
328
|
+
&& build.artifact.assetId && (build.artifact.installable === true
|
|
329
|
+
|| (build.status === 'succeeded' && build.artifact.available)))
|
|
330
|
+
return build;
|
|
331
|
+
if (build.logs.available || build.status === 'queued' || build.status === 'running' || build.status === 'staged') {
|
|
323
332
|
const chunk = await logChunk(client, id, offset, source);
|
|
324
333
|
if (chunk.text)
|
|
325
334
|
(flags.json ? process.stderr : process.stdout).write(chunk.text);
|
|
@@ -337,6 +346,14 @@ async function waitForBuild(client, id, flags) {
|
|
|
337
346
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
|
|
338
347
|
}
|
|
339
348
|
}
|
|
349
|
+
function validateWaitFlags(flags) {
|
|
350
|
+
if (flags.until !== undefined && !['terminal', 'installable'].includes(flags.until)) {
|
|
351
|
+
throw new Error('--until must be terminal or installable');
|
|
352
|
+
}
|
|
353
|
+
if (flags.until === 'installable' && flags.output) {
|
|
354
|
+
throw new Error('--output requires durable completion. Use --until terminal to download, or launch the installable Asset with runcloud mobile launch --asset.');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
340
357
|
async function downloadArtifact(client, build, output, force = false) {
|
|
341
358
|
if (!build.artifact.available)
|
|
342
359
|
throw new Error(`Artifact for Xcode build ${build.id} is not available`);
|
|
@@ -383,15 +400,18 @@ export function registerXcode(program) {
|
|
|
383
400
|
.option('--include <regex>', 'force-include matching source paths; repeatable', repeated, [])
|
|
384
401
|
.option('--additional-file <local=remote>', 'include an extra file; repeatable', repeated, [])
|
|
385
402
|
.option('--org-id <id>', 'organization to own the build')
|
|
403
|
+
.option('--application-id <id>', 'stable application identity for native artifact cache retirement')
|
|
386
404
|
.option('--idempotency-key <key>', 'stable key for retry-safe submission')
|
|
387
405
|
.option('--timeout-seconds <seconds>', 'remote xcodebuild timeout, max 21600', '3600')
|
|
388
406
|
.option('--wait-timeout <seconds>', 'client wait timeout, max 23400', '22500')
|
|
407
|
+
.option('--until <state>', 'wait for terminal completion or an installable simulator app', 'terminal')
|
|
389
408
|
.option('--no-wait', 'return after the control plane accepts the build')
|
|
390
409
|
.option('--detach', 'alias for --no-wait; terminal state remains queryable', false)
|
|
391
410
|
.option('-o, --output <path>', 'download the artifact here after a successful build')
|
|
392
411
|
.option('--force', 'overwrite --output if it exists', false)
|
|
393
412
|
.option('--json', 'print the build record as JSON; streamed logs go to stderr', false)
|
|
394
413
|
.action((sourcePath, flags) => action(async () => {
|
|
414
|
+
validateWaitFlags(flags);
|
|
395
415
|
const request = await requestFor(flags);
|
|
396
416
|
const source = await packSource(sourcePath, flags);
|
|
397
417
|
const archiveBuffer = new ArrayBuffer(source.body.byteLength);
|
|
@@ -415,13 +435,16 @@ export function registerXcode(program) {
|
|
|
415
435
|
if (flags.json) {
|
|
416
436
|
print({ ...build, ...(artifactPath ? { artifactPath } : {}) }, true);
|
|
417
437
|
}
|
|
438
|
+
else if (build.status === 'staged' && flags.until === 'installable') {
|
|
439
|
+
console.log(`Build ${build.id} is ready to install. Asset: ${build.artifact.assetId}`);
|
|
440
|
+
}
|
|
418
441
|
else if (build.status === 'succeeded') {
|
|
419
442
|
console.log(`Build ${build.id} succeeded (exit code ${build.result?.exitCode ?? 0}).`);
|
|
420
443
|
console.log(artifactPath
|
|
421
444
|
? `Artifact: ${artifactPath}`
|
|
422
445
|
: `Download: runcloud xcode artifact ${build.id}`);
|
|
423
446
|
}
|
|
424
|
-
if (build.status !== 'succeeded') {
|
|
447
|
+
if (build.status !== 'succeeded' && !(build.status === 'staged' && flags.until === 'installable')) {
|
|
425
448
|
const reason = build.failure?.reason ?? `Remote build ended with ${build.status}`;
|
|
426
449
|
const next = build.failure?.action ?? `Inspect logs with runcloud xcode logs ${build.id}`;
|
|
427
450
|
throw new Error(`${reason}\n${next}\nBuild ID: ${build.id}`);
|
|
@@ -437,15 +460,18 @@ export function registerXcode(program) {
|
|
|
437
460
|
.option('--include <regex>', 'force-include matching source paths; repeatable', repeated, [])
|
|
438
461
|
.option('--additional-file <local=remote>', 'include an extra file; repeatable', repeated, [])
|
|
439
462
|
.option('--org-id <id>', 'organization to own the build')
|
|
463
|
+
.option('--application-id <id>', 'stable application identity for native artifact cache retirement')
|
|
440
464
|
.option('--idempotency-key <key>', 'stable key for retry-safe submission')
|
|
441
465
|
.option('--timeout-seconds <seconds>', 'remote bundle timeout, max 21600', '3600')
|
|
442
466
|
.option('--wait-timeout <seconds>', 'client wait timeout, max 23400', '22500')
|
|
467
|
+
.option('--until <state>', 'wait for terminal completion or an installable simulator app', 'terminal')
|
|
443
468
|
.option('--no-wait', 'return after the control plane accepts the build')
|
|
444
469
|
.option('--detach', 'alias for --no-wait; terminal state remains queryable', false)
|
|
445
470
|
.option('-o, --output <path>', 'download the rebundled app here after success')
|
|
446
471
|
.option('--force', 'overwrite --output if it exists', false)
|
|
447
472
|
.option('--json', 'print the build record as JSON; streamed logs go to stderr', false)
|
|
448
473
|
.action((sourcePath, flags) => action(async () => {
|
|
474
|
+
validateWaitFlags(flags);
|
|
449
475
|
if (!/^xcb_[A-Za-z0-9]+$/.test(flags.basisBuild)) {
|
|
450
476
|
throw new Error('--basis-build must be a run.cloud Xcode build ID');
|
|
451
477
|
}
|
|
@@ -477,14 +503,31 @@ export function registerXcode(program) {
|
|
|
477
503
|
: undefined;
|
|
478
504
|
if (flags.json)
|
|
479
505
|
print({ ...build, ...(artifactPath ? { artifactPath } : {}) }, true);
|
|
506
|
+
else if (build.status === 'staged' && flags.until === 'installable') {
|
|
507
|
+
console.log(`Rebundle ${build.id} is ready to install. Asset: ${build.artifact.assetId}`);
|
|
508
|
+
}
|
|
480
509
|
else if (build.status === 'succeeded') {
|
|
481
510
|
console.log(`Rebundle ${build.id} succeeded.`);
|
|
482
511
|
console.log(artifactPath ? `Artifact: ${artifactPath}` : `Asset: ${build.artifact.assetId}`);
|
|
483
512
|
}
|
|
484
|
-
if (build.status !== 'succeeded') {
|
|
513
|
+
if (build.status !== 'succeeded' && !(build.status === 'staged' && flags.until === 'installable')) {
|
|
485
514
|
throw new Error(`${build.failure?.reason ?? `Rebundle ended with ${build.status}`}\n${build.failure?.action ?? `Inspect logs with runcloud xcode logs ${build.id}`}\nBuild ID: ${build.id}`);
|
|
486
515
|
}
|
|
487
516
|
}));
|
|
517
|
+
xcode
|
|
518
|
+
.command('wait')
|
|
519
|
+
.description('Wait for build completion or an installable simulator app')
|
|
520
|
+
.argument('<id>')
|
|
521
|
+
.option('--until <state>', 'terminal or installable', 'terminal')
|
|
522
|
+
.option('--wait-timeout <seconds>', 'client wait timeout, max 23400', '22500')
|
|
523
|
+
.option('--json', 'print the build record as JSON; logs go to stderr', false)
|
|
524
|
+
.action((id, flags) => action(async () => {
|
|
525
|
+
const build = await waitForBuild(api(), id, flags);
|
|
526
|
+
print(build, flags.json);
|
|
527
|
+
if (build.status === 'failed' || build.status === 'cancelled') {
|
|
528
|
+
throw new Error(build.failure?.reason ?? `Build ${id} ${build.status}`);
|
|
529
|
+
}
|
|
530
|
+
}));
|
|
488
531
|
xcode
|
|
489
532
|
.command('get')
|
|
490
533
|
.description('Show product-visible Xcode build state')
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.158';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.156.tgz
DELETED
|
Binary file
|