axusage 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/bin/axusage +2 -0
- package/dist/adapters/chatgpt.d.ts +8 -0
- package/dist/adapters/chatgpt.js +68 -0
- package/dist/adapters/claude.d.ts +9 -0
- package/dist/adapters/claude.js +108 -0
- package/dist/adapters/coalesce-claude-usage-response.d.ts +12 -0
- package/dist/adapters/coalesce-claude-usage-response.js +119 -0
- package/dist/adapters/gemini.d.ts +8 -0
- package/dist/adapters/gemini.js +43 -0
- package/dist/adapters/github-copilot.d.ts +6 -0
- package/dist/adapters/github-copilot.js +56 -0
- package/dist/adapters/parse-chatgpt-usage.d.ts +15 -0
- package/dist/adapters/parse-chatgpt-usage.js +28 -0
- package/dist/adapters/parse-claude-usage.d.ts +16 -0
- package/dist/adapters/parse-claude-usage.js +75 -0
- package/dist/adapters/parse-gemini-usage.d.ts +55 -0
- package/dist/adapters/parse-gemini-usage.js +151 -0
- package/dist/adapters/parse-github-copilot-usage.d.ts +23 -0
- package/dist/adapters/parse-github-copilot-usage.js +78 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +69 -0
- package/dist/commands/auth-clear-command.d.ts +5 -0
- package/dist/commands/auth-clear-command.js +25 -0
- package/dist/commands/auth-setup-command.d.ts +11 -0
- package/dist/commands/auth-setup-command.js +45 -0
- package/dist/commands/auth-status-command.d.ts +5 -0
- package/dist/commands/auth-status-command.js +25 -0
- package/dist/commands/fetch-service-usage-with-reauth.d.ts +7 -0
- package/dist/commands/fetch-service-usage-with-reauth.js +45 -0
- package/dist/commands/fetch-service-usage.d.ts +8 -0
- package/dist/commands/fetch-service-usage.js +19 -0
- package/dist/commands/run-auth-setup.d.ts +29 -0
- package/dist/commands/run-auth-setup.js +91 -0
- package/dist/commands/usage-command.d.ts +15 -0
- package/dist/commands/usage-command.js +146 -0
- package/dist/services/app-paths.d.ts +9 -0
- package/dist/services/app-paths.js +39 -0
- package/dist/services/auth-storage-path.d.ts +3 -0
- package/dist/services/auth-storage-path.js +7 -0
- package/dist/services/auth-timeouts.d.ts +4 -0
- package/dist/services/auth-timeouts.js +4 -0
- package/dist/services/browser-auth-manager.d.ts +49 -0
- package/dist/services/browser-auth-manager.js +113 -0
- package/dist/services/create-auth-context.d.ts +8 -0
- package/dist/services/create-auth-context.js +34 -0
- package/dist/services/do-setup-auth.d.ts +3 -0
- package/dist/services/do-setup-auth.js +25 -0
- package/dist/services/fetch-json-with-context.d.ts +5 -0
- package/dist/services/fetch-json-with-context.js +37 -0
- package/dist/services/gemini-api.d.ts +11 -0
- package/dist/services/gemini-api.js +109 -0
- package/dist/services/launch-chromium.d.ts +6 -0
- package/dist/services/launch-chromium.js +20 -0
- package/dist/services/persist-storage-state.d.ts +6 -0
- package/dist/services/persist-storage-state.js +16 -0
- package/dist/services/request-service.d.ts +3 -0
- package/dist/services/request-service.js +4 -0
- package/dist/services/service-adapter-registry.d.ts +18 -0
- package/dist/services/service-adapter-registry.js +26 -0
- package/dist/services/service-auth-configs.d.ts +15 -0
- package/dist/services/service-auth-configs.js +26 -0
- package/dist/services/setup-auth-flow.d.ts +3 -0
- package/dist/services/setup-auth-flow.js +40 -0
- package/dist/services/shared-browser-auth-manager.d.ts +4 -0
- package/dist/services/shared-browser-auth-manager.js +80 -0
- package/dist/services/supported-service.d.ts +6 -0
- package/dist/services/supported-service.js +16 -0
- package/dist/services/verify-session.d.ts +2 -0
- package/dist/services/verify-session.js +25 -0
- package/dist/services/wait-for-login.d.ts +5 -0
- package/dist/services/wait-for-login.js +44 -0
- package/dist/types/chatgpt.d.ts +32 -0
- package/dist/types/chatgpt.js +21 -0
- package/dist/types/domain.d.ts +57 -0
- package/dist/types/domain.js +16 -0
- package/dist/types/gemini.d.ts +31 -0
- package/dist/types/gemini.js +27 -0
- package/dist/types/github-copilot.d.ts +21 -0
- package/dist/types/github-copilot.js +27 -0
- package/dist/types/usage.d.ts +31 -0
- package/dist/types/usage.js +25 -0
- package/dist/utils/calculate-usage-rate.d.ts +9 -0
- package/dist/utils/calculate-usage-rate.js +31 -0
- package/dist/utils/classify-usage-rate.d.ts +6 -0
- package/dist/utils/classify-usage-rate.js +10 -0
- package/dist/utils/format-prometheus-metrics.d.ts +6 -0
- package/dist/utils/format-prometheus-metrics.js +20 -0
- package/dist/utils/format-service-usage.d.ts +18 -0
- package/dist/utils/format-service-usage.js +120 -0
- package/package.json +83 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { SUPPORTED_SERVICES, validateService, } from "../services/supported-service.js";
|
|
4
|
+
import { getStorageStatePathFor } from "../services/auth-storage-path.js";
|
|
5
|
+
import { getBrowserContextsDirectory } from "../services/app-paths.js";
|
|
6
|
+
export function authStatusCommand(options) {
|
|
7
|
+
const servicesToCheck = options.service
|
|
8
|
+
? [validateService(options.service)]
|
|
9
|
+
: SUPPORTED_SERVICES;
|
|
10
|
+
const dataDirectory = getBrowserContextsDirectory();
|
|
11
|
+
console.log(chalk.blue("\nAuthentication Status:\n"));
|
|
12
|
+
for (const service of servicesToCheck) {
|
|
13
|
+
const storagePath = getStorageStatePathFor(dataDirectory, service);
|
|
14
|
+
const hasAuth = existsSync(storagePath);
|
|
15
|
+
const status = hasAuth
|
|
16
|
+
? chalk.green("✓ Authenticated")
|
|
17
|
+
: chalk.gray("✗ Not authenticated");
|
|
18
|
+
console.log(`${chalk.bold(service)}: ${status}`);
|
|
19
|
+
console.log(` ${chalk.dim("Storage:")} ${chalk.dim(storagePath)}`);
|
|
20
|
+
}
|
|
21
|
+
const allAuthenticated = servicesToCheck.every((s) => existsSync(getStorageStatePathFor(dataDirectory, s)));
|
|
22
|
+
if (!allAuthenticated) {
|
|
23
|
+
console.error(chalk.gray(`\nTo set up authentication, run: ${chalk.cyan("axusage auth setup <service>")}`));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServiceResult } from "../types/domain.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetch usage for a service, with automatic re-authentication on auth errors.
|
|
4
|
+
* Prompts the user to re-authenticate if the initial fetch fails with an auth error,
|
|
5
|
+
* then retries the fetch. Returns the original result if re-authentication fails.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchServiceUsageWithAutoReauth(serviceName: string, interactive: boolean): Promise<ServiceResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { fetchServiceUsage } from "./fetch-service-usage.js";
|
|
3
|
+
import { isAuthFailure, runAuthSetup } from "./run-auth-setup.js";
|
|
4
|
+
import { validateService } from "../services/supported-service.js";
|
|
5
|
+
/**
|
|
6
|
+
* Fetch usage for a service, with automatic re-authentication on auth errors.
|
|
7
|
+
* Prompts the user to re-authenticate if the initial fetch fails with an auth error,
|
|
8
|
+
* then retries the fetch. Returns the original result if re-authentication fails.
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchServiceUsageWithAutoReauth(serviceName, interactive) {
|
|
11
|
+
const result = await fetchServiceUsage(serviceName);
|
|
12
|
+
if (!interactive) {
|
|
13
|
+
return { service: serviceName, result };
|
|
14
|
+
}
|
|
15
|
+
// If auth error, try to re-authenticate and retry
|
|
16
|
+
if (isAuthFailure(result)) {
|
|
17
|
+
console.error(chalk.yellow(`⚠ Authentication failed for ${serviceName}. Opening browser to re-authenticate...`));
|
|
18
|
+
try {
|
|
19
|
+
const service = validateService(serviceName);
|
|
20
|
+
const authSuccess = await runAuthSetup(service);
|
|
21
|
+
if (authSuccess) {
|
|
22
|
+
if (process.stderr.isTTY) {
|
|
23
|
+
console.error(chalk.blue(`Retrying ${serviceName} usage fetch...\n`));
|
|
24
|
+
}
|
|
25
|
+
const retryResult = await fetchServiceUsage(serviceName);
|
|
26
|
+
return { service: serviceName, result: retryResult };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.error(chalk.red(`Re-authentication failed for ${serviceName}.`));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
34
|
+
// Distinguish validation errors from auth setup errors
|
|
35
|
+
const isValidationError = errorMessage.includes("Unsupported service") ||
|
|
36
|
+
errorMessage.includes("Service is required");
|
|
37
|
+
const prefix = isValidationError
|
|
38
|
+
? "Invalid service"
|
|
39
|
+
: "Failed to re-authenticate";
|
|
40
|
+
console.error(chalk.red(`${prefix}: ${errorMessage}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Return original result if re-authentication failed or was not attempted
|
|
44
|
+
return { service: serviceName, result };
|
|
45
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApiError, Result, ServiceUsageData } from "../types/domain.js";
|
|
2
|
+
export type UsageCommandOptions = {
|
|
3
|
+
readonly service?: string;
|
|
4
|
+
readonly format?: "text" | "tsv" | "json" | "prometheus";
|
|
5
|
+
readonly interactive?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function selectServicesToQuery(service?: string): string[];
|
|
8
|
+
export declare function fetchServiceUsage(serviceName: string): Promise<Result<ServiceUsageData, ApiError>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApiError as ApiErrorClass } from "../types/domain.js";
|
|
2
|
+
import { getServiceAdapter } from "../services/service-adapter-registry.js";
|
|
3
|
+
const ALL_SERVICES = ["claude", "chatgpt", "github-copilot", "gemini"];
|
|
4
|
+
export function selectServicesToQuery(service) {
|
|
5
|
+
const normalized = service?.toLowerCase();
|
|
6
|
+
if (!service || normalized === "all")
|
|
7
|
+
return [...ALL_SERVICES];
|
|
8
|
+
return [service];
|
|
9
|
+
}
|
|
10
|
+
export async function fetchServiceUsage(serviceName) {
|
|
11
|
+
const adapter = getServiceAdapter(serviceName);
|
|
12
|
+
if (!adapter) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: new ApiErrorClass(`Unknown service "${serviceName}"`),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return await adapter.fetchUsage();
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SupportedService } from "../services/supported-service.js";
|
|
2
|
+
import type { ApiError, Result, ServiceUsageData } from "../types/domain.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check if an error message indicates an authentication issue.
|
|
5
|
+
* Matches common authentication error patterns like "unauthorized", "401",
|
|
6
|
+
* "authentication failed", etc. with word boundaries to avoid false positives.
|
|
7
|
+
*
|
|
8
|
+
* @param message - The error message to check
|
|
9
|
+
* @returns true if the message indicates an authentication error
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* isAuthError("401 Unauthorized") // true
|
|
13
|
+
* isAuthError("Network timeout") // false
|
|
14
|
+
*/
|
|
15
|
+
export declare function isAuthError(message: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a fetch result indicates an authentication failure.
|
|
18
|
+
* Combines the result error check with auth error pattern matching.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAuthFailure(result: Result<ServiceUsageData, ApiError>): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Run auth setup for a service programmatically.
|
|
23
|
+
* Returns true if auth setup completed successfully.
|
|
24
|
+
* Times out after 5 minutes to prevent indefinite hangs.
|
|
25
|
+
*
|
|
26
|
+
* Note: Gemini uses CLI-based auth and cannot use browser-based re-auth.
|
|
27
|
+
* This function prints instructions and returns false for Gemini.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runAuthSetup(service: SupportedService): Promise<boolean>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { BrowserAuthManager } from "../services/browser-auth-manager.js";
|
|
3
|
+
/** Timeout for authentication setup (5 minutes) */
|
|
4
|
+
const AUTH_SETUP_TIMEOUT_MS = 300_000;
|
|
5
|
+
/**
|
|
6
|
+
* Check if an error message indicates an authentication issue.
|
|
7
|
+
* Matches common authentication error patterns like "unauthorized", "401",
|
|
8
|
+
* "authentication failed", etc. with word boundaries to avoid false positives.
|
|
9
|
+
*
|
|
10
|
+
* @param message - The error message to check
|
|
11
|
+
* @returns true if the message indicates an authentication error
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* isAuthError("401 Unauthorized") // true
|
|
15
|
+
* isAuthError("Network timeout") // false
|
|
16
|
+
*/
|
|
17
|
+
export function isAuthError(message) {
|
|
18
|
+
const authPatterns = [
|
|
19
|
+
/\bauthentication\s+failed\b/iu,
|
|
20
|
+
/\bno\s+saved\s+authentication\b/iu,
|
|
21
|
+
/\b401\b/u,
|
|
22
|
+
/\bunauthorized\b/iu,
|
|
23
|
+
/\bsession\s+expired\b/iu,
|
|
24
|
+
/\blogin\s+required\b/iu,
|
|
25
|
+
/\bcredentials?\s+(expired|invalid)\b/iu,
|
|
26
|
+
];
|
|
27
|
+
return authPatterns.some((pattern) => pattern.test(message));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a fetch result indicates an authentication failure.
|
|
31
|
+
* Combines the result error check with auth error pattern matching.
|
|
32
|
+
*/
|
|
33
|
+
export function isAuthFailure(result) {
|
|
34
|
+
return (!result.ok &&
|
|
35
|
+
Boolean(result.error.message) &&
|
|
36
|
+
isAuthError(result.error.message));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run auth setup for a service programmatically.
|
|
40
|
+
* Returns true if auth setup completed successfully.
|
|
41
|
+
* Times out after 5 minutes to prevent indefinite hangs.
|
|
42
|
+
*
|
|
43
|
+
* Note: Gemini uses CLI-based auth and cannot use browser-based re-auth.
|
|
44
|
+
* This function prints instructions and returns false for Gemini.
|
|
45
|
+
*/
|
|
46
|
+
export async function runAuthSetup(service) {
|
|
47
|
+
// CLI-based auth cannot use browser auth flow
|
|
48
|
+
if (service === "gemini") {
|
|
49
|
+
console.error(chalk.yellow("\nGemini uses CLI-based authentication managed by the Gemini CLI."));
|
|
50
|
+
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
51
|
+
console.error(chalk.cyan(" gemini"));
|
|
52
|
+
console.error(chalk.gray("\nThe Gemini CLI will guide you through the OAuth login process.\n"));
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (service === "claude") {
|
|
56
|
+
console.error(chalk.yellow("\nClaude uses CLI-based authentication managed by Claude Code."));
|
|
57
|
+
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
58
|
+
console.error(chalk.cyan(" claude"));
|
|
59
|
+
console.error(chalk.gray("\nClaude Code will guide you through authentication.\n"));
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (service === "chatgpt") {
|
|
63
|
+
console.error(chalk.yellow("\nChatGPT uses CLI-based authentication managed by Codex."));
|
|
64
|
+
console.error(chalk.gray("\nTo re-authenticate, run:"));
|
|
65
|
+
console.error(chalk.cyan(" codex"));
|
|
66
|
+
console.error(chalk.gray("\nCodex will guide you through authentication.\n"));
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const manager = new BrowserAuthManager({ headless: false });
|
|
70
|
+
let timeoutId;
|
|
71
|
+
try {
|
|
72
|
+
console.error(chalk.blue(`\nOpening browser for ${service} authentication...\n`));
|
|
73
|
+
const setupPromise = manager.setupAuth(service);
|
|
74
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
75
|
+
timeoutId = setTimeout(() => {
|
|
76
|
+
reject(new Error("Authentication setup timed out after 5 minutes"));
|
|
77
|
+
}, AUTH_SETUP_TIMEOUT_MS);
|
|
78
|
+
});
|
|
79
|
+
await Promise.race([setupPromise, timeoutPromise]);
|
|
80
|
+
console.error(chalk.green(`\n✓ Authentication for ${service} is complete!\n`));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(chalk.red(`\n✗ Failed to set up authentication: ${error instanceof Error ? error.message : String(error)}`));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
await manager.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServiceResult } from "../types/domain.js";
|
|
2
|
+
import type { UsageCommandOptions } from "./fetch-service-usage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fetches usage for services using hybrid strategy:
|
|
5
|
+
* 1. Try all services in parallel first (fast path for valid credentials)
|
|
6
|
+
* 2. If any service fails with auth error, retry those sequentially with re-auth
|
|
7
|
+
*
|
|
8
|
+
* This maintains ~2s response time when credentials are valid while gracefully
|
|
9
|
+
* handling authentication failures that require interactive prompts.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchServicesWithHybridStrategy(servicesToQuery: string[], interactive: boolean): Promise<ServiceResult[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Executes the usage command
|
|
14
|
+
*/
|
|
15
|
+
export declare function usageCommand(options: UsageCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { formatServiceUsageData, formatServiceUsageDataAsJson, formatServiceUsageAsTsv, toJsonObject, } from "../utils/format-service-usage.js";
|
|
3
|
+
import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
|
|
4
|
+
import { fetchServiceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
|
|
5
|
+
import { fetchServiceUsageWithAutoReauth } from "./fetch-service-usage-with-reauth.js";
|
|
6
|
+
import { isAuthFailure } from "./run-auth-setup.js";
|
|
7
|
+
/**
|
|
8
|
+
* Fetches usage for services using hybrid strategy:
|
|
9
|
+
* 1. Try all services in parallel first (fast path for valid credentials)
|
|
10
|
+
* 2. If any service fails with auth error, retry those sequentially with re-auth
|
|
11
|
+
*
|
|
12
|
+
* This maintains ~2s response time when credentials are valid while gracefully
|
|
13
|
+
* handling authentication failures that require interactive prompts.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchServicesWithHybridStrategy(servicesToQuery, interactive) {
|
|
16
|
+
// First attempt: fetch all services in parallel
|
|
17
|
+
const parallelResults = await Promise.all(servicesToQuery.map(async (serviceName) => {
|
|
18
|
+
const result = await fetchServiceUsage(serviceName);
|
|
19
|
+
return { service: serviceName, result };
|
|
20
|
+
}));
|
|
21
|
+
// Check for auth errors
|
|
22
|
+
const authFailures = parallelResults.filter(({ result }) => isAuthFailure(result));
|
|
23
|
+
// If no auth failures, return parallel results
|
|
24
|
+
if (authFailures.length === 0 || !interactive) {
|
|
25
|
+
return parallelResults;
|
|
26
|
+
}
|
|
27
|
+
const shouldShowProgress = process.stderr.isTTY;
|
|
28
|
+
// Retry auth failures sequentially with re-authentication
|
|
29
|
+
const retryResults = [];
|
|
30
|
+
for (const [index, { service }] of authFailures.entries()) {
|
|
31
|
+
if (shouldShowProgress) {
|
|
32
|
+
console.error(chalk.dim(`[${String(index + 1)}/${String(authFailures.length)}] Re-authenticating ${service}...`));
|
|
33
|
+
}
|
|
34
|
+
const result = await fetchServiceUsageWithAutoReauth(service, interactive);
|
|
35
|
+
retryResults.push(result);
|
|
36
|
+
}
|
|
37
|
+
if (shouldShowProgress) {
|
|
38
|
+
console.error(chalk.green(`✓ Completed ${String(retryResults.length)} re-authentication${retryResults.length === 1 ? "" : "s"}\n`));
|
|
39
|
+
}
|
|
40
|
+
// Merge results: keep successful parallel results, replace auth failures with retries
|
|
41
|
+
// Build a map for O(1) lookups instead of O(n²) find() calls
|
|
42
|
+
const retryMap = new Map(retryResults.map((r) => [r.service, r]));
|
|
43
|
+
return parallelResults.map((parallelResult) => retryMap.get(parallelResult.service) ?? parallelResult);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Executes the usage command
|
|
47
|
+
*/
|
|
48
|
+
export async function usageCommand(options) {
|
|
49
|
+
const servicesToQuery = selectServicesToQuery(options.service);
|
|
50
|
+
const interactive = options.interactive ?? false;
|
|
51
|
+
// Fetch usage data using hybrid parallel/sequential strategy
|
|
52
|
+
const results = await fetchServicesWithHybridStrategy(servicesToQuery, interactive);
|
|
53
|
+
// Collect successful results and errors
|
|
54
|
+
const successes = [];
|
|
55
|
+
const errors = [];
|
|
56
|
+
const authFailureServices = new Set();
|
|
57
|
+
for (const { service, result } of results) {
|
|
58
|
+
if (result.ok) {
|
|
59
|
+
successes.push(result.value);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
errors.push({ service, error: result.error });
|
|
63
|
+
if (isAuthFailure(result))
|
|
64
|
+
authFailureServices.add(service);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Display errors if any (but don't exit if we have at least one success)
|
|
68
|
+
if (errors.length > 0) {
|
|
69
|
+
for (const { service, error } of errors) {
|
|
70
|
+
console.error(chalk.yellow(`⚠ Warning: Failed to fetch ${service} usage:`));
|
|
71
|
+
console.error(chalk.gray(` ${error.message}`));
|
|
72
|
+
if (error.status) {
|
|
73
|
+
console.error(chalk.gray(` Status: ${String(error.status)}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (successes.length > 0) {
|
|
77
|
+
console.error(); // Empty line for spacing
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!interactive && authFailureServices.size > 0) {
|
|
81
|
+
const list = [...authFailureServices].join(", ");
|
|
82
|
+
console.error(chalk.gray(`Authentication required for: ${list}. Run 'axusage auth setup <service>' or re-run with '--interactive' to re-authenticate during fetch.`));
|
|
83
|
+
if (successes.length > 0) {
|
|
84
|
+
console.error();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Exit if no services succeeded
|
|
88
|
+
if (successes.length === 0) {
|
|
89
|
+
console.error(chalk.red("\nNo services could be queried successfully."));
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Display results
|
|
94
|
+
const hasPartialFailures = errors.length > 0;
|
|
95
|
+
const format = options.format ?? "text";
|
|
96
|
+
switch (format) {
|
|
97
|
+
case "tsv": {
|
|
98
|
+
console.log(formatServiceUsageAsTsv(successes));
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "json": {
|
|
102
|
+
const [singleSuccess] = successes;
|
|
103
|
+
if (successes.length === 1 && !hasPartialFailures && singleSuccess) {
|
|
104
|
+
console.log(formatServiceUsageDataAsJson(singleSuccess));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const payload = successes.length === 1 && singleSuccess
|
|
108
|
+
? toJsonObject(singleSuccess)
|
|
109
|
+
: successes.map((data) => toJsonObject(data));
|
|
110
|
+
const output = hasPartialFailures
|
|
111
|
+
? {
|
|
112
|
+
results: payload,
|
|
113
|
+
errors: errors.map(({ service, error }) => ({
|
|
114
|
+
service,
|
|
115
|
+
message: error.message,
|
|
116
|
+
status: error.status,
|
|
117
|
+
})),
|
|
118
|
+
}
|
|
119
|
+
: payload;
|
|
120
|
+
// eslint-disable-next-line unicorn/no-null -- JSON.stringify requires null for no replacer
|
|
121
|
+
console.log(JSON.stringify(output, null, 2));
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "prometheus": {
|
|
126
|
+
// Emit Prometheus text metrics using prom-client
|
|
127
|
+
const output = await formatPrometheusMetrics(successes);
|
|
128
|
+
process.stdout.write(output);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "text": {
|
|
132
|
+
// For human-readable output, display each service's results
|
|
133
|
+
for (const data of successes) {
|
|
134
|
+
const index = successes.indexOf(data);
|
|
135
|
+
if (index > 0) {
|
|
136
|
+
console.log(); // Add spacing between services
|
|
137
|
+
}
|
|
138
|
+
console.log(formatServiceUsageData(data));
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (hasPartialFailures) {
|
|
144
|
+
process.exitCode = 2;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory for storing browser authentication contexts
|
|
3
|
+
*/
|
|
4
|
+
export declare function getBrowserContextsDirectory(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Ensure a directory exists with restricted permissions (owner-only access).
|
|
7
|
+
* Creates the directory recursively if needed and sets mode 0o700.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensureSecureDirectory(directoryPath: string): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import envPaths from "env-paths";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
4
|
+
/**
|
|
5
|
+
* env-paths resolves directories during module initialization, so changes to
|
|
6
|
+
* environment variables (like XDG_DATA_HOME) after the first import will not
|
|
7
|
+
* be picked up without restarting the process.
|
|
8
|
+
*/
|
|
9
|
+
const paths = envPaths("axusage", { suffix: "" });
|
|
10
|
+
/**
|
|
11
|
+
* Directory for storing browser authentication contexts
|
|
12
|
+
*/
|
|
13
|
+
export function getBrowserContextsDirectory() {
|
|
14
|
+
return path.join(paths.data, "browser-contexts");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ensure a directory exists with restricted permissions (owner-only access).
|
|
18
|
+
* Creates the directory recursively if needed and sets mode 0o700.
|
|
19
|
+
*/
|
|
20
|
+
export async function ensureSecureDirectory(directoryPath) {
|
|
21
|
+
try {
|
|
22
|
+
await mkdir(directoryPath, { recursive: true, mode: 0o700 });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// Only ignore EEXIST; re-throw everything else
|
|
26
|
+
// mkdir may ignore mode due to umask; we'll enforce via chmod below
|
|
27
|
+
const isEexist = error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
28
|
+
if (!isEexist) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await chmod(directoryPath, 0o700);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Best effort: some filesystems (network mounts, containers) may not
|
|
37
|
+
// support chmod or have different permission models
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function getStorageStatePathFor(dataDirectory, service) {
|
|
3
|
+
return path.join(dataDirectory, `${service}-auth.json`);
|
|
4
|
+
}
|
|
5
|
+
export function getAuthMetaPathFor(dataDirectory, service) {
|
|
6
|
+
return path.join(dataDirectory, `${service}-auth.meta.json`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright";
|
|
2
|
+
import type { SupportedService } from "./supported-service.js";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for browser authentication manager
|
|
5
|
+
*/
|
|
6
|
+
type BrowserAuthConfig = {
|
|
7
|
+
readonly dataDir?: string;
|
|
8
|
+
readonly headless?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Manages browser contexts for persistent authentication across services
|
|
12
|
+
*/
|
|
13
|
+
export declare class BrowserAuthManager {
|
|
14
|
+
private readonly dataDir;
|
|
15
|
+
private readonly headless;
|
|
16
|
+
private browser;
|
|
17
|
+
private browserPromise;
|
|
18
|
+
constructor(config?: BrowserAuthConfig);
|
|
19
|
+
/**
|
|
20
|
+
* Get the storage state file path for a service
|
|
21
|
+
*/
|
|
22
|
+
private getStorageStatePath;
|
|
23
|
+
/**
|
|
24
|
+
* Check if a service has saved authentication
|
|
25
|
+
*/
|
|
26
|
+
hasAuth(service: SupportedService): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Ensure a Chromium browser instance is available
|
|
29
|
+
*/
|
|
30
|
+
private ensureBrowser;
|
|
31
|
+
private launchAndStoreBrowser;
|
|
32
|
+
/**
|
|
33
|
+
* Set up authentication for a service by launching a browser for the user to log in
|
|
34
|
+
*/
|
|
35
|
+
setupAuth(service: SupportedService): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Get a browser context with saved authentication for a service
|
|
38
|
+
*/
|
|
39
|
+
getAuthContext(service: SupportedService): Promise<BrowserContext>;
|
|
40
|
+
/**
|
|
41
|
+
* Make an authenticated request to a URL using the browser context
|
|
42
|
+
*/
|
|
43
|
+
makeAuthenticatedRequest(service: SupportedService, url: string): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Close the browser and clean up resources
|
|
46
|
+
*/
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getServiceAuthConfig } from "./service-auth-configs.js";
|
|
3
|
+
import { launchChromium } from "./launch-chromium.js";
|
|
4
|
+
import { requestService } from "./request-service.js";
|
|
5
|
+
import { doSetupAuth } from "./do-setup-auth.js";
|
|
6
|
+
import { getStorageStatePathFor } from "./auth-storage-path.js";
|
|
7
|
+
import { createAuthContext, loadStoredUserAgent, } from "./create-auth-context.js";
|
|
8
|
+
import { getBrowserContextsDirectory, ensureSecureDirectory, } from "./app-paths.js";
|
|
9
|
+
import { persistStorageState } from "./persist-storage-state.js";
|
|
10
|
+
/**
|
|
11
|
+
* Manages browser contexts for persistent authentication across services
|
|
12
|
+
*/
|
|
13
|
+
export class BrowserAuthManager {
|
|
14
|
+
dataDir;
|
|
15
|
+
headless;
|
|
16
|
+
browser = undefined;
|
|
17
|
+
browserPromise = undefined;
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
this.dataDir = config.dataDir || getBrowserContextsDirectory();
|
|
20
|
+
// Default to headless for non-interactive usage flows; auth setup passes headless: false
|
|
21
|
+
this.headless = config.headless ?? true;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the storage state file path for a service
|
|
25
|
+
*/
|
|
26
|
+
getStorageStatePath(service) {
|
|
27
|
+
return getStorageStatePathFor(this.dataDir, service);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a service has saved authentication
|
|
31
|
+
*/
|
|
32
|
+
hasAuth(service) {
|
|
33
|
+
return existsSync(this.getStorageStatePath(service));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Ensure a Chromium browser instance is available
|
|
37
|
+
*/
|
|
38
|
+
async ensureBrowser() {
|
|
39
|
+
if (!this.browserPromise) {
|
|
40
|
+
this.browserPromise = this.launchAndStoreBrowser();
|
|
41
|
+
}
|
|
42
|
+
return this.browserPromise;
|
|
43
|
+
}
|
|
44
|
+
async launchAndStoreBrowser() {
|
|
45
|
+
try {
|
|
46
|
+
const browser = await launchChromium(this.headless);
|
|
47
|
+
this.browser = browser;
|
|
48
|
+
return browser;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Allow retries on subsequent calls if the launch fails
|
|
52
|
+
this.browserPromise = undefined;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Set up authentication for a service by launching a browser for the user to log in
|
|
58
|
+
*/
|
|
59
|
+
async setupAuth(service) {
|
|
60
|
+
const config = getServiceAuthConfig(service);
|
|
61
|
+
await ensureSecureDirectory(this.dataDir);
|
|
62
|
+
const browser = await this.ensureBrowser();
|
|
63
|
+
const storagePath = this.getStorageStatePath(service);
|
|
64
|
+
// Load existing storage state if available - this gives the browser a chance
|
|
65
|
+
// to refresh expired cookies/tokens during the login flow
|
|
66
|
+
const storageState = existsSync(storagePath) ? storagePath : undefined;
|
|
67
|
+
const userAgent = await loadStoredUserAgent(this.dataDir, service);
|
|
68
|
+
let context;
|
|
69
|
+
try {
|
|
70
|
+
context = await browser.newContext({ storageState, userAgent });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Corrupted storage state - fall back to fresh context
|
|
74
|
+
context = await browser.newContext({ userAgent });
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await doSetupAuth(service, context, this.getStorageStatePath(service), config.instructions);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await context.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get a browser context with saved authentication for a service
|
|
85
|
+
*/
|
|
86
|
+
async getAuthContext(service) {
|
|
87
|
+
const browser = await this.ensureBrowser();
|
|
88
|
+
return createAuthContext(browser, this.dataDir, service);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Make an authenticated request to a URL using the browser context
|
|
92
|
+
*/
|
|
93
|
+
async makeAuthenticatedRequest(service, url) {
|
|
94
|
+
const context = await this.getAuthContext(service);
|
|
95
|
+
try {
|
|
96
|
+
return await requestService(service, url, () => Promise.resolve(context));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await persistStorageState(context, this.getStorageStatePath(service));
|
|
100
|
+
await context.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Close the browser and clean up resources
|
|
105
|
+
*/
|
|
106
|
+
async close() {
|
|
107
|
+
if (this.browser) {
|
|
108
|
+
await this.browser.close();
|
|
109
|
+
this.browser = undefined;
|
|
110
|
+
this.browserPromise = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Browser, BrowserContext } from "playwright";
|
|
2
|
+
import type { SupportedService } from "./supported-service.js";
|
|
3
|
+
/**
|
|
4
|
+
* Load the stored userAgent from the auth meta file for a service.
|
|
5
|
+
* Returns undefined if meta file doesn't exist or is invalid.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadStoredUserAgent(dataDirectory: string, service: SupportedService): Promise<string | undefined>;
|
|
8
|
+
export declare function createAuthContext(browser: Browser, dataDirectory: string, service: SupportedService): Promise<BrowserContext>;
|