@capawesome/cli 4.2.1 → 4.3.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ ## [4.3.0](https://github.com/capawesome-team/cli/compare/v4.2.1...v4.3.0) (2026-03-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * add part-level progress for multipart zip uploads ([#122](https://github.com/capawesome-team/cli/issues/122)) ([f74cda4](https://github.com/capawesome-team/cli/commit/f74cda48f454dfbc786052498ebecccef98e6fc5))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **deps:** remove `glob` dependency ([#120](https://github.com/capawesome-team/cli/issues/120)) ([80709de](https://github.com/capawesome-team/cli/commit/80709deac3292b976f3633f3335004d2cb80fc22))
16
+ * read deployment ID from update response ([#121](https://github.com/capawesome-team/cli/issues/121)) ([9ef9979](https://github.com/capawesome-team/cli/commit/9ef99794a5bb14631fcecf41c1a9885931627dfd))
17
+ * use ISO string timestamps ([#119](https://github.com/capawesome-team/cli/issues/119)) ([7fe8263](https://github.com/capawesome-team/cli/commit/7fe8263956fc83ca7a93d803e6867133fea8c879))
18
+ * validate prompt response in bundle command ([#118](https://github.com/capawesome-team/cli/issues/118)) ([a1541a0](https://github.com/capawesome-team/cli/commit/a1541a074f614b42bf69f2c6a186eeef5d95200b))
19
+
5
20
  ## [4.2.1](https://github.com/capawesome-team/cli/compare/v4.2.0...v4.2.1) (2026-03-01)
6
21
 
7
22
  ## [4.2.0](https://github.com/capawesome-team/cli/compare/v4.1.0...v4.2.0) (2026-02-20)
@@ -33,10 +33,13 @@ export default defineCommand({
33
33
  process.exit(1);
34
34
  }
35
35
  consola.warn('Make sure you have built your web assets before creating a bundle (e.g., `npm run build`).');
36
- const response = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
36
+ inputPath = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
37
37
  type: 'text',
38
38
  });
39
- inputPath = response;
39
+ if (!inputPath) {
40
+ consola.error('You must provide an input path.');
41
+ process.exit(1);
42
+ }
40
43
  }
41
44
  // Convert to absolute path
42
45
  inputPath = pathModule.resolve(inputPath);
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CONSOLE_BASE_URL, MAX_CONCURRENT_UPLOADS } from '../../../config/index.js';
1
+ import { DEFAULT_CONSOLE_BASE_URL, MAX_CONCURRENT_FILE_UPLOADS } from '../../../config/index.js';
2
2
  import appBundleFilesService from '../../../services/app-bundle-files.js';
3
3
  import appBundlesService from '../../../services/app-bundles.js';
4
4
  import appsService from '../../../services/apps.js';
@@ -236,7 +236,7 @@ export default defineCommand({
236
236
  }
237
237
  // Create the app bundle
238
238
  consola.start('Creating bundle...');
239
- const response = await appBundlesService.create({
239
+ const createBundleResponse = await appBundlesService.create({
240
240
  appId,
241
241
  artifactType,
242
242
  channelName: channel,
@@ -258,29 +258,29 @@ export default defineCommand({
258
258
  let appBundleFileId;
259
259
  // Upload the app bundle files
260
260
  if (artifactType === 'manifest') {
261
- await uploadFiles({ appId, appBundleId: response.id, path, privateKeyBuffer });
261
+ await uploadFiles({ appId, appBundleId: createBundleResponse.id, path, privateKeyBuffer });
262
262
  }
263
263
  else {
264
- const result = await uploadZip({ appId, appBundleId: response.id, path, privateKeyBuffer });
264
+ const result = await uploadZip({ appId, appBundleId: createBundleResponse.id, path, privateKeyBuffer });
265
265
  appBundleFileId = result.appBundleFileId;
266
266
  }
267
267
  // Update the app bundle
268
268
  consola.start('Updating bundle...');
269
- await appBundlesService.update({
269
+ const updateBundleResponse = await appBundlesService.update({
270
270
  appBundleFileId,
271
271
  appId,
272
272
  artifactStatus: 'ready',
273
- appBundleId: response.id,
273
+ appBundleId: createBundleResponse.id,
274
274
  });
275
- consola.info(`Build Artifact ID: ${response.id}`);
276
- if (response.appDeploymentId) {
277
- consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.appDeploymentId}`);
275
+ consola.info(`Build Artifact ID: ${createBundleResponse.id}`);
276
+ if (updateBundleResponse.appDeploymentId) {
277
+ consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${updateBundleResponse.appDeploymentId}`);
278
278
  }
279
279
  consola.success('Live Update successfully uploaded.');
280
280
  }),
281
281
  });
282
282
  const uploadFile = async (options) => {
283
- let { appId, appBundleId, buffer, href, mimeType, name, privateKeyBuffer, retryOnFailure } = options;
283
+ let { appId, appBundleId, buffer, href, mimeType, name, onProgress, privateKeyBuffer, retryOnFailure } = options;
284
284
  try {
285
285
  // Generate checksum
286
286
  const hash = await createHash(buffer);
@@ -299,7 +299,7 @@ const uploadFile = async (options) => {
299
299
  mimeType,
300
300
  name,
301
301
  signature,
302
- });
302
+ }, onProgress);
303
303
  }
304
304
  catch (error) {
305
305
  if (retryOnFailure) {
@@ -339,8 +339,8 @@ const uploadFiles = async (options) => {
339
339
  });
340
340
  await uploadNextFile();
341
341
  };
342
- const uploadPromises = Array.from({ length: MAX_CONCURRENT_UPLOADS });
343
- for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
342
+ const uploadPromises = Array.from({ length: MAX_CONCURRENT_FILE_UPLOADS });
343
+ for (let i = 0; i < MAX_CONCURRENT_FILE_UPLOADS; i++) {
344
344
  uploadPromises[i] = uploadNextFile();
345
345
  }
346
346
  await Promise.all(uploadPromises);
@@ -365,6 +365,9 @@ const uploadZip = async (options) => {
365
365
  buffer: fileBuffer,
366
366
  mimeType: 'application/zip',
367
367
  name: 'bundle.zip',
368
+ onProgress: (completed, total) => {
369
+ consola.start(`Uploading file (part ${completed}/${total})...`);
370
+ },
368
371
  privateKeyBuffer: privateKeyBuffer,
369
372
  });
370
373
  return {
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_API_BASE_URL = 'https://api.cloud.capawesome.io';
2
2
  export const DEFAULT_CONSOLE_BASE_URL = 'https://console.cloud.capawesome.io';
3
3
  export const MANIFEST_JSON_FILE_NAME = 'capawesome-live-update-manifest.json'; // Do NOT change this!
4
- export const MAX_CONCURRENT_UPLOADS = 20;
4
+ export const MAX_CONCURRENT_FILE_UPLOADS = 20;
5
+ export const MAX_CONCURRENT_PART_UPLOADS = 4;
@@ -1,4 +1,4 @@
1
- import { MAX_CONCURRENT_UPLOADS } from '../config/index.js';
1
+ import { MAX_CONCURRENT_PART_UPLOADS } from '../config/index.js';
2
2
  import authorizationService from '../services/authorization-service.js';
3
3
  import httpClient from '../utils/http-client.js';
4
4
  import FormData from 'form-data';
@@ -7,7 +7,7 @@ class AppBundleFilesServiceImpl {
7
7
  constructor(httpClient) {
8
8
  this.httpClient = httpClient;
9
9
  }
10
- async create(dto) {
10
+ async create(dto, onProgress) {
11
11
  const sizeInBytes = dto.buffer.byteLength;
12
12
  const useMultipartUpload = sizeInBytes >= 50 * 1024 * 1024; // 50 MB
13
13
  const formData = new FormData();
@@ -38,7 +38,7 @@ class AppBundleFilesServiceImpl {
38
38
  buffer: dto.buffer,
39
39
  name: dto.name,
40
40
  checksum: dto.checksum,
41
- });
41
+ }, onProgress);
42
42
  }
43
43
  return response.data;
44
44
  }
@@ -74,7 +74,7 @@ class AppBundleFilesServiceImpl {
74
74
  })
75
75
  .then((response) => response.data);
76
76
  }
77
- async createUploadParts(dto) {
77
+ async createUploadParts(dto, onProgress) {
78
78
  const uploadedParts = [];
79
79
  const partSize = 10 * 1024 * 1024; // 10 MB. 5 MB is the minimum part size except for the last part.
80
80
  const totalParts = Math.ceil(dto.buffer.byteLength / partSize);
@@ -84,6 +84,7 @@ class AppBundleFilesServiceImpl {
84
84
  return;
85
85
  }
86
86
  partNumber++;
87
+ onProgress?.(partNumber, totalParts);
87
88
  const start = (partNumber - 1) * partSize;
88
89
  const end = Math.min(start + partSize, dto.buffer.byteLength);
89
90
  const partBuffer = dto.buffer.subarray(start, end);
@@ -99,14 +100,14 @@ class AppBundleFilesServiceImpl {
99
100
  uploadedParts.push(uploadedPart);
100
101
  await uploadNextPart();
101
102
  };
102
- const uploadPartPromises = Array.from({ length: MAX_CONCURRENT_UPLOADS });
103
- for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
103
+ const uploadPartPromises = Array.from({ length: MAX_CONCURRENT_PART_UPLOADS });
104
+ for (let i = 0; i < MAX_CONCURRENT_PART_UPLOADS; i++) {
104
105
  uploadPartPromises[i] = uploadNextPart();
105
106
  }
106
107
  await Promise.all(uploadPartPromises);
107
- return uploadedParts;
108
+ return uploadedParts.sort((a, b) => a.partNumber - b.partNumber);
108
109
  }
109
- async upload(dto) {
110
+ async upload(dto, onProgress) {
110
111
  // 1. Create a multipart upload
111
112
  const { uploadId } = await this.createUpload({
112
113
  appBundleFileId: dto.appBundleFileId,
@@ -121,7 +122,7 @@ class AppBundleFilesServiceImpl {
121
122
  buffer: dto.buffer,
122
123
  name: dto.name,
123
124
  uploadId,
124
- });
125
+ }, onProgress);
125
126
  // 3. Complete the upload
126
127
  await this.completeUpload({
127
128
  appBundleFileId: dto.appBundleFileId,
@@ -1,6 +1,6 @@
1
1
  export function formatTimeAgo(timestamp) {
2
2
  const now = Date.now();
3
- const diffInMs = now - timestamp;
3
+ const diffInMs = now - new Date(timestamp).getTime();
4
4
  const diffInSeconds = Math.floor(diffInMs / 1000);
5
5
  if (diffInSeconds < 60) {
6
6
  return 'just now';
package/dist/utils/zip.js CHANGED
@@ -1,15 +1,9 @@
1
- import archiver from 'archiver';
1
+ import AdmZip from 'adm-zip';
2
2
  class ZipImpl {
3
3
  async zipFolder(sourceFolder) {
4
- return new Promise((resolve, reject) => {
5
- const archive = archiver('zip', { zlib: { level: 9 } });
6
- const buffers = [];
7
- archive.on('data', (data) => buffers.push(data));
8
- archive.on('error', (err) => reject(err));
9
- archive.on('end', () => resolve(Buffer.concat(buffers)));
10
- archive.directory(sourceFolder, false);
11
- archive.finalize();
12
- });
4
+ const zip = new AdmZip();
5
+ zip.addLocalFolder(sourceFolder);
6
+ return zip.toBuffer();
13
7
  }
14
8
  isZipped(path) {
15
9
  return path.endsWith('.zip');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -51,15 +51,11 @@
51
51
  "url": "https://opencollective.com/capawesome"
52
52
  }
53
53
  ],
54
- "overrides": {
55
- "glob": "13.0.6",
56
- "minimatch": "10.2.4"
57
- },
58
54
  "dependencies": {
59
55
  "@clack/prompts": "0.7.0",
60
56
  "@robingenz/zli": "0.2.0",
61
57
  "@sentry/node": "8.55.0",
62
- "archiver": "7.0.1",
58
+ "adm-zip": "0.5.16",
63
59
  "axios": "1.13.5",
64
60
  "axios-retry": "4.5.0",
65
61
  "c12": "3.3.3",
@@ -77,7 +73,7 @@
77
73
  "devDependencies": {
78
74
  "@ionic/prettier-config": "4.0.0",
79
75
  "@sentry/cli": "2.52.0",
80
- "@types/archiver": "6.0.3",
76
+ "@types/adm-zip": "0.5.7",
81
77
  "@types/mime": "3.0.4",
82
78
  "@types/node": "24.2.1",
83
79
  "@types/semver": "7.5.8",