axusage 2.2.0 → 3.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.
Files changed (33) hide show
  1. package/README.md +61 -15
  2. package/dist/adapters/github-copilot.js +2 -1
  3. package/dist/cli.js +57 -41
  4. package/dist/commands/auth-clear-command.d.ts +2 -0
  5. package/dist/commands/auth-clear-command.js +62 -3
  6. package/dist/commands/auth-setup-command.d.ts +1 -0
  7. package/dist/commands/auth-setup-command.js +36 -5
  8. package/dist/commands/auth-status-command.js +36 -4
  9. package/dist/commands/fetch-service-usage-with-reauth.js +2 -2
  10. package/dist/commands/fetch-service-usage.js +4 -2
  11. package/dist/commands/run-auth-setup.js +26 -5
  12. package/dist/commands/usage-command.js +5 -3
  13. package/dist/config/credential-sources.js +22 -0
  14. package/dist/services/create-auth-context.js +2 -1
  15. package/dist/services/do-setup-auth.js +2 -8
  16. package/dist/services/persist-storage-state.d.ts +1 -1
  17. package/dist/services/persist-storage-state.js +8 -8
  18. package/dist/services/setup-auth-flow.js +38 -11
  19. package/dist/services/supported-service.js +4 -2
  20. package/dist/services/wait-for-login.d.ts +3 -2
  21. package/dist/services/wait-for-login.js +89 -18
  22. package/dist/utils/check-cli-dependency.d.ts +25 -0
  23. package/dist/utils/check-cli-dependency.js +81 -0
  24. package/dist/utils/color.d.ts +5 -0
  25. package/dist/utils/color.js +27 -0
  26. package/dist/utils/format-service-usage.js +1 -1
  27. package/dist/utils/resolve-prompt-capability.d.ts +1 -0
  28. package/dist/utils/resolve-prompt-capability.js +3 -0
  29. package/dist/utils/validate-root-options.d.ts +11 -0
  30. package/dist/utils/validate-root-options.js +18 -0
  31. package/dist/utils/write-atomic-json.d.ts +1 -0
  32. package/dist/utils/write-atomic-json.js +56 -0
  33. package/package.json +2 -1
@@ -1,9 +1,9 @@
1
- import chalk from "chalk";
2
1
  import { formatServiceUsageData, formatServiceUsageDataAsJson, formatServiceUsageAsTsv, toJsonObject, } from "../utils/format-service-usage.js";
3
2
  import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
4
3
  import { fetchServiceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
5
4
  import { fetchServiceUsageWithAutoReauth } from "./fetch-service-usage-with-reauth.js";
6
5
  import { isAuthFailure } from "./run-auth-setup.js";
6
+ import { chalk } from "../utils/color.js";
7
7
  /**
8
8
  * Fetches usage for services using hybrid strategy:
9
9
  * 1. Try all services in parallel first (fast path for valid credentials)
@@ -79,7 +79,9 @@ export async function usageCommand(options) {
79
79
  }
80
80
  if (!interactive && authFailureServices.size > 0) {
81
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.`));
82
+ console.error(chalk.gray(`Authentication required for: ${list}. ` +
83
+ "For GitHub Copilot, run 'axusage --auth-setup github-copilot --interactive'. " +
84
+ "For CLI-auth services, run the provider CLI (claude/codex/gemini), or re-run with '--interactive' to re-authenticate during fetch."));
83
85
  if (successes.length > 0) {
84
86
  console.error();
85
87
  }
@@ -141,6 +143,6 @@ export async function usageCommand(options) {
141
143
  }
142
144
  }
143
145
  if (hasPartialFailures) {
144
- process.exitCode = 2;
146
+ process.exitCode = 1;
145
147
  }
146
148
  }
@@ -26,6 +26,7 @@ function getConfig() {
26
26
  if (!configInstance) {
27
27
  configInstance = new Conf({
28
28
  projectName: "axusage",
29
+ projectSuffix: "",
29
30
  schema: {
30
31
  sources: {
31
32
  type: "object",
@@ -33,9 +34,30 @@ function getConfig() {
33
34
  },
34
35
  },
35
36
  });
37
+ // Migration runs once per process when the config is first initialized.
38
+ migrateLegacySources(configInstance);
36
39
  }
37
40
  return configInstance;
38
41
  }
42
+ function migrateLegacySources(config) {
43
+ // Respect explicit new config values; never overwrite them with legacy data.
44
+ if (config.get("sources") !== undefined)
45
+ return;
46
+ // Conf defaults to the legacy "-nodejs" suffix, which matches older configs.
47
+ const legacyConfig = new Conf({
48
+ projectName: "axusage",
49
+ });
50
+ const legacySources = legacyConfig.get("sources");
51
+ if (!legacySources)
52
+ return;
53
+ const parsed = SourcesConfig.safeParse(legacySources);
54
+ if (!parsed.success) {
55
+ console.error("Warning: Legacy axusage config contains invalid sources; skipping migration. Check your legacy config and migrate manually if needed.");
56
+ return;
57
+ }
58
+ config.set("sources", parsed.data);
59
+ console.error("Migrated credential source configuration from legacy axusage-nodejs config path.");
60
+ }
39
61
  /**
40
62
  * Get the full credential source configuration.
41
63
  *
@@ -27,7 +27,8 @@ export async function loadStoredUserAgent(dataDirectory, service) {
27
27
  export async function createAuthContext(browser, dataDirectory, service) {
28
28
  const storageStatePath = getStorageStatePathFor(dataDirectory, service);
29
29
  if (!existsSync(storageStatePath)) {
30
- throw new Error(`No saved authentication for ${service}. Run 'axusage auth setup ${service}' first.`);
30
+ throw new Error(`No saved authentication for ${service}. ` +
31
+ `Run 'axusage --auth-setup ${service} --interactive' first.`);
31
32
  }
32
33
  const userAgent = await loadStoredUserAgent(dataDirectory, service);
33
34
  return browser.newContext({ storageState: storageStatePath, userAgent });
@@ -1,7 +1,7 @@
1
1
  import { setupAuthInContext } from "./setup-auth-flow.js";
2
- import { writeFile, chmod } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { getAuthMetaPathFor } from "./auth-storage-path.js";
4
+ import { writeAtomicJson } from "../utils/write-atomic-json.js";
5
5
  export async function doSetupAuth(service, context, storagePath, instructions) {
6
6
  console.error(`\n${instructions}`);
7
7
  console.error("Waiting for login to complete (or press Enter to continue)\n");
@@ -9,13 +9,7 @@ export async function doSetupAuth(service, context, storagePath, instructions) {
9
9
  try {
10
10
  if (userAgent) {
11
11
  const metaPath = getAuthMetaPathFor(path.dirname(storagePath), service);
12
- await writeFile(metaPath, JSON.stringify({ userAgent }), "utf8");
13
- try {
14
- await chmod(metaPath, 0o600);
15
- }
16
- catch {
17
- // best effort
18
- }
12
+ await writeAtomicJson(metaPath, { userAgent }, 0o600);
19
13
  }
20
14
  }
21
15
  catch {
@@ -1,6 +1,6 @@
1
1
  import type { BrowserContext } from "playwright";
2
2
  /**
3
3
  * Persist context storage state to disk with secure permissions (0o600).
4
- * Errors are silently ignored to avoid blocking the main operation.
4
+ * Errors are logged as warnings to avoid blocking the main operation.
5
5
  */
6
6
  export declare function persistStorageState(context: BrowserContext, storagePath: string): Promise<void>;
@@ -1,16 +1,16 @@
1
- import { chmod } from "node:fs/promises";
1
+ import { writeAtomicJson } from "../utils/write-atomic-json.js";
2
+ import { chalk } from "../utils/color.js";
2
3
  /**
3
4
  * Persist context storage state to disk with secure permissions (0o600).
4
- * Errors are silently ignored to avoid blocking the main operation.
5
+ * Errors are logged as warnings to avoid blocking the main operation.
5
6
  */
6
7
  export async function persistStorageState(context, storagePath) {
7
8
  try {
8
- await context.storageState({ path: storagePath });
9
- await chmod(storagePath, 0o600).catch(() => {
10
- // best effort: permissions may already be correct or OS may ignore
11
- });
9
+ const state = await context.storageState();
10
+ await writeAtomicJson(storagePath, state, 0o600);
12
11
  }
13
- catch {
14
- // ignore persistence errors; do not block request completion
12
+ catch (error) {
13
+ const details = error instanceof Error ? error.message : String(error);
14
+ console.error(chalk.yellow(`Warning: Failed to persist auth state to ${storagePath} (${details}).`));
15
15
  }
16
16
  }
@@ -1,7 +1,29 @@
1
1
  import { getServiceAuthConfig } from "./service-auth-configs.js";
2
2
  import { waitForLogin } from "./wait-for-login.js";
3
3
  import { verifySessionByFetching } from "./verify-session.js";
4
- import { chmod } from "node:fs/promises";
4
+ import { writeAtomicJson } from "../utils/write-atomic-json.js";
5
+ function describeLoginOutcome(outcome) {
6
+ switch (outcome) {
7
+ case "manual": {
8
+ return "after manual continuation";
9
+ }
10
+ case "timeout": {
11
+ return "after login timeout";
12
+ }
13
+ case "closed": {
14
+ return "after the browser window closed";
15
+ }
16
+ case "aborted": {
17
+ return "after prompt cancellation";
18
+ }
19
+ case "selector": {
20
+ return "after detecting a login signal";
21
+ }
22
+ case "skipped": {
23
+ return "without waiting for a login signal";
24
+ }
25
+ }
26
+ }
5
27
  export async function setupAuthInContext(service, context, storagePath) {
6
28
  const page = await context.newPage();
7
29
  try {
@@ -9,24 +31,27 @@ export async function setupAuthInContext(service, context, storagePath) {
9
31
  await page.goto(config.url);
10
32
  const selectors = config.waitForSelectors ??
11
33
  (config.waitForSelector ? [config.waitForSelector] : []);
12
- await waitForLoginForService(page, selectors);
34
+ const loginOutcome = await waitForLoginForService(page, selectors);
35
+ const outcomeLabel = describeLoginOutcome(loginOutcome);
36
+ if (loginOutcome === "aborted") {
37
+ throw new Error("Authentication was canceled. Authentication was not saved.");
38
+ }
13
39
  if (config.verifyUrl) {
14
40
  const ok = config.verifyFunction
15
41
  ? await config.verifyFunction(context, config.verifyUrl)
16
42
  : await verifySessionByFetching(context, config.verifyUrl);
17
43
  if (!ok) {
18
- console.warn(`\n⚠ Unable to verify session via ${config.verifyUrl}. Saving state anyway...`);
44
+ throw new Error(`Unable to verify session via ${config.verifyUrl} ${outcomeLabel}. Authentication was not saved. Ensure login completed successfully and retry.`);
19
45
  }
20
46
  }
47
+ else if (selectors.length > 0 && loginOutcome !== "selector") {
48
+ // Without a verification URL, we only persist when a login selector confirms success.
49
+ throw new Error(`Login was not confirmed ${outcomeLabel}. Authentication was not saved.`);
50
+ }
21
51
  // Capture user agent for future headless contexts
22
52
  const userAgent = await page.evaluate(() => navigator.userAgent);
23
- await context.storageState({ path: storagePath });
24
- try {
25
- await chmod(storagePath, 0o600);
26
- }
27
- catch {
28
- // best effort to restrict sensitive storage state
29
- }
53
+ const state = await context.storageState();
54
+ await writeAtomicJson(storagePath, state, 0o600);
30
55
  return userAgent;
31
56
  }
32
57
  finally {
@@ -35,6 +60,8 @@ export async function setupAuthInContext(service, context, storagePath) {
35
60
  }
36
61
  async function waitForLoginForService(page, selectors) {
37
62
  if (selectors.length > 0) {
38
- await waitForLogin(page, selectors);
63
+ return waitForLogin(page, selectors);
39
64
  }
65
+ // When no selectors are configured, skip waiting and rely on verification if available.
66
+ return "skipped";
40
67
  }
@@ -6,11 +6,13 @@ export const SUPPORTED_SERVICES = [
6
6
  ];
7
7
  export function validateService(service) {
8
8
  if (!service) {
9
- throw new Error(`Service is required. Supported services: ${SUPPORTED_SERVICES.join(", ")}`);
9
+ throw new Error(`Service is required. Supported services: ${SUPPORTED_SERVICES.join(", ")}. ` +
10
+ "Run 'axusage --help' for usage.");
10
11
  }
11
12
  const normalizedService = service.toLowerCase();
12
13
  if (!SUPPORTED_SERVICES.includes(normalizedService)) {
13
- throw new Error(`Unsupported service: ${service}. Supported services: ${SUPPORTED_SERVICES.join(", ")}`);
14
+ throw new Error(`Unsupported service: ${service}. Supported services: ${SUPPORTED_SERVICES.join(", ")}. ` +
15
+ "Run 'axusage --help' for usage.");
14
16
  }
15
17
  return normalizedService;
16
18
  }
@@ -1,5 +1,6 @@
1
- import type { Page } from "playwright";
1
+ import { type Page } from "playwright";
2
2
  /**
3
3
  * Waits until one of the selectors appears on the page, or the user presses Enter to continue.
4
4
  */
5
- export declare function waitForLogin(page: Page, selectors: readonly string[]): Promise<void>;
5
+ export type LoginWaitOutcome = "selector" | "manual" | "timeout" | "closed" | "aborted" | "skipped";
6
+ export declare function waitForLogin(page: Page, selectors: readonly string[]): Promise<LoginWaitOutcome>;
@@ -1,24 +1,73 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
1
+ import { errors } from "playwright";
3
2
  import { LOGIN_TIMEOUT_MS } from "./auth-timeouts.js";
4
- /**
5
- * Waits until one of the selectors appears on the page, or the user presses Enter to continue.
6
- */
3
+ import { input } from "@inquirer/prompts";
4
+ function isTimeoutError(error) {
5
+ return error instanceof errors.TimeoutError;
6
+ }
7
+ const SELECTOR_CLOSED_MESSAGES = [
8
+ "target closed",
9
+ "page closed",
10
+ "context closed",
11
+ "execution context was destroyed",
12
+ ];
13
+ function isSelectorClosedError(error) {
14
+ if (!(error instanceof Error))
15
+ return false;
16
+ const message = error.message.toLowerCase();
17
+ return SELECTOR_CLOSED_MESSAGES.some((snippet) => message.includes(snippet));
18
+ }
19
+ function classifySelectorFailure(error) {
20
+ if (isTimeoutError(error))
21
+ return "timeout";
22
+ if (isSelectorClosedError(error))
23
+ return "closed";
24
+ return undefined;
25
+ }
26
+ function classifySelectorAggregate(error) {
27
+ if (error instanceof AggregateError) {
28
+ const outcomes = error.errors.map((item) => classifySelectorFailure(item));
29
+ if (outcomes.every((item) => item === "timeout"))
30
+ return "timeout";
31
+ if (outcomes.every((item) => item === "timeout" || item === "closed") &&
32
+ outcomes.includes("closed")) {
33
+ return "closed";
34
+ }
35
+ return undefined;
36
+ }
37
+ return classifySelectorFailure(error);
38
+ }
7
39
  export async function waitForLogin(page, selectors) {
8
- const reader = createInterface({ input, output });
9
- const manual = reader.question("Press Enter to continue without waiting for login... ");
10
- // Absorb rejection when the interface is closed to prevent
11
- // unhandled promise rejection (AbortError) after a selector wins.
12
- const manualSilenced = manual.catch(() => { });
13
40
  const timeoutMs = LOGIN_TIMEOUT_MS;
14
- const deadline = Date.now() + timeoutMs;
15
- // Prevent unhandled rejections if the page closes before all waiters finish
16
- const waiters = selectors.map((sel) => page.waitForSelector(sel, { timeout: timeoutMs }).catch(() => {
17
- // Intentionally ignored: the page may navigate/close before selector resolves
18
- }));
19
- const shouldShowCountdown = process.stderr.isTTY;
41
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
42
+ // Non-TTY sessions rely solely on selector waits (no manual continuation).
43
+ if (!canPrompt && selectors.length === 0) {
44
+ return "skipped";
45
+ }
46
+ const waiters = selectors.map((sel) => page.waitForSelector(sel, { timeout: timeoutMs }));
47
+ const shouldShowCountdown = process.stderr.isTTY && waiters.length > 0;
20
48
  let interval;
49
+ const manualController = canPrompt ? new AbortController() : undefined;
50
+ const manualPromise = manualController
51
+ ? input({
52
+ message: "Press Enter after completing login in the browser...",
53
+ default: "",
54
+ }, { signal: manualController.signal })
55
+ .then(() => "manual")
56
+ .catch((error) => {
57
+ if (error instanceof Error &&
58
+ (error.name === "AbortPromptError" || error.name === "AbortError")) {
59
+ // Expected when we cancel the prompt after a selector wins.
60
+ // Returning "manual" keeps the promise resolved for the race.
61
+ return "manual";
62
+ }
63
+ if (error instanceof Error && error.name === "ExitPromptError") {
64
+ return "aborted";
65
+ }
66
+ throw error;
67
+ })
68
+ : undefined;
21
69
  if (shouldShowCountdown) {
70
+ const deadline = Date.now() + timeoutMs;
22
71
  interval = setInterval(() => {
23
72
  const remaining = deadline - Date.now();
24
73
  if (remaining <= 0) {
@@ -34,11 +83,33 @@ export async function waitForLogin(page, selectors) {
34
83
  }, 60_000);
35
84
  }
36
85
  try {
37
- await Promise.race([manualSilenced, ...waiters]);
86
+ const selectorPromise = waiters.length > 0
87
+ ? Promise.any(waiters)
88
+ .then(() => "selector")
89
+ .catch((error) => {
90
+ // Promise.any only rejects once all selectors have settled.
91
+ const outcome = classifySelectorAggregate(error);
92
+ if (outcome)
93
+ return outcome;
94
+ throw error;
95
+ })
96
+ : undefined;
97
+ if (selectorPromise) {
98
+ // Avoid unhandled rejections if the manual prompt wins the race.
99
+ void selectorPromise.catch(() => { });
100
+ }
101
+ const raceTargets = [];
102
+ if (manualPromise)
103
+ raceTargets.push(manualPromise);
104
+ if (selectorPromise)
105
+ raceTargets.push(selectorPromise);
106
+ if (raceTargets.length === 0)
107
+ return "skipped";
108
+ return await Promise.race(raceTargets);
38
109
  }
39
110
  finally {
40
111
  if (interval)
41
112
  clearInterval(interval);
42
- reader.close();
113
+ manualController?.abort();
43
114
  }
44
115
  }
@@ -0,0 +1,25 @@
1
+ type CliDependency = {
2
+ readonly command: string;
3
+ readonly envVar: string;
4
+ readonly installHint: string;
5
+ };
6
+ declare const AUTH_CLI_SERVICES: readonly ["claude", "chatgpt", "gemini"];
7
+ type AuthCliService = (typeof AUTH_CLI_SERVICES)[number];
8
+ export declare function getAuthCliDependency(service: AuthCliService): CliDependency;
9
+ export declare function checkCliDependency(dep: CliDependency): {
10
+ ok: boolean;
11
+ path: string;
12
+ };
13
+ export declare function ensureAuthCliDependency(service: AuthCliService): {
14
+ ok: true;
15
+ path: string;
16
+ } | {
17
+ ok: false;
18
+ dependency: CliDependency;
19
+ path: string;
20
+ };
21
+ export declare function resolveAuthCliDependencyOrReport(service: AuthCliService, options?: {
22
+ readonly setExitCode?: boolean;
23
+ }): string | undefined;
24
+ export { AUTH_CLI_SERVICES };
25
+ export type { AuthCliService };
@@ -0,0 +1,81 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { chalk } from "./color.js";
3
+ const CLI_DEPENDENCIES = {
4
+ claude: {
5
+ command: "claude",
6
+ envVar: "AXUSAGE_CLAUDE_PATH",
7
+ installHint: "npm install -g @anthropic-ai/claude-code",
8
+ },
9
+ codex: {
10
+ command: "codex",
11
+ envVar: "AXUSAGE_CODEX_PATH",
12
+ installHint: "npm install -g @openai/codex",
13
+ },
14
+ gemini: {
15
+ command: "gemini",
16
+ envVar: "AXUSAGE_GEMINI_PATH",
17
+ installHint: "npm install -g @google/gemini-cli",
18
+ },
19
+ };
20
+ const AUTH_CLI_SERVICES = ["claude", "chatgpt", "gemini"];
21
+ function resolveCliDependencyTimeout() {
22
+ const raw = process.env.AXUSAGE_CLI_TIMEOUT_MS;
23
+ if (!raw)
24
+ return 5000;
25
+ const parsed = Number(raw);
26
+ if (!Number.isFinite(parsed) || parsed <= 0)
27
+ return 5000;
28
+ return Math.round(parsed);
29
+ }
30
+ export function getAuthCliDependency(service) {
31
+ if (service === "chatgpt")
32
+ return CLI_DEPENDENCIES.codex;
33
+ return CLI_DEPENDENCIES[service];
34
+ }
35
+ function resolveCliDependencyPath(dep) {
36
+ const environmentValue = process.env[dep.envVar]?.trim();
37
+ // Treat empty env vars as unset to fall back to the default command.
38
+ if (environmentValue)
39
+ return environmentValue;
40
+ return dep.command;
41
+ }
42
+ export function checkCliDependency(dep) {
43
+ const path = resolveCliDependencyPath(dep);
44
+ try {
45
+ const timeout = resolveCliDependencyTimeout();
46
+ execFileSync(path, ["--version"], {
47
+ stdio: "ignore",
48
+ timeout,
49
+ });
50
+ return { ok: true, path };
51
+ }
52
+ catch {
53
+ return { ok: false, path };
54
+ }
55
+ }
56
+ export function ensureAuthCliDependency(service) {
57
+ const dependency = getAuthCliDependency(service);
58
+ const result = checkCliDependency(dependency);
59
+ if (result.ok)
60
+ return { ok: true, path: result.path };
61
+ return { ok: false, dependency, path: result.path };
62
+ }
63
+ export function resolveAuthCliDependencyOrReport(service, options = {}) {
64
+ const result = ensureAuthCliDependency(service);
65
+ if (!result.ok) {
66
+ reportMissingCliDependency(result.dependency, result.path);
67
+ if (options.setExitCode)
68
+ process.exitCode = 1;
69
+ return undefined;
70
+ }
71
+ return result.path;
72
+ }
73
+ function reportMissingCliDependency(dependency, path) {
74
+ console.error(chalk.red(`Error: Required dependency '${dependency.command}' not found.`));
75
+ console.error(chalk.gray(`Looked for: ${path}`));
76
+ console.error(chalk.gray("\nTo fix, either:"));
77
+ console.error(chalk.gray(` 1. Install it: ${dependency.installHint}`));
78
+ console.error(chalk.gray(` 2. Set ${dependency.envVar}=/path/to/${dependency.command}`));
79
+ console.error(chalk.gray("Try 'axusage --help' for requirements and overrides."));
80
+ }
81
+ export { AUTH_CLI_SERVICES };
@@ -0,0 +1,5 @@
1
+ export { default as chalk } from "chalk";
2
+ type ColorConfig = {
3
+ readonly enabled?: boolean;
4
+ };
5
+ export declare function configureColor(config?: ColorConfig): void;
@@ -0,0 +1,27 @@
1
+ import chalkBase from "chalk";
2
+ export { default as chalk } from "chalk";
3
+ function resolveColorOverride(enabled) {
4
+ if (enabled === false)
5
+ return "disable";
6
+ if (enabled === true)
7
+ return "force";
8
+ if (process.env.FORCE_COLOR === "0")
9
+ return "disable";
10
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
11
+ return "disable";
12
+ }
13
+ return "auto";
14
+ }
15
+ const autoLevel = chalkBase.level;
16
+ export function configureColor(config = {}) {
17
+ const mode = resolveColorOverride(config.enabled);
18
+ if (mode === "disable") {
19
+ chalkBase.level = 0;
20
+ return;
21
+ }
22
+ if (mode === "force") {
23
+ chalkBase.level = 3;
24
+ return;
25
+ }
26
+ chalkBase.level = autoLevel;
27
+ }
@@ -1,4 +1,4 @@
1
- import chalk from "chalk";
1
+ import { chalk } from "./color.js";
2
2
  import { calculateUsageRate } from "./calculate-usage-rate.js";
3
3
  import { classifyUsageRate } from "./classify-usage-rate.js";
4
4
  /**
@@ -0,0 +1 @@
1
+ export declare function resolvePromptCapability(): boolean;
@@ -0,0 +1,3 @@
1
+ export function resolvePromptCapability() {
2
+ return process.stdin.isTTY && process.stdout.isTTY;
3
+ }
@@ -0,0 +1,11 @@
1
+ import type { UsageCommandOptions } from "../commands/fetch-service-usage.js";
2
+ export type RootOptions = {
3
+ readonly authSetup?: string;
4
+ readonly authStatus?: string | boolean;
5
+ readonly authClear?: string;
6
+ readonly force?: boolean;
7
+ readonly service?: UsageCommandOptions["service"];
8
+ readonly format?: UsageCommandOptions["format"];
9
+ readonly interactive?: UsageCommandOptions["interactive"];
10
+ };
11
+ export declare function getRootOptionsError(options: RootOptions, formatSource?: string): string | undefined;
@@ -0,0 +1,18 @@
1
+ export function getRootOptionsError(options, formatSource) {
2
+ // Commander sets optional args to `true` when provided without a value.
3
+ const authSelectionCount = Number(Boolean(options.authSetup)) +
4
+ Number(options.authStatus !== undefined) +
5
+ Number(Boolean(options.authClear));
6
+ if (authSelectionCount > 1) {
7
+ return "Use only one of --auth-setup, --auth-status, or --auth-clear.";
8
+ }
9
+ if (options.force && !options.authClear) {
10
+ return "--force is only supported with --auth-clear.";
11
+ }
12
+ const hasExplicitFormat = formatSource === "cli";
13
+ const hasUsageOptions = Boolean(options.service) || hasExplicitFormat;
14
+ if (authSelectionCount > 0 && hasUsageOptions) {
15
+ return "Usage options cannot be combined with auth operations.";
16
+ }
17
+ return undefined;
18
+ }
@@ -0,0 +1 @@
1
+ export declare function writeAtomicJson(filePath: string, data: unknown, mode?: number): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { chmod, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ function getErrorCode(error) {
4
+ if (error instanceof Error && "code" in error) {
5
+ return error.code;
6
+ }
7
+ return undefined;
8
+ }
9
+ export async function writeAtomicJson(filePath, data, mode) {
10
+ const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
11
+ const writeOptions = mode === undefined ? "utf8" : { encoding: "utf8", mode };
12
+ await writeFile(temporaryPath, JSON.stringify(data), writeOptions);
13
+ if (mode !== undefined) {
14
+ await chmod(temporaryPath, mode).catch(() => {
15
+ // Best-effort: some filesystems ignore chmod, but the mode was set at write.
16
+ });
17
+ }
18
+ try {
19
+ await rename(temporaryPath, filePath);
20
+ }
21
+ catch (error) {
22
+ const code = getErrorCode(error);
23
+ if (code === "EPERM" || code === "EACCES" || code === "EEXIST") {
24
+ // Windows can reject rename over an existing file; fall back to a backup swap.
25
+ // Best-effort: not fully atomic and assumes a single writer. Backups are
26
+ // cleaned up by auth-clear when possible.
27
+ const backupPath = `${filePath}.${randomUUID()}.bak`;
28
+ let hasBackup = false;
29
+ try {
30
+ await rename(filePath, backupPath);
31
+ hasBackup = true;
32
+ }
33
+ catch {
34
+ // Best-effort: source file may not exist or be locked.
35
+ }
36
+ try {
37
+ await rename(temporaryPath, filePath);
38
+ }
39
+ catch (fallbackError) {
40
+ if (hasBackup) {
41
+ await rename(backupPath, filePath).catch(() => {
42
+ console.warn(`Warning: Failed to restore backup from ${backupPath}`);
43
+ });
44
+ }
45
+ await unlink(temporaryPath).catch(() => { });
46
+ throw fallbackError;
47
+ }
48
+ if (hasBackup) {
49
+ await unlink(backupPath).catch(() => { });
50
+ }
51
+ return;
52
+ }
53
+ await unlink(temporaryPath).catch(() => { });
54
+ throw error;
55
+ }
56
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axusage",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "2.2.0",
5
+ "version": "3.0.0",
6
6
  "description": "Monitor API usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single CLI",
7
7
  "repository": {
8
8
  "type": "git",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@commander-js/extra-typings": "^14.0.0",
57
+ "@inquirer/prompts": "^8.2.0",
57
58
  "axauth": "^1.11.2",
58
59
  "chalk": "^5.6.2",
59
60
  "commander": "^14.0.2",