@tolinax/ayoune-cli 2026.9.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.
- package/lib/api/apiClient.js +219 -52
- package/lib/commands/_registry.js +17 -0
- package/lib/commands/createProgram.js +1 -1
- package/lib/commands/createSelfHostUpdateCommand.js +1 -20
- package/lib/commands/createServicesCommand.js +10 -8
- package/lib/commands/createSetupCommand.js +57 -5
- package/lib/commands/functions/_shared.js +38 -0
- package/lib/commands/functions/_validateSource.js +50 -0
- package/lib/commands/functions/create.js +109 -0
- package/lib/commands/functions/delete.js +40 -0
- package/lib/commands/functions/deploy.js +91 -0
- package/lib/commands/functions/get.js +31 -0
- package/lib/commands/functions/index.js +48 -0
- package/lib/commands/functions/invoke.js +75 -0
- package/lib/commands/functions/list.js +41 -0
- package/lib/commands/functions/logs.js +76 -0
- package/lib/commands/functions/rollback.js +44 -0
- package/lib/commands/functions/versions.js +32 -0
- package/lib/commands/local/_context.js +42 -0
- package/lib/commands/local/down.js +50 -0
- package/lib/commands/local/exec.js +45 -0
- package/lib/commands/local/index.js +40 -0
- package/lib/commands/local/logs.js +38 -0
- package/lib/commands/local/ps.js +41 -0
- package/lib/commands/local/pull.js +40 -0
- package/lib/commands/local/restart.js +31 -0
- package/lib/commands/local/up.js +80 -0
- package/lib/commands/provision/_detectTools.js +52 -0
- package/lib/commands/provision/_stateFile.js +36 -0
- package/lib/commands/provision/_wizard.js +60 -0
- package/lib/commands/provision/aws.js +107 -0
- package/lib/commands/provision/azure.js +113 -0
- package/lib/commands/provision/destroy.js +119 -0
- package/lib/commands/provision/digitalocean.js +82 -0
- package/lib/commands/provision/gcp.js +118 -0
- package/lib/commands/provision/hetzner.js +220 -0
- package/lib/commands/provision/index.js +44 -0
- package/lib/commands/provision/status.js +44 -0
- package/lib/helpers/dockerCompose.js +143 -0
- package/package.json +1 -1
package/lib/api/apiClient.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
54
|
-
*
|
|
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(
|
|
229
|
+
async function debugRequest(defaults, requestConfig) {
|
|
57
230
|
var _a, _b, _c, _d;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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 (
|
|
63
|
-
debugLog("params:", JSON.stringify(
|
|
236
|
+
if (merged.params && Object.keys(merged.params).length) {
|
|
237
|
+
debugLog("params:", JSON.stringify(merged.params));
|
|
64
238
|
}
|
|
65
|
-
if (
|
|
66
|
-
debugLog("body:", JSON.stringify(
|
|
239
|
+
if (merged.data) {
|
|
240
|
+
debugLog("body:", JSON.stringify(merged.data).substring(0, 200));
|
|
67
241
|
}
|
|
68
|
-
const auth = ((_a =
|
|
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
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
/** Callable
|
|
118
|
-
const
|
|
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 };
|
|
@@ -223,6 +223,23 @@ export const COMMAND_REGISTRY = [
|
|
|
223
223
|
description: "Check for and apply updates to a self-hosted aYOUne deployment",
|
|
224
224
|
loader: async () => (await import("./createSelfHostUpdateCommand.js")).createSelfHostUpdateCommand,
|
|
225
225
|
},
|
|
226
|
+
{
|
|
227
|
+
name: "local",
|
|
228
|
+
description: "Run a self-hosted aYOUne instance locally via Docker Compose",
|
|
229
|
+
loader: async () => (await import("./local/index.js")).createLocalCommand,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "functions",
|
|
233
|
+
aliases: ["fns"],
|
|
234
|
+
description: "Manage Custom Functions (sandboxed FaaS) for this customer",
|
|
235
|
+
loader: async () => (await import("./functions/index.js")).createFunctionsCommand,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "provision",
|
|
239
|
+
aliases: ["prov"],
|
|
240
|
+
description: "Provision aYOUne to a cloud provider (AWS / GCP / Azure / DigitalOcean / Hetzner)",
|
|
241
|
+
loader: async () => (await import("./provision/index.js")).createProvisionCommand,
|
|
242
|
+
},
|
|
226
243
|
{
|
|
227
244
|
name: "context",
|
|
228
245
|
aliases: ["ctx"],
|
|
@@ -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
|
|
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)
|
|
@@ -2,26 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { spinner } from "../../index.js";
|
|
3
3
|
import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
|
|
4
4
|
import { cliError } from "../helpers/cliError.js";
|
|
5
|
-
import {
|
|
6
|
-
function runCommand(cmd) {
|
|
7
|
-
try {
|
|
8
|
-
return execSync(cmd, { encoding: "utf-8", timeout: 30000 }).trim();
|
|
9
|
-
}
|
|
10
|
-
catch (_a) {
|
|
11
|
-
return "";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function detectRuntime() {
|
|
15
|
-
// Check for docker compose
|
|
16
|
-
const composeResult = runCommand("docker compose version 2>&1");
|
|
17
|
-
if (composeResult.includes("Docker Compose"))
|
|
18
|
-
return "compose";
|
|
19
|
-
// Check for kubectl
|
|
20
|
-
const kubectlResult = runCommand("kubectl version --client 2>&1");
|
|
21
|
-
if (kubectlResult.includes("Client Version"))
|
|
22
|
-
return "kubernetes";
|
|
23
|
-
return "unknown";
|
|
24
|
-
}
|
|
5
|
+
import { runCommand, detectRuntime } from "../helpers/dockerCompose.js";
|
|
25
6
|
function getRunningComposeServices() {
|
|
26
7
|
const output = runCommand('docker compose ps --format "{{.Name}}" 2>&1');
|
|
27
8
|
if (!output)
|
|
@@ -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
|
|
135
|
-
|
|
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
|
}));
|
|
@@ -3,9 +3,11 @@ import inquirer from "inquirer";
|
|
|
3
3
|
import { writeFile, mkdir } from "fs/promises";
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { spawn } from "child_process";
|
|
6
7
|
import { spinner } from "../../index.js";
|
|
7
8
|
import { EXIT_GENERAL_ERROR } from "../exitCodes.js";
|
|
8
9
|
import { cliError } from "../helpers/cliError.js";
|
|
10
|
+
import { detectRuntime } from "../helpers/dockerCompose.js";
|
|
9
11
|
const AVAILABLE_MODULES = [
|
|
10
12
|
{ name: "CRM", value: "crm", description: "Customer Relationship Management" },
|
|
11
13
|
{ name: "Marketing", value: "marketing", description: "Marketing automation & campaigns" },
|
|
@@ -152,6 +154,36 @@ function generateRandomString(length) {
|
|
|
152
154
|
function capitalize(s) {
|
|
153
155
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
154
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Launch the freshly-generated compose stack from the output dir, then poll
|
|
159
|
+
* the status command until services come up healthy or we time out. Wired
|
|
160
|
+
* into `ay setup` so the first-run experience is "answer questions →
|
|
161
|
+
* everything is running" instead of "answer questions → here's the next
|
|
162
|
+
* shell command to type".
|
|
163
|
+
*/
|
|
164
|
+
async function launchComposeStack(outputDir, profiles) {
|
|
165
|
+
spinner.start({ text: `Starting docker compose with profiles: ${profiles.join(", ")}`, color: "cyan" });
|
|
166
|
+
const profileArgs = profiles.flatMap((p) => ["--profile", p]);
|
|
167
|
+
const args = [...profileArgs, "up", "-d"];
|
|
168
|
+
const code = await new Promise((resolve) => {
|
|
169
|
+
const child = spawn("docker", ["compose", ...args], {
|
|
170
|
+
cwd: outputDir,
|
|
171
|
+
stdio: "inherit",
|
|
172
|
+
shell: process.platform === "win32",
|
|
173
|
+
});
|
|
174
|
+
child.on("exit", (c) => resolve(c !== null && c !== void 0 ? c : 0));
|
|
175
|
+
child.on("error", () => resolve(1));
|
|
176
|
+
});
|
|
177
|
+
if (code !== 0) {
|
|
178
|
+
spinner.error({ text: "docker compose up failed" });
|
|
179
|
+
cliError("Could not start the stack — check the docker compose output above.", EXIT_GENERAL_ERROR);
|
|
180
|
+
}
|
|
181
|
+
spinner.success({ text: "Stack started" });
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(chalk.green(" aYOUne is starting up."));
|
|
184
|
+
console.log(chalk.dim(" Run `ay status` to verify all services are healthy."));
|
|
185
|
+
console.log("");
|
|
186
|
+
}
|
|
155
187
|
export function createSetupCommand(program) {
|
|
156
188
|
program
|
|
157
189
|
.command("setup")
|
|
@@ -285,13 +317,33 @@ Examples:
|
|
|
285
317
|
else {
|
|
286
318
|
spinner.success({ text: "Configuration files generated!" });
|
|
287
319
|
spinner.stop();
|
|
288
|
-
const profiles = ["core", ...answers.modules]
|
|
320
|
+
const profiles = ["core", ...answers.modules];
|
|
289
321
|
console.log(chalk.green("\n Generated files:"));
|
|
290
322
|
console.log(chalk.dim(` ${envPath}`));
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
323
|
+
// Offer to launch the stack right away. Skipped non-interactively
|
|
324
|
+
// (CI / piped) — that path keeps the original "next steps" output.
|
|
325
|
+
const canLaunch = process.stdin.isTTY && detectRuntime() === "compose";
|
|
326
|
+
let launched = false;
|
|
327
|
+
if (canLaunch) {
|
|
328
|
+
const { launchNow } = await inquirer.prompt([
|
|
329
|
+
{
|
|
330
|
+
type: "confirm",
|
|
331
|
+
name: "launchNow",
|
|
332
|
+
message: "Start aYOUne now?",
|
|
333
|
+
default: true,
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
if (launchNow) {
|
|
337
|
+
await launchComposeStack(outputDir, profiles);
|
|
338
|
+
launched = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!launched) {
|
|
342
|
+
console.log(chalk.cyan("\n Next steps:"));
|
|
343
|
+
console.log(chalk.dim(" 1. Review and adjust the .env file"));
|
|
344
|
+
console.log(chalk.dim(` 2. docker compose --profile ${profiles.join(" --profile ")} up -d`));
|
|
345
|
+
console.log(chalk.dim(" 3. ay status — verify all services are healthy\n"));
|
|
346
|
+
}
|
|
295
347
|
}
|
|
296
348
|
if (!answers.licenseKey) {
|
|
297
349
|
console.log(chalk.yellow(" Note: Running in 14-day trial mode.") +
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Shared helpers for the `ay functions *` subcommands.
|
|
2
|
+
//
|
|
3
|
+
// Centralizes the user-functions resource path and the id-or-slug resolver
|
|
4
|
+
// so each subcommand stays small and consistent. The custom-functions API
|
|
5
|
+
// accepts both `_id` (Mongo) and `slug` in path params, but list/get/etc.
|
|
6
|
+
// vary in which they prefer — wrapping the resolution here keeps the
|
|
7
|
+
// per-subcommand files focused on their actual verb.
|
|
8
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
9
|
+
/** Module that hosts the userfunctions collection. */
|
|
10
|
+
export const FN_MODULE = "config";
|
|
11
|
+
/** Collection segment used in URL paths. */
|
|
12
|
+
export const FN_COLLECTION = "userfunctions";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a user-supplied identifier (slug or _id) to a concrete function
|
|
15
|
+
* record. Returns the full doc; throws if nothing matches.
|
|
16
|
+
*
|
|
17
|
+
* The userfunctions list endpoint supports `?slug=` and `?_id=` filters.
|
|
18
|
+
* Many subcommands need the full record (e.g. invoke needs the entry id +
|
|
19
|
+
* latest version) — fetching it once via the resolver keeps every callsite
|
|
20
|
+
* uniform and gives consistent error messages.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveFunction(idOrSlug) {
|
|
23
|
+
// Try direct GET first — works when the user passed a Mongo _id.
|
|
24
|
+
if (/^[a-f0-9]{24}$/i.test(idOrSlug)) {
|
|
25
|
+
const direct = await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${idOrSlug}`, "get");
|
|
26
|
+
if (direct === null || direct === void 0 ? void 0 : direct.payload)
|
|
27
|
+
return direct.payload;
|
|
28
|
+
}
|
|
29
|
+
// Otherwise treat it as a slug + filter the list endpoint.
|
|
30
|
+
const res = await apiCallHandler(FN_MODULE, FN_COLLECTION, "get", null, {
|
|
31
|
+
slug: idOrSlug,
|
|
32
|
+
limit: 1,
|
|
33
|
+
});
|
|
34
|
+
const hit = Array.isArray(res === null || res === void 0 ? void 0 : res.payload) ? res.payload[0] : res === null || res === void 0 ? void 0 : res.payload;
|
|
35
|
+
if (!hit)
|
|
36
|
+
throw new Error(`No function found for "${idOrSlug}"`);
|
|
37
|
+
return hit;
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Light source validation for `ay functions deploy`.
|
|
2
|
+
//
|
|
3
|
+
// We can't actually run the code (the sandbox lives in
|
|
4
|
+
// platform/custom-functions-worker via isolated-vm), but we can catch the
|
|
5
|
+
// most common end-user mistakes that would otherwise produce confusing
|
|
6
|
+
// runtime errors:
|
|
7
|
+
// - importing Node builtins like `fs` / `child_process` / `net` (sandbox
|
|
8
|
+
// forbids them — surface that early instead of after publishing)
|
|
9
|
+
// - using `import` syntax (the sandbox is CommonJS, isolated-vm doesn't
|
|
10
|
+
// run an ESM loader)
|
|
11
|
+
// - missing `module.exports = async function ...`
|
|
12
|
+
//
|
|
13
|
+
// Returns a list of warnings; the deploy subcommand prints them and asks
|
|
14
|
+
// for confirmation if any are present.
|
|
15
|
+
const FORBIDDEN_BUILTINS = [
|
|
16
|
+
"fs",
|
|
17
|
+
"child_process",
|
|
18
|
+
"net",
|
|
19
|
+
"tls",
|
|
20
|
+
"dgram",
|
|
21
|
+
"dns",
|
|
22
|
+
"http",
|
|
23
|
+
"https",
|
|
24
|
+
"cluster",
|
|
25
|
+
"worker_threads",
|
|
26
|
+
"vm",
|
|
27
|
+
"os",
|
|
28
|
+
"process",
|
|
29
|
+
"v8",
|
|
30
|
+
];
|
|
31
|
+
export function validateFunctionSource(source) {
|
|
32
|
+
const warnings = [];
|
|
33
|
+
// ESM imports — sandbox is CJS.
|
|
34
|
+
if (/^\s*import\s+/m.test(source)) {
|
|
35
|
+
warnings.push("Detected `import` syntax. The Custom Functions sandbox runs CommonJS — use `const x = require(...)` instead.");
|
|
36
|
+
}
|
|
37
|
+
// Forbidden builtins via require().
|
|
38
|
+
const requireMatches = source.matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
39
|
+
for (const m of requireMatches) {
|
|
40
|
+
const mod = m[1];
|
|
41
|
+
if (FORBIDDEN_BUILTINS.includes(mod)) {
|
|
42
|
+
warnings.push(`Importing Node builtin "${mod}" — the sandbox does not expose this module. Use the platform SDK (\`ay\`) instead.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Module.exports check.
|
|
46
|
+
if (!/module\.exports\s*=/.test(source) && !/exports\.\w+\s*=/.test(source)) {
|
|
47
|
+
warnings.push("Source does not export anything. The sandbox expects `module.exports = async (input, ay) => {...}`.");
|
|
48
|
+
}
|
|
49
|
+
return warnings;
|
|
50
|
+
}
|