@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 +8 -0
- package/dist/configdirector.mjs +60 -16
- package/package.json +1 -1
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
|
package/dist/configdirector.mjs
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.
|
|
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
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
headers
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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