@configdirector/cli 0.18.0 → 0.18.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.18.1] - 2026-11-03
8
+
9
+ ### Fixed
10
+
11
+ - Commands no longer fail with `Could not reach the Admin API` when opening a connection takes longer than a quarter of a second, as it sometimes does on GitHub-hosted runners, which find the Admin API's IPv6 addresses but cannot reach them. Each of the Admin API's addresses now gets two seconds to connect before the CLI tries the next.
12
+ - When none of the Admin API's addresses can be reached, `Could not reach the Admin API` says why each one failed, where it showed empty brackets.
13
+ - Commands no longer fail the first time the network drops a connection to the Admin API. When a connection cannot be opened, the CLI tries again up to twice, half a second and then a second later. When the network breaks a connection after a request went out, it tries again only for requests that read, replace or delete; the others, such as `configs create`, still exit with 2, because ConfigDirector may already have received them.
14
+
7
15
  ## [0.18.0] - 2026-11-03
8
16
 
9
17
  ### Changed
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { setDefaultAutoSelectFamilyAttemptTimeout } from "node:net";
2
3
  import { homedir } from "node:os";
3
4
  import { Command, CommanderError, Option } from "commander";
5
+ import { setTimeout } from "node:timers/promises";
4
6
  import * as v from "valibot";
5
7
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
8
  import { mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
@@ -12,7 +14,7 @@ import { promisify } from "node:util";
12
14
  import { randomUUID } from "node:crypto";
13
15
  import { text } from "node:stream/consumers";
14
16
  //#region package.json
15
- var version = "0.18.0";
17
+ var version = "0.18.1";
16
18
  //#endregion
17
19
  //#region src/CliError.ts
18
20
  var CliError = class extends Error {
@@ -84,9 +86,11 @@ var ServiceSuspendedError = class extends CliError {
84
86
  super(`Your organization's service is suspended, so ConfigDirector refuses Admin API requests. The dashboard's billing page shows why and how to reinstate it. Retry after ${retryAfterSeconds} seconds.`);
85
87
  }
86
88
  };
89
+ const messageOf = (failure) => failure instanceof Error ? failure.message : String(failure);
90
+ const connectionFailureReasonOf = (failure) => failure instanceof AggregateError ? failure.errors.map(messageOf).join("; ") : failure.message;
87
91
  var ApiUnreachableError = class extends CliError {
88
92
  constructor(baseUrl, cause) {
89
- const reason = cause instanceof Error && cause.cause instanceof Error ? cause.cause.message : String(cause);
93
+ const reason = cause instanceof Error && cause.cause instanceof Error ? connectionFailureReasonOf(cause.cause) : String(cause);
90
94
  super(`Could not reach the Admin API at ${baseUrl} (${reason}). Check the network connection, --base-url or CONFIGDIRECTOR_BASE_URL.`);
91
95
  }
92
96
  };
@@ -127,6 +131,40 @@ const CreatedConfigSchema = v.object({
127
131
  server: v.boolean()
128
132
  });
129
133
  //#endregion
134
+ //#region src/admin-api/connection-failure-retry.ts
135
+ const millisecondsBeforeEachRetry = [500, 1e3];
136
+ const repeatableMethods = new Set([
137
+ "GET",
138
+ "PUT",
139
+ "DELETE"
140
+ ]);
141
+ const retriesAfter = {
142
+ "never-opened": () => true,
143
+ dropped: (method) => repeatableMethods.has(method),
144
+ "not-transient": () => false
145
+ };
146
+ const neverOpenedCodes = new Set(["UND_ERR_CONNECT_TIMEOUT", "EAI_AGAIN"]);
147
+ const droppedCodes = new Set([
148
+ "ECONNRESET",
149
+ "EPIPE",
150
+ "UND_ERR_SOCKET"
151
+ ]);
152
+ const connectionFailureOf = (failure) => {
153
+ const cause = failure instanceof Error ? failure.cause : void 0;
154
+ if (cause instanceof AggregateError) return "never-opened";
155
+ if (!(cause instanceof Error)) return "not-transient";
156
+ const { code = "", syscall } = cause;
157
+ if (syscall === "connect" || neverOpenedCodes.has(code)) return "never-opened";
158
+ return droppedCodes.has(code) ? "dropped" : "not-transient";
159
+ };
160
+ const retryDecisionAfter = (method, failure, attemptsMade) => {
161
+ const waitMilliseconds = millisecondsBeforeEachRetry[attemptsMade - 1];
162
+ return waitMilliseconds !== void 0 && retriesAfter[connectionFailureOf(failure)](method) ? {
163
+ decision: "retry",
164
+ waitMilliseconds
165
+ } : { decision: "give-up" };
166
+ };
167
+ //#endregion
130
168
  //#region src/admin-api/targeting-rules.ts
131
169
  const RuleValueSchema = v.union([
132
170
  v.string(),
@@ -619,20 +657,25 @@ var AdminApiClient = class {
619
657
  "User-Agent": `configdirector-cli/${version}`,
620
658
  "X-SourceClient": "cli"
621
659
  };
622
- try {
623
- return await fetch(url, "body" in request ? {
624
- method: request.method,
625
- headers: {
626
- ...headers,
627
- "Content-Type": "application/json"
628
- },
629
- body: JSON.stringify(request.body)
630
- } : {
631
- method: request.method,
632
- headers
633
- });
634
- } catch (error) {
635
- throw new ApiUnreachableError(this.connection.baseUrl, error);
660
+ const requestInit = "body" in request ? {
661
+ method: request.method,
662
+ headers: {
663
+ ...headers,
664
+ "Content-Type": "application/json"
665
+ },
666
+ body: JSON.stringify(request.body)
667
+ } : {
668
+ method: request.method,
669
+ headers
670
+ };
671
+ for (let attemptsMade = 1;; attemptsMade += 1) try {
672
+ return await fetch(url, requestInit);
673
+ } catch (failure) {
674
+ const retry = retryDecisionAfter(request.method, failure, attemptsMade);
675
+ switch (retry.decision) {
676
+ case "give-up": throw new ApiUnreachableError(this.connection.baseUrl, failure);
677
+ case "retry": await setTimeout(retry.waitMilliseconds);
678
+ }
636
679
  }
637
680
  }
638
681
  };
@@ -5243,6 +5286,7 @@ const run = async (invocation) => {
5243
5286
  };
5244
5287
  //#endregion
5245
5288
  //#region src/main.ts
5289
+ setDefaultAutoSelectFamilyAttemptTimeout(2e3);
5246
5290
  process.exitCode = await run({
5247
5291
  commandLineArguments: process.argv.slice(2),
5248
5292
  environmentVariables: process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/cli",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Command line tool for ConfigDirector, the managed configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",