@supatest/cli 0.0.41 → 0.0.43

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.
Files changed (2) hide show
  1. package/dist/index.js +1334 -1388
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -313,7 +313,7 @@ Use these commands in interactive mode (type them and press Enter):
313
313
  - Choose between "Supatest Managed" (default) or "Claude Max"
314
314
  - Supatest Managed: Uses models through Supatest infrastructure
315
315
  - Claude Max: Uses your Claude subscription directly
316
- - Requires Claude Code login for Claude Max
316
+ - Authentication flow starts automatically when selecting Claude Max
317
317
  - Available on macOS, Linux, and Windows
318
318
 
319
319
  - **/mcp** - Show configured MCP servers
@@ -5423,7 +5423,9 @@ var init_shared_es = __esm({
5423
5423
  limit: coerce.number().min(1).max(100).default(50),
5424
5424
  status: testResultStatusSchema.optional(),
5425
5425
  isFlaky: coerce.boolean().optional(),
5426
- file: stringType().optional()
5426
+ file: stringType().optional(),
5427
+ groupByTestId: coerce.boolean().optional()
5428
+ // When true, paginate by unique testId (grouped tests)
5427
5429
  });
5428
5430
  runsListResponseSchema = objectType({
5429
5431
  runs: arrayType(runSchema),
@@ -5439,7 +5441,9 @@ var init_shared_es = __esm({
5439
5441
  tests: arrayType(testSchema),
5440
5442
  total: numberType(),
5441
5443
  page: numberType(),
5442
- limit: numberType()
5444
+ limit: numberType(),
5445
+ totalGroups: numberType().optional()
5446
+ // When groupByTestId=true, total unique test groups
5443
5447
  });
5444
5448
  testDetailResponseSchema = testSchema.extend({
5445
5449
  results: arrayType(
@@ -6054,703 +6058,121 @@ var init_shared_es = __esm({
6054
6058
  }
6055
6059
  });
6056
6060
 
6057
- // src/utils/claude-oauth.ts
6058
- var claude_oauth_exports = {};
6059
- __export(claude_oauth_exports, {
6060
- ClaudeOAuthService: () => ClaudeOAuthService
6061
- });
6062
- import { spawn } from "child_process";
6063
- import { createHash, randomBytes } from "crypto";
6064
- import http from "http";
6065
- import { platform } from "os";
6066
- var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS, ClaudeOAuthService;
6067
- var init_claude_oauth = __esm({
6068
- "src/utils/claude-oauth.ts"() {
6069
- "use strict";
6070
- OAUTH_CONFIG = {
6071
- clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
6072
- // Claude Code's client ID
6073
- authorizationEndpoint: "https://claude.ai/oauth/authorize",
6074
- tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
6075
- redirectUri: "http://localhost:8421/callback",
6076
- // Local callback for CLI
6077
- scopes: ["user:inference", "user:profile", "org:create_api_key"]
6061
+ // src/commands/setup.ts
6062
+ import { execSync, spawn, spawnSync } from "child_process";
6063
+ import fs from "fs";
6064
+ import os from "os";
6065
+ import path from "path";
6066
+ function parseVersion(versionString) {
6067
+ const cleaned = versionString.trim().replace(/^v/, "");
6068
+ const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
6069
+ if (!match) {
6070
+ return null;
6071
+ }
6072
+ return {
6073
+ major: Number.parseInt(match[1], 10),
6074
+ minor: Number.parseInt(match[2], 10),
6075
+ patch: Number.parseInt(match[3], 10),
6076
+ raw: versionString.trim()
6077
+ };
6078
+ }
6079
+ function getNodeVersion() {
6080
+ try {
6081
+ const versionOutput = execSync("node --version", {
6082
+ encoding: "utf-8",
6083
+ stdio: ["ignore", "pipe", "ignore"]
6084
+ });
6085
+ return parseVersion(versionOutput);
6086
+ } catch {
6087
+ return null;
6088
+ }
6089
+ }
6090
+ function getPlaywrightVersion() {
6091
+ try {
6092
+ const result = spawnSync("npx", ["playwright", "--version"], {
6093
+ encoding: "utf-8",
6094
+ stdio: ["ignore", "pipe", "ignore"],
6095
+ shell: true
6096
+ // Required for Windows where npx is npx.cmd
6097
+ });
6098
+ if (result.status === 0 && result.stdout) {
6099
+ return result.stdout.trim().replace("Version ", "");
6100
+ }
6101
+ return null;
6102
+ } catch {
6103
+ return null;
6104
+ }
6105
+ }
6106
+ function getPlaywrightCachePath() {
6107
+ const homeDir = os.homedir();
6108
+ const cachePaths = [
6109
+ path.join(homeDir, "Library", "Caches", "ms-playwright"),
6110
+ // macOS
6111
+ path.join(homeDir, ".cache", "ms-playwright"),
6112
+ // Linux
6113
+ path.join(homeDir, "AppData", "Local", "ms-playwright")
6114
+ // Windows
6115
+ ];
6116
+ for (const cachePath of cachePaths) {
6117
+ if (fs.existsSync(cachePath)) {
6118
+ return cachePath;
6119
+ }
6120
+ }
6121
+ return null;
6122
+ }
6123
+ function getInstalledChromiumVersion() {
6124
+ const cachePath = getPlaywrightCachePath();
6125
+ if (!cachePath) return null;
6126
+ try {
6127
+ const entries = fs.readdirSync(cachePath);
6128
+ const chromiumVersions = entries.filter((entry) => entry.startsWith("chromium-") && !entry.includes("headless")).map((entry) => entry.replace("chromium-", "")).sort((a, b) => Number(b) - Number(a));
6129
+ return chromiumVersions[0] || null;
6130
+ } catch {
6131
+ return null;
6132
+ }
6133
+ }
6134
+ function isChromiumInstalled() {
6135
+ return getInstalledChromiumVersion() !== null;
6136
+ }
6137
+ function checkNodeVersion() {
6138
+ const nodeVersion = getNodeVersion();
6139
+ if (!nodeVersion) {
6140
+ return {
6141
+ ok: false,
6142
+ message: `Node.js is not installed. Please install Node.js ${MINIMUM_NODE_VERSION} or higher.`,
6143
+ version: null
6078
6144
  };
6079
- CALLBACK_PORT = 8421;
6080
- CALLBACK_TIMEOUT_MS = 3e5;
6081
- ClaudeOAuthService = class _ClaudeOAuthService {
6082
- secretStorage;
6083
- static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
6084
- // 5 minutes
6085
- pendingCodeVerifier = null;
6086
- // Store code verifier for PKCE
6087
- constructor(secretStorage) {
6088
- this.secretStorage = secretStorage;
6089
- }
6090
- /**
6091
- * Starts the OAuth authorization flow
6092
- * Opens the default browser for user authentication
6093
- * Returns after successful authentication
6094
- */
6095
- async authorize() {
6096
- try {
6097
- const state = this.generateRandomState();
6098
- const pkce = this.generatePKCEChallenge();
6099
- this.pendingCodeVerifier = pkce.codeVerifier;
6100
- const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
6101
- console.log("\nAuthenticating with Claude...\n");
6102
- console.log(`Opening browser to: ${authUrl}
6103
- `);
6104
- const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
6105
- try {
6106
- this.openBrowser(authUrl);
6107
- } catch (error) {
6108
- console.warn("Failed to open browser automatically:", error);
6109
- console.log(`
6110
- Please manually open this URL in your browser:
6111
- ${authUrl}
6112
- `);
6113
- }
6114
- await tokenPromise;
6115
- console.log("\n\u2705 Successfully authenticated with Claude!\n");
6116
- return { success: true };
6117
- } catch (error) {
6118
- this.pendingCodeVerifier = null;
6119
- return {
6120
- success: false,
6121
- error: error instanceof Error ? error.message : "Authentication failed"
6122
- };
6123
- }
6124
- }
6125
- /**
6126
- * Start local HTTP server to receive OAuth callback
6127
- */
6128
- startCallbackServer(port, expectedState) {
6129
- return new Promise((resolve2, reject) => {
6130
- const server = http.createServer(async (req, res) => {
6131
- if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
6132
- res.writeHead(404);
6133
- res.end("Not Found");
6134
- return;
6135
- }
6136
- const url = new URL(req.url, `http://localhost:${port}`);
6137
- const code = url.searchParams.get("code");
6138
- const returnedState = url.searchParams.get("state");
6139
- const error = url.searchParams.get("error");
6140
- if (error) {
6141
- res.writeHead(200, { "Content-Type": "text/html" });
6142
- res.end(this.buildErrorPage(error));
6143
- server.close();
6144
- reject(new Error(`OAuth error: ${error}`));
6145
- return;
6146
- }
6147
- if (returnedState !== expectedState) {
6148
- const errorMsg = "Security error: state parameter mismatch";
6149
- res.writeHead(200, { "Content-Type": "text/html" });
6150
- res.end(this.buildErrorPage(errorMsg));
6151
- server.close();
6152
- reject(new Error(errorMsg));
6153
- return;
6154
- }
6155
- if (!code) {
6156
- const errorMsg = "No authorization code received";
6157
- res.writeHead(400, { "Content-Type": "text/html" });
6158
- res.end(this.buildErrorPage(errorMsg));
6159
- server.close();
6160
- reject(new Error(errorMsg));
6161
- return;
6162
- }
6163
- try {
6164
- await this.submitAuthCode(code, returnedState);
6165
- res.writeHead(200, { "Content-Type": "text/html" });
6166
- res.end(this.buildSuccessPage());
6167
- server.close();
6168
- resolve2();
6169
- } catch (err) {
6170
- const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
6171
- res.writeHead(200, { "Content-Type": "text/html" });
6172
- res.end(this.buildErrorPage(errorMsg));
6173
- server.close();
6174
- reject(err);
6175
- }
6176
- });
6177
- server.on("error", (error) => {
6178
- if (error.code === "EADDRINUSE") {
6179
- reject(new Error("Port already in use. Please try again."));
6180
- } else {
6181
- reject(error);
6182
- }
6183
- });
6184
- const timeout = setTimeout(() => {
6185
- server.close();
6186
- reject(new Error("Authentication timeout - no response received after 5 minutes"));
6187
- }, CALLBACK_TIMEOUT_MS);
6188
- server.on("close", () => {
6189
- clearTimeout(timeout);
6190
- });
6191
- server.listen(port, "127.0.0.1", () => {
6192
- console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
6193
- });
6194
- });
6195
- }
6196
- /**
6197
- * Submits the authorization code and exchanges it for tokens
6198
- */
6199
- async submitAuthCode(code, state) {
6200
- const tokens = await this.exchangeCodeForTokens(code, state);
6201
- await this.saveTokens(tokens);
6202
- return tokens;
6203
- }
6204
- /**
6205
- * Exchanges authorization code for access and refresh tokens
6206
- */
6207
- async exchangeCodeForTokens(code, state) {
6208
- if (!this.pendingCodeVerifier) {
6209
- throw new Error("No PKCE code verifier found. Please start the auth flow first.");
6210
- }
6211
- const body = {
6212
- grant_type: "authorization_code",
6213
- code,
6214
- state,
6215
- // Non-standard: state in body
6216
- redirect_uri: OAUTH_CONFIG.redirectUri,
6217
- client_id: OAUTH_CONFIG.clientId,
6218
- code_verifier: this.pendingCodeVerifier
6219
- // PKCE verifier
6220
- };
6221
- const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6222
- method: "POST",
6223
- headers: {
6224
- "Content-Type": "application/json"
6225
- // Non-standard: JSON instead of form-encoded
6226
- },
6227
- body: JSON.stringify(body)
6228
- });
6229
- this.pendingCodeVerifier = null;
6230
- if (!response.ok) {
6231
- const error = await response.text();
6232
- throw new Error(`Token exchange failed: ${error}`);
6233
- }
6234
- const data = await response.json();
6235
- return {
6236
- accessToken: data.access_token,
6237
- refreshToken: data.refresh_token,
6238
- expiresAt: Date.now() + data.expires_in * 1e3
6239
- };
6240
- }
6241
- /**
6242
- * Refreshes the access token using the refresh token
6243
- */
6244
- async refreshTokens() {
6245
- const tokens = await this.getTokens();
6246
- if (!tokens) {
6247
- throw new Error("No tokens found to refresh");
6248
- }
6249
- const body = {
6250
- grant_type: "refresh_token",
6251
- refresh_token: tokens.refreshToken,
6252
- client_id: OAUTH_CONFIG.clientId
6253
- };
6254
- const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
6255
- method: "POST",
6256
- headers: {
6257
- "Content-Type": "application/json"
6258
- },
6259
- body: JSON.stringify(body)
6260
- });
6261
- if (!response.ok) {
6262
- const error = await response.text();
6263
- throw new Error(`Token refresh failed: ${error}`);
6264
- }
6265
- const data = await response.json();
6266
- const newTokens = {
6267
- accessToken: data.access_token,
6268
- refreshToken: data.refresh_token,
6269
- expiresAt: Date.now() + data.expires_in * 1e3
6270
- };
6271
- await this.saveTokens(newTokens);
6272
- return newTokens;
6273
- }
6274
- /**
6275
- * Gets the current access token, refreshing if necessary
6276
- */
6277
- async getAccessToken() {
6278
- const tokens = await this.getTokens();
6279
- if (!tokens) {
6280
- return null;
6281
- }
6282
- if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
6283
- try {
6284
- const refreshedTokens = await this.refreshTokens();
6285
- return refreshedTokens.accessToken;
6286
- } catch (error) {
6287
- console.warn("Token refresh failed:", error);
6288
- return null;
6289
- }
6290
- }
6291
- return tokens.accessToken;
6292
- }
6293
- /**
6294
- * Gets the stored tokens
6295
- */
6296
- async getTokens() {
6297
- try {
6298
- const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
6299
- const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
6300
- const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
6301
- if (!accessToken || !refreshToken || !expiresAt) {
6302
- return null;
6303
- }
6304
- return {
6305
- accessToken,
6306
- refreshToken,
6307
- expiresAt: parseInt(expiresAt, 10)
6308
- };
6309
- } catch (error) {
6310
- console.error("Failed to get OAuth tokens:", error);
6311
- return null;
6312
- }
6313
- }
6314
- /**
6315
- * Saves OAuth tokens to secure storage
6316
- */
6317
- async saveTokens(tokens) {
6318
- await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
6319
- await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
6320
- await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
6321
- }
6322
- /**
6323
- * Deletes stored OAuth tokens
6324
- */
6325
- async deleteTokens() {
6326
- await this.secretStorage.deleteSecret("claude_oauth_access_token");
6327
- await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
6328
- await this.secretStorage.deleteSecret("claude_oauth_expires_at");
6329
- }
6330
- /**
6331
- * Checks if user is authenticated via OAuth
6332
- */
6333
- async isAuthenticated() {
6334
- const tokens = await this.getTokens();
6335
- return tokens !== null;
6336
- }
6337
- /**
6338
- * Gets the current OAuth authentication status
6339
- */
6340
- async getStatus() {
6341
- try {
6342
- const tokens = await this.getTokens();
6343
- if (!tokens) {
6344
- return { isAuthenticated: false };
6345
- }
6346
- return {
6347
- isAuthenticated: true,
6348
- expiresAt: tokens.expiresAt
6349
- };
6350
- } catch (error) {
6351
- return {
6352
- isAuthenticated: false,
6353
- error: error instanceof Error ? error.message : "Failed to get OAuth status"
6354
- };
6355
- }
6356
- }
6357
- /**
6358
- * Builds the authorization URL with all required parameters
6359
- */
6360
- buildAuthorizationUrl(state, codeChallenge) {
6361
- const params = new URLSearchParams({
6362
- response_type: "code",
6363
- client_id: OAUTH_CONFIG.clientId,
6364
- redirect_uri: OAUTH_CONFIG.redirectUri,
6365
- scope: OAUTH_CONFIG.scopes.join(" "),
6366
- state
6367
- });
6368
- if (codeChallenge) {
6369
- params.set("code_challenge", codeChallenge);
6370
- params.set("code_challenge_method", "S256");
6371
- }
6372
- return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
6373
- }
6374
- /**
6375
- * Generates a random state string for CSRF protection
6376
- */
6377
- generateRandomState() {
6378
- return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
6379
- }
6380
- /**
6381
- * Generates PKCE code verifier and challenge
6382
- * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
6383
- */
6384
- generatePKCEChallenge() {
6385
- const codeVerifier = randomBytes(32).toString("base64url");
6386
- const hash = createHash("sha256").update(codeVerifier).digest("base64url");
6387
- return {
6388
- codeVerifier,
6389
- codeChallenge: hash
6390
- };
6391
- }
6392
- /**
6393
- * Open a URL in the default browser cross-platform
6394
- */
6395
- openBrowser(url) {
6396
- const os3 = platform();
6397
- let command;
6398
- let args;
6399
- switch (os3) {
6400
- case "darwin":
6401
- command = "open";
6402
- args = [url];
6403
- break;
6404
- case "win32":
6405
- command = "start";
6406
- args = ["", url];
6407
- break;
6408
- default:
6409
- command = "xdg-open";
6410
- args = [url];
6411
- break;
6412
- }
6413
- const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
6414
- spawn(command, args, options).unref();
6415
- }
6416
- /**
6417
- * Build success HTML page
6418
- */
6419
- buildSuccessPage() {
6420
- return `
6421
- <!DOCTYPE html>
6422
- <html lang="en">
6423
- <head>
6424
- <meta charset="UTF-8" />
6425
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6426
- <title>Authentication Successful - Supatest CLI</title>
6427
- <style>
6428
- body {
6429
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6430
- display: flex;
6431
- align-items: center;
6432
- justify-content: center;
6433
- height: 100vh;
6434
- margin: 0;
6435
- background: #fefefe;
6436
- }
6437
- .container {
6438
- background: white;
6439
- padding: 3rem 2rem;
6440
- border-radius: 12px;
6441
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6442
- border: 1px solid #e5e7eb;
6443
- text-align: center;
6444
- max-width: 400px;
6445
- }
6446
- .success-icon {
6447
- font-size: 48px;
6448
- margin-bottom: 1rem;
6449
- }
6450
- h1 {
6451
- color: #10b981;
6452
- margin: 0 0 1rem 0;
6453
- font-size: 24px;
6454
- }
6455
- p {
6456
- color: #666;
6457
- margin: 0;
6458
- line-height: 1.5;
6459
- }
6460
- </style>
6461
- </head>
6462
- <body>
6463
- <div class="container">
6464
- <div class="success-icon">\u2705</div>
6465
- <h1>Authentication Successful!</h1>
6466
- <p>You're now authenticated with Claude.</p>
6467
- <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
6468
- </div>
6469
- </body>
6470
- </html>
6471
- `;
6472
- }
6473
- /**
6474
- * Build error HTML page
6475
- */
6476
- buildErrorPage(errorMessage) {
6477
- const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
6478
- return `
6479
- <!DOCTYPE html>
6480
- <html lang="en">
6481
- <head>
6482
- <meta charset="UTF-8" />
6483
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6484
- <title>Authentication Failed - Supatest CLI</title>
6485
- <style>
6486
- body {
6487
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
6488
- display: flex;
6489
- align-items: center;
6490
- justify-content: center;
6491
- height: 100vh;
6492
- margin: 0;
6493
- background: #fefefe;
6494
- }
6495
- .container {
6496
- background: white;
6497
- padding: 3rem 2rem;
6498
- border-radius: 12px;
6499
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
6500
- border: 1px solid #e5e7eb;
6501
- text-align: center;
6502
- max-width: 400px;
6503
- }
6504
- .error-icon {
6505
- font-size: 48px;
6506
- margin-bottom: 1rem;
6507
- }
6508
- h1 {
6509
- color: #dc2626;
6510
- margin: 0 0 1rem 0;
6511
- font-size: 24px;
6512
- }
6513
- p {
6514
- color: #666;
6515
- margin: 0;
6516
- line-height: 1.5;
6517
- }
6518
- </style>
6519
- </head>
6520
- <body>
6521
- <div class="container">
6522
- <div class="error-icon">\u274C</div>
6523
- <h1>Authentication Failed</h1>
6524
- <p>${escapedError}</p>
6525
- <p style="margin-top: 1rem;">You can close this window and try again.</p>
6526
- </div>
6527
- </body>
6528
- </html>
6529
- `;
6530
- }
6531
- };
6532
- }
6533
- });
6534
-
6535
- // src/utils/secret-storage.ts
6536
- var secret_storage_exports = {};
6537
- __export(secret_storage_exports, {
6538
- deleteSecret: () => deleteSecret,
6539
- getSecret: () => getSecret,
6540
- getSecretStorage: () => getSecretStorage,
6541
- listSecrets: () => listSecrets,
6542
- setSecret: () => setSecret
6543
- });
6544
- import { promises as fs } from "fs";
6545
- import { homedir } from "os";
6546
- import { dirname, join } from "path";
6547
- async function getSecret(key) {
6548
- return storage.getSecret(key);
6549
- }
6550
- async function setSecret(key, value) {
6551
- await storage.setSecret(key, value);
6552
- }
6553
- async function deleteSecret(key) {
6554
- return storage.deleteSecret(key);
6555
- }
6556
- async function listSecrets() {
6557
- return storage.listSecrets();
6558
- }
6559
- function getSecretStorage() {
6560
- return storage;
6561
- }
6562
- var SECRET_FILE_NAME, FileSecretStorage, storage;
6563
- var init_secret_storage = __esm({
6564
- "src/utils/secret-storage.ts"() {
6565
- "use strict";
6566
- SECRET_FILE_NAME = "secrets.json";
6567
- FileSecretStorage = class {
6568
- secretFilePath;
6569
- constructor() {
6570
- const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
6571
- const secretsDir = join(homedir(), rootDirName, "claude-auth");
6572
- this.secretFilePath = join(secretsDir, SECRET_FILE_NAME);
6573
- }
6574
- async ensureDirectoryExists() {
6575
- const dir = dirname(this.secretFilePath);
6576
- await fs.mkdir(dir, { recursive: true, mode: 448 });
6577
- }
6578
- async loadSecrets() {
6579
- try {
6580
- const data = await fs.readFile(this.secretFilePath, "utf-8");
6581
- const secrets = JSON.parse(data);
6582
- return new Map(Object.entries(secrets));
6583
- } catch (error) {
6584
- const err = error;
6585
- if (err.code === "ENOENT") {
6586
- return /* @__PURE__ */ new Map();
6587
- }
6588
- try {
6589
- await fs.unlink(this.secretFilePath);
6590
- } catch {
6591
- }
6592
- return /* @__PURE__ */ new Map();
6593
- }
6594
- }
6595
- async saveSecrets(secrets) {
6596
- await this.ensureDirectoryExists();
6597
- const data = Object.fromEntries(secrets);
6598
- const json = JSON.stringify(data, null, 2);
6599
- await fs.writeFile(this.secretFilePath, json, { mode: 384 });
6600
- }
6601
- async getSecret(key) {
6602
- const secrets = await this.loadSecrets();
6603
- return secrets.get(key) ?? null;
6604
- }
6605
- async setSecret(key, value) {
6606
- const secrets = await this.loadSecrets();
6607
- secrets.set(key, value);
6608
- await this.saveSecrets(secrets);
6609
- }
6610
- async deleteSecret(key) {
6611
- const secrets = await this.loadSecrets();
6612
- if (!secrets.has(key)) {
6613
- return false;
6614
- }
6615
- secrets.delete(key);
6616
- if (secrets.size === 0) {
6617
- try {
6618
- await fs.unlink(this.secretFilePath);
6619
- } catch (error) {
6620
- const err = error;
6621
- if (err.code !== "ENOENT") {
6622
- throw error;
6623
- }
6624
- }
6625
- } else {
6626
- await this.saveSecrets(secrets);
6627
- }
6628
- return true;
6629
- }
6630
- async listSecrets() {
6631
- const secrets = await this.loadSecrets();
6632
- return Array.from(secrets.keys());
6633
- }
6634
- };
6635
- storage = new FileSecretStorage();
6636
- }
6637
- });
6638
-
6639
- // src/commands/setup.ts
6640
- import { execSync, spawn as spawn2, spawnSync } from "child_process";
6641
- import fs2 from "fs";
6642
- import os from "os";
6643
- import path from "path";
6644
- function parseVersion(versionString) {
6645
- const cleaned = versionString.trim().replace(/^v/, "");
6646
- const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
6647
- if (!match) {
6648
- return null;
6649
- }
6650
- return {
6651
- major: Number.parseInt(match[1], 10),
6652
- minor: Number.parseInt(match[2], 10),
6653
- patch: Number.parseInt(match[3], 10),
6654
- raw: versionString.trim()
6655
- };
6656
- }
6657
- function getNodeVersion() {
6658
- try {
6659
- const versionOutput = execSync("node --version", {
6660
- encoding: "utf-8",
6661
- stdio: ["ignore", "pipe", "ignore"]
6662
- });
6663
- return parseVersion(versionOutput);
6664
- } catch {
6665
- return null;
6666
- }
6667
- }
6668
- function getPlaywrightVersion() {
6669
- try {
6670
- const result = spawnSync("npx", ["playwright", "--version"], {
6671
- encoding: "utf-8",
6672
- stdio: ["ignore", "pipe", "ignore"],
6673
- shell: true
6674
- // Required for Windows where npx is npx.cmd
6675
- });
6676
- if (result.status === 0 && result.stdout) {
6677
- return result.stdout.trim().replace("Version ", "");
6678
- }
6679
- return null;
6680
- } catch {
6681
- return null;
6682
- }
6683
- }
6684
- function getPlaywrightCachePath() {
6685
- const homeDir = os.homedir();
6686
- const cachePaths = [
6687
- path.join(homeDir, "Library", "Caches", "ms-playwright"),
6688
- // macOS
6689
- path.join(homeDir, ".cache", "ms-playwright"),
6690
- // Linux
6691
- path.join(homeDir, "AppData", "Local", "ms-playwright")
6692
- // Windows
6693
- ];
6694
- for (const cachePath of cachePaths) {
6695
- if (fs2.existsSync(cachePath)) {
6696
- return cachePath;
6697
- }
6698
- }
6699
- return null;
6700
- }
6701
- function getInstalledChromiumVersion() {
6702
- const cachePath = getPlaywrightCachePath();
6703
- if (!cachePath) return null;
6704
- try {
6705
- const entries = fs2.readdirSync(cachePath);
6706
- const chromiumVersions = entries.filter((entry) => entry.startsWith("chromium-") && !entry.includes("headless")).map((entry) => entry.replace("chromium-", "")).sort((a, b) => Number(b) - Number(a));
6707
- return chromiumVersions[0] || null;
6708
- } catch {
6709
- return null;
6710
- }
6711
- }
6712
- function isChromiumInstalled() {
6713
- return getInstalledChromiumVersion() !== null;
6714
- }
6715
- function checkNodeVersion() {
6716
- const nodeVersion = getNodeVersion();
6717
- if (!nodeVersion) {
6718
- return {
6719
- ok: false,
6720
- message: `Node.js is not installed. Please install Node.js ${MINIMUM_NODE_VERSION} or higher.`,
6721
- version: null
6722
- };
6723
- }
6724
- if (nodeVersion.major < MINIMUM_NODE_VERSION) {
6725
- return {
6726
- ok: false,
6727
- message: `Node.js ${nodeVersion.raw} is installed, but version ${MINIMUM_NODE_VERSION} or higher is required.`,
6728
- version: nodeVersion.raw
6729
- };
6730
- }
6731
- return {
6732
- ok: true,
6733
- message: `Node.js ${nodeVersion.raw}`,
6734
- version: nodeVersion.raw
6735
- };
6736
- }
6737
- async function installChromium() {
6738
- return new Promise((resolve2) => {
6739
- const child = spawn2("npx", ["playwright", "install", "chromium"], {
6740
- stdio: "inherit",
6741
- shell: true
6742
- // Required for Windows where npx is npx.cmd
6743
- });
6744
- child.on("close", (code) => {
6745
- if (code === 0) {
6746
- resolve2({
6747
- ok: true,
6748
- message: "Chromium browser installed successfully."
6749
- });
6750
- } else {
6751
- resolve2({
6752
- ok: false,
6753
- message: `Playwright install exited with code ${code}`
6145
+ }
6146
+ if (nodeVersion.major < MINIMUM_NODE_VERSION) {
6147
+ return {
6148
+ ok: false,
6149
+ message: `Node.js ${nodeVersion.raw} is installed, but version ${MINIMUM_NODE_VERSION} or higher is required.`,
6150
+ version: nodeVersion.raw
6151
+ };
6152
+ }
6153
+ return {
6154
+ ok: true,
6155
+ message: `Node.js ${nodeVersion.raw}`,
6156
+ version: nodeVersion.raw
6157
+ };
6158
+ }
6159
+ async function installChromium() {
6160
+ return new Promise((resolve2) => {
6161
+ const child = spawn("npx", ["playwright", "install", "chromium"], {
6162
+ stdio: "inherit",
6163
+ shell: true
6164
+ // Required for Windows where npx is npx.cmd
6165
+ });
6166
+ child.on("close", (code) => {
6167
+ if (code === 0) {
6168
+ resolve2({
6169
+ ok: true,
6170
+ message: "Chromium browser installed successfully."
6171
+ });
6172
+ } else {
6173
+ resolve2({
6174
+ ok: false,
6175
+ message: `Playwright install exited with code ${code}`
6754
6176
  });
6755
6177
  }
6756
6178
  });
@@ -6766,14 +6188,14 @@ function createSupatestConfig(cwd) {
6766
6188
  const supatestDir = path.join(cwd, ".supatest");
6767
6189
  const mcpJsonPath = path.join(supatestDir, "mcp.json");
6768
6190
  try {
6769
- if (!fs2.existsSync(supatestDir)) {
6770
- fs2.mkdirSync(supatestDir, { recursive: true });
6191
+ if (!fs.existsSync(supatestDir)) {
6192
+ fs.mkdirSync(supatestDir, { recursive: true });
6771
6193
  }
6772
6194
  let config2;
6773
6195
  let fileExisted = false;
6774
- if (fs2.existsSync(mcpJsonPath)) {
6196
+ if (fs.existsSync(mcpJsonPath)) {
6775
6197
  fileExisted = true;
6776
- const existingContent = fs2.readFileSync(mcpJsonPath, "utf-8");
6198
+ const existingContent = fs.readFileSync(mcpJsonPath, "utf-8");
6777
6199
  config2 = JSON.parse(existingContent);
6778
6200
  } else {
6779
6201
  config2 = {};
@@ -6784,7 +6206,7 @@ function createSupatestConfig(cwd) {
6784
6206
  if (!config2.mcpServers.playwright) {
6785
6207
  config2.mcpServers.playwright = DEFAULT_MCP_CONFIG.mcpServers.playwright;
6786
6208
  }
6787
- fs2.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6209
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6788
6210
  if (fileExisted) {
6789
6211
  return {
6790
6212
  ok: true,
@@ -6911,18 +6333,18 @@ var CLI_VERSION;
6911
6333
  var init_version = __esm({
6912
6334
  "src/version.ts"() {
6913
6335
  "use strict";
6914
- CLI_VERSION = "0.0.41";
6336
+ CLI_VERSION = "0.0.43";
6915
6337
  }
6916
6338
  });
6917
6339
 
6918
6340
  // src/utils/error-logger.ts
6919
- import * as fs3 from "fs";
6341
+ import * as fs2 from "fs";
6920
6342
  import * as os2 from "os";
6921
6343
  import * as path2 from "path";
6922
6344
  function ensureLogDir() {
6923
6345
  try {
6924
- if (!fs3.existsSync(LOGS_DIR)) {
6925
- fs3.mkdirSync(LOGS_DIR, { recursive: true });
6346
+ if (!fs2.existsSync(LOGS_DIR)) {
6347
+ fs2.mkdirSync(LOGS_DIR, { recursive: true });
6926
6348
  }
6927
6349
  return true;
6928
6350
  } catch {
@@ -6931,14 +6353,14 @@ function ensureLogDir() {
6931
6353
  }
6932
6354
  function rotateLogIfNeeded() {
6933
6355
  try {
6934
- if (!fs3.existsSync(ERROR_LOG_FILE)) return;
6935
- const stats = fs3.statSync(ERROR_LOG_FILE);
6356
+ if (!fs2.existsSync(ERROR_LOG_FILE)) return;
6357
+ const stats = fs2.statSync(ERROR_LOG_FILE);
6936
6358
  if (stats.size > MAX_LOG_SIZE) {
6937
6359
  const oldLogFile = `${ERROR_LOG_FILE}.old`;
6938
- if (fs3.existsSync(oldLogFile)) {
6939
- fs3.unlinkSync(oldLogFile);
6360
+ if (fs2.existsSync(oldLogFile)) {
6361
+ fs2.unlinkSync(oldLogFile);
6940
6362
  }
6941
- fs3.renameSync(ERROR_LOG_FILE, oldLogFile);
6363
+ fs2.renameSync(ERROR_LOG_FILE, oldLogFile);
6942
6364
  }
6943
6365
  } catch {
6944
6366
  }
@@ -6974,7 +6396,7 @@ function logError(error, context) {
6974
6396
  const logLine = `${JSON.stringify(entry)}
6975
6397
  `;
6976
6398
  try {
6977
- fs3.appendFileSync(ERROR_LOG_FILE, logLine);
6399
+ fs2.appendFileSync(ERROR_LOG_FILE, logLine);
6978
6400
  } catch {
6979
6401
  }
6980
6402
  }
@@ -6991,7 +6413,7 @@ var init_error_logger = __esm({
6991
6413
  });
6992
6414
 
6993
6415
  // src/utils/logger.ts
6994
- import * as fs4 from "fs";
6416
+ import * as fs3 from "fs";
6995
6417
  import * as path3 from "path";
6996
6418
  import chalk from "chalk";
6997
6419
  var Logger, logger;
@@ -7025,7 +6447,7 @@ ${"=".repeat(80)}
7025
6447
  ${"=".repeat(80)}
7026
6448
  `;
7027
6449
  try {
7028
- fs4.appendFileSync(this.logFile, separator);
6450
+ fs3.appendFileSync(this.logFile, separator);
7029
6451
  } catch (error) {
7030
6452
  }
7031
6453
  }
@@ -7039,7 +6461,7 @@ ${"=".repeat(80)}
7039
6461
  ` : `[${timestamp}] [${level}] ${message}
7040
6462
  `;
7041
6463
  try {
7042
- fs4.appendFileSync(this.logFile, logEntry);
6464
+ fs3.appendFileSync(this.logFile, logEntry);
7043
6465
  } catch (error) {
7044
6466
  }
7045
6467
  }
@@ -7785,7 +7207,7 @@ var init_api_client = __esm({
7785
7207
 
7786
7208
  // src/utils/command-discovery.ts
7787
7209
  import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
7788
- import { join as join4, relative } from "path";
7210
+ import { join as join3, relative } from "path";
7789
7211
  function parseMarkdownFrontmatter(content) {
7790
7212
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
7791
7213
  const match = content.match(frontmatterRegex);
@@ -7810,7 +7232,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
7810
7232
  }
7811
7233
  const entries = readdirSync(dir);
7812
7234
  for (const entry of entries) {
7813
- const fullPath = join4(dir, entry);
7235
+ const fullPath = join3(dir, entry);
7814
7236
  const stat = statSync2(fullPath);
7815
7237
  if (stat.isDirectory()) {
7816
7238
  discoverMarkdownFiles(fullPath, baseDir, files);
@@ -7821,7 +7243,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
7821
7243
  return files;
7822
7244
  }
7823
7245
  function discoverCommands(cwd) {
7824
- const commandsDir = join4(cwd, ".supatest", "commands");
7246
+ const commandsDir = join3(cwd, ".supatest", "commands");
7825
7247
  if (!existsSync2(commandsDir)) {
7826
7248
  return [];
7827
7249
  }
@@ -7844,9 +7266,9 @@ function discoverCommands(cwd) {
7844
7266
  return commands.sort((a, b) => a.name.localeCompare(b.name));
7845
7267
  }
7846
7268
  function expandCommand(cwd, commandName, args) {
7847
- const commandsDir = join4(cwd, ".supatest", "commands");
7269
+ const commandsDir = join3(cwd, ".supatest", "commands");
7848
7270
  const relativePath = commandName.replace(/\./g, "/") + ".md";
7849
- const filePath = join4(commandsDir, relativePath);
7271
+ const filePath = join3(commandsDir, relativePath);
7850
7272
  if (!existsSync2(filePath)) {
7851
7273
  return null;
7852
7274
  }
@@ -7869,7 +7291,7 @@ function expandCommand(cwd, commandName, args) {
7869
7291
  }
7870
7292
  }
7871
7293
  function discoverAgents(cwd) {
7872
- const agentsDir = join4(cwd, ".supatest", "agents");
7294
+ const agentsDir = join3(cwd, ".supatest", "agents");
7873
7295
  if (!existsSync2(agentsDir)) {
7874
7296
  return [];
7875
7297
  }
@@ -7900,8 +7322,8 @@ var init_command_discovery = __esm({
7900
7322
 
7901
7323
  // src/utils/mcp-loader.ts
7902
7324
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
7903
- import { homedir as homedir3 } from "os";
7904
- import { join as join5 } from "path";
7325
+ import { homedir as homedir2 } from "os";
7326
+ import { join as join4 } from "path";
7905
7327
  function expandEnvVar(value) {
7906
7328
  return value.replace(/\$\{([^}]+)\}/g, (_, expr) => {
7907
7329
  const [varName, defaultValue] = expr.split(":-");
@@ -7950,9 +7372,9 @@ function loadMcpServersFromFile(mcpPath) {
7950
7372
  }
7951
7373
  }
7952
7374
  function loadMcpServers(cwd) {
7953
- const globalMcpPath = join5(homedir3(), ".supatest", "mcp.json");
7375
+ const globalMcpPath = join4(homedir2(), ".supatest", "mcp.json");
7954
7376
  const globalServers = loadMcpServersFromFile(globalMcpPath);
7955
- const projectMcpPath = join5(cwd, ".supatest", "mcp.json");
7377
+ const projectMcpPath = join4(cwd, ".supatest", "mcp.json");
7956
7378
  const projectServers = loadMcpServersFromFile(projectMcpPath);
7957
7379
  return { ...globalServers, ...projectServers };
7958
7380
  }
@@ -7964,11 +7386,11 @@ var init_mcp_loader = __esm({
7964
7386
 
7965
7387
  // src/utils/project-instructions.ts
7966
7388
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
7967
- import { join as join6 } from "path";
7389
+ import { join as join5 } from "path";
7968
7390
  function loadProjectInstructions(cwd) {
7969
7391
  const paths = [
7970
- join6(cwd, "SUPATEST.md"),
7971
- join6(cwd, ".supatest", "SUPATEST.md")
7392
+ join5(cwd, "SUPATEST.md"),
7393
+ join5(cwd, ".supatest", "SUPATEST.md")
7972
7394
  ];
7973
7395
  for (const path6 of paths) {
7974
7396
  if (existsSync4(path6)) {
@@ -7988,8 +7410,8 @@ var init_project_instructions = __esm({
7988
7410
 
7989
7411
  // src/core/agent.ts
7990
7412
  import { createRequire } from "module";
7991
- import { homedir as homedir4 } from "os";
7992
- import { dirname as dirname2, join as join7 } from "path";
7413
+ import { homedir as homedir3 } from "os";
7414
+ import { dirname, join as join6 } from "path";
7993
7415
  import { query } from "@anthropic-ai/claude-agent-sdk";
7994
7416
  var CoreAgent;
7995
7417
  var init_agent = __esm({
@@ -8119,7 +7541,7 @@ ${projectInstructions}`,
8119
7541
  this.presenter.onLog(`Auth: Using Claude Max (default Claude Code credentials)`);
8120
7542
  logger.debug("[agent] Claude Max mode: Using default ~/.claude/ config, cleared provider credentials");
8121
7543
  } else {
8122
- const internalConfigDir = join7(homedir4(), ".supatest", "claude-internal");
7544
+ const internalConfigDir = join6(homedir3(), ".supatest", "claude-internal");
8123
7545
  cleanEnv.CLAUDE_CONFIG_DIR = internalConfigDir;
8124
7546
  cleanEnv.ANTHROPIC_API_KEY = config2.supatestApiKey || "";
8125
7547
  cleanEnv.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || "";
@@ -8449,7 +7871,7 @@ ${projectInstructions}`,
8449
7871
  let claudeCodePath;
8450
7872
  const require2 = createRequire(import.meta.url);
8451
7873
  const sdkPath = require2.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
8452
- claudeCodePath = join7(dirname2(sdkPath), "cli.js");
7874
+ claudeCodePath = join6(dirname(sdkPath), "cli.js");
8453
7875
  this.presenter.onLog(`Using SDK CLI: ${claudeCodePath}`);
8454
7876
  if (config.claudeCodeExecutablePath) {
8455
7877
  claudeCodePath = config.claudeCodeExecutablePath;
@@ -10143,642 +9565,1234 @@ function createInkStdio() {
10143
9565
  if (prop === "write") {
10144
9566
  return writeToStdout;
10145
9567
  }
10146
- const value = Reflect.get(target, prop, receiver);
10147
- if (typeof value === "function") {
10148
- return value.bind(target);
9568
+ const value = Reflect.get(target, prop, receiver);
9569
+ if (typeof value === "function") {
9570
+ return value.bind(target);
9571
+ }
9572
+ return value;
9573
+ }
9574
+ });
9575
+ const inkStderr = new Proxy(process.stderr, {
9576
+ get(target, prop, receiver) {
9577
+ if (prop === "write") {
9578
+ return writeToStderr;
9579
+ }
9580
+ const value = Reflect.get(target, prop, receiver);
9581
+ if (typeof value === "function") {
9582
+ return value.bind(target);
9583
+ }
9584
+ return value;
9585
+ }
9586
+ });
9587
+ return { stdout: inkStdout, stderr: inkStderr };
9588
+ }
9589
+ function clearTerminalViewportAndScrollback() {
9590
+ writeToStdout("\x1B[3J\x1B[H\x1B[2J");
9591
+ }
9592
+ var originalStdoutWrite, originalStderrWrite;
9593
+ var init_stdio = __esm({
9594
+ "src/utils/stdio.ts"() {
9595
+ "use strict";
9596
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
9597
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
9598
+ }
9599
+ });
9600
+
9601
+ // src/utils/encryption.ts
9602
+ import crypto2 from "crypto";
9603
+ import { hostname, userInfo } from "os";
9604
+ function deriveEncryptionKey() {
9605
+ const host = hostname();
9606
+ const user = userInfo().username;
9607
+ const salt = `${host}-${user}-supatest-cli`;
9608
+ if (process.env.DEBUG_ENCRYPTION) {
9609
+ console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
9610
+ }
9611
+ return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
9612
+ }
9613
+ function getEncryptionKey() {
9614
+ if (!cachedKey) {
9615
+ cachedKey = deriveEncryptionKey();
9616
+ }
9617
+ return cachedKey;
9618
+ }
9619
+ function encrypt(plaintext) {
9620
+ const key = getEncryptionKey();
9621
+ const iv = crypto2.randomBytes(IV_LENGTH);
9622
+ const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
9623
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
9624
+ encrypted += cipher.final("hex");
9625
+ const authTag = cipher.getAuthTag();
9626
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
9627
+ }
9628
+ function decrypt(encryptedData) {
9629
+ const parts = encryptedData.split(":");
9630
+ if (parts.length !== 3) {
9631
+ throw new Error("Invalid encrypted data format");
9632
+ }
9633
+ const [ivHex, authTagHex, encrypted] = parts;
9634
+ const iv = Buffer.from(ivHex, "hex");
9635
+ const authTag = Buffer.from(authTagHex, "hex");
9636
+ const key = getEncryptionKey();
9637
+ const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
9638
+ decipher.setAuthTag(authTag);
9639
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
9640
+ decrypted += decipher.final("utf8");
9641
+ return decrypted;
9642
+ }
9643
+ var ALGORITHM, KEY_LENGTH, IV_LENGTH, cachedKey;
9644
+ var init_encryption = __esm({
9645
+ "src/utils/encryption.ts"() {
9646
+ "use strict";
9647
+ ALGORITHM = "aes-256-gcm";
9648
+ KEY_LENGTH = 32;
9649
+ IV_LENGTH = 16;
9650
+ cachedKey = null;
9651
+ }
9652
+ });
9653
+
9654
+ // src/utils/token-storage.ts
9655
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
9656
+ import { homedir as homedir5 } from "os";
9657
+ import { join as join7 } from "path";
9658
+ function getTokenFilePath() {
9659
+ const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
9660
+ if (apiUrl === PRODUCTION_API_URL) {
9661
+ return join7(CONFIG_DIR, "token.json");
9662
+ }
9663
+ return join7(CONFIG_DIR, "token.local.json");
9664
+ }
9665
+ function isV2Format(stored) {
9666
+ return "version" in stored && stored.version === 2;
9667
+ }
9668
+ function ensureConfigDir() {
9669
+ if (!existsSync5(CONFIG_DIR)) {
9670
+ mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
9671
+ }
9672
+ }
9673
+ function saveToken(token, expiresAt) {
9674
+ ensureConfigDir();
9675
+ const payload = {
9676
+ token,
9677
+ expiresAt,
9678
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
9679
+ };
9680
+ const stored = {
9681
+ version: STORAGE_VERSION,
9682
+ encryptedData: encrypt(JSON.stringify(payload))
9683
+ };
9684
+ const tokenFile = getTokenFilePath();
9685
+ writeFileSync(tokenFile, JSON.stringify(stored, null, 2), { mode: 384 });
9686
+ }
9687
+ function loadToken() {
9688
+ const tokenFile = getTokenFilePath();
9689
+ if (!existsSync5(tokenFile)) {
9690
+ return null;
9691
+ }
9692
+ try {
9693
+ const data = readFileSync4(tokenFile, "utf8");
9694
+ const stored = JSON.parse(data);
9695
+ let payload;
9696
+ if (isV2Format(stored)) {
9697
+ payload = JSON.parse(decrypt(stored.encryptedData));
9698
+ } else {
9699
+ payload = stored;
9700
+ }
9701
+ if (payload.expiresAt) {
9702
+ const expiresAt = new Date(payload.expiresAt);
9703
+ if (expiresAt < /* @__PURE__ */ new Date()) {
9704
+ console.warn("CLI token has expired. Please run 'supatest login' again.");
9705
+ return null;
9706
+ }
9707
+ }
9708
+ return payload.token;
9709
+ } catch (error) {
9710
+ const err = error;
9711
+ if (err.message?.includes("Invalid encrypted data format") || err.message?.includes("Unsupported state or unable to authenticate data")) {
9712
+ try {
9713
+ unlinkSync2(tokenFile);
9714
+ } catch {
9715
+ }
9716
+ }
9717
+ return null;
9718
+ }
9719
+ }
9720
+ function removeToken() {
9721
+ const tokenFile = getTokenFilePath();
9722
+ if (existsSync5(tokenFile)) {
9723
+ unlinkSync2(tokenFile);
9724
+ }
9725
+ }
9726
+ var CONFIG_DIR, PRODUCTION_API_URL, STORAGE_VERSION, TOKEN_FILE;
9727
+ var init_token_storage = __esm({
9728
+ "src/utils/token-storage.ts"() {
9729
+ "use strict";
9730
+ init_encryption();
9731
+ CONFIG_DIR = join7(homedir5(), ".supatest");
9732
+ PRODUCTION_API_URL = "https://code-api.supatest.ai";
9733
+ STORAGE_VERSION = 2;
9734
+ TOKEN_FILE = join7(CONFIG_DIR, "token.json");
9735
+ }
9736
+ });
9737
+
9738
+ // src/core/message-bridge.ts
9739
+ var MessageBridge;
9740
+ var init_message_bridge = __esm({
9741
+ "src/core/message-bridge.ts"() {
9742
+ "use strict";
9743
+ MessageBridge = class {
9744
+ queue = [];
9745
+ resolvers = [];
9746
+ closed = false;
9747
+ sessionId;
9748
+ constructor(sessionId) {
9749
+ this.sessionId = sessionId;
9750
+ }
9751
+ /**
9752
+ * Update the session ID (useful when session is created after bridge).
9753
+ */
9754
+ setSessionId(sessionId) {
9755
+ this.sessionId = sessionId;
10149
9756
  }
10150
- return value;
10151
- }
10152
- });
10153
- const inkStderr = new Proxy(process.stderr, {
10154
- get(target, prop, receiver) {
10155
- if (prop === "write") {
10156
- return writeToStderr;
9757
+ /**
9758
+ * Push a user message to be injected into the session.
9759
+ * Call this from the UI when user submits a message during agent execution.
9760
+ */
9761
+ push(text) {
9762
+ if (this.closed) {
9763
+ console.warn("[MessageBridge] Cannot push to closed bridge");
9764
+ return;
9765
+ }
9766
+ const message = {
9767
+ type: "user",
9768
+ message: {
9769
+ role: "user",
9770
+ content: [{ type: "text", text }]
9771
+ },
9772
+ parent_tool_use_id: null,
9773
+ session_id: this.sessionId
9774
+ };
9775
+ const resolver = this.resolvers.shift();
9776
+ if (resolver) {
9777
+ resolver({ value: message, done: false });
9778
+ } else {
9779
+ this.queue.push(message);
9780
+ }
10157
9781
  }
10158
- const value = Reflect.get(target, prop, receiver);
10159
- if (typeof value === "function") {
10160
- return value.bind(target);
9782
+ /**
9783
+ * Check if there are pending messages in the queue.
9784
+ */
9785
+ hasPending() {
9786
+ return this.queue.length > 0;
10161
9787
  }
10162
- return value;
10163
- }
10164
- });
10165
- return { stdout: inkStdout, stderr: inkStderr };
10166
- }
10167
- function clearTerminalViewportAndScrollback() {
10168
- writeToStdout("\x1B[3J\x1B[H\x1B[2J");
10169
- }
10170
- var originalStdoutWrite, originalStderrWrite;
10171
- var init_stdio = __esm({
10172
- "src/utils/stdio.ts"() {
10173
- "use strict";
10174
- originalStdoutWrite = process.stdout.write.bind(process.stdout);
10175
- originalStderrWrite = process.stderr.write.bind(process.stderr);
9788
+ /**
9789
+ * Close the bridge. No more messages will be accepted.
9790
+ * Any pending async iterators will receive done: true.
9791
+ */
9792
+ close() {
9793
+ if (this.closed) return;
9794
+ this.closed = true;
9795
+ for (const resolver of this.resolvers) {
9796
+ resolver({ value: void 0, done: true });
9797
+ }
9798
+ this.resolvers = [];
9799
+ }
9800
+ /**
9801
+ * Check if the bridge is closed.
9802
+ */
9803
+ isClosed() {
9804
+ return this.closed;
9805
+ }
9806
+ [Symbol.asyncIterator]() {
9807
+ return {
9808
+ next: () => {
9809
+ const queued = this.queue.shift();
9810
+ if (queued) {
9811
+ return Promise.resolve({ value: queued, done: false });
9812
+ }
9813
+ if (this.closed) {
9814
+ return Promise.resolve({
9815
+ value: void 0,
9816
+ done: true
9817
+ });
9818
+ }
9819
+ return new Promise((resolve2) => {
9820
+ this.resolvers.push(resolve2);
9821
+ });
9822
+ }
9823
+ };
9824
+ }
9825
+ };
10176
9826
  }
10177
9827
  });
10178
9828
 
10179
- // src/utils/encryption.ts
10180
- import crypto2 from "crypto";
10181
- import { hostname, userInfo } from "os";
10182
- function deriveEncryptionKey() {
10183
- const host = hostname();
10184
- const user = userInfo().username;
10185
- const salt = `${host}-${user}-supatest-cli`;
10186
- if (process.env.DEBUG_ENCRYPTION) {
10187
- console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
10188
- }
10189
- return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
10190
- }
10191
- function getEncryptionKey() {
10192
- if (!cachedKey) {
10193
- cachedKey = deriveEncryptionKey();
10194
- }
10195
- return cachedKey;
9829
+ // src/commands/login.ts
9830
+ import { spawn as spawn3 } from "child_process";
9831
+ import crypto3 from "crypto";
9832
+ import http from "http";
9833
+ import { platform } from "os";
9834
+ function escapeHtml(text) {
9835
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10196
9836
  }
10197
- function encrypt(plaintext) {
10198
- const key = getEncryptionKey();
10199
- const iv = crypto2.randomBytes(IV_LENGTH);
10200
- const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
10201
- let encrypted = cipher.update(plaintext, "utf8", "hex");
10202
- encrypted += cipher.final("hex");
10203
- const authTag = cipher.getAuthTag();
10204
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
9837
+ function generateState() {
9838
+ return crypto3.randomBytes(STATE_LENGTH).toString("hex");
10205
9839
  }
10206
- function decrypt(encryptedData) {
10207
- const parts = encryptedData.split(":");
10208
- if (parts.length !== 3) {
10209
- throw new Error("Invalid encrypted data format");
9840
+ async function exchangeCodeForToken(code, state) {
9841
+ const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
9842
+ method: "POST",
9843
+ headers: { "Content-Type": "application/json" },
9844
+ body: JSON.stringify({ code, state })
9845
+ });
9846
+ if (!response.ok) {
9847
+ const error = await response.json();
9848
+ throw new Error(error.error || "Failed to exchange code for token");
10210
9849
  }
10211
- const [ivHex, authTagHex, encrypted] = parts;
10212
- const iv = Buffer.from(ivHex, "hex");
10213
- const authTag = Buffer.from(authTagHex, "hex");
10214
- const key = getEncryptionKey();
10215
- const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
10216
- decipher.setAuthTag(authTag);
10217
- let decrypted = decipher.update(encrypted, "hex", "utf8");
10218
- decrypted += decipher.final("utf8");
10219
- return decrypted;
9850
+ const data = await response.json();
9851
+ return { token: data.token, expiresAt: data.expiresAt };
10220
9852
  }
10221
- var ALGORITHM, KEY_LENGTH, IV_LENGTH, cachedKey;
10222
- var init_encryption = __esm({
10223
- "src/utils/encryption.ts"() {
10224
- "use strict";
10225
- ALGORITHM = "aes-256-gcm";
10226
- KEY_LENGTH = 32;
10227
- IV_LENGTH = 16;
10228
- cachedKey = null;
10229
- }
10230
- });
10231
-
10232
- // src/utils/token-storage.ts
10233
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
10234
- import { homedir as homedir6 } from "os";
10235
- import { join as join8 } from "path";
10236
- function getTokenFilePath() {
10237
- const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
10238
- if (apiUrl === PRODUCTION_API_URL) {
10239
- return join8(CONFIG_DIR, "token.json");
9853
+ function escapeForCmd(value) {
9854
+ return value.replace(/[&^]/g, "^$&");
9855
+ }
9856
+ function openBrowser(url) {
9857
+ const os3 = platform();
9858
+ let command;
9859
+ let args;
9860
+ switch (os3) {
9861
+ case "darwin":
9862
+ command = "open";
9863
+ args = [url];
9864
+ break;
9865
+ case "win32":
9866
+ command = "cmd.exe";
9867
+ args = ["/c", "start", '""', escapeForCmd(url)];
9868
+ break;
9869
+ default:
9870
+ command = "xdg-open";
9871
+ args = [url];
10240
9872
  }
10241
- return join8(CONFIG_DIR, "token.local.json");
9873
+ const options = { detached: true, stdio: "ignore" };
9874
+ spawn3(command, args, options).unref();
10242
9875
  }
10243
- function isV2Format(stored) {
10244
- return "version" in stored && stored.version === 2;
9876
+ function startCallbackServer(port, expectedState) {
9877
+ return new Promise((resolve2, reject) => {
9878
+ const server = http.createServer(async (req, res) => {
9879
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
9880
+ res.writeHead(404);
9881
+ res.end("Not Found");
9882
+ return;
9883
+ }
9884
+ const url = new URL(req.url, `http://localhost:${port}`);
9885
+ const code = url.searchParams.get("code");
9886
+ const state = url.searchParams.get("state");
9887
+ const error = url.searchParams.get("error");
9888
+ if (error) {
9889
+ res.writeHead(200, { "Content-Type": "text/html" });
9890
+ res.end(buildErrorPage(error));
9891
+ server.close();
9892
+ reject(new Error(error));
9893
+ return;
9894
+ }
9895
+ if (state !== expectedState) {
9896
+ const errorMsg = "Security error: state parameter mismatch";
9897
+ res.writeHead(200, { "Content-Type": "text/html" });
9898
+ res.end(buildErrorPage(errorMsg));
9899
+ server.close();
9900
+ reject(new Error(errorMsg));
9901
+ return;
9902
+ }
9903
+ if (!code) {
9904
+ const errorMsg = "No authorization code received";
9905
+ res.writeHead(400, { "Content-Type": "text/html" });
9906
+ res.end(buildErrorPage(errorMsg));
9907
+ server.close();
9908
+ reject(new Error(errorMsg));
9909
+ return;
9910
+ }
9911
+ try {
9912
+ const result = await exchangeCodeForToken(code, state);
9913
+ res.writeHead(200, { "Content-Type": "text/html" });
9914
+ res.end(buildSuccessPage());
9915
+ server.close();
9916
+ resolve2(result);
9917
+ } catch (err) {
9918
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
9919
+ res.writeHead(200, { "Content-Type": "text/html" });
9920
+ res.end(buildErrorPage(errorMsg));
9921
+ server.close();
9922
+ reject(err);
9923
+ }
9924
+ });
9925
+ server.on("error", (error) => {
9926
+ if (error.code === "EADDRINUSE") {
9927
+ const portError = new Error(
9928
+ "Something went wrong. Please restart the CLI and try again."
9929
+ );
9930
+ portError.code = "EADDRINUSE";
9931
+ reject(portError);
9932
+ } else {
9933
+ reject(error);
9934
+ }
9935
+ });
9936
+ const timeout = setTimeout(() => {
9937
+ server.close();
9938
+ reject(new Error("Login timeout - no response received after 5 minutes"));
9939
+ }, CALLBACK_TIMEOUT_MS);
9940
+ server.on("close", () => {
9941
+ clearTimeout(timeout);
9942
+ });
9943
+ server.listen(port, "127.0.0.1", () => {
9944
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
9945
+ });
9946
+ });
10245
9947
  }
10246
- function ensureConfigDir() {
10247
- if (!existsSync5(CONFIG_DIR)) {
10248
- mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
10249
- }
9948
+ function buildSuccessPage() {
9949
+ return `
9950
+ <!DOCTYPE html>
9951
+ <html lang="en">
9952
+ <head>
9953
+ <meta charset="UTF-8" />
9954
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9955
+ <title>Login Successful - Supatest CLI</title>
9956
+ <style>
9957
+ body {
9958
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9959
+ display: flex;
9960
+ align-items: center;
9961
+ justify-content: center;
9962
+ height: 100vh;
9963
+ margin: 0;
9964
+ background: #fefefe;
9965
+ }
9966
+ .container {
9967
+ background: white;
9968
+ padding: 3rem 2rem;
9969
+ border-radius: 12px;
9970
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
9971
+ border: 1px solid #e5e7eb;
9972
+ text-align: center;
9973
+ max-width: 400px;
9974
+ width: 90%;
9975
+ }
9976
+ .logo {
9977
+ margin: 0 auto 2rem;
9978
+ display: flex;
9979
+ align-items: center;
9980
+ justify-content: center;
9981
+ gap: 8px;
9982
+ }
9983
+ .logo img {
9984
+ width: 24px;
9985
+ height: 24px;
9986
+ border-radius: 6px;
9987
+ object-fit: contain;
9988
+ }
9989
+ .logo-text {
9990
+ font-weight: 600;
9991
+ font-size: 16px;
9992
+ color: inherit;
9993
+ }
9994
+ .success-icon {
9995
+ width: 64px;
9996
+ height: 64px;
9997
+ border-radius: 50%;
9998
+ background: #d1fae5;
9999
+ display: flex;
10000
+ align-items: center;
10001
+ justify-content: center;
10002
+ font-size: 32px;
10003
+ margin: 0 auto 1.5rem;
10004
+ }
10005
+ h1 {
10006
+ color: #10b981;
10007
+ margin: 0 0 1rem 0;
10008
+ font-size: 24px;
10009
+ font-weight: 600;
10010
+ }
10011
+ p {
10012
+ color: #666;
10013
+ margin: 0;
10014
+ line-height: 1.5;
10015
+ }
10016
+ @media (prefers-color-scheme: dark) {
10017
+ body {
10018
+ background: #1a1a1a;
10019
+ }
10020
+ .container {
10021
+ background: #1f1f1f;
10022
+ border-color: #333;
10023
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10024
+ }
10025
+ .success-icon {
10026
+ background: #064e3b;
10027
+ }
10028
+ h1 {
10029
+ color: #34d399;
10030
+ }
10031
+ p {
10032
+ color: #a3a3a3;
10033
+ }
10034
+ .logo-text {
10035
+ color: #e5e7eb;
10036
+ }
10037
+ }
10038
+ </style>
10039
+ </head>
10040
+ <body>
10041
+ <div class="container">
10042
+ <div class="logo">
10043
+ <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10044
+ <span class="logo-text">Supatest</span>
10045
+ </div>
10046
+ <div class="success-icon" role="img" aria-label="Success">\u2705</div>
10047
+ <h1>Login Successful!</h1>
10048
+ <p>You're now authenticated with Supatest CLI.</p>
10049
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10050
+ </div>
10051
+ </body>
10052
+ </html>
10053
+ `;
10250
10054
  }
10251
- function saveToken(token, expiresAt) {
10252
- ensureConfigDir();
10253
- const payload = {
10254
- token,
10255
- expiresAt,
10256
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
10257
- };
10258
- const stored = {
10259
- version: STORAGE_VERSION,
10260
- encryptedData: encrypt(JSON.stringify(payload))
10261
- };
10262
- const tokenFile = getTokenFilePath();
10263
- writeFileSync(tokenFile, JSON.stringify(stored, null, 2), { mode: 384 });
10055
+ function buildErrorPage(errorMessage) {
10056
+ return `
10057
+ <!DOCTYPE html>
10058
+ <html lang="en">
10059
+ <head>
10060
+ <meta charset="UTF-8" />
10061
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10062
+ <title>Login Failed - Supatest CLI</title>
10063
+ <style>
10064
+ body {
10065
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10066
+ display: flex;
10067
+ align-items: center;
10068
+ justify-content: center;
10069
+ height: 100vh;
10070
+ margin: 0;
10071
+ background: #fefefe;
10072
+ }
10073
+ .container {
10074
+ background: white;
10075
+ padding: 3rem 2rem;
10076
+ border-radius: 12px;
10077
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10078
+ border: 1px solid #e5e7eb;
10079
+ text-align: center;
10080
+ max-width: 400px;
10081
+ width: 90%;
10082
+ }
10083
+ .logo {
10084
+ width: 48px;
10085
+ height: 48px;
10086
+ margin: 0 auto 2rem;
10087
+ display: flex;
10088
+ align-items: center;
10089
+ justify-content: center;
10090
+ }
10091
+ .logo img {
10092
+ width: 48px;
10093
+ height: 48px;
10094
+ border-radius: 8px;
10095
+ object-fit: contain;
10096
+ }
10097
+ .error-icon {
10098
+ width: 64px;
10099
+ height: 64px;
10100
+ border-radius: 50%;
10101
+ background: #fee2e2;
10102
+ display: flex;
10103
+ align-items: center;
10104
+ justify-content: center;
10105
+ font-size: 32px;
10106
+ margin: 0 auto 1.5rem;
10107
+ }
10108
+ h1 {
10109
+ color: #dc2626;
10110
+ margin: 0 0 1rem 0;
10111
+ font-size: 24px;
10112
+ font-weight: 600;
10113
+ }
10114
+ p {
10115
+ color: #666;
10116
+ margin: 0;
10117
+ line-height: 1.5;
10118
+ }
10119
+ .brand {
10120
+ margin-top: 2rem;
10121
+ padding-top: 1.5rem;
10122
+ border-top: 1px solid #e5e7eb;
10123
+ color: #9333ff;
10124
+ font-weight: 600;
10125
+ font-size: 14px;
10126
+ }
10127
+ @media (prefers-color-scheme: dark) {
10128
+ body {
10129
+ background: #1a1a1a;
10130
+ }
10131
+ .container {
10132
+ background: #1f1f1f;
10133
+ border-color: #333;
10134
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10135
+ }
10136
+ .error-icon {
10137
+ background: #7f1d1d;
10138
+ }
10139
+ h1 {
10140
+ color: #f87171;
10141
+ }
10142
+ p {
10143
+ color: #a3a3a3;
10144
+ }
10145
+ .brand {
10146
+ border-top-color: #333;
10147
+ color: #a855f7;
10148
+ }
10149
+ }
10150
+ </style>
10151
+ </head>
10152
+ <body>
10153
+ <div class="container">
10154
+ <div class="logo">
10155
+ <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10156
+ <span class="logo-text">Supatest</span>
10157
+ </div>
10158
+ <div class="error-icon" role="img" aria-label="Error">\u274C</div>
10159
+ <h1>Login Failed</h1>
10160
+ <p>${escapeHtml(errorMessage)}</p>
10161
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
10162
+ </div>
10163
+ </body>
10164
+ </html>
10165
+ `;
10264
10166
  }
10265
- function loadToken() {
10266
- const tokenFile = getTokenFilePath();
10267
- if (!existsSync5(tokenFile)) {
10268
- return null;
10167
+ async function loginCommand() {
10168
+ console.log("\nAuthenticating with Supatest...\n");
10169
+ const state = generateState();
10170
+ const loginPromise = startCallbackServer(CLI_LOGIN_PORT, state);
10171
+ const loginUrl = `${FRONTEND_URL}/cli-login?port=${CLI_LOGIN_PORT}&state=${state}`;
10172
+ console.log(`Opening browser to: ${loginUrl}`);
10173
+ console.log("\nIf your browser doesn't open automatically, please visit the URL above.\n");
10174
+ try {
10175
+ openBrowser(loginUrl);
10176
+ } catch (error) {
10177
+ console.warn("Failed to open browser automatically:", error);
10178
+ console.log(`
10179
+ Please manually open this URL in your browser:
10180
+ ${loginUrl}
10181
+ `);
10269
10182
  }
10270
10183
  try {
10271
- const data = readFileSync4(tokenFile, "utf8");
10272
- const stored = JSON.parse(data);
10273
- let payload;
10274
- if (isV2Format(stored)) {
10275
- payload = JSON.parse(decrypt(stored.encryptedData));
10276
- } else {
10277
- payload = stored;
10278
- }
10279
- if (payload.expiresAt) {
10280
- const expiresAt = new Date(payload.expiresAt);
10281
- if (expiresAt < /* @__PURE__ */ new Date()) {
10282
- console.warn("CLI token has expired. Please run 'supatest login' again.");
10283
- return null;
10284
- }
10285
- }
10286
- return payload.token;
10184
+ const result = await loginPromise;
10185
+ console.log("\n\u2705 Login successful!\n");
10186
+ return result;
10287
10187
  } catch (error) {
10288
10188
  const err = error;
10289
- if (err.message?.includes("Invalid encrypted data format") || err.message?.includes("Unsupported state or unable to authenticate data")) {
10290
- try {
10291
- unlinkSync2(tokenFile);
10292
- } catch {
10293
- }
10189
+ if (err.code === "EADDRINUSE") {
10190
+ console.error("\n\u274C Login failed: Something went wrong.");
10191
+ console.error(" Please restart the CLI and try again.\n");
10192
+ } else {
10193
+ console.error("\n\u274C Login failed:", error.message, "\n");
10294
10194
  }
10295
- return null;
10296
- }
10297
- }
10298
- function removeToken() {
10299
- const tokenFile = getTokenFilePath();
10300
- if (existsSync5(tokenFile)) {
10301
- unlinkSync2(tokenFile);
10195
+ throw error;
10302
10196
  }
10303
10197
  }
10304
- var CONFIG_DIR, PRODUCTION_API_URL, STORAGE_VERSION, TOKEN_FILE;
10305
- var init_token_storage = __esm({
10306
- "src/utils/token-storage.ts"() {
10198
+ var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS, STATE_LENGTH;
10199
+ var init_login = __esm({
10200
+ "src/commands/login.ts"() {
10307
10201
  "use strict";
10308
- init_encryption();
10309
- CONFIG_DIR = join8(homedir6(), ".supatest");
10310
- PRODUCTION_API_URL = "https://code-api.supatest.ai";
10311
- STORAGE_VERSION = 2;
10312
- TOKEN_FILE = join8(CONFIG_DIR, "token.json");
10202
+ CLI_LOGIN_PORT = 8420;
10203
+ FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
10204
+ API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
10205
+ CALLBACK_TIMEOUT_MS = 3e5;
10206
+ STATE_LENGTH = 32;
10313
10207
  }
10314
10208
  });
10315
10209
 
10316
- // src/core/message-bridge.ts
10317
- var MessageBridge;
10318
- var init_message_bridge = __esm({
10319
- "src/core/message-bridge.ts"() {
10210
+ // src/utils/claude-oauth.ts
10211
+ var claude_oauth_exports = {};
10212
+ __export(claude_oauth_exports, {
10213
+ ClaudeOAuthService: () => ClaudeOAuthService
10214
+ });
10215
+ import { spawn as spawn4 } from "child_process";
10216
+ import { createHash, randomBytes } from "crypto";
10217
+ import http2 from "http";
10218
+ import { platform as platform2 } from "os";
10219
+ var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS2, ClaudeOAuthService;
10220
+ var init_claude_oauth = __esm({
10221
+ "src/utils/claude-oauth.ts"() {
10320
10222
  "use strict";
10321
- MessageBridge = class {
10322
- queue = [];
10323
- resolvers = [];
10324
- closed = false;
10325
- sessionId;
10326
- constructor(sessionId) {
10327
- this.sessionId = sessionId;
10223
+ OAUTH_CONFIG = {
10224
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
10225
+ // Claude Code's client ID
10226
+ authorizationEndpoint: "https://claude.ai/oauth/authorize",
10227
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
10228
+ redirectUri: "http://localhost:8421/callback",
10229
+ // Local callback for CLI
10230
+ scopes: ["user:inference", "user:profile", "org:create_api_key"]
10231
+ };
10232
+ CALLBACK_PORT = 8421;
10233
+ CALLBACK_TIMEOUT_MS2 = 3e5;
10234
+ ClaudeOAuthService = class _ClaudeOAuthService {
10235
+ secretStorage;
10236
+ static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
10237
+ // 5 minutes
10238
+ pendingCodeVerifier = null;
10239
+ // Store code verifier for PKCE
10240
+ constructor(secretStorage) {
10241
+ this.secretStorage = secretStorage;
10242
+ }
10243
+ /**
10244
+ * Starts the OAuth authorization flow
10245
+ * Opens the default browser for user authentication
10246
+ * Returns after successful authentication
10247
+ */
10248
+ async authorize() {
10249
+ try {
10250
+ const state = this.generateRandomState();
10251
+ const pkce = this.generatePKCEChallenge();
10252
+ this.pendingCodeVerifier = pkce.codeVerifier;
10253
+ const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
10254
+ console.log("\nAuthenticating with Claude...\n");
10255
+ console.log(`Opening browser to: ${authUrl}
10256
+ `);
10257
+ const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
10258
+ try {
10259
+ this.openBrowser(authUrl);
10260
+ } catch (error) {
10261
+ console.warn("Failed to open browser automatically:", error);
10262
+ console.log(`
10263
+ Please manually open this URL in your browser:
10264
+ ${authUrl}
10265
+ `);
10266
+ }
10267
+ await tokenPromise;
10268
+ console.log("\n\u2705 Successfully authenticated with Claude!\n");
10269
+ return { success: true };
10270
+ } catch (error) {
10271
+ this.pendingCodeVerifier = null;
10272
+ return {
10273
+ success: false,
10274
+ error: error instanceof Error ? error.message : "Authentication failed"
10275
+ };
10276
+ }
10277
+ }
10278
+ /**
10279
+ * Start local HTTP server to receive OAuth callback
10280
+ */
10281
+ startCallbackServer(port, expectedState) {
10282
+ return new Promise((resolve2, reject) => {
10283
+ const server = http2.createServer(async (req, res) => {
10284
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
10285
+ res.writeHead(404);
10286
+ res.end("Not Found");
10287
+ return;
10288
+ }
10289
+ const url = new URL(req.url, `http://localhost:${port}`);
10290
+ const code = url.searchParams.get("code");
10291
+ const returnedState = url.searchParams.get("state");
10292
+ const error = url.searchParams.get("error");
10293
+ if (error) {
10294
+ res.writeHead(200, { "Content-Type": "text/html" });
10295
+ res.end(this.buildErrorPage(error));
10296
+ server.close();
10297
+ reject(new Error(`OAuth error: ${error}`));
10298
+ return;
10299
+ }
10300
+ if (returnedState !== expectedState) {
10301
+ const errorMsg = "Security error: state parameter mismatch";
10302
+ res.writeHead(200, { "Content-Type": "text/html" });
10303
+ res.end(this.buildErrorPage(errorMsg));
10304
+ server.close();
10305
+ reject(new Error(errorMsg));
10306
+ return;
10307
+ }
10308
+ if (!code) {
10309
+ const errorMsg = "No authorization code received";
10310
+ res.writeHead(400, { "Content-Type": "text/html" });
10311
+ res.end(this.buildErrorPage(errorMsg));
10312
+ server.close();
10313
+ reject(new Error(errorMsg));
10314
+ return;
10315
+ }
10316
+ try {
10317
+ await this.submitAuthCode(code, returnedState);
10318
+ res.writeHead(200, { "Content-Type": "text/html" });
10319
+ res.end(this.buildSuccessPage());
10320
+ server.close();
10321
+ resolve2();
10322
+ } catch (err) {
10323
+ const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
10324
+ res.writeHead(200, { "Content-Type": "text/html" });
10325
+ res.end(this.buildErrorPage(errorMsg));
10326
+ server.close();
10327
+ reject(err);
10328
+ }
10329
+ });
10330
+ server.on("error", (error) => {
10331
+ if (error.code === "EADDRINUSE") {
10332
+ reject(new Error("Port already in use. Please try again."));
10333
+ } else {
10334
+ reject(error);
10335
+ }
10336
+ });
10337
+ const timeout = setTimeout(() => {
10338
+ server.close();
10339
+ reject(new Error("Authentication timeout - no response received after 5 minutes"));
10340
+ }, CALLBACK_TIMEOUT_MS2);
10341
+ server.on("close", () => {
10342
+ clearTimeout(timeout);
10343
+ });
10344
+ server.listen(port, "127.0.0.1", () => {
10345
+ console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
10346
+ });
10347
+ });
10348
+ }
10349
+ /**
10350
+ * Submits the authorization code and exchanges it for tokens
10351
+ */
10352
+ async submitAuthCode(code, state) {
10353
+ const tokens = await this.exchangeCodeForTokens(code, state);
10354
+ await this.saveTokens(tokens);
10355
+ return tokens;
10356
+ }
10357
+ /**
10358
+ * Exchanges authorization code for access and refresh tokens
10359
+ */
10360
+ async exchangeCodeForTokens(code, state) {
10361
+ if (!this.pendingCodeVerifier) {
10362
+ throw new Error("No PKCE code verifier found. Please start the auth flow first.");
10363
+ }
10364
+ const body = {
10365
+ grant_type: "authorization_code",
10366
+ code,
10367
+ state,
10368
+ // Non-standard: state in body
10369
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10370
+ client_id: OAUTH_CONFIG.clientId,
10371
+ code_verifier: this.pendingCodeVerifier
10372
+ // PKCE verifier
10373
+ };
10374
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10375
+ method: "POST",
10376
+ headers: {
10377
+ "Content-Type": "application/json"
10378
+ // Non-standard: JSON instead of form-encoded
10379
+ },
10380
+ body: JSON.stringify(body)
10381
+ });
10382
+ this.pendingCodeVerifier = null;
10383
+ if (!response.ok) {
10384
+ const error = await response.text();
10385
+ throw new Error(`Token exchange failed: ${error}`);
10386
+ }
10387
+ const data = await response.json();
10388
+ return {
10389
+ accessToken: data.access_token,
10390
+ refreshToken: data.refresh_token,
10391
+ expiresAt: Date.now() + data.expires_in * 1e3
10392
+ };
10393
+ }
10394
+ /**
10395
+ * Refreshes the access token using the refresh token
10396
+ */
10397
+ async refreshTokens() {
10398
+ const tokens = await this.getTokens();
10399
+ if (!tokens) {
10400
+ throw new Error("No tokens found to refresh");
10401
+ }
10402
+ const body = {
10403
+ grant_type: "refresh_token",
10404
+ refresh_token: tokens.refreshToken,
10405
+ client_id: OAUTH_CONFIG.clientId
10406
+ };
10407
+ const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
10408
+ method: "POST",
10409
+ headers: {
10410
+ "Content-Type": "application/json"
10411
+ },
10412
+ body: JSON.stringify(body)
10413
+ });
10414
+ if (!response.ok) {
10415
+ const error = await response.text();
10416
+ throw new Error(`Token refresh failed: ${error}`);
10417
+ }
10418
+ const data = await response.json();
10419
+ const newTokens = {
10420
+ accessToken: data.access_token,
10421
+ refreshToken: data.refresh_token,
10422
+ expiresAt: Date.now() + data.expires_in * 1e3
10423
+ };
10424
+ await this.saveTokens(newTokens);
10425
+ return newTokens;
10328
10426
  }
10329
10427
  /**
10330
- * Update the session ID (useful when session is created after bridge).
10428
+ * Gets the current access token, refreshing if necessary
10331
10429
  */
10332
- setSessionId(sessionId) {
10333
- this.sessionId = sessionId;
10430
+ async getAccessToken() {
10431
+ const tokens = await this.getTokens();
10432
+ if (!tokens) {
10433
+ return null;
10434
+ }
10435
+ if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
10436
+ try {
10437
+ const refreshedTokens = await this.refreshTokens();
10438
+ return refreshedTokens.accessToken;
10439
+ } catch (error) {
10440
+ console.warn("Token refresh failed:", error);
10441
+ return null;
10442
+ }
10443
+ }
10444
+ return tokens.accessToken;
10334
10445
  }
10335
10446
  /**
10336
- * Push a user message to be injected into the session.
10337
- * Call this from the UI when user submits a message during agent execution.
10447
+ * Gets the stored tokens
10338
10448
  */
10339
- push(text) {
10340
- if (this.closed) {
10341
- console.warn("[MessageBridge] Cannot push to closed bridge");
10342
- return;
10343
- }
10344
- const message = {
10345
- type: "user",
10346
- message: {
10347
- role: "user",
10348
- content: [{ type: "text", text }]
10349
- },
10350
- parent_tool_use_id: null,
10351
- session_id: this.sessionId
10352
- };
10353
- const resolver = this.resolvers.shift();
10354
- if (resolver) {
10355
- resolver({ value: message, done: false });
10356
- } else {
10357
- this.queue.push(message);
10449
+ async getTokens() {
10450
+ try {
10451
+ const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
10452
+ const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
10453
+ const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
10454
+ if (!accessToken || !refreshToken || !expiresAt) {
10455
+ return null;
10456
+ }
10457
+ return {
10458
+ accessToken,
10459
+ refreshToken,
10460
+ expiresAt: parseInt(expiresAt, 10)
10461
+ };
10462
+ } catch (error) {
10463
+ console.error("Failed to get OAuth tokens:", error);
10464
+ return null;
10358
10465
  }
10359
10466
  }
10360
10467
  /**
10361
- * Check if there are pending messages in the queue.
10468
+ * Saves OAuth tokens to secure storage
10362
10469
  */
10363
- hasPending() {
10364
- return this.queue.length > 0;
10470
+ async saveTokens(tokens) {
10471
+ await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
10472
+ await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
10473
+ await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
10365
10474
  }
10366
10475
  /**
10367
- * Close the bridge. No more messages will be accepted.
10368
- * Any pending async iterators will receive done: true.
10476
+ * Deletes stored OAuth tokens
10369
10477
  */
10370
- close() {
10371
- if (this.closed) return;
10372
- this.closed = true;
10373
- for (const resolver of this.resolvers) {
10374
- resolver({ value: void 0, done: true });
10375
- }
10376
- this.resolvers = [];
10478
+ async deleteTokens() {
10479
+ await this.secretStorage.deleteSecret("claude_oauth_access_token");
10480
+ await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
10481
+ await this.secretStorage.deleteSecret("claude_oauth_expires_at");
10377
10482
  }
10378
10483
  /**
10379
- * Check if the bridge is closed.
10484
+ * Checks if user is authenticated via OAuth
10380
10485
  */
10381
- isClosed() {
10382
- return this.closed;
10486
+ async isAuthenticated() {
10487
+ const tokens = await this.getTokens();
10488
+ return tokens !== null;
10383
10489
  }
10384
- [Symbol.asyncIterator]() {
10385
- return {
10386
- next: () => {
10387
- const queued = this.queue.shift();
10388
- if (queued) {
10389
- return Promise.resolve({ value: queued, done: false });
10390
- }
10391
- if (this.closed) {
10392
- return Promise.resolve({
10393
- value: void 0,
10394
- done: true
10395
- });
10396
- }
10397
- return new Promise((resolve2) => {
10398
- this.resolvers.push(resolve2);
10399
- });
10490
+ /**
10491
+ * Gets the current OAuth authentication status
10492
+ */
10493
+ async getStatus() {
10494
+ try {
10495
+ const tokens = await this.getTokens();
10496
+ if (!tokens) {
10497
+ return { isAuthenticated: false };
10400
10498
  }
10401
- };
10402
- }
10403
- };
10404
- }
10405
- });
10406
-
10407
- // src/commands/login.ts
10408
- import { spawn as spawn4 } from "child_process";
10409
- import crypto3 from "crypto";
10410
- import http2 from "http";
10411
- import { platform as platform2 } from "os";
10412
- function escapeHtml(text) {
10413
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10414
- }
10415
- function generateState() {
10416
- return crypto3.randomBytes(STATE_LENGTH).toString("hex");
10417
- }
10418
- async function exchangeCodeForToken(code, state) {
10419
- const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
10420
- method: "POST",
10421
- headers: { "Content-Type": "application/json" },
10422
- body: JSON.stringify({ code, state })
10423
- });
10424
- if (!response.ok) {
10425
- const error = await response.json();
10426
- throw new Error(error.error || "Failed to exchange code for token");
10427
- }
10428
- const data = await response.json();
10429
- return { token: data.token, expiresAt: data.expiresAt };
10430
- }
10431
- function openBrowser(url) {
10432
- const os3 = platform2();
10433
- let command;
10434
- let args;
10435
- switch (os3) {
10436
- case "darwin":
10437
- command = "open";
10438
- args = [url];
10439
- break;
10440
- case "win32":
10441
- command = "start";
10442
- args = ["", url];
10443
- break;
10444
- default:
10445
- command = "xdg-open";
10446
- args = [url];
10447
- }
10448
- const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
10449
- spawn4(command, args, options).unref();
10450
- }
10451
- function startCallbackServer(port, expectedState) {
10452
- return new Promise((resolve2, reject) => {
10453
- const server = http2.createServer(async (req, res) => {
10454
- if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
10455
- res.writeHead(404);
10456
- res.end("Not Found");
10457
- return;
10458
- }
10459
- const url = new URL(req.url, `http://localhost:${port}`);
10460
- const code = url.searchParams.get("code");
10461
- const state = url.searchParams.get("state");
10462
- const error = url.searchParams.get("error");
10463
- if (error) {
10464
- res.writeHead(200, { "Content-Type": "text/html" });
10465
- res.end(buildErrorPage(error));
10466
- server.close();
10467
- reject(new Error(error));
10468
- return;
10499
+ return {
10500
+ isAuthenticated: true,
10501
+ expiresAt: tokens.expiresAt
10502
+ };
10503
+ } catch (error) {
10504
+ return {
10505
+ isAuthenticated: false,
10506
+ error: error instanceof Error ? error.message : "Failed to get OAuth status"
10507
+ };
10508
+ }
10469
10509
  }
10470
- if (state !== expectedState) {
10471
- const errorMsg = "Security error: state parameter mismatch";
10472
- res.writeHead(200, { "Content-Type": "text/html" });
10473
- res.end(buildErrorPage(errorMsg));
10474
- server.close();
10475
- reject(new Error(errorMsg));
10476
- return;
10510
+ /**
10511
+ * Builds the authorization URL with all required parameters
10512
+ */
10513
+ buildAuthorizationUrl(state, codeChallenge) {
10514
+ const params = new URLSearchParams({
10515
+ response_type: "code",
10516
+ client_id: OAUTH_CONFIG.clientId,
10517
+ redirect_uri: OAUTH_CONFIG.redirectUri,
10518
+ scope: OAUTH_CONFIG.scopes.join(" "),
10519
+ state
10520
+ });
10521
+ if (codeChallenge) {
10522
+ params.set("code_challenge", codeChallenge);
10523
+ params.set("code_challenge_method", "S256");
10524
+ }
10525
+ return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
10477
10526
  }
10478
- if (!code) {
10479
- const errorMsg = "No authorization code received";
10480
- res.writeHead(400, { "Content-Type": "text/html" });
10481
- res.end(buildErrorPage(errorMsg));
10482
- server.close();
10483
- reject(new Error(errorMsg));
10484
- return;
10527
+ /**
10528
+ * Generates a random state string for CSRF protection
10529
+ */
10530
+ generateRandomState() {
10531
+ return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
10485
10532
  }
10486
- try {
10487
- const result = await exchangeCodeForToken(code, state);
10488
- res.writeHead(200, { "Content-Type": "text/html" });
10489
- res.end(buildSuccessPage());
10490
- server.close();
10491
- resolve2(result);
10492
- } catch (err) {
10493
- const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
10494
- res.writeHead(200, { "Content-Type": "text/html" });
10495
- res.end(buildErrorPage(errorMsg));
10496
- server.close();
10497
- reject(err);
10533
+ /**
10534
+ * Generates PKCE code verifier and challenge
10535
+ * PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
10536
+ */
10537
+ generatePKCEChallenge() {
10538
+ const codeVerifier = randomBytes(32).toString("base64url");
10539
+ const hash = createHash("sha256").update(codeVerifier).digest("base64url");
10540
+ return {
10541
+ codeVerifier,
10542
+ codeChallenge: hash
10543
+ };
10498
10544
  }
10499
- });
10500
- server.on("error", (error) => {
10501
- if (error.code === "EADDRINUSE") {
10502
- const portError = new Error(
10503
- "Something went wrong. Please restart the CLI and try again."
10504
- );
10505
- portError.code = "EADDRINUSE";
10506
- reject(portError);
10507
- } else {
10508
- reject(error);
10545
+ /**
10546
+ * Escape special characters for Windows cmd.exe
10547
+ * The ^ character is the escape character in cmd.exe
10548
+ */
10549
+ escapeForCmd(value) {
10550
+ return value.replace(/[&^]/g, "^$&");
10509
10551
  }
10510
- });
10511
- const timeout = setTimeout(() => {
10512
- server.close();
10513
- reject(new Error("Login timeout - no response received after 5 minutes"));
10514
- }, CALLBACK_TIMEOUT_MS2);
10515
- server.on("close", () => {
10516
- clearTimeout(timeout);
10517
- });
10518
- server.listen(port, "127.0.0.1", () => {
10519
- console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
10520
- });
10521
- });
10522
- }
10523
- function buildSuccessPage() {
10524
- return `
10525
- <!DOCTYPE html>
10526
- <html lang="en">
10527
- <head>
10528
- <meta charset="UTF-8" />
10529
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10530
- <title>Login Successful - Supatest CLI</title>
10531
- <style>
10532
- body {
10533
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10534
- display: flex;
10535
- align-items: center;
10536
- justify-content: center;
10537
- height: 100vh;
10538
- margin: 0;
10539
- background: #fefefe;
10540
- }
10541
- .container {
10542
- background: white;
10543
- padding: 3rem 2rem;
10544
- border-radius: 12px;
10545
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10546
- border: 1px solid #e5e7eb;
10547
- text-align: center;
10548
- max-width: 400px;
10549
- width: 90%;
10550
- }
10551
- .logo {
10552
- margin: 0 auto 2rem;
10553
- display: flex;
10554
- align-items: center;
10555
- justify-content: center;
10556
- gap: 8px;
10557
- }
10558
- .logo img {
10559
- width: 24px;
10560
- height: 24px;
10561
- border-radius: 6px;
10562
- object-fit: contain;
10563
- }
10564
- .logo-text {
10565
- font-weight: 600;
10566
- font-size: 16px;
10567
- color: inherit;
10568
- }
10569
- .success-icon {
10570
- width: 64px;
10571
- height: 64px;
10572
- border-radius: 50%;
10573
- background: #d1fae5;
10574
- display: flex;
10575
- align-items: center;
10576
- justify-content: center;
10577
- font-size: 32px;
10578
- margin: 0 auto 1.5rem;
10579
- }
10580
- h1 {
10581
- color: #10b981;
10582
- margin: 0 0 1rem 0;
10583
- font-size: 24px;
10584
- font-weight: 600;
10585
- }
10586
- p {
10587
- color: #666;
10588
- margin: 0;
10589
- line-height: 1.5;
10590
- }
10591
- @media (prefers-color-scheme: dark) {
10552
+ /**
10553
+ * Open a URL in the default browser cross-platform
10554
+ */
10555
+ openBrowser(url) {
10556
+ const os3 = platform2();
10557
+ let command;
10558
+ let args;
10559
+ switch (os3) {
10560
+ case "darwin":
10561
+ command = "open";
10562
+ args = [url];
10563
+ break;
10564
+ case "win32":
10565
+ command = "cmd.exe";
10566
+ args = ["/c", "start", '""', this.escapeForCmd(url)];
10567
+ break;
10568
+ default:
10569
+ command = "xdg-open";
10570
+ args = [url];
10571
+ break;
10572
+ }
10573
+ const options = { detached: true, stdio: "ignore" };
10574
+ spawn4(command, args, options).unref();
10575
+ }
10576
+ /**
10577
+ * Build success HTML page
10578
+ */
10579
+ buildSuccessPage() {
10580
+ return `
10581
+ <!DOCTYPE html>
10582
+ <html lang="en">
10583
+ <head>
10584
+ <meta charset="UTF-8" />
10585
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10586
+ <title>Authentication Successful - Supatest CLI</title>
10587
+ <style>
10592
10588
  body {
10593
- background: #1a1a1a;
10589
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10590
+ display: flex;
10591
+ align-items: center;
10592
+ justify-content: center;
10593
+ height: 100vh;
10594
+ margin: 0;
10595
+ background: #fefefe;
10594
10596
  }
10595
10597
  .container {
10596
- background: #1f1f1f;
10597
- border-color: #333;
10598
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10598
+ background: white;
10599
+ padding: 3rem 2rem;
10600
+ border-radius: 12px;
10601
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10602
+ border: 1px solid #e5e7eb;
10603
+ text-align: center;
10604
+ max-width: 400px;
10599
10605
  }
10600
10606
  .success-icon {
10601
- background: #064e3b;
10607
+ font-size: 48px;
10608
+ margin-bottom: 1rem;
10602
10609
  }
10603
10610
  h1 {
10604
- color: #34d399;
10611
+ color: #10b981;
10612
+ margin: 0 0 1rem 0;
10613
+ font-size: 24px;
10605
10614
  }
10606
10615
  p {
10607
- color: #a3a3a3;
10608
- }
10609
- .logo-text {
10610
- color: #e5e7eb;
10616
+ color: #666;
10617
+ margin: 0;
10618
+ line-height: 1.5;
10611
10619
  }
10612
- }
10613
- </style>
10614
- </head>
10615
- <body>
10616
- <div class="container">
10617
- <div class="logo">
10618
- <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10619
- <span class="logo-text">Supatest</span>
10620
- </div>
10621
- <div class="success-icon" role="img" aria-label="Success">\u2705</div>
10622
- <h1>Login Successful!</h1>
10623
- <p>You're now authenticated with Supatest CLI.</p>
10624
- <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10625
- </div>
10626
- </body>
10627
- </html>
10628
- `;
10629
- }
10630
- function buildErrorPage(errorMessage) {
10631
- return `
10632
- <!DOCTYPE html>
10633
- <html lang="en">
10634
- <head>
10635
- <meta charset="UTF-8" />
10636
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10637
- <title>Login Failed - Supatest CLI</title>
10638
- <style>
10639
- body {
10640
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10641
- display: flex;
10642
- align-items: center;
10643
- justify-content: center;
10644
- height: 100vh;
10645
- margin: 0;
10646
- background: #fefefe;
10647
- }
10648
- .container {
10649
- background: white;
10650
- padding: 3rem 2rem;
10651
- border-radius: 12px;
10652
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
10653
- border: 1px solid #e5e7eb;
10654
- text-align: center;
10655
- max-width: 400px;
10656
- width: 90%;
10657
- }
10658
- .logo {
10659
- width: 48px;
10660
- height: 48px;
10661
- margin: 0 auto 2rem;
10662
- display: flex;
10663
- align-items: center;
10664
- justify-content: center;
10665
- }
10666
- .logo img {
10667
- width: 48px;
10668
- height: 48px;
10669
- border-radius: 8px;
10670
- object-fit: contain;
10671
- }
10672
- .error-icon {
10673
- width: 64px;
10674
- height: 64px;
10675
- border-radius: 50%;
10676
- background: #fee2e2;
10677
- display: flex;
10678
- align-items: center;
10679
- justify-content: center;
10680
- font-size: 32px;
10681
- margin: 0 auto 1.5rem;
10682
- }
10683
- h1 {
10684
- color: #dc2626;
10685
- margin: 0 0 1rem 0;
10686
- font-size: 24px;
10687
- font-weight: 600;
10688
- }
10689
- p {
10690
- color: #666;
10691
- margin: 0;
10692
- line-height: 1.5;
10693
- }
10694
- .brand {
10695
- margin-top: 2rem;
10696
- padding-top: 1.5rem;
10697
- border-top: 1px solid #e5e7eb;
10698
- color: #9333ff;
10699
- font-weight: 600;
10700
- font-size: 14px;
10701
- }
10702
- @media (prefers-color-scheme: dark) {
10620
+ </style>
10621
+ </head>
10622
+ <body>
10623
+ <div class="container">
10624
+ <div class="success-icon">\u2705</div>
10625
+ <h1>Authentication Successful!</h1>
10626
+ <p>You're now authenticated with Claude.</p>
10627
+ <p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
10628
+ </div>
10629
+ </body>
10630
+ </html>
10631
+ `;
10632
+ }
10633
+ /**
10634
+ * Build error HTML page
10635
+ */
10636
+ buildErrorPage(errorMessage) {
10637
+ const escapedError = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10638
+ return `
10639
+ <!DOCTYPE html>
10640
+ <html lang="en">
10641
+ <head>
10642
+ <meta charset="UTF-8" />
10643
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10644
+ <title>Authentication Failed - Supatest CLI</title>
10645
+ <style>
10703
10646
  body {
10704
- background: #1a1a1a;
10647
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
10648
+ display: flex;
10649
+ align-items: center;
10650
+ justify-content: center;
10651
+ height: 100vh;
10652
+ margin: 0;
10653
+ background: #fefefe;
10705
10654
  }
10706
10655
  .container {
10707
- background: #1f1f1f;
10708
- border-color: #333;
10709
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
10656
+ background: white;
10657
+ padding: 3rem 2rem;
10658
+ border-radius: 12px;
10659
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
10660
+ border: 1px solid #e5e7eb;
10661
+ text-align: center;
10662
+ max-width: 400px;
10710
10663
  }
10711
10664
  .error-icon {
10712
- background: #7f1d1d;
10665
+ font-size: 48px;
10666
+ margin-bottom: 1rem;
10713
10667
  }
10714
10668
  h1 {
10715
- color: #f87171;
10669
+ color: #dc2626;
10670
+ margin: 0 0 1rem 0;
10671
+ font-size: 24px;
10716
10672
  }
10717
10673
  p {
10718
- color: #a3a3a3;
10719
- }
10720
- .brand {
10721
- border-top-color: #333;
10722
- color: #a855f7;
10674
+ color: #666;
10675
+ margin: 0;
10676
+ line-height: 1.5;
10723
10677
  }
10724
- }
10725
- </style>
10726
- </head>
10727
- <body>
10728
- <div class="container">
10729
- <div class="logo">
10730
- <img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
10731
- <span class="logo-text">Supatest</span>
10678
+ </style>
10679
+ </head>
10680
+ <body>
10681
+ <div class="container">
10682
+ <div class="error-icon">\u274C</div>
10683
+ <h1>Authentication Failed</h1>
10684
+ <p>${escapedError}</p>
10685
+ <p style="margin-top: 1rem;">You can close this window and try again.</p>
10732
10686
  </div>
10733
- <div class="error-icon" role="img" aria-label="Error">\u274C</div>
10734
- <h1>Login Failed</h1>
10735
- <p>${escapeHtml(errorMessage)}</p>
10736
- <p style="margin-top: 1rem;">You can close this window and try again.</p>
10737
- </div>
10738
- </body>
10739
- </html>
10740
- `;
10741
- }
10742
- async function loginCommand() {
10743
- console.log("\nAuthenticating with Supatest...\n");
10744
- const state = generateState();
10745
- const loginPromise = startCallbackServer(CLI_LOGIN_PORT, state);
10746
- const loginUrl = `${FRONTEND_URL}/cli-login?port=${CLI_LOGIN_PORT}&state=${state}`;
10747
- console.log(`Opening browser to: ${loginUrl}`);
10748
- console.log("\nIf your browser doesn't open automatically, please visit the URL above.\n");
10749
- try {
10750
- openBrowser(loginUrl);
10751
- } catch (error) {
10752
- console.warn("Failed to open browser automatically:", error);
10753
- console.log(`
10754
- Please manually open this URL in your browser:
10755
- ${loginUrl}
10756
- `);
10757
- }
10758
- try {
10759
- const result = await loginPromise;
10760
- console.log("\n\u2705 Login successful!\n");
10761
- return result;
10762
- } catch (error) {
10763
- const err = error;
10764
- if (err.code === "EADDRINUSE") {
10765
- console.error("\n\u274C Login failed: Something went wrong.");
10766
- console.error(" Please restart the CLI and try again.\n");
10767
- } else {
10768
- console.error("\n\u274C Login failed:", error.message, "\n");
10769
- }
10770
- throw error;
10687
+ </body>
10688
+ </html>
10689
+ `;
10690
+ }
10691
+ };
10771
10692
  }
10693
+ });
10694
+
10695
+ // src/utils/secret-storage.ts
10696
+ var secret_storage_exports = {};
10697
+ __export(secret_storage_exports, {
10698
+ deleteSecret: () => deleteSecret,
10699
+ getSecret: () => getSecret,
10700
+ getSecretStorage: () => getSecretStorage,
10701
+ listSecrets: () => listSecrets,
10702
+ setSecret: () => setSecret
10703
+ });
10704
+ import { promises as fs4 } from "fs";
10705
+ import { homedir as homedir6 } from "os";
10706
+ import { dirname as dirname2, join as join8 } from "path";
10707
+ async function getSecret(key) {
10708
+ return storage.getSecret(key);
10772
10709
  }
10773
- var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS2, STATE_LENGTH;
10774
- var init_login = __esm({
10775
- "src/commands/login.ts"() {
10710
+ async function setSecret(key, value) {
10711
+ await storage.setSecret(key, value);
10712
+ }
10713
+ async function deleteSecret(key) {
10714
+ return storage.deleteSecret(key);
10715
+ }
10716
+ async function listSecrets() {
10717
+ return storage.listSecrets();
10718
+ }
10719
+ function getSecretStorage() {
10720
+ return storage;
10721
+ }
10722
+ var SECRET_FILE_NAME, FileSecretStorage, storage;
10723
+ var init_secret_storage = __esm({
10724
+ "src/utils/secret-storage.ts"() {
10776
10725
  "use strict";
10777
- CLI_LOGIN_PORT = 8420;
10778
- FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
10779
- API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
10780
- CALLBACK_TIMEOUT_MS2 = 3e5;
10781
- STATE_LENGTH = 32;
10726
+ SECRET_FILE_NAME = "secrets.json";
10727
+ FileSecretStorage = class {
10728
+ secretFilePath;
10729
+ constructor() {
10730
+ const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
10731
+ const secretsDir = join8(homedir6(), rootDirName, "claude-auth");
10732
+ this.secretFilePath = join8(secretsDir, SECRET_FILE_NAME);
10733
+ }
10734
+ async ensureDirectoryExists() {
10735
+ const dir = dirname2(this.secretFilePath);
10736
+ await fs4.mkdir(dir, { recursive: true, mode: 448 });
10737
+ }
10738
+ async loadSecrets() {
10739
+ try {
10740
+ const data = await fs4.readFile(this.secretFilePath, "utf-8");
10741
+ const secrets = JSON.parse(data);
10742
+ return new Map(Object.entries(secrets));
10743
+ } catch (error) {
10744
+ const err = error;
10745
+ if (err.code === "ENOENT") {
10746
+ return /* @__PURE__ */ new Map();
10747
+ }
10748
+ try {
10749
+ await fs4.unlink(this.secretFilePath);
10750
+ } catch {
10751
+ }
10752
+ return /* @__PURE__ */ new Map();
10753
+ }
10754
+ }
10755
+ async saveSecrets(secrets) {
10756
+ await this.ensureDirectoryExists();
10757
+ const data = Object.fromEntries(secrets);
10758
+ const json = JSON.stringify(data, null, 2);
10759
+ await fs4.writeFile(this.secretFilePath, json, { mode: 384 });
10760
+ }
10761
+ async getSecret(key) {
10762
+ const secrets = await this.loadSecrets();
10763
+ return secrets.get(key) ?? null;
10764
+ }
10765
+ async setSecret(key, value) {
10766
+ const secrets = await this.loadSecrets();
10767
+ secrets.set(key, value);
10768
+ await this.saveSecrets(secrets);
10769
+ }
10770
+ async deleteSecret(key) {
10771
+ const secrets = await this.loadSecrets();
10772
+ if (!secrets.has(key)) {
10773
+ return false;
10774
+ }
10775
+ secrets.delete(key);
10776
+ if (secrets.size === 0) {
10777
+ try {
10778
+ await fs4.unlink(this.secretFilePath);
10779
+ } catch (error) {
10780
+ const err = error;
10781
+ if (err.code !== "ENOENT") {
10782
+ throw error;
10783
+ }
10784
+ }
10785
+ } else {
10786
+ await this.saveSecrets(secrets);
10787
+ }
10788
+ return true;
10789
+ }
10790
+ async listSecrets() {
10791
+ const secrets = await this.loadSecrets();
10792
+ return Array.from(secrets.keys());
10793
+ }
10794
+ };
10795
+ storage = new FileSecretStorage();
10782
10796
  }
10783
10797
  });
10784
10798
 
@@ -13321,7 +13335,7 @@ var init_InputPrompt = __esm({
13321
13335
  }
13322
13336
  return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.primary, key: idx }, line);
13323
13337
  })), !hasContent && disabled && /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim, italic: true }, "Waiting for agent to complete...")))
13324
- ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (ctrl+shift+m)"))), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: usageStats && usageStats.contextPct >= 90 ? theme.text.error : usageStats && usageStats.contextPct >= 75 ? theme.text.warning : theme.text.dim }, usageStats?.contextPct ?? 0, "% context used"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " ", "(", usageStats ? usageStats.inputTokens >= 1e3 ? `${(usageStats.inputTokens / 1e3).toFixed(1)}K` : usageStats.inputTokens : 0, " / ", usageStats ? usageStats.contextWindow >= 1e3 ? `${(usageStats.contextWindow / 1e3).toFixed(0)}K` : usageStats.contextWindow : "200K", ")"))));
13338
+ ), /* @__PURE__ */ React24.createElement(Box21, { justifyContent: "space-between", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box21, { gap: 2 }, /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: agentMode === "plan" ? theme.status.inProgress : theme.text.dim }, agentMode === "plan" ? "\u23F8 plan" : "\u25B6 build"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (shift+tab)")), /* @__PURE__ */ React24.createElement(Box21, null, /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, "model:"), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelDisplayName(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, " (Cost: "), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.info }, getModelCostLabel(selectedModel)), /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.dim }, ") (ctrl+shift+m)")))));
13325
13339
  });
13326
13340
  InputPromptInner.displayName = "InputPromptInner";
13327
13341
  InputPrompt = memo3(InputPromptInner);
@@ -13616,12 +13630,9 @@ var init_ProviderSelector = __esm({
13616
13630
  ProviderSelector = ({
13617
13631
  currentProvider,
13618
13632
  onSelect,
13619
- onCancel,
13620
- claudeMaxAvailable
13633
+ onCancel
13621
13634
  }) => {
13622
- const availableProviders = PROVIDERS.filter(
13623
- (p) => p.id !== "claude-max" || claudeMaxAvailable
13624
- );
13635
+ const availableProviders = PROVIDERS;
13625
13636
  const currentIndex = availableProviders.findIndex(
13626
13637
  (p) => p.id === currentProvider
13627
13638
  );
@@ -14168,7 +14179,6 @@ var init_App = __esm({
14168
14179
  return;
14169
14180
  }
14170
14181
  if (command === "/provider") {
14171
- isClaudeMaxAvailable().then(setClaudeMaxAvailable);
14172
14182
  setShowProviderSelector(true);
14173
14183
  return;
14174
14184
  }
@@ -14683,7 +14693,6 @@ var init_App = __esm({
14683
14693
  showProviderSelector && /* @__PURE__ */ React31.createElement(
14684
14694
  ProviderSelector,
14685
14695
  {
14686
- claudeMaxAvailable,
14687
14696
  currentProvider: llmProvider,
14688
14697
  onCancel: handleProviderSelectorCancel,
14689
14698
  onSelect: handleProviderSelect
@@ -15351,61 +15360,9 @@ var init_interactive = __esm({
15351
15360
  // src/index.ts
15352
15361
  await init_config();
15353
15362
  init_shared_es();
15354
- import { Command } from "commander";
15355
-
15356
- // src/commands/claude-login.ts
15357
- init_claude_oauth();
15358
- init_secret_storage();
15359
- async function claudeLoginCommand() {
15360
- const secretStorage = getSecretStorage();
15361
- const oauthService = new ClaudeOAuthService(secretStorage);
15362
- const isAuth = await oauthService.isAuthenticated();
15363
- if (isAuth) {
15364
- console.log("\u2705 You're already authenticated with Claude.");
15365
- console.log("\nTo re-authenticate, first run: supatest claude-logout\n");
15366
- return;
15367
- }
15368
- const result = await oauthService.authorize();
15369
- if (!result.success) {
15370
- console.error(`
15371
- \u274C Authentication failed: ${result.error}
15372
- `);
15373
- process.exit(1);
15374
- }
15375
- }
15376
- async function claudeLogoutCommand() {
15377
- const secretStorage = getSecretStorage();
15378
- const oauthService = new ClaudeOAuthService(secretStorage);
15379
- const isAuth = await oauthService.isAuthenticated();
15380
- if (!isAuth) {
15381
- console.log("You're not currently authenticated with Claude.\n");
15382
- return;
15383
- }
15384
- await oauthService.deleteTokens();
15385
- console.log("\u2705 Successfully logged out from Claude.\n");
15386
- }
15387
- async function claudeStatusCommand() {
15388
- const secretStorage = getSecretStorage();
15389
- const oauthService = new ClaudeOAuthService(secretStorage);
15390
- const status = await oauthService.getStatus();
15391
- if (status.isAuthenticated) {
15392
- console.log("\u2705 Authenticated with Claude");
15393
- if (status.expiresAt) {
15394
- const expiresDate = new Date(status.expiresAt);
15395
- console.log(` Token expires: ${expiresDate.toLocaleString()}`);
15396
- }
15397
- } else {
15398
- console.log("\u274C Not authenticated with Claude");
15399
- if (status.error) {
15400
- console.log(` Error: ${status.error}`);
15401
- }
15402
- }
15403
- console.log();
15404
- }
15405
-
15406
- // src/index.ts
15407
15363
  init_setup();
15408
15364
  await init_config();
15365
+ import { Command } from "commander";
15409
15366
 
15410
15367
  // src/modes/headless.ts
15411
15368
  init_api_client();
@@ -15419,7 +15376,7 @@ init_react();
15419
15376
  init_MessageList();
15420
15377
  init_SessionContext();
15421
15378
  import { execSync as execSync2 } from "child_process";
15422
- import { homedir as homedir5 } from "os";
15379
+ import { homedir as homedir4 } from "os";
15423
15380
  import { Box as Box13, useApp } from "ink";
15424
15381
  import React14, { useEffect as useEffect2, useRef as useRef4, useState as useState3 } from "react";
15425
15382
  var getGitBranch = () => {
@@ -15431,7 +15388,7 @@ var getGitBranch = () => {
15431
15388
  };
15432
15389
  var getCurrentFolder = () => {
15433
15390
  const cwd = process.cwd();
15434
- const home = homedir5();
15391
+ const home = homedir4();
15435
15392
  if (cwd.startsWith(home)) {
15436
15393
  return `~${cwd.slice(home.length)}`;
15437
15394
  }
@@ -15678,7 +15635,7 @@ async function runAgent(config2) {
15678
15635
  // src/utils/auto-update.ts
15679
15636
  init_version();
15680
15637
  init_error_logger();
15681
- import { execSync as execSync3, spawn as spawn3 } from "child_process";
15638
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
15682
15639
  import latestVersion from "latest-version";
15683
15640
  import { gt } from "semver";
15684
15641
  var UPDATE_CHECK_TIMEOUT = 3e3;
@@ -15719,8 +15676,24 @@ Updating Supatest CLI ${CLI_VERSION} \u2192 ${latest}...`);
15719
15676
  timeout: INSTALL_TIMEOUT
15720
15677
  });
15721
15678
  }
15679
+ let updateVerified = false;
15680
+ try {
15681
+ const installedVersion = execSync3("npm ls -g @supatest/cli --json 2>/dev/null", {
15682
+ encoding: "utf-8"
15683
+ });
15684
+ const parsed = JSON.parse(installedVersion);
15685
+ const newVersion = parsed.dependencies?.["@supatest/cli"]?.version;
15686
+ if (newVersion && gt(newVersion, CLI_VERSION)) {
15687
+ updateVerified = true;
15688
+ }
15689
+ } catch {
15690
+ }
15691
+ if (!updateVerified) {
15692
+ console.log("Update completed but could not verify new version. Continuing with current version...\n");
15693
+ return;
15694
+ }
15722
15695
  console.log("\u2713 Updated successfully\n");
15723
- const child = spawn3(process.argv[0], process.argv.slice(1), {
15696
+ const child = spawn2(process.argv[0], process.argv.slice(1), {
15724
15697
  stdio: "inherit",
15725
15698
  detached: false
15726
15699
  });
@@ -16005,33 +15978,6 @@ program.command("setup").description("Check prerequisites and set up required to
16005
15978
  process.exit(1);
16006
15979
  }
16007
15980
  });
16008
- program.command("claude-login").description("Authenticate with Claude using OAuth (uses your Claude Pro/Max subscription)").action(async () => {
16009
- try {
16010
- await claudeLoginCommand();
16011
- } catch (error) {
16012
- logError(error, { source: "claude-login" });
16013
- logger.error(`Claude login failed: ${error instanceof Error ? error.message : String(error)}`);
16014
- process.exit(1);
16015
- }
16016
- });
16017
- program.command("claude-logout").description("Sign out from Claude OAuth").action(async () => {
16018
- try {
16019
- await claudeLogoutCommand();
16020
- } catch (error) {
16021
- logError(error, { source: "claude-logout" });
16022
- logger.error(`Claude logout failed: ${error instanceof Error ? error.message : String(error)}`);
16023
- process.exit(1);
16024
- }
16025
- });
16026
- program.command("claude-status").description("Show Claude OAuth authentication status").action(async () => {
16027
- try {
16028
- await claudeStatusCommand();
16029
- } catch (error) {
16030
- logError(error, { source: "claude-status" });
16031
- logger.error(`Claude status check failed: ${error instanceof Error ? error.message : String(error)}`);
16032
- process.exit(1);
16033
- }
16034
- });
16035
15981
  var filteredArgv = process.argv.filter((arg, index) => {
16036
15982
  return !(arg === "--" && index > 1);
16037
15983
  });