@supatest/cli 0.0.41 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1209 -1277
- 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
|
-
-
|
|
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
|
|
@@ -6054,591 +6054,9 @@ var init_shared_es = __esm({
|
|
|
6054
6054
|
}
|
|
6055
6055
|
});
|
|
6056
6056
|
|
|
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"]
|
|
6078
|
-
};
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
6057
|
// src/commands/setup.ts
|
|
6640
|
-
import { execSync, spawn
|
|
6641
|
-
import
|
|
6058
|
+
import { execSync, spawn, spawnSync } from "child_process";
|
|
6059
|
+
import fs from "fs";
|
|
6642
6060
|
import os from "os";
|
|
6643
6061
|
import path from "path";
|
|
6644
6062
|
function parseVersion(versionString) {
|
|
@@ -6692,7 +6110,7 @@ function getPlaywrightCachePath() {
|
|
|
6692
6110
|
// Windows
|
|
6693
6111
|
];
|
|
6694
6112
|
for (const cachePath of cachePaths) {
|
|
6695
|
-
if (
|
|
6113
|
+
if (fs.existsSync(cachePath)) {
|
|
6696
6114
|
return cachePath;
|
|
6697
6115
|
}
|
|
6698
6116
|
}
|
|
@@ -6702,7 +6120,7 @@ function getInstalledChromiumVersion() {
|
|
|
6702
6120
|
const cachePath = getPlaywrightCachePath();
|
|
6703
6121
|
if (!cachePath) return null;
|
|
6704
6122
|
try {
|
|
6705
|
-
const entries =
|
|
6123
|
+
const entries = fs.readdirSync(cachePath);
|
|
6706
6124
|
const chromiumVersions = entries.filter((entry) => entry.startsWith("chromium-") && !entry.includes("headless")).map((entry) => entry.replace("chromium-", "")).sort((a, b) => Number(b) - Number(a));
|
|
6707
6125
|
return chromiumVersions[0] || null;
|
|
6708
6126
|
} catch {
|
|
@@ -6736,7 +6154,7 @@ function checkNodeVersion() {
|
|
|
6736
6154
|
}
|
|
6737
6155
|
async function installChromium() {
|
|
6738
6156
|
return new Promise((resolve2) => {
|
|
6739
|
-
const child =
|
|
6157
|
+
const child = spawn("npx", ["playwright", "install", "chromium"], {
|
|
6740
6158
|
stdio: "inherit",
|
|
6741
6159
|
shell: true
|
|
6742
6160
|
// Required for Windows where npx is npx.cmd
|
|
@@ -6766,14 +6184,14 @@ function createSupatestConfig(cwd) {
|
|
|
6766
6184
|
const supatestDir = path.join(cwd, ".supatest");
|
|
6767
6185
|
const mcpJsonPath = path.join(supatestDir, "mcp.json");
|
|
6768
6186
|
try {
|
|
6769
|
-
if (!
|
|
6770
|
-
|
|
6187
|
+
if (!fs.existsSync(supatestDir)) {
|
|
6188
|
+
fs.mkdirSync(supatestDir, { recursive: true });
|
|
6771
6189
|
}
|
|
6772
6190
|
let config2;
|
|
6773
6191
|
let fileExisted = false;
|
|
6774
|
-
if (
|
|
6192
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
6775
6193
|
fileExisted = true;
|
|
6776
|
-
const existingContent =
|
|
6194
|
+
const existingContent = fs.readFileSync(mcpJsonPath, "utf-8");
|
|
6777
6195
|
config2 = JSON.parse(existingContent);
|
|
6778
6196
|
} else {
|
|
6779
6197
|
config2 = {};
|
|
@@ -6784,7 +6202,7 @@ function createSupatestConfig(cwd) {
|
|
|
6784
6202
|
if (!config2.mcpServers.playwright) {
|
|
6785
6203
|
config2.mcpServers.playwright = DEFAULT_MCP_CONFIG.mcpServers.playwright;
|
|
6786
6204
|
}
|
|
6787
|
-
|
|
6205
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
6788
6206
|
if (fileExisted) {
|
|
6789
6207
|
return {
|
|
6790
6208
|
ok: true,
|
|
@@ -6911,18 +6329,18 @@ var CLI_VERSION;
|
|
|
6911
6329
|
var init_version = __esm({
|
|
6912
6330
|
"src/version.ts"() {
|
|
6913
6331
|
"use strict";
|
|
6914
|
-
CLI_VERSION = "0.0.
|
|
6332
|
+
CLI_VERSION = "0.0.42";
|
|
6915
6333
|
}
|
|
6916
6334
|
});
|
|
6917
6335
|
|
|
6918
6336
|
// src/utils/error-logger.ts
|
|
6919
|
-
import * as
|
|
6337
|
+
import * as fs2 from "fs";
|
|
6920
6338
|
import * as os2 from "os";
|
|
6921
6339
|
import * as path2 from "path";
|
|
6922
6340
|
function ensureLogDir() {
|
|
6923
6341
|
try {
|
|
6924
|
-
if (!
|
|
6925
|
-
|
|
6342
|
+
if (!fs2.existsSync(LOGS_DIR)) {
|
|
6343
|
+
fs2.mkdirSync(LOGS_DIR, { recursive: true });
|
|
6926
6344
|
}
|
|
6927
6345
|
return true;
|
|
6928
6346
|
} catch {
|
|
@@ -6931,14 +6349,14 @@ function ensureLogDir() {
|
|
|
6931
6349
|
}
|
|
6932
6350
|
function rotateLogIfNeeded() {
|
|
6933
6351
|
try {
|
|
6934
|
-
if (!
|
|
6935
|
-
const stats =
|
|
6352
|
+
if (!fs2.existsSync(ERROR_LOG_FILE)) return;
|
|
6353
|
+
const stats = fs2.statSync(ERROR_LOG_FILE);
|
|
6936
6354
|
if (stats.size > MAX_LOG_SIZE) {
|
|
6937
6355
|
const oldLogFile = `${ERROR_LOG_FILE}.old`;
|
|
6938
|
-
if (
|
|
6939
|
-
|
|
6356
|
+
if (fs2.existsSync(oldLogFile)) {
|
|
6357
|
+
fs2.unlinkSync(oldLogFile);
|
|
6940
6358
|
}
|
|
6941
|
-
|
|
6359
|
+
fs2.renameSync(ERROR_LOG_FILE, oldLogFile);
|
|
6942
6360
|
}
|
|
6943
6361
|
} catch {
|
|
6944
6362
|
}
|
|
@@ -6974,7 +6392,7 @@ function logError(error, context) {
|
|
|
6974
6392
|
const logLine = `${JSON.stringify(entry)}
|
|
6975
6393
|
`;
|
|
6976
6394
|
try {
|
|
6977
|
-
|
|
6395
|
+
fs2.appendFileSync(ERROR_LOG_FILE, logLine);
|
|
6978
6396
|
} catch {
|
|
6979
6397
|
}
|
|
6980
6398
|
}
|
|
@@ -6991,7 +6409,7 @@ var init_error_logger = __esm({
|
|
|
6991
6409
|
});
|
|
6992
6410
|
|
|
6993
6411
|
// src/utils/logger.ts
|
|
6994
|
-
import * as
|
|
6412
|
+
import * as fs3 from "fs";
|
|
6995
6413
|
import * as path3 from "path";
|
|
6996
6414
|
import chalk from "chalk";
|
|
6997
6415
|
var Logger, logger;
|
|
@@ -7025,7 +6443,7 @@ ${"=".repeat(80)}
|
|
|
7025
6443
|
${"=".repeat(80)}
|
|
7026
6444
|
`;
|
|
7027
6445
|
try {
|
|
7028
|
-
|
|
6446
|
+
fs3.appendFileSync(this.logFile, separator);
|
|
7029
6447
|
} catch (error) {
|
|
7030
6448
|
}
|
|
7031
6449
|
}
|
|
@@ -7039,7 +6457,7 @@ ${"=".repeat(80)}
|
|
|
7039
6457
|
` : `[${timestamp}] [${level}] ${message}
|
|
7040
6458
|
`;
|
|
7041
6459
|
try {
|
|
7042
|
-
|
|
6460
|
+
fs3.appendFileSync(this.logFile, logEntry);
|
|
7043
6461
|
} catch (error) {
|
|
7044
6462
|
}
|
|
7045
6463
|
}
|
|
@@ -7785,7 +7203,7 @@ var init_api_client = __esm({
|
|
|
7785
7203
|
|
|
7786
7204
|
// src/utils/command-discovery.ts
|
|
7787
7205
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
|
|
7788
|
-
import { join as
|
|
7206
|
+
import { join as join3, relative } from "path";
|
|
7789
7207
|
function parseMarkdownFrontmatter(content) {
|
|
7790
7208
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
7791
7209
|
const match = content.match(frontmatterRegex);
|
|
@@ -7810,7 +7228,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
|
|
|
7810
7228
|
}
|
|
7811
7229
|
const entries = readdirSync(dir);
|
|
7812
7230
|
for (const entry of entries) {
|
|
7813
|
-
const fullPath =
|
|
7231
|
+
const fullPath = join3(dir, entry);
|
|
7814
7232
|
const stat = statSync2(fullPath);
|
|
7815
7233
|
if (stat.isDirectory()) {
|
|
7816
7234
|
discoverMarkdownFiles(fullPath, baseDir, files);
|
|
@@ -7821,7 +7239,7 @@ function discoverMarkdownFiles(dir, baseDir, files = []) {
|
|
|
7821
7239
|
return files;
|
|
7822
7240
|
}
|
|
7823
7241
|
function discoverCommands(cwd) {
|
|
7824
|
-
const commandsDir =
|
|
7242
|
+
const commandsDir = join3(cwd, ".supatest", "commands");
|
|
7825
7243
|
if (!existsSync2(commandsDir)) {
|
|
7826
7244
|
return [];
|
|
7827
7245
|
}
|
|
@@ -7844,9 +7262,9 @@ function discoverCommands(cwd) {
|
|
|
7844
7262
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
7845
7263
|
}
|
|
7846
7264
|
function expandCommand(cwd, commandName, args) {
|
|
7847
|
-
const commandsDir =
|
|
7265
|
+
const commandsDir = join3(cwd, ".supatest", "commands");
|
|
7848
7266
|
const relativePath = commandName.replace(/\./g, "/") + ".md";
|
|
7849
|
-
const filePath =
|
|
7267
|
+
const filePath = join3(commandsDir, relativePath);
|
|
7850
7268
|
if (!existsSync2(filePath)) {
|
|
7851
7269
|
return null;
|
|
7852
7270
|
}
|
|
@@ -7869,7 +7287,7 @@ function expandCommand(cwd, commandName, args) {
|
|
|
7869
7287
|
}
|
|
7870
7288
|
}
|
|
7871
7289
|
function discoverAgents(cwd) {
|
|
7872
|
-
const agentsDir =
|
|
7290
|
+
const agentsDir = join3(cwd, ".supatest", "agents");
|
|
7873
7291
|
if (!existsSync2(agentsDir)) {
|
|
7874
7292
|
return [];
|
|
7875
7293
|
}
|
|
@@ -7900,8 +7318,8 @@ var init_command_discovery = __esm({
|
|
|
7900
7318
|
|
|
7901
7319
|
// src/utils/mcp-loader.ts
|
|
7902
7320
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
7903
|
-
import { homedir as
|
|
7904
|
-
import { join as
|
|
7321
|
+
import { homedir as homedir2 } from "os";
|
|
7322
|
+
import { join as join4 } from "path";
|
|
7905
7323
|
function expandEnvVar(value) {
|
|
7906
7324
|
return value.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
7907
7325
|
const [varName, defaultValue] = expr.split(":-");
|
|
@@ -7950,9 +7368,9 @@ function loadMcpServersFromFile(mcpPath) {
|
|
|
7950
7368
|
}
|
|
7951
7369
|
}
|
|
7952
7370
|
function loadMcpServers(cwd) {
|
|
7953
|
-
const globalMcpPath =
|
|
7371
|
+
const globalMcpPath = join4(homedir2(), ".supatest", "mcp.json");
|
|
7954
7372
|
const globalServers = loadMcpServersFromFile(globalMcpPath);
|
|
7955
|
-
const projectMcpPath =
|
|
7373
|
+
const projectMcpPath = join4(cwd, ".supatest", "mcp.json");
|
|
7956
7374
|
const projectServers = loadMcpServersFromFile(projectMcpPath);
|
|
7957
7375
|
return { ...globalServers, ...projectServers };
|
|
7958
7376
|
}
|
|
@@ -7964,11 +7382,11 @@ var init_mcp_loader = __esm({
|
|
|
7964
7382
|
|
|
7965
7383
|
// src/utils/project-instructions.ts
|
|
7966
7384
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
7967
|
-
import { join as
|
|
7385
|
+
import { join as join5 } from "path";
|
|
7968
7386
|
function loadProjectInstructions(cwd) {
|
|
7969
7387
|
const paths = [
|
|
7970
|
-
|
|
7971
|
-
|
|
7388
|
+
join5(cwd, "SUPATEST.md"),
|
|
7389
|
+
join5(cwd, ".supatest", "SUPATEST.md")
|
|
7972
7390
|
];
|
|
7973
7391
|
for (const path6 of paths) {
|
|
7974
7392
|
if (existsSync4(path6)) {
|
|
@@ -7988,8 +7406,8 @@ var init_project_instructions = __esm({
|
|
|
7988
7406
|
|
|
7989
7407
|
// src/core/agent.ts
|
|
7990
7408
|
import { createRequire } from "module";
|
|
7991
|
-
import { homedir as
|
|
7992
|
-
import { dirname
|
|
7409
|
+
import { homedir as homedir3 } from "os";
|
|
7410
|
+
import { dirname, join as join6 } from "path";
|
|
7993
7411
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7994
7412
|
var CoreAgent;
|
|
7995
7413
|
var init_agent = __esm({
|
|
@@ -8119,7 +7537,7 @@ ${projectInstructions}`,
|
|
|
8119
7537
|
this.presenter.onLog(`Auth: Using Claude Max (default Claude Code credentials)`);
|
|
8120
7538
|
logger.debug("[agent] Claude Max mode: Using default ~/.claude/ config, cleared provider credentials");
|
|
8121
7539
|
} else {
|
|
8122
|
-
const internalConfigDir =
|
|
7540
|
+
const internalConfigDir = join6(homedir3(), ".supatest", "claude-internal");
|
|
8123
7541
|
cleanEnv.CLAUDE_CONFIG_DIR = internalConfigDir;
|
|
8124
7542
|
cleanEnv.ANTHROPIC_API_KEY = config2.supatestApiKey || "";
|
|
8125
7543
|
cleanEnv.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || "";
|
|
@@ -8449,7 +7867,7 @@ ${projectInstructions}`,
|
|
|
8449
7867
|
let claudeCodePath;
|
|
8450
7868
|
const require2 = createRequire(import.meta.url);
|
|
8451
7869
|
const sdkPath = require2.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
|
|
8452
|
-
claudeCodePath =
|
|
7870
|
+
claudeCodePath = join6(dirname(sdkPath), "cli.js");
|
|
8453
7871
|
this.presenter.onLog(`Using SDK CLI: ${claudeCodePath}`);
|
|
8454
7872
|
if (config.claudeCodeExecutablePath) {
|
|
8455
7873
|
claudeCodePath = config.claudeCodeExecutablePath;
|
|
@@ -10143,642 +9561,1224 @@ function createInkStdio() {
|
|
|
10143
9561
|
if (prop === "write") {
|
|
10144
9562
|
return writeToStdout;
|
|
10145
9563
|
}
|
|
10146
|
-
const value = Reflect.get(target, prop, receiver);
|
|
10147
|
-
if (typeof value === "function") {
|
|
10148
|
-
return value.bind(target);
|
|
9564
|
+
const value = Reflect.get(target, prop, receiver);
|
|
9565
|
+
if (typeof value === "function") {
|
|
9566
|
+
return value.bind(target);
|
|
9567
|
+
}
|
|
9568
|
+
return value;
|
|
9569
|
+
}
|
|
9570
|
+
});
|
|
9571
|
+
const inkStderr = new Proxy(process.stderr, {
|
|
9572
|
+
get(target, prop, receiver) {
|
|
9573
|
+
if (prop === "write") {
|
|
9574
|
+
return writeToStderr;
|
|
9575
|
+
}
|
|
9576
|
+
const value = Reflect.get(target, prop, receiver);
|
|
9577
|
+
if (typeof value === "function") {
|
|
9578
|
+
return value.bind(target);
|
|
9579
|
+
}
|
|
9580
|
+
return value;
|
|
9581
|
+
}
|
|
9582
|
+
});
|
|
9583
|
+
return { stdout: inkStdout, stderr: inkStderr };
|
|
9584
|
+
}
|
|
9585
|
+
function clearTerminalViewportAndScrollback() {
|
|
9586
|
+
writeToStdout("\x1B[3J\x1B[H\x1B[2J");
|
|
9587
|
+
}
|
|
9588
|
+
var originalStdoutWrite, originalStderrWrite;
|
|
9589
|
+
var init_stdio = __esm({
|
|
9590
|
+
"src/utils/stdio.ts"() {
|
|
9591
|
+
"use strict";
|
|
9592
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
9593
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
9594
|
+
}
|
|
9595
|
+
});
|
|
9596
|
+
|
|
9597
|
+
// src/utils/encryption.ts
|
|
9598
|
+
import crypto2 from "crypto";
|
|
9599
|
+
import { hostname, userInfo } from "os";
|
|
9600
|
+
function deriveEncryptionKey() {
|
|
9601
|
+
const host = hostname();
|
|
9602
|
+
const user = userInfo().username;
|
|
9603
|
+
const salt = `${host}-${user}-supatest-cli`;
|
|
9604
|
+
if (process.env.DEBUG_ENCRYPTION) {
|
|
9605
|
+
console.error(`[encryption] hostname=${host}, username=${user}, salt=${salt}`);
|
|
9606
|
+
}
|
|
9607
|
+
return crypto2.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
|
|
9608
|
+
}
|
|
9609
|
+
function getEncryptionKey() {
|
|
9610
|
+
if (!cachedKey) {
|
|
9611
|
+
cachedKey = deriveEncryptionKey();
|
|
9612
|
+
}
|
|
9613
|
+
return cachedKey;
|
|
9614
|
+
}
|
|
9615
|
+
function encrypt(plaintext) {
|
|
9616
|
+
const key = getEncryptionKey();
|
|
9617
|
+
const iv = crypto2.randomBytes(IV_LENGTH);
|
|
9618
|
+
const cipher = crypto2.createCipheriv(ALGORITHM, key, iv);
|
|
9619
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
9620
|
+
encrypted += cipher.final("hex");
|
|
9621
|
+
const authTag = cipher.getAuthTag();
|
|
9622
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
9623
|
+
}
|
|
9624
|
+
function decrypt(encryptedData) {
|
|
9625
|
+
const parts = encryptedData.split(":");
|
|
9626
|
+
if (parts.length !== 3) {
|
|
9627
|
+
throw new Error("Invalid encrypted data format");
|
|
9628
|
+
}
|
|
9629
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
9630
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
9631
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
9632
|
+
const key = getEncryptionKey();
|
|
9633
|
+
const decipher = crypto2.createDecipheriv(ALGORITHM, key, iv);
|
|
9634
|
+
decipher.setAuthTag(authTag);
|
|
9635
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
9636
|
+
decrypted += decipher.final("utf8");
|
|
9637
|
+
return decrypted;
|
|
9638
|
+
}
|
|
9639
|
+
var ALGORITHM, KEY_LENGTH, IV_LENGTH, cachedKey;
|
|
9640
|
+
var init_encryption = __esm({
|
|
9641
|
+
"src/utils/encryption.ts"() {
|
|
9642
|
+
"use strict";
|
|
9643
|
+
ALGORITHM = "aes-256-gcm";
|
|
9644
|
+
KEY_LENGTH = 32;
|
|
9645
|
+
IV_LENGTH = 16;
|
|
9646
|
+
cachedKey = null;
|
|
9647
|
+
}
|
|
9648
|
+
});
|
|
9649
|
+
|
|
9650
|
+
// src/utils/token-storage.ts
|
|
9651
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
9652
|
+
import { homedir as homedir5 } from "os";
|
|
9653
|
+
import { join as join7 } from "path";
|
|
9654
|
+
function getTokenFilePath() {
|
|
9655
|
+
const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
|
|
9656
|
+
if (apiUrl === PRODUCTION_API_URL) {
|
|
9657
|
+
return join7(CONFIG_DIR, "token.json");
|
|
9658
|
+
}
|
|
9659
|
+
return join7(CONFIG_DIR, "token.local.json");
|
|
9660
|
+
}
|
|
9661
|
+
function isV2Format(stored) {
|
|
9662
|
+
return "version" in stored && stored.version === 2;
|
|
9663
|
+
}
|
|
9664
|
+
function ensureConfigDir() {
|
|
9665
|
+
if (!existsSync5(CONFIG_DIR)) {
|
|
9666
|
+
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9669
|
+
function saveToken(token, expiresAt) {
|
|
9670
|
+
ensureConfigDir();
|
|
9671
|
+
const payload = {
|
|
9672
|
+
token,
|
|
9673
|
+
expiresAt,
|
|
9674
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9675
|
+
};
|
|
9676
|
+
const stored = {
|
|
9677
|
+
version: STORAGE_VERSION,
|
|
9678
|
+
encryptedData: encrypt(JSON.stringify(payload))
|
|
9679
|
+
};
|
|
9680
|
+
const tokenFile = getTokenFilePath();
|
|
9681
|
+
writeFileSync(tokenFile, JSON.stringify(stored, null, 2), { mode: 384 });
|
|
9682
|
+
}
|
|
9683
|
+
function loadToken() {
|
|
9684
|
+
const tokenFile = getTokenFilePath();
|
|
9685
|
+
if (!existsSync5(tokenFile)) {
|
|
9686
|
+
return null;
|
|
9687
|
+
}
|
|
9688
|
+
try {
|
|
9689
|
+
const data = readFileSync4(tokenFile, "utf8");
|
|
9690
|
+
const stored = JSON.parse(data);
|
|
9691
|
+
let payload;
|
|
9692
|
+
if (isV2Format(stored)) {
|
|
9693
|
+
payload = JSON.parse(decrypt(stored.encryptedData));
|
|
9694
|
+
} else {
|
|
9695
|
+
payload = stored;
|
|
9696
|
+
}
|
|
9697
|
+
if (payload.expiresAt) {
|
|
9698
|
+
const expiresAt = new Date(payload.expiresAt);
|
|
9699
|
+
if (expiresAt < /* @__PURE__ */ new Date()) {
|
|
9700
|
+
console.warn("CLI token has expired. Please run 'supatest login' again.");
|
|
9701
|
+
return null;
|
|
9702
|
+
}
|
|
9703
|
+
}
|
|
9704
|
+
return payload.token;
|
|
9705
|
+
} catch (error) {
|
|
9706
|
+
const err = error;
|
|
9707
|
+
if (err.message?.includes("Invalid encrypted data format") || err.message?.includes("Unsupported state or unable to authenticate data")) {
|
|
9708
|
+
try {
|
|
9709
|
+
unlinkSync2(tokenFile);
|
|
9710
|
+
} catch {
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
9713
|
+
return null;
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
function removeToken() {
|
|
9717
|
+
const tokenFile = getTokenFilePath();
|
|
9718
|
+
if (existsSync5(tokenFile)) {
|
|
9719
|
+
unlinkSync2(tokenFile);
|
|
9720
|
+
}
|
|
9721
|
+
}
|
|
9722
|
+
var CONFIG_DIR, PRODUCTION_API_URL, STORAGE_VERSION, TOKEN_FILE;
|
|
9723
|
+
var init_token_storage = __esm({
|
|
9724
|
+
"src/utils/token-storage.ts"() {
|
|
9725
|
+
"use strict";
|
|
9726
|
+
init_encryption();
|
|
9727
|
+
CONFIG_DIR = join7(homedir5(), ".supatest");
|
|
9728
|
+
PRODUCTION_API_URL = "https://code-api.supatest.ai";
|
|
9729
|
+
STORAGE_VERSION = 2;
|
|
9730
|
+
TOKEN_FILE = join7(CONFIG_DIR, "token.json");
|
|
9731
|
+
}
|
|
9732
|
+
});
|
|
9733
|
+
|
|
9734
|
+
// src/core/message-bridge.ts
|
|
9735
|
+
var MessageBridge;
|
|
9736
|
+
var init_message_bridge = __esm({
|
|
9737
|
+
"src/core/message-bridge.ts"() {
|
|
9738
|
+
"use strict";
|
|
9739
|
+
MessageBridge = class {
|
|
9740
|
+
queue = [];
|
|
9741
|
+
resolvers = [];
|
|
9742
|
+
closed = false;
|
|
9743
|
+
sessionId;
|
|
9744
|
+
constructor(sessionId) {
|
|
9745
|
+
this.sessionId = sessionId;
|
|
9746
|
+
}
|
|
9747
|
+
/**
|
|
9748
|
+
* Update the session ID (useful when session is created after bridge).
|
|
9749
|
+
*/
|
|
9750
|
+
setSessionId(sessionId) {
|
|
9751
|
+
this.sessionId = sessionId;
|
|
9752
|
+
}
|
|
9753
|
+
/**
|
|
9754
|
+
* Push a user message to be injected into the session.
|
|
9755
|
+
* Call this from the UI when user submits a message during agent execution.
|
|
9756
|
+
*/
|
|
9757
|
+
push(text) {
|
|
9758
|
+
if (this.closed) {
|
|
9759
|
+
console.warn("[MessageBridge] Cannot push to closed bridge");
|
|
9760
|
+
return;
|
|
9761
|
+
}
|
|
9762
|
+
const message = {
|
|
9763
|
+
type: "user",
|
|
9764
|
+
message: {
|
|
9765
|
+
role: "user",
|
|
9766
|
+
content: [{ type: "text", text }]
|
|
9767
|
+
},
|
|
9768
|
+
parent_tool_use_id: null,
|
|
9769
|
+
session_id: this.sessionId
|
|
9770
|
+
};
|
|
9771
|
+
const resolver = this.resolvers.shift();
|
|
9772
|
+
if (resolver) {
|
|
9773
|
+
resolver({ value: message, done: false });
|
|
9774
|
+
} else {
|
|
9775
|
+
this.queue.push(message);
|
|
9776
|
+
}
|
|
10149
9777
|
}
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
if (prop === "write") {
|
|
10156
|
-
return writeToStderr;
|
|
9778
|
+
/**
|
|
9779
|
+
* Check if there are pending messages in the queue.
|
|
9780
|
+
*/
|
|
9781
|
+
hasPending() {
|
|
9782
|
+
return this.queue.length > 0;
|
|
10157
9783
|
}
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
9784
|
+
/**
|
|
9785
|
+
* Close the bridge. No more messages will be accepted.
|
|
9786
|
+
* Any pending async iterators will receive done: true.
|
|
9787
|
+
*/
|
|
9788
|
+
close() {
|
|
9789
|
+
if (this.closed) return;
|
|
9790
|
+
this.closed = true;
|
|
9791
|
+
for (const resolver of this.resolvers) {
|
|
9792
|
+
resolver({ value: void 0, done: true });
|
|
9793
|
+
}
|
|
9794
|
+
this.resolvers = [];
|
|
10161
9795
|
}
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
9796
|
+
/**
|
|
9797
|
+
* Check if the bridge is closed.
|
|
9798
|
+
*/
|
|
9799
|
+
isClosed() {
|
|
9800
|
+
return this.closed;
|
|
9801
|
+
}
|
|
9802
|
+
[Symbol.asyncIterator]() {
|
|
9803
|
+
return {
|
|
9804
|
+
next: () => {
|
|
9805
|
+
const queued = this.queue.shift();
|
|
9806
|
+
if (queued) {
|
|
9807
|
+
return Promise.resolve({ value: queued, done: false });
|
|
9808
|
+
}
|
|
9809
|
+
if (this.closed) {
|
|
9810
|
+
return Promise.resolve({
|
|
9811
|
+
value: void 0,
|
|
9812
|
+
done: true
|
|
9813
|
+
});
|
|
9814
|
+
}
|
|
9815
|
+
return new Promise((resolve2) => {
|
|
9816
|
+
this.resolvers.push(resolve2);
|
|
9817
|
+
});
|
|
9818
|
+
}
|
|
9819
|
+
};
|
|
9820
|
+
}
|
|
9821
|
+
};
|
|
10176
9822
|
}
|
|
10177
9823
|
});
|
|
10178
9824
|
|
|
10179
|
-
// src/
|
|
10180
|
-
import
|
|
10181
|
-
import
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
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;
|
|
9825
|
+
// src/commands/login.ts
|
|
9826
|
+
import { spawn as spawn3 } from "child_process";
|
|
9827
|
+
import crypto3 from "crypto";
|
|
9828
|
+
import http from "http";
|
|
9829
|
+
import { platform } from "os";
|
|
9830
|
+
function escapeHtml(text) {
|
|
9831
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10196
9832
|
}
|
|
10197
|
-
function
|
|
10198
|
-
|
|
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}`;
|
|
9833
|
+
function generateState() {
|
|
9834
|
+
return crypto3.randomBytes(STATE_LENGTH).toString("hex");
|
|
10205
9835
|
}
|
|
10206
|
-
function
|
|
10207
|
-
const
|
|
10208
|
-
|
|
10209
|
-
|
|
9836
|
+
async function exchangeCodeForToken(code, state) {
|
|
9837
|
+
const response = await fetch(`${API_URL}/web/auth/cli-token-exchange`, {
|
|
9838
|
+
method: "POST",
|
|
9839
|
+
headers: { "Content-Type": "application/json" },
|
|
9840
|
+
body: JSON.stringify({ code, state })
|
|
9841
|
+
});
|
|
9842
|
+
if (!response.ok) {
|
|
9843
|
+
const error = await response.json();
|
|
9844
|
+
throw new Error(error.error || "Failed to exchange code for token");
|
|
10210
9845
|
}
|
|
10211
|
-
const
|
|
10212
|
-
|
|
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;
|
|
9846
|
+
const data = await response.json();
|
|
9847
|
+
return { token: data.token, expiresAt: data.expiresAt };
|
|
10220
9848
|
}
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
const apiUrl = process.env.SUPATEST_API_URL || PRODUCTION_API_URL;
|
|
10238
|
-
if (apiUrl === PRODUCTION_API_URL) {
|
|
10239
|
-
return join8(CONFIG_DIR, "token.json");
|
|
9849
|
+
function openBrowser(url) {
|
|
9850
|
+
const os3 = platform();
|
|
9851
|
+
let command;
|
|
9852
|
+
let args;
|
|
9853
|
+
switch (os3) {
|
|
9854
|
+
case "darwin":
|
|
9855
|
+
command = "open";
|
|
9856
|
+
args = [url];
|
|
9857
|
+
break;
|
|
9858
|
+
case "win32":
|
|
9859
|
+
command = "start";
|
|
9860
|
+
args = ["", url];
|
|
9861
|
+
break;
|
|
9862
|
+
default:
|
|
9863
|
+
command = "xdg-open";
|
|
9864
|
+
args = [url];
|
|
10240
9865
|
}
|
|
10241
|
-
|
|
9866
|
+
const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
|
|
9867
|
+
spawn3(command, args, options).unref();
|
|
10242
9868
|
}
|
|
10243
|
-
function
|
|
10244
|
-
return
|
|
9869
|
+
function startCallbackServer(port, expectedState) {
|
|
9870
|
+
return new Promise((resolve2, reject) => {
|
|
9871
|
+
const server = http.createServer(async (req, res) => {
|
|
9872
|
+
if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
|
|
9873
|
+
res.writeHead(404);
|
|
9874
|
+
res.end("Not Found");
|
|
9875
|
+
return;
|
|
9876
|
+
}
|
|
9877
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
9878
|
+
const code = url.searchParams.get("code");
|
|
9879
|
+
const state = url.searchParams.get("state");
|
|
9880
|
+
const error = url.searchParams.get("error");
|
|
9881
|
+
if (error) {
|
|
9882
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9883
|
+
res.end(buildErrorPage(error));
|
|
9884
|
+
server.close();
|
|
9885
|
+
reject(new Error(error));
|
|
9886
|
+
return;
|
|
9887
|
+
}
|
|
9888
|
+
if (state !== expectedState) {
|
|
9889
|
+
const errorMsg = "Security error: state parameter mismatch";
|
|
9890
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9891
|
+
res.end(buildErrorPage(errorMsg));
|
|
9892
|
+
server.close();
|
|
9893
|
+
reject(new Error(errorMsg));
|
|
9894
|
+
return;
|
|
9895
|
+
}
|
|
9896
|
+
if (!code) {
|
|
9897
|
+
const errorMsg = "No authorization code received";
|
|
9898
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
9899
|
+
res.end(buildErrorPage(errorMsg));
|
|
9900
|
+
server.close();
|
|
9901
|
+
reject(new Error(errorMsg));
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
try {
|
|
9905
|
+
const result = await exchangeCodeForToken(code, state);
|
|
9906
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9907
|
+
res.end(buildSuccessPage());
|
|
9908
|
+
server.close();
|
|
9909
|
+
resolve2(result);
|
|
9910
|
+
} catch (err) {
|
|
9911
|
+
const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
|
|
9912
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
9913
|
+
res.end(buildErrorPage(errorMsg));
|
|
9914
|
+
server.close();
|
|
9915
|
+
reject(err);
|
|
9916
|
+
}
|
|
9917
|
+
});
|
|
9918
|
+
server.on("error", (error) => {
|
|
9919
|
+
if (error.code === "EADDRINUSE") {
|
|
9920
|
+
const portError = new Error(
|
|
9921
|
+
"Something went wrong. Please restart the CLI and try again."
|
|
9922
|
+
);
|
|
9923
|
+
portError.code = "EADDRINUSE";
|
|
9924
|
+
reject(portError);
|
|
9925
|
+
} else {
|
|
9926
|
+
reject(error);
|
|
9927
|
+
}
|
|
9928
|
+
});
|
|
9929
|
+
const timeout = setTimeout(() => {
|
|
9930
|
+
server.close();
|
|
9931
|
+
reject(new Error("Login timeout - no response received after 5 minutes"));
|
|
9932
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
9933
|
+
server.on("close", () => {
|
|
9934
|
+
clearTimeout(timeout);
|
|
9935
|
+
});
|
|
9936
|
+
server.listen(port, "127.0.0.1", () => {
|
|
9937
|
+
console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
|
|
9938
|
+
});
|
|
9939
|
+
});
|
|
10245
9940
|
}
|
|
10246
|
-
function
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
9941
|
+
function buildSuccessPage() {
|
|
9942
|
+
return `
|
|
9943
|
+
<!DOCTYPE html>
|
|
9944
|
+
<html lang="en">
|
|
9945
|
+
<head>
|
|
9946
|
+
<meta charset="UTF-8" />
|
|
9947
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9948
|
+
<title>Login Successful - Supatest CLI</title>
|
|
9949
|
+
<style>
|
|
9950
|
+
body {
|
|
9951
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
9952
|
+
display: flex;
|
|
9953
|
+
align-items: center;
|
|
9954
|
+
justify-content: center;
|
|
9955
|
+
height: 100vh;
|
|
9956
|
+
margin: 0;
|
|
9957
|
+
background: #fefefe;
|
|
9958
|
+
}
|
|
9959
|
+
.container {
|
|
9960
|
+
background: white;
|
|
9961
|
+
padding: 3rem 2rem;
|
|
9962
|
+
border-radius: 12px;
|
|
9963
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
9964
|
+
border: 1px solid #e5e7eb;
|
|
9965
|
+
text-align: center;
|
|
9966
|
+
max-width: 400px;
|
|
9967
|
+
width: 90%;
|
|
9968
|
+
}
|
|
9969
|
+
.logo {
|
|
9970
|
+
margin: 0 auto 2rem;
|
|
9971
|
+
display: flex;
|
|
9972
|
+
align-items: center;
|
|
9973
|
+
justify-content: center;
|
|
9974
|
+
gap: 8px;
|
|
9975
|
+
}
|
|
9976
|
+
.logo img {
|
|
9977
|
+
width: 24px;
|
|
9978
|
+
height: 24px;
|
|
9979
|
+
border-radius: 6px;
|
|
9980
|
+
object-fit: contain;
|
|
9981
|
+
}
|
|
9982
|
+
.logo-text {
|
|
9983
|
+
font-weight: 600;
|
|
9984
|
+
font-size: 16px;
|
|
9985
|
+
color: inherit;
|
|
9986
|
+
}
|
|
9987
|
+
.success-icon {
|
|
9988
|
+
width: 64px;
|
|
9989
|
+
height: 64px;
|
|
9990
|
+
border-radius: 50%;
|
|
9991
|
+
background: #d1fae5;
|
|
9992
|
+
display: flex;
|
|
9993
|
+
align-items: center;
|
|
9994
|
+
justify-content: center;
|
|
9995
|
+
font-size: 32px;
|
|
9996
|
+
margin: 0 auto 1.5rem;
|
|
9997
|
+
}
|
|
9998
|
+
h1 {
|
|
9999
|
+
color: #10b981;
|
|
10000
|
+
margin: 0 0 1rem 0;
|
|
10001
|
+
font-size: 24px;
|
|
10002
|
+
font-weight: 600;
|
|
10003
|
+
}
|
|
10004
|
+
p {
|
|
10005
|
+
color: #666;
|
|
10006
|
+
margin: 0;
|
|
10007
|
+
line-height: 1.5;
|
|
10008
|
+
}
|
|
10009
|
+
@media (prefers-color-scheme: dark) {
|
|
10010
|
+
body {
|
|
10011
|
+
background: #1a1a1a;
|
|
10012
|
+
}
|
|
10013
|
+
.container {
|
|
10014
|
+
background: #1f1f1f;
|
|
10015
|
+
border-color: #333;
|
|
10016
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
10017
|
+
}
|
|
10018
|
+
.success-icon {
|
|
10019
|
+
background: #064e3b;
|
|
10020
|
+
}
|
|
10021
|
+
h1 {
|
|
10022
|
+
color: #34d399;
|
|
10023
|
+
}
|
|
10024
|
+
p {
|
|
10025
|
+
color: #a3a3a3;
|
|
10026
|
+
}
|
|
10027
|
+
.logo-text {
|
|
10028
|
+
color: #e5e7eb;
|
|
10029
|
+
}
|
|
10030
|
+
}
|
|
10031
|
+
</style>
|
|
10032
|
+
</head>
|
|
10033
|
+
<body>
|
|
10034
|
+
<div class="container">
|
|
10035
|
+
<div class="logo">
|
|
10036
|
+
<img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
|
|
10037
|
+
<span class="logo-text">Supatest</span>
|
|
10038
|
+
</div>
|
|
10039
|
+
<div class="success-icon" role="img" aria-label="Success">\u2705</div>
|
|
10040
|
+
<h1>Login Successful!</h1>
|
|
10041
|
+
<p>You're now authenticated with Supatest CLI.</p>
|
|
10042
|
+
<p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
|
|
10043
|
+
</div>
|
|
10044
|
+
</body>
|
|
10045
|
+
</html>
|
|
10046
|
+
`;
|
|
10250
10047
|
}
|
|
10251
|
-
function
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10048
|
+
function buildErrorPage(errorMessage) {
|
|
10049
|
+
return `
|
|
10050
|
+
<!DOCTYPE html>
|
|
10051
|
+
<html lang="en">
|
|
10052
|
+
<head>
|
|
10053
|
+
<meta charset="UTF-8" />
|
|
10054
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10055
|
+
<title>Login Failed - Supatest CLI</title>
|
|
10056
|
+
<style>
|
|
10057
|
+
body {
|
|
10058
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10059
|
+
display: flex;
|
|
10060
|
+
align-items: center;
|
|
10061
|
+
justify-content: center;
|
|
10062
|
+
height: 100vh;
|
|
10063
|
+
margin: 0;
|
|
10064
|
+
background: #fefefe;
|
|
10065
|
+
}
|
|
10066
|
+
.container {
|
|
10067
|
+
background: white;
|
|
10068
|
+
padding: 3rem 2rem;
|
|
10069
|
+
border-radius: 12px;
|
|
10070
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
10071
|
+
border: 1px solid #e5e7eb;
|
|
10072
|
+
text-align: center;
|
|
10073
|
+
max-width: 400px;
|
|
10074
|
+
width: 90%;
|
|
10075
|
+
}
|
|
10076
|
+
.logo {
|
|
10077
|
+
width: 48px;
|
|
10078
|
+
height: 48px;
|
|
10079
|
+
margin: 0 auto 2rem;
|
|
10080
|
+
display: flex;
|
|
10081
|
+
align-items: center;
|
|
10082
|
+
justify-content: center;
|
|
10083
|
+
}
|
|
10084
|
+
.logo img {
|
|
10085
|
+
width: 48px;
|
|
10086
|
+
height: 48px;
|
|
10087
|
+
border-radius: 8px;
|
|
10088
|
+
object-fit: contain;
|
|
10089
|
+
}
|
|
10090
|
+
.error-icon {
|
|
10091
|
+
width: 64px;
|
|
10092
|
+
height: 64px;
|
|
10093
|
+
border-radius: 50%;
|
|
10094
|
+
background: #fee2e2;
|
|
10095
|
+
display: flex;
|
|
10096
|
+
align-items: center;
|
|
10097
|
+
justify-content: center;
|
|
10098
|
+
font-size: 32px;
|
|
10099
|
+
margin: 0 auto 1.5rem;
|
|
10100
|
+
}
|
|
10101
|
+
h1 {
|
|
10102
|
+
color: #dc2626;
|
|
10103
|
+
margin: 0 0 1rem 0;
|
|
10104
|
+
font-size: 24px;
|
|
10105
|
+
font-weight: 600;
|
|
10106
|
+
}
|
|
10107
|
+
p {
|
|
10108
|
+
color: #666;
|
|
10109
|
+
margin: 0;
|
|
10110
|
+
line-height: 1.5;
|
|
10111
|
+
}
|
|
10112
|
+
.brand {
|
|
10113
|
+
margin-top: 2rem;
|
|
10114
|
+
padding-top: 1.5rem;
|
|
10115
|
+
border-top: 1px solid #e5e7eb;
|
|
10116
|
+
color: #9333ff;
|
|
10117
|
+
font-weight: 600;
|
|
10118
|
+
font-size: 14px;
|
|
10119
|
+
}
|
|
10120
|
+
@media (prefers-color-scheme: dark) {
|
|
10121
|
+
body {
|
|
10122
|
+
background: #1a1a1a;
|
|
10123
|
+
}
|
|
10124
|
+
.container {
|
|
10125
|
+
background: #1f1f1f;
|
|
10126
|
+
border-color: #333;
|
|
10127
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
10128
|
+
}
|
|
10129
|
+
.error-icon {
|
|
10130
|
+
background: #7f1d1d;
|
|
10131
|
+
}
|
|
10132
|
+
h1 {
|
|
10133
|
+
color: #f87171;
|
|
10134
|
+
}
|
|
10135
|
+
p {
|
|
10136
|
+
color: #a3a3a3;
|
|
10137
|
+
}
|
|
10138
|
+
.brand {
|
|
10139
|
+
border-top-color: #333;
|
|
10140
|
+
color: #a855f7;
|
|
10141
|
+
}
|
|
10142
|
+
}
|
|
10143
|
+
</style>
|
|
10144
|
+
</head>
|
|
10145
|
+
<body>
|
|
10146
|
+
<div class="container">
|
|
10147
|
+
<div class="logo">
|
|
10148
|
+
<img src="${FRONTEND_URL}/logo.png" alt="Supatest Logo" />
|
|
10149
|
+
<span class="logo-text">Supatest</span>
|
|
10150
|
+
</div>
|
|
10151
|
+
<div class="error-icon" role="img" aria-label="Error">\u274C</div>
|
|
10152
|
+
<h1>Login Failed</h1>
|
|
10153
|
+
<p>${escapeHtml(errorMessage)}</p>
|
|
10154
|
+
<p style="margin-top: 1rem;">You can close this window and try again.</p>
|
|
10155
|
+
</div>
|
|
10156
|
+
</body>
|
|
10157
|
+
</html>
|
|
10158
|
+
`;
|
|
10264
10159
|
}
|
|
10265
|
-
function
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10160
|
+
async function loginCommand() {
|
|
10161
|
+
console.log("\nAuthenticating with Supatest...\n");
|
|
10162
|
+
const state = generateState();
|
|
10163
|
+
const loginPromise = startCallbackServer(CLI_LOGIN_PORT, state);
|
|
10164
|
+
const loginUrl = `${FRONTEND_URL}/cli-login?port=${CLI_LOGIN_PORT}&state=${state}`;
|
|
10165
|
+
console.log(`Opening browser to: ${loginUrl}`);
|
|
10166
|
+
console.log("\nIf your browser doesn't open automatically, please visit the URL above.\n");
|
|
10167
|
+
try {
|
|
10168
|
+
openBrowser(loginUrl);
|
|
10169
|
+
} catch (error) {
|
|
10170
|
+
console.warn("Failed to open browser automatically:", error);
|
|
10171
|
+
console.log(`
|
|
10172
|
+
Please manually open this URL in your browser:
|
|
10173
|
+
${loginUrl}
|
|
10174
|
+
`);
|
|
10269
10175
|
}
|
|
10270
10176
|
try {
|
|
10271
|
-
const
|
|
10272
|
-
|
|
10273
|
-
|
|
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;
|
|
10177
|
+
const result = await loginPromise;
|
|
10178
|
+
console.log("\n\u2705 Login successful!\n");
|
|
10179
|
+
return result;
|
|
10287
10180
|
} catch (error) {
|
|
10288
10181
|
const err = error;
|
|
10289
|
-
if (err.
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10182
|
+
if (err.code === "EADDRINUSE") {
|
|
10183
|
+
console.error("\n\u274C Login failed: Something went wrong.");
|
|
10184
|
+
console.error(" Please restart the CLI and try again.\n");
|
|
10185
|
+
} else {
|
|
10186
|
+
console.error("\n\u274C Login failed:", error.message, "\n");
|
|
10294
10187
|
}
|
|
10295
|
-
|
|
10296
|
-
}
|
|
10297
|
-
}
|
|
10298
|
-
function removeToken() {
|
|
10299
|
-
const tokenFile = getTokenFilePath();
|
|
10300
|
-
if (existsSync5(tokenFile)) {
|
|
10301
|
-
unlinkSync2(tokenFile);
|
|
10188
|
+
throw error;
|
|
10302
10189
|
}
|
|
10303
10190
|
}
|
|
10304
|
-
var
|
|
10305
|
-
var
|
|
10306
|
-
"src/
|
|
10191
|
+
var CLI_LOGIN_PORT, FRONTEND_URL, API_URL, CALLBACK_TIMEOUT_MS, STATE_LENGTH;
|
|
10192
|
+
var init_login = __esm({
|
|
10193
|
+
"src/commands/login.ts"() {
|
|
10307
10194
|
"use strict";
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10195
|
+
CLI_LOGIN_PORT = 8420;
|
|
10196
|
+
FRONTEND_URL = process.env.SUPATEST_FRONTEND_URL || "https://code.supatest.ai";
|
|
10197
|
+
API_URL = process.env.SUPATEST_API_URL || "https://code-api.supatest.ai";
|
|
10198
|
+
CALLBACK_TIMEOUT_MS = 3e5;
|
|
10199
|
+
STATE_LENGTH = 32;
|
|
10313
10200
|
}
|
|
10314
10201
|
});
|
|
10315
10202
|
|
|
10316
|
-
// src/
|
|
10317
|
-
var
|
|
10318
|
-
|
|
10319
|
-
|
|
10203
|
+
// src/utils/claude-oauth.ts
|
|
10204
|
+
var claude_oauth_exports = {};
|
|
10205
|
+
__export(claude_oauth_exports, {
|
|
10206
|
+
ClaudeOAuthService: () => ClaudeOAuthService
|
|
10207
|
+
});
|
|
10208
|
+
import { spawn as spawn4 } from "child_process";
|
|
10209
|
+
import { createHash, randomBytes } from "crypto";
|
|
10210
|
+
import http2 from "http";
|
|
10211
|
+
import { platform as platform2 } from "os";
|
|
10212
|
+
var OAUTH_CONFIG, CALLBACK_PORT, CALLBACK_TIMEOUT_MS2, ClaudeOAuthService;
|
|
10213
|
+
var init_claude_oauth = __esm({
|
|
10214
|
+
"src/utils/claude-oauth.ts"() {
|
|
10320
10215
|
"use strict";
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10216
|
+
OAUTH_CONFIG = {
|
|
10217
|
+
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
10218
|
+
// Claude Code's client ID
|
|
10219
|
+
authorizationEndpoint: "https://claude.ai/oauth/authorize",
|
|
10220
|
+
tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
|
|
10221
|
+
redirectUri: "http://localhost:8421/callback",
|
|
10222
|
+
// Local callback for CLI
|
|
10223
|
+
scopes: ["user:inference", "user:profile", "org:create_api_key"]
|
|
10224
|
+
};
|
|
10225
|
+
CALLBACK_PORT = 8421;
|
|
10226
|
+
CALLBACK_TIMEOUT_MS2 = 3e5;
|
|
10227
|
+
ClaudeOAuthService = class _ClaudeOAuthService {
|
|
10228
|
+
secretStorage;
|
|
10229
|
+
static TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
10230
|
+
// 5 minutes
|
|
10231
|
+
pendingCodeVerifier = null;
|
|
10232
|
+
// Store code verifier for PKCE
|
|
10233
|
+
constructor(secretStorage) {
|
|
10234
|
+
this.secretStorage = secretStorage;
|
|
10235
|
+
}
|
|
10236
|
+
/**
|
|
10237
|
+
* Starts the OAuth authorization flow
|
|
10238
|
+
* Opens the default browser for user authentication
|
|
10239
|
+
* Returns after successful authentication
|
|
10240
|
+
*/
|
|
10241
|
+
async authorize() {
|
|
10242
|
+
try {
|
|
10243
|
+
const state = this.generateRandomState();
|
|
10244
|
+
const pkce = this.generatePKCEChallenge();
|
|
10245
|
+
this.pendingCodeVerifier = pkce.codeVerifier;
|
|
10246
|
+
const authUrl = this.buildAuthorizationUrl(state, pkce.codeChallenge);
|
|
10247
|
+
console.log("\nAuthenticating with Claude...\n");
|
|
10248
|
+
console.log(`Opening browser to: ${authUrl}
|
|
10249
|
+
`);
|
|
10250
|
+
const tokenPromise = this.startCallbackServer(CALLBACK_PORT, state);
|
|
10251
|
+
try {
|
|
10252
|
+
this.openBrowser(authUrl);
|
|
10253
|
+
} catch (error) {
|
|
10254
|
+
console.warn("Failed to open browser automatically:", error);
|
|
10255
|
+
console.log(`
|
|
10256
|
+
Please manually open this URL in your browser:
|
|
10257
|
+
${authUrl}
|
|
10258
|
+
`);
|
|
10259
|
+
}
|
|
10260
|
+
await tokenPromise;
|
|
10261
|
+
console.log("\n\u2705 Successfully authenticated with Claude!\n");
|
|
10262
|
+
return { success: true };
|
|
10263
|
+
} catch (error) {
|
|
10264
|
+
this.pendingCodeVerifier = null;
|
|
10265
|
+
return {
|
|
10266
|
+
success: false,
|
|
10267
|
+
error: error instanceof Error ? error.message : "Authentication failed"
|
|
10268
|
+
};
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
/**
|
|
10272
|
+
* Start local HTTP server to receive OAuth callback
|
|
10273
|
+
*/
|
|
10274
|
+
startCallbackServer(port, expectedState) {
|
|
10275
|
+
return new Promise((resolve2, reject) => {
|
|
10276
|
+
const server = http2.createServer(async (req, res) => {
|
|
10277
|
+
if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
|
|
10278
|
+
res.writeHead(404);
|
|
10279
|
+
res.end("Not Found");
|
|
10280
|
+
return;
|
|
10281
|
+
}
|
|
10282
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
10283
|
+
const code = url.searchParams.get("code");
|
|
10284
|
+
const returnedState = url.searchParams.get("state");
|
|
10285
|
+
const error = url.searchParams.get("error");
|
|
10286
|
+
if (error) {
|
|
10287
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
10288
|
+
res.end(this.buildErrorPage(error));
|
|
10289
|
+
server.close();
|
|
10290
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
10291
|
+
return;
|
|
10292
|
+
}
|
|
10293
|
+
if (returnedState !== expectedState) {
|
|
10294
|
+
const errorMsg = "Security error: state parameter mismatch";
|
|
10295
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
10296
|
+
res.end(this.buildErrorPage(errorMsg));
|
|
10297
|
+
server.close();
|
|
10298
|
+
reject(new Error(errorMsg));
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
if (!code) {
|
|
10302
|
+
const errorMsg = "No authorization code received";
|
|
10303
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
10304
|
+
res.end(this.buildErrorPage(errorMsg));
|
|
10305
|
+
server.close();
|
|
10306
|
+
reject(new Error(errorMsg));
|
|
10307
|
+
return;
|
|
10308
|
+
}
|
|
10309
|
+
try {
|
|
10310
|
+
await this.submitAuthCode(code, returnedState);
|
|
10311
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
10312
|
+
res.end(this.buildSuccessPage());
|
|
10313
|
+
server.close();
|
|
10314
|
+
resolve2();
|
|
10315
|
+
} catch (err) {
|
|
10316
|
+
const errorMsg = err instanceof Error ? err.message : "Token exchange failed";
|
|
10317
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
10318
|
+
res.end(this.buildErrorPage(errorMsg));
|
|
10319
|
+
server.close();
|
|
10320
|
+
reject(err);
|
|
10321
|
+
}
|
|
10322
|
+
});
|
|
10323
|
+
server.on("error", (error) => {
|
|
10324
|
+
if (error.code === "EADDRINUSE") {
|
|
10325
|
+
reject(new Error("Port already in use. Please try again."));
|
|
10326
|
+
} else {
|
|
10327
|
+
reject(error);
|
|
10328
|
+
}
|
|
10329
|
+
});
|
|
10330
|
+
const timeout = setTimeout(() => {
|
|
10331
|
+
server.close();
|
|
10332
|
+
reject(new Error("Authentication timeout - no response received after 5 minutes"));
|
|
10333
|
+
}, CALLBACK_TIMEOUT_MS2);
|
|
10334
|
+
server.on("close", () => {
|
|
10335
|
+
clearTimeout(timeout);
|
|
10336
|
+
});
|
|
10337
|
+
server.listen(port, "127.0.0.1", () => {
|
|
10338
|
+
console.log(`Waiting for authentication callback on http://localhost:${port}/callback`);
|
|
10339
|
+
});
|
|
10340
|
+
});
|
|
10341
|
+
}
|
|
10342
|
+
/**
|
|
10343
|
+
* Submits the authorization code and exchanges it for tokens
|
|
10344
|
+
*/
|
|
10345
|
+
async submitAuthCode(code, state) {
|
|
10346
|
+
const tokens = await this.exchangeCodeForTokens(code, state);
|
|
10347
|
+
await this.saveTokens(tokens);
|
|
10348
|
+
return tokens;
|
|
10349
|
+
}
|
|
10350
|
+
/**
|
|
10351
|
+
* Exchanges authorization code for access and refresh tokens
|
|
10352
|
+
*/
|
|
10353
|
+
async exchangeCodeForTokens(code, state) {
|
|
10354
|
+
if (!this.pendingCodeVerifier) {
|
|
10355
|
+
throw new Error("No PKCE code verifier found. Please start the auth flow first.");
|
|
10356
|
+
}
|
|
10357
|
+
const body = {
|
|
10358
|
+
grant_type: "authorization_code",
|
|
10359
|
+
code,
|
|
10360
|
+
state,
|
|
10361
|
+
// Non-standard: state in body
|
|
10362
|
+
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
10363
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
10364
|
+
code_verifier: this.pendingCodeVerifier
|
|
10365
|
+
// PKCE verifier
|
|
10366
|
+
};
|
|
10367
|
+
const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
|
|
10368
|
+
method: "POST",
|
|
10369
|
+
headers: {
|
|
10370
|
+
"Content-Type": "application/json"
|
|
10371
|
+
// Non-standard: JSON instead of form-encoded
|
|
10372
|
+
},
|
|
10373
|
+
body: JSON.stringify(body)
|
|
10374
|
+
});
|
|
10375
|
+
this.pendingCodeVerifier = null;
|
|
10376
|
+
if (!response.ok) {
|
|
10377
|
+
const error = await response.text();
|
|
10378
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
10379
|
+
}
|
|
10380
|
+
const data = await response.json();
|
|
10381
|
+
return {
|
|
10382
|
+
accessToken: data.access_token,
|
|
10383
|
+
refreshToken: data.refresh_token,
|
|
10384
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
10385
|
+
};
|
|
10386
|
+
}
|
|
10387
|
+
/**
|
|
10388
|
+
* Refreshes the access token using the refresh token
|
|
10389
|
+
*/
|
|
10390
|
+
async refreshTokens() {
|
|
10391
|
+
const tokens = await this.getTokens();
|
|
10392
|
+
if (!tokens) {
|
|
10393
|
+
throw new Error("No tokens found to refresh");
|
|
10394
|
+
}
|
|
10395
|
+
const body = {
|
|
10396
|
+
grant_type: "refresh_token",
|
|
10397
|
+
refresh_token: tokens.refreshToken,
|
|
10398
|
+
client_id: OAUTH_CONFIG.clientId
|
|
10399
|
+
};
|
|
10400
|
+
const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
|
|
10401
|
+
method: "POST",
|
|
10402
|
+
headers: {
|
|
10403
|
+
"Content-Type": "application/json"
|
|
10404
|
+
},
|
|
10405
|
+
body: JSON.stringify(body)
|
|
10406
|
+
});
|
|
10407
|
+
if (!response.ok) {
|
|
10408
|
+
const error = await response.text();
|
|
10409
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
10410
|
+
}
|
|
10411
|
+
const data = await response.json();
|
|
10412
|
+
const newTokens = {
|
|
10413
|
+
accessToken: data.access_token,
|
|
10414
|
+
refreshToken: data.refresh_token,
|
|
10415
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
10416
|
+
};
|
|
10417
|
+
await this.saveTokens(newTokens);
|
|
10418
|
+
return newTokens;
|
|
10328
10419
|
}
|
|
10329
10420
|
/**
|
|
10330
|
-
*
|
|
10421
|
+
* Gets the current access token, refreshing if necessary
|
|
10331
10422
|
*/
|
|
10332
|
-
|
|
10333
|
-
|
|
10423
|
+
async getAccessToken() {
|
|
10424
|
+
const tokens = await this.getTokens();
|
|
10425
|
+
if (!tokens) {
|
|
10426
|
+
return null;
|
|
10427
|
+
}
|
|
10428
|
+
if (Date.now() > tokens.expiresAt - _ClaudeOAuthService.TOKEN_REFRESH_BUFFER_MS) {
|
|
10429
|
+
try {
|
|
10430
|
+
const refreshedTokens = await this.refreshTokens();
|
|
10431
|
+
return refreshedTokens.accessToken;
|
|
10432
|
+
} catch (error) {
|
|
10433
|
+
console.warn("Token refresh failed:", error);
|
|
10434
|
+
return null;
|
|
10435
|
+
}
|
|
10436
|
+
}
|
|
10437
|
+
return tokens.accessToken;
|
|
10334
10438
|
}
|
|
10335
10439
|
/**
|
|
10336
|
-
*
|
|
10337
|
-
* Call this from the UI when user submits a message during agent execution.
|
|
10440
|
+
* Gets the stored tokens
|
|
10338
10441
|
*/
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
}
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
resolver({ value: message, done: false });
|
|
10356
|
-
} else {
|
|
10357
|
-
this.queue.push(message);
|
|
10442
|
+
async getTokens() {
|
|
10443
|
+
try {
|
|
10444
|
+
const accessToken = await this.secretStorage.getSecret("claude_oauth_access_token");
|
|
10445
|
+
const refreshToken = await this.secretStorage.getSecret("claude_oauth_refresh_token");
|
|
10446
|
+
const expiresAt = await this.secretStorage.getSecret("claude_oauth_expires_at");
|
|
10447
|
+
if (!accessToken || !refreshToken || !expiresAt) {
|
|
10448
|
+
return null;
|
|
10449
|
+
}
|
|
10450
|
+
return {
|
|
10451
|
+
accessToken,
|
|
10452
|
+
refreshToken,
|
|
10453
|
+
expiresAt: parseInt(expiresAt, 10)
|
|
10454
|
+
};
|
|
10455
|
+
} catch (error) {
|
|
10456
|
+
console.error("Failed to get OAuth tokens:", error);
|
|
10457
|
+
return null;
|
|
10358
10458
|
}
|
|
10359
10459
|
}
|
|
10360
10460
|
/**
|
|
10361
|
-
*
|
|
10461
|
+
* Saves OAuth tokens to secure storage
|
|
10362
10462
|
*/
|
|
10363
|
-
|
|
10364
|
-
|
|
10463
|
+
async saveTokens(tokens) {
|
|
10464
|
+
await this.secretStorage.setSecret("claude_oauth_access_token", tokens.accessToken);
|
|
10465
|
+
await this.secretStorage.setSecret("claude_oauth_refresh_token", tokens.refreshToken);
|
|
10466
|
+
await this.secretStorage.setSecret("claude_oauth_expires_at", tokens.expiresAt.toString());
|
|
10365
10467
|
}
|
|
10366
10468
|
/**
|
|
10367
|
-
*
|
|
10368
|
-
* Any pending async iterators will receive done: true.
|
|
10469
|
+
* Deletes stored OAuth tokens
|
|
10369
10470
|
*/
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
this.
|
|
10373
|
-
|
|
10374
|
-
resolver({ value: void 0, done: true });
|
|
10375
|
-
}
|
|
10376
|
-
this.resolvers = [];
|
|
10471
|
+
async deleteTokens() {
|
|
10472
|
+
await this.secretStorage.deleteSecret("claude_oauth_access_token");
|
|
10473
|
+
await this.secretStorage.deleteSecret("claude_oauth_refresh_token");
|
|
10474
|
+
await this.secretStorage.deleteSecret("claude_oauth_expires_at");
|
|
10377
10475
|
}
|
|
10378
10476
|
/**
|
|
10379
|
-
*
|
|
10477
|
+
* Checks if user is authenticated via OAuth
|
|
10380
10478
|
*/
|
|
10381
|
-
|
|
10382
|
-
|
|
10479
|
+
async isAuthenticated() {
|
|
10480
|
+
const tokens = await this.getTokens();
|
|
10481
|
+
return tokens !== null;
|
|
10383
10482
|
}
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
return Promise.resolve({
|
|
10393
|
-
value: void 0,
|
|
10394
|
-
done: true
|
|
10395
|
-
});
|
|
10396
|
-
}
|
|
10397
|
-
return new Promise((resolve2) => {
|
|
10398
|
-
this.resolvers.push(resolve2);
|
|
10399
|
-
});
|
|
10483
|
+
/**
|
|
10484
|
+
* Gets the current OAuth authentication status
|
|
10485
|
+
*/
|
|
10486
|
+
async getStatus() {
|
|
10487
|
+
try {
|
|
10488
|
+
const tokens = await this.getTokens();
|
|
10489
|
+
if (!tokens) {
|
|
10490
|
+
return { isAuthenticated: false };
|
|
10400
10491
|
}
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
})
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
import { platform as platform2 } from "os";
|
|
10412
|
-
function escapeHtml(text) {
|
|
10413
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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;
|
|
10492
|
+
return {
|
|
10493
|
+
isAuthenticated: true,
|
|
10494
|
+
expiresAt: tokens.expiresAt
|
|
10495
|
+
};
|
|
10496
|
+
} catch (error) {
|
|
10497
|
+
return {
|
|
10498
|
+
isAuthenticated: false,
|
|
10499
|
+
error: error instanceof Error ? error.message : "Failed to get OAuth status"
|
|
10500
|
+
};
|
|
10501
|
+
}
|
|
10469
10502
|
}
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10503
|
+
/**
|
|
10504
|
+
* Builds the authorization URL with all required parameters
|
|
10505
|
+
*/
|
|
10506
|
+
buildAuthorizationUrl(state, codeChallenge) {
|
|
10507
|
+
const params = new URLSearchParams({
|
|
10508
|
+
response_type: "code",
|
|
10509
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
10510
|
+
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
10511
|
+
scope: OAUTH_CONFIG.scopes.join(" "),
|
|
10512
|
+
state
|
|
10513
|
+
});
|
|
10514
|
+
if (codeChallenge) {
|
|
10515
|
+
params.set("code_challenge", codeChallenge);
|
|
10516
|
+
params.set("code_challenge_method", "S256");
|
|
10517
|
+
}
|
|
10518
|
+
return `${OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
|
10477
10519
|
}
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
reject(new Error(errorMsg));
|
|
10484
|
-
return;
|
|
10520
|
+
/**
|
|
10521
|
+
* Generates a random state string for CSRF protection
|
|
10522
|
+
*/
|
|
10523
|
+
generateRandomState() {
|
|
10524
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10485
10525
|
}
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
reject(err);
|
|
10526
|
+
/**
|
|
10527
|
+
* Generates PKCE code verifier and challenge
|
|
10528
|
+
* PKCE (Proof Key for Code Exchange) adds security to OAuth for public clients
|
|
10529
|
+
*/
|
|
10530
|
+
generatePKCEChallenge() {
|
|
10531
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
10532
|
+
const hash = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
10533
|
+
return {
|
|
10534
|
+
codeVerifier,
|
|
10535
|
+
codeChallenge: hash
|
|
10536
|
+
};
|
|
10498
10537
|
}
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10538
|
+
/**
|
|
10539
|
+
* Open a URL in the default browser cross-platform
|
|
10540
|
+
*/
|
|
10541
|
+
openBrowser(url) {
|
|
10542
|
+
const os3 = platform2();
|
|
10543
|
+
let command;
|
|
10544
|
+
let args;
|
|
10545
|
+
switch (os3) {
|
|
10546
|
+
case "darwin":
|
|
10547
|
+
command = "open";
|
|
10548
|
+
args = [url];
|
|
10549
|
+
break;
|
|
10550
|
+
case "win32":
|
|
10551
|
+
command = "start";
|
|
10552
|
+
args = ["", url];
|
|
10553
|
+
break;
|
|
10554
|
+
default:
|
|
10555
|
+
command = "xdg-open";
|
|
10556
|
+
args = [url];
|
|
10557
|
+
break;
|
|
10558
|
+
}
|
|
10559
|
+
const options = { detached: true, stdio: "ignore", shell: os3 === "win32" };
|
|
10560
|
+
spawn4(command, args, options).unref();
|
|
10509
10561
|
}
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
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) {
|
|
10562
|
+
/**
|
|
10563
|
+
* Build success HTML page
|
|
10564
|
+
*/
|
|
10565
|
+
buildSuccessPage() {
|
|
10566
|
+
return `
|
|
10567
|
+
<!DOCTYPE html>
|
|
10568
|
+
<html lang="en">
|
|
10569
|
+
<head>
|
|
10570
|
+
<meta charset="UTF-8" />
|
|
10571
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10572
|
+
<title>Authentication Successful - Supatest CLI</title>
|
|
10573
|
+
<style>
|
|
10592
10574
|
body {
|
|
10593
|
-
|
|
10575
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10576
|
+
display: flex;
|
|
10577
|
+
align-items: center;
|
|
10578
|
+
justify-content: center;
|
|
10579
|
+
height: 100vh;
|
|
10580
|
+
margin: 0;
|
|
10581
|
+
background: #fefefe;
|
|
10594
10582
|
}
|
|
10595
10583
|
.container {
|
|
10596
|
-
background:
|
|
10597
|
-
|
|
10598
|
-
|
|
10584
|
+
background: white;
|
|
10585
|
+
padding: 3rem 2rem;
|
|
10586
|
+
border-radius: 12px;
|
|
10587
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
10588
|
+
border: 1px solid #e5e7eb;
|
|
10589
|
+
text-align: center;
|
|
10590
|
+
max-width: 400px;
|
|
10599
10591
|
}
|
|
10600
10592
|
.success-icon {
|
|
10601
|
-
|
|
10593
|
+
font-size: 48px;
|
|
10594
|
+
margin-bottom: 1rem;
|
|
10602
10595
|
}
|
|
10603
10596
|
h1 {
|
|
10604
|
-
color: #
|
|
10597
|
+
color: #10b981;
|
|
10598
|
+
margin: 0 0 1rem 0;
|
|
10599
|
+
font-size: 24px;
|
|
10605
10600
|
}
|
|
10606
10601
|
p {
|
|
10607
|
-
color: #
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
color: #e5e7eb;
|
|
10602
|
+
color: #666;
|
|
10603
|
+
margin: 0;
|
|
10604
|
+
line-height: 1.5;
|
|
10611
10605
|
}
|
|
10612
|
-
|
|
10613
|
-
</
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
<
|
|
10619
|
-
<
|
|
10620
|
-
</div>
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
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) {
|
|
10606
|
+
</style>
|
|
10607
|
+
</head>
|
|
10608
|
+
<body>
|
|
10609
|
+
<div class="container">
|
|
10610
|
+
<div class="success-icon">\u2705</div>
|
|
10611
|
+
<h1>Authentication Successful!</h1>
|
|
10612
|
+
<p>You're now authenticated with Claude.</p>
|
|
10613
|
+
<p style="margin-top: 1rem;">You can close this window and return to your terminal.</p>
|
|
10614
|
+
</div>
|
|
10615
|
+
</body>
|
|
10616
|
+
</html>
|
|
10617
|
+
`;
|
|
10618
|
+
}
|
|
10619
|
+
/**
|
|
10620
|
+
* Build error HTML page
|
|
10621
|
+
*/
|
|
10622
|
+
buildErrorPage(errorMessage) {
|
|
10623
|
+
const escapedError = errorMessage.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10624
|
+
return `
|
|
10625
|
+
<!DOCTYPE html>
|
|
10626
|
+
<html lang="en">
|
|
10627
|
+
<head>
|
|
10628
|
+
<meta charset="UTF-8" />
|
|
10629
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10630
|
+
<title>Authentication Failed - Supatest CLI</title>
|
|
10631
|
+
<style>
|
|
10703
10632
|
body {
|
|
10704
|
-
|
|
10633
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10634
|
+
display: flex;
|
|
10635
|
+
align-items: center;
|
|
10636
|
+
justify-content: center;
|
|
10637
|
+
height: 100vh;
|
|
10638
|
+
margin: 0;
|
|
10639
|
+
background: #fefefe;
|
|
10705
10640
|
}
|
|
10706
10641
|
.container {
|
|
10707
|
-
background:
|
|
10708
|
-
|
|
10709
|
-
|
|
10642
|
+
background: white;
|
|
10643
|
+
padding: 3rem 2rem;
|
|
10644
|
+
border-radius: 12px;
|
|
10645
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
10646
|
+
border: 1px solid #e5e7eb;
|
|
10647
|
+
text-align: center;
|
|
10648
|
+
max-width: 400px;
|
|
10710
10649
|
}
|
|
10711
10650
|
.error-icon {
|
|
10712
|
-
|
|
10651
|
+
font-size: 48px;
|
|
10652
|
+
margin-bottom: 1rem;
|
|
10713
10653
|
}
|
|
10714
10654
|
h1 {
|
|
10715
|
-
color: #
|
|
10655
|
+
color: #dc2626;
|
|
10656
|
+
margin: 0 0 1rem 0;
|
|
10657
|
+
font-size: 24px;
|
|
10716
10658
|
}
|
|
10717
10659
|
p {
|
|
10718
|
-
color: #
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
border-top-color: #333;
|
|
10722
|
-
color: #a855f7;
|
|
10660
|
+
color: #666;
|
|
10661
|
+
margin: 0;
|
|
10662
|
+
line-height: 1.5;
|
|
10723
10663
|
}
|
|
10724
|
-
|
|
10725
|
-
</
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
<
|
|
10731
|
-
<
|
|
10664
|
+
</style>
|
|
10665
|
+
</head>
|
|
10666
|
+
<body>
|
|
10667
|
+
<div class="container">
|
|
10668
|
+
<div class="error-icon">\u274C</div>
|
|
10669
|
+
<h1>Authentication Failed</h1>
|
|
10670
|
+
<p>${escapedError}</p>
|
|
10671
|
+
<p style="margin-top: 1rem;">You can close this window and try again.</p>
|
|
10732
10672
|
</div>
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
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;
|
|
10673
|
+
</body>
|
|
10674
|
+
</html>
|
|
10675
|
+
`;
|
|
10676
|
+
}
|
|
10677
|
+
};
|
|
10771
10678
|
}
|
|
10679
|
+
});
|
|
10680
|
+
|
|
10681
|
+
// src/utils/secret-storage.ts
|
|
10682
|
+
var secret_storage_exports = {};
|
|
10683
|
+
__export(secret_storage_exports, {
|
|
10684
|
+
deleteSecret: () => deleteSecret,
|
|
10685
|
+
getSecret: () => getSecret,
|
|
10686
|
+
getSecretStorage: () => getSecretStorage,
|
|
10687
|
+
listSecrets: () => listSecrets,
|
|
10688
|
+
setSecret: () => setSecret
|
|
10689
|
+
});
|
|
10690
|
+
import { promises as fs4 } from "fs";
|
|
10691
|
+
import { homedir as homedir6 } from "os";
|
|
10692
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
10693
|
+
async function getSecret(key) {
|
|
10694
|
+
return storage.getSecret(key);
|
|
10772
10695
|
}
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10696
|
+
async function setSecret(key, value) {
|
|
10697
|
+
await storage.setSecret(key, value);
|
|
10698
|
+
}
|
|
10699
|
+
async function deleteSecret(key) {
|
|
10700
|
+
return storage.deleteSecret(key);
|
|
10701
|
+
}
|
|
10702
|
+
async function listSecrets() {
|
|
10703
|
+
return storage.listSecrets();
|
|
10704
|
+
}
|
|
10705
|
+
function getSecretStorage() {
|
|
10706
|
+
return storage;
|
|
10707
|
+
}
|
|
10708
|
+
var SECRET_FILE_NAME, FileSecretStorage, storage;
|
|
10709
|
+
var init_secret_storage = __esm({
|
|
10710
|
+
"src/utils/secret-storage.ts"() {
|
|
10776
10711
|
"use strict";
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10712
|
+
SECRET_FILE_NAME = "secrets.json";
|
|
10713
|
+
FileSecretStorage = class {
|
|
10714
|
+
secretFilePath;
|
|
10715
|
+
constructor() {
|
|
10716
|
+
const rootDirName = process.env.NODE_ENV === "development" ? ".supatest-dev" : ".supatest";
|
|
10717
|
+
const secretsDir = join8(homedir6(), rootDirName, "claude-auth");
|
|
10718
|
+
this.secretFilePath = join8(secretsDir, SECRET_FILE_NAME);
|
|
10719
|
+
}
|
|
10720
|
+
async ensureDirectoryExists() {
|
|
10721
|
+
const dir = dirname2(this.secretFilePath);
|
|
10722
|
+
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
10723
|
+
}
|
|
10724
|
+
async loadSecrets() {
|
|
10725
|
+
try {
|
|
10726
|
+
const data = await fs4.readFile(this.secretFilePath, "utf-8");
|
|
10727
|
+
const secrets = JSON.parse(data);
|
|
10728
|
+
return new Map(Object.entries(secrets));
|
|
10729
|
+
} catch (error) {
|
|
10730
|
+
const err = error;
|
|
10731
|
+
if (err.code === "ENOENT") {
|
|
10732
|
+
return /* @__PURE__ */ new Map();
|
|
10733
|
+
}
|
|
10734
|
+
try {
|
|
10735
|
+
await fs4.unlink(this.secretFilePath);
|
|
10736
|
+
} catch {
|
|
10737
|
+
}
|
|
10738
|
+
return /* @__PURE__ */ new Map();
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
async saveSecrets(secrets) {
|
|
10742
|
+
await this.ensureDirectoryExists();
|
|
10743
|
+
const data = Object.fromEntries(secrets);
|
|
10744
|
+
const json = JSON.stringify(data, null, 2);
|
|
10745
|
+
await fs4.writeFile(this.secretFilePath, json, { mode: 384 });
|
|
10746
|
+
}
|
|
10747
|
+
async getSecret(key) {
|
|
10748
|
+
const secrets = await this.loadSecrets();
|
|
10749
|
+
return secrets.get(key) ?? null;
|
|
10750
|
+
}
|
|
10751
|
+
async setSecret(key, value) {
|
|
10752
|
+
const secrets = await this.loadSecrets();
|
|
10753
|
+
secrets.set(key, value);
|
|
10754
|
+
await this.saveSecrets(secrets);
|
|
10755
|
+
}
|
|
10756
|
+
async deleteSecret(key) {
|
|
10757
|
+
const secrets = await this.loadSecrets();
|
|
10758
|
+
if (!secrets.has(key)) {
|
|
10759
|
+
return false;
|
|
10760
|
+
}
|
|
10761
|
+
secrets.delete(key);
|
|
10762
|
+
if (secrets.size === 0) {
|
|
10763
|
+
try {
|
|
10764
|
+
await fs4.unlink(this.secretFilePath);
|
|
10765
|
+
} catch (error) {
|
|
10766
|
+
const err = error;
|
|
10767
|
+
if (err.code !== "ENOENT") {
|
|
10768
|
+
throw error;
|
|
10769
|
+
}
|
|
10770
|
+
}
|
|
10771
|
+
} else {
|
|
10772
|
+
await this.saveSecrets(secrets);
|
|
10773
|
+
}
|
|
10774
|
+
return true;
|
|
10775
|
+
}
|
|
10776
|
+
async listSecrets() {
|
|
10777
|
+
const secrets = await this.loadSecrets();
|
|
10778
|
+
return Array.from(secrets.keys());
|
|
10779
|
+
}
|
|
10780
|
+
};
|
|
10781
|
+
storage = new FileSecretStorage();
|
|
10782
10782
|
}
|
|
10783
10783
|
});
|
|
10784
10784
|
|
|
@@ -13321,7 +13321,7 @@ var init_InputPrompt = __esm({
|
|
|
13321
13321
|
}
|
|
13322
13322
|
return /* @__PURE__ */ React24.createElement(Text19, { color: theme.text.primary, key: idx }, line);
|
|
13323
13323
|
})), !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)")))
|
|
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)")))));
|
|
13325
13325
|
});
|
|
13326
13326
|
InputPromptInner.displayName = "InputPromptInner";
|
|
13327
13327
|
InputPrompt = memo3(InputPromptInner);
|
|
@@ -13616,12 +13616,9 @@ var init_ProviderSelector = __esm({
|
|
|
13616
13616
|
ProviderSelector = ({
|
|
13617
13617
|
currentProvider,
|
|
13618
13618
|
onSelect,
|
|
13619
|
-
onCancel
|
|
13620
|
-
claudeMaxAvailable
|
|
13619
|
+
onCancel
|
|
13621
13620
|
}) => {
|
|
13622
|
-
const availableProviders = PROVIDERS
|
|
13623
|
-
(p) => p.id !== "claude-max" || claudeMaxAvailable
|
|
13624
|
-
);
|
|
13621
|
+
const availableProviders = PROVIDERS;
|
|
13625
13622
|
const currentIndex = availableProviders.findIndex(
|
|
13626
13623
|
(p) => p.id === currentProvider
|
|
13627
13624
|
);
|
|
@@ -14168,7 +14165,6 @@ var init_App = __esm({
|
|
|
14168
14165
|
return;
|
|
14169
14166
|
}
|
|
14170
14167
|
if (command === "/provider") {
|
|
14171
|
-
isClaudeMaxAvailable().then(setClaudeMaxAvailable);
|
|
14172
14168
|
setShowProviderSelector(true);
|
|
14173
14169
|
return;
|
|
14174
14170
|
}
|
|
@@ -14683,7 +14679,6 @@ var init_App = __esm({
|
|
|
14683
14679
|
showProviderSelector && /* @__PURE__ */ React31.createElement(
|
|
14684
14680
|
ProviderSelector,
|
|
14685
14681
|
{
|
|
14686
|
-
claudeMaxAvailable,
|
|
14687
14682
|
currentProvider: llmProvider,
|
|
14688
14683
|
onCancel: handleProviderSelectorCancel,
|
|
14689
14684
|
onSelect: handleProviderSelect
|
|
@@ -15351,61 +15346,9 @@ var init_interactive = __esm({
|
|
|
15351
15346
|
// src/index.ts
|
|
15352
15347
|
await init_config();
|
|
15353
15348
|
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
15349
|
init_setup();
|
|
15408
15350
|
await init_config();
|
|
15351
|
+
import { Command } from "commander";
|
|
15409
15352
|
|
|
15410
15353
|
// src/modes/headless.ts
|
|
15411
15354
|
init_api_client();
|
|
@@ -15419,7 +15362,7 @@ init_react();
|
|
|
15419
15362
|
init_MessageList();
|
|
15420
15363
|
init_SessionContext();
|
|
15421
15364
|
import { execSync as execSync2 } from "child_process";
|
|
15422
|
-
import { homedir as
|
|
15365
|
+
import { homedir as homedir4 } from "os";
|
|
15423
15366
|
import { Box as Box13, useApp } from "ink";
|
|
15424
15367
|
import React14, { useEffect as useEffect2, useRef as useRef4, useState as useState3 } from "react";
|
|
15425
15368
|
var getGitBranch = () => {
|
|
@@ -15431,7 +15374,7 @@ var getGitBranch = () => {
|
|
|
15431
15374
|
};
|
|
15432
15375
|
var getCurrentFolder = () => {
|
|
15433
15376
|
const cwd = process.cwd();
|
|
15434
|
-
const home =
|
|
15377
|
+
const home = homedir4();
|
|
15435
15378
|
if (cwd.startsWith(home)) {
|
|
15436
15379
|
return `~${cwd.slice(home.length)}`;
|
|
15437
15380
|
}
|
|
@@ -15678,7 +15621,7 @@ async function runAgent(config2) {
|
|
|
15678
15621
|
// src/utils/auto-update.ts
|
|
15679
15622
|
init_version();
|
|
15680
15623
|
init_error_logger();
|
|
15681
|
-
import { execSync as execSync3, spawn as
|
|
15624
|
+
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
15682
15625
|
import latestVersion from "latest-version";
|
|
15683
15626
|
import { gt } from "semver";
|
|
15684
15627
|
var UPDATE_CHECK_TIMEOUT = 3e3;
|
|
@@ -15719,8 +15662,24 @@ Updating Supatest CLI ${CLI_VERSION} \u2192 ${latest}...`);
|
|
|
15719
15662
|
timeout: INSTALL_TIMEOUT
|
|
15720
15663
|
});
|
|
15721
15664
|
}
|
|
15665
|
+
let updateVerified = false;
|
|
15666
|
+
try {
|
|
15667
|
+
const installedVersion = execSync3("npm ls -g @supatest/cli --json 2>/dev/null", {
|
|
15668
|
+
encoding: "utf-8"
|
|
15669
|
+
});
|
|
15670
|
+
const parsed = JSON.parse(installedVersion);
|
|
15671
|
+
const newVersion = parsed.dependencies?.["@supatest/cli"]?.version;
|
|
15672
|
+
if (newVersion && gt(newVersion, CLI_VERSION)) {
|
|
15673
|
+
updateVerified = true;
|
|
15674
|
+
}
|
|
15675
|
+
} catch {
|
|
15676
|
+
}
|
|
15677
|
+
if (!updateVerified) {
|
|
15678
|
+
console.log("Update completed but could not verify new version. Continuing with current version...\n");
|
|
15679
|
+
return;
|
|
15680
|
+
}
|
|
15722
15681
|
console.log("\u2713 Updated successfully\n");
|
|
15723
|
-
const child =
|
|
15682
|
+
const child = spawn2(process.argv[0], process.argv.slice(1), {
|
|
15724
15683
|
stdio: "inherit",
|
|
15725
15684
|
detached: false
|
|
15726
15685
|
});
|
|
@@ -16005,33 +15964,6 @@ program.command("setup").description("Check prerequisites and set up required to
|
|
|
16005
15964
|
process.exit(1);
|
|
16006
15965
|
}
|
|
16007
15966
|
});
|
|
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
15967
|
var filteredArgv = process.argv.filter((arg, index) => {
|
|
16036
15968
|
return !(arg === "--" && index > 1);
|
|
16037
15969
|
});
|