@voxli/cli 0.5.1 → 0.6.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/dist/cli.js CHANGED
@@ -20,5 +20,6 @@ program
20
20
  .command("listen")
21
21
  .description("Poll for pending test work and run it locally")
22
22
  .requiredOption("--command <cmd>", "Shell command to run per batch")
23
+ .option("--name <name>", "Display name for this agent (defaults to hostname)")
23
24
  .action(listenCommand);
24
25
  program.parse();
@@ -1,3 +1,4 @@
1
1
  export declare function listenCommand(options: {
2
2
  command: string;
3
+ name?: string;
3
4
  }): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { spawn } from "node:child_process";
3
3
  import { resolveApiKey, resolveConfig, attemptTokenRefresh, } from "../lib/config.js";
4
- import { getStableHostname } from "../lib/hostname.js";
4
+ import { buildAgentIdentifier, getStableHostname } from "../lib/hostname.js";
5
5
  import { register, ApiError } from "../lib/api.js";
6
6
  const POLL_INTERVAL = 5_000;
7
7
  export async function listenCommand(options) {
@@ -24,7 +24,8 @@ export async function listenCommand(options) {
24
24
  console.error("Error: No API key found. Set VOXLI_API_TOKEN or run `voxli auth`.");
25
25
  process.exit(1);
26
26
  }
27
- const hostname = getStableHostname();
27
+ const displayName = options.name ?? getStableHostname();
28
+ const uniqueIdentifier = buildAgentIdentifier(options.name);
28
29
  const children = new Set();
29
30
  // Graceful shutdown
30
31
  const shutdown = () => {
@@ -36,12 +37,12 @@ export async function listenCommand(options) {
36
37
  };
37
38
  process.on("SIGINT", shutdown);
38
39
  process.on("SIGTERM", shutdown);
39
- console.log(`Listening as ${hostname} using credentials from ${credentialSource}`);
40
+ console.log(`Listening as ${displayName} (${uniqueIdentifier}) using credentials from ${credentialSource}`);
40
41
  while (true) {
41
42
  try {
42
43
  const data = await register(apiKey, {
43
- name: hostname,
44
- unique_identifier: hostname,
44
+ name: displayName,
45
+ unique_identifier: uniqueIdentifier,
45
46
  });
46
47
  const testResultIds = data.test_result_ids ?? [];
47
48
  if (testResultIds.length > 0) {
@@ -86,7 +87,7 @@ export async function listenCommand(options) {
86
87
  process.exit(1);
87
88
  }
88
89
  console.log("Access token expired, attempting refresh...");
89
- const newToken = await attemptTokenRefresh();
90
+ const newToken = await attemptTokenRefresh(apiKey ?? undefined);
90
91
  if (newToken) {
91
92
  apiKey = newToken;
92
93
  console.log("Token refreshed successfully.");
@@ -23,4 +23,4 @@ export declare function writeConfig(config: {
23
23
  }): Promise<string>;
24
24
  export declare function resolveApiKey(): string | null;
25
25
  export declare function resolveApiKeyAsync(): Promise<string | null>;
26
- export declare function attemptTokenRefresh(): Promise<string | null>;
26
+ export declare function attemptTokenRefresh(expiredToken?: string): Promise<string | null>;
@@ -98,22 +98,45 @@ export async function resolveApiKeyAsync() {
98
98
  const config = resolved?.config;
99
99
  return config?.accessToken ?? config?.apiKey ?? null;
100
100
  }
101
- export async function attemptTokenRefresh() {
101
+ export async function attemptTokenRefresh(expiredToken) {
102
102
  try {
103
103
  const resolved = await resolveConfig();
104
104
  if (!resolved)
105
105
  return null;
106
106
  const { config, configDir } = resolved;
107
+ // Another listener may have already refreshed — adopt that token.
108
+ if (expiredToken &&
109
+ config.accessToken &&
110
+ config.accessToken !== expiredToken) {
111
+ return config.accessToken;
112
+ }
107
113
  if (!config.refreshToken || !config.clientId)
108
114
  return null;
109
115
  const baseUrl = getApiBaseUrl();
110
- const result = await refreshAccessToken(baseUrl, config.refreshToken, config.clientId);
111
- await writeConfig({
112
- accessToken: result.accessToken,
113
- refreshToken: result.refreshToken ?? config.refreshToken,
114
- clientId: config.clientId,
115
- }, { configDir });
116
- return result.accessToken;
116
+ try {
117
+ const result = await refreshAccessToken(baseUrl, config.refreshToken, config.clientId);
118
+ await writeConfig({
119
+ accessToken: result.accessToken,
120
+ refreshToken: result.refreshToken ?? config.refreshToken,
121
+ clientId: config.clientId,
122
+ }, { configDir });
123
+ return result.accessToken;
124
+ }
125
+ catch {
126
+ // Refresh failed (likely because a sibling listener already used the
127
+ // refresh token). Retry-read the config briefly in case the winner is
128
+ // still flushing its write to disk.
129
+ if (!expiredToken)
130
+ return null;
131
+ for (let i = 0; i < 5; i++) {
132
+ await new Promise((r) => setTimeout(r, 100));
133
+ const recheck = await resolveConfig();
134
+ const fresh = recheck?.config.accessToken;
135
+ if (fresh && fresh !== expiredToken)
136
+ return fresh;
137
+ }
138
+ return null;
139
+ }
117
140
  }
118
141
  catch {
119
142
  return null;
@@ -1 +1,2 @@
1
1
  export declare function getStableHostname(): string;
2
+ export declare function buildAgentIdentifier(name: string | undefined): string;
@@ -14,3 +14,10 @@ export function getStableHostname() {
14
14
  }
15
15
  return hostname();
16
16
  }
17
+ export function buildAgentIdentifier(name) {
18
+ const host = getStableHostname();
19
+ if (!name)
20
+ return host;
21
+ const trimmed = name.replace(/\s+/g, "");
22
+ return trimmed ? `${trimmed}-${host}` : host;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxli/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "CLI agent for running Voxli test scenarios locally",
5
5
  "type": "module",
6
6
  "bin": {