@vizzly-testing/cli 0.38.0 → 0.39.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/README.md +222 -183
- package/dist/auth/client.js +4 -1
- package/dist/auth/core.js +64 -3
- package/dist/auth/operations.js +21 -11
- package/dist/cli.js +45 -53
- package/dist/commands/init.js +156 -483
- package/dist/commands/login.js +71 -137
- package/dist/commands/project.js +128 -44
- package/dist/commands/run.js +2 -2
- package/dist/commands/tdd.js +89 -38
- package/dist/commands/upload.js +2 -2
- package/dist/init/agents.js +99 -0
- package/dist/init/core.js +31 -0
- package/dist/init/prompts.js +44 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +31 -127
- package/dist/server/handlers/tdd-handler.js +33 -2
- package/dist/server/routers/auth.js +32 -42
- package/dist/services/index.js +8 -2
- package/dist/tdd/services/comparison-service.js +4 -1
- package/dist/tdd/tdd-service.js +14 -2
- package/dist/test-runner/core.js +28 -3
- package/dist/test-runner/operations.js +41 -20
- package/dist/types/index.d.ts +8 -0
- package/dist/uploader/core.js +1 -0
- package/dist/utils/browser.js +7 -1
- package/dist/utils/config-discovery.js +73 -0
- package/dist/utils/config-loader.js +29 -8
- package/dist/utils/config-schema.js +5 -0
- package/dist/utils/file-changes.js +105 -0
- package/dist/utils/login-poll-wait.js +50 -0
- package/dist/utils/project-binding.js +96 -0
- package/dist/utils/project-link-store.js +13 -7
- package/package.json +27 -27
- package/skills/vizzly/SKILL.md +4 -3
- package/skills/vizzly/references/ci.md +33 -0
- package/skills/vizzly/references/cli-context.md +81 -71
- package/skills/vizzly/references/dynamic-content.md +16 -17
- package/skills/vizzly/references/sdks.md +22 -22
- package/skills/vizzly/references/setup-ci.md +0 -44
package/dist/commands/login.js
CHANGED
|
@@ -3,110 +3,13 @@
|
|
|
3
3
|
* Authenticates user via OAuth device flow
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { buildTokens, normalizeDeviceFlowResponse, resolveAuthorizedTokenData } from '../auth/core.js';
|
|
6
8
|
import { completeDeviceFlow, createAuthClient, createTokenStore, initiateDeviceFlow, pollDeviceAuthorization } from '../auth/index.js';
|
|
7
9
|
import { openBrowser } from '../utils/browser.js';
|
|
8
10
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
11
|
+
import { waitForLoginPoll } from '../utils/login-poll-wait.js';
|
|
9
12
|
import * as output from '../utils/output.js';
|
|
10
|
-
function createLoginDeps(deps = {}) {
|
|
11
|
-
return {
|
|
12
|
-
completeDeviceFlow: deps.completeDeviceFlow || completeDeviceFlow,
|
|
13
|
-
createAuthClient: deps.createAuthClient || createAuthClient,
|
|
14
|
-
createTokenStore: deps.createTokenStore || createTokenStore,
|
|
15
|
-
getApiUrl: deps.getApiUrl || getApiUrl,
|
|
16
|
-
initiateDeviceFlow: deps.initiateDeviceFlow || initiateDeviceFlow,
|
|
17
|
-
now: deps.now || (() => Date.now()),
|
|
18
|
-
openBrowser: deps.openBrowser || openBrowser,
|
|
19
|
-
output: deps.output || output,
|
|
20
|
-
pollDeviceAuthorization: deps.pollDeviceAuthorization || pollDeviceAuthorization,
|
|
21
|
-
waitForEnter: deps.waitForEnter || waitForEnter,
|
|
22
|
-
exit: deps.exit || (code => process.exit(code))
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function configureOutput(output, globalOptions) {
|
|
26
|
-
output.configure({
|
|
27
|
-
json: globalOptions.json,
|
|
28
|
-
verbose: globalOptions.verbose,
|
|
29
|
-
color: !globalOptions.noColor
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
export function normalizeDeviceFlowResponse(deviceFlow) {
|
|
33
|
-
let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
|
|
34
|
-
let userCode = deviceFlow.user_code || deviceFlow.userCode;
|
|
35
|
-
let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
|
|
36
|
-
if (!verificationUri || !userCode || !deviceCode) {
|
|
37
|
-
throw new Error('Invalid device flow response from server');
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
verificationUri,
|
|
41
|
-
userCode,
|
|
42
|
-
deviceCode,
|
|
43
|
-
urlWithCode: `${verificationUri}?code=${encodeURIComponent(userCode)}`
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export function resolveAuthorizedTokenData(pollResponse) {
|
|
47
|
-
let accessToken = pollResponse.tokens?.accessToken || pollResponse.tokens?.access_token;
|
|
48
|
-
if (accessToken) {
|
|
49
|
-
return pollResponse;
|
|
50
|
-
}
|
|
51
|
-
if (pollResponse.status === 'pending') {
|
|
52
|
-
throw new Error('Authorization not complete yet. Please complete the authorization in your browser and try running "vizzly login" again.');
|
|
53
|
-
}
|
|
54
|
-
if (pollResponse.status === 'expired') {
|
|
55
|
-
throw new Error('Device code expired. Please try logging in again.');
|
|
56
|
-
}
|
|
57
|
-
if (pollResponse.status === 'denied') {
|
|
58
|
-
throw new Error('Authorization denied. Please try logging in again.');
|
|
59
|
-
}
|
|
60
|
-
throw new Error('Unexpected response from authorization server. Please try logging in again.');
|
|
61
|
-
}
|
|
62
|
-
export function buildTokens(tokenData, now = Date.now()) {
|
|
63
|
-
let tokensData = tokenData.tokens || tokenData;
|
|
64
|
-
let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
65
|
-
let tokenExpiresAt = tokenExpiresIn ? new Date(now + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
|
|
66
|
-
return {
|
|
67
|
-
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
68
|
-
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
69
|
-
expiresAt: tokenExpiresAt,
|
|
70
|
-
user: tokenData.user,
|
|
71
|
-
organizations: tokenData.organizations
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
export function getTokenExpiryHint(expiresAt, now = Date.now()) {
|
|
75
|
-
if (!expiresAt) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
let expiry = new Date(expiresAt);
|
|
79
|
-
let msUntilExpiry = expiry.getTime() - now;
|
|
80
|
-
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
81
|
-
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
82
|
-
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
83
|
-
if (daysUntilExpiry > 0) {
|
|
84
|
-
return `Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`;
|
|
85
|
-
}
|
|
86
|
-
if (hoursUntilExpiry > 0) {
|
|
87
|
-
return `Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`;
|
|
88
|
-
}
|
|
89
|
-
if (minutesUntilExpiry > 0) {
|
|
90
|
-
return `Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`;
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
export async function waitForEnter(stdin = process.stdin) {
|
|
95
|
-
await new Promise(resolve => {
|
|
96
|
-
let canSetRawMode = typeof stdin.setRawMode === 'function';
|
|
97
|
-
if (canSetRawMode) {
|
|
98
|
-
stdin.setRawMode(true);
|
|
99
|
-
}
|
|
100
|
-
stdin.resume();
|
|
101
|
-
stdin.once('data', () => {
|
|
102
|
-
if (canSetRawMode) {
|
|
103
|
-
stdin.setRawMode(false);
|
|
104
|
-
}
|
|
105
|
-
stdin.pause();
|
|
106
|
-
resolve();
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
13
|
|
|
111
14
|
/**
|
|
112
15
|
* Login command implementation using OAuth device flow
|
|
@@ -115,19 +18,14 @@ export async function waitForEnter(stdin = process.stdin) {
|
|
|
115
18
|
*/
|
|
116
19
|
export async function loginCommand(options = {}, globalOptions = {}, deps = {}) {
|
|
117
20
|
let {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pollDeviceAuthorization,
|
|
127
|
-
waitForEnter,
|
|
128
|
-
exit
|
|
129
|
-
} = createLoginDeps(deps);
|
|
130
|
-
configureOutput(output, globalOptions);
|
|
21
|
+
now = Date.now,
|
|
22
|
+
delay: sleep = delay
|
|
23
|
+
} = deps;
|
|
24
|
+
output.configure({
|
|
25
|
+
json: globalOptions.json,
|
|
26
|
+
verbose: globalOptions.verbose,
|
|
27
|
+
color: !globalOptions.noColor
|
|
28
|
+
});
|
|
131
29
|
let colors = output.getColors();
|
|
132
30
|
try {
|
|
133
31
|
output.header('login');
|
|
@@ -142,6 +40,12 @@ export async function loginCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
142
40
|
output.startSpinner('Connecting to Vizzly...');
|
|
143
41
|
let deviceFlow = await initiateDeviceFlow(client);
|
|
144
42
|
output.stopSpinner();
|
|
43
|
+
let expiresIn = Number(deviceFlow.expires_in ?? deviceFlow.expiresIn);
|
|
44
|
+
let lifetime = Number.isFinite(expiresIn) && expiresIn > 0 ? Math.min(expiresIn, 120) : 120;
|
|
45
|
+
let deadline = now() + lifetime * 1000;
|
|
46
|
+
let timeoutMessage = expiresIn > 0 && expiresIn <= 120 ? 'Device code expired. Please try logging in again.' : 'Login timed out after 2 minutes. Please try logging in again.';
|
|
47
|
+
let interval = Number(deviceFlow.interval);
|
|
48
|
+
let pollDelay = Number.isFinite(interval) && interval > 0 ? interval * 1000 : 5000;
|
|
145
49
|
|
|
146
50
|
// Handle both snake_case and camelCase field names
|
|
147
51
|
let {
|
|
@@ -166,14 +70,57 @@ export async function loginCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
166
70
|
output.hint('Please open the URL manually');
|
|
167
71
|
}
|
|
168
72
|
output.blank();
|
|
169
|
-
output.hint('
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
73
|
+
output.hint('Waiting for authorization in your browser (up to 2 minutes)...');
|
|
74
|
+
let interactive = process.stdin.isTTY && !globalOptions.json;
|
|
75
|
+
let backoff = false;
|
|
76
|
+
let lastCheckAt = 0;
|
|
77
|
+
let pollResponse;
|
|
78
|
+
while (true) {
|
|
79
|
+
let remaining = deadline - now();
|
|
80
|
+
if (remaining <= 0) {
|
|
81
|
+
throw new Error(timeoutMessage);
|
|
82
|
+
}
|
|
83
|
+
await waitForLoginPoll({
|
|
84
|
+
milliseconds: Math.min(pollDelay, remaining),
|
|
85
|
+
manualAllowed: !backoff,
|
|
86
|
+
earliestCheckAt: lastCheckAt + 1000,
|
|
87
|
+
stdin: interactive ? process.stdin : {
|
|
88
|
+
isTTY: false
|
|
89
|
+
},
|
|
90
|
+
now,
|
|
91
|
+
sleep,
|
|
92
|
+
onTick: (milliseconds, canCheck) => {
|
|
93
|
+
let seconds = Math.ceil(milliseconds / 1000);
|
|
94
|
+
let message = backoff ? `Rate limited. Next check in ${seconds}s` : `Next check in ${seconds}s`;
|
|
95
|
+
if (canCheck) message += ' — press Enter to check now';
|
|
96
|
+
if (interactive) output.updateSpinner(message);else output.hint(message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
remaining = deadline - now();
|
|
100
|
+
if (remaining <= 0) {
|
|
101
|
+
throw new Error(timeoutMessage);
|
|
102
|
+
}
|
|
103
|
+
lastCheckAt = now();
|
|
104
|
+
output.updateSpinner('Checking authorization...');
|
|
105
|
+
try {
|
|
106
|
+
pollResponse = await pollDeviceAuthorization(client, deviceCode, {
|
|
107
|
+
signal: AbortSignal.timeout(Math.ceil(Math.min(remaining, 30000)))
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (now() >= deadline) {
|
|
111
|
+
throw new Error(timeoutMessage);
|
|
112
|
+
}
|
|
113
|
+
if (error.code !== 'RATE_LIMIT_ERROR') throw error;
|
|
114
|
+
backoff = true;
|
|
115
|
+
let retryAfter = Number(error.context?.retryAfter);
|
|
116
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
117
|
+
pollDelay = Math.max(pollDelay, retryAfter * 1000);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
backoff = false;
|
|
122
|
+
if (pollResponse.status !== 'pending') break;
|
|
123
|
+
}
|
|
177
124
|
output.stopSpinner();
|
|
178
125
|
let tokenData = resolveAuthorizedTokenData(pollResponse);
|
|
179
126
|
|
|
@@ -201,17 +148,8 @@ export async function loginCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
201
148
|
let orgItems = tokens.organizations.map(org => `${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
|
|
202
149
|
output.list(orgItems);
|
|
203
150
|
}
|
|
204
|
-
|
|
205
|
-
// Show token expiry info
|
|
206
|
-
if (tokens.expiresAt) {
|
|
207
|
-
output.blank();
|
|
208
|
-
let expiryHint = getTokenExpiryHint(tokens.expiresAt, now());
|
|
209
|
-
if (expiryHint) {
|
|
210
|
-
output.hint(expiryHint);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
151
|
output.blank();
|
|
214
|
-
output.hint('
|
|
152
|
+
output.hint('You can now browse cloud builds and download baselines');
|
|
215
153
|
output.cleanup();
|
|
216
154
|
} catch (error) {
|
|
217
155
|
output.stopSpinner();
|
|
@@ -222,18 +160,14 @@ export async function loginCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
222
160
|
output.blank();
|
|
223
161
|
output.hint('Please try logging in again');
|
|
224
162
|
output.hint("If you don't have an account, sign up at https://vizzly.dev");
|
|
225
|
-
output.cleanup();
|
|
226
|
-
exit(1);
|
|
227
163
|
} else if (error.code === 'RATE_LIMIT_ERROR') {
|
|
228
164
|
output.error('Too many login attempts', error);
|
|
229
165
|
output.blank();
|
|
230
166
|
output.hint('Please wait a few minutes before trying again');
|
|
231
|
-
output.cleanup();
|
|
232
|
-
exit(1);
|
|
233
167
|
} else {
|
|
234
168
|
output.error('Login failed', error);
|
|
235
|
-
output.cleanup();
|
|
236
|
-
exit(1);
|
|
237
169
|
}
|
|
170
|
+
output.cleanup();
|
|
171
|
+
process.exit(1);
|
|
238
172
|
}
|
|
239
173
|
}
|
package/dist/commands/project.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createApiClient as defaultCreateApiClient } from '../api/client.js';
|
|
2
2
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
3
3
|
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
4
|
+
import { preflightChanges } from '../utils/file-changes.js';
|
|
4
5
|
import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
|
|
5
6
|
import * as defaultOutput from '../utils/output.js';
|
|
6
|
-
import {
|
|
7
|
+
import { planProjectBinding as defaultPlanProjectBinding, writeProjectBinding as defaultWriteProjectBinding } from '../utils/project-binding.js';
|
|
8
|
+
import { getProjectLink as defaultGetProjectLink, saveProjectLink as defaultSaveProjectLink } from '../utils/project-link-store.js';
|
|
7
9
|
export function parseProjectSelector(selector, options = {}) {
|
|
8
10
|
let organizationSlug = options.org || null;
|
|
9
11
|
let projectSlug = options.project || null;
|
|
@@ -42,7 +44,10 @@ export async function projectLinkCommand(selector, options = {}, globalOptions =
|
|
|
42
44
|
getApiUrl = defaultGetApiUrl,
|
|
43
45
|
loadConfig = defaultLoadConfig,
|
|
44
46
|
output = defaultOutput,
|
|
47
|
+
getProjectLink = defaultGetProjectLink,
|
|
48
|
+
planProjectBinding = defaultPlanProjectBinding,
|
|
45
49
|
saveProjectLink = defaultSaveProjectLink,
|
|
50
|
+
writeProjectBinding = defaultWriteProjectBinding,
|
|
46
51
|
exit = code => process.exit(code)
|
|
47
52
|
} = deps;
|
|
48
53
|
output.configure({
|
|
@@ -55,66 +60,145 @@ export async function projectLinkCommand(selector, options = {}, globalOptions =
|
|
|
55
60
|
projectSlug
|
|
56
61
|
} = parseProjectSelector(selector, options);
|
|
57
62
|
try {
|
|
58
|
-
let config
|
|
59
|
-
let
|
|
60
|
-
|
|
63
|
+
let config;
|
|
64
|
+
let configError;
|
|
65
|
+
try {
|
|
66
|
+
config = await loadConfig(globalOptions.config, globalOptions);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
configError = error;
|
|
69
|
+
}
|
|
70
|
+
let apiUrl = config?.apiUrl || getApiUrl();
|
|
71
|
+
let project = {
|
|
72
|
+
organizationSlug,
|
|
73
|
+
projectSlug
|
|
74
|
+
};
|
|
75
|
+
let binding = await planProjectBinding({
|
|
76
|
+
configPath: globalOptions.config || config?._configPath,
|
|
77
|
+
project,
|
|
78
|
+
apiUrl,
|
|
79
|
+
currentProject: config?.project,
|
|
80
|
+
currentApiUrl: config?._configApiUrl
|
|
81
|
+
});
|
|
82
|
+
if (binding.status === 'manual') {
|
|
83
|
+
let result = {
|
|
84
|
+
linked: false,
|
|
85
|
+
organizationSlug,
|
|
86
|
+
projectSlug,
|
|
87
|
+
binding: {
|
|
88
|
+
status: 'manual',
|
|
89
|
+
configPath: binding.configPath,
|
|
90
|
+
instructions: binding.instructions
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
if (globalOptions.json) output.data(result);else {
|
|
94
|
+
output.error('Project configuration needs an explicit edit');
|
|
95
|
+
output.hint(binding.instructions);
|
|
96
|
+
}
|
|
97
|
+
output.cleanup();
|
|
98
|
+
exit(2);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
if (configError) throw configError;
|
|
102
|
+
if (binding.after !== null) {
|
|
103
|
+
await preflightChanges([{
|
|
104
|
+
path: binding.configPath,
|
|
105
|
+
before: binding.before,
|
|
106
|
+
after: binding.after
|
|
107
|
+
}]);
|
|
108
|
+
}
|
|
109
|
+
let linkedProject = !options.name && !options.expiresAt ? await getProjectLink({
|
|
110
|
+
apiUrl,
|
|
111
|
+
...project
|
|
112
|
+
}) : null;
|
|
113
|
+
let reused = Boolean(linkedProject);
|
|
114
|
+
let userToken = linkedProject ? null : config.userToken || globalOptions.token || (await getAccessToken());
|
|
115
|
+
if (!linkedProject && !userToken) {
|
|
61
116
|
output.error('Login required before linking a project');
|
|
62
117
|
output.hint('Run "vizzly login" first, then try project link again');
|
|
63
118
|
output.cleanup();
|
|
64
119
|
exit(1);
|
|
65
|
-
return
|
|
120
|
+
return {
|
|
121
|
+
linked: false,
|
|
122
|
+
reason: 'auth_required'
|
|
123
|
+
};
|
|
66
124
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
if (!linkedProject) {
|
|
126
|
+
output.startSpinner(`Linking ${organizationSlug}/${projectSlug}...`);
|
|
127
|
+
let client = createApiClient({
|
|
128
|
+
baseUrl: apiUrl,
|
|
129
|
+
token: userToken,
|
|
130
|
+
command: 'project-link'
|
|
131
|
+
});
|
|
132
|
+
let response = await client.request(`/api/cli/${projectSlug}/link-token`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'X-Organization': organizationSlug
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
name: options.name,
|
|
140
|
+
expiresAt: options.expiresAt
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
linkedProject = await saveProjectLink({
|
|
144
|
+
apiUrl,
|
|
145
|
+
organizationSlug: response.organization?.slug || organizationSlug,
|
|
146
|
+
organizationName: response.organization?.name,
|
|
147
|
+
projectSlug: response.project?.slug || projectSlug,
|
|
148
|
+
projectName: response.project?.name,
|
|
149
|
+
token: response.token.token,
|
|
150
|
+
tokenId: response.token.id,
|
|
151
|
+
tokenPrefix: response.token.token_prefix,
|
|
152
|
+
expiresAt: response.token.expires_at,
|
|
153
|
+
createdAt: response.token.created_at
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (linkedProject.organizationSlug !== organizationSlug || linkedProject.projectSlug !== projectSlug) {
|
|
157
|
+
binding = await planProjectBinding({
|
|
158
|
+
configPath: binding.configPath,
|
|
159
|
+
project: {
|
|
160
|
+
organizationSlug: linkedProject.organizationSlug,
|
|
161
|
+
projectSlug: linkedProject.projectSlug
|
|
162
|
+
},
|
|
163
|
+
apiUrl,
|
|
164
|
+
currentProject: config?.project,
|
|
165
|
+
currentApiUrl: config?._configApiUrl
|
|
166
|
+
});
|
|
167
|
+
if (binding.status === 'manual') throw new Error(binding.instructions);
|
|
168
|
+
}
|
|
169
|
+
await writeProjectBinding(binding);
|
|
97
170
|
output.stopSpinner();
|
|
171
|
+
let result = {
|
|
172
|
+
linked: true,
|
|
173
|
+
organizationSlug: linkedProject.organizationSlug,
|
|
174
|
+
projectSlug: linkedProject.projectSlug,
|
|
175
|
+
tokenPrefix: linkedProject.tokenPrefix,
|
|
176
|
+
storage: linkedProject.storage,
|
|
177
|
+
reused,
|
|
178
|
+
binding: {
|
|
179
|
+
status: 'bound',
|
|
180
|
+
configPath: binding.configPath
|
|
181
|
+
}
|
|
182
|
+
};
|
|
98
183
|
if (globalOptions.json) {
|
|
99
|
-
output.data(
|
|
100
|
-
linked: true,
|
|
101
|
-
organizationSlug: linkedProject.organizationSlug,
|
|
102
|
-
projectSlug: linkedProject.projectSlug,
|
|
103
|
-
tokenPrefix: linkedProject.tokenPrefix,
|
|
104
|
-
storage: linkedProject.storage
|
|
105
|
-
});
|
|
184
|
+
output.data(result);
|
|
106
185
|
output.cleanup();
|
|
107
|
-
return;
|
|
186
|
+
return result;
|
|
108
187
|
}
|
|
109
188
|
output.complete(`Linked ${linkedProject.organizationSlug}/${linkedProject.projectSlug}`);
|
|
110
|
-
output.hint(`
|
|
189
|
+
output.hint(`Repository project: ${linkedProject.organizationSlug}/${linkedProject.projectSlug}`);
|
|
111
190
|
output.hint(`Cloud uploads and project-scoped context will use ${linkedProject.tokenPrefix}...`);
|
|
112
191
|
output.hint(`Credential storage: ${linkedProject.storage}`);
|
|
113
192
|
output.cleanup();
|
|
193
|
+
return result;
|
|
114
194
|
} catch (error) {
|
|
115
195
|
output.stopSpinner();
|
|
116
196
|
output.error('Failed to link project', error);
|
|
117
197
|
output.cleanup();
|
|
118
198
|
exit(1);
|
|
199
|
+
return {
|
|
200
|
+
linked: false,
|
|
201
|
+
reason: 'link_failed'
|
|
202
|
+
};
|
|
119
203
|
}
|
|
120
204
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -274,8 +274,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
274
274
|
commitAuthorName: commitAuthor.name,
|
|
275
275
|
commitAuthorEmail: commitAuthor.email,
|
|
276
276
|
environment: config.build.environment,
|
|
277
|
-
threshold: config.
|
|
278
|
-
minClusterSize: config.
|
|
277
|
+
threshold: config._comparisonOverrides?.threshold,
|
|
278
|
+
minClusterSize: config._comparisonOverrides?.minClusterSize,
|
|
279
279
|
eager: config.eager || false,
|
|
280
280
|
allowNoToken: config.allowNoToken || false,
|
|
281
281
|
wait: config.wait || options.wait || false,
|