@vizzly-testing/cli 0.27.1 → 0.28.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/api/endpoints.js +2 -0
- package/dist/cli.js +3 -35
- package/dist/commands/builds.js +1 -0
- package/dist/commands/comparisons.js +1 -0
- package/dist/commands/config-cmd.js +1 -20
- package/dist/project/core.js +0 -84
- package/dist/project/operations.js +1 -77
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +55 -55
- package/dist/server/routers/projects.js +1 -101
- package/dist/services/project-service.js +1 -49
- package/dist/utils/config-loader.js +4 -27
- package/dist/utils/context.js +4 -38
- package/dist/utils/global-config.js +1 -76
- package/package.json +1 -1
- package/dist/commands/project.js +0 -414
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Projects Router
|
|
3
|
-
* Handles project
|
|
3
|
+
* Handles project listing and builds endpoints
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as output from '../../utils/output.js';
|
|
7
|
-
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
7
|
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -40,105 +39,6 @@ export function createProjectsRouter({
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// List project directory mappings
|
|
44
|
-
if (req.method === 'GET' && pathname === '/api/projects/mappings') {
|
|
45
|
-
try {
|
|
46
|
-
const mappings = await projectService.listMappings();
|
|
47
|
-
sendSuccess(res, {
|
|
48
|
-
mappings
|
|
49
|
-
});
|
|
50
|
-
return true;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
output.debug('Error listing project mappings:', {
|
|
53
|
-
error: error.message
|
|
54
|
-
});
|
|
55
|
-
sendError(res, 500, error.message);
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Create or update project mapping
|
|
61
|
-
if (req.method === 'POST' && pathname === '/api/projects/mappings') {
|
|
62
|
-
try {
|
|
63
|
-
const body = await parseJsonBody(req);
|
|
64
|
-
const {
|
|
65
|
-
directory,
|
|
66
|
-
projectSlug,
|
|
67
|
-
organizationSlug,
|
|
68
|
-
token,
|
|
69
|
-
projectName
|
|
70
|
-
} = body;
|
|
71
|
-
const mapping = await projectService.createMapping(directory, {
|
|
72
|
-
projectSlug,
|
|
73
|
-
organizationSlug,
|
|
74
|
-
token,
|
|
75
|
-
projectName
|
|
76
|
-
});
|
|
77
|
-
sendSuccess(res, {
|
|
78
|
-
success: true,
|
|
79
|
-
mapping
|
|
80
|
-
});
|
|
81
|
-
return true;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
output.debug('Error creating project mapping:', {
|
|
84
|
-
error: error.message
|
|
85
|
-
});
|
|
86
|
-
sendError(res, 500, error.message);
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Delete project mapping
|
|
92
|
-
if (req.method === 'DELETE' && pathname.startsWith('/api/projects/mappings/')) {
|
|
93
|
-
try {
|
|
94
|
-
const directory = decodeURIComponent(pathname.replace('/api/projects/mappings/', ''));
|
|
95
|
-
await projectService.removeMapping(directory);
|
|
96
|
-
sendSuccess(res, {
|
|
97
|
-
success: true,
|
|
98
|
-
message: 'Mapping deleted'
|
|
99
|
-
});
|
|
100
|
-
return true;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
output.debug('Error deleting project mapping:', {
|
|
103
|
-
error: error.message
|
|
104
|
-
});
|
|
105
|
-
sendError(res, 500, error.message);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Get recent builds for current project
|
|
111
|
-
if (req.method === 'GET' && pathname === '/api/builds/recent') {
|
|
112
|
-
if (!projectService) {
|
|
113
|
-
sendServiceUnavailable(res, 'Project service');
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const currentDir = process.cwd();
|
|
118
|
-
const mapping = await projectService.getMapping(currentDir);
|
|
119
|
-
if (!mapping || !mapping.projectSlug || !mapping.organizationSlug) {
|
|
120
|
-
sendError(res, 400, 'No project configured for this directory');
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
const limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
|
|
124
|
-
const branch = parsedUrl.searchParams.get('branch') || undefined;
|
|
125
|
-
const builds = await projectService.getRecentBuilds(mapping.projectSlug, mapping.organizationSlug, {
|
|
126
|
-
limit,
|
|
127
|
-
branch
|
|
128
|
-
});
|
|
129
|
-
sendSuccess(res, {
|
|
130
|
-
builds
|
|
131
|
-
});
|
|
132
|
-
return true;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
output.debug('Error fetching recent builds:', {
|
|
135
|
-
error: error.message
|
|
136
|
-
});
|
|
137
|
-
sendError(res, 500, error.message);
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
42
|
// Get builds for a specific project (used by /builds page)
|
|
143
43
|
const projectBuildsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/([^/]+)\/builds$/);
|
|
144
44
|
if (req.method === 'GET' && projectBuildsMatch) {
|
|
@@ -4,29 +4,22 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Provides the interface expected by src/server/routers/projects.js:
|
|
6
6
|
* - listProjects() - Returns [] if not authenticated
|
|
7
|
-
* - listMappings() - Returns [] if no mappings
|
|
8
|
-
* - getMapping(directory) - Returns null if not found
|
|
9
|
-
* - createMapping(directory, projectData) - Throws on invalid input
|
|
10
|
-
* - removeMapping(directory) - Throws on invalid directory
|
|
11
7
|
* - getRecentBuilds(projectSlug, organizationSlug, options) - Returns [] if not authenticated
|
|
12
8
|
*
|
|
13
9
|
* Error handling:
|
|
14
10
|
* - API methods (listProjects, getRecentBuilds) return empty arrays when not authenticated
|
|
15
|
-
* - Local methods (listMappings, getMapping) never require authentication
|
|
16
|
-
* - Validation errors (createMapping, removeMapping) throw with descriptive messages
|
|
17
11
|
*/
|
|
18
12
|
|
|
19
13
|
import { createAuthClient } from '../auth/client.js';
|
|
20
14
|
import * as projectOps from '../project/operations.js';
|
|
21
15
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
22
|
-
import {
|
|
16
|
+
import { getAuthTokens } from '../utils/global-config.js';
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
19
|
* Create a project service instance
|
|
26
20
|
* @param {Object} [options]
|
|
27
21
|
* @param {string} [options.apiUrl] - API base URL (defaults to VIZZLY_API_URL or https://app.vizzly.dev)
|
|
28
22
|
* @param {Object} [options.httpClient] - Injectable HTTP client (for testing)
|
|
29
|
-
* @param {Object} [options.mappingStore] - Injectable mapping store (for testing)
|
|
30
23
|
* @param {Function} [options.getAuthTokens] - Injectable token getter (for testing)
|
|
31
24
|
* @returns {Object} Project service
|
|
32
25
|
*/
|
|
@@ -39,15 +32,6 @@ export function createProjectService(options = {}) {
|
|
|
39
32
|
baseUrl: apiUrl
|
|
40
33
|
});
|
|
41
34
|
|
|
42
|
-
// Create mapping store adapter for global config
|
|
43
|
-
// Allow injection for testing
|
|
44
|
-
let mappingStore = options.mappingStore || {
|
|
45
|
-
getMappings: getProjectMappings,
|
|
46
|
-
getMapping: getProjectMapping,
|
|
47
|
-
saveMapping: saveProjectMapping,
|
|
48
|
-
deleteMapping: deleteProjectMapping
|
|
49
|
-
};
|
|
50
|
-
|
|
51
35
|
// Allow injection of getAuthTokens for testing
|
|
52
36
|
let tokenGetter = options.getAuthTokens || getAuthTokens;
|
|
53
37
|
|
|
@@ -80,38 +64,6 @@ export function createProjectService(options = {}) {
|
|
|
80
64
|
apiClient: null
|
|
81
65
|
});
|
|
82
66
|
},
|
|
83
|
-
/**
|
|
84
|
-
* List all project mappings
|
|
85
|
-
* @returns {Promise<Array>} Array of project mappings
|
|
86
|
-
*/
|
|
87
|
-
async listMappings() {
|
|
88
|
-
return projectOps.listMappings(mappingStore);
|
|
89
|
-
},
|
|
90
|
-
/**
|
|
91
|
-
* Get project mapping for a specific directory
|
|
92
|
-
* @param {string} directory - Directory path
|
|
93
|
-
* @returns {Promise<Object|null>} Project mapping or null
|
|
94
|
-
*/
|
|
95
|
-
async getMapping(directory) {
|
|
96
|
-
return projectOps.getMapping(mappingStore, directory);
|
|
97
|
-
},
|
|
98
|
-
/**
|
|
99
|
-
* Create or update project mapping
|
|
100
|
-
* @param {string} directory - Directory path
|
|
101
|
-
* @param {Object} projectData - Project data
|
|
102
|
-
* @returns {Promise<Object>} Created mapping
|
|
103
|
-
*/
|
|
104
|
-
async createMapping(directory, projectData) {
|
|
105
|
-
return projectOps.createMapping(mappingStore, directory, projectData);
|
|
106
|
-
},
|
|
107
|
-
/**
|
|
108
|
-
* Remove project mapping
|
|
109
|
-
* @param {string} directory - Directory path
|
|
110
|
-
* @returns {Promise<void>}
|
|
111
|
-
*/
|
|
112
|
-
async removeMapping(directory) {
|
|
113
|
-
return projectOps.removeMapping(mappingStore, directory);
|
|
114
|
-
},
|
|
115
67
|
/**
|
|
116
68
|
* Get recent builds for a project
|
|
117
69
|
* Returns empty array if not authenticated (projectOps handles null oauthClient)
|
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
3
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
4
4
|
import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
|
|
5
|
-
import { getAccessToken
|
|
5
|
+
import { getAccessToken } from './global-config.js';
|
|
6
6
|
import * as output from './output.js';
|
|
7
7
|
const DEFAULT_CONFIG = {
|
|
8
8
|
// API Configuration
|
|
@@ -73,29 +73,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
73
73
|
// Merge validated file config
|
|
74
74
|
mergeConfig(config, validatedFileConfig);
|
|
75
75
|
|
|
76
|
-
// 3.
|
|
77
|
-
if (!cliOverrides.token) {
|
|
78
|
-
const currentDir = process.cwd();
|
|
79
|
-
const projectMapping = await getProjectMapping(currentDir);
|
|
80
|
-
if (projectMapping?.token) {
|
|
81
|
-
// Handle both string tokens and token objects (backward compatibility)
|
|
82
|
-
let token;
|
|
83
|
-
if (typeof projectMapping.token === 'string') {
|
|
84
|
-
token = projectMapping.token;
|
|
85
|
-
} else if (typeof projectMapping.token === 'object' && projectMapping.token.token) {
|
|
86
|
-
// Handle nested token object from old API responses
|
|
87
|
-
token = projectMapping.token.token;
|
|
88
|
-
} else {
|
|
89
|
-
token = String(projectMapping.token);
|
|
90
|
-
}
|
|
91
|
-
config.apiKey = token;
|
|
92
|
-
config.projectSlug = projectMapping.projectSlug;
|
|
93
|
-
config.organizationSlug = projectMapping.organizationSlug;
|
|
94
|
-
output.debug('config', `linked to ${projectMapping.projectSlug} (${projectMapping.organizationSlug})`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 4. Override with environment variables (higher priority than fallbacks)
|
|
76
|
+
// 3. Override with environment variables (higher priority than fallbacks)
|
|
99
77
|
const envApiKey = getApiToken();
|
|
100
78
|
const envApiUrl = getApiUrl();
|
|
101
79
|
const envBuildName = getBuildName();
|
|
@@ -111,20 +89,19 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
111
89
|
}
|
|
112
90
|
if (envParallelId) config.parallelId = envParallelId;
|
|
113
91
|
|
|
114
|
-
//
|
|
92
|
+
// 4. Apply CLI overrides (highest priority)
|
|
115
93
|
if (cliOverrides.token) {
|
|
116
94
|
output.debug('config', 'using token from --token flag');
|
|
117
95
|
}
|
|
118
96
|
applyCLIOverrides(config, cliOverrides);
|
|
119
97
|
|
|
120
|
-
//
|
|
98
|
+
// 5. Fall back to user auth token if no other token found
|
|
121
99
|
// This enables interactive commands (builds, comparisons, approve, etc.)
|
|
122
100
|
// to work without a project token when the user is logged in
|
|
123
101
|
if (!config.apiKey) {
|
|
124
102
|
let userToken = await getAccessToken();
|
|
125
103
|
if (userToken) {
|
|
126
104
|
config.apiKey = userToken;
|
|
127
|
-
config.isUserAuth = true; // Flag to indicate this is user auth, not project token
|
|
128
105
|
output.debug('config', 'using token from user login');
|
|
129
106
|
}
|
|
130
107
|
}
|
package/dist/utils/context.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
|
-
import {
|
|
13
|
+
import { join } from 'node:path';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Get dynamic context about the current Vizzly state
|
|
@@ -63,20 +63,6 @@ export function getContext() {
|
|
|
63
63
|
// Ignore
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// Check for project mapping (from vizzly project:select)
|
|
67
|
-
// Traverse up to find project config, with bounds check for Windows compatibility
|
|
68
|
-
let projectMapping = null;
|
|
69
|
-
let checkPath = cwd;
|
|
70
|
-
let prevPath = null;
|
|
71
|
-
while (checkPath && checkPath !== prevPath) {
|
|
72
|
-
if (globalConfig.projects?.[checkPath]) {
|
|
73
|
-
projectMapping = globalConfig.projects[checkPath];
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
prevPath = checkPath;
|
|
77
|
-
checkPath = dirname(checkPath);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
66
|
// Check for OAuth login (from vizzly login)
|
|
81
67
|
let isLoggedIn = !!globalConfig.auth?.accessToken;
|
|
82
68
|
let userName = globalConfig.auth?.user?.name || globalConfig.auth?.user?.email;
|
|
@@ -92,13 +78,7 @@ export function getContext() {
|
|
|
92
78
|
value: `running on :${serverPort}`
|
|
93
79
|
});
|
|
94
80
|
}
|
|
95
|
-
if (
|
|
96
|
-
items.push({
|
|
97
|
-
type: 'success',
|
|
98
|
-
label: 'Project',
|
|
99
|
-
value: `${projectMapping.projectName} (${projectMapping.organizationSlug})`
|
|
100
|
-
});
|
|
101
|
-
} else if (isLoggedIn && userName) {
|
|
81
|
+
if (isLoggedIn && userName) {
|
|
102
82
|
items.push({
|
|
103
83
|
type: 'success',
|
|
104
84
|
label: 'Logged in',
|
|
@@ -114,7 +94,7 @@ export function getContext() {
|
|
|
114
94
|
items.push({
|
|
115
95
|
type: 'info',
|
|
116
96
|
label: 'Not connected',
|
|
117
|
-
value: 'run vizzly login
|
|
97
|
+
value: 'run vizzly login'
|
|
118
98
|
});
|
|
119
99
|
}
|
|
120
100
|
if (baselineCount > 0) {
|
|
@@ -153,8 +133,7 @@ export function getDetailedContext() {
|
|
|
153
133
|
port: null
|
|
154
134
|
},
|
|
155
135
|
project: {
|
|
156
|
-
hasConfig: false
|
|
157
|
-
mapping: null
|
|
136
|
+
hasConfig: false
|
|
158
137
|
},
|
|
159
138
|
auth: {
|
|
160
139
|
loggedIn: false,
|
|
@@ -204,19 +183,6 @@ export function getDetailedContext() {
|
|
|
204
183
|
// Ignore
|
|
205
184
|
}
|
|
206
185
|
|
|
207
|
-
// Check for project mapping
|
|
208
|
-
// Traverse up to find project config, with bounds check for Windows compatibility
|
|
209
|
-
let checkPath = cwd;
|
|
210
|
-
let prevPath = null;
|
|
211
|
-
while (checkPath && checkPath !== prevPath) {
|
|
212
|
-
if (globalConfig.projects?.[checkPath]) {
|
|
213
|
-
context.project.mapping = globalConfig.projects[checkPath];
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
prevPath = checkPath;
|
|
217
|
-
checkPath = dirname(checkPath);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
186
|
// Check auth status
|
|
221
187
|
context.auth.loggedIn = !!globalConfig.auth?.accessToken;
|
|
222
188
|
context.auth.userName = globalConfig.auth?.user?.name || globalConfig.auth?.user?.email;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import {
|
|
9
|
+
import { join } from 'node:path';
|
|
10
10
|
import * as output from './output.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -197,79 +197,4 @@ export async function getAccessToken() {
|
|
|
197
197
|
if (!valid) return null;
|
|
198
198
|
let auth = await getAuthTokens();
|
|
199
199
|
return auth?.accessToken || null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Get project mapping for a directory
|
|
204
|
-
* Walks up the directory tree to find the closest mapping
|
|
205
|
-
* @param {string} directoryPath - Absolute path to project directory
|
|
206
|
-
* @returns {Promise<Object|null>} Project data or null
|
|
207
|
-
*/
|
|
208
|
-
export async function getProjectMapping(directoryPath) {
|
|
209
|
-
const config = await loadGlobalConfig();
|
|
210
|
-
if (!config.projects) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Walk up the directory tree looking for a mapping
|
|
215
|
-
let currentPath = directoryPath;
|
|
216
|
-
const {
|
|
217
|
-
root
|
|
218
|
-
} = parse(currentPath);
|
|
219
|
-
while (currentPath !== root) {
|
|
220
|
-
if (config.projects[currentPath]) {
|
|
221
|
-
return config.projects[currentPath];
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Move to parent directory
|
|
225
|
-
const parentPath = dirname(currentPath);
|
|
226
|
-
if (parentPath === currentPath) {
|
|
227
|
-
// We've reached the root
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
currentPath = parentPath;
|
|
231
|
-
}
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Save project mapping for a directory
|
|
237
|
-
* @param {string} directoryPath - Absolute path to project directory
|
|
238
|
-
* @param {Object} projectData - Project configuration
|
|
239
|
-
* @param {string} projectData.token - Project API token (vzt_...)
|
|
240
|
-
* @param {string} projectData.projectSlug - Project slug
|
|
241
|
-
* @param {string} projectData.organizationSlug - Organization slug
|
|
242
|
-
* @param {string} projectData.projectName - Project name
|
|
243
|
-
*/
|
|
244
|
-
export async function saveProjectMapping(directoryPath, projectData) {
|
|
245
|
-
const config = await loadGlobalConfig();
|
|
246
|
-
if (!config.projects) {
|
|
247
|
-
config.projects = {};
|
|
248
|
-
}
|
|
249
|
-
config.projects[directoryPath] = {
|
|
250
|
-
...projectData,
|
|
251
|
-
createdAt: new Date().toISOString()
|
|
252
|
-
};
|
|
253
|
-
await saveGlobalConfig(config);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Get all project mappings
|
|
258
|
-
* @returns {Promise<Object>} Map of directory paths to project data
|
|
259
|
-
*/
|
|
260
|
-
export async function getProjectMappings() {
|
|
261
|
-
const config = await loadGlobalConfig();
|
|
262
|
-
return config.projects || {};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Delete project mapping for a directory
|
|
267
|
-
* @param {string} directoryPath - Absolute path to project directory
|
|
268
|
-
*/
|
|
269
|
-
export async function deleteProjectMapping(directoryPath) {
|
|
270
|
-
const config = await loadGlobalConfig();
|
|
271
|
-
if (config.projects?.[directoryPath]) {
|
|
272
|
-
delete config.projects[directoryPath];
|
|
273
|
-
await saveGlobalConfig(config);
|
|
274
|
-
}
|
|
275
200
|
}
|