@vizzly-testing/cli 0.26.1 → 0.27.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/dist/cli.js +250 -5
- package/dist/commands/api.js +190 -0
- package/dist/commands/baselines.js +265 -0
- package/dist/commands/builds.js +274 -0
- package/dist/commands/comparisons.js +345 -0
- package/dist/commands/config-cmd.js +184 -0
- package/dist/commands/init.js +54 -12
- package/dist/commands/orgs.js +79 -0
- package/dist/commands/preview.js +13 -3
- package/dist/commands/project.js +69 -15
- package/dist/commands/projects.js +96 -0
- package/dist/commands/review.js +319 -0
- package/dist/commands/run.js +142 -2
- package/dist/commands/tdd-daemon.js +78 -12
- package/dist/commands/tdd.js +61 -2
- package/dist/commands/upload.js +94 -2
- package/dist/server/handlers/tdd-handler.js +23 -2
- package/dist/utils/config-loader.js +13 -1
- package/dist/utils/output.js +107 -4
- package/package.json +2 -1
package/dist/commands/preview.js
CHANGED
|
@@ -597,11 +597,21 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
597
597
|
success: true,
|
|
598
598
|
buildId,
|
|
599
599
|
previewUrl: result.previewUrl,
|
|
600
|
-
files: result.uploaded,
|
|
601
|
-
|
|
600
|
+
files: result.uploaded || fileCount,
|
|
601
|
+
bytes: totalSize,
|
|
602
|
+
compressedBytes: zipBuffer.length,
|
|
603
|
+
compressionRatio: parseFloat(compressionRatio) / 100,
|
|
602
604
|
newBytes: result.newBytes,
|
|
603
|
-
|
|
605
|
+
reusedBlobs: result.reusedBlobs || 0,
|
|
606
|
+
deduplicationRatio: result.deduplicationRatio,
|
|
607
|
+
basePath: result.basePath || null,
|
|
608
|
+
expiresAt: result.expiresAt || null
|
|
604
609
|
});
|
|
610
|
+
output.cleanup();
|
|
611
|
+
return {
|
|
612
|
+
success: true,
|
|
613
|
+
result
|
|
614
|
+
};
|
|
605
615
|
} else {
|
|
606
616
|
output.complete('Preview uploaded');
|
|
607
617
|
output.blank();
|
package/dist/commands/project.js
CHANGED
|
@@ -107,6 +107,25 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
107
107
|
projectName: selectedProject.name,
|
|
108
108
|
organizationSlug: selectedOrg.slug
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
// JSON output for success
|
|
112
|
+
if (globalOptions.json) {
|
|
113
|
+
output.data({
|
|
114
|
+
status: 'configured',
|
|
115
|
+
project: {
|
|
116
|
+
name: selectedProject.name,
|
|
117
|
+
slug: selectedProject.slug
|
|
118
|
+
},
|
|
119
|
+
organization: {
|
|
120
|
+
name: selectedOrg.name,
|
|
121
|
+
slug: selectedOrg.slug
|
|
122
|
+
},
|
|
123
|
+
directory: currentDir,
|
|
124
|
+
tokenCreated: true
|
|
125
|
+
});
|
|
126
|
+
output.cleanup();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
110
129
|
output.complete('Project configured');
|
|
111
130
|
output.blank();
|
|
112
131
|
output.keyValue({
|
|
@@ -136,9 +155,13 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
136
155
|
try {
|
|
137
156
|
let mappings = await getProjectMappings();
|
|
138
157
|
let paths = Object.keys(mappings);
|
|
158
|
+
let currentDir = resolve(process.cwd());
|
|
139
159
|
if (paths.length === 0) {
|
|
140
160
|
if (globalOptions.json) {
|
|
141
|
-
output.data({
|
|
161
|
+
output.data({
|
|
162
|
+
projects: [],
|
|
163
|
+
current: null
|
|
164
|
+
});
|
|
142
165
|
} else {
|
|
143
166
|
output.header('project:list');
|
|
144
167
|
output.print(' No projects configured');
|
|
@@ -149,13 +172,29 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
149
172
|
return;
|
|
150
173
|
}
|
|
151
174
|
if (globalOptions.json) {
|
|
152
|
-
|
|
175
|
+
let projects = paths.map(path => {
|
|
176
|
+
let mapping = mappings[path];
|
|
177
|
+
return {
|
|
178
|
+
directory: path,
|
|
179
|
+
isCurrent: path === currentDir,
|
|
180
|
+
project: {
|
|
181
|
+
name: mapping.projectName,
|
|
182
|
+
slug: mapping.projectSlug
|
|
183
|
+
},
|
|
184
|
+
organization: mapping.organizationSlug,
|
|
185
|
+
createdAt: mapping.createdAt
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
let current = projects.find(p => p.isCurrent) || null;
|
|
189
|
+
output.data({
|
|
190
|
+
projects,
|
|
191
|
+
current
|
|
192
|
+
});
|
|
153
193
|
output.cleanup();
|
|
154
194
|
return;
|
|
155
195
|
}
|
|
156
196
|
output.header('project:list');
|
|
157
197
|
let colors = output.getColors();
|
|
158
|
-
let currentDir = resolve(process.cwd());
|
|
159
198
|
for (let path of paths) {
|
|
160
199
|
let mapping = mappings[path];
|
|
161
200
|
let isCurrent = path === currentDir;
|
|
@@ -211,8 +250,13 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
211
250
|
if (globalOptions.json) {
|
|
212
251
|
output.data({
|
|
213
252
|
token: tokenStr,
|
|
214
|
-
|
|
215
|
-
|
|
253
|
+
directory: currentDir,
|
|
254
|
+
project: {
|
|
255
|
+
name: mapping.projectName,
|
|
256
|
+
slug: mapping.projectSlug
|
|
257
|
+
},
|
|
258
|
+
organization: mapping.organizationSlug,
|
|
259
|
+
createdAt: mapping.createdAt
|
|
216
260
|
});
|
|
217
261
|
output.cleanup();
|
|
218
262
|
return;
|
|
@@ -304,7 +348,23 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
304
348
|
return;
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
//
|
|
351
|
+
// In JSON mode, skip confirmation (for scripting)
|
|
352
|
+
if (globalOptions.json) {
|
|
353
|
+
await deleteProjectMapping(currentDir);
|
|
354
|
+
output.data({
|
|
355
|
+
removed: true,
|
|
356
|
+
directory: currentDir,
|
|
357
|
+
project: {
|
|
358
|
+
name: mapping.projectName,
|
|
359
|
+
slug: mapping.projectSlug
|
|
360
|
+
},
|
|
361
|
+
organization: mapping.organizationSlug
|
|
362
|
+
});
|
|
363
|
+
output.cleanup();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Confirm removal (interactive mode only)
|
|
308
368
|
output.header('project:remove');
|
|
309
369
|
output.labelValue('Current configuration', '');
|
|
310
370
|
output.keyValue({
|
|
@@ -320,15 +380,9 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
320
380
|
return;
|
|
321
381
|
}
|
|
322
382
|
await deleteProjectMapping(currentDir);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
} else {
|
|
328
|
-
output.complete('Project configuration removed');
|
|
329
|
-
output.blank();
|
|
330
|
-
output.hint('Run "vizzly project:select" to configure a different project');
|
|
331
|
-
}
|
|
383
|
+
output.complete('Project configuration removed');
|
|
384
|
+
output.blank();
|
|
385
|
+
output.hint('Run "vizzly project:select" to configure a different project');
|
|
332
386
|
output.cleanup();
|
|
333
387
|
} catch (error) {
|
|
334
388
|
output.error('Failed to remove project configuration', error);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects command - List projects the user has access to
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createApiClient } from '../api/client.js';
|
|
6
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
7
|
+
import { getApiUrl } from '../utils/environment-config.js';
|
|
8
|
+
import * as output from '../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Projects command implementation
|
|
12
|
+
* @param {Object} options - Command options
|
|
13
|
+
* @param {Object} globalOptions - Global CLI options
|
|
14
|
+
*/
|
|
15
|
+
export async function projectsCommand(options = {}, globalOptions = {}) {
|
|
16
|
+
output.configure({
|
|
17
|
+
json: globalOptions.json,
|
|
18
|
+
verbose: globalOptions.verbose,
|
|
19
|
+
color: !globalOptions.noColor
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
let config = await loadConfig(globalOptions.config, globalOptions);
|
|
23
|
+
if (!config.apiKey) {
|
|
24
|
+
output.error('API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"');
|
|
25
|
+
output.cleanup();
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
let client = createApiClient({
|
|
29
|
+
baseUrl: config.apiUrl || getApiUrl(),
|
|
30
|
+
token: config.apiKey
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Build query params
|
|
34
|
+
let params = new URLSearchParams();
|
|
35
|
+
if (options.org) params.set('organization', options.org);
|
|
36
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
37
|
+
if (options.offset) params.set('offset', String(options.offset));
|
|
38
|
+
let queryString = params.toString();
|
|
39
|
+
let endpoint = `/api/sdk/projects${queryString ? `?${queryString}` : ''}`;
|
|
40
|
+
output.startSpinner('Fetching projects...');
|
|
41
|
+
let response = await client.request(endpoint);
|
|
42
|
+
output.stopSpinner();
|
|
43
|
+
let projects = response.projects || [];
|
|
44
|
+
let pagination = response.pagination || {};
|
|
45
|
+
if (globalOptions.json) {
|
|
46
|
+
output.data({
|
|
47
|
+
projects: projects.map(p => ({
|
|
48
|
+
id: p.id,
|
|
49
|
+
name: p.name,
|
|
50
|
+
slug: p.slug,
|
|
51
|
+
organizationName: p.organizationName,
|
|
52
|
+
organizationSlug: p.organizationSlug,
|
|
53
|
+
buildCount: p.buildCount,
|
|
54
|
+
createdAt: p.created_at,
|
|
55
|
+
updatedAt: p.updated_at
|
|
56
|
+
})),
|
|
57
|
+
pagination
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
output.header('projects');
|
|
61
|
+
let colors = output.getColors();
|
|
62
|
+
if (projects.length === 0) {
|
|
63
|
+
output.print(' No projects found');
|
|
64
|
+
if (options.org) {
|
|
65
|
+
output.hint(`No projects in organization "${options.org}"`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
output.labelValue('Showing', `${projects.length} of ${pagination.total}`);
|
|
69
|
+
output.blank();
|
|
70
|
+
for (let project of projects) {
|
|
71
|
+
output.print(` ${colors.bold(project.name)} ${colors.dim(`@${project.organizationSlug}/${project.slug}`)}`);
|
|
72
|
+
output.print(` ${colors.dim(`${project.buildCount} builds`)}`);
|
|
73
|
+
}
|
|
74
|
+
if (pagination.hasMore) {
|
|
75
|
+
output.blank();
|
|
76
|
+
output.hint(`Use --offset ${(options.offset || 0) + projects.length} to see more`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
output.cleanup();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
output.stopSpinner();
|
|
83
|
+
output.error('Failed to fetch projects', error);
|
|
84
|
+
output.cleanup();
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate projects options
|
|
91
|
+
* @param {Object} _options - Command options
|
|
92
|
+
* @returns {string[]} Validation errors
|
|
93
|
+
*/
|
|
94
|
+
export function validateProjectsOptions(_options = {}) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review commands - approve, reject, and comment on comparisons/builds
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createApiClient as defaultCreateApiClient } from '../api/index.js';
|
|
6
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
7
|
+
import * as defaultOutput from '../utils/output.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Approve a comparison
|
|
11
|
+
* @param {string} comparisonId - Comparison ID to approve
|
|
12
|
+
* @param {Object} options - Command options
|
|
13
|
+
* @param {Object} globalOptions - Global CLI options
|
|
14
|
+
* @param {Object} deps - Dependencies for testing
|
|
15
|
+
*/
|
|
16
|
+
export async function approveCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
|
|
17
|
+
let {
|
|
18
|
+
loadConfig = defaultLoadConfig,
|
|
19
|
+
createApiClient = defaultCreateApiClient,
|
|
20
|
+
output = defaultOutput,
|
|
21
|
+
exit = code => process.exit(code)
|
|
22
|
+
} = deps;
|
|
23
|
+
output.configure({
|
|
24
|
+
json: globalOptions.json,
|
|
25
|
+
verbose: globalOptions.verbose,
|
|
26
|
+
color: !globalOptions.noColor
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
let allOptions = {
|
|
30
|
+
...globalOptions,
|
|
31
|
+
...options
|
|
32
|
+
};
|
|
33
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
34
|
+
if (!config.apiKey) {
|
|
35
|
+
output.error('API token required');
|
|
36
|
+
output.hint('Use --token or set VIZZLY_TOKEN environment variable');
|
|
37
|
+
exit(1);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
output.startSpinner('Approving comparison...');
|
|
41
|
+
let client = createApiClient({
|
|
42
|
+
baseUrl: config.apiUrl,
|
|
43
|
+
token: config.apiKey,
|
|
44
|
+
command: 'approve'
|
|
45
|
+
});
|
|
46
|
+
let body = {};
|
|
47
|
+
if (options.comment) {
|
|
48
|
+
body.comment = options.comment;
|
|
49
|
+
}
|
|
50
|
+
let response = await client.request(`/api/sdk/comparisons/${comparisonId}/approve`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
|
|
53
|
+
headers: Object.keys(body).length > 0 ? {
|
|
54
|
+
'Content-Type': 'application/json'
|
|
55
|
+
} : undefined
|
|
56
|
+
});
|
|
57
|
+
output.stopSpinner();
|
|
58
|
+
if (globalOptions.json) {
|
|
59
|
+
output.data({
|
|
60
|
+
approved: true,
|
|
61
|
+
comparisonId,
|
|
62
|
+
comparison: response.comparison
|
|
63
|
+
});
|
|
64
|
+
output.cleanup();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
output.complete(`Comparison ${comparisonId} approved`);
|
|
68
|
+
if (options.comment) {
|
|
69
|
+
output.hint(`Comment: "${options.comment}"`);
|
|
70
|
+
}
|
|
71
|
+
output.cleanup();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
output.stopSpinner();
|
|
74
|
+
if (globalOptions.json) {
|
|
75
|
+
output.data({
|
|
76
|
+
approved: false,
|
|
77
|
+
comparisonId,
|
|
78
|
+
error: {
|
|
79
|
+
message: error.message,
|
|
80
|
+
code: error.code
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
output.cleanup();
|
|
84
|
+
exit(1);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
output.error('Failed to approve comparison', error);
|
|
88
|
+
output.cleanup();
|
|
89
|
+
exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reject a comparison
|
|
95
|
+
* @param {string} comparisonId - Comparison ID to reject
|
|
96
|
+
* @param {Object} options - Command options
|
|
97
|
+
* @param {Object} globalOptions - Global CLI options
|
|
98
|
+
* @param {Object} deps - Dependencies for testing
|
|
99
|
+
*/
|
|
100
|
+
export async function rejectCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
|
|
101
|
+
let {
|
|
102
|
+
loadConfig = defaultLoadConfig,
|
|
103
|
+
createApiClient = defaultCreateApiClient,
|
|
104
|
+
output = defaultOutput,
|
|
105
|
+
exit = code => process.exit(code)
|
|
106
|
+
} = deps;
|
|
107
|
+
output.configure({
|
|
108
|
+
json: globalOptions.json,
|
|
109
|
+
verbose: globalOptions.verbose,
|
|
110
|
+
color: !globalOptions.noColor
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
let allOptions = {
|
|
114
|
+
...globalOptions,
|
|
115
|
+
...options
|
|
116
|
+
};
|
|
117
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
118
|
+
if (!config.apiKey) {
|
|
119
|
+
output.error('API token required');
|
|
120
|
+
output.hint('Use --token or set VIZZLY_TOKEN environment variable');
|
|
121
|
+
exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!options.reason) {
|
|
125
|
+
output.error('Reason required when rejecting');
|
|
126
|
+
output.hint('Use --reason "explanation" to provide a reason');
|
|
127
|
+
exit(1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
output.startSpinner('Rejecting comparison...');
|
|
131
|
+
let client = createApiClient({
|
|
132
|
+
baseUrl: config.apiUrl,
|
|
133
|
+
token: config.apiKey,
|
|
134
|
+
command: 'reject'
|
|
135
|
+
});
|
|
136
|
+
let response = await client.request(`/api/sdk/comparisons/${comparisonId}/reject`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
reason: options.reason
|
|
140
|
+
}),
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json'
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
output.stopSpinner();
|
|
146
|
+
if (globalOptions.json) {
|
|
147
|
+
output.data({
|
|
148
|
+
rejected: true,
|
|
149
|
+
comparisonId,
|
|
150
|
+
reason: options.reason,
|
|
151
|
+
comparison: response.comparison
|
|
152
|
+
});
|
|
153
|
+
output.cleanup();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
output.complete(`Comparison ${comparisonId} rejected`);
|
|
157
|
+
output.hint(`Reason: "${options.reason}"`);
|
|
158
|
+
output.cleanup();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
output.stopSpinner();
|
|
161
|
+
if (globalOptions.json) {
|
|
162
|
+
output.data({
|
|
163
|
+
rejected: false,
|
|
164
|
+
comparisonId,
|
|
165
|
+
error: {
|
|
166
|
+
message: error.message,
|
|
167
|
+
code: error.code
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
output.cleanup();
|
|
171
|
+
exit(1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
output.error('Failed to reject comparison', error);
|
|
175
|
+
output.cleanup();
|
|
176
|
+
exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Add a comment to a build
|
|
182
|
+
* @param {string} buildId - Build ID to comment on
|
|
183
|
+
* @param {string} message - Comment message
|
|
184
|
+
* @param {Object} options - Command options
|
|
185
|
+
* @param {Object} globalOptions - Global CLI options
|
|
186
|
+
* @param {Object} deps - Dependencies for testing
|
|
187
|
+
*/
|
|
188
|
+
export async function commentCommand(buildId, message, options = {}, globalOptions = {}, deps = {}) {
|
|
189
|
+
let {
|
|
190
|
+
loadConfig = defaultLoadConfig,
|
|
191
|
+
createApiClient = defaultCreateApiClient,
|
|
192
|
+
output = defaultOutput,
|
|
193
|
+
exit = code => process.exit(code)
|
|
194
|
+
} = deps;
|
|
195
|
+
output.configure({
|
|
196
|
+
json: globalOptions.json,
|
|
197
|
+
verbose: globalOptions.verbose,
|
|
198
|
+
color: !globalOptions.noColor
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
let allOptions = {
|
|
202
|
+
...globalOptions,
|
|
203
|
+
...options
|
|
204
|
+
};
|
|
205
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
206
|
+
if (!config.apiKey) {
|
|
207
|
+
output.error('API token required');
|
|
208
|
+
output.hint('Use --token or set VIZZLY_TOKEN environment variable');
|
|
209
|
+
exit(1);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!message || message.trim() === '') {
|
|
213
|
+
output.error('Comment message required');
|
|
214
|
+
exit(1);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
output.startSpinner('Adding comment...');
|
|
218
|
+
let client = createApiClient({
|
|
219
|
+
baseUrl: config.apiUrl,
|
|
220
|
+
token: config.apiKey,
|
|
221
|
+
command: 'comment'
|
|
222
|
+
});
|
|
223
|
+
let body = {
|
|
224
|
+
content: message,
|
|
225
|
+
type: options.type || 'general'
|
|
226
|
+
};
|
|
227
|
+
let response = await client.request(`/api/sdk/builds/${buildId}/comments`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body: JSON.stringify(body),
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
output.stopSpinner();
|
|
235
|
+
if (globalOptions.json) {
|
|
236
|
+
output.data({
|
|
237
|
+
created: true,
|
|
238
|
+
buildId,
|
|
239
|
+
comment: response.comment
|
|
240
|
+
});
|
|
241
|
+
output.cleanup();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
output.complete('Comment added');
|
|
245
|
+
output.labelValue('Build', buildId);
|
|
246
|
+
output.labelValue('Message', message);
|
|
247
|
+
output.cleanup();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
output.stopSpinner();
|
|
250
|
+
if (globalOptions.json) {
|
|
251
|
+
output.data({
|
|
252
|
+
created: false,
|
|
253
|
+
buildId,
|
|
254
|
+
error: {
|
|
255
|
+
message: error.message,
|
|
256
|
+
code: error.code
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
output.cleanup();
|
|
260
|
+
exit(1);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
output.error('Failed to add comment', error);
|
|
264
|
+
output.cleanup();
|
|
265
|
+
exit(1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Validate approve command options
|
|
271
|
+
* @param {string} comparisonId - Comparison ID
|
|
272
|
+
* @param {Object} options - Command options
|
|
273
|
+
* @returns {string[]} Array of error messages
|
|
274
|
+
*/
|
|
275
|
+
export function validateApproveOptions(comparisonId, _options = {}) {
|
|
276
|
+
let errors = [];
|
|
277
|
+
if (!comparisonId || comparisonId.trim() === '') {
|
|
278
|
+
errors.push('Comparison ID is required');
|
|
279
|
+
}
|
|
280
|
+
return errors;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Validate reject command options
|
|
285
|
+
* @param {string} comparisonId - Comparison ID
|
|
286
|
+
* @param {Object} options - Command options
|
|
287
|
+
* @returns {string[]} Array of error messages
|
|
288
|
+
*/
|
|
289
|
+
export function validateRejectOptions(comparisonId, options = {}) {
|
|
290
|
+
let errors = [];
|
|
291
|
+
if (!comparisonId || comparisonId.trim() === '') {
|
|
292
|
+
errors.push('Comparison ID is required');
|
|
293
|
+
}
|
|
294
|
+
if (!options.reason || options.reason.trim() === '') {
|
|
295
|
+
errors.push('--reason is required when rejecting');
|
|
296
|
+
}
|
|
297
|
+
return errors;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Validate comment command options
|
|
302
|
+
* @param {string} buildId - Build ID
|
|
303
|
+
* @param {string} message - Comment message
|
|
304
|
+
* @param {Object} options - Command options
|
|
305
|
+
* @returns {string[]} Array of error messages
|
|
306
|
+
*/
|
|
307
|
+
export function validateCommentOptions(buildId, message, options = {}) {
|
|
308
|
+
let errors = [];
|
|
309
|
+
if (!buildId || buildId.trim() === '') {
|
|
310
|
+
errors.push('Build ID is required');
|
|
311
|
+
}
|
|
312
|
+
if (!message || message.trim() === '') {
|
|
313
|
+
errors.push('Comment message is required');
|
|
314
|
+
}
|
|
315
|
+
if (options.type && !['general', 'approval', 'rejection'].includes(options.type)) {
|
|
316
|
+
errors.push('--type must be one of: general, approval, rejection');
|
|
317
|
+
}
|
|
318
|
+
return errors;
|
|
319
|
+
}
|