runcloud 0.2.155 → 0.2.157

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.
@@ -1,6 +1,14 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  const PREVIEW_APP_ID = 'app.newly.preview';
4
+ const PLACEHOLDER_COPY = /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|\bplaceholder\b/i;
5
+ const INPUT_PLACEHOLDER = /\bplaceholder\??\s*[=:]\s*(\{[^}]*\}|"[^"]*"|'[^']*'|`[^`]*`|[\w.$]+)/gi;
6
+ const PLACEHOLDER_NAME = /\.placeholder\b|\bplaceholder\b(?=\s*[,})\]])/gi;
7
+ function withoutInputPlaceholders(line) {
8
+ return line
9
+ .replace(INPUT_PLACEHOLDER, (_prop, value) => (/^["'`]/.test(value) ? value.replace(/example\.com/gi, '') : ''))
10
+ .replace(PLACEHOLDER_NAME, '');
11
+ }
4
12
  const CONFIG_RULES = {
5
13
  app_name: { path: ['expo', 'name'], absent: 'fail' },
6
14
  slug: { path: ['expo', 'slug'], absent: 'fail' },
@@ -157,7 +165,9 @@ function evaluateLocalItem(root, appJson, item) {
157
165
  case 'http_endpoints':
158
166
  return patternItem(root, item, /http:\/\/(?!localhost|127\.0\.0\.1)/i, 'warning', 'Non-local HTTP endpoint found.', 'No non-local HTTP endpoints found.');
159
167
  case 'placeholder_content':
160
- return patternItem(root, item, /lorem ipsum|example\.com|\bTBD\b|\bTODO\b|Coming soon|placeholder/i, 'warning', 'Placeholder text found.', 'No placeholder text found.');
168
+ return patternItem(root, item, PLACEHOLDER_COPY, 'warning', 'Placeholder text found.', 'No placeholder text found.', {
169
+ filter: (match) => PLACEHOLDER_COPY.test(withoutInputPlaceholders(match.text)),
170
+ });
161
171
  case 'competitor_reference':
162
172
  return patternItem(root, item, /google play|\.apk\b|samsung|android app/i, 'warning', 'Possible platform/competitor reference found in user-facing copy.', 'No competitor platform references found.', { dirs: ['app', 'src', 'components'] });
163
173
  case 'private_api_usage':
@@ -180,7 +190,7 @@ function evaluateLocalItem(root, appJson, item) {
180
190
  case 'external_payment_digital':
181
191
  return evaluateExternalPayment(root, item);
182
192
  case 'expo_layout_missing':
183
- return evaluateExpoLayout(root, item);
193
+ return makeItem(item, 'pass', 'Expo Router adds a layout where a directory has none.', 'expo-router getRoutesCore.js');
184
194
  case 'expo_duplicate_route':
185
195
  return evaluateDuplicateRoutes(root, item);
186
196
  case 'ios_encryption':
@@ -248,16 +258,6 @@ function evaluateExternalPayment(root, item) {
248
258
  return makeItem(item, 'pass', 'No external payment SDK patterns found.', 'scan: no Stripe/PayPal/Braintree patterns found');
249
259
  return makeItem(item, 'warning', 'External payment SDK found; verify it is not used for digital goods on iOS.', evidence(matches));
250
260
  }
251
- function evaluateExpoLayout(root, item) {
252
- const appDir = path.join(root, 'app');
253
- if (!existsSync(appDir))
254
- return makeItem(item, 'pass', 'No Expo Router app directory found.', 'scan: app/ directory absent');
255
- const dirs = walkDirs(appDir).map((dir) => path.relative(root, dir) || 'app');
256
- const missing = dirs.filter((dir) => !hasLayoutFile(path.join(root, dir)));
257
- if (!missing.length)
258
- return makeItem(item, 'pass', 'Every app route directory has a layout file.', 'scan: app route directories');
259
- return makeItem(item, 'fail', 'Expo Router directories without _layout files found.', missing.slice(0, 8).join(', '));
260
- }
261
261
  function evaluateDuplicateRoutes(root, item) {
262
262
  const appDir = routerDir(root);
263
263
  if (!existsSync(appDir))
@@ -516,17 +516,6 @@ function walkFiles(dir) {
516
516
  }
517
517
  return files;
518
518
  }
519
- function walkDirs(dir) {
520
- const dirs = [dir];
521
- for (const entry of safeReadDir(dir)) {
522
- if (SKIP_DIRS.has(entry.name))
523
- continue;
524
- const full = path.join(dir, entry.name);
525
- if (entry.isDirectory())
526
- dirs.push(...walkDirs(full));
527
- }
528
- return dirs;
529
- }
530
519
  function shouldScanFile(file) {
531
520
  return TEXT_EXTENSIONS.has(path.extname(file));
532
521
  }
@@ -536,9 +525,6 @@ function findByName(root, name) {
536
525
  function sourceText(root, dirs) {
537
526
  return collectScanFiles(root, dirs).map((file) => readText(file) ?? '').join('\n');
538
527
  }
539
- function hasLayoutFile(dir) {
540
- return ['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'].some((name) => existsSync(path.join(dir, name)));
541
- }
542
528
  function routerDir(root) {
543
529
  const src = path.join(root, 'src', 'app');
544
530
  return safeStat(src)?.isDirectory() ? src : path.join(root, 'app');
@@ -1666,7 +1666,7 @@ export function registerRunCloud(program) {
1666
1666
  .command('launch')
1667
1667
  .description('Create one session and install one reusable output')
1668
1668
  .option('--asset <id>', 'Asset ID to install')
1669
- .option('--build <id>', 'successful Xcode simulator build to install')
1669
+ .option('--build <id>', 'installable Xcode simulator build to install')
1670
1670
  .requiredOption('--idempotency-key <key>', 'stable retry key owned by this launch intent')
1671
1671
  .option('--platform <ios|android>', 'mobile platform', 'ios')
1672
1672
  .option('--org-id <id>', 'organization ID')
@@ -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 (build.logs.available || build.status === 'queued' || build.status === 'running') {
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.155';
1
+ export const CLI_VERSION = '0.2.157';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.2.155",
3
+ "version": "0.2.157",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file