@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `ay functions list` — list user functions for the current customer.
|
|
2
|
+
import { spinner } from "../../../index.js";
|
|
3
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
4
|
+
import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
|
|
5
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
6
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
7
|
+
import { FN_MODULE, FN_COLLECTION } from "./_shared.js";
|
|
8
|
+
export function addListSubcommand(fns, rootProgram) {
|
|
9
|
+
fns
|
|
10
|
+
.command("list")
|
|
11
|
+
.alias("ls")
|
|
12
|
+
.description("List user functions for the current customer")
|
|
13
|
+
.option("-l, --limit <n>", "Limit results", (v) => parseInt(v, 10), 50)
|
|
14
|
+
.option("-p, --page <n>", "Page number", (v) => parseInt(v, 10), 1)
|
|
15
|
+
.option("--trigger <type>", "Filter by trigger type (cron, http, event, manual, queue)")
|
|
16
|
+
.option("--search <q>", "Search by name or slug")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
var _a, _b, _c, _d, _e;
|
|
19
|
+
try {
|
|
20
|
+
const opts = { ...rootProgram.opts(), ...options };
|
|
21
|
+
spinner.start({ text: "Fetching functions...", color: "cyan" });
|
|
22
|
+
const params = {
|
|
23
|
+
page: opts.page,
|
|
24
|
+
limit: opts.limit,
|
|
25
|
+
responseFormat: opts.responseFormat,
|
|
26
|
+
verbosity: opts.verbosity,
|
|
27
|
+
};
|
|
28
|
+
if (opts.trigger)
|
|
29
|
+
params.triggerType = opts.trigger;
|
|
30
|
+
if (opts.search)
|
|
31
|
+
params.q = opts.search;
|
|
32
|
+
const res = await apiCallHandler(FN_MODULE, FN_COLLECTION, "get", null, params);
|
|
33
|
+
handleResponseFormatOptions(opts, res);
|
|
34
|
+
const total = (_e = (_c = (_b = (_a = res === null || res === void 0 ? void 0 : res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : (_d = res === null || res === void 0 ? void 0 : res.payload) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
|
|
35
|
+
spinner.success({ text: `Found ${total} function${total === 1 ? "" : "s"}` });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
cliError(e.message || "Failed to list functions", EXIT_GENERAL_ERROR);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// `ay functions logs <id-or-slug>` — fetch execution logs.
|
|
2
|
+
//
|
|
3
|
+
// Hits `GET /userfunctions/:id/logs?limit=...`. With `--follow`, polls every
|
|
4
|
+
// 2s and prints any new entries (poor-man's tail -f — the API doesn't expose
|
|
5
|
+
// SSE for function logs yet).
|
|
6
|
+
import chalk from "chalk";
|
|
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 } from "../../exitCodes.js";
|
|
12
|
+
import { FN_MODULE, FN_COLLECTION, resolveFunction } from "./_shared.js";
|
|
13
|
+
function printLogLines(entries) {
|
|
14
|
+
var _a, _b, _c, _d, _e;
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const ts = (_b = (_a = entry.createdAt) !== null && _a !== void 0 ? _a : entry.timestamp) !== null && _b !== void 0 ? _b : "";
|
|
17
|
+
const level = ((_c = entry.level) !== null && _c !== void 0 ? _c : "info").toUpperCase();
|
|
18
|
+
const color = level === "ERROR" ? chalk.red : level === "WARN" ? chalk.yellow : chalk.dim;
|
|
19
|
+
const message = (_e = (_d = entry.message) !== null && _d !== void 0 ? _d : entry.text) !== null && _e !== void 0 ? _e : JSON.stringify(entry);
|
|
20
|
+
console.log(`${chalk.dim(ts)} ${color(level.padEnd(5))} ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function addLogsSubcommand(fns, rootProgram) {
|
|
24
|
+
fns
|
|
25
|
+
.command("logs <id-or-slug>")
|
|
26
|
+
.description("Show execution logs for a function")
|
|
27
|
+
.option("-f, --follow", "Poll for new log entries every 2s", false)
|
|
28
|
+
.option("-t, --tail <n>", "Number of recent entries to show", (v) => parseInt(v, 10), 50)
|
|
29
|
+
.option("--json", "Output raw JSON instead of formatted lines", false)
|
|
30
|
+
.action(async (idOrSlug, options) => {
|
|
31
|
+
var _a;
|
|
32
|
+
try {
|
|
33
|
+
const opts = { ...rootProgram.opts(), ...options };
|
|
34
|
+
spinner.start({ text: `Resolving ${idOrSlug}...`, color: "cyan" });
|
|
35
|
+
const fn = await resolveFunction(idOrSlug);
|
|
36
|
+
spinner.stop();
|
|
37
|
+
const fetchLogs = async (since) => {
|
|
38
|
+
const params = { limit: options.tail };
|
|
39
|
+
if (since)
|
|
40
|
+
params.since = since;
|
|
41
|
+
const res = await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fn._id}/logs`, "get", null, params);
|
|
42
|
+
return Array.isArray(res === null || res === void 0 ? void 0 : res.payload) ? res.payload : [];
|
|
43
|
+
};
|
|
44
|
+
const initial = await fetchLogs();
|
|
45
|
+
if (options.json) {
|
|
46
|
+
handleResponseFormatOptions(opts, { payload: initial, meta: {} });
|
|
47
|
+
if (!options.follow)
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
printLogLines(initial);
|
|
52
|
+
}
|
|
53
|
+
if (!options.follow)
|
|
54
|
+
return;
|
|
55
|
+
let lastSeen = initial.length > 0 ? initial[initial.length - 1].createdAt : new Date().toISOString();
|
|
56
|
+
// Plain polling loop. Ctrl-C exits the process.
|
|
57
|
+
while (true) {
|
|
58
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
59
|
+
const fresh = await fetchLogs(lastSeen);
|
|
60
|
+
if (fresh.length > 0) {
|
|
61
|
+
if (options.json) {
|
|
62
|
+
for (const entry of fresh)
|
|
63
|
+
console.log(JSON.stringify(entry));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
printLogLines(fresh);
|
|
67
|
+
}
|
|
68
|
+
lastSeen = (_a = fresh[fresh.length - 1].createdAt) !== null && _a !== void 0 ? _a : lastSeen;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
cliError(e.message || "Failed to fetch logs", EXIT_GENERAL_ERROR);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `ay functions rollback <id-or-slug> <version>` — roll a function back to a
|
|
2
|
+
// previous version. Wraps `POST /userfunctions/:id/actions/rollback`.
|
|
3
|
+
import { spinner } from "../../../index.js";
|
|
4
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
5
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
6
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
7
|
+
import { FN_MODULE, FN_COLLECTION, resolveFunction } from "./_shared.js";
|
|
8
|
+
export function addRollbackSubcommand(fns, _rootProgram) {
|
|
9
|
+
fns
|
|
10
|
+
.command("rollback <id-or-slug> <version>")
|
|
11
|
+
.description("Roll a function back to a previous version")
|
|
12
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
13
|
+
.action(async (idOrSlug, versionRaw, options) => {
|
|
14
|
+
var _a;
|
|
15
|
+
try {
|
|
16
|
+
const version = parseInt(versionRaw, 10);
|
|
17
|
+
if (Number.isNaN(version) || version < 1) {
|
|
18
|
+
cliError(`Invalid version "${versionRaw}" — expected a positive integer`, EXIT_MISUSE);
|
|
19
|
+
}
|
|
20
|
+
spinner.start({ text: `Resolving ${idOrSlug}...`, color: "cyan" });
|
|
21
|
+
const fn = await resolveFunction(idOrSlug);
|
|
22
|
+
spinner.stop();
|
|
23
|
+
if (!options.yes && process.stdin.isTTY) {
|
|
24
|
+
const inquirer = (await import("inquirer")).default;
|
|
25
|
+
const { ok } = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: "confirm",
|
|
28
|
+
name: "ok",
|
|
29
|
+
message: `Roll "${(_a = fn.name) !== null && _a !== void 0 ? _a : fn.slug}" back to version ${version}?`,
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
if (!ok)
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
spinner.start({ text: `Rolling back to version ${version}...`, color: "yellow" });
|
|
37
|
+
await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fn._id}/actions/rollback`, "post", { version });
|
|
38
|
+
spinner.success({ text: `Rolled back to version ${version}` });
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
cliError(e.message || "Rollback failed", EXIT_GENERAL_ERROR);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `ay functions versions <id-or-slug>` — list version history for a function.
|
|
2
|
+
//
|
|
3
|
+
// Hits `GET /userfunctions/:id/versions`. Used as the input for
|
|
4
|
+
// `ay functions rollback`.
|
|
5
|
+
import { spinner } from "../../../index.js";
|
|
6
|
+
import { apiCallHandler } from "../../api/apiCallHandler.js";
|
|
7
|
+
import { handleResponseFormatOptions } from "../../helpers/handleResponseFormatOptions.js";
|
|
8
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
9
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
10
|
+
import { FN_MODULE, FN_COLLECTION, resolveFunction } from "./_shared.js";
|
|
11
|
+
export function addVersionsSubcommand(fns, rootProgram) {
|
|
12
|
+
fns
|
|
13
|
+
.command("versions <id-or-slug>")
|
|
14
|
+
.description("Show version history for a function")
|
|
15
|
+
.option("-l, --limit <n>", "Limit results", (v) => parseInt(v, 10), 50)
|
|
16
|
+
.action(async (idOrSlug, options) => {
|
|
17
|
+
var _a, _b, _c, _d, _e;
|
|
18
|
+
try {
|
|
19
|
+
const opts = { ...rootProgram.opts(), ...options };
|
|
20
|
+
spinner.start({ text: `Resolving ${idOrSlug}...`, color: "cyan" });
|
|
21
|
+
const fn = await resolveFunction(idOrSlug);
|
|
22
|
+
spinner.update({ text: "Fetching versions..." });
|
|
23
|
+
const res = await apiCallHandler(FN_MODULE, `${FN_COLLECTION}/${fn._id}/versions`, "get", null, { limit: opts.limit });
|
|
24
|
+
const total = (_e = (_c = (_b = (_a = res === null || res === void 0 ? void 0 : res.meta) === null || _a === void 0 ? void 0 : _a.pageInfo) === null || _b === void 0 ? void 0 : _b.totalEntries) !== null && _c !== void 0 ? _c : (_d = res === null || res === void 0 ? void 0 : res.payload) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
|
|
25
|
+
spinner.success({ text: `Found ${total} version${total === 1 ? "" : "s"}` });
|
|
26
|
+
handleResponseFormatOptions(opts, res);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
cliError(e.message || "Failed to fetch versions", EXIT_GENERAL_ERROR);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Shared helpers for the `ay local *` subcommands. Centralized so that
|
|
2
|
+
// every subcommand uses the same compose-context discovery + the same
|
|
3
|
+
// "no docker / no monorepo" error messages — keeps the surface uniform
|
|
4
|
+
// and the per-subcommand files small.
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { detectRuntime, resolveLocalContext, } from "../../helpers/dockerCompose.js";
|
|
7
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
8
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the compose context to use for a `ay local` subcommand.
|
|
11
|
+
*
|
|
12
|
+
* - Verifies docker compose is installed; aborts via `cliError` otherwise.
|
|
13
|
+
* - Tries to find the monorepo's `infrastructure/local/` overlay first.
|
|
14
|
+
* - Falls back to a minimal context (cwd, no extra files) for customers who
|
|
15
|
+
* run `ay local` against a vanilla `docker-compose.yml` in their own
|
|
16
|
+
* directory. The fallback still passes `-p ayoune` so the project name
|
|
17
|
+
* stays consistent across the monorepo and customer installs.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveContextOrExit() {
|
|
20
|
+
const runtime = detectRuntime();
|
|
21
|
+
if (runtime !== "compose") {
|
|
22
|
+
cliError("Docker Compose is not available. Install Docker Desktop (or `docker compose` plugin) and try again.", EXIT_GENERAL_ERROR);
|
|
23
|
+
}
|
|
24
|
+
const ctx = resolveLocalContext();
|
|
25
|
+
if (ctx)
|
|
26
|
+
return ctx;
|
|
27
|
+
// Customer-install fallback — no monorepo overlay, plain `docker-compose.yml` in cwd.
|
|
28
|
+
return {
|
|
29
|
+
projectName: "ayoune",
|
|
30
|
+
cwd: process.cwd(),
|
|
31
|
+
files: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Print a green "next steps" footer after a successful `up`. */
|
|
35
|
+
export function printDevToolsBanner() {
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(chalk.green(" Dev tools:"));
|
|
38
|
+
console.log(chalk.dim(" Traefik Dashboard http://localhost:8080"));
|
|
39
|
+
console.log(chalk.dim(" Redis Commander http://localhost:8081"));
|
|
40
|
+
console.log(chalk.dim(" Bull Monitor http://localhost:8082"));
|
|
41
|
+
console.log("");
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// `ay local down` — stop the local stack.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors `dev.ps1 down` which passes `--profile "*"` so containers from
|
|
4
|
+
// every profile (not just `core`) get cleaned up. Adds a confirmation prompt
|
|
5
|
+
// in TTY mode unless `--force`/`-y` is given.
|
|
6
|
+
import { spinner } from "../../../index.js";
|
|
7
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
8
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
9
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
10
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
11
|
+
export function addDownSubcommand(local) {
|
|
12
|
+
local
|
|
13
|
+
.command("down")
|
|
14
|
+
.description("Stop and remove the local stack containers")
|
|
15
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
16
|
+
.option("--volumes", "Remove named volumes too (DESTRUCTIVE)", false)
|
|
17
|
+
.option("--remove-orphans", "Remove containers for services not defined in the compose files", false)
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const ctx = resolveContextOrExit();
|
|
21
|
+
if (!options.yes && process.stdin.isTTY) {
|
|
22
|
+
const inquirer = (await import("inquirer")).default;
|
|
23
|
+
const message = options.volumes
|
|
24
|
+
? "Stop the local stack AND delete named volumes?"
|
|
25
|
+
: "Stop the local stack?";
|
|
26
|
+
const ans = await inquirer.prompt([
|
|
27
|
+
{ type: "confirm", name: "ok", message, default: false },
|
|
28
|
+
]);
|
|
29
|
+
if (!ans.ok)
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const args = ["--profile", "*", "down"];
|
|
33
|
+
if (options.volumes)
|
|
34
|
+
args.push("--volumes");
|
|
35
|
+
if (options.removeOrphans)
|
|
36
|
+
args.push("--remove-orphans");
|
|
37
|
+
spinner.start({ text: "Stopping local stack...", color: "yellow" });
|
|
38
|
+
const code = await streamCompose(ctx, args);
|
|
39
|
+
if (code !== 0) {
|
|
40
|
+
spinner.error({ text: "docker compose down failed" });
|
|
41
|
+
cliError("Failed to stop local stack", EXIT_GENERAL_ERROR);
|
|
42
|
+
}
|
|
43
|
+
spinner.success({ text: "Local stack stopped" });
|
|
44
|
+
spinner.stop();
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
cliError(e.message || "Failed to stop local stack", EXIT_GENERAL_ERROR);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `ay local exec <service> <cmd...>` — run a command inside a service container.
|
|
2
|
+
//
|
|
3
|
+
// Forwards everything after the service name as the command + args. Always
|
|
4
|
+
// passes `-T` only when stdin is NOT a TTY (matches `docker compose exec`'s
|
|
5
|
+
// own auto-detection but explicit so this works under nohup/CI).
|
|
6
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
7
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
8
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
9
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
10
|
+
export function addExecSubcommand(local) {
|
|
11
|
+
local
|
|
12
|
+
.command("exec <service> [args...]")
|
|
13
|
+
.description("Run a command inside a service container")
|
|
14
|
+
.allowUnknownOption(true)
|
|
15
|
+
.option("-u, --user <user>", "Run as a specific user")
|
|
16
|
+
.option("-w, --workdir <dir>", "Working directory inside the container")
|
|
17
|
+
.action(async (service, args, options) => {
|
|
18
|
+
try {
|
|
19
|
+
if (!service)
|
|
20
|
+
cliError("Service name is required", EXIT_MISUSE);
|
|
21
|
+
const ctx = resolveContextOrExit();
|
|
22
|
+
const composeArgs = ["exec"];
|
|
23
|
+
if (!process.stdin.isTTY)
|
|
24
|
+
composeArgs.push("-T");
|
|
25
|
+
if (options.user)
|
|
26
|
+
composeArgs.push("-u", options.user);
|
|
27
|
+
if (options.workdir)
|
|
28
|
+
composeArgs.push("-w", options.workdir);
|
|
29
|
+
composeArgs.push(service);
|
|
30
|
+
if (args && args.length > 0) {
|
|
31
|
+
composeArgs.push(...args);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Sensible default — drop into a shell. `sh` is more portable than `bash`.
|
|
35
|
+
composeArgs.push("sh");
|
|
36
|
+
}
|
|
37
|
+
const code = await streamCompose(ctx, composeArgs);
|
|
38
|
+
if (code !== 0)
|
|
39
|
+
process.exit(code);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
cliError(e.message || "Exec failed", EXIT_GENERAL_ERROR);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `ay local` parent command — cross-platform Docker Compose wrapper that
|
|
2
|
+
// replaces `infrastructure/local/dev.ps1` for end-users on macOS/Linux as
|
|
3
|
+
// well as Windows. Each subcommand wraps a single `docker compose` verb and
|
|
4
|
+
// reuses the shared helper in `src/lib/helpers/dockerCompose.ts` so that the
|
|
5
|
+
// `ay self-host-update` command and these stay in lock-step.
|
|
6
|
+
//
|
|
7
|
+
// All subcommands prefer the monorepo's `infrastructure/local/` overlay
|
|
8
|
+
// (auto-discovered via `resolveLocalContext`) when run from inside the
|
|
9
|
+
// monorepo. When run from a customer install, they fall back to the local
|
|
10
|
+
// directory's `docker-compose.yml`.
|
|
11
|
+
import { addUpSubcommand } from "./up.js";
|
|
12
|
+
import { addDownSubcommand } from "./down.js";
|
|
13
|
+
import { addRestartSubcommand } from "./restart.js";
|
|
14
|
+
import { addLogsSubcommand } from "./logs.js";
|
|
15
|
+
import { addPsSubcommand } from "./ps.js";
|
|
16
|
+
import { addPullSubcommand } from "./pull.js";
|
|
17
|
+
import { addExecSubcommand } from "./exec.js";
|
|
18
|
+
export function createLocalCommand(program) {
|
|
19
|
+
const local = program
|
|
20
|
+
.command("local")
|
|
21
|
+
.description("Run a self-hosted aYOUne instance locally via Docker Compose")
|
|
22
|
+
.addHelpText("after", `
|
|
23
|
+
Examples:
|
|
24
|
+
ay local up Start core services
|
|
25
|
+
ay local up crm marketing Start core + CRM + marketing profiles
|
|
26
|
+
ay local ps Show running services
|
|
27
|
+
ay local logs api-config -f Tail config-api logs
|
|
28
|
+
ay local restart api-crm Restart a single service
|
|
29
|
+
ay local pull Pull latest images
|
|
30
|
+
ay local exec api-crm sh Open a shell in a container
|
|
31
|
+
ay local down Stop everything`);
|
|
32
|
+
addUpSubcommand(local);
|
|
33
|
+
addDownSubcommand(local);
|
|
34
|
+
addRestartSubcommand(local);
|
|
35
|
+
addLogsSubcommand(local);
|
|
36
|
+
addPsSubcommand(local);
|
|
37
|
+
addPullSubcommand(local);
|
|
38
|
+
addExecSubcommand(local);
|
|
39
|
+
}
|
|
40
|
+
export default createLocalCommand;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// `ay local logs [service] [--follow] [--tail N]` — tail compose logs.
|
|
2
|
+
//
|
|
3
|
+
// Without a service, streams logs for the whole stack. With `-f/--follow`,
|
|
4
|
+
// stays attached until the user hits Ctrl-C; otherwise prints the last
|
|
5
|
+
// `--tail N` lines (default 100) and exits.
|
|
6
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
7
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
8
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
9
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
10
|
+
export function addLogsSubcommand(local) {
|
|
11
|
+
local
|
|
12
|
+
.command("logs [service]")
|
|
13
|
+
.description("Tail logs from the local stack")
|
|
14
|
+
.option("-f, --follow", "Follow log output", false)
|
|
15
|
+
.option("-t, --tail <n>", "Number of lines to show from the end of each container", "100")
|
|
16
|
+
.option("--timestamps", "Show timestamps", false)
|
|
17
|
+
.action(async (service, options) => {
|
|
18
|
+
try {
|
|
19
|
+
const ctx = resolveContextOrExit();
|
|
20
|
+
const args = ["logs"];
|
|
21
|
+
if (options.follow)
|
|
22
|
+
args.push("-f");
|
|
23
|
+
if (options.tail)
|
|
24
|
+
args.push("--tail", String(options.tail));
|
|
25
|
+
if (options.timestamps)
|
|
26
|
+
args.push("--timestamps");
|
|
27
|
+
if (service)
|
|
28
|
+
args.push(service);
|
|
29
|
+
const code = await streamCompose(ctx, args);
|
|
30
|
+
if (code !== 0 && !options.follow) {
|
|
31
|
+
cliError("docker compose logs failed", EXIT_GENERAL_ERROR);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
cliError(e.message || "Failed to read logs", EXIT_GENERAL_ERROR);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// `ay local ps` — list running services.
|
|
2
|
+
//
|
|
3
|
+
// Two modes:
|
|
4
|
+
// - default: streams `docker compose ps` with a pretty table format
|
|
5
|
+
// (matches dev.ps1's `--format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"`)
|
|
6
|
+
// - `--json`: synchronously runs `docker compose ps --format json` and pipes
|
|
7
|
+
// the output through `handleResponseFormatOptions` so the user can pipe it
|
|
8
|
+
// into jq / save it. Keeps the CLI's response-format conventions intact
|
|
9
|
+
// for scripted usage.
|
|
10
|
+
import { runCompose, streamCompose } from "../../helpers/dockerCompose.js";
|
|
11
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
12
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
13
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
14
|
+
export function addPsSubcommand(local) {
|
|
15
|
+
local
|
|
16
|
+
.command("ps")
|
|
17
|
+
.description("List running services in the local stack")
|
|
18
|
+
.option("--json", "Output JSON instead of a table", false)
|
|
19
|
+
.option("-a, --all", "Show stopped containers too", false)
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
const ctx = resolveContextOrExit();
|
|
23
|
+
const args = ["ps"];
|
|
24
|
+
if (options.all)
|
|
25
|
+
args.push("-a");
|
|
26
|
+
if (options.json) {
|
|
27
|
+
args.push("--format", "json");
|
|
28
|
+
const out = runCompose(ctx, args, { silent: false });
|
|
29
|
+
console.log(out);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
args.push("--format", "table {{.Name}}\t{{.Status}}\t{{.Ports}}");
|
|
33
|
+
const code = await streamCompose(ctx, args);
|
|
34
|
+
if (code !== 0)
|
|
35
|
+
cliError("docker compose ps failed", EXIT_GENERAL_ERROR);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
cliError(e.message || "Failed to list services", EXIT_GENERAL_ERROR);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `ay local pull [profiles...]` — pull latest images for the local stack.
|
|
2
|
+
//
|
|
3
|
+
// Like `up`, always includes the `core` profile so foundation images are
|
|
4
|
+
// refreshed even when the user only asked for a sub-set. Doesn't restart
|
|
5
|
+
// anything — that's `ay local restart` or `ay local up` after a pull.
|
|
6
|
+
import { spinner } from "../../../index.js";
|
|
7
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
8
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
9
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
10
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
11
|
+
export function addPullSubcommand(local) {
|
|
12
|
+
local
|
|
13
|
+
.command("pull [profiles...]")
|
|
14
|
+
.description("Pull the latest images for the local stack")
|
|
15
|
+
.option("--quiet", "Suppress per-image progress output", false)
|
|
16
|
+
.action(async (profiles, options) => {
|
|
17
|
+
try {
|
|
18
|
+
const ctx = resolveContextOrExit();
|
|
19
|
+
const allProfiles = ["core", ...(profiles !== null && profiles !== void 0 ? profiles : [])];
|
|
20
|
+
const profileArgs = allProfiles.flatMap((p) => ["--profile", p]);
|
|
21
|
+
const args = [...profileArgs, "pull"];
|
|
22
|
+
if (options.quiet)
|
|
23
|
+
args.push("--quiet");
|
|
24
|
+
spinner.start({
|
|
25
|
+
text: `Pulling images for: ${allProfiles.join(", ")}`,
|
|
26
|
+
color: "cyan",
|
|
27
|
+
});
|
|
28
|
+
const code = await streamCompose(ctx, args);
|
|
29
|
+
if (code !== 0) {
|
|
30
|
+
spinner.error({ text: "docker compose pull failed" });
|
|
31
|
+
cliError("Image pull failed", EXIT_GENERAL_ERROR);
|
|
32
|
+
}
|
|
33
|
+
spinner.success({ text: "Images pulled" });
|
|
34
|
+
spinner.stop();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
cliError(e.message || "Image pull failed", EXIT_GENERAL_ERROR);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `ay local restart [service]` — restart the whole stack or a single service.
|
|
2
|
+
import { spinner } from "../../../index.js";
|
|
3
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
4
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
5
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
6
|
+
import { resolveContextOrExit } from "./_context.js";
|
|
7
|
+
export function addRestartSubcommand(local) {
|
|
8
|
+
local
|
|
9
|
+
.command("restart [service]")
|
|
10
|
+
.description("Restart the entire stack or a single service")
|
|
11
|
+
.action(async (service) => {
|
|
12
|
+
try {
|
|
13
|
+
const ctx = resolveContextOrExit();
|
|
14
|
+
const args = service ? ["restart", service] : ["restart"];
|
|
15
|
+
spinner.start({
|
|
16
|
+
text: service ? `Restarting ${service}...` : "Restarting all services...",
|
|
17
|
+
color: "yellow",
|
|
18
|
+
});
|
|
19
|
+
const code = await streamCompose(ctx, args);
|
|
20
|
+
if (code !== 0) {
|
|
21
|
+
spinner.error({ text: "docker compose restart failed" });
|
|
22
|
+
cliError("Failed to restart", EXIT_GENERAL_ERROR);
|
|
23
|
+
}
|
|
24
|
+
spinner.success({ text: service ? `${service} restarted` : "All services restarted" });
|
|
25
|
+
spinner.stop();
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
cliError(e.message || "Failed to restart", EXIT_GENERAL_ERROR);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `ay local up [profiles...]` — start the local stack.
|
|
2
|
+
//
|
|
3
|
+
// Always passes `--profile core` so foundation services come up. Extra
|
|
4
|
+
// profiles are appended on top. With no args and a TTY, prompts the user
|
|
5
|
+
// to pick optional profiles via inquirer (wizard-first DX). Non-TTY or
|
|
6
|
+
// when called with explicit profile args, runs non-interactively.
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { spinner } from "../../../index.js";
|
|
9
|
+
import { streamCompose } from "../../helpers/dockerCompose.js";
|
|
10
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
11
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
12
|
+
import { resolveContextOrExit, printDevToolsBanner } from "./_context.js";
|
|
13
|
+
const OPTIONAL_PROFILES = [
|
|
14
|
+
"crm",
|
|
15
|
+
"marketing",
|
|
16
|
+
"hr",
|
|
17
|
+
"ecommerce",
|
|
18
|
+
"pm",
|
|
19
|
+
"automation",
|
|
20
|
+
"accounting",
|
|
21
|
+
"cms",
|
|
22
|
+
"monitoring",
|
|
23
|
+
"observability",
|
|
24
|
+
"devops",
|
|
25
|
+
"support",
|
|
26
|
+
"reporting",
|
|
27
|
+
];
|
|
28
|
+
export function addUpSubcommand(local) {
|
|
29
|
+
local
|
|
30
|
+
.command("up [profiles...]")
|
|
31
|
+
.description("Start the local stack (core profile is always included)")
|
|
32
|
+
.option("-d, --detach", "Run in detached mode (default)", true)
|
|
33
|
+
.option("--no-detach", "Run in foreground (stream logs to terminal)")
|
|
34
|
+
.option("--build", "Build images before starting")
|
|
35
|
+
.action(async (profiles, options) => {
|
|
36
|
+
try {
|
|
37
|
+
const ctx = resolveContextOrExit();
|
|
38
|
+
// Wizard mode — TTY + no profiles given.
|
|
39
|
+
let selected = profiles !== null && profiles !== void 0 ? profiles : [];
|
|
40
|
+
if (selected.length === 0 && process.stdin.isTTY) {
|
|
41
|
+
const inquirer = (await import("inquirer")).default;
|
|
42
|
+
const ans = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: "checkbox",
|
|
45
|
+
name: "extras",
|
|
46
|
+
message: "Select optional profiles to start (core is always included):",
|
|
47
|
+
choices: OPTIONAL_PROFILES.map((p) => ({ name: p, value: p })),
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
selected = ans.extras;
|
|
51
|
+
}
|
|
52
|
+
const allProfiles = ["core", ...selected];
|
|
53
|
+
const profileArgs = allProfiles.flatMap((p) => ["--profile", p]);
|
|
54
|
+
const composeArgs = [...profileArgs, "up"];
|
|
55
|
+
if (options.detach)
|
|
56
|
+
composeArgs.push("-d");
|
|
57
|
+
if (options.build)
|
|
58
|
+
composeArgs.push("--build");
|
|
59
|
+
spinner.start({
|
|
60
|
+
text: `Starting profiles: ${allProfiles.join(", ")}`,
|
|
61
|
+
color: "cyan",
|
|
62
|
+
});
|
|
63
|
+
const code = await streamCompose(ctx, composeArgs);
|
|
64
|
+
if (code !== 0) {
|
|
65
|
+
spinner.error({ text: "docker compose up failed" });
|
|
66
|
+
cliError("Failed to start local stack", EXIT_GENERAL_ERROR);
|
|
67
|
+
}
|
|
68
|
+
spinner.success({ text: "Local stack is up" });
|
|
69
|
+
spinner.stop();
|
|
70
|
+
if (options.detach) {
|
|
71
|
+
printDevToolsBanner();
|
|
72
|
+
console.log(chalk.dim(" Tip: use `ay local ps` to list services and `ay local logs <svc>` to tail logs."));
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
cliError(e.message || "Failed to start local stack", EXIT_GENERAL_ERROR);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|