@devicecloud.dev/dcd 5.0.0-beta.0 → 5.0.0-beta.2

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/commands/artifacts.d.ts +28 -28
  4. package/dist/commands/artifacts.js +20 -23
  5. package/dist/commands/cloud.d.ts +57 -57
  6. package/dist/commands/cloud.js +224 -192
  7. package/dist/commands/list.d.ts +22 -22
  8. package/dist/commands/list.js +43 -40
  9. package/dist/commands/live.js +134 -127
  10. package/dist/commands/login.d.ts +11 -11
  11. package/dist/commands/login.js +46 -44
  12. package/dist/commands/logout.js +16 -18
  13. package/dist/commands/status.d.ts +11 -11
  14. package/dist/commands/status.js +53 -44
  15. package/dist/commands/switch-org.d.ts +7 -7
  16. package/dist/commands/switch-org.js +19 -21
  17. package/dist/commands/upgrade.js +41 -33
  18. package/dist/commands/upload.d.ts +10 -10
  19. package/dist/commands/upload.js +42 -43
  20. package/dist/commands/whoami.js +17 -20
  21. package/dist/config/environments.js +6 -12
  22. package/dist/config/flags/api.flags.js +1 -4
  23. package/dist/config/flags/binary.flags.js +1 -4
  24. package/dist/config/flags/device.flags.js +6 -9
  25. package/dist/config/flags/environment.flags.js +1 -4
  26. package/dist/config/flags/execution.flags.js +1 -4
  27. package/dist/config/flags/github.flags.js +1 -4
  28. package/dist/config/flags/output.flags.js +1 -4
  29. package/dist/constants.js +15 -18
  30. package/dist/gateways/api-gateway.d.ts +31 -6
  31. package/dist/gateways/api-gateway.js +70 -16
  32. package/dist/gateways/cli-auth-gateway.d.ts +1 -1
  33. package/dist/gateways/cli-auth-gateway.js +3 -6
  34. package/dist/gateways/realtime-gateway.d.ts +32 -0
  35. package/dist/gateways/realtime-gateway.js +103 -0
  36. package/dist/gateways/supabase-gateway.d.ts +1 -1
  37. package/dist/gateways/supabase-gateway.js +10 -14
  38. package/dist/index.js +41 -38
  39. package/dist/mcp/context.d.ts +33 -0
  40. package/dist/mcp/context.js +33 -0
  41. package/dist/mcp/helpers.d.ts +16 -0
  42. package/dist/mcp/helpers.js +34 -0
  43. package/dist/mcp/index.d.ts +2 -0
  44. package/dist/mcp/index.js +24 -0
  45. package/dist/mcp/server.d.ts +7 -0
  46. package/dist/mcp/server.js +27 -0
  47. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  48. package/dist/mcp/tools/download-artifacts.js +84 -0
  49. package/dist/mcp/tools/get-status.d.ts +7 -0
  50. package/dist/mcp/tools/get-status.js +39 -0
  51. package/dist/mcp/tools/list-devices.d.ts +7 -0
  52. package/dist/mcp/tools/list-devices.js +27 -0
  53. package/dist/mcp/tools/list-runs.d.ts +3 -0
  54. package/dist/mcp/tools/list-runs.js +60 -0
  55. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  56. package/dist/mcp/tools/run-cloud-test.js +233 -0
  57. package/dist/methods.d.ts +32 -1
  58. package/dist/methods.js +133 -79
  59. package/dist/services/device-validation.service.d.ts +1 -1
  60. package/dist/services/device-validation.service.js +1 -5
  61. package/dist/services/execution-plan.service.js +14 -17
  62. package/dist/services/execution-plan.utils.js +15 -23
  63. package/dist/services/flow-paths.d.ts +17 -0
  64. package/dist/services/flow-paths.js +52 -0
  65. package/dist/services/metadata-extractor.service.js +22 -25
  66. package/dist/services/moropo.service.js +18 -20
  67. package/dist/services/report-download.service.d.ts +1 -1
  68. package/dist/services/report-download.service.js +5 -9
  69. package/dist/services/results-polling.service.d.ts +18 -3
  70. package/dist/services/results-polling.service.js +211 -108
  71. package/dist/services/telemetry.service.d.ts +10 -1
  72. package/dist/services/telemetry.service.js +40 -18
  73. package/dist/services/test-submission.service.d.ts +21 -4
  74. package/dist/services/test-submission.service.js +51 -34
  75. package/dist/services/version.service.d.ts +30 -7
  76. package/dist/services/version.service.js +88 -32
  77. package/dist/types/domain/auth.types.d.ts +8 -0
  78. package/dist/types/domain/auth.types.js +1 -2
  79. package/dist/types/domain/device.types.js +8 -11
  80. package/dist/types/domain/live.types.js +1 -2
  81. package/dist/types/generated/schema.types.js +1 -2
  82. package/dist/types/index.d.ts +2 -2
  83. package/dist/types/index.js +2 -18
  84. package/dist/types.js +1 -2
  85. package/dist/utils/auth.d.ts +1 -1
  86. package/dist/utils/auth.js +27 -28
  87. package/dist/utils/ci.d.ts +12 -0
  88. package/dist/utils/ci.js +39 -0
  89. package/dist/utils/cli.d.ts +16 -2
  90. package/dist/utils/cli.js +57 -29
  91. package/dist/utils/compatibility.d.ts +1 -1
  92. package/dist/utils/compatibility.js +5 -7
  93. package/dist/utils/config-store.js +33 -43
  94. package/dist/utils/connectivity.js +1 -4
  95. package/dist/utils/expo.js +15 -21
  96. package/dist/utils/orgs.js +8 -12
  97. package/dist/utils/paths.js +2 -5
  98. package/dist/utils/progress.d.ts +3 -0
  99. package/dist/utils/progress.js +47 -8
  100. package/dist/utils/styling.d.ts +35 -37
  101. package/dist/utils/styling.js +52 -86
  102. package/dist/utils/ui.d.ts +41 -0
  103. package/dist/utils/ui.js +95 -0
  104. package/package.json +27 -24
package/dist/methods.js CHANGED
@@ -1,20 +1,17 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = void 0;
4
- const progress_1 = require("./utils/progress");
5
- const node_crypto_1 = require("node:crypto");
6
- const node_fs_1 = require("node:fs");
7
- const promises_1 = require("node:fs/promises");
8
- const os = require("node:os");
9
- const path = require("node:path");
10
- const promises_2 = require("node:stream/promises");
11
- const StreamZip = require("node-stream-zip");
12
- const yazl = require("yazl");
13
- const environments_1 = require("./config/environments");
14
- const api_gateway_1 = require("./gateways/api-gateway");
15
- const supabase_gateway_1 = require("./gateways/supabase-gateway");
16
- const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
17
- const styling_1 = require("./utils/styling");
1
+ import { ux } from './utils/progress.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { createReadStream, createWriteStream, mkdirSync, readdirSync, writeFileSync, } from 'node:fs';
4
+ import { access, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import StreamZip from 'node-stream-zip';
9
+ import * as yazl from 'yazl';
10
+ import { inferEnvFromApiUrl } from './config/environments.js';
11
+ import { ApiError, ApiGateway } from './gateways/api-gateway.js';
12
+ import { SupabaseGateway } from './gateways/supabase-gateway.js';
13
+ import { MetadataExtractorService } from './services/metadata-extractor.service.js';
14
+ import { colors, formatId } from './utils/styling.js';
18
15
  const mimeTypeLookupByExtension = {
19
16
  apk: 'application/vnd.android.package-archive',
20
17
  yaml: 'application/x-yaml',
@@ -44,7 +41,7 @@ function toZipEntryName(relativePath) {
44
41
  async function compressFolderToTempZip(sourceDir) {
45
42
  const zipfile = new yazl.ZipFile();
46
43
  const rootName = path.basename(sourceDir);
47
- const entries = (0, node_fs_1.readdirSync)(sourceDir, {
44
+ const entries = readdirSync(sourceDir, {
48
45
  recursive: true,
49
46
  withFileTypes: true,
50
47
  });
@@ -55,13 +52,13 @@ async function compressFolderToTempZip(sourceDir) {
55
52
  const relativePath = path.relative(sourceDir, absolutePath);
56
53
  zipfile.addFile(absolutePath, toZipEntryName(path.join(rootName, relativePath)));
57
54
  }
58
- const tempDir = await (0, promises_1.mkdtemp)(path.join(os.tmpdir(), 'dcd-app-zip-'));
55
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-app-zip-'));
59
56
  const zipPath = path.join(tempDir, `${rootName}.zip`);
60
57
  zipfile.end();
61
- await (0, promises_2.pipeline)(zipfile.outputStream, (0, node_fs_1.createWriteStream)(zipPath));
58
+ await pipeline(zipfile.outputStream, createWriteStream(zipPath));
62
59
  return { tempDir, zipPath };
63
60
  }
64
- const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
61
+ export const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
65
62
  const zipfile = new yazl.ZipFile();
66
63
  for (const file of files) {
67
64
  // Anchored prefix strip — replace() would remove the first occurrence
@@ -70,8 +67,7 @@ const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
70
67
  }
71
68
  return zipToBuffer(zipfile);
72
69
  };
73
- exports.compressFilesFromRelativePath = compressFilesFromRelativePath;
74
- const verifyAppZip = async (zipPath) => {
70
+ export const verifyAppZip = async (zipPath) => {
75
71
  // eslint-disable-next-line import/namespace, new-cap
76
72
  const zip = await new StreamZip.async({
77
73
  file: zipPath,
@@ -96,11 +92,10 @@ const verifyAppZip = async (zipPath) => {
96
92
  zip.close();
97
93
  }
98
94
  };
99
- exports.verifyAppZip = verifyAppZip;
100
- const uploadBinary = async (config) => {
95
+ export const uploadBinary = async (config) => {
101
96
  const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config;
102
97
  if (log) {
103
- progress_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
98
+ ux.action.start(colors.bold('Checking and uploading binary'), colors.dim('Initializing'), {
104
99
  stdout: true,
105
100
  });
106
101
  }
@@ -122,8 +117,8 @@ const uploadBinary = async (config) => {
122
117
  const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug);
123
118
  if (exists && binaryId) {
124
119
  if (log) {
125
- progress_1.ux.info(styling_1.colors.dim('SHA hash matches existing binary with ID: ') + (0, styling_1.formatId)(binaryId) + styling_1.colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
126
- progress_1.ux.action.stop(styling_1.colors.info('Skipping upload'));
120
+ ux.info(colors.dim('SHA hash matches existing binary with ID: ') + formatId(binaryId) + colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
121
+ ux.action.stop(colors.info('Skipping upload'));
127
122
  }
128
123
  return binaryId;
129
124
  }
@@ -131,13 +126,13 @@ const uploadBinary = async (config) => {
131
126
  // Perform the upload
132
127
  const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime });
133
128
  if (log) {
134
- progress_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
129
+ ux.action.stop(colors.success('\n✓ Binary uploaded with ID: ') + formatId(uploadId));
135
130
  }
136
131
  return uploadId;
137
132
  }
138
133
  catch (error) {
139
134
  if (log) {
140
- progress_1.ux.action.stop(styling_1.colors.error('✗ Failed'));
135
+ ux.action.stop(colors.error('✗ Failed'));
141
136
  }
142
137
  if (debug) {
143
138
  console.error('[DEBUG] === BINARY UPLOAD FAILED ===');
@@ -153,11 +148,10 @@ const uploadBinary = async (config) => {
153
148
  }
154
149
  finally {
155
150
  if (source?.cleanupDir) {
156
- await (0, promises_1.rm)(source.cleanupDir, { recursive: true, force: true }).catch(() => { });
151
+ await rm(source.cleanupDir, { recursive: true, force: true }).catch(() => { });
157
152
  }
158
153
  }
159
154
  };
160
- exports.uploadBinary = uploadBinary;
161
155
  /**
162
156
  * Prepares a file for upload: .app directories are zipped to a temp file on
163
157
  * disk; everything else is described in place. Nothing is read into memory.
@@ -178,7 +172,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
178
172
  // Validate that the .app directory exists before attempting to compress —
179
173
  // zipping a non-existent path silently produces an empty 22-byte zip.
180
174
  try {
181
- await (0, promises_1.access)(filePath);
175
+ await access(filePath);
182
176
  }
183
177
  catch {
184
178
  // Provide helpful error message for common quoting issues
@@ -201,7 +195,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
201
195
  throw new Error(errorMessage);
202
196
  }
203
197
  const { tempDir, zipPath } = await compressFolderToTempZip(filePath);
204
- const { size } = await (0, promises_1.stat)(zipPath);
198
+ const { size } = await stat(zipPath);
205
199
  source = {
206
200
  contentType: 'application/zip',
207
201
  cleanupDir: tempDir,
@@ -214,7 +208,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
214
208
  }
215
209
  }
216
210
  else {
217
- const { size } = await (0, promises_1.stat)(filePath);
211
+ const { size } = await stat(filePath);
218
212
  if (debug) {
219
213
  console.log(`[DEBUG] File size: ${(size / 1024 / 1024).toFixed(2)} MB`);
220
214
  }
@@ -276,7 +270,7 @@ async function checkExistingUpload(apiUrl, auth, sha, debug) {
276
270
  console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
277
271
  }
278
272
  const shaCheckStartTime = Date.now();
279
- const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, auth, sha);
273
+ const { appBinaryId, exists } = await ApiGateway.checkForExistingUpload(apiUrl, auth, sha);
280
274
  if (debug) {
281
275
  console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
282
276
  console.log(`[DEBUG] Existing binary found: ${exists}`);
@@ -289,7 +283,7 @@ async function checkExistingUpload(apiUrl, auth, sha, debug) {
289
283
  catch (error) {
290
284
  // Invalid credentials will fail every subsequent request — surface now
291
285
  // rather than after the user has waited through a potentially huge upload.
292
- if (error instanceof api_gateway_1.ApiError && (error.status === 401 || error.status === 403)) {
286
+ if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
293
287
  throw error;
294
288
  }
295
289
  if (debug) {
@@ -325,7 +319,7 @@ async function uploadToSupabase(env, tempPath, source, debug) {
325
319
  }
326
320
  try {
327
321
  const uploadStartTime = Date.now();
328
- await supabase_gateway_1.SupabaseGateway.uploadResumable(env, tempPath, source, debug);
322
+ await SupabaseGateway.uploadResumable(env, tempPath, source, debug);
329
323
  if (debug) {
330
324
  const uploadDuration = Date.now() - uploadStartTime;
331
325
  const uploadDurationSeconds = uploadDuration / 1000;
@@ -428,7 +422,7 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
428
422
  }
429
423
  try {
430
424
  const urlRequestStartTime = Date.now();
431
- const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, auth, platform, fileSize);
425
+ const { id, tempPath, finalPath, b2 } = await ApiGateway.getBinaryUploadUrl(apiUrl, auth, platform, fileSize);
432
426
  if (debug) {
433
427
  const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
434
428
  console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
@@ -451,9 +445,11 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
451
445
  // Add context to the error
452
446
  if (error instanceof Error) {
453
447
  if (error.name === 'NetworkError') {
454
- throw new Error(`Failed to request upload URL from API.\n\n${error.message}`);
448
+ throw new Error(`Failed to request upload URL from API.\n\n${error.message}`, { cause: error });
455
449
  }
456
- throw new Error(`Failed to request upload URL: ${error.message}`);
450
+ throw new Error(`Failed to request upload URL: ${error.message}`, {
451
+ cause: error,
452
+ });
457
453
  }
458
454
  throw error;
459
455
  }
@@ -467,7 +463,7 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
467
463
  async function extractBinaryMetadata(filePath, debug) {
468
464
  if (debug)
469
465
  console.log('[DEBUG] Extracting app metadata...');
470
- const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
466
+ const metadataExtractor = new MetadataExtractorService();
471
467
  const metadata = await metadataExtractor.extract(filePath);
472
468
  if (!metadata) {
473
469
  throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
@@ -513,7 +509,7 @@ async function performUpload(config) {
513
509
  const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, auth, filePath, source.size, debug);
514
510
  // Extract app metadata
515
511
  const metadata = await extractBinaryMetadata(filePath, debug);
516
- const env = (0, environments_1.inferEnvFromApiUrl)(apiUrl);
512
+ const env = inferEnvFromApiUrl(apiUrl);
517
513
  // Upload to Backblaze first (primary)
518
514
  const backblazeResult = await handleBackblazeUpload({
519
515
  auth,
@@ -525,11 +521,10 @@ async function performUpload(config) {
525
521
  });
526
522
  let lastError = backblazeResult.error;
527
523
  // Always upload to Supabase (re-enabled as always-on alongside Backblaze)
528
- let supabaseResult = { error: null, success: false };
529
524
  if (debug) {
530
525
  console.log('[DEBUG] Uploading to Supabase...');
531
526
  }
532
- supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
527
+ const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
533
528
  if (!supabaseResult.success && supabaseResult.error) {
534
529
  lastError = supabaseResult.error;
535
530
  }
@@ -549,7 +544,7 @@ async function performUpload(config) {
549
544
  }
550
545
  // Finalize upload
551
546
  const finalizeStartTime = Date.now();
552
- await api_gateway_1.ApiGateway.finaliseUpload({
547
+ await ApiGateway.finaliseUpload({
553
548
  auth,
554
549
  backblazeSuccess: backblazeResult.success,
555
550
  baseUrl: apiUrl,
@@ -567,6 +562,70 @@ async function performUpload(config) {
567
562
  }
568
563
  return id;
569
564
  }
565
+ /**
566
+ * Uploads an already-built flow zip directly to storage, mirroring the binary
567
+ * client-direct path: getFlowUploadUrl → upload to storage (Backblaze if
568
+ * offered + TUS to Supabase) → return the storage reference for submitFlowTest.
569
+ *
570
+ * The zip arrives as an in-memory Buffer (from `compressFilesFromRelativePath`);
571
+ * it's written to a temp file so the exact same disk-streaming uploaders the
572
+ * binary path uses can be reused unchanged. `supabaseSuccess`/`backblazeSuccess`
573
+ * are reported honestly based on which uploads succeeded.
574
+ *
575
+ * Errors from `getFlowUploadUrl` propagate untouched so callers can detect a
576
+ * 404 from an older API and fall back to the legacy multipart endpoint.
577
+ */
578
+ export const uploadFlowZip = async (config) => {
579
+ const { apiUrl, auth, buffer, debug = false } = config;
580
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-flow-zip-'));
581
+ const diskPath = path.join(tempDir, 'flowFile.zip');
582
+ try {
583
+ await writeFile(diskPath, buffer);
584
+ const source = {
585
+ contentType: 'application/zip',
586
+ diskPath,
587
+ name: 'flowFile.zip',
588
+ size: buffer.length,
589
+ };
590
+ // Same response shape as getBinaryUploadUrl. A 404 here means the API
591
+ // predates the client-direct flow path — let it propagate so the caller
592
+ // falls back to the multipart endpoint.
593
+ const { id, tempPath, finalPath, b2 } = await ApiGateway.getFlowUploadUrl(apiUrl, auth, buffer.length);
594
+ if (!tempPath)
595
+ throw new Error('No upload path provided by API');
596
+ const env = inferEnvFromApiUrl(apiUrl);
597
+ // Upload to Backblaze first (primary, if configured)
598
+ const backblazeResult = await handleBackblazeUpload({
599
+ auth,
600
+ apiUrl,
601
+ b2: b2,
602
+ debug,
603
+ finalPath,
604
+ source,
605
+ });
606
+ let lastError = backblazeResult.error;
607
+ // Always upload to Supabase (always-on alongside Backblaze)
608
+ const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
609
+ if (!supabaseResult.success && supabaseResult.error) {
610
+ lastError = supabaseResult.error;
611
+ }
612
+ validateUploadResults(supabaseResult.success, backblazeResult.success, lastError, b2, debug);
613
+ if (debug) {
614
+ console.log(`[DEBUG] Flow zip upload summary - Backblaze: ${backblazeResult.success ? '✓' : '✗'}, Supabase: ${supabaseResult.success ? '✓' : '✗'}`);
615
+ }
616
+ return {
617
+ backblazeSuccess: backblazeResult.success,
618
+ bytes: buffer.length,
619
+ id,
620
+ path: tempPath,
621
+ supabaseSuccess: supabaseResult.success,
622
+ useTus: true,
623
+ };
624
+ }
625
+ finally {
626
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
627
+ }
628
+ };
570
629
  /**
571
630
  * Upload file to Backblaze using signed URL (simple upload for files < 100MB)
572
631
  * @param uploadUrl - Backblaze upload URL
@@ -582,9 +641,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
582
641
  // get the multi-part path), so one transient buffer here is bounded.
583
642
  // S3 pre-signed PUTs reject chunked transfer encoding, which rules out a
584
643
  // plain stream body.
585
- const body = await (0, promises_1.readFile)(source.diskPath);
644
+ const body = await readFile(source.diskPath);
586
645
  // Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
587
- const sha1 = (0, node_crypto_1.createHash)('sha1');
646
+ const sha1 = createHash('sha1');
588
647
  sha1.update(body);
589
648
  const sha1Hex = sha1.digest('hex');
590
649
  // Detect if this is an S3 pre-signed URL (authorization token is empty)
@@ -617,8 +676,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
617
676
  if (debug) {
618
677
  console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`);
619
678
  }
620
- // Don't throw - we don't want Backblaze failures to block the primary upload
621
- console.warn(`Warning: Backblaze upload failed with status ${response.status}`);
679
+ // Don't throw and don't warn Backblaze is the primary attempt and the
680
+ // Supabase fallback usually recovers. A user-facing error is raised only
681
+ // if every strategy fails (see validateUploadResults).
622
682
  return false;
623
683
  }
624
684
  if (debug) {
@@ -641,12 +701,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
641
701
  if (debug) {
642
702
  console.error('[DEBUG] Network error detected - could be DNS, connection, or SSL issue');
643
703
  }
644
- console.warn('Warning: Backblaze upload failed due to network error');
645
- }
646
- else {
647
- // Don't throw - we don't want Backblaze failures to block the primary upload
648
- console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
649
704
  }
705
+ // Don't throw and don't warn — the Supabase fallback usually recovers, and
706
+ // validateUploadResults raises a user-facing error only if all fail.
650
707
  return false;
651
708
  }
652
709
  }
@@ -660,7 +717,7 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
660
717
  async function readFileChunk(filePath, start, end) {
661
718
  return new Promise((resolve, reject) => {
662
719
  const chunks = [];
663
- const stream = (0, node_fs_1.createReadStream)(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
720
+ const stream = createReadStream(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
664
721
  stream.on('data', (chunk) => {
665
722
  chunks.push(chunk);
666
723
  });
@@ -678,7 +735,7 @@ async function readFileChunk(filePath, start, end) {
678
735
  * @returns SHA1 hash as hex string
679
736
  */
680
737
  function calculateSha1(buffer) {
681
- const sha1 = (0, node_crypto_1.createHash)('sha1');
738
+ const sha1 = createHash('sha1');
682
739
  sha1.update(buffer);
683
740
  return sha1.digest('hex');
684
741
  }
@@ -716,7 +773,9 @@ async function uploadPartToBackblaze(config) {
716
773
  if (debug) {
717
774
  console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
718
775
  }
719
- throw new Error(`Part ${partNumber} upload failed due to network error`);
776
+ throw new Error(`Part ${partNumber} upload failed due to network error`, {
777
+ cause: error,
778
+ });
720
779
  }
721
780
  throw error;
722
781
  }
@@ -740,12 +799,9 @@ function logBackblazeUploadError(error, debug) {
740
799
  console.error(`[DEBUG] Stack trace:\n${error.stack}`);
741
800
  }
742
801
  }
743
- if (error instanceof Error && error.message.includes('network error')) {
744
- console.warn('Warning: Backblaze large file upload failed due to network error');
745
- }
746
- else {
747
- console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
748
- }
802
+ // No user-facing warning: Backblaze is the primary attempt and the Supabase
803
+ // fallback usually recovers; validateUploadResults raises the only
804
+ // user-facing error, and only when every strategy fails.
749
805
  }
750
806
  /**
751
807
  * Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
@@ -790,7 +846,7 @@ async function uploadLargeFileToBackblaze(config) {
790
846
  console.log('[DEBUG] Finishing large file upload...');
791
847
  console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
792
848
  }
793
- await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
849
+ await ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
794
850
  if (debug)
795
851
  console.log('[DEBUG] Large file upload completed successfully');
796
852
  return true;
@@ -801,8 +857,8 @@ async function uploadLargeFileToBackblaze(config) {
801
857
  }
802
858
  }
803
859
  async function getFileHashFromPath(filePath) {
804
- const hash = (0, node_crypto_1.createHash)('sha256');
805
- for await (const chunk of (0, node_fs_1.createReadStream)(filePath)) {
860
+ const hash = createHash('sha256');
861
+ for await (const chunk of createReadStream(filePath)) {
806
862
  hash.update(chunk);
807
863
  }
808
864
  return hash.digest('hex');
@@ -814,37 +870,36 @@ async function getFileHashFromPath(filePath) {
814
870
  * @param logger - Logger object with log and warn methods
815
871
  * @returns true if successful, false if an error occurred
816
872
  */
817
- const writeJSONFile = (filePath, data, logger) => {
873
+ export const writeJSONFile = (filePath, data, logger) => {
818
874
  try {
819
875
  const directory = path.dirname(filePath);
820
876
  if (directory !== '.') {
821
- (0, node_fs_1.mkdirSync)(directory, { recursive: true });
877
+ mkdirSync(directory, { recursive: true });
822
878
  }
823
- (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2));
824
- logger.log(styling_1.colors.dim('JSON output written to: ') + styling_1.colors.highlight(path.resolve(filePath)));
879
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
880
+ logger.log(colors.dim('JSON output written to: ') + colors.highlight(path.resolve(filePath)));
825
881
  }
826
882
  catch (error) {
827
883
  const errorMessage = error instanceof Error ? error.message : String(error);
828
884
  const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
829
885
  const isNoSuchFileError = errorMessage.includes('ENOENT');
830
- logger.warn(styling_1.colors.error(`Failed to write JSON output to file: ${filePath}`));
886
+ logger.warn(colors.error(`Failed to write JSON output to file: ${filePath}`));
831
887
  if (isPermissionError) {
832
- logger.warn(styling_1.colors.dim(' Permission denied - check file/directory write permissions'));
833
- logger.warn(styling_1.colors.dim(' Try running with appropriate permissions or choose a different output location'));
888
+ logger.warn(colors.dim(' Permission denied - check file/directory write permissions'));
889
+ logger.warn(colors.dim(' Try running with appropriate permissions or choose a different output location'));
834
890
  }
835
891
  else if (isNoSuchFileError) {
836
- logger.warn(styling_1.colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
892
+ logger.warn(colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
837
893
  }
838
- logger.warn(styling_1.colors.dim(' Error details: ') + errorMessage);
894
+ logger.warn(colors.dim(' Error details: ') + errorMessage);
839
895
  }
840
896
  };
841
- exports.writeJSONFile = writeJSONFile;
842
897
  /**
843
898
  * Formats duration in seconds into a human readable string
844
899
  * @param durationSeconds - Duration in seconds
845
900
  * @returns Formatted duration string (e.g. "2m 30s" or "45s")
846
901
  */
847
- const formatDurationSeconds = (durationSeconds) => {
902
+ export const formatDurationSeconds = (durationSeconds) => {
848
903
  const minutes = Math.floor(durationSeconds / 60);
849
904
  const seconds = durationSeconds % 60;
850
905
  if (minutes > 0) {
@@ -852,4 +907,3 @@ const formatDurationSeconds = (durationSeconds) => {
852
907
  }
853
908
  return `${durationSeconds}s`;
854
909
  };
855
- exports.formatDurationSeconds = formatDurationSeconds;
@@ -1,4 +1,4 @@
1
- import { CompatibilityData } from '../utils/compatibility';
1
+ import { CompatibilityData } from '../utils/compatibility.js';
2
2
  export interface DeviceValidationOptions {
3
3
  debug?: boolean;
4
4
  logger?: (message: string) => void;
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DeviceValidationService = void 0;
4
1
  /**
5
2
  * Service for validating device configurations against compatibility data
6
3
  */
7
- class DeviceValidationService {
4
+ export class DeviceValidationService {
8
5
  /**
9
6
  * Validate Android device configuration
10
7
  * @param androidApiLevel Android API level to validate
@@ -91,4 +88,3 @@ class DeviceValidationService {
91
88
  }
92
89
  }
93
90
  }
94
- exports.DeviceValidationService = DeviceValidationService;
@@ -1,9 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.plan = plan;
4
- const fs = require("node:fs");
5
- const path = require("node:path");
6
- const execution_plan_utils_1 = require("./execution-plan.utils");
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { getFlowsToRunInSequence, isFlowFile, processDependencies, readDirectory, readTestYamlFileAsJson, readYamlFileAsJson, } from './execution-plan.utils.js';
7
4
  /**
8
5
  * Recursively check and resolve all dependencies for a flow file
9
6
  * Includes runFlow references, JavaScript scripts, and media files
@@ -16,8 +13,8 @@ async function checkDependencies(input) {
16
13
  const uncheckedDependencies = [input];
17
14
  while (uncheckedDependencies.length > 0) {
18
15
  const fileToCheck = uncheckedDependencies.shift();
19
- const { config, testSteps } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(fileToCheck);
20
- const { allErrors, allFiles } = (0, execution_plan_utils_1.processDependencies)({
16
+ const { config, testSteps } = readTestYamlFileAsJson(fileToCheck);
17
+ const { allErrors, allFiles } = processDependencies({
21
18
  config,
22
19
  input: fileToCheck,
23
20
  testSteps,
@@ -27,7 +24,7 @@ async function checkDependencies(input) {
27
24
  allErrors.join('\n'));
28
25
  }
29
26
  for (const file of allFiles) {
30
- if (!(0, execution_plan_utils_1.isFlowFile)(file)) {
27
+ if (!isFlowFile(file)) {
31
28
  // js/media files don't have dependencies
32
29
  checkedDependencies.push(file);
33
30
  }
@@ -63,7 +60,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
63
60
  const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
64
61
  const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
65
62
  const config = configFilePath
66
- ? (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath)
63
+ ? readYamlFileAsJson(configFilePath)
67
64
  : {};
68
65
  return config;
69
66
  }
@@ -99,7 +96,7 @@ async function planSingleFile(normalizedInput, configFile) {
99
96
  normalizedInput.endsWith('config.yml')) {
100
97
  throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
101
98
  }
102
- const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
99
+ const { config } = readTestYamlFileAsJson(normalizedInput);
103
100
  const flowMetadata = {};
104
101
  const flowOverrides = {};
105
102
  if (config) {
@@ -112,7 +109,7 @@ async function planSingleFile(normalizedInput, configFile) {
112
109
  if (!fs.existsSync(configFilePath)) {
113
110
  throw new Error(`Config file does not exist: ${configFilePath}`);
114
111
  }
115
- workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
112
+ workspaceConfig = readYamlFileAsJson(configFilePath);
116
113
  }
117
114
  const checkedDependancies = await checkDependencies(normalizedInput);
118
115
  return {
@@ -193,7 +190,7 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
193
190
  if (debug && flowOrder !== normalizedFlowOrder) {
194
191
  console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
195
192
  }
196
- return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
193
+ return getFlowsToRunInSequence(pathsByName, [normalizedFlowOrder], debug);
197
194
  })),
198
195
  ];
199
196
  if (debug) {
@@ -225,7 +222,7 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
225
222
  * @returns Complete execution plan with flows, dependencies, and metadata
226
223
  * @throws Error if input path doesn't exist, no flows found, or dependencies missing
227
224
  */
228
- async function plan(options) {
225
+ export async function plan(options) {
229
226
  const { input, includeTags = [], excludeTags = [], excludeFlows, configFile, debug = false, } = options;
230
227
  const normalizedInput = path.normalize(input);
231
228
  const flowMetadata = {};
@@ -235,7 +232,7 @@ async function plan(options) {
235
232
  if (fs.lstatSync(normalizedInput).isFile()) {
236
233
  return planSingleFile(normalizedInput, configFile);
237
234
  }
238
- let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
235
+ let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile);
239
236
  if (unfilteredFlowFiles.length === 0) {
240
237
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
241
238
  }
@@ -246,7 +243,7 @@ async function plan(options) {
246
243
  if (!fs.existsSync(configFilePath)) {
247
244
  throw new Error(`Config file does not exist: ${configFilePath}`);
248
245
  }
249
- workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
246
+ workspaceConfig = readYamlFileAsJson(configFilePath);
250
247
  }
251
248
  else {
252
249
  workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
@@ -260,7 +257,7 @@ async function plan(options) {
260
257
  }
261
258
  // eslint-disable-next-line unicorn/no-array-reduce
262
259
  const configPerFlowFile = unfilteredFlowFiles.reduce((acc, filePath) => {
263
- const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
260
+ const { config } = readTestYamlFileAsJson(filePath);
264
261
  acc[filePath] = config;
265
262
  return acc;
266
263
  }, {});