axusage 3.3.0 → 3.4.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/README.md +67 -0
- package/dist/cli.js +12 -0
- package/dist/commands/serve-command.d.ts +11 -0
- package/dist/commands/serve-command.js +116 -0
- package/dist/config/serve-config.d.ts +20 -0
- package/dist/config/serve-config.js +40 -0
- package/dist/server/routes.d.ts +18 -0
- package/dist/server/routes.js +37 -0
- package/dist/server/server.d.ts +19 -0
- package/dist/server/server.js +72 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -151,6 +151,73 @@ Human-readable output includes:
|
|
|
151
151
|
JSON output provides structured data for automation.
|
|
152
152
|
Prometheus output emits text metrics suitable for scraping.
|
|
153
153
|
|
|
154
|
+
## Serve Mode
|
|
155
|
+
|
|
156
|
+
`axusage serve` starts an HTTP server that exposes Prometheus metrics at `/metrics` for scraping, with automatic polling.
|
|
157
|
+
|
|
158
|
+
### Usage
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Start with defaults (port 3848, poll every 5 minutes)
|
|
162
|
+
axusage serve
|
|
163
|
+
|
|
164
|
+
# Custom configuration
|
|
165
|
+
axusage serve --port 9090 --interval 60 --service claude
|
|
166
|
+
|
|
167
|
+
# With environment variables
|
|
168
|
+
AXUSAGE_PORT=9090 AXUSAGE_INTERVAL=60 axusage serve
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Options
|
|
172
|
+
|
|
173
|
+
| Flag | Env Var | Default | Description |
|
|
174
|
+
| ---------------------- | ------------------ | ----------- | --------------------------- |
|
|
175
|
+
| `--port <port>` | `AXUSAGE_PORT` | `3848` | Port to listen on |
|
|
176
|
+
| `--host <host>` | `AXUSAGE_HOST` | `127.0.0.1` | Host to bind to |
|
|
177
|
+
| `--interval <seconds>` | `AXUSAGE_INTERVAL` | `300` | Polling interval in seconds |
|
|
178
|
+
| `--service <service>` | `AXUSAGE_SERVICE` | all | Service to monitor |
|
|
179
|
+
|
|
180
|
+
### Endpoints
|
|
181
|
+
|
|
182
|
+
- `GET /metrics` — Prometheus text exposition (`text/plain; version=0.0.4`). Returns 503 if no data has been fetched yet.
|
|
183
|
+
- `GET /health` — JSON health status with version, last refresh time, tracked services, and errors.
|
|
184
|
+
|
|
185
|
+
### Container Deployment
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Build image
|
|
189
|
+
podman build -t axusage .
|
|
190
|
+
|
|
191
|
+
# Run (configure credential sources via AXUSAGE_SOURCES)
|
|
192
|
+
podman run -p 3848:3848 --user 1000:1000 \
|
|
193
|
+
-e AXUSAGE_SOURCES='{"claude":{"source":"vault","name":"claude-oauth"}}' \
|
|
194
|
+
-e AXVAULT_URL=http://axvault:3847 \
|
|
195
|
+
-e AXVAULT_API_KEY=axv_sk_... \
|
|
196
|
+
axusage
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Docker Compose
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
cp .env.example .env
|
|
203
|
+
# Edit .env with credential sources and vault config
|
|
204
|
+
docker compose up -d --build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Publishing
|
|
208
|
+
|
|
209
|
+
Container publishing is part of CI/CD:
|
|
210
|
+
|
|
211
|
+
- Pushes to `main` run the `Release` workflow.
|
|
212
|
+
- If `semantic-release` creates a new version tag, CI builds and publishes a multi-arch image to `registry.j4k.dev/axusage:<version>`.
|
|
213
|
+
|
|
214
|
+
For manual publishing:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
./scripts/publish-image.sh --dry-run
|
|
218
|
+
./scripts/publish-image.sh --version 1.0.0
|
|
219
|
+
```
|
|
220
|
+
|
|
154
221
|
## Troubleshooting
|
|
155
222
|
|
|
156
223
|
### "Required dependency '... not found'"
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command, Option } from "@commander-js/extra-typings";
|
|
|
3
3
|
import packageJson from "../package.json" with { type: "json" };
|
|
4
4
|
import { authSetupCommand } from "./commands/auth-setup-command.js";
|
|
5
5
|
import { authStatusCommand } from "./commands/auth-status-command.js";
|
|
6
|
+
import { serveCommand } from "./commands/serve-command.js";
|
|
6
7
|
import { usageCommand } from "./commands/usage-command.js";
|
|
7
8
|
import { getCredentialSourcesPath } from "./config/credential-sources.js";
|
|
8
9
|
import { getAvailableServices } from "./services/service-adapter-registry.js";
|
|
@@ -19,6 +20,7 @@ const program = new Command()
|
|
|
19
20
|
.showHelpAfterError("(add --help for additional information)")
|
|
20
21
|
.showSuggestionAfterError()
|
|
21
22
|
.helpCommand(false)
|
|
23
|
+
.enablePositionalOptions()
|
|
22
24
|
.option("--no-color", "disable color output")
|
|
23
25
|
.option("-s, --service <service>", `Service to query (${getAvailableServices().join(", ")}, all) - defaults to all`)
|
|
24
26
|
.addOption(new Option("-o, --format <format>", "Output format")
|
|
@@ -27,6 +29,16 @@ const program = new Command()
|
|
|
27
29
|
.option("--auth-setup <service>", "set up authentication for a service (directs to appropriate CLI)")
|
|
28
30
|
.option("--auth-status [service]", "check authentication status for services")
|
|
29
31
|
.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\nSources config file: ${getCredentialSourcesPath()}\n(or set AXUSAGE_SOURCES to JSON to bypass file)\n\n${formatRequiresHelpText()}\nOverride CLI paths: AXUSAGE_CLAUDE_PATH, AXUSAGE_CODEX_PATH, AXUSAGE_GEMINI_PATH, AXUSAGE_GH_PATH\n`);
|
|
32
|
+
program
|
|
33
|
+
.command("serve")
|
|
34
|
+
.description("Start HTTP server exposing Prometheus metrics at /metrics")
|
|
35
|
+
.option("-p, --port <port>", "Port to listen on (env: AXUSAGE_PORT)")
|
|
36
|
+
.option("-H, --host <host>", "Host to bind to (env: AXUSAGE_HOST)")
|
|
37
|
+
.option("--interval <seconds>", "Polling interval in seconds (env: AXUSAGE_INTERVAL)")
|
|
38
|
+
.option("-s, --service <service>", "Service to monitor (env: AXUSAGE_SERVICE, default: all)")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
await serveCommand(options);
|
|
41
|
+
});
|
|
30
42
|
function fail(message) {
|
|
31
43
|
console.error(`Error: ${message}`);
|
|
32
44
|
console.error("Try 'axusage --help' for details.");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command handler — starts an HTTP server exposing Prometheus metrics.
|
|
3
|
+
*/
|
|
4
|
+
type ServeCommandOptions = {
|
|
5
|
+
readonly port?: string;
|
|
6
|
+
readonly host?: string;
|
|
7
|
+
readonly interval?: string;
|
|
8
|
+
readonly service?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function serveCommand(options: ServeCommandOptions): Promise<void>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command handler — starts an HTTP server exposing Prometheus metrics.
|
|
3
|
+
*/
|
|
4
|
+
import { getServeConfig } from "../config/serve-config.js";
|
|
5
|
+
import { selectServicesToQuery } from "./fetch-service-usage.js";
|
|
6
|
+
import { fetchServicesInParallel } from "./usage-command.js";
|
|
7
|
+
import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
|
|
8
|
+
import { createServer } from "../server/server.js";
|
|
9
|
+
import { createHealthRouter, createMetricsRouter } from "../server/routes.js";
|
|
10
|
+
import { getAvailableServices } from "../services/service-adapter-registry.js";
|
|
11
|
+
export async function serveCommand(options) {
|
|
12
|
+
const config = getServeConfig(options);
|
|
13
|
+
const availableServices = getAvailableServices();
|
|
14
|
+
if (config.service !== undefined &&
|
|
15
|
+
config.service.toLowerCase() !== "all" &&
|
|
16
|
+
!availableServices.includes(config.service.toLowerCase())) {
|
|
17
|
+
console.error(`Unknown service "${config.service}". Supported: ${availableServices.join(", ")}.`);
|
|
18
|
+
if (process.exitCode === undefined)
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const servicesToQuery = selectServicesToQuery(config.service);
|
|
23
|
+
// Cached state
|
|
24
|
+
let cachedMetrics;
|
|
25
|
+
let lastRefreshTime;
|
|
26
|
+
let lastRefreshErrors = [];
|
|
27
|
+
let refreshing = false;
|
|
28
|
+
async function refreshMetrics() {
|
|
29
|
+
if (refreshing)
|
|
30
|
+
return;
|
|
31
|
+
refreshing = true;
|
|
32
|
+
try {
|
|
33
|
+
const results = await fetchServicesInParallel(servicesToQuery);
|
|
34
|
+
const successes = [];
|
|
35
|
+
const errors = [];
|
|
36
|
+
for (const { service, result } of results) {
|
|
37
|
+
if (result.ok) {
|
|
38
|
+
successes.push(result.value);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const statusSuffix = result.error.status === undefined
|
|
42
|
+
? ""
|
|
43
|
+
: ` (HTTP ${String(result.error.status)})`;
|
|
44
|
+
errors.push(`${service}: fetch failed${statusSuffix}`);
|
|
45
|
+
console.error(`Warning: Failed to fetch ${service}: ${result.error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
lastRefreshErrors = errors;
|
|
49
|
+
lastRefreshTime = new Date();
|
|
50
|
+
// All services failed → clear cache so /metrics returns 503 instead of
|
|
51
|
+
// serving stale data that could mask outages in Prometheus alerting.
|
|
52
|
+
cachedMetrics =
|
|
53
|
+
successes.length > 0
|
|
54
|
+
? await formatPrometheusMetrics(successes)
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
refreshing = false; // eslint-disable-line require-atomic-updates -- single-threaded guard, no race
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Initial fetch
|
|
62
|
+
console.error(`Fetching initial metrics for: ${servicesToQuery.join(", ")}`);
|
|
63
|
+
await refreshMetrics();
|
|
64
|
+
// Create server
|
|
65
|
+
const healthRouter = createHealthRouter(() => ({
|
|
66
|
+
lastRefreshTime,
|
|
67
|
+
services: servicesToQuery,
|
|
68
|
+
errors: lastRefreshErrors,
|
|
69
|
+
hasMetrics: cachedMetrics !== undefined,
|
|
70
|
+
}));
|
|
71
|
+
const metricsRouter = createMetricsRouter(() => ({
|
|
72
|
+
metrics: cachedMetrics,
|
|
73
|
+
}));
|
|
74
|
+
const server = createServer(config, [healthRouter, metricsRouter]);
|
|
75
|
+
// Graceful shutdown handler — registered before start so signals during
|
|
76
|
+
// startup are handled. process.once ensures at-most-one invocation per signal.
|
|
77
|
+
// Object wrapper lets the shutdown closure reference the interval assigned
|
|
78
|
+
// after server.start(), without needing a reassignable `let`.
|
|
79
|
+
const poll = {
|
|
80
|
+
intervalId: undefined,
|
|
81
|
+
};
|
|
82
|
+
const shutdown = () => {
|
|
83
|
+
console.error("\nShutting down...");
|
|
84
|
+
if (poll.intervalId !== undefined)
|
|
85
|
+
clearInterval(poll.intervalId);
|
|
86
|
+
// Force-exit if server.stop() hangs (e.g. keep-alive connections not closing)
|
|
87
|
+
const forceExit = setTimeout(() => {
|
|
88
|
+
console.error("Shutdown timed out, forcing exit");
|
|
89
|
+
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}, 5000);
|
|
92
|
+
forceExit.unref();
|
|
93
|
+
server.stop().then(() => {
|
|
94
|
+
clearTimeout(forceExit);
|
|
95
|
+
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}, (error) => {
|
|
98
|
+
clearTimeout(forceExit);
|
|
99
|
+
console.error("Error during shutdown:", error);
|
|
100
|
+
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
process.once("SIGTERM", shutdown);
|
|
105
|
+
process.once("SIGINT", shutdown);
|
|
106
|
+
// Start server first — if this throws (e.g. EADDRINUSE), no polling interval
|
|
107
|
+
// is left dangling keeping the process alive.
|
|
108
|
+
await server.start();
|
|
109
|
+
// Start polling only after a successful listen.
|
|
110
|
+
poll.intervalId = setInterval(() => {
|
|
111
|
+
void refreshMetrics().catch((error) => {
|
|
112
|
+
console.error("Unexpected error during metrics refresh:", error);
|
|
113
|
+
});
|
|
114
|
+
}, config.intervalMs);
|
|
115
|
+
console.error(`Polling every ${String(config.intervalMs / 1000)}s for: ${servicesToQuery.join(", ")}`);
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for axusage serve mode.
|
|
3
|
+
*
|
|
4
|
+
* Priority: CLI flags > environment variables > defaults.
|
|
5
|
+
*/
|
|
6
|
+
export type ServeConfig = {
|
|
7
|
+
readonly port: number;
|
|
8
|
+
readonly host: string;
|
|
9
|
+
readonly intervalMs: number;
|
|
10
|
+
readonly service: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
type ServeConfigOverrides = {
|
|
13
|
+
readonly port?: string;
|
|
14
|
+
readonly host?: string;
|
|
15
|
+
readonly interval?: string;
|
|
16
|
+
readonly service?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Parse serve config from environment and CLI overrides */
|
|
19
|
+
export declare function getServeConfig(overrides?: ServeConfigOverrides): ServeConfig;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for axusage serve mode.
|
|
3
|
+
*
|
|
4
|
+
* Priority: CLI flags > environment variables > defaults.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_PORT = 3848;
|
|
7
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
8
|
+
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
|
|
9
|
+
/** Parse serve config from environment and CLI overrides */
|
|
10
|
+
export function getServeConfig(overrides = {}) {
|
|
11
|
+
const port = parsePort(overrides.port ?? process.env.AXUSAGE_PORT);
|
|
12
|
+
const host = overrides.host || process.env.AXUSAGE_HOST || DEFAULT_HOST;
|
|
13
|
+
const intervalSeconds = parsePositiveInt(overrides.interval ?? process.env.AXUSAGE_INTERVAL, DEFAULT_INTERVAL_SECONDS, "AXUSAGE_INTERVAL");
|
|
14
|
+
const service = (overrides.service ?? process.env.AXUSAGE_SERVICE) || undefined;
|
|
15
|
+
return {
|
|
16
|
+
port,
|
|
17
|
+
host,
|
|
18
|
+
intervalMs: intervalSeconds * 1000,
|
|
19
|
+
service,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function parsePort(value) {
|
|
23
|
+
if (!value)
|
|
24
|
+
return DEFAULT_PORT;
|
|
25
|
+
const port = Number(value);
|
|
26
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
27
|
+
throw new Error(`Invalid port: ${value}`);
|
|
28
|
+
}
|
|
29
|
+
return port;
|
|
30
|
+
}
|
|
31
|
+
function parsePositiveInt(value, defaultValue, name) {
|
|
32
|
+
if (!value)
|
|
33
|
+
return defaultValue;
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
36
|
+
console.error(`Invalid ${name} value "${value}", using default ${String(defaultValue)}`);
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for axusage serve mode.
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from "express";
|
|
5
|
+
type HealthStatus = {
|
|
6
|
+
readonly lastRefreshTime: Date | undefined;
|
|
7
|
+
readonly services: readonly string[];
|
|
8
|
+
readonly errors: readonly string[];
|
|
9
|
+
readonly hasMetrics: boolean;
|
|
10
|
+
};
|
|
11
|
+
type MetricsStatus = {
|
|
12
|
+
readonly metrics: string | undefined;
|
|
13
|
+
};
|
|
14
|
+
/** Create router for GET /health */
|
|
15
|
+
export declare function createHealthRouter(getStatus: () => HealthStatus): Router;
|
|
16
|
+
/** Create router for GET /metrics */
|
|
17
|
+
export declare function createMetricsRouter(getMetrics: () => MetricsStatus): Router;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for axusage serve mode.
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from "express";
|
|
5
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
6
|
+
/** Create router for GET /health */
|
|
7
|
+
export function createHealthRouter(getStatus) {
|
|
8
|
+
const router = Router();
|
|
9
|
+
router.get("/health", (_request, response) => {
|
|
10
|
+
const status = getStatus();
|
|
11
|
+
const healthy = status.hasMetrics;
|
|
12
|
+
response.status(healthy ? 200 : 503).json({
|
|
13
|
+
status: healthy ? "ok" : "degraded",
|
|
14
|
+
version: packageJson.version,
|
|
15
|
+
lastRefresh: status.lastRefreshTime?.toISOString(),
|
|
16
|
+
services: status.services,
|
|
17
|
+
errors: status.errors,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return router;
|
|
21
|
+
}
|
|
22
|
+
/** Create router for GET /metrics */
|
|
23
|
+
export function createMetricsRouter(getMetrics) {
|
|
24
|
+
const router = Router();
|
|
25
|
+
router.get("/metrics", (_request, response) => {
|
|
26
|
+
const { metrics } = getMetrics();
|
|
27
|
+
if (!metrics) {
|
|
28
|
+
response.status(503).type("text/plain").send("No data yet\n");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
response
|
|
32
|
+
.status(200)
|
|
33
|
+
.type("text/plain; version=0.0.4; charset=utf-8")
|
|
34
|
+
.send(metrics);
|
|
35
|
+
});
|
|
36
|
+
return router;
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express HTTP server for axusage metrics.
|
|
3
|
+
*
|
|
4
|
+
* Designed for both CLI and library usage via factory pattern.
|
|
5
|
+
*/
|
|
6
|
+
import { type Express, type Router } from "express";
|
|
7
|
+
import type { ServeConfig } from "../config/serve-config.js";
|
|
8
|
+
/** Server instance with lifecycle methods */
|
|
9
|
+
type AxusageServer = {
|
|
10
|
+
/** The Express application instance */
|
|
11
|
+
readonly app: Express;
|
|
12
|
+
/** Start listening on the configured host:port */
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
/** Stop the server gracefully */
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
/** Create an axusage server instance */
|
|
18
|
+
export declare function createServer(config: Pick<ServeConfig, "port" | "host">, routers: Router[]): AxusageServer;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express HTTP server for axusage metrics.
|
|
3
|
+
*
|
|
4
|
+
* Designed for both CLI and library usage via factory pattern.
|
|
5
|
+
*/
|
|
6
|
+
import express, {} from "express";
|
|
7
|
+
/** Create and configure Express application */
|
|
8
|
+
function createApp(routers) {
|
|
9
|
+
const app = express();
|
|
10
|
+
// Security: disable X-Powered-By header
|
|
11
|
+
app.disable("x-powered-by");
|
|
12
|
+
// Register all routers
|
|
13
|
+
for (const router of routers) {
|
|
14
|
+
app.use(router);
|
|
15
|
+
}
|
|
16
|
+
// 404 handler for unmatched routes
|
|
17
|
+
app.use((_request, response) => {
|
|
18
|
+
response.status(404).json({ error: "Not found" });
|
|
19
|
+
});
|
|
20
|
+
return app;
|
|
21
|
+
}
|
|
22
|
+
/** Create an axusage server instance */
|
|
23
|
+
export function createServer(config, routers) {
|
|
24
|
+
const app = createApp(routers);
|
|
25
|
+
let server;
|
|
26
|
+
let stopPromise;
|
|
27
|
+
return {
|
|
28
|
+
app,
|
|
29
|
+
start() {
|
|
30
|
+
if (server) {
|
|
31
|
+
return Promise.reject(new Error("Server is already running. Call stop() before restarting."));
|
|
32
|
+
}
|
|
33
|
+
stopPromise = undefined;
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const onStartupError = (error) => {
|
|
36
|
+
server = undefined;
|
|
37
|
+
reject(error);
|
|
38
|
+
};
|
|
39
|
+
server = app.listen(config.port, config.host, () => {
|
|
40
|
+
server?.removeListener("error", onStartupError);
|
|
41
|
+
server?.on("error", (error) => {
|
|
42
|
+
console.error("Server error:", error);
|
|
43
|
+
});
|
|
44
|
+
console.error(`axusage listening on http://${config.host}:${String(config.port)}`);
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
47
|
+
server.once("error", onStartupError);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
stop() {
|
|
51
|
+
if (stopPromise) {
|
|
52
|
+
return stopPromise;
|
|
53
|
+
}
|
|
54
|
+
if (!server) {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
const serverToClose = server;
|
|
58
|
+
server = undefined;
|
|
59
|
+
stopPromise = new Promise((resolve, reject) => {
|
|
60
|
+
serverToClose.close((error) => {
|
|
61
|
+
if (error) {
|
|
62
|
+
reject(error);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return stopPromise;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "axusage",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.4.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",
|
|
@@ -58,11 +58,13 @@
|
|
|
58
58
|
"commander": "^14.0.3",
|
|
59
59
|
"conf": "^15.1.0",
|
|
60
60
|
"env-paths": "^4.0.0",
|
|
61
|
+
"express": "^5.2.1",
|
|
61
62
|
"prom-client": "^15.1.3",
|
|
62
63
|
"zod": "^4.3.6"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
67
|
+
"@types/express": "^5.0.6",
|
|
66
68
|
"@types/node": "^25.3.0",
|
|
67
69
|
"@vitest/coverage-v8": "^4.0.18",
|
|
68
70
|
"eslint": "^10.0.1",
|