@capawesome/cli 3.10.2 → 3.11.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,13 @@
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.11.0](https://github.com/capawesome-team/cli/compare/v3.10.2...v3.11.0) (2026-01-08)
6
+
7
+
8
+ ### Features
9
+
10
+ * **apps:** add commands for managing environments ([#109](https://github.com/capawesome-team/cli/issues/109)) ([2d548f3](https://github.com/capawesome-team/cli/commit/2d548f380e536e94a38b6908dadb31566eac449e))
11
+
5
12
  ## [3.10.2](https://github.com/capawesome-team/cli/compare/v3.10.1...v3.10.2) (2025-12-30)
6
13
 
7
14
  ## [3.10.1](https://github.com/capawesome-team/cli/compare/v3.10.0...v3.10.1) (2025-12-29)
@@ -64,9 +64,17 @@ export default defineCommand({
64
64
  consola.error('You must provide either the channel ID or name when running in non-interactive environment.');
65
65
  process.exit(1);
66
66
  }
67
- name = await prompt('Enter the channel name:', {
68
- type: 'text',
67
+ const channels = await appChannelsService.findAll({ appId });
68
+ if (!channels.length) {
69
+ consola.error('No channels found for this app. Create one first.');
70
+ process.exit(1);
71
+ }
72
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
73
+ const selectedChannelId = await prompt('Select the channel to delete:', {
74
+ type: 'select',
75
+ options: channels.map((channel) => ({ label: channel.name, value: channel.id })),
69
76
  });
77
+ channelId = selectedChannelId;
70
78
  }
71
79
  // Confirm deletion
72
80
  if (isInteractive()) {
@@ -107,22 +107,37 @@ describe('apps-channels-delete', () => {
107
107
  expect(deleteScope.isDone()).toBe(true);
108
108
  expect(mockConsola.success).toHaveBeenCalledWith('Channel deleted successfully.');
109
109
  });
110
- it('should prompt for channel name when neither channelId nor name provided', async () => {
110
+ it('should prompt for channel selection when channelId and name not provided', async () => {
111
111
  const appId = 'app-123';
112
+ const channelId = 'channel-456';
112
113
  const channelName = 'development';
113
114
  const testToken = 'test-token';
114
115
  const options = { appId };
115
- const scope = nock(DEFAULT_API_BASE_URL)
116
- .delete(`/v1/apps/${appId}/channels`)
117
- .query({ name: channelName })
116
+ const channelsListScope = nock(DEFAULT_API_BASE_URL)
117
+ .get(`/v1/apps/${appId}/channels`)
118
+ .query(true)
119
+ .matchHeader('Authorization', `Bearer ${testToken}`)
120
+ .reply(200, [
121
+ {
122
+ id: channelId,
123
+ name: channelName,
124
+ appId,
125
+ },
126
+ ]);
127
+ const deleteScope = nock(DEFAULT_API_BASE_URL)
128
+ .delete(`/v1/apps/${appId}/channels/${channelId}`)
118
129
  .matchHeader('Authorization', `Bearer ${testToken}`)
119
130
  .reply(200);
120
131
  mockPrompt
121
- .mockResolvedValueOnce(channelName) // channel name input
132
+ .mockResolvedValueOnce(channelId) // channel selection
122
133
  .mockResolvedValueOnce(true); // confirmation
123
134
  await deleteChannelCommand.action(options, undefined);
124
- expect(scope.isDone()).toBe(true);
125
- expect(mockPrompt).toHaveBeenCalledWith('Enter the channel name:', { type: 'text' });
135
+ expect(channelsListScope.isDone()).toBe(true);
136
+ expect(deleteScope.isDone()).toBe(true);
137
+ expect(mockPrompt).toHaveBeenCalledWith('Select the channel to delete:', {
138
+ type: 'select',
139
+ options: [{ label: channelName, value: channelId }],
140
+ });
126
141
  expect(mockConsola.success).toHaveBeenCalledWith('Channel deleted successfully.');
127
142
  });
128
143
  it('should handle API error during deletion', async () => {
@@ -1,5 +1,9 @@
1
1
  import appChannelsService from '../../../services/app-channels.js';
2
+ import appsService from '../../../services/apps.js';
2
3
  import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { isInteractive } from '../../../utils/environment.js';
6
+ import { prompt } from '../../../utils/prompt.js';
3
7
  import { defineCommand, defineOptions } from '@robingenz/zli';
4
8
  import consola from 'consola';
5
9
  import { z } from 'zod';
@@ -18,8 +22,36 @@ export default defineCommand({
18
22
  process.exit(1);
19
23
  }
20
24
  if (!appId) {
21
- consola.error('You must provide an app ID.');
22
- process.exit(1);
25
+ if (!isInteractive()) {
26
+ consola.error('You must provide an app ID when running in non-interactive environment.');
27
+ process.exit(1);
28
+ }
29
+ const organizations = await organizationsService.findAll();
30
+ if (organizations.length === 0) {
31
+ consola.error('You must create an organization before listing channels.');
32
+ process.exit(1);
33
+ }
34
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
35
+ const organizationId = await prompt('Select the organization of the app for which you want to list channels.', {
36
+ type: 'select',
37
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
38
+ });
39
+ if (!organizationId) {
40
+ consola.error('You must select the organization of an app for which you want to list channels.');
41
+ process.exit(1);
42
+ }
43
+ const apps = await appsService.findAll({
44
+ organizationId,
45
+ });
46
+ if (!apps.length) {
47
+ consola.error('You must create an app before listing channels.');
48
+ process.exit(1);
49
+ }
50
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
51
+ appId = await prompt('Which app do you want to list the channels for?', {
52
+ type: 'select',
53
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
54
+ });
23
55
  }
24
56
  const foundChannels = await appChannelsService.findAll({
25
57
  appId,
@@ -38,7 +38,7 @@ describe('apps-channels-list', () => {
38
38
  it('should require appId', async () => {
39
39
  const options = { appId: undefined };
40
40
  await expect(listChannelsCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
41
- expect(mockConsola.error).toHaveBeenCalledWith('You must provide an app ID.');
41
+ expect(mockConsola.error).toHaveBeenCalledWith('You must provide an app ID when running in non-interactive environment.');
42
42
  });
43
43
  it('should list channels and display table format', async () => {
44
44
  const appId = 'app-123';
@@ -0,0 +1,68 @@
1
+ import appEnvironmentsService from '../../../services/app-environments.js';
2
+ import appsService from '../../../services/apps.js';
3
+ import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { isInteractive } from '../../../utils/environment.js';
6
+ import { prompt } from '../../../utils/prompt.js';
7
+ import { defineCommand, defineOptions } from '@robingenz/zli';
8
+ import consola from 'consola';
9
+ import { z } from 'zod';
10
+ export default defineCommand({
11
+ description: 'Create a new environment.',
12
+ options: defineOptions(z.object({
13
+ appId: z.string().optional().describe('ID of the app.'),
14
+ name: z.string().optional().describe('Name of the environment.'),
15
+ })),
16
+ action: async (options, args) => {
17
+ let { appId, name } = options;
18
+ if (!authorizationService.hasAuthorizationToken()) {
19
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
20
+ process.exit(1);
21
+ }
22
+ if (!appId) {
23
+ if (!isInteractive()) {
24
+ consola.error('You must provide an app ID when running in non-interactive environment.');
25
+ process.exit(1);
26
+ }
27
+ const organizations = await organizationsService.findAll();
28
+ if (organizations.length === 0) {
29
+ consola.error('You must create an organization before creating an environment.');
30
+ process.exit(1);
31
+ }
32
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
33
+ const organizationId = await prompt('Select the organization of the app for which you want to create an environment.', {
34
+ type: 'select',
35
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
36
+ });
37
+ if (!organizationId) {
38
+ consola.error('You must select the organization of an app for which you want to create an environment.');
39
+ process.exit(1);
40
+ }
41
+ const apps = await appsService.findAll({
42
+ organizationId,
43
+ });
44
+ if (!apps.length) {
45
+ consola.error('You must create an app before creating an environment.');
46
+ process.exit(1);
47
+ }
48
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
49
+ appId = await prompt('Which app do you want to create the environment for?', {
50
+ type: 'select',
51
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
52
+ });
53
+ }
54
+ if (!name) {
55
+ if (!isInteractive()) {
56
+ consola.error('You must provide the environment name when running in non-interactive environment.');
57
+ process.exit(1);
58
+ }
59
+ name = await prompt('Enter the name of the environment:', { type: 'text' });
60
+ }
61
+ const response = await appEnvironmentsService.create({
62
+ appId,
63
+ name,
64
+ });
65
+ consola.success('Environment created successfully.');
66
+ consola.info(`Environment ID: ${response.id}`);
67
+ },
68
+ });
@@ -0,0 +1,87 @@
1
+ import appEnvironmentsService from '../../../services/app-environments.js';
2
+ import appsService from '../../../services/apps.js';
3
+ import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { isInteractive } from '../../../utils/environment.js';
6
+ import { prompt } from '../../../utils/prompt.js';
7
+ import { defineCommand, defineOptions } from '@robingenz/zli';
8
+ import consola from 'consola';
9
+ import { z } from 'zod';
10
+ export default defineCommand({
11
+ description: 'Delete an environment.',
12
+ options: defineOptions(z.object({
13
+ appId: z.string().optional().describe('ID of the app.'),
14
+ environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
15
+ name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
16
+ })),
17
+ action: async (options, args) => {
18
+ let { appId, environmentId, name } = options;
19
+ if (!authorizationService.hasAuthorizationToken()) {
20
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
21
+ process.exit(1);
22
+ }
23
+ if (!appId) {
24
+ if (!isInteractive()) {
25
+ consola.error('You must provide an app ID when running in non-interactive environment.');
26
+ process.exit(1);
27
+ }
28
+ const organizations = await organizationsService.findAll();
29
+ if (organizations.length === 0) {
30
+ consola.error('You must create an organization before deleting an environment.');
31
+ process.exit(1);
32
+ }
33
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
34
+ const organizationId = await prompt('Select the organization of the app from which you want to delete an environment.', {
35
+ type: 'select',
36
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
37
+ });
38
+ if (!organizationId) {
39
+ consola.error('You must select the organization of an app from which you want to delete an environment.');
40
+ process.exit(1);
41
+ }
42
+ const apps = await appsService.findAll({
43
+ organizationId,
44
+ });
45
+ if (!apps.length) {
46
+ consola.error('You must create an app before deleting an environment.');
47
+ process.exit(1);
48
+ }
49
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
50
+ appId = await prompt('Which app do you want to delete the environment from?', {
51
+ type: 'select',
52
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
53
+ });
54
+ }
55
+ if (!environmentId && !name) {
56
+ if (!isInteractive()) {
57
+ consola.error('You must provide either the environment ID or name when running in non-interactive environment.');
58
+ process.exit(1);
59
+ }
60
+ const environments = await appEnvironmentsService.findAll({ appId });
61
+ if (!environments.length) {
62
+ consola.error('No environments found for this app. Create one first.');
63
+ process.exit(1);
64
+ }
65
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
66
+ const selectedEnvironmentId = await prompt('Select the environment to delete:', {
67
+ type: 'select',
68
+ options: environments.map((env) => ({ label: env.name, value: env.id })),
69
+ });
70
+ environmentId = selectedEnvironmentId;
71
+ }
72
+ if (isInteractive()) {
73
+ const confirmed = await prompt('Are you sure you want to delete this environment?', {
74
+ type: 'confirm',
75
+ });
76
+ if (!confirmed) {
77
+ return;
78
+ }
79
+ }
80
+ await appEnvironmentsService.delete({
81
+ appId,
82
+ id: environmentId,
83
+ name,
84
+ });
85
+ consola.success('Environment deleted successfully.');
86
+ },
87
+ });
@@ -0,0 +1,69 @@
1
+ import appEnvironmentsService from '../../../services/app-environments.js';
2
+ import appsService from '../../../services/apps.js';
3
+ import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { isInteractive } from '../../../utils/environment.js';
6
+ import { prompt } from '../../../utils/prompt.js';
7
+ import { defineCommand, defineOptions } from '@robingenz/zli';
8
+ import consola from 'consola';
9
+ import { z } from 'zod';
10
+ export default defineCommand({
11
+ description: 'List all environments for an app.',
12
+ options: defineOptions(z.object({
13
+ appId: z.string().optional().describe('ID of the app.'),
14
+ json: z.boolean().optional().describe('Output in JSON format.'),
15
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
16
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
17
+ })),
18
+ action: async (options, args) => {
19
+ let { appId, json, limit, offset } = options;
20
+ if (!authorizationService.hasAuthorizationToken()) {
21
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
22
+ process.exit(1);
23
+ }
24
+ if (!appId) {
25
+ if (!isInteractive()) {
26
+ consola.error('You must provide an app ID when running in non-interactive environment.');
27
+ process.exit(1);
28
+ }
29
+ const organizations = await organizationsService.findAll();
30
+ if (organizations.length === 0) {
31
+ consola.error('You must create an organization before listing environments.');
32
+ process.exit(1);
33
+ }
34
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
35
+ const organizationId = await prompt('Select the organization of the app for which you want to list environments.', {
36
+ type: 'select',
37
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
38
+ });
39
+ if (!organizationId) {
40
+ consola.error('You must select the organization of an app for which you want to list environments.');
41
+ process.exit(1);
42
+ }
43
+ const apps = await appsService.findAll({
44
+ organizationId,
45
+ });
46
+ if (!apps.length) {
47
+ consola.error('You must create an app before listing environments.');
48
+ process.exit(1);
49
+ }
50
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
51
+ appId = await prompt('Which app do you want to list the environments for?', {
52
+ type: 'select',
53
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
54
+ });
55
+ }
56
+ const environments = await appEnvironmentsService.findAll({
57
+ appId,
58
+ limit,
59
+ offset,
60
+ });
61
+ if (json) {
62
+ console.log(JSON.stringify(environments, null, 2));
63
+ }
64
+ else {
65
+ console.table(environments);
66
+ consola.success('Environments retrieved successfully.');
67
+ }
68
+ },
69
+ });
@@ -0,0 +1,126 @@
1
+ import appEnvironmentsService from '../../../services/app-environments.js';
2
+ import appsService from '../../../services/apps.js';
3
+ import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { parseKeyValuePairs } from '../../../utils/app-environments.js';
6
+ import { isInteractive } from '../../../utils/environment.js';
7
+ import { prompt } from '../../../utils/prompt.js';
8
+ import { defineCommand, defineOptions } from '@robingenz/zli';
9
+ import consola from 'consola';
10
+ import fs from 'fs';
11
+ import { z } from 'zod';
12
+ export default defineCommand({
13
+ description: 'Set environment variables and secrets.',
14
+ options: defineOptions(z.object({
15
+ appId: z.string().optional().describe('ID of the app.'),
16
+ environmentId: z.string().optional().describe('ID of the environment.'),
17
+ variable: z
18
+ .array(z.string())
19
+ .optional()
20
+ .describe('Environment variable in key=value format. Can be specified multiple times.'),
21
+ variableFile: z.string().optional().describe('Path to a file containing environment variables in .env format.'),
22
+ secret: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe('Environment secret in key=value format. Can be specified multiple times.'),
26
+ secretFile: z.string().optional().describe('Path to a file containing environment secrets in .env format.'),
27
+ })),
28
+ action: async (options, args) => {
29
+ let { appId, environmentId, variable, variableFile, secret, secretFile } = options;
30
+ if (!authorizationService.hasAuthorizationToken()) {
31
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
32
+ process.exit(1);
33
+ }
34
+ if (!appId) {
35
+ if (!isInteractive()) {
36
+ consola.error('You must provide an app ID when running in non-interactive environment.');
37
+ process.exit(1);
38
+ }
39
+ const organizations = await organizationsService.findAll();
40
+ if (organizations.length === 0) {
41
+ consola.error('You must create an organization before setting environment variables or secrets.');
42
+ process.exit(1);
43
+ }
44
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
45
+ const organizationId = await prompt('Select the organization of the app for which you want to set environment variables or secrets.', {
46
+ type: 'select',
47
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
48
+ });
49
+ if (!organizationId) {
50
+ consola.error('You must select the organization of an app for which you want to set environment variables or secrets.');
51
+ process.exit(1);
52
+ }
53
+ const apps = await appsService.findAll({
54
+ organizationId,
55
+ });
56
+ if (!apps.length) {
57
+ consola.error('You must create an app before setting environment variables or secrets.');
58
+ process.exit(1);
59
+ }
60
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
61
+ appId = await prompt('Which app do you want to set the environment variables or secrets for?', {
62
+ type: 'select',
63
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
64
+ });
65
+ }
66
+ if (!environmentId) {
67
+ if (!isInteractive()) {
68
+ consola.error('You must provide an environment ID when running in non-interactive environment.');
69
+ process.exit(1);
70
+ }
71
+ const environments = await appEnvironmentsService.findAll({ appId });
72
+ if (!environments.length) {
73
+ consola.error('No environments found for this app. Create one first.');
74
+ process.exit(1);
75
+ }
76
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
77
+ environmentId = await prompt('Select the environment:', {
78
+ type: 'select',
79
+ options: environments.map((env) => ({ label: env.name, value: env.id })),
80
+ });
81
+ }
82
+ // Parse variables from inline and file
83
+ const variablesMap = new Map();
84
+ if (variableFile) {
85
+ const fileContent = await fs.promises.readFile(variableFile, 'utf-8');
86
+ const fileVariables = parseKeyValuePairs(fileContent);
87
+ fileVariables.forEach((v) => variablesMap.set(v.key, v.value));
88
+ }
89
+ if (variable) {
90
+ const inlineVariables = parseKeyValuePairs(variable.join('\n'));
91
+ inlineVariables.forEach((v) => variablesMap.set(v.key, v.value));
92
+ }
93
+ const allVariables = Array.from(variablesMap.entries()).map(([key, value]) => ({ key, value }));
94
+ // Parse secrets from inline and file
95
+ const secretsMap = new Map();
96
+ if (secretFile) {
97
+ const fileContent = await fs.promises.readFile(secretFile, 'utf-8');
98
+ const fileSecrets = parseKeyValuePairs(fileContent);
99
+ fileSecrets.forEach((s) => secretsMap.set(s.key, s.value));
100
+ }
101
+ if (secret) {
102
+ const inlineSecrets = parseKeyValuePairs(secret.join('\n'));
103
+ inlineSecrets.forEach((s) => secretsMap.set(s.key, s.value));
104
+ }
105
+ const allSecrets = Array.from(secretsMap.entries()).map(([key, value]) => ({ key, value }));
106
+ if (!allVariables.length && !allSecrets.length) {
107
+ consola.error('You must provide at least one variable or secret to set.');
108
+ process.exit(1);
109
+ }
110
+ if (allVariables.length) {
111
+ await appEnvironmentsService.setVariables({
112
+ appId,
113
+ environmentId,
114
+ variables: allVariables,
115
+ });
116
+ }
117
+ if (allSecrets.length) {
118
+ await appEnvironmentsService.setSecrets({
119
+ appId,
120
+ environmentId,
121
+ secrets: allSecrets,
122
+ });
123
+ }
124
+ consola.success('Environment variables and secrets set successfully.');
125
+ },
126
+ });
@@ -0,0 +1,98 @@
1
+ import appEnvironmentsService from '../../../services/app-environments.js';
2
+ import appsService from '../../../services/apps.js';
3
+ import authorizationService from '../../../services/authorization-service.js';
4
+ import organizationsService from '../../../services/organizations.js';
5
+ import { isInteractive } from '../../../utils/environment.js';
6
+ import { prompt } from '../../../utils/prompt.js';
7
+ import { defineCommand, defineOptions } from '@robingenz/zli';
8
+ import consola from 'consola';
9
+ import { z } from 'zod';
10
+ export default defineCommand({
11
+ description: 'Unset environment variables and secrets.',
12
+ options: defineOptions(z.object({
13
+ appId: z.string().optional().describe('ID of the app.'),
14
+ environmentId: z.string().optional().describe('ID of the environment.'),
15
+ variable: z
16
+ .array(z.string())
17
+ .optional()
18
+ .describe('Key of the environment variable to unset. Can be specified multiple times.'),
19
+ secret: z
20
+ .array(z.string())
21
+ .optional()
22
+ .describe('Key of the environment secret to unset. Can be specified multiple times.'),
23
+ })),
24
+ action: async (options, args) => {
25
+ let { appId, environmentId, variable: variableKeys, secret: secretKeys } = options;
26
+ if (!authorizationService.hasAuthorizationToken()) {
27
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
28
+ process.exit(1);
29
+ }
30
+ if (!appId) {
31
+ if (!isInteractive()) {
32
+ consola.error('You must provide an app ID when running in non-interactive environment.');
33
+ process.exit(1);
34
+ }
35
+ const organizations = await organizationsService.findAll();
36
+ if (organizations.length === 0) {
37
+ consola.error('You must create an organization before unsetting environment variables or secrets.');
38
+ process.exit(1);
39
+ }
40
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
41
+ const organizationId = await prompt('Select the organization of the app for which you want to unset environment variables or secrets.', {
42
+ type: 'select',
43
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
44
+ });
45
+ if (!organizationId) {
46
+ consola.error('You must select the organization of an app for which you want to unset environment variables or secrets.');
47
+ process.exit(1);
48
+ }
49
+ const apps = await appsService.findAll({
50
+ organizationId,
51
+ });
52
+ if (!apps.length) {
53
+ consola.error('You must create an app before unsetting environment variables or secrets.');
54
+ process.exit(1);
55
+ }
56
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
57
+ appId = await prompt('Which app do you want to unset the environment variables or secrets for?', {
58
+ type: 'select',
59
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
60
+ });
61
+ }
62
+ if (!environmentId) {
63
+ if (!isInteractive()) {
64
+ consola.error('You must provide an environment ID when running in non-interactive environment.');
65
+ process.exit(1);
66
+ }
67
+ const environments = await appEnvironmentsService.findAll({ appId });
68
+ if (!environments.length) {
69
+ consola.error('No environments found for this app. Create one first.');
70
+ process.exit(1);
71
+ }
72
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
73
+ environmentId = await prompt('Select the environment:', {
74
+ type: 'select',
75
+ options: environments.map((env) => ({ label: env.name, value: env.id })),
76
+ });
77
+ }
78
+ if (!variableKeys?.length && !secretKeys?.length) {
79
+ consola.error('You must provide at least one variable key or secret key to unset.');
80
+ process.exit(1);
81
+ }
82
+ if (variableKeys?.length) {
83
+ await appEnvironmentsService.unsetVariables({
84
+ appId,
85
+ environmentId,
86
+ keys: variableKeys,
87
+ });
88
+ }
89
+ if (secretKeys?.length) {
90
+ await appEnvironmentsService.unsetSecrets({
91
+ appId,
92
+ environmentId,
93
+ keys: secretKeys,
94
+ });
95
+ }
96
+ consola.success('Environment variables and secrets unset successfully.');
97
+ },
98
+ });
package/dist/index.js CHANGED
@@ -35,6 +35,11 @@ const config = defineConfig({
35
35
  'apps:channels:get': await import('./commands/apps/channels/get.js').then((mod) => mod.default),
36
36
  'apps:channels:list': await import('./commands/apps/channels/list.js').then((mod) => mod.default),
37
37
  'apps:channels:update': await import('./commands/apps/channels/update.js').then((mod) => mod.default),
38
+ 'apps:environments:create': await import('./commands/apps/environments/create.js').then((mod) => mod.default),
39
+ 'apps:environments:delete': await import('./commands/apps/environments/delete.js').then((mod) => mod.default),
40
+ 'apps:environments:list': await import('./commands/apps/environments/list.js').then((mod) => mod.default),
41
+ 'apps:environments:set': await import('./commands/apps/environments/set.js').then((mod) => mod.default),
42
+ 'apps:environments:unset': await import('./commands/apps/environments/unset.js').then((mod) => mod.default),
38
43
  'apps:deployments:create': await import('./commands/apps/deployments/create.js').then((mod) => mod.default),
39
44
  'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
40
45
  'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
@@ -5,15 +5,80 @@ class AppEnvironmentsServiceImpl {
5
5
  constructor(httpClient) {
6
6
  this.httpClient = httpClient;
7
7
  }
8
+ async create(dto) {
9
+ const response = await this.httpClient.post(`/v1/apps/${dto.appId}/environments`, dto, {
10
+ headers: {
11
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
12
+ },
13
+ });
14
+ return response.data;
15
+ }
16
+ async delete(dto) {
17
+ if (dto.id) {
18
+ await this.httpClient.delete(`/v1/apps/${dto.appId}/environments/${dto.id}`, {
19
+ headers: {
20
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
21
+ },
22
+ });
23
+ }
24
+ else if (dto.name) {
25
+ await this.httpClient.delete(`/v1/apps/${dto.appId}/environments`, {
26
+ headers: {
27
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
28
+ },
29
+ params: {
30
+ name: dto.name,
31
+ },
32
+ });
33
+ }
34
+ }
8
35
  async findAll(dto) {
9
- const { appId } = dto;
10
- const response = await this.httpClient.get(`/v1/apps/${appId}/environments`, {
36
+ const queryParams = new URLSearchParams();
37
+ if (dto.limit) {
38
+ queryParams.append('limit', dto.limit.toString());
39
+ }
40
+ if (dto.offset) {
41
+ queryParams.append('offset', dto.offset.toString());
42
+ }
43
+ const queryString = queryParams.toString();
44
+ const url = queryString
45
+ ? `/v1/apps/${dto.appId}/environments?${queryString}`
46
+ : `/v1/apps/${dto.appId}/environments`;
47
+ const response = await this.httpClient.get(url, {
11
48
  headers: {
12
49
  Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
13
50
  },
14
51
  });
15
52
  return response.data;
16
53
  }
54
+ async setVariables(dto) {
55
+ await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/variables/set`, dto.variables, {
56
+ headers: {
57
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
58
+ },
59
+ });
60
+ }
61
+ async setSecrets(dto) {
62
+ await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/secrets/set`, dto.secrets, {
63
+ headers: {
64
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
65
+ },
66
+ });
67
+ }
68
+ async unsetVariables(dto) {
69
+ await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/variables/unset`, dto.keys, {
70
+ headers: {
71
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
72
+ },
73
+ });
74
+ }
75
+ async unsetSecrets(dto) {
76
+ await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/secrets/unset`, dto.keys, {
77
+ headers: {
78
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
79
+ },
80
+ });
81
+ }
17
82
  }
18
83
  const appEnvironmentsService = new AppEnvironmentsServiceImpl(httpClient);
19
84
  export default appEnvironmentsService;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Parse key-value pairs from content string.
3
+ *
4
+ * Format: KEY=value (one per line)
5
+ * - Empty lines are ignored
6
+ * - Lines starting with # are ignored (comments)
7
+ * - Lines without = are skipped
8
+ * - Keys and values are trimmed
9
+ * - Values can contain = characters
10
+ * - Lines with empty keys are skipped
11
+ *
12
+ * @param content - Content string to parse
13
+ * @returns Array of key-value pairs
14
+ */
15
+ export function parseKeyValuePairs(content) {
16
+ const lines = content.split('\n');
17
+ const pairs = [];
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith('#')) {
21
+ continue;
22
+ }
23
+ const separatorIndex = trimmed.indexOf('=');
24
+ if (separatorIndex === -1) {
25
+ continue;
26
+ }
27
+ const key = trimmed.slice(0, separatorIndex).trim();
28
+ const value = trimmed.slice(separatorIndex + 1).trim();
29
+ if (key) {
30
+ pairs.push({ key, value });
31
+ }
32
+ }
33
+ return pairs;
34
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseKeyValuePairs } from './app-environments.js';
3
+ describe('parseKeyValuePairs', () => {
4
+ it('should parse valid key-value pairs', () => {
5
+ const result = parseKeyValuePairs('KEY1=value1\nKEY2=value2');
6
+ expect(result).toEqual([
7
+ { key: 'KEY1', value: 'value1' },
8
+ { key: 'KEY2', value: 'value2' },
9
+ ]);
10
+ });
11
+ it('should handle empty lines', () => {
12
+ const result = parseKeyValuePairs('KEY1=value1\n\nKEY2=value2\n\n');
13
+ expect(result).toEqual([
14
+ { key: 'KEY1', value: 'value1' },
15
+ { key: 'KEY2', value: 'value2' },
16
+ ]);
17
+ });
18
+ it('should ignore comments', () => {
19
+ const result = parseKeyValuePairs('# Comment\nKEY1=value1\n# Another comment\nKEY2=value2');
20
+ expect(result).toEqual([
21
+ { key: 'KEY1', value: 'value1' },
22
+ { key: 'KEY2', value: 'value2' },
23
+ ]);
24
+ });
25
+ it('should handle values with = characters', () => {
26
+ const result = parseKeyValuePairs('KEY1=value=with=equals\nKEY2=a=b');
27
+ expect(result).toEqual([
28
+ { key: 'KEY1', value: 'value=with=equals' },
29
+ { key: 'KEY2', value: 'a=b' },
30
+ ]);
31
+ });
32
+ it('should trim whitespace from keys and values', () => {
33
+ const result = parseKeyValuePairs(' KEY1 = value1 \n KEY2 = value2 ');
34
+ expect(result).toEqual([
35
+ { key: 'KEY1', value: 'value1' },
36
+ { key: 'KEY2', value: 'value2' },
37
+ ]);
38
+ });
39
+ it('should skip lines without = separator', () => {
40
+ const result = parseKeyValuePairs('KEY1=value1\nINVALID_LINE\nKEY2=value2');
41
+ expect(result).toEqual([
42
+ { key: 'KEY1', value: 'value1' },
43
+ { key: 'KEY2', value: 'value2' },
44
+ ]);
45
+ });
46
+ it('should skip lines with empty keys', () => {
47
+ const result = parseKeyValuePairs('KEY1=value1\n=value2\nKEY3=value3');
48
+ expect(result).toEqual([
49
+ { key: 'KEY1', value: 'value1' },
50
+ { key: 'KEY3', value: 'value3' },
51
+ ]);
52
+ });
53
+ it('should skip lines with empty values', () => {
54
+ const result = parseKeyValuePairs('KEY1=value1\nKEY2=\nKEY3=value3');
55
+ expect(result).toEqual([
56
+ { key: 'KEY1', value: 'value1' },
57
+ { key: 'KEY3', value: 'value3' },
58
+ ]);
59
+ });
60
+ it('should handle empty content', () => {
61
+ const result = parseKeyValuePairs('');
62
+ expect(result).toEqual([]);
63
+ });
64
+ it('should handle content with only comments', () => {
65
+ const result = parseKeyValuePairs('# Comment 1\n# Comment 2');
66
+ expect(result).toEqual([]);
67
+ });
68
+ it('should handle mixed valid and invalid lines', () => {
69
+ const result = parseKeyValuePairs('KEY1=value1\nINVALID\n=nokey\nKEY2=\nKEY3=value3\n# comment\nKEY4=value4');
70
+ expect(result).toEqual([
71
+ { key: 'KEY1', value: 'value1' },
72
+ { key: 'KEY3', value: 'value3' },
73
+ { key: 'KEY4', value: 'value4' },
74
+ ]);
75
+ });
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "3.10.2",
3
+ "version": "3.11.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {