@empiricalrun/test-gen 0.66.2 → 0.68.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,17 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.68.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cb957ea: feat: use cli user auth to upload recorder output
8
+
9
+ ## 0.67.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 267b012: feat: add user login in test-gen cli
14
+
3
15
  ## 0.66.2
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,11 @@
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 {};
11
+ //# 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;YA2BN,WAAW;YAuBX,mBAAmB;YAiBnB,YAAY;CA4C3B;AAED,eAAO,MAAM,SAAS,WAAkB,CAAC"}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiClient = void 0;
4
+ const token_store_1 = require("./token-store");
5
+ class APIClient {
6
+ appUrl;
7
+ constructor() {
8
+ this.appUrl = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
9
+ }
10
+ async request(endpoint, options = {}) {
11
+ await this.ensureAuthenticated();
12
+ const tokens = await (0, token_store_1.getStoredTokens)();
13
+ if (!tokens) {
14
+ throw new Error("Not authenticated. Please run the login command first.");
15
+ }
16
+ const response = await this.makeRequest(endpoint, options, tokens.access_token);
17
+ if (response.status === 401) {
18
+ // console.log("Access token expired, attempting to refresh...");
19
+ const refreshed = await this.refreshToken();
20
+ if (refreshed) {
21
+ const newTokens = await (0, token_store_1.getStoredTokens)();
22
+ if (newTokens) {
23
+ return this.makeRequest(endpoint, options, newTokens.access_token);
24
+ }
25
+ }
26
+ throw new Error("Authentication failed. Please run the login command again.");
27
+ }
28
+ return response;
29
+ }
30
+ async makeRequest(endpoint, options, accessToken) {
31
+ const url = endpoint.startsWith("http")
32
+ ? endpoint
33
+ : `${this.appUrl}${endpoint}`;
34
+ // Don't set Content-Type for FormData (let browser set it automatically)
35
+ const headers = {
36
+ Authorization: `Bearer ${accessToken}`,
37
+ ...options.headers,
38
+ };
39
+ // Only set Content-Type to application/json if body is not FormData
40
+ if (!(options.body instanceof FormData)) {
41
+ headers["Content-Type"] = "application/json";
42
+ }
43
+ return fetch(url, {
44
+ ...options,
45
+ headers,
46
+ });
47
+ }
48
+ async ensureAuthenticated() {
49
+ if (!(await (0, token_store_1.isAuthenticated)())) {
50
+ const tokens = await (0, token_store_1.getStoredTokens)();
51
+ if (!tokens) {
52
+ throw new Error("Not authenticated. Please run the login command first.");
53
+ }
54
+ // Token exists but is expired, try to refresh
55
+ const refreshed = await this.refreshToken();
56
+ if (!refreshed) {
57
+ throw new Error("Session expired. Please run the login command again.");
58
+ }
59
+ }
60
+ }
61
+ async refreshToken() {
62
+ const tokens = await (0, token_store_1.getStoredTokens)();
63
+ if (!tokens || !tokens.refresh_token) {
64
+ return false;
65
+ }
66
+ try {
67
+ // Use dashboard refresh endpoint instead of Supabase directly
68
+ const response = await fetch(`${this.appUrl}/api/cli/refresh`, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: JSON.stringify({
74
+ refresh_token: tokens.refresh_token,
75
+ }),
76
+ });
77
+ if (!response.ok) {
78
+ const errorText = await response.text();
79
+ console.error("Token refresh failed:", errorText);
80
+ await (0, token_store_1.clearTokens)();
81
+ return false;
82
+ }
83
+ const data = await response.json();
84
+ // Store the new tokens (refresh token rotation means we get a new refresh token)
85
+ await (0, token_store_1.storeTokens)({
86
+ access_token: data.access_token,
87
+ refresh_token: data.refresh_token,
88
+ expires_at: data.expires_at,
89
+ user_id: data.user?.id || tokens.user_id,
90
+ user_email: data.user?.email || tokens.user_email,
91
+ });
92
+ console.log("Access token refreshed successfully via dashboard");
93
+ return true;
94
+ }
95
+ catch (error) {
96
+ console.error("Token refresh error:", error);
97
+ await (0, token_store_1.clearTokens)();
98
+ return false;
99
+ }
100
+ }
101
+ }
102
+ exports.apiClient = new APIClient();
@@ -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 } 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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,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,14 @@
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.apiClient = void 0;
4
+ var api_client_1 = require("./api-client");
5
+ Object.defineProperty(exports, "apiClient", { enumerable: true, get: function () { return api_client_1.apiClient; } });
6
+ var cli_auth_1 = require("./cli-auth");
7
+ Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return cli_auth_1.authenticate; } });
8
+ Object.defineProperty(exports, "getAuthStatus", { enumerable: true, get: function () { return cli_auth_1.getAuthStatus; } });
9
+ Object.defineProperty(exports, "logout", { enumerable: true, get: function () { return cli_auth_1.logout; } });
10
+ var token_store_1 = require("./token-store");
11
+ Object.defineProperty(exports, "clearTokens", { enumerable: true, get: function () { return token_store_1.clearTokens; } });
12
+ Object.defineProperty(exports, "getStoredTokens", { enumerable: true, get: function () { return token_store_1.getStoredTokens; } });
13
+ Object.defineProperty(exports, "isAuthenticated", { enumerable: true, get: function () { return token_store_1.isAuthenticated; } });
14
+ 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,8 @@ 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");
20
+ const api_client_1 = require("../auth/api-client");
19
21
  const recorder_1 = require("../recorder");
20
22
  const reporter_1 = require("../reporter");
21
23
  const session_1 = require("../session");
@@ -26,16 +28,6 @@ const scenarios_1 = require("./utils/scenarios");
26
28
  dotenv_1.default.config({
27
29
  path: [".env.local", ".env"],
28
30
  });
29
- const flushEvents = async () => {
30
- await (0, llm_1.flushAllTraces)();
31
- };
32
- function setupProcessListeners(cleanup) {
33
- const events = ["beforeExit", "exit", "SIGINT", "SIGTERM"];
34
- events.forEach((event) => process.once(event, cleanup));
35
- return () => {
36
- events.forEach((event) => process.removeListener(event, cleanup));
37
- };
38
- }
39
31
  async function runChatAgent({ modelInput, chatSessionId, useDiskForChatState, initialPromptPath, }) {
40
32
  if (modelInput && !utils_2.ARGS_TO_MODEL_MAP[modelInput]) {
41
33
  throw new Error(`Invalid chat model: ${modelInput}`);
@@ -200,92 +192,177 @@ async function runAgentsWorkflow(testGenConfig, testGenToken) {
200
192
  return agent;
201
193
  }
202
194
  async function main() {
203
- const removeListeners = setupProcessListeners(flushEvents);
204
195
  await (0, utils_2.printBanner)();
205
196
  const program = new commander_1.Command();
206
197
  program
207
- .option("--token <token>", "Test generation token")
208
- .option("--prompt <prompt>", "Prompt for the chat agent")
209
- .option("--name <test-name>", "Name of the test case")
210
- .option("--file <test-file>", "File path of the test case (inside tests dir)")
211
- .option("--suites <suites>", "Comma separated list of describe blocks")
212
- .option("--use-chat", "Use chat agent (and not the workflow)")
213
- .option("--use-recorder", "Run the recorder flow to create a request")
214
- .option("--chat-session-id <chat-session-id>", "Identifier for chat session (fetched from dash.empirical.run)")
215
- .option("--use-disk-for-chat-state", "Save and load chat state from disk")
216
- .option("--chat-model <model>", "Chat model to use (claude-3-7-sonnet-20250219 or claude-3-5-sonnet-20241022 or gemini-2.5-pro-preview-06-05)")
217
- .option("--initial-prompt <path>", "Path to an initial prompt file (e.g. prompt.md)")
218
- .option("--with-retry", "Use the retry strategy")
219
- .parse(process.argv);
220
- const options = program.opts();
221
- const completedOptions = await (0, utils_2.validateAndCompleteCliOptions)(options);
222
- const testGenConfig = completedOptions.token
223
- ? (0, scenarios_1.loadTestConfigs)(completedOptions.token)
224
- : (0, scenarios_1.buildTestConfigFromOptions)(completedOptions);
225
- const testGenToken = completedOptions.token
226
- ? completedOptions.token
227
- : (0, scenarios_1.buildTokenFromOptions)(completedOptions);
228
- (0, reporter_1.setReporterConfig)({
229
- projectRepoName: testGenConfig.options?.metadata.projectRepoName,
230
- testSessionId: testGenConfig.options?.metadata.testSessionId,
231
- generationId: testGenConfig.options?.metadata.generationId,
198
+ .command("login")
199
+ .description("Authenticate with your Empirical account")
200
+ .action(async () => {
201
+ console.log("🔐 Starting authentication...\n");
202
+ try {
203
+ const result = await (0, auth_1.authenticate)();
204
+ if (result.success) {
205
+ console.log(" Authentication successful!");
206
+ if (result.user) {
207
+ console.log(`👤 Logged in as: ${result.user.email}`);
208
+ }
209
+ console.log("\nYou can now use authenticated CLI commands.\n");
210
+ }
211
+ else {
212
+ console.error("❌ Authentication failed:", result.error);
213
+ process.exit(1);
214
+ }
215
+ }
216
+ catch (error) {
217
+ console.error("❌ Authentication error:", error.message);
218
+ process.exit(1);
219
+ }
220
+ process.exit(0);
221
+ });
222
+ program
223
+ .command("logout")
224
+ .description("Sign out of your Empirical account")
225
+ .action(async () => {
226
+ try {
227
+ await (0, auth_1.logout)();
228
+ console.log("✅ Successfully signed out.");
229
+ }
230
+ catch (error) {
231
+ console.error("❌ Logout error:", error.message);
232
+ process.exit(1);
233
+ }
234
+ process.exit(0);
235
+ });
236
+ program
237
+ .command("whoami")
238
+ .description("Check your authentication status")
239
+ .action(async () => {
240
+ try {
241
+ const status = await (0, auth_1.getAuthStatus)();
242
+ if (status.authenticated) {
243
+ console.log("✅ You are authenticated");
244
+ if (status.user) {
245
+ console.log(`👤 Logged in as: ${status.user.email}`);
246
+ }
247
+ }
248
+ else {
249
+ console.log("❌ You are not authenticated");
250
+ console.log('Run "npx @empiricalrun/test-gen login" to authenticate');
251
+ }
252
+ }
253
+ catch (error) {
254
+ console.error("❌ Error checking auth status:", error.message);
255
+ process.exit(1);
256
+ }
257
+ process.exit(0);
232
258
  });
233
- (0, session_1.setSessionDetails)({
234
- testCaseId: testGenConfig.testCase.id,
235
- sessionId: testGenConfig.options?.metadata.testSessionId,
236
- generationId: testGenConfig.options?.metadata.generationId,
237
- projectRepoName: testGenConfig.options?.metadata.projectRepoName,
259
+ program
260
+ .command("record")
261
+ .description("Record a new test case")
262
+ .option("--name <string>", "Name of the test case")
263
+ .action(async (opts) => {
264
+ const options = await (0, utils_2.validateAndCompleteCliOptions)(opts, ["name"]);
265
+ await (0, recorder_1.runRecorder)({ name: options.name });
266
+ process.exit(0);
238
267
  });
239
- if (testGenConfig.build?.url) {
240
- // Download the build if repo has a download script
241
- await (0, test_build_1.downloadBuild)({
242
- buildUrl: testGenConfig.build.url,
243
- repoPath: process.cwd(),
244
- apiKey: process.env.EMPIRICALRUN_API_KEY,
245
- });
246
- }
247
- if (completedOptions.useRecorder) {
248
- await (0, recorder_1.runRecorder)({ name: completedOptions.name });
249
- return;
250
- }
251
- if (completedOptions.useChat) {
268
+ program
269
+ .command("repos")
270
+ .description("List your projects and repositories")
271
+ .action(async () => {
272
+ try {
273
+ const response = await api_client_1.apiClient.request("/api/projects");
274
+ if (!response.ok) {
275
+ console.error("❌ Failed to fetch projects:", response.statusText);
276
+ process.exit(1);
277
+ }
278
+ const result = await response.json();
279
+ result.data.projects.forEach((project) => {
280
+ console.log(` ${project.repo_name}`);
281
+ });
282
+ }
283
+ catch (error) {
284
+ console.error("❌ Error fetching projects:", error.message);
285
+ process.exit(1);
286
+ }
287
+ process.exit(0);
288
+ });
289
+ program
290
+ .command("chat-agent")
291
+ .description("Run the chat agent")
292
+ .option("--chat-model <model>", "LLM to use (claude-3-7, claude-4 or gemini-2.5)")
293
+ .option("--use-disk-for-chat-state", "Save and load chat state from disk")
294
+ .option("--initial-prompt <path>", "Path to an initial prompt file (e.g. prompt.md)")
295
+ .option("--chat-session-id <chat-session-id>", "Identifier for chat session (fetched from dash.empirical.run)")
296
+ .action(async (options) => {
252
297
  await runChatAgent({
253
- chatSessionId: completedOptions.chatSessionId,
254
- modelInput: completedOptions.chatModel,
255
- useDiskForChatState: completedOptions.useDiskForChatState,
256
- initialPromptPath: completedOptions.initialPrompt,
298
+ chatSessionId: options.chatSessionId,
299
+ modelInput: options.chatModel,
300
+ useDiskForChatState: options.useDiskForChatState,
301
+ initialPromptPath: options.initialPrompt,
257
302
  });
258
- return;
259
- }
260
- let agentUsed;
261
- let testGenFailed = false;
262
- try {
263
- agentUsed = await runAgentsWorkflow(testGenConfig, testGenToken);
264
- }
265
- catch (e) {
266
- testGenFailed = true;
267
- new logger_1.CustomLogger().error(`Failed to generate test for the scenario. ${process.env.LOG_URL ? `[view log](${process.env.LOG_URL})` : ""}`, e?.message, e?.stack);
268
- }
269
- if (agentUsed &&
270
- agentUsed !== "code" &&
271
- agentUsed !== "plan" &&
272
- testGenConfig.testCase.name &&
273
- testGenConfig.options) {
274
- await new reporter_1.TestGenUpdatesReporter().reportGenAssets({
275
- projectRepoName: testGenConfig.options.metadata.projectRepoName,
276
- testName: testGenConfig.testCase.name,
277
- });
278
- }
279
- removeListeners();
280
- await (0, llm_1.flushAllTraces)();
281
- await (0, logger_1.waitForLogsToFlush)();
282
- await (0, session_1.endSession)();
283
- if (testGenFailed) {
284
- process.exit(1);
285
- }
286
- else {
287
303
  process.exit(0);
288
- }
304
+ });
305
+ program
306
+ .command("legacy")
307
+ .description("Run the legacy workflows")
308
+ .option("-t, --token <token>", "Test generation token")
309
+ .action(async (opts) => {
310
+ const options = await (0, utils_2.validateAndCompleteCliOptions)(opts);
311
+ const testGenConfig = options.token
312
+ ? (0, scenarios_1.loadTestConfigs)(options.token)
313
+ : (0, scenarios_1.buildTestConfigFromOptions)(options);
314
+ const testGenToken = options.token
315
+ ? options.token
316
+ : (0, scenarios_1.buildTokenFromOptions)(options);
317
+ (0, reporter_1.setReporterConfig)({
318
+ projectRepoName: testGenConfig.options?.metadata.projectRepoName,
319
+ testSessionId: testGenConfig.options?.metadata.testSessionId,
320
+ generationId: testGenConfig.options?.metadata.generationId,
321
+ });
322
+ (0, session_1.setSessionDetails)({
323
+ testCaseId: testGenConfig.testCase.id,
324
+ sessionId: testGenConfig.options?.metadata.testSessionId,
325
+ generationId: testGenConfig.options?.metadata.generationId,
326
+ projectRepoName: testGenConfig.options?.metadata.projectRepoName,
327
+ });
328
+ if (testGenConfig.build?.url) {
329
+ // Download the build if repo has a download script
330
+ await (0, test_build_1.downloadBuild)({
331
+ buildUrl: testGenConfig.build.url,
332
+ repoPath: process.cwd(),
333
+ apiKey: process.env.EMPIRICALRUN_API_KEY,
334
+ });
335
+ }
336
+ let agentUsed;
337
+ let testGenFailed = false;
338
+ try {
339
+ agentUsed = await runAgentsWorkflow(testGenConfig, testGenToken);
340
+ }
341
+ catch (e) {
342
+ testGenFailed = true;
343
+ new logger_1.CustomLogger().error(`Failed to generate test for the scenario. ${process.env.LOG_URL ? `[view log](${process.env.LOG_URL})` : ""}`, e?.message, e?.stack);
344
+ }
345
+ if (agentUsed &&
346
+ agentUsed !== "code" &&
347
+ agentUsed !== "plan" &&
348
+ testGenConfig.testCase.name &&
349
+ testGenConfig.options) {
350
+ await new reporter_1.TestGenUpdatesReporter().reportGenAssets({
351
+ projectRepoName: testGenConfig.options.metadata.projectRepoName,
352
+ testName: testGenConfig.testCase.name,
353
+ });
354
+ }
355
+ await (0, llm_1.flushAllTraces)();
356
+ await (0, logger_1.waitForLogsToFlush)();
357
+ await (0, session_1.endSession)();
358
+ if (testGenFailed) {
359
+ process.exit(1);
360
+ }
361
+ else {
362
+ process.exit(0);
363
+ }
364
+ });
365
+ program.parse(process.argv);
289
366
  }
290
367
  main().catch((error) => {
291
368
  console.error("Unhandled error in main function:", error);
@@ -6,13 +6,7 @@ export interface CLIOptions {
6
6
  file?: string;
7
7
  prompt?: string;
8
8
  suites?: string;
9
- useChat?: boolean;
10
- useDiskForChatState?: boolean;
11
- initialPrompt?: string;
12
- chatSessionId?: string;
13
- chatModel?: (typeof ARGS_TO_MODEL_MAP)[keyof typeof ARGS_TO_MODEL_MAP];
14
- useRecorder?: boolean;
15
9
  }
16
- export declare function validateAndCompleteCliOptions(options: CLIOptions): Promise<CLIOptions>;
10
+ export declare function validateAndCompleteCliOptions(options: CLIOptions, requiredFields?: string[]): Promise<CLIOptions>;
17
11
  export declare function printBanner(): Promise<void>;
18
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAKjE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAgBjE,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;IAGvE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAQD,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,UAAU,CAAC,CA8DrB;AAeD,wBAAsB,WAAW,kBAgDhC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAKjE,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAgBjE,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAQD,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,UAAU,EACnB,cAAc,GAAE,MAAM,EAA+B,GACpD,OAAO,CAAC,UAAU,CAAC,CAqDrB;AAeD,wBAAsB,WAAW,kBAgDhC"}
@@ -22,20 +22,12 @@ exports.ARGS_TO_MODEL_MAP = {
22
22
  "o4-mini": "o4-mini-2025-04-16",
23
23
  "o4-mini-2025-04-16": "o4-mini-2025-04-16",
24
24
  };
25
- async function validateAndCompleteCliOptions(options) {
25
+ async function validateAndCompleteCliOptions(options, requiredFields = ["name", "file", "prompt"]) {
26
26
  // For existing flow between dashboard <> test-gen (via ci-worker)
27
27
  const hasToken = !!options.token;
28
28
  if (hasToken) {
29
29
  return options;
30
30
  }
31
- let requiredFields = ["name", "file", "prompt"];
32
- if (options.useChat) {
33
- // Chat agent can prompt the user directly, nothing is required in CLI args
34
- requiredFields = [];
35
- }
36
- if (options.useRecorder) {
37
- requiredFields = ["name"];
38
- }
39
31
  const questions = [];
40
32
  if (!options.name && requiredFields.includes("name")) {
41
33
  questions.push({
@@ -105,10 +97,10 @@ async function printBanner() {
105
97
  let versionSuffix = "";
106
98
  if (latestVersion) {
107
99
  if (version === latestVersion) {
108
- versionSuffix = ` ${gray}(latest)${reset}`;
100
+ versionSuffix = ` (latest)`;
109
101
  }
110
102
  else {
111
- versionSuffix = ` (latest is ${latestVersion})`;
103
+ versionSuffix = ` ${reset}(latest is ${latestVersion})`;
112
104
  }
113
105
  }
114
106
  const logLine1 = `Running ${PACKAGE_NAME} v${version}${versionSuffix}`;
@@ -0,0 +1,2 @@
1
+ export declare function fetchEnvironmentVariables(repoName: string): Promise<Record<string, string>>;
2
+ //# sourceMappingURL=env-variables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-variables.d.ts","sourceRoot":"","sources":["../../src/recorder/env-variables.ts"],"names":[],"mappings":"AAEA,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgCjC"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchEnvironmentVariables = fetchEnvironmentVariables;
4
+ const api_client_1 = require("../auth/api-client");
5
+ async function fetchEnvironmentVariables(repoName) {
6
+ try {
7
+ const response = await api_client_1.apiClient.request(`/api/environment-variables?project_repo_name=${encodeURIComponent(repoName)}`, {
8
+ method: "GET",
9
+ });
10
+ if (!response.ok) {
11
+ const errorMessage = await response.text();
12
+ throw new Error(`Failed to fetch environment variables: ${errorMessage}`);
13
+ }
14
+ const data = await response.json();
15
+ if (!data.data) {
16
+ console.error("Failed to fetch environment variables:", data);
17
+ throw new Error("Failed to fetch environment variables");
18
+ }
19
+ const envVars = data.data.environment_variables.reduce((acc, envVar) => {
20
+ acc[envVar.name] = envVar.value;
21
+ return acc;
22
+ }, {});
23
+ return envVars;
24
+ }
25
+ catch (error) {
26
+ console.error("Failed to fetch environment variables:", error);
27
+ return {};
28
+ }
29
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/recorder/index.ts"],"names":[],"mappings":"AAyCA,wBAAsB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,iBAiF3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/recorder/index.ts"],"names":[],"mappings":"AAiBA,wBAAsB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,iBA2E3D"}
@@ -6,51 +6,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runRecorder = runRecorder;
7
7
  const test_run_1 = require("@empiricalrun/test-run");
8
8
  const detect_port_1 = __importDefault(require("detect-port"));
9
- const fs_1 = __importDefault(require("fs"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const utils_1 = require("../agent/browsing/utils");
12
- const chat_1 = require("../agent/chat");
13
11
  const pw_pause_1 = require("../agent/cua/pw-codegen/pw-pause");
14
- const utils_2 = require("../artifacts/utils");
15
12
  const server_1 = require("../file/server");
16
13
  const display_1 = require("./display");
14
+ const env_variables_1 = require("./env-variables");
17
15
  const request_1 = require("./request");
18
16
  const temp_files_1 = require("./temp-files");
19
17
  const upload_1 = require("./upload");
20
18
  const validation_1 = require("./validation");
21
- function extractVideoAttachments(repoDir) {
22
- try {
23
- const summaryPath = path_1.default.join(repoDir, "summary.json");
24
- if (!fs_1.default.existsSync(summaryPath)) {
25
- console.log("summary.json not found");
26
- return [];
27
- }
28
- const summaryContent = JSON.parse(fs_1.default.readFileSync(summaryPath, "utf-8"));
29
- const attachments = (0, utils_2.extractAttachmentsFromPlaywrightJSONReport)(summaryContent, "temp test");
30
- const videoPaths = attachments
31
- .filter((attachment) => attachment.contentType === "video/webm")
32
- .map((attachment) => attachment.path);
33
- return videoPaths;
34
- }
35
- catch (error) {
36
- console.warn("Error processing summary.json:", error);
37
- return [];
38
- }
39
- }
40
19
  async function runRecorder({ name }) {
41
20
  console.log(`Recording for test name: ${name}`);
42
21
  const repoDir = process.cwd();
22
+ let repoName = "";
43
23
  try {
44
- await (0, validation_1.validate)(repoDir);
24
+ repoName = await (0, validation_1.validatePackageJson)(repoDir);
45
25
  }
46
26
  catch (error) {
47
27
  console.error("Error running recorder:", error);
48
28
  process.exit(1);
49
29
  }
50
- if (!process.env.EMPIRICALRUN_API_KEY) {
51
- console.error("EMPIRICALRUN_API_KEY is not set. Please set it in your environment variables.");
52
- process.exit(1);
53
- }
54
30
  try {
55
31
  // Prepare playwright for codegen
56
32
  console.log("[generateTestWithBrowserAgent] Preparing playwright for codegen");
@@ -59,7 +35,7 @@ async function runRecorder({ name }) {
59
35
  catch (err) {
60
36
  console.warn("[generateTestWithBrowserAgent] Error preparing playwright for codegen", err);
61
37
  }
62
- const envVariables = await (0, chat_1.fetchEnvironmentVariables)();
38
+ const envVariables = await (0, env_variables_1.fetchEnvironmentVariables)(repoName);
63
39
  await (0, temp_files_1.createTempTestFile)();
64
40
  const absFilePath = path_1.default.join(process.cwd(), "tests", "temp-test.spec.ts");
65
41
  await (0, utils_1.addImportForMethod)(absFilePath, "recordTest");
@@ -84,7 +60,7 @@ async function runRecorder({ name }) {
84
60
  IPC_FILE_SERVICE_PORT: availablePort.toString(),
85
61
  },
86
62
  });
87
- const videoPaths = extractVideoAttachments(repoDir);
63
+ const videoPaths = (0, upload_1.extractVideoAttachments)(repoDir);
88
64
  let attachments = [];
89
65
  if (videoPaths.length === 0) {
90
66
  console.warn("No video attachments found for temp test");
@@ -101,6 +77,7 @@ async function runRecorder({ name }) {
101
77
  await fileServer.stop();
102
78
  const finalCode = await (0, display_1.displayResultsAndConfirm)(name, codegenResult);
103
79
  await (0, request_1.sendToDashboardAsRequest)({
80
+ repoName,
104
81
  testName: name,
105
82
  codegenResult: finalCode,
106
83
  attachments,
@@ -1,4 +1,5 @@
1
- export declare function sendToDashboardAsRequest({ testName, codegenResult, attachments, }: {
1
+ export declare function sendToDashboardAsRequest({ repoName, testName, codegenResult, attachments, }: {
2
+ repoName: string;
2
3
  testName: string;
3
4
  codegenResult: string;
4
5
  attachments: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/recorder/request.ts"],"names":[],"mappings":"AAeA,wBAAsB,wBAAwB,CAAC,EAC7C,QAAQ,EACR,aAAa,EACb,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,iBAKA"}
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/recorder/request.ts"],"names":[],"mappings":"AAcA,wBAAsB,wBAAwB,CAAC,EAC7C,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,iBAMA"}
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sendToDashboardAsRequest = sendToDashboardAsRequest;
4
- const DASHBOARD_DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
4
+ const api_client_1 = require("../auth/api-client");
5
5
  const title = (name) => `Add a test: ${name}`;
6
6
  function description(codegenResult, attachments) {
7
7
  return [
@@ -12,42 +12,37 @@ function description(codegenResult, attachments) {
12
12
  ...attachments,
13
13
  ].join("\n\n");
14
14
  }
15
- async function sendToDashboardAsRequest({ testName, codegenResult, attachments, }) {
15
+ async function sendToDashboardAsRequest({ repoName, testName, codegenResult, attachments, }) {
16
16
  return createRequest({
17
+ repoName,
17
18
  title: title(testName),
18
19
  description: description(codegenResult, attachments),
19
20
  });
20
21
  }
21
- async function createRequest({ title, description, }) {
22
- if (!DASHBOARD_DOMAIN) {
23
- console.warn("DASHBOARD_DOMAIN not set, skipping request creation");
24
- return;
25
- }
26
- if (!process.env.EMPIRICALRUN_API_KEY) {
27
- console.warn("EMPIRICALRUN_API_KEY not set, skipping request creation");
28
- return;
29
- }
22
+ async function createRequest({ repoName, title, description, }) {
30
23
  try {
24
+ // TODO: Fix the source and sourceIdentifier
31
25
  const source = "cli";
32
26
  const sourceIdentifier = "random-string";
33
- const response = await fetch(`${DASHBOARD_DOMAIN}/api/requests`, {
27
+ const response = await api_client_1.apiClient.request("/api/requests", {
34
28
  method: "POST",
35
- headers: {
36
- "Content-Type": "application/json",
37
- Authorization: `Bearer ${process.env.EMPIRICALRUN_API_KEY}`,
38
- },
39
29
  body: JSON.stringify({
40
30
  source,
41
31
  source_identifier: sourceIdentifier,
42
32
  title,
43
33
  description,
34
+ project_repo_name: repoName,
44
35
  }),
45
36
  });
46
37
  if (!response.ok) {
47
- throw new Error(`Failed to create request: ${response.statusText}`);
38
+ const errorMessage = await response.text();
39
+ throw new Error(`Failed to create request: ${errorMessage}`);
48
40
  }
49
41
  const data = await response.json();
50
- console.log("Request created successfully:", data);
42
+ const dashboardDomain = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
43
+ const requestUrl = `${dashboardDomain}/${repoName}/requests/${data.data.request.id}`;
44
+ // console.log("Request created successfully:", data);
45
+ console.log(`View request: ${requestUrl}`);
51
46
  }
52
47
  catch (error) {
53
48
  console.error("Failed to create request:", error);
@@ -1,2 +1,3 @@
1
+ export declare function extractVideoAttachments(repoDir: string): string[];
1
2
  export declare function uploadVideosWithSpinner(videoPaths: string[], testName: string): Promise<void | string[]>;
2
3
  //# sourceMappingURL=upload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/recorder/upload.ts"],"names":[],"mappings":"AAgFA,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC,CAe1B"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/recorder/upload.ts"],"names":[],"mappings":"AAcA,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBjE;AA4DD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC,CAe1B"}
@@ -3,16 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractVideoAttachments = extractVideoAttachments;
6
7
  exports.uploadVideosWithSpinner = uploadVideosWithSpinner;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const ora_1 = __importDefault(require("ora"));
9
10
  const path_1 = __importDefault(require("path"));
11
+ const utils_1 = require("../artifacts/utils");
12
+ const api_client_1 = require("../auth/api-client");
10
13
  const slug_1 = require("../utils/slug");
11
14
  const ASSETS_PRODUCTION_BUCKET = "empirical-assets-production";
12
15
  const BUCKET_DOMAINS = {
13
16
  "empirical-assets-staging": "assets-staging.empirical.run",
14
17
  "empirical-assets-production": "assets.empirical.run",
15
18
  };
19
+ function extractVideoAttachments(repoDir) {
20
+ try {
21
+ const summaryPath = path_1.default.join(repoDir, "summary.json");
22
+ if (!fs_1.default.existsSync(summaryPath)) {
23
+ console.log("summary.json not found");
24
+ return [];
25
+ }
26
+ const summaryContent = JSON.parse(fs_1.default.readFileSync(summaryPath, "utf-8"));
27
+ const attachments = (0, utils_1.extractAttachmentsFromPlaywrightJSONReport)(summaryContent, "temp test");
28
+ const videoPaths = attachments
29
+ .filter((attachment) => attachment.contentType === "video/webm")
30
+ .map((attachment) => attachment.path);
31
+ return videoPaths;
32
+ }
33
+ catch (error) {
34
+ console.warn("Error processing summary.json:", error);
35
+ return [];
36
+ }
37
+ }
16
38
  function buildVideoUrl(localPath, directory, bucket) {
17
39
  const domain = BUCKET_DOMAINS[bucket];
18
40
  const fileName = path_1.default.basename(localPath);
@@ -22,12 +44,6 @@ async function uploadVideos(videoPaths, testSlug) {
22
44
  if (videoPaths.length === 0) {
23
45
  return null;
24
46
  }
25
- const DASHBOARD_DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
26
- const API_KEY = process.env.EMPIRICALRUN_API_KEY;
27
- if (!API_KEY) {
28
- console.warn("EMPIRICALRUN_API_KEY not set, skipping video upload");
29
- return null;
30
- }
31
47
  const uploadDestinationDir = `recorder-uploads/${testSlug}`;
32
48
  const bucket = ASSETS_PRODUCTION_BUCKET;
33
49
  try {
@@ -42,11 +58,8 @@ async function uploadVideos(videoPaths, testSlug) {
42
58
  formData.append("files", blob, fileName);
43
59
  }
44
60
  }
45
- const response = await fetch(`${DASHBOARD_DOMAIN}/api/upload`, {
61
+ const response = await api_client_1.apiClient.request("/api/upload", {
46
62
  method: "POST",
47
- headers: {
48
- Authorization: `Bearer ${API_KEY}`,
49
- },
50
63
  body: formData,
51
64
  });
52
65
  if (!response.ok) {
@@ -1,2 +1,2 @@
1
- export declare function validate(repoDir: string): Promise<void>;
1
+ export declare function validatePackageJson(repoDir: string): Promise<any>;
2
2
  //# sourceMappingURL=validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/recorder/validation.ts"],"names":[],"mappings":"AAGA,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,iBAoB7C"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/recorder/validation.ts"],"names":[],"mappings":"AAGA,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,gBAyBxD"}
@@ -3,16 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.validate = validate;
6
+ exports.validatePackageJson = validatePackageJson;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- async function validate(repoDir) {
10
- // Check if package.json exists
9
+ async function validatePackageJson(repoDir) {
11
10
  const packageJsonPath = path_1.default.join(repoDir, "package.json");
12
11
  if (!fs_1.default.existsSync(packageJsonPath)) {
13
12
  throw new Error("package.json not found in the repository");
14
13
  }
15
- // Check if playwright is installed
16
14
  const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8"));
17
15
  const hasPlaywright = (packageJson.dependencies &&
18
16
  packageJson.dependencies["@playwright/test"]) ||
@@ -21,4 +19,11 @@ async function validate(repoDir) {
21
19
  if (!hasPlaywright) {
22
20
  throw new Error("Playwright is not installed. Please install @playwright/test");
23
21
  }
22
+ const repoName = packageJson.name;
23
+ if (repoName && repoName.endsWith("-tests")) {
24
+ return repoName;
25
+ }
26
+ else {
27
+ throw new Error(`Invalid repo name in package.json: ${repoName}`);
28
+ }
24
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.66.2",
3
+ "version": "0.68.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/env-variables.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"}