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