@vizzly-testing/cli 0.38.0 → 0.38.1

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.
@@ -3,9 +3,10 @@
3
3
  * Handles authentication endpoints (device flow login, logout, status)
4
4
  */
5
5
 
6
+ import { buildTokens, normalizeDeviceFlowResponse } from '../../auth/core.js';
6
7
  import * as output from '../../utils/output.js';
7
8
  import { parseJsonBody } from '../middleware/json-parser.js';
8
- import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
9
+ import { sendError, sendJson, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
9
10
 
10
11
  /**
11
12
  * Create auth router
@@ -50,22 +51,20 @@ export function createAuthRouter({
50
51
  // Initiate device flow login
51
52
  if (req.method === 'POST' && pathname === '/api/auth/login') {
52
53
  try {
53
- const deviceFlow = await authService.initiateDeviceFlow();
54
-
55
- // Transform snake_case to camelCase for frontend
56
- const response = {
57
- deviceCode: deviceFlow.device_code,
58
- userCode: deviceFlow.user_code,
59
- verificationUri: deviceFlow.verification_uri,
60
- verificationUriComplete: deviceFlow.verification_uri_complete,
61
- expiresIn: deviceFlow.expires_in,
54
+ let deviceFlow = await authService.initiateDeviceFlow();
55
+ let flow = normalizeDeviceFlowResponse(deviceFlow);
56
+ sendSuccess(res, {
57
+ deviceCode: flow.deviceCode,
58
+ userCode: flow.userCode,
59
+ verificationUri: flow.verificationUri,
60
+ verificationUriComplete: flow.urlWithCode,
61
+ expiresIn: deviceFlow.expires_in ?? deviceFlow.expiresIn,
62
62
  interval: deviceFlow.interval
63
- };
64
- sendSuccess(res, response);
63
+ });
65
64
  return true;
66
65
  } catch (error) {
67
66
  output.error('Error initiating device flow:', error);
68
- sendError(res, 500, error.message);
67
+ sendAuthError(res, error);
69
68
  return true;
70
69
  }
71
70
  }
@@ -81,45 +80,25 @@ export function createAuthRouter({
81
80
  sendError(res, 400, 'deviceCode is required');
82
81
  return true;
83
82
  }
84
- let result;
85
- try {
86
- result = await authService.pollDeviceAuthorization(deviceCode);
87
- } catch (error) {
88
- // Handle "Authorization pending" as a valid response
89
- if (error.message?.includes('Authorization pending')) {
90
- sendSuccess(res, {
91
- status: 'pending'
92
- });
93
- return true;
94
- }
95
- throw error;
96
- }
97
-
98
- // Check if authorization is complete by looking for tokens
99
- if (result.tokens?.accessToken) {
100
- const tokensData = result.tokens;
101
- const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
102
- const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
103
- const tokens = {
104
- accessToken: tokensData.accessToken || tokensData.access_token,
105
- refreshToken: tokensData.refreshToken || tokensData.refresh_token,
106
- expiresAt: tokenExpiresAt,
107
- user: result.user
108
- };
83
+ let result = await authService.pollDeviceAuthorization(deviceCode);
84
+ if (result.tokens?.accessToken || result.tokens?.access_token) {
85
+ let tokens = buildTokens(result);
109
86
  await authService.completeDeviceFlow(tokens);
110
87
  sendSuccess(res, {
111
88
  status: 'complete',
112
89
  user: result.user
113
90
  });
114
- } else {
91
+ } else if (['pending', 'expired', 'denied', 'invalid'].includes(result.status)) {
115
92
  sendSuccess(res, {
116
- status: 'pending'
93
+ status: result.status
117
94
  });
95
+ } else {
96
+ throw new Error('Unexpected response from authorization server');
118
97
  }
119
98
  return true;
120
99
  } catch (error) {
121
100
  output.error('Error polling device authorization:', error);
122
- sendError(res, 500, error.message);
101
+ sendAuthError(res, error);
123
102
  return true;
124
103
  }
125
104
  }
@@ -135,10 +114,21 @@ export function createAuthRouter({
135
114
  return true;
136
115
  } catch (error) {
137
116
  output.error('Error logging out:', error);
138
- sendError(res, 500, error.message);
117
+ sendAuthError(res, error);
139
118
  return true;
140
119
  }
141
120
  }
142
121
  return false;
143
122
  };
123
+ }
124
+ function sendAuthError(res, error) {
125
+ let retryAfter = error.context?.retryAfter;
126
+ if (retryAfter) res.setHeader('Retry-After', String(retryAfter));
127
+ sendJson(res, error.context?.status || 500, {
128
+ error: error.message,
129
+ code: error.context?.serverCode || error.code,
130
+ ...(retryAfter ? {
131
+ retryAfter
132
+ } : {})
133
+ });
144
134
  }
@@ -52,7 +52,13 @@ export async function openBrowser(url) {
52
52
  args = [url];
53
53
  break;
54
54
  }
55
- execFile(command, args, error => {
55
+
56
+ // Some launchers stay in the foreground. Bound the attempt so login and
57
+ // other CLI workflows can continue using the displayed URL.
58
+ execFile(command, args, {
59
+ timeout: 5000,
60
+ killSignal: 'SIGKILL'
61
+ }, error => {
56
62
  if (error) {
57
63
  // Browser opening failed, but don't throw - user can manually open
58
64
  resolve(false);
@@ -0,0 +1,50 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+
3
+ /** Wait for the next check, with optional terminal input and a live countdown. */
4
+ export async function waitForLoginPoll({
5
+ milliseconds,
6
+ manualAllowed = true,
7
+ earliestCheckAt = 0,
8
+ stdin = process.stdin,
9
+ now = Date.now,
10
+ sleep = delay,
11
+ onTick
12
+ }) {
13
+ if (!stdin.isTTY) {
14
+ onTick(milliseconds, false);
15
+ await sleep(milliseconds);
16
+ return;
17
+ }
18
+ let deadline = now() + milliseconds;
19
+ let requested = false;
20
+ let timer;
21
+ let wasFlowing = stdin.readableFlowing === true;
22
+ let onData = data => {
23
+ if (manualAllowed && /[\r\n]/.test(data.toString())) {
24
+ requested = true;
25
+ timer?.abort();
26
+ }
27
+ };
28
+ if (manualAllowed) {
29
+ stdin.on('data', onData);
30
+ stdin.resume();
31
+ }
32
+ try {
33
+ while (true) {
34
+ let remaining = Math.min(deadline, requested ? earliestCheckAt : deadline) - now();
35
+ if (remaining <= 0) return;
36
+ onTick(remaining, manualAllowed && !requested);
37
+ timer = new AbortController();
38
+ try {
39
+ await sleep(Math.min(remaining, 1000), undefined, {
40
+ signal: timer.signal
41
+ });
42
+ } catch (error) {
43
+ if (!timer.signal.aborted) throw error;
44
+ }
45
+ }
46
+ } finally {
47
+ stdin.removeListener('data', onData);
48
+ if (manualAllowed && !wasFlowing) stdin.pause();
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -87,24 +87,24 @@
87
87
  "fix": "pnpm run format && pnpm run lint:fix"
88
88
  },
89
89
  "engines": {
90
- "node": ">=22.0.0"
90
+ "node": "^22.18.0 || >=24.0.0"
91
91
  },
92
92
  "publishConfig": {
93
93
  "access": "public",
94
94
  "registry": "https://registry.npmjs.org/"
95
95
  },
96
96
  "dependencies": {
97
- "@vizzly-testing/bear-den": "0.1.4",
97
+ "@vizzly-testing/bear-den": "0.1.6",
98
98
  "@vizzly-testing/honeydiff": "^0.17.0",
99
- "ansis": "^4.2.0",
99
+ "ansis": "^4.4.0",
100
100
  "commander": "^15.0.0",
101
- "cosmiconfig": "^9.0.0",
102
- "dotenv": "^17.2.1",
103
- "form-data": "^4.0.0",
104
- "glob": "^13.0.0",
105
- "react": "^19.1.1",
106
- "react-dom": "^19.1.1",
107
- "zod": "^4.1.12"
101
+ "cosmiconfig": "^10.0.1",
102
+ "dotenv": "^17.4.2",
103
+ "form-data": "^4.0.6",
104
+ "glob": "^13.0.6",
105
+ "react": "^19.3.0",
106
+ "react-dom": "^19.3.0",
107
+ "zod": "^4.6.5"
108
108
  },
109
109
  "tsd": {
110
110
  "directory": "test-d",
@@ -117,29 +117,29 @@
117
117
  }
118
118
  },
119
119
  "devDependencies": {
120
- "@babel/cli": "^8.0.1",
121
- "@babel/core": "^8.0.1",
122
- "@babel/preset-env": "^8.0.2",
120
+ "@babel/cli": "^8.0.6",
121
+ "@babel/core": "^8.0.6",
122
+ "@babel/preset-env": "^8.0.6",
123
123
  "@babel/preset-react": "^8.0.1",
124
124
  "@babel/preset-typescript": "^8.0.1",
125
- "@biomejs/biome": "^2.5.10",
125
+ "@biomejs/biome": "^2.5.15",
126
126
  "@heroicons/react": "^2.2.0",
127
- "@modelcontextprotocol/sdk": "^1.0.4",
128
- "@playwright/test": "^1.55.1",
129
- "@tailwindcss/postcss": "^4.1.13",
130
- "@tanstack/react-query": "^5.90.11",
131
- "@types/node": "^26.0.0",
132
- "@vitejs/plugin-react": "^6.0.1",
133
- "autoprefixer": "^10.4.21",
127
+ "@modelcontextprotocol/sdk": "^1.31.0",
128
+ "@playwright/test": "^1.63.0",
129
+ "@tailwindcss/postcss": "^4.3.3",
130
+ "@tanstack/react-query": "^5.104.0",
131
+ "@types/node": "^26.6.3",
132
+ "@vitejs/plugin-react": "^6.1.2",
133
+ "autoprefixer": "^10.6.1",
134
134
  "babel-plugin-react-compiler": "^1.0.0",
135
135
  "babel-plugin-transform-remove-console": "^6.9.4",
136
- "postcss": "^8.5.6",
137
- "rimraf": "^6.0.1",
138
- "tailwindcss": "^4.1.13",
136
+ "postcss": "^8.5.29",
137
+ "rimraf": "^6.1.3",
138
+ "tailwindcss": "^4.3.3",
139
139
  "tsd": "^0.33.0",
140
140
  "tui-driver": "github:vizzly-testing/tui-driver",
141
141
  "typescript": "^7.0.2",
142
- "vite": "^8.2.2",
143
- "wouter": "^3.7.1"
142
+ "vite": "^8.3.1",
143
+ "wouter": "^3.11.1"
144
144
  }
145
145
  }