axusage 2.1.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 (39) hide show
  1. package/README.md +61 -15
  2. package/dist/adapters/chatgpt.js +2 -2
  3. package/dist/adapters/claude.js +2 -2
  4. package/dist/adapters/gemini.js +2 -2
  5. package/dist/adapters/github-copilot.js +2 -1
  6. package/dist/cli.js +57 -41
  7. package/dist/commands/auth-clear-command.d.ts +2 -0
  8. package/dist/commands/auth-clear-command.js +62 -3
  9. package/dist/commands/auth-setup-command.d.ts +1 -0
  10. package/dist/commands/auth-setup-command.js +36 -5
  11. package/dist/commands/auth-status-command.js +36 -4
  12. package/dist/commands/fetch-service-usage-with-reauth.js +2 -2
  13. package/dist/commands/fetch-service-usage.js +4 -2
  14. package/dist/commands/run-auth-setup.js +26 -5
  15. package/dist/commands/usage-command.js +5 -3
  16. package/dist/config/credential-sources.d.ts +38 -0
  17. package/dist/config/credential-sources.js +117 -0
  18. package/dist/services/create-auth-context.js +2 -1
  19. package/dist/services/do-setup-auth.js +2 -8
  20. package/dist/services/get-service-access-token.d.ts +28 -0
  21. package/dist/services/get-service-access-token.js +146 -0
  22. package/dist/services/persist-storage-state.d.ts +1 -1
  23. package/dist/services/persist-storage-state.js +8 -8
  24. package/dist/services/setup-auth-flow.js +38 -11
  25. package/dist/services/supported-service.js +4 -2
  26. package/dist/services/wait-for-login.d.ts +3 -2
  27. package/dist/services/wait-for-login.js +89 -18
  28. package/dist/utils/check-cli-dependency.d.ts +25 -0
  29. package/dist/utils/check-cli-dependency.js +81 -0
  30. package/dist/utils/color.d.ts +5 -0
  31. package/dist/utils/color.js +27 -0
  32. package/dist/utils/format-service-usage.js +1 -1
  33. package/dist/utils/resolve-prompt-capability.d.ts +1 -0
  34. package/dist/utils/resolve-prompt-capability.js +3 -0
  35. package/dist/utils/validate-root-options.d.ts +11 -0
  36. package/dist/utils/validate-root-options.js +18 -0
  37. package/dist/utils/write-atomic-json.d.ts +1 -0
  38. package/dist/utils/write-atomic-json.js +56 -0
  39. package/package.json +11 -15
package/README.md CHANGED
@@ -12,15 +12,47 @@ npm install -g axusage
12
12
  claude
13
13
  codex
14
14
  gemini
15
- axusage auth setup github-copilot
15
+ axusage --auth-setup github-copilot --interactive
16
16
 
17
17
  # Check authentication status
18
- axusage auth status
18
+ axusage --auth-status
19
+ # For CLI-auth services, this reports CLI availability; use the CLI to confirm login.
19
20
 
20
21
  # Fetch usage
21
22
  axusage
22
23
  ```
23
24
 
25
+ ## Requirements
26
+
27
+ - `claude` CLI (Claude auth) — `npm install -g @anthropic-ai/claude-code`
28
+ - `codex` CLI (ChatGPT auth) — `npm install -g @openai/codex`
29
+ - `gemini` CLI (Gemini auth) — `npm install -g @google/gemini-cli`
30
+ - Playwright Chromium for GitHub Copilot auth (see "Browser installation" below for global install steps)
31
+
32
+ ### Custom Paths
33
+
34
+ If the CLIs are not on your `PATH`, override the binaries:
35
+
36
+ ```bash
37
+ export AXUSAGE_CLAUDE_PATH=/path/to/claude
38
+ export AXUSAGE_CODEX_PATH=/path/to/codex
39
+ export AXUSAGE_GEMINI_PATH=/path/to/gemini
40
+
41
+ # Optional: adjust CLI dependency check timeout (milliseconds)
42
+ export AXUSAGE_CLI_TIMEOUT_MS=5000
43
+ ```
44
+
45
+ For Playwright-managed browsers:
46
+
47
+ ```bash
48
+ export PLAYWRIGHT_BROWSERS_PATH=/path/to/playwright-browsers
49
+ ```
50
+
51
+ ### Exit Codes
52
+
53
+ - `0`: success
54
+ - `1`: errors, including partial failures
55
+
24
56
  ## Authentication
25
57
 
26
58
  Claude, ChatGPT, and Gemini use their respective CLI OAuth sessions. GitHub
@@ -33,13 +65,16 @@ Copilot uses browser-based authentication for persistent, long-lived sessions.
33
65
  claude
34
66
  codex
35
67
  gemini
36
- axusage auth setup github-copilot
68
+ axusage --auth-setup github-copilot --interactive
37
69
 
38
70
  # Check authentication status
39
- axusage auth status
71
+ axusage --auth-status
40
72
  ```
41
73
 
42
- When you run `auth setup` for GitHub Copilot, a browser window will open.
74
+ `--auth-status` reports browser-auth status for GitHub Copilot and CLI availability
75
+ for Claude/ChatGPT/Gemini. Use `claude`, `codex`, or `gemini` to confirm CLI login.
76
+
77
+ When you run `axusage --auth-setup github-copilot --interactive`, a browser window will open.
43
78
  Simply log in to GitHub as you normally would. Your authentication will be
44
79
  saved and automatically used for future requests.
45
80
 
@@ -62,9 +97,13 @@ Security notes:
62
97
  - To revoke access for GitHub Copilot, clear saved browser auth:
63
98
 
64
99
  ```bash
65
- axusage auth clear github-copilot
100
+ axusage --auth-clear github-copilot --interactive
66
101
  ```
67
102
 
103
+ This moves the saved browser files to your system Trash/Recycle Bin (recoverable).
104
+
105
+ Use `--force` to skip confirmation in scripts.
106
+
68
107
  Browser installation:
69
108
 
70
109
  - Playwright Chromium is installed automatically on `pnpm install` via a postinstall script. If this fails in your environment, install manually:
@@ -111,6 +150,9 @@ axusage --service claude --format=json
111
150
 
112
151
  # TSV output (parseable with cut, awk, sort)
113
152
  axusage --format=tsv
153
+
154
+ # Disable color output
155
+ axusage --no-color
114
156
  ```
115
157
 
116
158
  ## Examples
@@ -169,16 +211,17 @@ Use `axusage` when you need a quick, scriptable snapshot of API usage across Cla
169
211
 
170
212
  - The CLI shows a countdown while waiting for login.
171
213
  - If you have completed login, press Enter in the terminal to continue.
172
- - If it still fails, run `axusage auth clear <service>` and retry.
214
+ - If it still fails, run `axusage --auth-clear <service> --interactive` and retry.
173
215
 
174
216
  ### "No saved authentication" error
175
217
 
176
- - Check which services are authenticated: `axusage auth status`.
177
- - Set up the missing service: `axusage auth setup <service>`.
218
+ - Check which services are authenticated: `axusage --auth-status`.
219
+ - For GitHub Copilot, set up the missing service: `axusage --auth-setup <service> --interactive`.
220
+ - For Claude/ChatGPT/Gemini, run the provider CLI to authenticate.
178
221
 
179
222
  ### Sessions expire
180
223
 
181
- - Browser sessions can expire based on provider policy. Re-run `auth setup` for the affected service when you see authentication errors.
224
+ - Browser sessions can expire based on provider policy. Re-run `axusage --auth-setup <service> --interactive` for the affected service when you see authentication errors.
182
225
 
183
226
  ## Remote authentication and Prometheus export
184
227
 
@@ -195,15 +238,17 @@ You can perform the interactive login flow on a workstation (for example, a loca
195
238
  claude
196
239
  codex
197
240
  gemini
198
- axusage auth setup github-copilot
241
+ axusage --auth-setup github-copilot --interactive
199
242
  ```
200
243
 
201
244
  2. Confirm the workstation has valid sessions:
202
245
 
203
246
  ```bash
204
- axusage auth status
247
+ axusage --auth-status
205
248
  ```
206
249
 
250
+ For CLI-auth services, run `claude`, `codex`, or `gemini` to confirm login.
251
+
207
252
  3. Package the saved contexts so they can be transferred. Set `CONTEXT_DIR` to the path for your platform (see the table above):
208
253
 
209
254
  ```bash
@@ -211,7 +256,8 @@ You can perform the interactive login flow on a workstation (for example, a loca
211
256
  tar czf axusage-contexts.tgz -C "$(dirname "$CONTEXT_DIR")" "$(basename "$CONTEXT_DIR")"
212
257
  ```
213
258
 
214
- Archive structure: `browser-contexts/claude/`, `browser-contexts/chatgpt/`, etc.
259
+ Archive structure: `browser-contexts/github-copilot-auth.json` plus matching
260
+ `github-copilot-auth.meta.json` (CLI-auth services do not use browser contexts).
215
261
 
216
262
  ### 2. Transfer the browser contexts to the Linux server
217
263
 
@@ -237,7 +283,7 @@ You can perform the interactive login flow on a workstation (for example, a loca
237
283
  3. Verify that the sessions are available on the server:
238
284
 
239
285
  ```bash
240
- axusage auth status
286
+ axusage --auth-status
241
287
  ```
242
288
 
243
289
  If the server does not yet have the tool installed, run `npm install -g axusage` before checking the status.
@@ -245,7 +291,7 @@ You can perform the interactive login flow on a workstation (for example, a loca
245
291
  Notes:
246
292
 
247
293
  - Use `--service <name>` to restrict services.
248
- - Sessions may expire or become invalid if you change your password or log out of the service in another browser. Re-run `auth setup` as needed.
294
+ - Sessions may expire or become invalid if you change your password or log out of the service in another browser. Re-run `axusage --auth-setup <service> --interactive` as needed.
249
295
  - If you transfer browser contexts between machines, ensure the target system is secure and permissions are restricted to the intended user.
250
296
  - The CLI stores authentication data in the platform-specific directories listed above; protect that directory to prevent unauthorized access.
251
297
 
@@ -1,5 +1,5 @@
1
- import { getAgentAccessToken } from "axauth";
2
1
  import { ApiError } from "../types/domain.js";
2
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
3
3
  import { ChatGPTUsageResponse as ChatGPTUsageResponseSchema } from "../types/chatgpt.js";
4
4
  import { toServiceUsageData } from "./parse-chatgpt-usage.js";
5
5
  const API_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -12,7 +12,7 @@ const API_URL = "https://chatgpt.com/backend-api/wham/usage";
12
12
  export const chatGPTAdapter = {
13
13
  name: "ChatGPT",
14
14
  async fetchUsage() {
15
- const accessToken = await getAgentAccessToken("codex");
15
+ const accessToken = await getServiceAccessToken("chatgpt");
16
16
  if (!accessToken) {
17
17
  return {
18
18
  ok: false,
@@ -1,6 +1,6 @@
1
- import { getAgentAccessToken } from "axauth";
2
1
  import { z } from "zod";
3
2
  import { ApiError } from "../types/domain.js";
3
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
4
4
  import { UsageResponse as UsageResponseSchema } from "../types/usage.js";
5
5
  import { coalesceClaudeUsageResponse } from "./coalesce-claude-usage-response.js";
6
6
  import { toServiceUsageData } from "./parse-claude-usage.js";
@@ -43,7 +43,7 @@ async function fetchPlanType(accessToken) {
43
43
  export const claudeAdapter = {
44
44
  name: "Claude",
45
45
  async fetchUsage() {
46
- const accessToken = await getAgentAccessToken("claude");
46
+ const accessToken = await getServiceAccessToken("claude");
47
47
  if (!accessToken) {
48
48
  return {
49
49
  ok: false,
@@ -1,6 +1,6 @@
1
- import { getAgentAccessToken } from "axauth";
2
1
  import { ApiError } from "../types/domain.js";
3
2
  import { fetchGeminiQuota, fetchGeminiProject, } from "../services/gemini-api.js";
3
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
4
4
  import { toServiceUsageData } from "./parse-gemini-usage.js";
5
5
  /**
6
6
  * Gemini service adapter using direct API access.
@@ -11,7 +11,7 @@ import { toServiceUsageData } from "./parse-gemini-usage.js";
11
11
  export const geminiAdapter = {
12
12
  name: "Gemini",
13
13
  async fetchUsage() {
14
- const accessToken = await getAgentAccessToken("gemini");
14
+ const accessToken = await getServiceAccessToken("gemini");
15
15
  if (!accessToken) {
16
16
  return {
17
17
  ok: false,
@@ -16,7 +16,8 @@ export const githubCopilotAdapter = {
16
16
  if (!manager.hasAuth("github-copilot")) {
17
17
  return {
18
18
  ok: false,
19
- error: new ApiError("No saved authentication for github-copilot. Run 'axusage auth setup github-copilot' first."),
19
+ error: new ApiError("No saved authentication for github-copilot. " +
20
+ "Run 'axusage --auth-setup github-copilot --interactive' first."),
20
21
  };
21
22
  }
22
23
  const body = await manager.makeAuthenticatedRequest("github-copilot", API_URL);
package/dist/cli.js CHANGED
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from "@commander-js/extra-typings";
3
3
  import packageJson from "../package.json" with { type: "json" };
4
- import { usageCommand } from "./commands/usage-command.js";
4
+ import { authClearCommand } from "./commands/auth-clear-command.js";
5
5
  import { authSetupCommand } from "./commands/auth-setup-command.js";
6
6
  import { authStatusCommand } from "./commands/auth-status-command.js";
7
- import { authClearCommand } from "./commands/auth-clear-command.js";
7
+ import { usageCommand } from "./commands/usage-command.js";
8
+ import { getBrowserContextsDirectory } from "./services/app-paths.js";
8
9
  import { getAvailableServices } from "./services/service-adapter-registry.js";
9
10
  import { installAuthManagerCleanup } from "./services/shared-browser-auth-manager.js";
10
- import { getBrowserContextsDirectory } from "./services/app-paths.js";
11
+ import { configureColor } from "./utils/color.js";
12
+ import { getRootOptionsError, } from "./utils/validate-root-options.js";
13
+ // Parse --no-color early so help/error output is consistently uncolored.
14
+ const shouldDisableColor = process.argv.includes("--no-color");
15
+ configureColor({ enabled: shouldDisableColor ? false : undefined });
11
16
  const program = new Command()
12
17
  .name(packageJson.name)
13
18
  .description(packageJson.description)
@@ -15,49 +20,60 @@ const program = new Command()
15
20
  .showHelpAfterError("(add --help for additional information)")
16
21
  .showSuggestionAfterError()
17
22
  .helpCommand(false)
18
- .addHelpText("after", `\nExamples:\n # Fetch usage for all services\n ${packageJson.name}\n\n # JSON output for a single service\n ${packageJson.name} --service claude --format=json\n\n # TSV output for piping to cut, awk, sort\n ${packageJson.name} --format=tsv | tail -n +2 | awk -F'\\t' '{print $1, $4"%"}'\n\n # Filter Prometheus metrics with standard tools\n ${packageJson.name} --format=prometheus | grep axusage_utilization_percent\n`);
19
- // Ensure browser resources are cleaned when process exits
20
- installAuthManagerCleanup();
21
- // Usage command (default)
22
- program
23
- .command("usage", { isDefault: true })
24
- .description("Fetch API usage statistics (defaults to all: Claude, ChatGPT, GitHub Copilot)")
23
+ .option("--no-color", "disable color output")
25
24
  .option("-s, --service <service>", `Service to query (${getAvailableServices().join(", ")}, all) - defaults to all`)
26
- .option("-i, --interactive", "allow interactive re-authentication during usage fetch")
25
+ .option("-i, --interactive", "allow interactive authentication prompts (usage reauth, --auth-setup/--auth-clear; ignored with --auth-status)")
27
26
  .addOption(new Option("-o, --format <format>", "Output format")
28
27
  .choices(["text", "tsv", "json", "prometheus"])
29
28
  .default("text"))
30
- .addHelpText("after", `\nExamples:\n # Query all services (default)\n ${packageJson.name}\n\n # Query a single service\n ${packageJson.name} --service claude\n\n # TSV output for piping to Unix tools\n ${packageJson.name} --format=tsv | tail -n +2 | cut -f1,4\n\n # Filter Prometheus metrics\n ${packageJson.name} --format=prometheus | grep claude\n`)
31
- .action(async (options) => {
32
- await usageCommand(options);
33
- });
34
- // Auth command group
35
- const auth = program
36
- .command("auth")
37
- .description("Manage authentication for services")
38
- .helpCommand(false)
39
- .addHelpText("after", `\nStorage: ${getBrowserContextsDirectory()}\n(respects XDG_DATA_HOME and platform defaults)`);
40
- auth
41
- .command("setup")
42
- .description("Set up browser-based authentication for a service")
43
- .argument("<service>", "Service to authenticate (claude, chatgpt, github-copilot)")
44
- .action(async (service) => {
45
- await authSetupCommand({ service });
46
- });
47
- auth
48
- .command("status")
49
- .description("Check authentication status for services")
50
- .option("-s, --service <service>", "Check status for specific service")
51
- .action((options) => {
52
- authStatusCommand(options);
53
- });
54
- auth
55
- .command("clear")
56
- .description("Clear saved browser authentication for a service")
57
- .argument("<service>", "Service to clear (claude, chatgpt, github-copilot)")
58
- .action(async (service) => {
59
- await authClearCommand({ service });
29
+ .option("--auth-setup <service>", "set up authentication for a service (CLI or browser-based)")
30
+ .option("--auth-status [service]", "check authentication status for services")
31
+ .option("--auth-clear <service>", "clear saved browser authentication for a service (moves files to system Trash)")
32
+ .option("-f, --force", "skip confirmation for destructive actions")
33
+ .addHelpText("after", `\nExamples:\n # Fetch usage for all services\n ${packageJson.name}\n\n # JSON output for a single service\n ${packageJson.name} --service claude --format=json\n\n # TSV output for piping to cut, awk, sort\n ${packageJson.name} --format=tsv | tail -n +2 | awk -F'\\t' '{print $1, $4"%"}'\n\n # Filter Prometheus metrics with standard tools\n ${packageJson.name} --format=prometheus | grep axusage_utilization_percent\n\n # Check authentication status for all services\n ${packageJson.name} --auth-status\n\nStorage: ${getBrowserContextsDirectory()}\n(respects XDG_DATA_HOME and platform defaults)\n\nRequires: claude, codex (ChatGPT), gemini (CLI auth); Playwright Chromium (GitHub Copilot auth)\nOverride CLI paths: AXUSAGE_CLAUDE_PATH, AXUSAGE_CODEX_PATH, AXUSAGE_GEMINI_PATH\nPlaywright: PLAYWRIGHT_BROWSERS_PATH\n`);
34
+ function fail(message) {
35
+ console.error(`Error: ${message}`);
36
+ console.error("Try 'axusage --help' for details.");
37
+ if (process.exitCode === undefined)
38
+ process.exitCode = 1;
39
+ }
40
+ program.action(async (options, command) => {
41
+ const errorMessage = getRootOptionsError(options, command.getOptionValueSource("format"));
42
+ if (errorMessage) {
43
+ fail(errorMessage);
44
+ return;
45
+ }
46
+ if (options.authSetup) {
47
+ await authSetupCommand({
48
+ service: options.authSetup,
49
+ interactive: options.interactive,
50
+ });
51
+ return;
52
+ }
53
+ if (options.authStatus !== undefined) {
54
+ const service = typeof options.authStatus === "string" && options.authStatus.length > 0
55
+ ? options.authStatus
56
+ : undefined;
57
+ authStatusCommand({ service });
58
+ return;
59
+ }
60
+ if (options.authClear) {
61
+ await authClearCommand({
62
+ service: options.authClear,
63
+ interactive: options.interactive,
64
+ force: options.force,
65
+ });
66
+ return;
67
+ }
68
+ const usageOptions = {
69
+ service: options.service,
70
+ format: options.format,
71
+ interactive: options.interactive,
72
+ };
73
+ await usageCommand(usageOptions);
60
74
  });
75
+ // Ensure browser resources are cleaned when process exits
76
+ installAuthManagerCleanup();
61
77
  try {
62
78
  await program.parseAsync();
63
79
  }
@@ -1,5 +1,7 @@
1
1
  type AuthClearOptions = {
2
2
  readonly service?: string;
3
+ readonly interactive?: boolean;
4
+ readonly force?: boolean;
3
5
  };
4
6
  export declare function authClearCommand(options: AuthClearOptions): Promise<void>;
5
7
  export {};
@@ -1,20 +1,79 @@
1
- import chalk from "chalk";
2
- import { existsSync } from "node:fs";
1
+ import { confirm } from "@inquirer/prompts";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import path from "node:path";
3
4
  import trash from "trash";
4
5
  import { validateService } from "../services/supported-service.js";
5
6
  import { getAuthMetaPathFor, getStorageStatePathFor, } from "../services/auth-storage-path.js";
6
7
  import { getBrowserContextsDirectory } from "../services/app-paths.js";
8
+ import { chalk } from "../utils/color.js";
9
+ import { resolvePromptCapability } from "../utils/resolve-prompt-capability.js";
10
+ function isPromptCancellation(error) {
11
+ return (error instanceof Error &&
12
+ (error.name === "AbortPromptError" ||
13
+ error.name === "CancelPromptError" ||
14
+ error.name === "ExitPromptError"));
15
+ }
16
+ function collectRelatedArtifacts(filePath) {
17
+ const directory = path.dirname(filePath);
18
+ const baseName = path.basename(filePath);
19
+ try {
20
+ return readdirSync(directory)
21
+ .filter((entry) => entry.startsWith(`${baseName}.`) &&
22
+ (entry.endsWith(".bak") || entry.endsWith(".tmp")))
23
+ .map((entry) => path.join(directory, entry));
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
7
29
  export async function authClearCommand(options) {
8
30
  const service = validateService(options.service);
9
31
  const dataDirectory = getBrowserContextsDirectory();
10
32
  const storage = getStorageStatePathFor(dataDirectory, service);
11
33
  const meta = getAuthMetaPathFor(dataDirectory, service);
12
34
  try {
13
- const targets = [storage, meta].filter((p) => existsSync(p));
35
+ const artifactTargets = [storage, meta].filter((p) => existsSync(p));
36
+ const backupTargets = [storage, meta].flatMap((filePath) => collectRelatedArtifacts(filePath));
37
+ const targets = [...new Set([...artifactTargets, ...backupTargets])];
14
38
  if (targets.length === 0) {
15
39
  console.error(chalk.gray(`\nNo saved authentication found for ${service}.`));
16
40
  return;
17
41
  }
42
+ if (!options.force) {
43
+ if (!options.interactive) {
44
+ console.error(chalk.red("Error: Clearing saved authentication requires confirmation."));
45
+ console.error(chalk.gray("Re-run with --interactive to confirm, or use --force to skip confirmation."));
46
+ console.error(chalk.gray("Try 'axusage --help' for details."));
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ if (!resolvePromptCapability()) {
51
+ console.error(chalk.red("Error: --interactive requires a TTY-enabled terminal."));
52
+ console.error(chalk.gray("Re-run in a terminal or pass --force instead."));
53
+ console.error(chalk.gray("Try 'axusage --help' for details."));
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ let confirmed = false;
58
+ try {
59
+ confirmed = await confirm({
60
+ message: `Remove saved authentication for ${service}?`,
61
+ default: false,
62
+ });
63
+ }
64
+ catch (error) {
65
+ if (isPromptCancellation(error)) {
66
+ console.error(chalk.gray("Aborted."));
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ throw error;
71
+ }
72
+ if (!confirmed) {
73
+ console.error(chalk.gray("Aborted."));
74
+ return;
75
+ }
76
+ }
18
77
  await trash(targets, { glob: false });
19
78
  console.error(chalk.green(`\n✓ Cleared authentication for ${service}`));
20
79
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  type AuthSetupOptions = {
5
5
  readonly service?: string;
6
+ readonly interactive?: boolean;
6
7
  };
7
8
  /**
8
9
  * Set up authentication for a service
@@ -1,6 +1,8 @@
1
- import chalk from "chalk";
2
1
  import { BrowserAuthManager } from "../services/browser-auth-manager.js";
3
2
  import { validateService } from "../services/supported-service.js";
3
+ import { resolveAuthCliDependencyOrReport } from "../utils/check-cli-dependency.js";
4
+ import { chalk } from "../utils/color.js";
5
+ import { resolvePromptCapability } from "../utils/resolve-prompt-capability.js";
4
6
  /**
5
7
  * Set up authentication for a service
6
8
  */
@@ -8,32 +10,61 @@ export async function authSetupCommand(options) {
8
10
  const service = validateService(options.service);
9
11
  // CLI-based auth - users should run the native CLI directly
10
12
  if (service === "gemini") {
13
+ const cliPath = resolveAuthCliDependencyOrReport("gemini", {
14
+ setExitCode: true,
15
+ });
16
+ if (!cliPath)
17
+ return;
11
18
  console.error(chalk.yellow("\nGemini uses CLI-based authentication managed by the Gemini CLI."));
12
19
  console.error(chalk.gray("\nTo authenticate, run:"));
13
- console.error(chalk.cyan(" gemini"));
20
+ console.error(chalk.cyan(` ${cliPath}`));
14
21
  console.error(chalk.gray("\nThe Gemini CLI will guide you through the OAuth login process.\n"));
15
22
  return;
16
23
  }
17
24
  if (service === "claude") {
25
+ const cliPath = resolveAuthCliDependencyOrReport("claude", {
26
+ setExitCode: true,
27
+ });
28
+ if (!cliPath)
29
+ return;
18
30
  console.error(chalk.yellow("\nClaude uses CLI-based authentication managed by Claude Code."));
19
31
  console.error(chalk.gray("\nTo authenticate, run:"));
20
- console.error(chalk.cyan(" claude"));
32
+ console.error(chalk.cyan(` ${cliPath}`));
21
33
  console.error(chalk.gray("\nClaude Code will guide you through authentication.\n"));
22
34
  return;
23
35
  }
24
36
  if (service === "chatgpt") {
37
+ const cliPath = resolveAuthCliDependencyOrReport("chatgpt", {
38
+ setExitCode: true,
39
+ });
40
+ if (!cliPath)
41
+ return;
25
42
  console.error(chalk.yellow("\nChatGPT uses CLI-based authentication managed by Codex."));
26
43
  console.error(chalk.gray("\nTo authenticate, run:"));
27
- console.error(chalk.cyan(" codex"));
44
+ console.error(chalk.cyan(` ${cliPath}`));
28
45
  console.error(chalk.gray("\nCodex will guide you through authentication.\n"));
29
46
  return;
30
47
  }
48
+ if (!options.interactive) {
49
+ console.error(chalk.red("Error: Authentication setup requires --interactive."));
50
+ console.error(chalk.gray("Re-run with --interactive in a TTY-enabled terminal to continue."));
51
+ console.error(chalk.gray("Try 'axusage --help' for details."));
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ if (!resolvePromptCapability()) {
56
+ console.error(chalk.red("Error: --interactive requires a TTY-enabled terminal."));
57
+ console.error(chalk.gray("Re-run in a terminal session to complete authentication."));
58
+ console.error(chalk.gray("Try 'axusage --help' for details."));
59
+ process.exitCode = 1;
60
+ return;
61
+ }
31
62
  const manager = new BrowserAuthManager({ headless: false });
32
63
  try {
33
64
  console.error(chalk.blue(`\nSetting up authentication for ${service}...\n`));
34
65
  await manager.setupAuth(service);
35
66
  console.error(chalk.green(`\n✓ Authentication for ${service} is complete!`));
36
- console.error(chalk.gray(`\nYou can now run: ${chalk.cyan(`axusage usage --service ${service}`)}`));
67
+ console.error(chalk.gray(`\nYou can now run: ${chalk.cyan(`axusage --service ${service}`)}`));
37
68
  }
38
69
  catch (error) {
39
70
  console.error(chalk.red(`\n✗ Failed to set up authentication for ${service}: ${error instanceof Error ? error.message : String(error)}`));
@@ -1,25 +1,57 @@
1
- import chalk from "chalk";
2
1
  import { existsSync } from "node:fs";
3
2
  import { SUPPORTED_SERVICES, validateService, } from "../services/supported-service.js";
4
3
  import { getStorageStatePathFor } from "../services/auth-storage-path.js";
5
4
  import { getBrowserContextsDirectory } from "../services/app-paths.js";
5
+ import { AUTH_CLI_SERVICES, checkCliDependency, getAuthCliDependency, } from "../utils/check-cli-dependency.js";
6
+ import { chalk } from "../utils/color.js";
6
7
  export function authStatusCommand(options) {
7
8
  const servicesToCheck = options.service
8
9
  ? [validateService(options.service)]
9
10
  : SUPPORTED_SERVICES;
11
+ const cliAuthServices = new Set(AUTH_CLI_SERVICES);
10
12
  const dataDirectory = getBrowserContextsDirectory();
13
+ let hasFailures = false;
11
14
  console.log(chalk.blue("\nAuthentication Status:\n"));
12
15
  for (const service of servicesToCheck) {
16
+ if (cliAuthServices.has(service)) {
17
+ const dependency = getAuthCliDependency(service);
18
+ const result = checkCliDependency(dependency);
19
+ const status = result.ok
20
+ ? chalk.green("↪ CLI-managed")
21
+ : chalk.red("✗ CLI missing");
22
+ if (!result.ok) {
23
+ hasFailures = true;
24
+ }
25
+ console.log(`${chalk.bold(service)}: ${status}`);
26
+ console.log(` ${chalk.dim("CLI:")} ${chalk.dim(result.path)}`);
27
+ if (result.ok) {
28
+ console.log(` ${chalk.dim("Auth:")} ${chalk.dim(`run ${result.path} to check/login`)}`);
29
+ }
30
+ else {
31
+ console.log(` ${chalk.dim("Install:")} ${chalk.dim(dependency.installHint)}`);
32
+ console.log(` ${chalk.dim("Override:")} ${chalk.dim(`${dependency.envVar}=/path/to/${dependency.command}`)}`);
33
+ }
34
+ continue;
35
+ }
13
36
  const storagePath = getStorageStatePathFor(dataDirectory, service);
14
37
  const hasAuth = existsSync(storagePath);
15
38
  const status = hasAuth
16
39
  ? chalk.green("✓ Authenticated")
17
40
  : chalk.gray("✗ Not authenticated");
41
+ if (!hasAuth) {
42
+ hasFailures = true;
43
+ }
18
44
  console.log(`${chalk.bold(service)}: ${status}`);
19
45
  console.log(` ${chalk.dim("Storage:")} ${chalk.dim(storagePath)}`);
20
46
  }
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>")}`));
47
+ const browserServices = servicesToCheck.filter((service) => !cliAuthServices.has(service));
48
+ const copilotService = "github-copilot";
49
+ const needsCopilotSetup = browserServices.includes(copilotService) &&
50
+ !existsSync(getStorageStatePathFor(dataDirectory, copilotService));
51
+ if (needsCopilotSetup) {
52
+ console.error(chalk.gray(`\nTo set up authentication, run: ${chalk.cyan("axusage --auth-setup github-copilot --interactive")}`));
53
+ }
54
+ if (hasFailures) {
55
+ process.exitCode = 1;
24
56
  }
25
57
  }
@@ -1,7 +1,7 @@
1
- import chalk from "chalk";
2
1
  import { fetchServiceUsage } from "./fetch-service-usage.js";
3
2
  import { isAuthFailure, runAuthSetup } from "./run-auth-setup.js";
4
3
  import { validateService } from "../services/supported-service.js";
4
+ import { chalk } from "../utils/color.js";
5
5
  /**
6
6
  * Fetch usage for a service, with automatic re-authentication on auth errors.
7
7
  * Prompts the user to re-authenticate if the initial fetch fails with an auth error,
@@ -14,7 +14,7 @@ export async function fetchServiceUsageWithAutoReauth(serviceName, interactive)
14
14
  }
15
15
  // If auth error, try to re-authenticate and retry
16
16
  if (isAuthFailure(result)) {
17
- console.error(chalk.yellow(`⚠ Authentication failed for ${serviceName}. Opening browser to re-authenticate...`));
17
+ console.error(chalk.yellow(`⚠ Authentication failed for ${serviceName}. Attempting to re-authenticate...`));
18
18
  try {
19
19
  const service = validateService(serviceName);
20
20
  const authSuccess = await runAuthSetup(service);
@@ -1,5 +1,5 @@
1
1
  import { ApiError as ApiErrorClass } from "../types/domain.js";
2
- import { getServiceAdapter } from "../services/service-adapter-registry.js";
2
+ import { getAvailableServices, getServiceAdapter, } from "../services/service-adapter-registry.js";
3
3
  const ALL_SERVICES = ["claude", "chatgpt", "github-copilot", "gemini"];
4
4
  export function selectServicesToQuery(service) {
5
5
  const normalized = service?.toLowerCase();
@@ -10,9 +10,11 @@ export function selectServicesToQuery(service) {
10
10
  export async function fetchServiceUsage(serviceName) {
11
11
  const adapter = getServiceAdapter(serviceName);
12
12
  if (!adapter) {
13
+ const available = getAvailableServices().join(", ");
13
14
  return {
14
15
  ok: false,
15
- error: new ApiErrorClass(`Unknown service "${serviceName}"`),
16
+ error: new ApiErrorClass(`Unknown service "${serviceName}". Supported services: ${available}. ` +
17
+ "Run 'axusage --help' for usage."),
16
18
  };
17
19
  }
18
20
  return await adapter.fetchUsage();