@tolinax/ayoune-cli 2026.10.0 → 2026.10.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.
@@ -1,5 +1,25 @@
1
- import { http } from "@tolinax/ayoune-core/lib/http/index.js";
1
+ // HTTP client for the `ay` CLI.
2
+ //
3
+ // Uses Node's built-in `fetch` (Node ≥ 18) — NO axios, NO @tolinax/ayoune-core
4
+ // HTTP wrapper. The `core/lib/http` re-export was previously used here, but
5
+ // it transitively requires `core/lib/aYOUne` at module top-level which loads
6
+ // the entire backend framework: mongoose schemas, prom-client metric
7
+ // registries, and an ioredis client wired to env vars that don't exist on
8
+ // end-user machines (causing visible "ERR_SOCKET_BAD_PORT" errors and a
9
+ // duplicate-index warning). The CLI is a pure HTTP client — it has no
10
+ // business loading any of that.
11
+ //
12
+ // The exported `api({...})` and `audit({...})` functions preserve the
13
+ // axios-shaped contract that callers expect:
14
+ //
15
+ // const res = await api({ baseURL, method, url, data, params, headers });
16
+ // res.status / res.statusText / res.data / res.headers
17
+ //
18
+ // Errors throw with `.response = { status, statusText, data, headers }` and
19
+ // `.config = { baseURL, url, method }` so `handleAPIError` keeps working
20
+ // unchanged.
2
21
  import chalk from "chalk";
22
+ import { Readable } from "stream";
3
23
  import { config } from "../helpers/config.js";
4
24
  let debugEnabled = false;
5
25
  export function enableDebug() {
@@ -22,57 +42,211 @@ const MODULE_HOST_OVERRIDES = {
22
42
  export function getModuleBaseUrl(module) {
23
43
  return MODULE_HOST_OVERRIDES[module] || `https://${module}-api.ayoune.app`;
24
44
  }
25
- const retryConfig = {
26
- retries: 3,
27
- retryDelay: (retryCount, error) => {
28
- var _a, _b;
29
- // Respect Retry-After header for 429 responses
30
- const retryAfter = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b["retry-after"];
31
- if (retryAfter) {
32
- const seconds = parseInt(retryAfter, 10);
33
- if (!isNaN(seconds)) {
34
- debugLog(chalk.yellow(`Rate limited. Retrying in ${seconds}s...`));
35
- return seconds * 1000;
45
+ const DEFAULT_TIMEOUT = 30000;
46
+ const MAX_RETRIES = 3;
47
+ /**
48
+ * Build the full request URL from baseURL + url + serialized params.
49
+ * Handles the same edge cases as axios: collapses duplicate slashes,
50
+ * preserves the protocol's `://`, and skips falsy params.
51
+ */
52
+ function buildUrl(req) {
53
+ const base = req.baseURL || "";
54
+ const path = req.url || "";
55
+ let combined = `${base}/${path}`.replace(/\/+/g, "/").replace(":/", "://");
56
+ if (req.params && typeof req.params === "object") {
57
+ const usp = new URLSearchParams();
58
+ for (const [k, v] of Object.entries(req.params)) {
59
+ if (v === undefined || v === null)
60
+ continue;
61
+ // Booleans / numbers stringify cleanly; arrays get repeated keys; objects get JSON.
62
+ if (Array.isArray(v)) {
63
+ v.forEach((item) => usp.append(k, String(item)));
64
+ }
65
+ else if (typeof v === "object") {
66
+ usp.append(k, JSON.stringify(v));
67
+ }
68
+ else {
69
+ usp.append(k, String(v));
36
70
  }
37
71
  }
38
- // Exponential backoff: 1s, 2s, 4s
39
- const delay = Math.pow(2, retryCount - 1) * 1000;
40
- debugLog(chalk.yellow(`Retrying in ${delay / 1000}s (attempt ${retryCount}/3)...`));
41
- return delay;
42
- },
43
- retryCondition: (error) => {
44
- var _a;
45
- // Retry on network errors, 5xx, and 429 (rate limit)
46
- const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
47
- if (!status)
48
- return true; // network error
49
- return status >= 500 || status === 429;
50
- },
51
- };
72
+ const qs = usp.toString();
73
+ if (qs)
74
+ combined += (combined.includes("?") ? "&" : "?") + qs;
75
+ }
76
+ return combined;
77
+ }
78
+ /** Build a "configured" request — used by api()/audit() to apply defaults. */
79
+ function withDefaults(defaults, req) {
80
+ return {
81
+ ...defaults,
82
+ ...req,
83
+ headers: { ...(defaults.headers || {}), ...(req.headers || {}) },
84
+ };
85
+ }
86
+ /** Sleep for n ms — used by retry backoff. */
87
+ function sleep(ms) {
88
+ return new Promise((resolve) => setTimeout(resolve, ms));
89
+ }
90
+ /** Decide whether to retry given the last error. */
91
+ function shouldRetry(err) {
92
+ // No response → network or timeout error → retry.
93
+ if (!err.response)
94
+ return true;
95
+ const status = err.response.status;
96
+ return status >= 500 || status === 429;
97
+ }
98
+ /** Compute backoff in ms, honouring Retry-After on 429. */
99
+ function backoffMs(attempt, err) {
100
+ var _a, _b, _c;
101
+ const status = (_a = err.response) === null || _a === void 0 ? void 0 : _a.status;
102
+ const retryAfter = (_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.headers) === null || _c === void 0 ? void 0 : _c["retry-after"];
103
+ if (status === 429 && retryAfter) {
104
+ const seconds = parseInt(retryAfter, 10);
105
+ if (!Number.isNaN(seconds)) {
106
+ debugLog(chalk.yellow(`Rate limited. Retrying in ${seconds}s...`));
107
+ return seconds * 1000;
108
+ }
109
+ }
110
+ // Exponential: 1s, 2s, 4s
111
+ const delay = Math.pow(2, attempt) * 1000;
112
+ debugLog(chalk.yellow(`Retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`));
113
+ return delay;
114
+ }
115
+ /**
116
+ * Headers#entries() returned by undici returns a flat record. Normalize to
117
+ * a plain `Record<string, string>` so callers (and handleAPIError) can index
118
+ * by header name without thinking about Headers semantics.
119
+ */
120
+ function headersToObject(headers) {
121
+ const out = {};
122
+ headers.forEach((value, key) => {
123
+ out[key.toLowerCase()] = value;
124
+ });
125
+ return out;
126
+ }
127
+ /**
128
+ * Single-shot fetch — no retry. Throws axios-shaped errors so the existing
129
+ * `handleAPIError` works unchanged.
130
+ */
131
+ async function performRequest(req) {
132
+ var _a;
133
+ const method = (req.method || "GET").toUpperCase();
134
+ const url = buildUrl(req);
135
+ const headers = { ...(req.headers || {}) };
136
+ let body;
137
+ if (req.data !== undefined && req.data !== null && method !== "GET" && method !== "HEAD") {
138
+ body = typeof req.data === "string" ? req.data : JSON.stringify(req.data);
139
+ if (!headers["Content-Type"] && !headers["content-type"]) {
140
+ headers["Content-Type"] = "application/json";
141
+ }
142
+ }
143
+ const timeout = (_a = req.timeout) !== null && _a !== void 0 ? _a : DEFAULT_TIMEOUT;
144
+ const controller = new AbortController();
145
+ const timer = setTimeout(() => controller.abort(), timeout);
146
+ let response;
147
+ try {
148
+ response = await fetch(url, {
149
+ method,
150
+ headers,
151
+ body,
152
+ signal: controller.signal,
153
+ });
154
+ }
155
+ catch (e) {
156
+ clearTimeout(timer);
157
+ const err = new Error(e.name === "AbortError" ? `Request timed out after ${timeout}ms` : e.message);
158
+ err.code = e.code;
159
+ err.config = { baseURL: req.baseURL, url: req.url, method };
160
+ throw err;
161
+ }
162
+ clearTimeout(timer);
163
+ // Stream mode: hand back a Node Readable wrapping the fetch body. Used by
164
+ // searchClient.searchGlobal() for the SSE endpoint. The caller .on('data')s
165
+ // it directly, no JSON parsing.
166
+ let data = undefined;
167
+ if (req.responseType === "stream" && response.body) {
168
+ // Readable.fromWeb is available since Node 18.
169
+ data = Readable.fromWeb(response.body);
170
+ }
171
+ else {
172
+ // Parse body — JSON if Content-Type says so, else text. Empty body → undefined.
173
+ const ct = response.headers.get("content-type") || "";
174
+ const text = await response.text();
175
+ if (text.length > 0) {
176
+ if (ct.includes("application/json")) {
177
+ try {
178
+ data = JSON.parse(text);
179
+ }
180
+ catch (_b) {
181
+ data = text;
182
+ }
183
+ }
184
+ else {
185
+ data = text;
186
+ }
187
+ }
188
+ }
189
+ const apiResponse = {
190
+ status: response.status,
191
+ statusText: response.statusText,
192
+ data,
193
+ headers: headersToObject(response.headers),
194
+ config: { baseURL: req.baseURL, url: req.url, method },
195
+ };
196
+ if (response.status >= 200 && response.status < 300) {
197
+ return apiResponse;
198
+ }
199
+ // Non-2xx → throw axios-shaped error.
200
+ const err = new Error(`HTTP ${response.status} ${response.statusText}`);
201
+ err.response = apiResponse;
202
+ err.config = apiResponse.config;
203
+ throw err;
204
+ }
205
+ /**
206
+ * Retry wrapper around `performRequest`. Mirrors the previous axios-retry
207
+ * config: 3 retries, exponential backoff (1s/2s/4s), respects Retry-After on
208
+ * 429, retries on network errors and 5xx/429 status codes.
209
+ */
210
+ async function performRequestWithRetry(req) {
211
+ let lastErr;
212
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
213
+ try {
214
+ return await performRequest(req);
215
+ }
216
+ catch (err) {
217
+ lastErr = err;
218
+ if (attempt >= MAX_RETRIES || !shouldRetry(err))
219
+ throw err;
220
+ await sleep(backoffMs(attempt, err));
221
+ }
222
+ }
223
+ throw lastErr;
224
+ }
52
225
  /**
53
- * Debug-aware HTTP request wrapper.
54
- * Logs request/response details to stderr when debug mode is enabled.
226
+ * Debug-aware request wrapper. Logs request/response details to stderr when
227
+ * debug mode is enabled, then delegates to the retry wrapper.
55
228
  */
56
- async function debugRequest(client, requestConfig) {
229
+ async function debugRequest(defaults, requestConfig) {
57
230
  var _a, _b, _c, _d;
58
- const url = `${requestConfig.baseURL || ""}/${requestConfig.url || ""}`.replace(/\/+/g, "/").replace(":/", "://");
59
- const method = (requestConfig.method || "GET").toUpperCase();
231
+ const merged = withDefaults(defaults, requestConfig);
232
+ const url = buildUrl(merged);
233
+ const method = (merged.method || "GET").toUpperCase();
60
234
  if (debugEnabled) {
61
235
  debugLog(`${chalk.yellow(method)} ${chalk.cyan(url)}`);
62
- if (requestConfig.params && Object.keys(requestConfig.params).length) {
63
- debugLog("params:", JSON.stringify(requestConfig.params));
236
+ if (merged.params && Object.keys(merged.params).length) {
237
+ debugLog("params:", JSON.stringify(merged.params));
64
238
  }
65
- if (requestConfig.data) {
66
- debugLog("body:", JSON.stringify(requestConfig.data).substring(0, 200));
239
+ if (merged.data) {
240
+ debugLog("body:", JSON.stringify(merged.data).substring(0, 200));
67
241
  }
68
- const auth = ((_a = requestConfig.headers) === null || _a === void 0 ? void 0 : _a.Authorization) || ((_b = requestConfig.headers) === null || _b === void 0 ? void 0 : _b.authorization);
242
+ const auth = ((_a = merged.headers) === null || _a === void 0 ? void 0 : _a.Authorization) || ((_b = merged.headers) === null || _b === void 0 ? void 0 : _b.authorization);
69
243
  if (auth) {
70
244
  const token = String(auth);
71
245
  debugLog("auth:", token.substring(0, 15) + "..." + token.substring(token.length - 10));
72
246
  }
73
247
  }
74
248
  try {
75
- const res = await client.request(requestConfig);
249
+ const res = await performRequestWithRetry(merged);
76
250
  if (debugEnabled) {
77
251
  const meta = (_c = res.data) === null || _c === void 0 ? void 0 : _c.meta;
78
252
  const total = (_d = meta === null || meta === void 0 ? void 0 : meta.pageInfo) === null || _d === void 0 ? void 0 : _d.totalEntries;
@@ -102,20 +276,13 @@ async function debugRequest(client, requestConfig) {
102
276
  throw err;
103
277
  }
104
278
  }
105
- const apiClient = http.create({
106
- timeout: 30000,
107
- retry: retryConfig,
108
- logging: false,
109
- metrics: false,
110
- });
111
- const auditClient = http.create({
279
+ const API_DEFAULTS = { timeout: DEFAULT_TIMEOUT };
280
+ const AUDIT_DEFAULTS = {
112
281
  baseURL: config.auditUrl,
113
- retry: retryConfig,
114
- logging: false,
115
- metrics: false,
116
- });
117
- /** Callable API client that supports debug logging */
118
- const api = (requestConfig) => debugRequest(apiClient, requestConfig);
119
- /** Callable audit client that supports debug logging */
120
- const audit = (requestConfig) => debugRequest(auditClient, requestConfig);
282
+ timeout: DEFAULT_TIMEOUT,
283
+ };
284
+ /** Callable API client that supports debug logging. */
285
+ const api = (requestConfig) => debugRequest(API_DEFAULTS, requestConfig);
286
+ /** Callable audit client that supports debug logging. */
287
+ const audit = (requestConfig) => debugRequest(AUDIT_DEFAULTS, requestConfig);
121
288
  export { api, audit };
@@ -34,7 +34,7 @@ import { loadAliases } from "./createAliasCommand.js";
34
34
  import { getLogoSync, brandHighlight, dim } from "../helpers/logo.js";
35
35
  import { createRequire } from "module";
36
36
  // HEAVY modules deliberately NOT imported at the top of this file:
37
- // - ../api/apiClient (pulls in @tolinax/ayoune-core HTTP client + axios)
37
+ // - ../api/apiClient (uses native fetch but still pulls in chalk + config)
38
38
  // - ../api/apiCallHandler (pulls in apiClient + secureStorage + login)
39
39
  // - ../helpers/secureStorage (pulls in node-localstorage + crypto)
40
40
  // - ../api/login (pulls in socket.io-client)
@@ -127,16 +127,17 @@ Examples:
127
127
  // Deduplicate by host
128
128
  const uniqueTargets = [...new Map(targets.map((t) => [t.host, t])).values()];
129
129
  spinner.start({ text: `Checking ${uniqueTargets.length} service(s)...`, color: "magenta" });
130
- const { http } = await import("@tolinax/ayoune-core/lib/http");
131
130
  const results = await Promise.allSettled(uniqueTargets.map(async (t) => {
132
131
  const start = Date.now();
132
+ // Native fetch with AbortController-driven timeout. We previously
133
+ // used @tolinax/ayoune-core's http wrapper here, but it loads the
134
+ // backend AY singleton (mongoose + ioredis + prom-client) at import
135
+ // time and has no place in a CLI process.
136
+ const controller = new AbortController();
137
+ const timer = setTimeout(() => controller.abort(), opts.timeout);
133
138
  try {
134
- const resp = await http.get(`https://${t.host}/`, {
135
- timeout: opts.timeout,
136
- validateStatus: () => true,
137
- logging: false,
138
- metrics: false,
139
- });
139
+ const resp = await fetch(`https://${t.host}/`, { signal: controller.signal });
140
+ clearTimeout(timer);
140
141
  return {
141
142
  host: t.host,
142
143
  name: t.name,
@@ -146,13 +147,14 @@ Examples:
146
147
  };
147
148
  }
148
149
  catch (e) {
150
+ clearTimeout(timer);
149
151
  return {
150
152
  host: t.host,
151
153
  name: t.name,
152
154
  status: "unreachable",
153
155
  statusCode: 0,
154
156
  responseTime: Date.now() - start,
155
- error: e.code || e.message,
157
+ error: e.name === "AbortError" ? "timeout" : (e.code || e.message),
156
158
  };
157
159
  }
158
160
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolinax/ayoune-cli",
3
- "version": "2026.10.0",
3
+ "version": "2026.10.1",
4
4
  "description": "CLI for the aYOUne Business-as-a-Service platform",
5
5
  "type": "module",
6
6
  "main": "./index.js",