@eik/cli 3.1.69 → 3.2.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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # [3.2.0](https://github.com/eik-lib/cli/compare/v3.1.69...v3.2.0) (2026-08-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add retry to latest-version HTTP utility ([45b3115](https://github.com/eik-lib/cli/commit/45b31152e6b620df0831e9ba6ef054085ba82f98))
7
+ * retry HTTP requests on transient 5xx responses ([f7fa656](https://github.com/eik-lib/cli/commit/f7fa656a7001bacd98b2baf21903b935bbb4b535))
8
+
9
+
10
+ ### Features
11
+
12
+ * make retry count and delay configurable through API and CLI ([b909b81](https://github.com/eik-lib/cli/commit/b909b81f46f59597ad52a80df3bea815e53f8eb7))
13
+
1
14
  ## [3.1.69](https://github.com/eik-lib/cli/compare/v3.1.68...v3.1.69) (2026-08-24)
2
15
 
3
16
 
@@ -3,6 +3,7 @@ import assert from "@eik/common/lib/schemas/assert.js";
3
3
  import ValidationError from "@eik/common/lib/schemas/validation-error.js";
4
4
  import typeSlug from "@eik/common/lib/helpers/type-slug.js";
5
5
  import { joinUrlPathname } from "../utils/url.js";
6
+ import { fetchWithRetry } from "../utils/http/retry.js";
6
7
 
7
8
  /**
8
9
  * @typedef {object} IntegrityOptions
@@ -13,6 +14,8 @@ import { joinUrlPathname } from "../utils/url.js";
13
14
  * @property {string} version
14
15
  * @property {string} [cwd]
15
16
  * @property {boolean} [debug]
17
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
18
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
16
19
  */
17
20
 
18
21
  export default class Integrity {
@@ -28,6 +31,8 @@ export default class Integrity {
28
31
  type,
29
32
  debug = false,
30
33
  cwd = process.cwd(),
34
+ retries,
35
+ retryDelay,
31
36
  }) {
32
37
  this.log = abslog(logger);
33
38
  this.server = server;
@@ -36,6 +41,8 @@ export default class Integrity {
36
41
  this.debug = debug;
37
42
  this.cwd = cwd;
38
43
  this.type = type;
44
+ this.retries = retries;
45
+ this.retryDelay = retryDelay;
39
46
  }
40
47
 
41
48
  async run() {
@@ -80,7 +87,10 @@ export default class Integrity {
80
87
  );
81
88
  this.log.debug(` ==> url: ${url}`);
82
89
 
83
- const res = await fetch(url);
90
+ const res = await fetchWithRetry(() => fetch(url), {
91
+ maxRetries: this.retries !== undefined ? this.retries + 1 : undefined,
92
+ baseDelayMs: this.retryDelay,
93
+ });
84
94
 
85
95
  if (res.ok) {
86
96
  this.log.debug(` ==> ok: true`);
@@ -25,6 +25,8 @@ import Cleanup from "./tasks/cleanup.js";
25
25
  * @property {string[]} [map]
26
26
  * @property {string} [out="./.eik"]
27
27
  * @property {Record<string, string>} files
28
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
29
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
28
30
  */
29
31
 
30
32
  /**
@@ -59,6 +61,8 @@ export default class Publish {
59
61
  map = [],
60
62
  out = "./.eik",
61
63
  files = {},
64
+ retries,
65
+ retryDelay,
62
66
  }) {
63
67
  const config = new EikConfig(
64
68
  {
@@ -106,8 +110,15 @@ export default class Publish {
106
110
  logger: this.log,
107
111
  path: this.path,
108
112
  config,
113
+ retries,
114
+ retryDelay,
115
+ });
116
+ this.uploadFiles = new UploadFiles({
117
+ logger: this.log,
118
+ config,
119
+ retries,
120
+ retryDelay,
109
121
  });
110
- this.uploadFiles = new UploadFiles({ logger: this.log, config });
111
122
  this.saveMetafile = new SaveMetafile({ logger: this.log, cwd, config });
112
123
  this.cleanup = new Cleanup({
113
124
  logger: this.log,
@@ -15,7 +15,12 @@ export default class CheckIfAlreadyPublished extends Task {
15
15
  log.debug(" ==> Fetching package metadata from server");
16
16
 
17
17
  try {
18
- if (await integrity(server, typeSlug(type), name, version)) {
18
+ if (
19
+ await integrity(server, typeSlug(type), name, version, {
20
+ retries: this.retries,
21
+ retryDelay: this.retryDelay,
22
+ })
23
+ ) {
19
24
  throw new Error(
20
25
  `${name} version ${version} already exists on the Eik server. Publishing is not necessary.`,
21
26
  );
@@ -7,5 +7,7 @@ export default class Task {
7
7
  this.log = abslog(options.logger);
8
8
  this.path = options.path;
9
9
  this.config = options.config;
10
+ this.retries = options.retries;
11
+ this.retryDelay = options.retryDelay;
10
12
  }
11
13
  }
@@ -23,6 +23,8 @@ export default class UploadFiles extends Task {
23
23
  pathname,
24
24
  file: zipFile,
25
25
  token,
26
+ retries: this.retries,
27
+ retryDelay: this.retryDelay,
26
28
  });
27
29
 
28
30
  return message;
@@ -23,6 +23,8 @@ import hashCompare from "../utils/hash/compare.js";
23
23
  * @property {string} [out="./.eik"]
24
24
  * @property {string | Record<string, string>} files
25
25
  * @property {string} [configFile="eik.json"]
26
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
27
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
26
28
  */
27
29
 
28
30
  export default class Version {
@@ -41,6 +43,8 @@ export default class Version {
41
43
  out = "./.eik",
42
44
  files,
43
45
  configFile = "eik.json",
46
+ retries,
47
+ retryDelay,
44
48
  }) {
45
49
  const config = new EikConfig(
46
50
  {
@@ -61,6 +65,8 @@ export default class Version {
61
65
  this.configFile = configFile;
62
66
  this.path = isAbsolute(config.out) ? config.out : join(cwd, config.out);
63
67
  this.level = level;
68
+ this.retries = retries;
69
+ this.retryDelay = retryDelay;
64
70
  }
65
71
 
66
72
  /**
@@ -99,7 +105,10 @@ export default class Version {
99
105
 
100
106
  let integrityHash;
101
107
  try {
102
- integrityHash = await integrity(server, typeSlug(type), name, version);
108
+ integrityHash = await integrity(server, typeSlug(type), name, version, {
109
+ retries: this.retries,
110
+ retryDelay: this.retryDelay,
111
+ });
103
112
  } catch (err) {
104
113
  throw new Error(
105
114
  `Unable to fetch package metadata from server: ${/** @type {any} */ (err).message}`,
@@ -17,6 +17,11 @@ export const builder = (yargs) => {
17
17
  alias: "s",
18
18
  describe: "Eik server address, if different from configuration file",
19
19
  },
20
+ retries: {
21
+ describe:
22
+ "Number of retry attempts on transient server errors. Each retry waits longer than the last (500 ms, 1000 ms, …). Default: 2 retries (3 total attempts). Set to 0 to disable.",
23
+ type: "number",
24
+ },
20
25
  })
21
26
  .example("eik integrity")
22
27
  .example("eik integrity --server https://assets.myserver.com");
@@ -25,7 +30,7 @@ export const builder = (yargs) => {
25
30
  export const handler = commandHandler(
26
31
  { command, options: ["server"] },
27
32
  async (argv, log, spinner) => {
28
- const { name, version, server, out, type, cwd, debug } = argv;
33
+ const { name, version, server, out, type, cwd, debug, retries } = argv;
29
34
 
30
35
  const integrity = await new Integrity({
31
36
  logger: log,
@@ -35,6 +40,7 @@ export const handler = commandHandler(
35
40
  debug,
36
41
  cwd,
37
42
  type,
43
+ retries,
38
44
  }).run();
39
45
 
40
46
  if (integrity) {
@@ -27,6 +27,11 @@ export const builder = (yargs) => {
27
27
  type: "string",
28
28
  alias: "t",
29
29
  },
30
+ retries: {
31
+ describe:
32
+ "Number of retry attempts on transient server errors. Each retry waits longer than the last (500 ms, 1000 ms, …). Default: 2 retries (3 total attempts). Set to 0 to disable.",
33
+ type: "number",
34
+ },
30
35
  })
31
36
  .example("eik publish")
32
37
  .example("eik publish --dry-run")
@@ -49,6 +54,7 @@ export const handler = commandHandler(
49
54
  files,
50
55
  type,
51
56
  configFile,
57
+ retries,
52
58
  } = argv;
53
59
 
54
60
  if (type === "map") {
@@ -71,6 +77,7 @@ export const handler = commandHandler(
71
77
  map,
72
78
  out,
73
79
  files,
80
+ retries,
74
81
  };
75
82
 
76
83
  const publish = await new PublishPackage(options).run();
@@ -25,6 +25,11 @@ export const builder = (yargs) => {
25
25
  describe: "Log details about the operation and skip upload",
26
26
  type: "boolean",
27
27
  },
28
+ retries: {
29
+ describe:
30
+ "Number of retry attempts on transient server errors. Each retry waits longer than the last (500 ms, 1000 ms, …). Default: 2 retries (3 total attempts). Set to 0 to disable.",
31
+ type: "number",
32
+ },
28
33
  })
29
34
  .example("eik version")
30
35
  .example("eik version minor")
@@ -46,6 +51,7 @@ export const handler = commandHandler(
46
51
  type,
47
52
  files,
48
53
  configFile,
54
+ retries,
49
55
  } = argv;
50
56
 
51
57
  const options = {
@@ -60,6 +66,7 @@ export const handler = commandHandler(
60
66
  out,
61
67
  files,
62
68
  configFile,
69
+ retries,
63
70
  };
64
71
 
65
72
  const newVersion = await new VersionPackage(options).run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eik/cli",
3
- "version": "3.1.69",
3
+ "version": "3.2.0",
4
4
  "description": "CLI tool for publishing assets to an Eik server",
5
5
  "main": "./classes/index.js",
6
6
  "types": "./types/classes/index.d.ts",
@@ -7,6 +7,14 @@ export type IntegrityOptions = {
7
7
  version: string;
8
8
  cwd?: string;
9
9
  debug?: boolean;
10
+ /**
11
+ * - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
12
+ */
13
+ retries?: number;
14
+ /**
15
+ * - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
16
+ */
17
+ retryDelay?: number;
10
18
  };
11
19
  /**
12
20
  * @typedef {object} IntegrityOptions
@@ -17,6 +25,8 @@ export type IntegrityOptions = {
17
25
  * @property {string} version
18
26
  * @property {string} [cwd]
19
27
  * @property {boolean} [debug]
28
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
29
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
20
30
  */
21
31
  export default class Integrity {
22
32
  log: abslog.ValidLogger;
@@ -26,10 +36,12 @@ export default class Integrity {
26
36
  debug: boolean;
27
37
  cwd: string;
28
38
  type: "map" | "npm" | "package" | undefined;
39
+ retries: number | undefined;
40
+ retryDelay: number | undefined;
29
41
  /**
30
42
  *
31
43
  * @param {IntegrityOptions} options
32
44
  */
33
- constructor({ logger, name, version, server, type, debug, cwd, }: IntegrityOptions);
45
+ constructor({ logger, name, version, server, type, debug, cwd, retries, retryDelay, }: IntegrityOptions);
34
46
  run(): Promise<any>;
35
47
  }
@@ -21,6 +21,14 @@ export type PublishOptions = {
21
21
  map?: string[];
22
22
  out?: string;
23
23
  files: Record<string, string>;
24
+ /**
25
+ * - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
26
+ */
27
+ retries?: number;
28
+ /**
29
+ * - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
30
+ */
31
+ retryDelay?: number;
24
32
  };
25
33
  export type PublishResult = {
26
34
  type: string;
@@ -52,6 +60,8 @@ export type PublishResult = {
52
60
  * @property {string[]} [map]
53
61
  * @property {string} [out="./.eik"]
54
62
  * @property {Record<string, string>} files
63
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
64
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
55
65
  */
56
66
  /**
57
67
  * @typedef {object} PublishResult
@@ -86,7 +96,7 @@ export default class Publish {
86
96
  /**
87
97
  * @param {PublishOptions} options
88
98
  */
89
- constructor({ logger, cwd, token, dryRun, server, type, name, version, map, out, files, }: PublishOptions);
99
+ constructor({ logger, cwd, token, dryRun, server, type, name, version, map, out, files, retries, retryDelay, }: PublishOptions);
90
100
  /**
91
101
  * @returns {Promise<PublishResult>}
92
102
  */
@@ -4,6 +4,8 @@ export default class Task {
4
4
  log: abslog.ValidLogger;
5
5
  path: any;
6
6
  config: any;
7
+ retries: any;
8
+ retryDelay: any;
7
9
  /** @param {any} options */
8
10
  constructor(options: any);
9
11
  }
@@ -11,6 +11,14 @@ export type VersionOptions = {
11
11
  out?: string;
12
12
  files: string | Record<string, string>;
13
13
  configFile?: string;
14
+ /**
15
+ * - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
16
+ */
17
+ retries?: number;
18
+ /**
19
+ * - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
20
+ */
21
+ retryDelay?: number;
14
22
  };
15
23
  /**
16
24
  * @typedef {object} VersionOptions
@@ -25,6 +33,8 @@ export type VersionOptions = {
25
33
  * @property {string} [out="./.eik"]
26
34
  * @property {string | Record<string, string>} files
27
35
  * @property {string} [configFile="eik.json"]
36
+ * @property {number} [retries=2] - Number of retry attempts on transient 5xx/network errors (0 to disable). Default 2 retries = 3 total attempts
37
+ * @property {number} [retryDelay=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
28
38
  */
29
39
  export default class Version {
30
40
  log: abslog.ValidLogger;
@@ -32,10 +42,12 @@ export default class Version {
32
42
  configFile: string;
33
43
  path: any;
34
44
  level: import("semver").ReleaseType;
45
+ retries: number | undefined;
46
+ retryDelay: number | undefined;
35
47
  /**
36
48
  * @param {VersionOptions} options
37
49
  */
38
- constructor({ logger, server, type, name, version, level, cwd, map, out, files, configFile, }: VersionOptions);
50
+ constructor({ logger, server, type, name, version, level, cwd, map, out, files, configFile, retries, retryDelay, }: VersionOptions);
39
51
  /**
40
52
  * Similar to `npm version`, but updates `eik.json`
41
53
  * @returns {Promise<string | null>} The new version number, or null if the versioning failed
@@ -11,4 +11,14 @@ export default _default;
11
11
  *
12
12
  * @throws Error
13
13
  */
14
- declare function _default(server: string, type: string, name: string, version: string): Promise<string | null>;
14
+ /**
15
+ * @param {string} server
16
+ * @param {string} type
17
+ * @param {string} name
18
+ * @param {string} version
19
+ * @param {{ retries?: number, retryDelay?: number }} [options]
20
+ */
21
+ declare function _default(server: string, type: string, name: string, version: string, { retries, retryDelay }?: {
22
+ retries?: number;
23
+ retryDelay?: number;
24
+ }): Promise<any>;
@@ -6,6 +6,8 @@ export type RequestOptions = {
6
6
  file?: string;
7
7
  map?: string;
8
8
  token?: string;
9
+ retries?: number;
10
+ retryDelay?: number;
9
11
  };
10
12
  /**
11
13
  * @typedef {object} RequestOptions
@@ -16,6 +18,8 @@ export type RequestOptions = {
16
18
  * @property {string} [file]
17
19
  * @property {string} [map]
18
20
  * @property {string} [token]
21
+ * @property {number} [retries]
22
+ * @property {number} [retryDelay]
19
23
  */
20
24
  /**
21
25
  * HTTP Utility for making requests against an Eik server
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Wraps a fetch call with retry logic for transient server errors (5xx).
3
+ * 404 responses are returned immediately without retry — they indicate the
4
+ * resource does not exist, not a transient failure.
5
+ *
6
+ * @param {() => Promise<Response>} fn - Function that performs the fetch call
7
+ * @param {object} [options]
8
+ * @param {number} [options.maxRetries=3] - Total number of attempts (default 3 = 1 initial + 2 retries). Pass 1 to make exactly one attempt with no retry.
9
+ * @param {number} [options.baseDelayMs=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
10
+ * @returns {Promise<Response>}
11
+ */
12
+ export declare function fetchWithRetry(fn: () => Promise<Response>, { maxRetries, baseDelayMs }?: {
13
+ maxRetries?: number;
14
+ baseDelayMs?: number;
15
+ }): Promise<Response>;
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { fetchWithRetry } from "./retry.js";
2
3
 
3
4
  /**
4
5
  * Fetches package integrity string by name and version from a given Eik asset server.
@@ -12,11 +13,27 @@ import { join } from "node:path";
12
13
  *
13
14
  * @throws Error
14
15
  */
15
- export default async (server, type, name, version) => {
16
+ /**
17
+ * @param {string} server
18
+ * @param {string} type
19
+ * @param {string} name
20
+ * @param {string} version
21
+ * @param {{ retries?: number, retryDelay?: number }} [options]
22
+ */
23
+ export default async (
24
+ server,
25
+ type,
26
+ name,
27
+ version,
28
+ { retries, retryDelay } = {},
29
+ ) => {
16
30
  const url = new URL(join(type, name, version), server);
17
31
  url.search = `?t=${Date.now()}`;
18
32
 
19
- const res = await fetch(url);
33
+ const res = await fetchWithRetry(() => fetch(url), {
34
+ maxRetries: retries !== undefined ? retries + 1 : undefined,
35
+ baseDelayMs: retryDelay,
36
+ });
20
37
 
21
38
  if (!res.ok) {
22
39
  if (res.status === 404) {
@@ -1,4 +1,5 @@
1
1
  import { join } from "path";
2
+ import { fetchWithRetry } from "./retry.js";
2
3
 
3
4
  /**
4
5
  * Fetches the latest version from an Eik server of a package by name, optionally restricting the lookup to a specified semver major version
@@ -11,9 +12,21 @@ import { join } from "path";
11
12
  *
12
13
  * @throws Error
13
14
  */
14
- export default async (server, type, name, major) => {
15
+ export default async (
16
+ server,
17
+ type,
18
+ name,
19
+ major,
20
+ /** @type {{ retries?: number, retryDelay?: number }} */ {
21
+ retries,
22
+ retryDelay,
23
+ } = {},
24
+ ) => {
15
25
  const url = new URL(`${join(type, name)}?t=${Date.now()}`, server);
16
- const res = await fetch(url);
26
+ const res = await fetchWithRetry(() => fetch(url), {
27
+ maxRetries: retries !== undefined ? retries + 1 : undefined,
28
+ baseDelayMs: retryDelay,
29
+ });
17
30
  if (!res.ok) {
18
31
  if (res.status === 404) {
19
32
  return null;
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { fetchWithRetry } from "./retry.js";
2
3
 
3
4
  /**
4
5
  * @typedef {object} RequestOptions
@@ -9,6 +10,8 @@ import { readFile } from "node:fs/promises";
9
10
  * @property {string} [file]
10
11
  * @property {string} [map]
11
12
  * @property {string} [token]
13
+ * @property {number} [retries]
14
+ * @property {number} [retryDelay]
12
15
  */
13
16
 
14
17
  /**
@@ -21,7 +24,17 @@ import { readFile } from "node:fs/promises";
21
24
  * @throws Error
22
25
  */
23
26
  async function request(options) {
24
- const { method = "POST", host, pathname, data, file, map, token } = options;
27
+ const {
28
+ method = "POST",
29
+ host,
30
+ pathname,
31
+ data,
32
+ file,
33
+ map,
34
+ token,
35
+ retries,
36
+ retryDelay,
37
+ } = options;
25
38
  const body = new FormData();
26
39
  const headers = new Headers();
27
40
 
@@ -49,7 +62,13 @@ async function request(options) {
49
62
  const url = new URL(pathname, host);
50
63
  url.search = `?t=${Date.now()}`;
51
64
 
52
- const res = await fetch(url, { method, body, headers });
65
+ const res = await fetchWithRetry(
66
+ () => fetch(url, { method, body, headers }),
67
+ {
68
+ maxRetries: retries !== undefined ? retries + 1 : undefined,
69
+ baseDelayMs: retryDelay,
70
+ },
71
+ );
53
72
 
54
73
  if (!res.ok) {
55
74
  const err = new Error(
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Wraps a fetch call with retry logic for transient server errors (5xx).
3
+ * 404 responses are returned immediately without retry — they indicate the
4
+ * resource does not exist, not a transient failure.
5
+ *
6
+ * @param {() => Promise<Response>} fn - Function that performs the fetch call
7
+ * @param {object} [options]
8
+ * @param {number} [options.maxRetries=3] - Total number of attempts (default 3 = 1 initial + 2 retries). Pass 1 to make exactly one attempt with no retry.
9
+ * @param {number} [options.baseDelayMs=500] - Base delay in ms between retries; doubles each attempt (500 ms, 1000 ms, …)
10
+ * @returns {Promise<Response>}
11
+ */
12
+ export async function fetchWithRetry(
13
+ fn,
14
+ { maxRetries = 3, baseDelayMs = 500 } = {},
15
+ ) {
16
+ let lastErr;
17
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
18
+ let res;
19
+ try {
20
+ res = await fn();
21
+ } catch (err) {
22
+ // Network-level failure (ECONNRESET, ENOTFOUND, etc.)
23
+ lastErr = err;
24
+ if (attempt < maxRetries) {
25
+ await delay(baseDelayMs * attempt);
26
+ continue;
27
+ }
28
+ throw err;
29
+ }
30
+
31
+ // 5xx — transient server error, retry
32
+ if (res.status >= 500 && attempt < maxRetries) {
33
+ await delay(baseDelayMs * attempt);
34
+ continue;
35
+ }
36
+
37
+ return res;
38
+ }
39
+ throw lastErr;
40
+ }
41
+
42
+ /** @param {number} ms */
43
+ function delay(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }