@devicecloud.dev/dcd 3.3.7 → 3.3.9

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.
@@ -57,6 +57,17 @@ export default class Cloud extends Command {
57
57
  quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
58
58
  retry: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
59
59
  report: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
60
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
60
61
  };
61
- run(): Promise<void>;
62
+ static enableJsonFlag: boolean;
63
+ private versionCheck;
64
+ run(): Promise<{
65
+ uploadId: string;
66
+ consoleUrl: string;
67
+ status: string;
68
+ tests: {
69
+ name: string;
70
+ status: string;
71
+ }[];
72
+ } | undefined>;
62
73
  }
@@ -63,15 +63,30 @@ class Cloud extends core_1.Command {
63
63
  static description = `Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev)\nProvide your application file and a folder with Maestro flows to run them in parallel on multiple devices in devicecloud.dev\nThe command will block until all analyses have completed`;
64
64
  static examples = ['<%= config.bin %> <%= command.id %>'];
65
65
  static flags = constants_1.flags;
66
+ static enableJsonFlag = true;
67
+ versionCheck = async () => {
68
+ const versionResponse = await fetch('https://registry.npmjs.org/@devicecloud.dev/dcd/latest');
69
+ const versionResponseJson = await versionResponse.json();
70
+ const latestVersion = versionResponseJson.version;
71
+ if (latestVersion !== this.config.version) {
72
+ this.log(`
73
+ -------------------
74
+ A new version of the devicecloud.dev CLI is available: ${latestVersion}
75
+ Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
76
+ -------------------
77
+ `);
78
+ }
79
+ };
66
80
  async run() {
81
+ let output = null;
67
82
  try {
68
83
  const [major] = process.versions.node.split('.').map(Number);
69
84
  if (major < 18) {
70
85
  throw new Error(`You are using node version ${major}. DCD requires node version 18 or later`);
71
86
  }
72
- await (0, methods_1.versionCheck)(this.config.version);
87
+ await this.versionCheck();
73
88
  const { args, flags, raw } = await this.parse(Cloud);
74
- const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, 'x86-arch': x86Arch, ...rest } = flags;
89
+ const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, 'x86-arch': x86Arch, json, ...rest } = flags;
75
90
  const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
76
91
  if (!apiKey)
77
92
  throw new Error('You must provide an API key via --api-key flag or DEVICE_CLOUD_API_KEY environment variable');
@@ -181,12 +196,12 @@ class Cloud extends core_1.Command {
181
196
  if (!finalBinaryId) {
182
197
  if (!finalAppFile)
183
198
  throw new Error('You must provide either an app binary id or an app file');
184
- const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck);
199
+ const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey, ignoreShaCheck, !json);
185
200
  finalBinaryId = binaryId;
186
201
  }
187
202
  let uploadedBinaryIds = [];
188
203
  if (additionalAppFiles?.length) {
189
- uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey, ignoreShaCheck);
204
+ uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey, ignoreShaCheck, !json);
190
205
  finalAdditionalBinaryIds = [
191
206
  ...finalAdditionalBinaryIds,
192
207
  ...uploadedBinaryIds,
@@ -269,18 +284,32 @@ class Cloud extends core_1.Command {
269
284
  .join(', ')}\n`);
270
285
  this.log('Run triggered, you can access the results at:');
271
286
  const url = `https://console.devicecloud.dev/results?upload=${results[0].test_upload_id}&result=${results[0].id}`;
272
- core_1.ux.url(url, url);
287
+ this.log(url);
273
288
  this.log(`\n`);
274
289
  this.log(`Your upload ID is: ${results[0].test_upload_id}`);
275
290
  this.log(`Poll upload status using: dcd status --api-key ... --upload-id ${results[0].test_upload_id}`);
276
- this.log(`\n`);
277
291
  if (async) {
292
+ if (json) {
293
+ return {
294
+ uploadId: results[0].test_upload_id,
295
+ consoleUrl: url,
296
+ status: 'PENDING',
297
+ tests: results.map((r) => ({
298
+ name: r.test_file_name,
299
+ status: r.status,
300
+ })),
301
+ };
302
+ }
278
303
  this.log('Not waiting for results as async flag is set to true');
279
304
  return;
280
305
  }
281
306
  // poll for the run status every 5 seconds
282
- core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
283
- this.log('\nYou can safely close this terminal and the tests will continue\n');
307
+ if (!json) {
308
+ core_1.ux.action.start('Waiting for results', 'Initializing', {
309
+ stdout: true,
310
+ });
311
+ this.log('\nYou can safely close this terminal and the tests will continue\n');
312
+ }
284
313
  let sequentialPollFaillures = 0;
285
314
  await new Promise((resolve, reject) => {
286
315
  const intervalId = setInterval(async () => {
@@ -291,7 +320,7 @@ class Cloud extends core_1.Command {
291
320
  if (!updatedResults) {
292
321
  throw new Error('no results');
293
322
  }
294
- if (!quiet) {
323
+ if (!quiet && !json) {
295
324
  core_1.ux.action.status =
296
325
  '\nStatus Test\n─────────── ───────────────';
297
326
  for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
@@ -299,18 +328,20 @@ class Cloud extends core_1.Command {
299
328
  }
300
329
  }
301
330
  if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
302
- core_1.ux.action.stop('completed');
303
- this.log('\n');
304
- (0, cli_ux_1.table)(updatedResults, {
305
- status: { get: (row) => row.status },
306
- test: {
307
- get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
308
- },
309
- }, { printLine: this.log.bind(this) });
310
- this.log('\n');
311
- this.log('Run completed, you can access the results at:');
312
- core_1.ux.url(url, url);
313
- this.log('\n');
331
+ if (!json) {
332
+ core_1.ux.action.stop('completed');
333
+ this.log('\n');
334
+ (0, cli_ux_1.table)(updatedResults, {
335
+ status: { get: (row) => row.status },
336
+ test: {
337
+ get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
338
+ },
339
+ }, { printLine: this.log.bind(this) });
340
+ this.log('\n');
341
+ this.log('Run completed, you can access the results at:');
342
+ this.log(url);
343
+ this.log('\n');
344
+ }
314
345
  clearInterval(intervalId);
315
346
  if (downloadArtifacts) {
316
347
  try {
@@ -334,8 +365,32 @@ class Cloud extends core_1.Command {
334
365
  return result.id === Math.max(...tries.map((t) => t.id));
335
366
  });
336
367
  if (resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')) {
368
+ if (json) {
369
+ output = {
370
+ uploadId: results[0].test_upload_id,
371
+ consoleUrl: url,
372
+ status: 'FAILED',
373
+ tests: resultsWithoutEarlierTries.map((r) => ({
374
+ name: r.test_file_name,
375
+ status: r.status,
376
+ })),
377
+ };
378
+ }
337
379
  reject(new Error('RUN_FAILED'));
338
380
  }
381
+ else {
382
+ if (json) {
383
+ output = {
384
+ uploadId: results[0].test_upload_id,
385
+ consoleUrl: url,
386
+ status: 'PASSED',
387
+ tests: resultsWithoutEarlierTries.map((r) => ({
388
+ name: r.test_file_name,
389
+ status: r.status,
390
+ })),
391
+ };
392
+ }
393
+ }
339
394
  sequentialPollFaillures = 0;
340
395
  resolve();
341
396
  }
@@ -358,6 +413,11 @@ class Cloud extends core_1.Command {
358
413
  }
359
414
  this.error(error, { exit: 1 });
360
415
  }
416
+ finally {
417
+ if (output) {
418
+ return output;
419
+ }
420
+ }
361
421
  }
362
422
  }
363
423
  exports.default = Cloud;
@@ -1,7 +1,18 @@
1
1
  import { Command } from '@oclif/core';
2
+ type StatusResponse = {
3
+ status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
4
+ tests: {
5
+ name: string;
6
+ status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
7
+ }[];
8
+ uploadId?: string;
9
+ appBinaryId?: string;
10
+ consoleUrl?: string;
11
+ };
2
12
  export default class Status extends Command {
3
13
  static description: string;
4
14
  static examples: string[];
15
+ static enableJsonFlag: boolean;
5
16
  static flags: {
6
17
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
18
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -10,5 +21,6 @@ export default class Status extends Command {
10
21
  apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
22
  };
12
23
  private getStatusSymbol;
13
- run(): Promise<void>;
24
+ run(): Promise<StatusResponse | void>;
14
25
  }
26
+ export {};
@@ -9,6 +9,7 @@ class Status extends core_1.Command {
9
9
  '<%= config.bin %> <%= command.id %> --name my-upload-name',
10
10
  '<%= config.bin %> <%= command.id %> --upload-id 123e4567-e89b-12d3-a456-426614174000 --json',
11
11
  ];
12
+ static enableJsonFlag = true;
12
13
  static flags = {
13
14
  json: core_1.Flags.boolean({
14
15
  description: 'output in json format',
@@ -40,7 +41,7 @@ class Status extends core_1.Command {
40
41
  }
41
42
  async run() {
42
43
  const { flags } = await this.parse(Status);
43
- const { apiUrl, apiKey, name, 'upload-id': uploadId } = flags;
44
+ const { apiUrl, apiKey, name, 'upload-id': uploadId, json } = flags;
44
45
  if (!apiKey) {
45
46
  this.error('API Key is required. Please provide it via --api-key flag or DEVICE_CLOUD_API_KEY environment variable.');
46
47
  return;
@@ -54,8 +55,8 @@ class Status extends core_1.Command {
54
55
  name,
55
56
  uploadId,
56
57
  }));
57
- if (flags.json) {
58
- this.log(JSON.stringify(status, null, 2));
58
+ if (json) {
59
+ return status;
59
60
  }
60
61
  else {
61
62
  this.log('\n📊 Upload Status');
@@ -27,6 +27,7 @@ export declare const flags: {
27
27
  quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
28
28
  retry: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
29
29
  report: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
30
31
  };
31
32
  export declare const iOSCompatibilityLookup: {
32
33
  [k in EiOSDevices]: string[];
package/dist/constants.js CHANGED
@@ -166,6 +166,9 @@ exports.flags = {
166
166
  description: 'Runs Maestro with the --format flag, this will generate a report in the specified format',
167
167
  options: ['junit', 'html'],
168
168
  }),
169
+ json: core_1.Flags.boolean({
170
+ description: 'Output results in JSON format - note: will always provide exit code 0',
171
+ }),
169
172
  };
170
173
  exports.iOSCompatibilityLookup = {
171
174
  'ipad-pro-6th-gen': ['16', '17', '18'],
package/dist/methods.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as archiver from 'archiver';
2
2
  import { paths } from '../../api/schema.types';
3
3
  import { TAppMetadata } from './types';
4
- export declare const versionCheck: (currentVersion: string) => Promise<void>;
5
4
  export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
6
5
  body?: BodyInit;
7
6
  headers?: HeadersInit;
@@ -22,8 +21,8 @@ export declare const verifyAppZip: (zipPath: string) => Promise<void>;
22
21
  export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
23
22
  export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
24
23
  export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
25
- export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string>;
26
- export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean) => Promise<string[]>;
24
+ export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
25
+ export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string[]>;
27
26
  export declare const verifyAdditionalAppFiles: (appFiles: string[] | undefined) => Promise<void>;
28
27
  export declare const getUploadStatus: (apiUrl: string, apiKey: string, options: {
29
28
  uploadId?: string;
package/dist/methods.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getUploadStatus = exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePostDownload = exports.typeSafePost = exports.versionCheck = void 0;
3
+ exports.getUploadStatus = exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePostDownload = exports.typeSafePost = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const supabase_js_1 = require("@supabase/supabase-js");
6
6
  // required polyfill for node 18
@@ -29,20 +29,6 @@ const PERMITTED_EXTENSIONS = new Set([
29
29
  'mp4',
30
30
  'js',
31
31
  ]);
32
- const versionCheck = async (currentVersion) => {
33
- const versionResponse = await fetch('https://registry.npmjs.org/@devicecloud.dev/dcd/latest');
34
- const versionResponseJson = await versionResponse.json();
35
- const latestVersion = versionResponseJson.version;
36
- if (latestVersion !== currentVersion) {
37
- console.log(`
38
- -------------------
39
- A new version of the devicecloud.dev CLI is available: ${latestVersion}
40
- Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
41
- -------------------
42
- `);
43
- }
44
- };
45
- exports.versionCheck = versionCheck;
46
32
  const typeSafePost = async (baseUrl, path, init) => {
47
33
  const res = await fetch(baseUrl + path, {
48
34
  ...init,
@@ -193,10 +179,7 @@ const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, re
193
179
  })
194
180
  .catch(reject);
195
181
  });
196
- zip.on('error', (err) => {
197
- console.error(err);
198
- reject(err);
199
- });
182
+ zip.on('error', reject);
200
183
  });
201
184
  exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
202
185
  const extractAppMetadataIos = async (appFolderPath) => {
@@ -207,10 +190,12 @@ const extractAppMetadataIos = async (appFolderPath) => {
207
190
  return { appId, platform: 'ios' };
208
191
  };
209
192
  exports.extractAppMetadataIos = extractAppMetadataIos;
210
- const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) => {
211
- core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
212
- stdout: true,
213
- });
193
+ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
194
+ if (log) {
195
+ core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
196
+ stdout: true,
197
+ });
198
+ }
214
199
  let file;
215
200
  if (filePath?.endsWith('.app')) {
216
201
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
@@ -227,7 +212,9 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
227
212
  sha = await getFileHashFromFile(file);
228
213
  }
229
214
  catch (e) {
230
- console.warn('Warning: Failed to get file hash', e);
215
+ if (log) {
216
+ console.warn('Warning: Failed to get file hash', e);
217
+ }
231
218
  }
232
219
  if (!ignoreShaCheck && sha) {
233
220
  try {
@@ -239,8 +226,10 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
239
226
  },
240
227
  });
241
228
  if (exists) {
242
- core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
243
- core_1.ux.action.stop(`Skipping upload.`);
229
+ if (log) {
230
+ core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
231
+ core_1.ux.action.stop(`Skipping upload.`);
232
+ }
244
233
  return appBinaryId;
245
234
  }
246
235
  }
@@ -268,7 +257,9 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
268
257
  : await (0, exports.extractAppMetadataIos)(filePath);
269
258
  }
270
259
  catch {
271
- core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
260
+ if (log) {
261
+ core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
262
+ }
272
263
  }
273
264
  // this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
274
265
  const SB = {
@@ -297,11 +288,13 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false) =>
297
288
  });
298
289
  if (error)
299
290
  throw new Error(error);
300
- core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
291
+ if (log) {
292
+ core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
293
+ }
301
294
  return id;
302
295
  };
303
296
  exports.uploadBinary = uploadBinary;
304
- const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck)));
297
+ const uploadBinaries = async (finalAppFiles, apiUrl, apiKey, ignoreShaCheck = false, log = true) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey, ignoreShaCheck, log)));
305
298
  exports.uploadBinaries = uploadBinaries;
306
299
  const verifyAdditionalAppFiles = async (appFiles) => {
307
300
  if (appFiles?.length) {
@@ -19,6 +19,12 @@
19
19
  "<%= config.bin %> <%= command.id %>"
20
20
  ],
21
21
  "flags": {
22
+ "json": {
23
+ "description": "Output results in JSON format - note: will always provide exit code 0",
24
+ "name": "json",
25
+ "allowNo": false,
26
+ "type": "boolean"
27
+ },
22
28
  "additional-app-binary-ids": {
23
29
  "description": "The ID of the additional app binary(s) previously uploaded to devicecloud.dev to install before execution",
24
30
  "name": "additional-app-binary-ids",
@@ -315,7 +321,7 @@
315
321
  "pluginName": "@devicecloud.dev/dcd",
316
322
  "pluginType": "core",
317
323
  "strict": true,
318
- "enableJsonFlag": false,
324
+ "enableJsonFlag": true,
319
325
  "isESM": false,
320
326
  "relativePath": [
321
327
  "dist",
@@ -389,7 +395,7 @@
389
395
  "pluginName": "@devicecloud.dev/dcd",
390
396
  "pluginType": "core",
391
397
  "strict": true,
392
- "enableJsonFlag": false,
398
+ "enableJsonFlag": true,
393
399
  "isESM": false,
394
400
  "relativePath": [
395
401
  "dist",
@@ -398,5 +404,5 @@
398
404
  ]
399
405
  }
400
406
  },
401
- "version": "3.3.7"
407
+ "version": "3.3.9"
402
408
  }
package/package.json CHANGED
@@ -80,7 +80,7 @@
80
80
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
81
  "version": "oclif readme && git add README.md"
82
82
  },
83
- "version": "3.3.7",
83
+ "version": "3.3.9",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },