@tolinax/ayoune-cli 2026.9.0 → 2026.10.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/lib/commands/_registry.js +17 -0
- package/lib/commands/createSelfHostUpdateCommand.js +1 -20
- 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
|
@@ -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"],
|
|
@@ -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)
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// `ay functions create` — interactive wizard to scaffold a new user function.
|
|
2
|
+
//
|
|
3
|
+
// Walks the user through name, slug, trigger type, and starter template, then
|
|
4
|
+
// either opens $EDITOR for them to write the body or accepts `--source <path>`
|
|
5
|
+
// for a file. Once the function exists, it's saved as a draft (publish via
|
|
6
|
+
// `ay functions deploy` or the editor).
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import { spinner } from "../../../index.js";
|
|
13
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
14
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
15
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
16
|
+
import { FN_MODULE, FN_COLLECTION } from "./_shared.js";
|
|
17
|
+
const TRIGGER_TYPES = ["http", "cron", "event", "queue", "manual"];
|
|
18
|
+
const STARTER_TEMPLATE = `// Custom Function — runs in an isolated-vm sandbox.
|
|
19
|
+
// The platform SDK is exposed as 'ay'. Return a JSON-serializable value.
|
|
20
|
+
|
|
21
|
+
module.exports = async function handler(input, ay) {
|
|
22
|
+
ay.log.info("hello from ay functions", { input });
|
|
23
|
+
return { ok: true, input };
|
|
24
|
+
};
|
|
25
|
+
`;
|
|
26
|
+
export function addCreateSubcommand(fns, rootProgram) {
|
|
27
|
+
fns
|
|
28
|
+
.command("create")
|
|
29
|
+
.description("Create a new user function (interactive wizard or via flags)")
|
|
30
|
+
.option("--name <name>", "Function name")
|
|
31
|
+
.option("--slug <slug>", "URL-safe slug")
|
|
32
|
+
.option("--trigger <type>", `Trigger type (${TRIGGER_TYPES.join(", ")})`)
|
|
33
|
+
.option("--source <path>", "Path to a local source file")
|
|
34
|
+
.option("--description <text>", "Description")
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
try {
|
|
38
|
+
let { name, slug, trigger, source, description } = options;
|
|
39
|
+
// Wizard mode when running in TTY without enough flags.
|
|
40
|
+
if (process.stdin.isTTY && (!name || !trigger)) {
|
|
41
|
+
const inquirer = (await import("inquirer")).default;
|
|
42
|
+
const answers = await inquirer.prompt([
|
|
43
|
+
{ type: "input", name: "name", message: "Function name:", default: name, when: !name,
|
|
44
|
+
validate: (v) => v.length > 0 || "Required" },
|
|
45
|
+
{ type: "input", name: "slug", message: "Slug:", default: (a) => { var _a; return slug || ((_a = a.name) !== null && _a !== void 0 ? _a : name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } },
|
|
46
|
+
{ type: "list", name: "trigger", message: "Trigger type:", choices: [...TRIGGER_TYPES], default: trigger !== null && trigger !== void 0 ? trigger : "manual", when: !trigger },
|
|
47
|
+
{ type: "input", name: "description", message: "Description (optional):", default: description !== null && description !== void 0 ? description : "", when: !description },
|
|
48
|
+
{ type: "confirm", name: "openEditor", message: "Open $EDITOR with the starter template?", default: true, when: !source },
|
|
49
|
+
]);
|
|
50
|
+
name = name !== null && name !== void 0 ? name : answers.name;
|
|
51
|
+
slug = slug !== null && slug !== void 0 ? slug : answers.slug;
|
|
52
|
+
trigger = trigger !== null && trigger !== void 0 ? trigger : answers.trigger;
|
|
53
|
+
description = description !== null && description !== void 0 ? description : answers.description;
|
|
54
|
+
if (!source && answers.openEditor) {
|
|
55
|
+
source = await editInTempFile(STARTER_TEMPLATE);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!name)
|
|
59
|
+
cliError("--name is required (or run interactively)", EXIT_MISUSE);
|
|
60
|
+
if (!trigger)
|
|
61
|
+
cliError("--trigger is required (or run interactively)", EXIT_MISUSE);
|
|
62
|
+
if (!TRIGGER_TYPES.includes(trigger)) {
|
|
63
|
+
cliError(`Invalid trigger "${trigger}" — expected one of: ${TRIGGER_TYPES.join(", ")}`, EXIT_MISUSE);
|
|
64
|
+
}
|
|
65
|
+
let code = STARTER_TEMPLATE;
|
|
66
|
+
if (source) {
|
|
67
|
+
if (typeof source !== "string") {
|
|
68
|
+
cliError("--source must be a file path", EXIT_MISUSE);
|
|
69
|
+
}
|
|
70
|
+
if (!existsSync(source)) {
|
|
71
|
+
cliError(`Source file not found: ${source}`, EXIT_MISUSE);
|
|
72
|
+
}
|
|
73
|
+
code = await readFile(source, "utf-8");
|
|
74
|
+
}
|
|
75
|
+
if (!slug) {
|
|
76
|
+
slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
77
|
+
}
|
|
78
|
+
spinner.start({ text: `Creating function "${name}"...`, color: "cyan" });
|
|
79
|
+
const body = {
|
|
80
|
+
name,
|
|
81
|
+
slug,
|
|
82
|
+
triggerType: trigger,
|
|
83
|
+
description: description !== null && description !== void 0 ? description : "",
|
|
84
|
+
source: code,
|
|
85
|
+
status: "draft",
|
|
86
|
+
};
|
|
87
|
+
const res = await apiCallHandler(FN_MODULE, FN_COLLECTION, "post", body);
|
|
88
|
+
spinner.success({ text: `Function created (id: ${(_b = (_a = res === null || res === void 0 ? void 0 : res.payload) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : "?"})` });
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
cliError(e.message || "Failed to create function", EXIT_GENERAL_ERROR);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Write the starter template to a temp file, open `$EDITOR` (or `vi`/`notepad`)
|
|
97
|
+
* synchronously, then read it back. Returns the (possibly empty) edited body.
|
|
98
|
+
*/
|
|
99
|
+
async function editInTempFile(initial) {
|
|
100
|
+
const dir = path.join(os.tmpdir(), "ay-functions");
|
|
101
|
+
await mkdir(dir, { recursive: true });
|
|
102
|
+
const file = path.join(dir, `function-${Date.now()}.js`);
|
|
103
|
+
await writeFile(file, initial, "utf-8");
|
|
104
|
+
const editor = process.env.VISUAL ||
|
|
105
|
+
process.env.EDITOR ||
|
|
106
|
+
(process.platform === "win32" ? "notepad" : "vi");
|
|
107
|
+
spawnSync(editor, [file], { stdio: "inherit", shell: process.platform === "win32" });
|
|
108
|
+
return readFile(file, "utf-8");
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `ay functions delete <id-or-slug>` — delete a user function with confirmation.
|
|
2
|
+
import { spinner } from "../../../index.js";
|
|
3
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
4
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
5
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
6
|
+
import { FN_MODULE, FN_COLLECTION, resolveFunction } from "./_shared.js";
|
|
7
|
+
export function addDeleteSubcommand(fns, _rootProgram) {
|
|
8
|
+
fns
|
|
9
|
+
.command("delete <id-or-slug>")
|
|
10
|
+
.alias("rm")
|
|
11
|
+
.description("Delete a user function")
|
|
12
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
13
|
+
.action(async (idOrSlug, options) => {
|
|
14
|
+
var _a, _b;
|
|
15
|
+
try {
|
|
16
|
+
spinner.start({ text: `Resolving ${idOrSlug}...`, color: "cyan" });
|
|
17
|
+
const fn = await resolveFunction(idOrSlug);
|
|
18
|
+
spinner.stop();
|
|
19
|
+
if (!options.yes && process.stdin.isTTY) {
|
|
20
|
+
const inquirer = (await import("inquirer")).default;
|
|
21
|
+
const { ok } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: "confirm",
|
|
24
|
+
name: "ok",
|
|
25
|
+
message: `Delete function "${(_a = fn.name) !== null && _a !== void 0 ? _a : fn.slug}" (${fn._id})?`,
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
if (!ok)
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
spinner.start({ text: `Deleting ${(_b = fn.name) !== null && _b !== void 0 ? _b : fn.slug}...`, color: "red" });
|
|
33
|
+
await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fn._id}`, "delete");
|
|
34
|
+
spinner.success({ text: "Function deleted" });
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
cliError(e.message || "Delete failed", EXIT_GENERAL_ERROR);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `ay functions deploy <path> [--name <name>]` — upload a local source file
|
|
2
|
+
// as a function and publish it.
|
|
3
|
+
//
|
|
4
|
+
// If a function with the matching slug already exists, this updates its
|
|
5
|
+
// source and triggers a publish; otherwise it creates a new function first.
|
|
6
|
+
// Validates the source up-front and asks the user to confirm if any
|
|
7
|
+
// warnings come back.
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { spinner } from "../../../index.js";
|
|
13
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
14
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
15
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
16
|
+
import { FN_MODULE, FN_COLLECTION } from "./_shared.js";
|
|
17
|
+
import { validateFunctionSource } from "./_validateSource.js";
|
|
18
|
+
export function addDeploySubcommand(fns, _rootProgram) {
|
|
19
|
+
fns
|
|
20
|
+
.command("deploy <path>")
|
|
21
|
+
.description("Upload a local source file as a function and publish it")
|
|
22
|
+
.option("--name <name>", "Override the function name (defaults to filename)")
|
|
23
|
+
.option("--slug <slug>", "Override the slug (defaults to slugified name)")
|
|
24
|
+
.option("--trigger <type>", "Trigger type if creating a new function", "manual")
|
|
25
|
+
.option("--no-publish", "Upload as draft only, do not publish")
|
|
26
|
+
.option("-y, --yes", "Skip warning confirmation prompt", false)
|
|
27
|
+
.action(async (filePath, options) => {
|
|
28
|
+
var _a, _b, _c;
|
|
29
|
+
try {
|
|
30
|
+
if (!filePath)
|
|
31
|
+
cliError("Path to source file is required", EXIT_MISUSE);
|
|
32
|
+
if (!existsSync(filePath))
|
|
33
|
+
cliError(`Source file not found: ${filePath}`, EXIT_MISUSE);
|
|
34
|
+
const source = await readFile(filePath, "utf-8");
|
|
35
|
+
const warnings = validateFunctionSource(source);
|
|
36
|
+
if (warnings.length > 0) {
|
|
37
|
+
console.log(chalk.yellow("\n Source validation warnings:"));
|
|
38
|
+
for (const w of warnings)
|
|
39
|
+
console.log(chalk.yellow(` - ${w}`));
|
|
40
|
+
console.log("");
|
|
41
|
+
if (!options.yes && process.stdin.isTTY) {
|
|
42
|
+
const inquirer = (await import("inquirer")).default;
|
|
43
|
+
const { ok } = await inquirer.prompt([
|
|
44
|
+
{ type: "confirm", name: "ok", message: "Deploy anyway?", default: false },
|
|
45
|
+
]);
|
|
46
|
+
if (!ok)
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
51
|
+
const name = (_a = options.name) !== null && _a !== void 0 ? _a : baseName;
|
|
52
|
+
const slug = (_b = options.slug) !== null && _b !== void 0 ? _b : name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
53
|
+
spinner.start({ text: `Looking up "${slug}"...`, color: "cyan" });
|
|
54
|
+
// Check whether a function with this slug already exists.
|
|
55
|
+
const existingRes = await apiCallHandler(FN_MODULE, FN_COLLECTION, "get", null, {
|
|
56
|
+
slug,
|
|
57
|
+
limit: 1,
|
|
58
|
+
});
|
|
59
|
+
const existing = Array.isArray(existingRes === null || existingRes === void 0 ? void 0 : existingRes.payload) ? existingRes.payload[0] : existingRes === null || existingRes === void 0 ? void 0 : existingRes.payload;
|
|
60
|
+
let fnId;
|
|
61
|
+
if (existing === null || existing === void 0 ? void 0 : existing._id) {
|
|
62
|
+
spinner.update({ text: `Updating "${slug}" (${existing._id})...` });
|
|
63
|
+
await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${existing._id}`, "put", { source });
|
|
64
|
+
fnId = existing._id;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
spinner.update({ text: `Creating "${slug}"...` });
|
|
68
|
+
const created = await apiCallHandler(FN_MODULE, FN_COLLECTION, "post", {
|
|
69
|
+
name,
|
|
70
|
+
slug,
|
|
71
|
+
triggerType: options.trigger,
|
|
72
|
+
source,
|
|
73
|
+
status: "draft",
|
|
74
|
+
});
|
|
75
|
+
fnId = (_c = created === null || created === void 0 ? void 0 : created.payload) === null || _c === void 0 ? void 0 : _c._id;
|
|
76
|
+
if (!fnId)
|
|
77
|
+
cliError("Could not determine function id after create", EXIT_GENERAL_ERROR);
|
|
78
|
+
}
|
|
79
|
+
if (options.publish !== false) {
|
|
80
|
+
spinner.update({ text: "Publishing..." });
|
|
81
|
+
await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fnId}/actions/publish`, "post");
|
|
82
|
+
}
|
|
83
|
+
spinner.success({
|
|
84
|
+
text: options.publish === false ? `Uploaded "${slug}" as draft` : `Deployed "${slug}"`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
cliError(e.message || "Deploy failed", EXIT_GENERAL_ERROR);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `ay functions get <id-or-slug>` — show metadata + source for a function.
|
|
2
|
+
import { spinner } from "../../../index.js";
|
|
3
|
+
import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
|
|
4
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
5
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
6
|
+
import { resolveFunction } from "./_shared.js";
|
|
7
|
+
export function addGetSubcommand(fns, rootProgram) {
|
|
8
|
+
fns
|
|
9
|
+
.command("get <id-or-slug>")
|
|
10
|
+
.description("Show metadata and source for a single function")
|
|
11
|
+
.option("--source-only", "Print only the source code (no metadata)", false)
|
|
12
|
+
.action(async (idOrSlug, options) => {
|
|
13
|
+
var _a, _b, _c, _d;
|
|
14
|
+
try {
|
|
15
|
+
const opts = { ...rootProgram.opts(), ...options };
|
|
16
|
+
spinner.start({ text: `Fetching ${idOrSlug}...`, color: "cyan" });
|
|
17
|
+
const fn = await resolveFunction(idOrSlug);
|
|
18
|
+
spinner.stop();
|
|
19
|
+
if (options.sourceOnly) {
|
|
20
|
+
process.stdout.write((_b = (_a = fn.source) !== null && _a !== void 0 ? _a : fn.code) !== null && _b !== void 0 ? _b : "");
|
|
21
|
+
if (!((_d = (_c = fn.source) !== null && _c !== void 0 ? _c : fn.code) !== null && _d !== void 0 ? _d : "").endsWith("\n"))
|
|
22
|
+
process.stdout.write("\n");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
handleResponseFormatOptions(opts, { payload: fn, meta: {} });
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
cliError(e.message || "Failed to fetch function", EXIT_GENERAL_ERROR);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `ay functions` parent command — manage Custom Functions (sandboxed FaaS).
|
|
2
|
+
//
|
|
3
|
+
// Custom Functions live in the `userfunctions` collection on config-api with
|
|
4
|
+
// a custom routerUserFunctionsCustom that adds /actions/{execute,publish,...}
|
|
5
|
+
// endpoints, /versions, /logs, and /env CRUD. The CLI surface mirrors what
|
|
6
|
+
// the user-functions-editor frontend exposes so platform customers can manage
|
|
7
|
+
// their FaaS code from a terminal without opening the editor.
|
|
8
|
+
//
|
|
9
|
+
// Subcommand layout follows the same pattern as ay aggregate / ay deploy:
|
|
10
|
+
// each verb is its own file, registered onto the parent here. The shared
|
|
11
|
+
// `_validateSource` lives alongside the deploy subcommand and is reused by
|
|
12
|
+
// `create` (template scaffolding) when the editor pre-populates a stub.
|
|
13
|
+
import { addListSubcommand } from "./list.js";
|
|
14
|
+
import { addGetSubcommand } from "./get.js";
|
|
15
|
+
import { addCreateSubcommand } from "./create.js";
|
|
16
|
+
import { addDeploySubcommand } from "./deploy.js";
|
|
17
|
+
import { addInvokeSubcommand } from "./invoke.js";
|
|
18
|
+
import { addLogsSubcommand } from "./logs.js";
|
|
19
|
+
import { addDeleteSubcommand } from "./delete.js";
|
|
20
|
+
import { addVersionsSubcommand } from "./versions.js";
|
|
21
|
+
import { addRollbackSubcommand } from "./rollback.js";
|
|
22
|
+
export function createFunctionsCommand(program) {
|
|
23
|
+
const fns = program
|
|
24
|
+
.command("functions")
|
|
25
|
+
.alias("fns")
|
|
26
|
+
.description("Manage Custom Functions (sandboxed FaaS) for this customer")
|
|
27
|
+
.addHelpText("after", `
|
|
28
|
+
Examples:
|
|
29
|
+
ay functions list List all functions
|
|
30
|
+
ay functions get my-fn Show metadata + source for one function
|
|
31
|
+
ay functions create Wizard: name, trigger, code template
|
|
32
|
+
ay functions deploy ./hello.ts Upload a local file as a function
|
|
33
|
+
ay functions invoke my-fn --data '{}' Test-invoke with input
|
|
34
|
+
ay functions logs my-fn -f Tail execution logs
|
|
35
|
+
ay functions versions my-fn Show version history
|
|
36
|
+
ay functions rollback my-fn 3 Roll back to version 3
|
|
37
|
+
ay functions delete my-fn Delete (with confirmation)`);
|
|
38
|
+
addListSubcommand(fns, program);
|
|
39
|
+
addGetSubcommand(fns, program);
|
|
40
|
+
addCreateSubcommand(fns, program);
|
|
41
|
+
addDeploySubcommand(fns, program);
|
|
42
|
+
addInvokeSubcommand(fns, program);
|
|
43
|
+
addLogsSubcommand(fns, program);
|
|
44
|
+
addDeleteSubcommand(fns, program);
|
|
45
|
+
addVersionsSubcommand(fns, program);
|
|
46
|
+
addRollbackSubcommand(fns, program);
|
|
47
|
+
}
|
|
48
|
+
export default createFunctionsCommand;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// `ay functions invoke <id-or-slug>` — test-invoke a function with input data.
|
|
2
|
+
//
|
|
3
|
+
// Hits the custom action `POST /userfunctions/:id/actions/execute` which
|
|
4
|
+
// runs the function in the worker sandbox and returns its output. Input can
|
|
5
|
+
// come from `--data <json>`, `--file <path>` (json), or stdin pipe.
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { spinner } from "../../../index.js";
|
|
8
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
9
|
+
import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
|
|
10
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
11
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
12
|
+
import { FN_MODULE, FN_COLLECTION, resolveFunction } from "./_shared.js";
|
|
13
|
+
async function readStdin() {
|
|
14
|
+
if (process.stdin.isTTY)
|
|
15
|
+
return "";
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let data = "";
|
|
18
|
+
process.stdin.setEncoding("utf-8");
|
|
19
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
20
|
+
process.stdin.on("end", () => resolve(data));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function addInvokeSubcommand(fns, rootProgram) {
|
|
24
|
+
fns
|
|
25
|
+
.command("invoke <id-or-slug>")
|
|
26
|
+
.description("Test-invoke a function with input data")
|
|
27
|
+
.option("-d, --data <json>", "Input data as a JSON string")
|
|
28
|
+
.option("-f, --file <path>", "Input data from a JSON file")
|
|
29
|
+
.option("--async", "Run async (return jobId instead of waiting for output)", false)
|
|
30
|
+
.action(async (idOrSlug, options) => {
|
|
31
|
+
var _a;
|
|
32
|
+
try {
|
|
33
|
+
const opts = { ...rootProgram.opts(), ...options };
|
|
34
|
+
let inputJson = null;
|
|
35
|
+
if (options.data) {
|
|
36
|
+
try {
|
|
37
|
+
inputJson = JSON.parse(options.data);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
cliError(`Invalid JSON in --data: ${e.message}`, EXIT_MISUSE);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (options.file) {
|
|
44
|
+
const raw = await readFile(options.file, "utf-8");
|
|
45
|
+
try {
|
|
46
|
+
inputJson = JSON.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
cliError(`Invalid JSON in ${options.file}: ${e.message}`, EXIT_MISUSE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (!process.stdin.isTTY) {
|
|
53
|
+
const raw = await readStdin();
|
|
54
|
+
if (raw.trim()) {
|
|
55
|
+
try {
|
|
56
|
+
inputJson = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
cliError(`Invalid JSON on stdin: ${e.message}`, EXIT_MISUSE);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
spinner.start({ text: `Resolving ${idOrSlug}...`, color: "cyan" });
|
|
64
|
+
const fn = await resolveFunction(idOrSlug);
|
|
65
|
+
spinner.update({ text: `Invoking ${(_a = fn.name) !== null && _a !== void 0 ? _a : fn.slug}...` });
|
|
66
|
+
const action = options.async ? "execute-async" : "execute";
|
|
67
|
+
const res = await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fn._id}/actions/${action}`, "post", { input: inputJson });
|
|
68
|
+
spinner.success({ text: options.async ? "Function queued" : "Function executed" });
|
|
69
|
+
handleResponseFormatOptions(opts, res);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
cliError(e.message || "Invoke failed", EXIT_GENERAL_ERROR);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|