@vizzly-testing/cli 0.16.4 → 0.18.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 +4 -4
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
- package/dist/cli.js +84 -58
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +18 -17
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +30 -30
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +27 -27
- package/dist/commands/upload.js +33 -33
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +12 -12
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +33 -33
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +101 -95
- package/dist/services/test-runner.js +14 -4
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +19 -19
- package/dist/utils/config-schema.js +10 -9
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +56 -27
- package/docs/doctor-command.md +1 -1
- package/docs/tdd-mode.md +3 -3
- package/package.json +9 -13
package/dist/commands/init.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
5
|
-
import * as output from '../utils/output.js';
|
|
6
6
|
import { loadPlugins } from '../plugin-loader.js';
|
|
7
7
|
import { loadConfig } from '../utils/config-loader.js';
|
|
8
|
-
import
|
|
8
|
+
import * as output from '../utils/output.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Simple configuration setup for Vizzly CLI
|
|
@@ -19,8 +19,8 @@ export class InitCommand {
|
|
|
19
19
|
output.blank();
|
|
20
20
|
try {
|
|
21
21
|
// Check for existing config
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const configPath = path.join(process.cwd(), 'vizzly.config.js');
|
|
23
|
+
const hasConfig = await this.fileExists(configPath);
|
|
24
24
|
if (hasConfig && !options.force) {
|
|
25
25
|
output.info('❌ A vizzly.config.js file already exists. Use --force to overwrite.');
|
|
26
26
|
return;
|
|
@@ -60,9 +60,9 @@ export class InitCommand {
|
|
|
60
60
|
timeout: 30000
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
-
// Comparison configuration
|
|
63
|
+
// Comparison configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
|
|
64
64
|
comparison: {
|
|
65
|
-
threshold: 0
|
|
65
|
+
threshold: 2.0
|
|
66
66
|
},
|
|
67
67
|
|
|
68
68
|
// TDD configuration
|
|
@@ -71,16 +71,16 @@ export class InitCommand {
|
|
|
71
71
|
}`;
|
|
72
72
|
|
|
73
73
|
// Add plugin configurations
|
|
74
|
-
|
|
74
|
+
const pluginConfigs = this.generatePluginConfigs();
|
|
75
75
|
if (pluginConfigs) {
|
|
76
|
-
coreConfig +=
|
|
76
|
+
coreConfig += `,\n\n${pluginConfigs}`;
|
|
77
77
|
}
|
|
78
78
|
coreConfig += '\n};\n';
|
|
79
79
|
await fs.writeFile(configPath, coreConfig, 'utf8');
|
|
80
80
|
output.info(`📄 Created vizzly.config.js`);
|
|
81
81
|
|
|
82
82
|
// Log discovered plugins
|
|
83
|
-
|
|
83
|
+
const pluginsWithConfig = this.plugins.filter(p => p.configSchema);
|
|
84
84
|
if (pluginsWithConfig.length > 0) {
|
|
85
85
|
output.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
86
86
|
pluginsWithConfig.forEach(p => {
|
|
@@ -94,10 +94,10 @@ export class InitCommand {
|
|
|
94
94
|
* @returns {string} Plugin config sections as formatted string
|
|
95
95
|
*/
|
|
96
96
|
generatePluginConfigs() {
|
|
97
|
-
|
|
98
|
-
for (
|
|
97
|
+
const sections = [];
|
|
98
|
+
for (const plugin of this.plugins) {
|
|
99
99
|
if (plugin.configSchema) {
|
|
100
|
-
|
|
100
|
+
const configStr = this.formatPluginConfig(plugin);
|
|
101
101
|
if (configStr) {
|
|
102
102
|
sections.push(configStr);
|
|
103
103
|
}
|
|
@@ -114,18 +114,18 @@ export class InitCommand {
|
|
|
114
114
|
formatPluginConfig(plugin) {
|
|
115
115
|
try {
|
|
116
116
|
// Validate config schema structure with Zod (defensive check)
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
|
|
118
|
+
const configSchemaValidator = z.record(configValueSchema);
|
|
119
119
|
configSchemaValidator.parse(plugin.configSchema);
|
|
120
|
-
|
|
121
|
-
for (
|
|
122
|
-
|
|
120
|
+
const configEntries = [];
|
|
121
|
+
for (const [key, value] of Object.entries(plugin.configSchema)) {
|
|
122
|
+
const formattedValue = this.formatValue(value, 1);
|
|
123
123
|
configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
|
|
124
124
|
}
|
|
125
125
|
return configEntries.join(',\n\n');
|
|
126
126
|
} catch (error) {
|
|
127
127
|
if (error instanceof z.ZodError) {
|
|
128
|
-
|
|
128
|
+
const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
129
129
|
output.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
130
130
|
} else {
|
|
131
131
|
output.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
@@ -141,25 +141,25 @@ export class InitCommand {
|
|
|
141
141
|
* @returns {string} Formatted value
|
|
142
142
|
*/
|
|
143
143
|
formatValue(value, depth = 0) {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
const indent = ' '.repeat(depth);
|
|
145
|
+
const nextIndent = ' '.repeat(depth + 1);
|
|
146
146
|
if (value === null) return 'null';
|
|
147
147
|
if (value === undefined) return 'undefined';
|
|
148
148
|
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
|
|
149
149
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
150
150
|
if (Array.isArray(value)) {
|
|
151
151
|
if (value.length === 0) return '[]';
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const items = value.map(item => {
|
|
153
|
+
const formatted = this.formatValue(item, depth + 1);
|
|
154
154
|
return `${nextIndent}${formatted}`;
|
|
155
155
|
});
|
|
156
156
|
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
157
157
|
}
|
|
158
158
|
if (typeof value === 'object') {
|
|
159
|
-
|
|
159
|
+
const entries = Object.entries(value);
|
|
160
160
|
if (entries.length === 0) return '{}';
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
const props = entries.map(([k, v]) => {
|
|
162
|
+
const formatted = this.formatValue(v, depth + 1);
|
|
163
163
|
return `${nextIndent}${k}: ${formatted}`;
|
|
164
164
|
});
|
|
165
165
|
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
@@ -188,7 +188,7 @@ export class InitCommand {
|
|
|
188
188
|
|
|
189
189
|
// Export factory function for CLI
|
|
190
190
|
export function createInitCommand(options) {
|
|
191
|
-
|
|
191
|
+
const command = new InitCommand(options.plugins);
|
|
192
192
|
return () => command.run(options);
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -204,7 +204,7 @@ export async function init(options = {}) {
|
|
|
204
204
|
// Try to load plugins if not provided
|
|
205
205
|
if (!options.plugins) {
|
|
206
206
|
try {
|
|
207
|
-
|
|
207
|
+
const config = await loadConfig(options.config, {});
|
|
208
208
|
plugins = await loadPlugins(options.config, config, null);
|
|
209
209
|
} catch {
|
|
210
210
|
// Silent fail - plugins are optional for init
|
|
@@ -212,6 +212,6 @@ export async function init(options = {}) {
|
|
|
212
212
|
} else {
|
|
213
213
|
plugins = options.plugins;
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
const command = new InitCommand(plugins);
|
|
216
216
|
return await command.run(options);
|
|
217
217
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Authenticates user via OAuth device flow
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as output from '../utils/output.js';
|
|
7
6
|
import { AuthService } from '../services/auth-service.js';
|
|
8
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
7
|
import { openBrowser } from '../utils/browser.js';
|
|
8
|
+
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
|
+
import * as output from '../utils/output.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Login command implementation using OAuth device flow
|
|
@@ -19,31 +19,31 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
19
19
|
verbose: globalOptions.verbose,
|
|
20
20
|
color: !globalOptions.noColor
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
const colors = output.getColors();
|
|
23
23
|
try {
|
|
24
24
|
output.info('Starting Vizzly authentication...');
|
|
25
25
|
output.blank();
|
|
26
26
|
|
|
27
27
|
// Create auth service
|
|
28
|
-
|
|
28
|
+
const authService = new AuthService({
|
|
29
29
|
baseUrl: options.apiUrl || getApiUrl()
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
// Initiate device flow
|
|
33
33
|
output.startSpinner('Connecting to Vizzly...');
|
|
34
|
-
|
|
34
|
+
const deviceFlow = await authService.initiateDeviceFlow();
|
|
35
35
|
output.stopSpinner();
|
|
36
36
|
|
|
37
37
|
// Handle both snake_case and camelCase field names
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
|
|
39
|
+
const userCode = deviceFlow.user_code || deviceFlow.userCode;
|
|
40
|
+
const deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
|
|
41
41
|
if (!verificationUri || !userCode || !deviceCode) {
|
|
42
42
|
throw new Error('Invalid device flow response from server');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Build URL with pre-filled code
|
|
46
|
-
|
|
46
|
+
const urlWithCode = `${verificationUri}?code=${userCode}`;
|
|
47
47
|
|
|
48
48
|
// Display user code prominently
|
|
49
49
|
output.blank();
|
|
@@ -61,7 +61,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
61
61
|
output.blank();
|
|
62
62
|
|
|
63
63
|
// Try to open browser with pre-filled code
|
|
64
|
-
|
|
64
|
+
const browserOpened = await openBrowser(urlWithCode);
|
|
65
65
|
if (browserOpened) {
|
|
66
66
|
output.info('Opening browser...');
|
|
67
67
|
} else {
|
|
@@ -83,12 +83,12 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
83
83
|
|
|
84
84
|
// Check authorization status
|
|
85
85
|
output.startSpinner('Checking authorization status...');
|
|
86
|
-
|
|
86
|
+
const pollResponse = await authService.pollDeviceAuthorization(deviceCode);
|
|
87
87
|
output.stopSpinner();
|
|
88
88
|
let tokenData = null;
|
|
89
89
|
|
|
90
90
|
// Check if authorization was successful by looking for tokens
|
|
91
|
-
if (pollResponse.tokens
|
|
91
|
+
if (pollResponse.tokens?.accessToken) {
|
|
92
92
|
// Success! We got tokens
|
|
93
93
|
tokenData = pollResponse;
|
|
94
94
|
} else if (pollResponse.status === 'pending') {
|
|
@@ -103,10 +103,10 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
103
103
|
|
|
104
104
|
// Complete device flow and save tokens
|
|
105
105
|
// Handle both snake_case and camelCase for token data, and nested tokens object
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const tokensData = tokenData.tokens || tokenData;
|
|
107
|
+
const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
108
|
+
const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
|
|
109
|
+
const tokens = {
|
|
110
110
|
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
111
111
|
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
112
112
|
expiresAt: tokenExpiresAt,
|
|
@@ -129,7 +129,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
129
129
|
if (tokens.organizations && tokens.organizations.length > 0) {
|
|
130
130
|
output.blank();
|
|
131
131
|
output.info('Organizations:');
|
|
132
|
-
for (
|
|
132
|
+
for (const org of tokens.organizations) {
|
|
133
133
|
output.print(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -137,11 +137,11 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
137
137
|
// Show token expiry info
|
|
138
138
|
if (tokens.expiresAt) {
|
|
139
139
|
output.blank();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const expiresAt = new Date(tokens.expiresAt);
|
|
141
|
+
const msUntilExpiry = expiresAt.getTime() - Date.now();
|
|
142
|
+
const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
143
|
+
const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
144
|
+
const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
145
145
|
if (daysUntilExpiry > 0) {
|
|
146
146
|
output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
|
|
147
147
|
} else if (hoursUntilExpiry > 0) {
|
|
@@ -180,7 +180,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
180
180
|
* @param {Object} options - Command options
|
|
181
181
|
*/
|
|
182
182
|
export function validateLoginOptions() {
|
|
183
|
-
|
|
183
|
+
const errors = [];
|
|
184
184
|
|
|
185
185
|
// No specific validation needed for login command
|
|
186
186
|
// OAuth device flow handles everything via browser
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Clears stored authentication tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as output from '../utils/output.js';
|
|
7
6
|
import { AuthService } from '../services/auth-service.js';
|
|
8
7
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
8
|
import { getAuthTokens } from '../utils/global-config.js';
|
|
9
|
+
import * as output from '../utils/output.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Logout command implementation
|
|
@@ -21,7 +21,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
21
21
|
});
|
|
22
22
|
try {
|
|
23
23
|
// Check if user is logged in
|
|
24
|
-
|
|
24
|
+
const auth = await getAuthTokens();
|
|
25
25
|
if (!auth || !auth.accessToken) {
|
|
26
26
|
output.info('You are not logged in');
|
|
27
27
|
output.cleanup();
|
|
@@ -30,7 +30,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
30
30
|
|
|
31
31
|
// Logout
|
|
32
32
|
output.startSpinner('Logging out...');
|
|
33
|
-
|
|
33
|
+
const authService = new AuthService({
|
|
34
34
|
baseUrl: options.apiUrl || getApiUrl()
|
|
35
35
|
});
|
|
36
36
|
await authService.logout();
|
|
@@ -58,7 +58,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
58
58
|
* @param {Object} options - Command options
|
|
59
59
|
*/
|
|
60
60
|
export function validateLogoutOptions() {
|
|
61
|
-
|
|
61
|
+
const errors = [];
|
|
62
62
|
|
|
63
63
|
// No specific validation needed for logout command
|
|
64
64
|
|
package/dist/commands/project.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Select, list, and manage project tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import readline from 'node:readline';
|
|
7
8
|
import { AuthService } from '../services/auth-service.js';
|
|
8
9
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import readline from 'readline';
|
|
10
|
+
import { deleteProjectMapping, getAuthTokens, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
|
|
11
|
+
import * as output from '../utils/output.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Project select command - configure project for current directory
|
|
@@ -23,20 +23,20 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
23
23
|
});
|
|
24
24
|
try {
|
|
25
25
|
// Check authentication
|
|
26
|
-
|
|
26
|
+
const auth = await getAuthTokens();
|
|
27
27
|
if (!auth || !auth.accessToken) {
|
|
28
28
|
output.error('Not authenticated');
|
|
29
29
|
output.blank();
|
|
30
30
|
output.info('Run "vizzly login" to authenticate first');
|
|
31
31
|
process.exit(1);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const authService = new AuthService({
|
|
34
34
|
baseUrl: options.apiUrl || getApiUrl()
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// Get user info to show organizations
|
|
38
38
|
output.startSpinner('Fetching organizations...');
|
|
39
|
-
|
|
39
|
+
const userInfo = await authService.whoami();
|
|
40
40
|
output.stopSpinner();
|
|
41
41
|
if (!userInfo.organizations || userInfo.organizations.length === 0) {
|
|
42
42
|
output.error('No organizations found');
|
|
@@ -53,12 +53,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
53
53
|
output.print(` ${index + 1}. ${org.name} (@${org.slug})`);
|
|
54
54
|
});
|
|
55
55
|
output.blank();
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
|
|
57
|
+
const selectedOrg = userInfo.organizations[orgChoice - 1];
|
|
58
58
|
|
|
59
59
|
// List projects for organization
|
|
60
60
|
output.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
|
|
61
|
-
|
|
61
|
+
const response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
|
|
62
62
|
headers: {
|
|
63
63
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
64
64
|
'X-Organization': selectedOrg.slug
|
|
@@ -67,7 +67,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
67
67
|
output.stopSpinner();
|
|
68
68
|
|
|
69
69
|
// Handle both array response and object with projects property
|
|
70
|
-
|
|
70
|
+
const projects = Array.isArray(response) ? response : response.projects || [];
|
|
71
71
|
if (projects.length === 0) {
|
|
72
72
|
output.error('No projects found');
|
|
73
73
|
output.blank();
|
|
@@ -83,12 +83,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
83
83
|
output.print(` ${index + 1}. ${project.name} (${project.slug})`);
|
|
84
84
|
});
|
|
85
85
|
output.blank();
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const projectChoice = await promptNumber('Enter number', 1, projects.length);
|
|
87
|
+
const selectedProject = projects[projectChoice - 1];
|
|
88
88
|
|
|
89
89
|
// Create API token for project
|
|
90
90
|
output.startSpinner(`Creating API token for ${selectedProject.name}...`);
|
|
91
|
-
|
|
91
|
+
const tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
|
|
92
92
|
method: 'POST',
|
|
93
93
|
headers: {
|
|
94
94
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
@@ -103,7 +103,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
103
103
|
output.stopSpinner();
|
|
104
104
|
|
|
105
105
|
// Save project mapping
|
|
106
|
-
|
|
106
|
+
const currentDir = resolve(process.cwd());
|
|
107
107
|
await saveProjectMapping(currentDir, {
|
|
108
108
|
token: tokenResponse.token,
|
|
109
109
|
projectSlug: selectedProject.slug,
|
|
@@ -135,8 +135,8 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
135
135
|
color: !globalOptions.noColor
|
|
136
136
|
});
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const mappings = await getProjectMappings();
|
|
139
|
+
const paths = Object.keys(mappings);
|
|
140
140
|
if (paths.length === 0) {
|
|
141
141
|
output.info('No projects configured');
|
|
142
142
|
output.blank();
|
|
@@ -151,14 +151,14 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
output.info('Configured projects:');
|
|
153
153
|
output.blank();
|
|
154
|
-
|
|
155
|
-
for (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
const currentDir = resolve(process.cwd());
|
|
155
|
+
for (const path of paths) {
|
|
156
|
+
const mapping = mappings[path];
|
|
157
|
+
const isCurrent = path === currentDir;
|
|
158
|
+
const marker = isCurrent ? '→' : ' ';
|
|
159
159
|
|
|
160
160
|
// Extract token string (handle both string and object formats)
|
|
161
|
-
|
|
161
|
+
const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
162
162
|
output.print(`${marker} ${path}`);
|
|
163
163
|
output.print(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
|
|
164
164
|
output.print(` Organization: ${mapping.organizationSlug}`);
|
|
@@ -187,8 +187,8 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
187
187
|
color: !globalOptions.noColor
|
|
188
188
|
});
|
|
189
189
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
const currentDir = resolve(process.cwd());
|
|
191
|
+
const mapping = await getProjectMapping(currentDir);
|
|
192
192
|
if (!mapping) {
|
|
193
193
|
output.error('No project configured for this directory');
|
|
194
194
|
output.blank();
|
|
@@ -197,7 +197,7 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Extract token string (handle both string and object formats)
|
|
200
|
-
|
|
200
|
+
const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
201
201
|
if (globalOptions.json) {
|
|
202
202
|
output.data({
|
|
203
203
|
token: tokenStr,
|
|
@@ -224,11 +224,11 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
224
224
|
* Helper to make authenticated API request
|
|
225
225
|
*/
|
|
226
226
|
async function makeAuthenticatedRequest(url, options = {}) {
|
|
227
|
-
|
|
227
|
+
const response = await fetch(url, options);
|
|
228
228
|
if (!response.ok) {
|
|
229
229
|
let errorText = '';
|
|
230
230
|
try {
|
|
231
|
-
|
|
231
|
+
const errorData = await response.json();
|
|
232
232
|
errorText = errorData.error || errorData.message || '';
|
|
233
233
|
} catch {
|
|
234
234
|
errorText = await response.text();
|
|
@@ -243,14 +243,14 @@ async function makeAuthenticatedRequest(url, options = {}) {
|
|
|
243
243
|
*/
|
|
244
244
|
function promptNumber(message, min, max) {
|
|
245
245
|
return new Promise(resolve => {
|
|
246
|
-
|
|
246
|
+
const rl = readline.createInterface({
|
|
247
247
|
input: process.stdin,
|
|
248
248
|
output: process.stdout
|
|
249
249
|
});
|
|
250
|
-
|
|
250
|
+
const ask = () => {
|
|
251
251
|
rl.question(`${message} (${min}-${max}): `, answer => {
|
|
252
|
-
|
|
253
|
-
if (isNaN(num) || num < min || num > max) {
|
|
252
|
+
const num = parseInt(answer, 10);
|
|
253
|
+
if (Number.isNaN(num) || num < min || num > max) {
|
|
254
254
|
output.print(`Please enter a number between ${min} and ${max}`);
|
|
255
255
|
ask();
|
|
256
256
|
} else {
|
|
@@ -275,8 +275,8 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
275
275
|
color: !globalOptions.noColor
|
|
276
276
|
});
|
|
277
277
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
const currentDir = resolve(process.cwd());
|
|
279
|
+
const mapping = await getProjectMapping(currentDir);
|
|
280
280
|
if (!mapping) {
|
|
281
281
|
output.info('No project configured for this directory');
|
|
282
282
|
output.cleanup();
|
|
@@ -290,7 +290,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
290
290
|
output.print(` Organization: ${mapping.organizationSlug}`);
|
|
291
291
|
output.print(` Directory: ${currentDir}`);
|
|
292
292
|
output.blank();
|
|
293
|
-
|
|
293
|
+
const confirmed = await promptConfirm('Remove this project configuration?');
|
|
294
294
|
if (!confirmed) {
|
|
295
295
|
output.info('Cancelled');
|
|
296
296
|
output.cleanup();
|
|
@@ -312,7 +312,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
312
312
|
*/
|
|
313
313
|
function promptConfirm(message) {
|
|
314
314
|
return new Promise(resolve => {
|
|
315
|
-
|
|
315
|
+
const rl = readline.createInterface({
|
|
316
316
|
input: process.stdin,
|
|
317
317
|
output: process.stdout
|
|
318
318
|
});
|