@capawesome/cli 3.1.0 → 3.2.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,18 @@
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
+ ## [3.2.0](https://github.com/capawesome-team/cli/compare/v3.1.0...v3.2.0) (2025-09-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * **apps:channels:create:** add `--expires-in-days` option ([#69](https://github.com/capawesome-team/cli/issues/69)) ([ef2dd36](https://github.com/capawesome-team/cli/commit/ef2dd3638653e35a3ee3390ad336f39d368cd7ca))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **apps:bundles:create:** add error handling for invalid path types ([1fae8c6](https://github.com/capawesome-team/cli/commit/1fae8c6a831bdc0f5bb1e80f2dea4fa3a981e625))
16
+
5
17
  ## [3.1.0](https://github.com/capawesome-team/cli/compare/v3.0.0...v3.1.0) (2025-09-17)
6
18
 
7
19
 
@@ -5,10 +5,10 @@ import appsService from '../../../services/apps.js';
5
5
  import authorizationService from '../../../services/authorization-service.js';
6
6
  import organizationsService from '../../../services/organizations.js';
7
7
  import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
8
- import { formatPrivateKey } from '../../../utils/private-key.js';
9
8
  import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
10
9
  import { createHash } from '../../../utils/hash.js';
11
10
  import { generateManifestJson } from '../../../utils/manifest.js';
11
+ import { formatPrivateKey } from '../../../utils/private-key.js';
12
12
  import { prompt } from '../../../utils/prompt.js';
13
13
  import { createSignature } from '../../../utils/signature.js';
14
14
  import zip from '../../../utils/zip.js';
@@ -94,7 +94,7 @@ export default defineCommand({
94
94
  consola.error('You must be logged in to run this command.');
95
95
  process.exit(1);
96
96
  }
97
- // Validate the expiration days
97
+ // Calculate the expiration date
98
98
  let expiresAt;
99
99
  if (expiresInDays) {
100
100
  const expiresAtDate = new Date();
@@ -128,6 +128,13 @@ export default defineCommand({
128
128
  process.exit(1);
129
129
  }
130
130
  }
131
+ else if (zip.isZipped(path)) {
132
+ // No-op
133
+ }
134
+ else {
135
+ consola.error('The path must be either a folder or a zip file.');
136
+ process.exit(1);
137
+ }
131
138
  }
132
139
  // Check that the path is a directory when creating a bundle with an artifact type
133
140
  if (artifactType === 'manifest' && path) {
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
2
  import authorizationService from '../../../services/authorization-service.js';
3
- import { fileExistsAtPath, isDirectory } from '../../../utils/file.js';
3
+ import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
4
4
  import userConfig from '../../../utils/user-config.js';
5
5
  import consola from 'consola';
6
6
  import nock from 'nock';
@@ -20,6 +20,7 @@ describe('apps-bundles-create', () => {
20
20
  const mockUserConfig = vi.mocked(userConfig);
21
21
  const mockAuthorizationService = vi.mocked(authorizationService);
22
22
  const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
23
+ const mockGetFilesInDirectoryAndSubdirectories = vi.mocked(getFilesInDirectoryAndSubdirectories);
23
24
  const mockIsDirectory = vi.mocked(isDirectory);
24
25
  const mockConsola = vi.mocked(consola);
25
26
  beforeEach(() => {
@@ -100,6 +101,9 @@ describe('apps-bundles-create', () => {
100
101
  };
101
102
  mockFileExistsAtPath.mockResolvedValue(true);
102
103
  mockIsDirectory.mockResolvedValue(false);
104
+ // Mock zip utility to return true so path validation passes
105
+ const mockZip = await import('../../../utils/zip.js');
106
+ vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
103
107
  await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
104
108
  expect(mockConsola.error).toHaveBeenCalledWith('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
105
109
  });
@@ -249,6 +253,10 @@ describe('apps-bundles-create', () => {
249
253
  return Promise.resolve(false);
250
254
  return Promise.resolve(true);
251
255
  });
256
+ mockIsDirectory.mockResolvedValue(true);
257
+ mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
258
+ { href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
259
+ ]);
252
260
  // Mock utility functions
253
261
  const mockBuffer = await import('../../../utils/buffer.js');
254
262
  vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
@@ -267,6 +275,9 @@ describe('apps-bundles-create', () => {
267
275
  };
268
276
  mockFileExistsAtPath.mockResolvedValue(true);
269
277
  mockIsDirectory.mockResolvedValue(false);
278
+ // Mock zip utility to pass path validation
279
+ const mockZip = await import('../../../utils/zip.js');
280
+ vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
270
281
  // Mock utility functions
271
282
  const mockBuffer = await import('../../../utils/buffer.js');
272
283
  vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
@@ -15,15 +15,31 @@ export default defineCommand({
15
15
  .number()
16
16
  .optional()
17
17
  .describe('Maximum number of bundles that can be assigned to the channel. If more bundles are assigned, the oldest bundles will be automatically deleted.'),
18
+ expiresInDays: z.coerce
19
+ .number({
20
+ message: 'Expiration days must be an integer.',
21
+ })
22
+ .int({
23
+ message: 'Expiration days must be an integer.',
24
+ })
25
+ .optional()
26
+ .describe('The number of days until the channel is automatically deleted.'),
18
27
  ignoreErrors: z.boolean().optional().describe('Whether to ignore errors or not.'),
19
28
  name: z.string().optional().describe('Name of the channel.'),
20
29
  })),
21
30
  action: async (options, args) => {
22
- let { appId, bundleLimit, ignoreErrors, name } = options;
31
+ let { appId, bundleLimit, expiresInDays, ignoreErrors, name } = options;
23
32
  if (!authorizationService.hasAuthorizationToken()) {
24
33
  consola.error('You must be logged in to run this command.');
25
34
  process.exit(1);
26
35
  }
36
+ // Calculate the expiration date
37
+ let expiresAt;
38
+ if (expiresInDays) {
39
+ const expiresAtDate = new Date();
40
+ expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
41
+ expiresAt = expiresAtDate.toISOString();
42
+ }
27
43
  // Validate the app ID
28
44
  if (!appId) {
29
45
  const organizations = await organizationsService.findAll();
@@ -62,6 +78,7 @@ export default defineCommand({
62
78
  appId,
63
79
  name,
64
80
  totalAppBundleLimit: bundleLimit,
81
+ expiresAt,
65
82
  });
66
83
  consola.success('Channel created successfully.');
67
84
  consola.info(`Channel ID: ${response.id}`);
@@ -116,4 +116,32 @@ describe('apps-channels-create', () => {
116
116
  await expect(createChannelCommand.action(options, undefined)).rejects.toThrow();
117
117
  expect(scope.isDone()).toBe(true);
118
118
  });
119
+ it('should create channel with expiresInDays option', async () => {
120
+ const appId = 'app-123';
121
+ const channelName = 'production';
122
+ const expiresInDays = 30;
123
+ const channelId = 'channel-456';
124
+ const testToken = 'test-token';
125
+ const options = { appId, name: channelName, expiresInDays };
126
+ // Calculate expected expiration date
127
+ const expectedExpiresAt = new Date();
128
+ expectedExpiresAt.setDate(expectedExpiresAt.getDate() + expiresInDays);
129
+ const scope = nock(DEFAULT_API_BASE_URL)
130
+ .post(`/v1/apps/${appId}/channels`, (body) => {
131
+ // Verify the request includes expiresAt and it's approximately correct (within 1 minute)
132
+ const actualExpiresAt = new Date(body.expiresAt);
133
+ const timeDiff = Math.abs(actualExpiresAt.getTime() - expectedExpiresAt.getTime());
134
+ const oneMinute = 60 * 1000;
135
+ return (body.appId === appId &&
136
+ body.name === channelName &&
137
+ body.totalAppBundleLimit === undefined &&
138
+ timeDiff < oneMinute);
139
+ })
140
+ .matchHeader('Authorization', `Bearer ${testToken}`)
141
+ .reply(201, { id: channelId, name: channelName });
142
+ await createChannelCommand.action(options, undefined);
143
+ expect(scope.isDone()).toBe(true);
144
+ expect(mockConsola.success).toHaveBeenCalledWith('Channel created successfully.');
145
+ expect(mockConsola.info).toHaveBeenCalledWith(`Channel ID: ${channelId}`);
146
+ });
119
147
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {