@empiricalrun/test-gen 0.66.2 → 0.67.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.67.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 267b012: feat: add user login in test-gen cli
8
+
3
9
  ## 0.66.2
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,12 @@
1
+ declare class APIClient {
2
+ private appUrl;
3
+ constructor();
4
+ request(endpoint: string, options?: RequestInit): Promise<Response>;
5
+ private makeRequest;
6
+ private ensureAuthenticated;
7
+ private refreshToken;
8
+ }
9
+ export declare const apiClient: APIClient;
10
+ export { APIClient };
11
+ export declare function authenticatedFetch(endpoint: string, options?: RequestInit): Promise<any>;
12
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/auth/api-client.ts"],"names":[],"mappings":"AAOA,cAAM,SAAS;IACb,OAAO,CAAC,MAAM,CAAS;;IAMjB,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC;YAoCN,WAAW;YAmBX,mBAAmB;YAiBnB,YAAY;CA4C3B;AAED,eAAO,MAAM,SAAS,WAAkB,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,GAAG,CAAC,CAqCd"}
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APIClient = exports.apiClient = void 0;
4
+ exports.authenticatedFetch = authenticatedFetch;
5
+ const token_store_1 = require("./token-store");
6
+ class APIClient {
7
+ appUrl;
8
+ constructor() {
9
+ this.appUrl = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
10
+ }
11
+ async request(endpoint, options = {}) {
12
+ // Ensure user is authenticated
13
+ await this.ensureAuthenticated();
14
+ const tokens = await (0, token_store_1.getStoredTokens)();
15
+ if (!tokens) {
16
+ throw new Error("Not authenticated. Please run the login command first.");
17
+ }
18
+ // Make the request with the access token
19
+ const response = await this.makeRequest(endpoint, options, tokens.access_token);
20
+ // If token is expired, try refreshing and retry once
21
+ if (response.status === 401) {
22
+ console.log("Access token expired, attempting to refresh...");
23
+ const refreshed = await this.refreshToken();
24
+ if (refreshed) {
25
+ const newTokens = await (0, token_store_1.getStoredTokens)();
26
+ if (newTokens) {
27
+ return this.makeRequest(endpoint, options, newTokens.access_token);
28
+ }
29
+ }
30
+ throw new Error("Authentication failed. Please run the login command again.");
31
+ }
32
+ return response;
33
+ }
34
+ async makeRequest(endpoint, options, accessToken) {
35
+ const url = endpoint.startsWith("http")
36
+ ? endpoint
37
+ : `${this.appUrl}/api${endpoint}`;
38
+ return fetch(url, {
39
+ ...options,
40
+ headers: {
41
+ Authorization: `Bearer ${accessToken}`,
42
+ "Content-Type": "application/json",
43
+ ...options.headers,
44
+ },
45
+ });
46
+ }
47
+ async ensureAuthenticated() {
48
+ if (!(await (0, token_store_1.isAuthenticated)())) {
49
+ const tokens = await (0, token_store_1.getStoredTokens)();
50
+ if (!tokens) {
51
+ throw new Error("Not authenticated. Please run the login command first.");
52
+ }
53
+ // Token exists but is expired, try to refresh
54
+ const refreshed = await this.refreshToken();
55
+ if (!refreshed) {
56
+ throw new Error("Session expired. Please run the login command again.");
57
+ }
58
+ }
59
+ }
60
+ async refreshToken() {
61
+ const tokens = await (0, token_store_1.getStoredTokens)();
62
+ if (!tokens || !tokens.refresh_token) {
63
+ return false;
64
+ }
65
+ try {
66
+ // Use dashboard refresh endpoint instead of Supabase directly
67
+ const response = await fetch(`${this.appUrl}/api/cli/refresh`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ },
72
+ body: JSON.stringify({
73
+ refresh_token: tokens.refresh_token,
74
+ }),
75
+ });
76
+ if (!response.ok) {
77
+ const errorText = await response.text();
78
+ console.error("Token refresh failed:", errorText);
79
+ await (0, token_store_1.clearTokens)();
80
+ return false;
81
+ }
82
+ const data = await response.json();
83
+ // Store the new tokens (refresh token rotation means we get a new refresh token)
84
+ await (0, token_store_1.storeTokens)({
85
+ access_token: data.access_token,
86
+ refresh_token: data.refresh_token,
87
+ expires_at: data.expires_at,
88
+ user_id: data.user?.id || tokens.user_id,
89
+ user_email: data.user?.email || tokens.user_email,
90
+ });
91
+ console.log("Access token refreshed successfully via dashboard");
92
+ return true;
93
+ }
94
+ catch (error) {
95
+ console.error("Token refresh error:", error);
96
+ await (0, token_store_1.clearTokens)();
97
+ return false;
98
+ }
99
+ }
100
+ }
101
+ exports.APIClient = APIClient;
102
+ exports.apiClient = new APIClient();
103
+ async function authenticatedFetch(endpoint, options) {
104
+ try {
105
+ const response = await exports.apiClient.request(endpoint, options);
106
+ if (!response.ok) {
107
+ const errorText = await response.text();
108
+ let errorMessage;
109
+ try {
110
+ const errorJson = JSON.parse(errorText);
111
+ errorMessage =
112
+ errorJson.error || errorJson.message || "API request failed";
113
+ }
114
+ catch {
115
+ errorMessage =
116
+ errorText || `Request failed with status ${response.status}`;
117
+ }
118
+ throw new Error(errorMessage);
119
+ }
120
+ const contentType = response.headers.get("content-type");
121
+ if (contentType && contentType.includes("application/json")) {
122
+ return response.json();
123
+ }
124
+ return response.text();
125
+ }
126
+ catch (error) {
127
+ if (error.message.includes("Not authenticated") ||
128
+ error.message.includes("Please run the login command")) {
129
+ throw new Error("Authentication required. Please run: npx @empiricalrun/test-gen login");
130
+ }
131
+ throw error;
132
+ }
133
+ }
@@ -0,0 +1,18 @@
1
+ export interface AuthResult {
2
+ success: boolean;
3
+ user?: {
4
+ id: string;
5
+ email: string;
6
+ };
7
+ error?: string;
8
+ }
9
+ export declare function authenticate(): Promise<AuthResult>;
10
+ export declare function logout(): Promise<void>;
11
+ export declare function getAuthStatus(): Promise<{
12
+ authenticated: boolean;
13
+ user?: {
14
+ id: string;
15
+ email: string;
16
+ };
17
+ }>;
18
+ //# sourceMappingURL=cli-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-auth.d.ts","sourceRoot":"","sources":["../../src/auth/cli-auth.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC,CAmKxD;AAED,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC;IAC7C,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC,CAAC,CAeD"}
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.authenticate = authenticate;
7
+ exports.logout = logout;
8
+ exports.getAuthStatus = getAuthStatus;
9
+ const detect_port_1 = __importDefault(require("detect-port"));
10
+ const http_1 = require("http");
11
+ const open_1 = __importDefault(require("open"));
12
+ const url_1 = require("url");
13
+ const token_store_1 = require("./token-store");
14
+ const CLIENT_PORT_DEFAULT = 8080;
15
+ // Get dashboard URL
16
+ const getDashboardUrl = () => {
17
+ return process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
18
+ };
19
+ async function authenticate() {
20
+ // Check if already authenticated
21
+ if (await (0, token_store_1.isAuthenticated)()) {
22
+ const tokens = await (0, token_store_1.getStoredTokens)();
23
+ if (tokens) {
24
+ return {
25
+ success: true,
26
+ user: {
27
+ id: tokens.user_id || "",
28
+ email: tokens.user_email || "",
29
+ },
30
+ };
31
+ }
32
+ }
33
+ const appUrl = getDashboardUrl();
34
+ const clientPort = await (0, detect_port_1.default)(CLIENT_PORT_DEFAULT);
35
+ const redirectUri = `http://localhost:${clientPort}/callback`;
36
+ return new Promise((resolve) => {
37
+ // Create temporary local server to receive callback
38
+ const server = (0, http_1.createServer)(async (req, res) => {
39
+ const { pathname, query } = (0, url_1.parse)(req.url, true);
40
+ if (pathname === "/callback") {
41
+ const { code, error } = query;
42
+ if (error) {
43
+ res.writeHead(400, { "Content-Type": "text/html" });
44
+ res.end(`
45
+ <html>
46
+ <head><title>Authentication Failed</title></head>
47
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
48
+ <h1 style="color: #e74c3c;">Authentication Failed</h1>
49
+ <p>Error: ${error}</p>
50
+ <p>You can close this window and try again.</p>
51
+ </body>
52
+ </html>
53
+ `);
54
+ server.close();
55
+ resolve({
56
+ success: false,
57
+ error: error,
58
+ });
59
+ return;
60
+ }
61
+ if (code) {
62
+ try {
63
+ // Exchange the temporary code for session tokens
64
+ const response = await fetch(`${appUrl}/api/auth/cli/exchange`, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ },
69
+ body: JSON.stringify({ code }),
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(`Authentication failed: ${response.statusText}`);
73
+ }
74
+ const { session, user } = await response.json();
75
+ if (!session || !user) {
76
+ throw new Error("Invalid response from authentication server");
77
+ }
78
+ const tokens = {
79
+ access_token: session.access_token,
80
+ refresh_token: session.refresh_token,
81
+ expires_at: session.expires_at,
82
+ user_id: user.id,
83
+ user_email: user.email,
84
+ };
85
+ await (0, token_store_1.storeTokens)(tokens);
86
+ res.writeHead(200, { "Content-Type": "text/html" });
87
+ res.end(`
88
+ <html>
89
+ <head><title>Authentication Successful</title></head>
90
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
91
+ <h1 style="color: #27ae60;">Authentication Successful!</h1>
92
+ <p>Welcome, ${user.email}!</p>
93
+ <p>You can close this window and return to your CLI.</p>
94
+ <script>setTimeout(() => window.close(), 3000);</script>
95
+ </body>
96
+ </html>
97
+ `);
98
+ server.close();
99
+ resolve({
100
+ success: true,
101
+ user: {
102
+ id: user.id,
103
+ email: user.email,
104
+ },
105
+ });
106
+ }
107
+ catch (err) {
108
+ res.writeHead(500, { "Content-Type": "text/html" });
109
+ res.end(`
110
+ <html>
111
+ <head><title>Authentication Error</title></head>
112
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
113
+ <h1 style="color: #e74c3c;">Authentication Error</h1>
114
+ <p>Something went wrong during authentication.</p>
115
+ <p>Please try again.</p>
116
+ </body>
117
+ </html>
118
+ `);
119
+ server.close();
120
+ resolve({
121
+ success: false,
122
+ error: err.message,
123
+ });
124
+ }
125
+ }
126
+ }
127
+ else {
128
+ // Handle other paths
129
+ res.writeHead(404, { "Content-Type": "text/html" });
130
+ res.end("<h1>Not Found</h1>");
131
+ }
132
+ });
133
+ server.on("error", (err) => {
134
+ if (err.code === "EADDRINUSE") {
135
+ resolve({
136
+ success: false,
137
+ error: `Port ${clientPort} is already in use. Please ensure no other authentication process is running.`,
138
+ });
139
+ }
140
+ else {
141
+ resolve({
142
+ success: false,
143
+ error: `Server error: ${err.message}`,
144
+ });
145
+ }
146
+ });
147
+ server.listen(clientPort, () => {
148
+ // Open browser to your Next.js auth page
149
+ const authUrl = `${appUrl}/auth/cli?redirect_uri=${encodeURIComponent(redirectUri)}`;
150
+ console.log("Opening browser for authentication...");
151
+ console.log(`If the browser doesn't open automatically, visit: ${authUrl}`);
152
+ (0, open_1.default)(authUrl).catch((err) => {
153
+ console.warn("Could not open browser automatically:", err.message);
154
+ console.log(`Please manually visit: ${authUrl}`);
155
+ });
156
+ });
157
+ // Timeout after 10 minutes
158
+ setTimeout(() => {
159
+ server.close();
160
+ resolve({
161
+ success: false,
162
+ error: "Authentication timeout (10 minutes)",
163
+ });
164
+ }, 10 * 60 * 1000);
165
+ });
166
+ }
167
+ async function logout() {
168
+ await (0, token_store_1.clearTokens)();
169
+ }
170
+ async function getAuthStatus() {
171
+ if (await (0, token_store_1.isAuthenticated)()) {
172
+ const tokens = await (0, token_store_1.getStoredTokens)();
173
+ if (tokens) {
174
+ return {
175
+ authenticated: true,
176
+ user: {
177
+ id: tokens.user_id || "",
178
+ email: tokens.user_email || "",
179
+ },
180
+ };
181
+ }
182
+ }
183
+ return { authenticated: false };
184
+ }
@@ -0,0 +1,4 @@
1
+ export { APIClient, apiClient, authenticatedFetch } from "./api-client";
2
+ export { authenticate, getAuthStatus, logout } from "./cli-auth";
3
+ export { clearTokens, getStoredTokens, isAuthenticated, type StoredTokens, storeTokens, } from "./token-store";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EACL,WAAW,EACX,eAAe,EACf,eAAe,EACf,KAAK,YAAY,EACjB,WAAW,GACZ,MAAM,eAAe,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storeTokens = exports.isAuthenticated = exports.getStoredTokens = exports.clearTokens = exports.logout = exports.getAuthStatus = exports.authenticate = exports.authenticatedFetch = exports.apiClient = exports.APIClient = void 0;
4
+ // Export all auth-related functionality
5
+ var api_client_1 = require("./api-client");
6
+ Object.defineProperty(exports, "APIClient", { enumerable: true, get: function () { return api_client_1.APIClient; } });
7
+ Object.defineProperty(exports, "apiClient", { enumerable: true, get: function () { return api_client_1.apiClient; } });
8
+ Object.defineProperty(exports, "authenticatedFetch", { enumerable: true, get: function () { return api_client_1.authenticatedFetch; } });
9
+ var cli_auth_1 = require("./cli-auth");
10
+ Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return cli_auth_1.authenticate; } });
11
+ Object.defineProperty(exports, "getAuthStatus", { enumerable: true, get: function () { return cli_auth_1.getAuthStatus; } });
12
+ Object.defineProperty(exports, "logout", { enumerable: true, get: function () { return cli_auth_1.logout; } });
13
+ var token_store_1 = require("./token-store");
14
+ Object.defineProperty(exports, "clearTokens", { enumerable: true, get: function () { return token_store_1.clearTokens; } });
15
+ Object.defineProperty(exports, "getStoredTokens", { enumerable: true, get: function () { return token_store_1.getStoredTokens; } });
16
+ Object.defineProperty(exports, "isAuthenticated", { enumerable: true, get: function () { return token_store_1.isAuthenticated; } });
17
+ Object.defineProperty(exports, "storeTokens", { enumerable: true, get: function () { return token_store_1.storeTokens; } });
@@ -0,0 +1,15 @@
1
+ export interface StoredTokens {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: number;
5
+ user_id?: string;
6
+ user_email?: string;
7
+ }
8
+ export interface TokenStorage {
9
+ [dashboardDomain: string]: StoredTokens;
10
+ }
11
+ export declare function storeTokens(tokens: StoredTokens): Promise<void>;
12
+ export declare function getStoredTokens(): Promise<StoredTokens | null>;
13
+ export declare function clearTokens(): Promise<void>;
14
+ export declare function isAuthenticated(): Promise<boolean>;
15
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,eAAe,EAAE,MAAM,GAAG,YAAY,CAAC;CACzC;AAgCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA2CrE;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqBpE;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA4CjD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAUxD"}
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storeTokens = storeTokens;
4
+ exports.getStoredTokens = getStoredTokens;
5
+ exports.clearTokens = clearTokens;
6
+ exports.isAuthenticated = isAuthenticated;
7
+ const crypto_1 = require("crypto");
8
+ const promises_1 = require("fs/promises");
9
+ const os_1 = require("os");
10
+ const path_1 = require("path");
11
+ const util_1 = require("util");
12
+ const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), ".empiricalrun");
13
+ const TOKEN_FILE = (0, path_1.join)(CONFIG_DIR, "auth.enc");
14
+ async function getEncryptionKey(salt) {
15
+ const machineId =
16
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
17
+ process.env.HOME || process.env.USERPROFILE || "default-machine";
18
+ const scryptAsync = (0, util_1.promisify)(crypto_1.scrypt);
19
+ return (await scryptAsync("empirical-cli-tokens-" + machineId, salt, 32));
20
+ }
21
+ function getDashboardDomain() {
22
+ const domain = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
23
+ // Remove protocol (http:// or https://) to make keys more stable
24
+ return domain.replace(/^https?:\/\//, "");
25
+ }
26
+ async function getTokenStorage() {
27
+ try {
28
+ const encryptedData = await (0, promises_1.readFile)(TOKEN_FILE);
29
+ // Extract salt, IV, and encrypted data
30
+ const salt = encryptedData.subarray(0, 16);
31
+ const iv = encryptedData.subarray(16, 32);
32
+ const encrypted = encryptedData.subarray(32);
33
+ // Derive key from password and salt
34
+ const key = await getEncryptionKey(salt);
35
+ // Decrypt the data
36
+ const decipher = (0, crypto_1.createDecipheriv)("aes-256-cbc", key, iv);
37
+ let decrypted = decipher.update(encrypted, undefined, "utf8");
38
+ decrypted += decipher.final("utf8");
39
+ return JSON.parse(decrypted);
40
+ }
41
+ catch (error) {
42
+ // Token file doesn't exist or is corrupted
43
+ return null;
44
+ }
45
+ }
46
+ async function storeTokens(tokens) {
47
+ try {
48
+ await (0, promises_1.mkdir)(CONFIG_DIR, { recursive: true });
49
+ const dashboardDomain = getDashboardDomain();
50
+ // Load existing token storage or create new
51
+ let tokenStorage = {};
52
+ try {
53
+ const existingStorage = await getTokenStorage();
54
+ if (existingStorage) {
55
+ tokenStorage = existingStorage;
56
+ }
57
+ }
58
+ catch {
59
+ // Ignore errors when loading existing storage
60
+ }
61
+ // Update tokens for current dashboard domain
62
+ tokenStorage[dashboardDomain] = tokens;
63
+ // Generate a random salt and IV
64
+ const salt = (0, crypto_1.randomBytes)(16);
65
+ const iv = (0, crypto_1.randomBytes)(16);
66
+ // Derive key from password and salt
67
+ const key = await getEncryptionKey(salt);
68
+ // Encrypt the token data
69
+ const cipher = (0, crypto_1.createCipheriv)("aes-256-cbc", key, iv);
70
+ let encrypted = cipher.update(JSON.stringify(tokenStorage), "utf8", "hex");
71
+ encrypted += cipher.final("hex");
72
+ // Store salt + iv + encrypted data
73
+ const encryptedData = Buffer.concat([
74
+ salt,
75
+ iv,
76
+ Buffer.from(encrypted, "hex"),
77
+ ]);
78
+ await (0, promises_1.writeFile)(TOKEN_FILE, encryptedData, { mode: 0o600 }); // Restrict file permissions
79
+ }
80
+ catch (error) {
81
+ throw new Error(`Failed to store authentication tokens: ${error}`);
82
+ }
83
+ }
84
+ async function getStoredTokens() {
85
+ try {
86
+ const dashboardDomain = getDashboardDomain();
87
+ const tokenStorage = await getTokenStorage();
88
+ if (!tokenStorage || !tokenStorage[dashboardDomain]) {
89
+ return null;
90
+ }
91
+ const tokens = tokenStorage[dashboardDomain];
92
+ // Validate token structure
93
+ if (!tokens.access_token || !tokens.refresh_token || !tokens.expires_at) {
94
+ throw new Error("Invalid token structure");
95
+ }
96
+ return tokens;
97
+ }
98
+ catch (error) {
99
+ // Token file doesn't exist or is corrupted
100
+ return null;
101
+ }
102
+ }
103
+ async function clearTokens() {
104
+ try {
105
+ const dashboardDomain = getDashboardDomain();
106
+ const tokenStorage = await getTokenStorage();
107
+ if (!tokenStorage) {
108
+ return; // Nothing to clear
109
+ }
110
+ // Remove tokens for current dashboard domain
111
+ delete tokenStorage[dashboardDomain];
112
+ // If no tokens left, remove the file
113
+ if (Object.keys(tokenStorage).length === 0) {
114
+ await (0, promises_1.unlink)(TOKEN_FILE);
115
+ return;
116
+ }
117
+ // Otherwise, save the updated storage
118
+ await (0, promises_1.mkdir)(CONFIG_DIR, { recursive: true });
119
+ // Generate a random salt and IV
120
+ const salt = (0, crypto_1.randomBytes)(16);
121
+ const iv = (0, crypto_1.randomBytes)(16);
122
+ // Derive key from password and salt
123
+ const key = await getEncryptionKey(salt);
124
+ // Encrypt the token data
125
+ const cipher = (0, crypto_1.createCipheriv)("aes-256-cbc", key, iv);
126
+ let encrypted = cipher.update(JSON.stringify(tokenStorage), "utf8", "hex");
127
+ encrypted += cipher.final("hex");
128
+ // Store salt + iv + encrypted data
129
+ const encryptedData = Buffer.concat([
130
+ salt,
131
+ iv,
132
+ Buffer.from(encrypted, "hex"),
133
+ ]);
134
+ await (0, promises_1.writeFile)(TOKEN_FILE, encryptedData, { mode: 0o600 });
135
+ }
136
+ catch (error) {
137
+ // File doesn't exist, ignore
138
+ }
139
+ }
140
+ async function isAuthenticated() {
141
+ const tokens = await getStoredTokens();
142
+ if (!tokens)
143
+ return false;
144
+ // Check if token is not expired (with 5 minute buffer)
145
+ const expirationBuffer = 5 * 60 * 1000; // 5 minutes
146
+ const now = Date.now();
147
+ const tokenExpiry = tokens.expires_at * 1000;
148
+ return tokenExpiry > now + expirationBuffer;
149
+ }
package/dist/bin/index.js CHANGED
@@ -16,6 +16,7 @@ const diagnosis_agent_1 = require("../agent/diagnosis-agent");
16
16
  const enrich_prompt_1 = require("../agent/enrich-prompt");
17
17
  const infer_agent_1 = require("../agent/infer-agent");
18
18
  const run_3 = require("../agent/planner/run");
19
+ const auth_1 = require("../auth");
19
20
  const recorder_1 = require("../recorder");
20
21
  const reporter_1 = require("../reporter");
21
22
  const session_1 = require("../session");
@@ -203,6 +204,94 @@ async function main() {
203
204
  const removeListeners = setupProcessListeners(flushEvents);
204
205
  await (0, utils_2.printBanner)();
205
206
  const program = new commander_1.Command();
207
+ // Add authentication commands
208
+ program
209
+ .command("login")
210
+ .description("Authenticate with your Empirical Run account")
211
+ .action(async () => {
212
+ console.log("🔐 Starting authentication...\n");
213
+ try {
214
+ const result = await (0, auth_1.authenticate)();
215
+ if (result.success) {
216
+ console.log("✅ Authentication successful!");
217
+ if (result.user) {
218
+ console.log(`👤 Logged in as: ${result.user.email}`);
219
+ }
220
+ console.log("\nYou can now use authenticated CLI commands.\n");
221
+ }
222
+ else {
223
+ console.error("❌ Authentication failed:", result.error);
224
+ process.exit(1);
225
+ }
226
+ }
227
+ catch (error) {
228
+ console.error("❌ Authentication error:", error.message);
229
+ process.exit(1);
230
+ }
231
+ process.exit(0);
232
+ });
233
+ program
234
+ .command("logout")
235
+ .description("Sign out of your Empirical Run account")
236
+ .action(async () => {
237
+ try {
238
+ await (0, auth_1.logout)();
239
+ console.log("✅ Successfully signed out.");
240
+ }
241
+ catch (error) {
242
+ console.error("❌ Logout error:", error.message);
243
+ process.exit(1);
244
+ }
245
+ process.exit(0);
246
+ });
247
+ program
248
+ .command("whoami")
249
+ .description("Check your authentication status")
250
+ .action(async () => {
251
+ try {
252
+ const status = await (0, auth_1.getAuthStatus)();
253
+ if (status.authenticated) {
254
+ console.log("✅ You are authenticated");
255
+ if (status.user) {
256
+ console.log(`👤 Logged in as: ${status.user.email}`);
257
+ }
258
+ }
259
+ else {
260
+ console.log("❌ You are not authenticated");
261
+ console.log('Run "npx @empiricalrun/test-gen login" to authenticate');
262
+ }
263
+ }
264
+ catch (error) {
265
+ console.error("❌ Error checking auth status:", error.message);
266
+ process.exit(1);
267
+ }
268
+ removeListeners();
269
+ await (0, llm_1.flushAllTraces)();
270
+ await (0, logger_1.waitForLogsToFlush)();
271
+ process.exit(0);
272
+ });
273
+ program
274
+ .command("auth-test")
275
+ .description("Test authentication by making an API call")
276
+ .action(async () => {
277
+ try {
278
+ console.log("🔍 Testing authentication with API call...\n");
279
+ const data = await (0, auth_1.authenticatedFetch)("/requests");
280
+ console.log("✅ API call successful!");
281
+ console.log("📄 Response:", JSON.stringify(data, null, 2));
282
+ }
283
+ catch (error) {
284
+ console.error("❌ API call failed:", error.message);
285
+ if (error.message.includes("Authentication required")) {
286
+ console.log('\n💡 Tip: Run "npx @empiricalrun/test-gen login" to authenticate');
287
+ }
288
+ process.exit(1);
289
+ }
290
+ removeListeners();
291
+ await (0, llm_1.flushAllTraces)();
292
+ await (0, logger_1.waitForLogsToFlush)();
293
+ process.exit(0);
294
+ });
206
295
  program
207
296
  .option("--token <token>", "Test generation token")
208
297
  .option("--prompt <prompt>", "Prompt for the chat agent")
@@ -217,7 +306,9 @@ async function main() {
217
306
  .option("--initial-prompt <path>", "Path to an initial prompt file (e.g. prompt.md)")
218
307
  .option("--with-retry", "Use the retry strategy")
219
308
  .parse(process.argv);
309
+ // Check if a command was executed (auth commands exit early)
220
310
  const options = program.opts();
311
+ // Only proceed with main workflow if no auth command was executed
221
312
  const completedOptions = await (0, utils_2.validateAndCompleteCliOptions)(options);
222
313
  const testGenConfig = completedOptions.token
223
314
  ? (0, scenarios_1.loadTestConfigs)(completedOptions.token)
@@ -105,10 +105,10 @@ async function printBanner() {
105
105
  let versionSuffix = "";
106
106
  if (latestVersion) {
107
107
  if (version === latestVersion) {
108
- versionSuffix = ` ${gray}(latest)${reset}`;
108
+ versionSuffix = ` (latest)`;
109
109
  }
110
110
  else {
111
- versionSuffix = ` (latest is ${latestVersion})`;
111
+ versionSuffix = ` ${reset}(latest is ${latestVersion})`;
112
112
  }
113
113
  }
114
114
  const logLine1 = `Running ${PACKAGE_NAME} v${version}${versionSuffix}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.66.2",
3
+ "version": "0.67.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -53,6 +53,7 @@
53
53
  "mime": "3.0.0",
54
54
  "minimatch": "^10.0.1",
55
55
  "nanoid": "^5.0.7",
56
+ "open": "^10.1.2",
56
57
  "openai": "4.87.3",
57
58
  "ora": "^8.1.0",
58
59
  "picocolors": "^1.0.1",
@@ -1 +1 @@
1
- {"root":["./src/index.ts","./src/actions/assert.ts","./src/actions/click.ts","./src/actions/done.ts","./src/actions/fill.ts","./src/actions/goto.ts","./src/actions/hover.ts","./src/actions/index.ts","./src/actions/next-task.ts","./src/actions/press.ts","./src/actions/skill.ts","./src/actions/text-content.ts","./src/actions/constants/index.ts","./src/actions/utils/index.ts","./src/agent/browsing/index.ts","./src/agent/browsing/run.ts","./src/agent/browsing/utils.ts","./src/agent/chat/agent-loop.ts","./src/agent/chat/exports.ts","./src/agent/chat/index.ts","./src/agent/chat/models.ts","./src/agent/chat/state.ts","./src/agent/chat/types.ts","./src/agent/chat/utils.ts","./src/agent/chat/prompt/index.ts","./src/agent/chat/prompt/pw-utils-docs.ts","./src/agent/chat/prompt/repo.ts","./src/agent/codegen/create-test-block.ts","./src/agent/codegen/fix-ts-errors.ts","./src/agent/codegen/generate-code-apply-changes.ts","./src/agent/codegen/lexical-scoped-vars.ts","./src/agent/codegen/repo-edit.ts","./src/agent/codegen/run.ts","./src/agent/codegen/skills-retriever.ts","./src/agent/codegen/test-update-feedback.ts","./src/agent/codegen/types.ts","./src/agent/codegen/update-flow.ts","./src/agent/codegen/use-skill.ts","./src/agent/codegen/utils.ts","./src/agent/cua/computer.ts","./src/agent/cua/index.ts","./src/agent/cua/model.ts","./src/agent/cua/pw-codegen/element-from-point.ts","./src/agent/cua/pw-codegen/types.ts","./src/agent/cua/pw-codegen/pw-pause/for-recorder.ts","./src/agent/cua/pw-codegen/pw-pause/index.ts","./src/agent/cua/pw-codegen/pw-pause/ipc.ts","./src/agent/cua/pw-codegen/pw-pause/patch.ts","./src/agent/cua/pw-codegen/pw-pause/types.ts","./src/agent/diagnosis-agent/index.ts","./src/agent/diagnosis-agent/strict-mode-violation.ts","./src/agent/enrich-prompt/index.ts","./src/agent/enrich-prompt/utils.ts","./src/agent/infer-agent/index.ts","./src/agent/master/action-tool-calls.ts","./src/agent/master/element-annotation.ts","./src/agent/master/execute-browser-action.ts","./src/agent/master/execute-skill-action.ts","./src/agent/master/next-action.ts","./src/agent/master/planner.ts","./src/agent/master/run.ts","./src/agent/master/scroller.ts","./src/agent/master/with-hints.ts","./src/agent/master/browser-tests/cua.spec.ts","./src/agent/master/browser-tests/fixtures.ts","./src/agent/master/browser-tests/index.spec.ts","./src/agent/master/browser-tests/skills.spec.ts","./src/agent/master/icon-descriptor/index.ts","./src/agent/master/icon-descriptor/normalize-svg.ts","./src/agent/planner/run-time-planner.ts","./src/agent/planner/run.ts","./src/artifacts/index.ts","./src/artifacts/utils.ts","./src/bin/index.ts","./src/bin/logger/index.ts","./src/bin/utils/context.ts","./src/bin/utils/index.ts","./src/bin/utils/fs/index.ts","./src/bin/utils/platform/web/index.ts","./src/bin/utils/platform/web/test-files/ts-path-import-validate.ts","./src/bin/utils/scenarios/index.ts","./src/browser-injected-scripts/annotate-elements.spec.ts","./src/constants/index.ts","./src/errors/index.ts","./src/evals/add-scenario-agent.evals.ts","./src/evals/append-create-test-agent.evals.ts","./src/evals/fetch-pom-skills-agent.evals.ts","./src/evals/infer-master-or-code-agent.evals.ts","./src/evals/master-agent.evals.ts","./src/evals/type.ts","./src/evals/update-scenario-agent.evals.ts","./src/file/client.ts","./src/file/server.ts","./src/human-in-the-loop/cli.ts","./src/human-in-the-loop/index.ts","./src/human-in-the-loop/ipc.ts","./src/page/index.ts","./src/prompts/lib/ts-transformer.ts","./src/recorder/display.ts","./src/recorder/index.ts","./src/recorder/request.ts","./src/recorder/temp-files.ts","./src/recorder/upload.ts","./src/recorder/validation.ts","./src/reporter/index.ts","./src/reporter/lib.ts","./src/session/index.ts","./src/test-build/index.ts","./src/tool-call-service/index.ts","./src/tool-call-service/utils.ts","./src/tools/commit-and-create-pr.ts","./src/tools/diagnosis-fetcher.ts","./src/tools/download-build.ts","./src/tools/list-environments.ts","./src/tools/str_replace_editor.ts","./src/tools/test-gen-browser.ts","./src/tools/test-run.ts","./src/tools/grep/index.ts","./src/tools/grep/ripgrep/index.ts","./src/tools/grep/ripgrep/types.ts","./src/tools/test-run-fetcher/index.ts","./src/tools/test-run-fetcher/types.ts","./src/tools/upgrade-packages/index.ts","./src/tools/upgrade-packages/utils.ts","./src/tools/utils/index.ts","./src/types/handlebars.d.ts","./src/types/index.ts","./src/uploader/index.ts","./src/uploader/utils.ts","./src/utils/checkpoint.ts","./src/utils/env.ts","./src/utils/exec.ts","./src/utils/file-tree.ts","./src/utils/file.ts","./src/utils/git.ts","./src/utils/html.ts","./src/utils/index.ts","./src/utils/json.ts","./src/utils/repo-tree.ts","./src/utils/slug.ts","./src/utils/string.ts","./src/utils/stripAnsi.ts"],"version":"5.8.3"}
1
+ {"root":["./src/index.ts","./src/actions/assert.ts","./src/actions/click.ts","./src/actions/done.ts","./src/actions/fill.ts","./src/actions/goto.ts","./src/actions/hover.ts","./src/actions/index.ts","./src/actions/next-task.ts","./src/actions/press.ts","./src/actions/skill.ts","./src/actions/text-content.ts","./src/actions/constants/index.ts","./src/actions/utils/index.ts","./src/agent/browsing/index.ts","./src/agent/browsing/run.ts","./src/agent/browsing/utils.ts","./src/agent/chat/agent-loop.ts","./src/agent/chat/exports.ts","./src/agent/chat/index.ts","./src/agent/chat/models.ts","./src/agent/chat/state.ts","./src/agent/chat/types.ts","./src/agent/chat/utils.ts","./src/agent/chat/prompt/index.ts","./src/agent/chat/prompt/pw-utils-docs.ts","./src/agent/chat/prompt/repo.ts","./src/agent/codegen/create-test-block.ts","./src/agent/codegen/fix-ts-errors.ts","./src/agent/codegen/generate-code-apply-changes.ts","./src/agent/codegen/lexical-scoped-vars.ts","./src/agent/codegen/repo-edit.ts","./src/agent/codegen/run.ts","./src/agent/codegen/skills-retriever.ts","./src/agent/codegen/test-update-feedback.ts","./src/agent/codegen/types.ts","./src/agent/codegen/update-flow.ts","./src/agent/codegen/use-skill.ts","./src/agent/codegen/utils.ts","./src/agent/cua/computer.ts","./src/agent/cua/index.ts","./src/agent/cua/model.ts","./src/agent/cua/pw-codegen/element-from-point.ts","./src/agent/cua/pw-codegen/types.ts","./src/agent/cua/pw-codegen/pw-pause/for-recorder.ts","./src/agent/cua/pw-codegen/pw-pause/index.ts","./src/agent/cua/pw-codegen/pw-pause/ipc.ts","./src/agent/cua/pw-codegen/pw-pause/patch.ts","./src/agent/cua/pw-codegen/pw-pause/types.ts","./src/agent/diagnosis-agent/index.ts","./src/agent/diagnosis-agent/strict-mode-violation.ts","./src/agent/enrich-prompt/index.ts","./src/agent/enrich-prompt/utils.ts","./src/agent/infer-agent/index.ts","./src/agent/master/action-tool-calls.ts","./src/agent/master/element-annotation.ts","./src/agent/master/execute-browser-action.ts","./src/agent/master/execute-skill-action.ts","./src/agent/master/next-action.ts","./src/agent/master/planner.ts","./src/agent/master/run.ts","./src/agent/master/scroller.ts","./src/agent/master/with-hints.ts","./src/agent/master/browser-tests/cua.spec.ts","./src/agent/master/browser-tests/fixtures.ts","./src/agent/master/browser-tests/index.spec.ts","./src/agent/master/browser-tests/skills.spec.ts","./src/agent/master/icon-descriptor/index.ts","./src/agent/master/icon-descriptor/normalize-svg.ts","./src/agent/planner/run-time-planner.ts","./src/agent/planner/run.ts","./src/artifacts/index.ts","./src/artifacts/utils.ts","./src/auth/api-client.ts","./src/auth/cli-auth.ts","./src/auth/index.ts","./src/auth/token-store.ts","./src/bin/index.ts","./src/bin/logger/index.ts","./src/bin/utils/context.ts","./src/bin/utils/index.ts","./src/bin/utils/fs/index.ts","./src/bin/utils/platform/web/index.ts","./src/bin/utils/platform/web/test-files/ts-path-import-validate.ts","./src/bin/utils/scenarios/index.ts","./src/browser-injected-scripts/annotate-elements.spec.ts","./src/constants/index.ts","./src/errors/index.ts","./src/evals/add-scenario-agent.evals.ts","./src/evals/append-create-test-agent.evals.ts","./src/evals/fetch-pom-skills-agent.evals.ts","./src/evals/infer-master-or-code-agent.evals.ts","./src/evals/master-agent.evals.ts","./src/evals/type.ts","./src/evals/update-scenario-agent.evals.ts","./src/file/client.ts","./src/file/server.ts","./src/human-in-the-loop/cli.ts","./src/human-in-the-loop/index.ts","./src/human-in-the-loop/ipc.ts","./src/page/index.ts","./src/prompts/lib/ts-transformer.ts","./src/recorder/display.ts","./src/recorder/index.ts","./src/recorder/request.ts","./src/recorder/temp-files.ts","./src/recorder/upload.ts","./src/recorder/validation.ts","./src/reporter/index.ts","./src/reporter/lib.ts","./src/session/index.ts","./src/test-build/index.ts","./src/tool-call-service/index.ts","./src/tool-call-service/utils.ts","./src/tools/commit-and-create-pr.ts","./src/tools/diagnosis-fetcher.ts","./src/tools/download-build.ts","./src/tools/list-environments.ts","./src/tools/str_replace_editor.ts","./src/tools/test-gen-browser.ts","./src/tools/test-run.ts","./src/tools/grep/index.ts","./src/tools/grep/ripgrep/index.ts","./src/tools/grep/ripgrep/types.ts","./src/tools/test-run-fetcher/index.ts","./src/tools/test-run-fetcher/types.ts","./src/tools/upgrade-packages/index.ts","./src/tools/upgrade-packages/utils.ts","./src/tools/utils/index.ts","./src/types/handlebars.d.ts","./src/types/index.ts","./src/uploader/index.ts","./src/uploader/utils.ts","./src/utils/checkpoint.ts","./src/utils/env.ts","./src/utils/exec.ts","./src/utils/file-tree.ts","./src/utils/file.ts","./src/utils/git.ts","./src/utils/html.ts","./src/utils/index.ts","./src/utils/json.ts","./src/utils/repo-tree.ts","./src/utils/slug.ts","./src/utils/string.ts","./src/utils/stripAnsi.ts"],"version":"5.8.3"}