@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,52 @@
|
|
|
1
|
+
// Provider-tool detection — checks whether the CLI binary needed by a
|
|
2
|
+
// provider subcommand is installed and prints a clear "how to install"
|
|
3
|
+
// message if not. Pattern lifted from `detectRuntime()` in
|
|
4
|
+
// `createSelfHostUpdateCommand.ts`.
|
|
5
|
+
import { runCommand } from "../../helpers/dockerCompose.js";
|
|
6
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
7
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
8
|
+
const TOOL_PROBES = {
|
|
9
|
+
terraform: {
|
|
10
|
+
cmd: "terraform version",
|
|
11
|
+
expect: "Terraform v",
|
|
12
|
+
install: "Install Terraform: https://developer.hashicorp.com/terraform/install — or `brew install terraform` / `choco install terraform`.",
|
|
13
|
+
},
|
|
14
|
+
aws: {
|
|
15
|
+
cmd: "aws --version",
|
|
16
|
+
expect: "aws-cli",
|
|
17
|
+
install: "Install AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
|
|
18
|
+
},
|
|
19
|
+
gcloud: {
|
|
20
|
+
cmd: "gcloud --version",
|
|
21
|
+
expect: "Google Cloud SDK",
|
|
22
|
+
install: "Install gcloud: https://cloud.google.com/sdk/docs/install",
|
|
23
|
+
},
|
|
24
|
+
az: {
|
|
25
|
+
cmd: "az --version",
|
|
26
|
+
expect: "azure-cli",
|
|
27
|
+
install: "Install Azure CLI: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli",
|
|
28
|
+
},
|
|
29
|
+
doctl: {
|
|
30
|
+
cmd: "doctl version",
|
|
31
|
+
expect: "doctl version",
|
|
32
|
+
install: "Install doctl: https://docs.digitalocean.com/reference/doctl/how-to/install/",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Verify that a provider tool is installed. Aborts via cliError when
|
|
37
|
+
* missing — the user gets one clear "install X" line instead of a confusing
|
|
38
|
+
* shell error from the actual provider command.
|
|
39
|
+
*/
|
|
40
|
+
export function requireTool(tool) {
|
|
41
|
+
const probe = TOOL_PROBES[tool];
|
|
42
|
+
const out = runCommand(`${probe.cmd} 2>&1`);
|
|
43
|
+
if (!out.toLowerCase().includes(probe.expect.toLowerCase())) {
|
|
44
|
+
cliError(`${tool} is not installed or not on PATH.\n ${probe.install}`, EXIT_GENERAL_ERROR);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Check whether a tool is installed without aborting — used by `ay doctor`. */
|
|
48
|
+
export function hasTool(tool) {
|
|
49
|
+
const probe = TOOL_PROBES[tool];
|
|
50
|
+
const out = runCommand(`${probe.cmd} 2>&1`);
|
|
51
|
+
return out.toLowerCase().includes(probe.expect.toLowerCase());
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Per-provider provisioning state — persisted to
|
|
2
|
+
// `~/.ayoune/provision/<provider>.json`. Holds whatever the wizard collected
|
|
3
|
+
// + outputs from the provider tool (terraform outputs, CFN stack name, etc.)
|
|
4
|
+
// so `ay provision status` and `ay provision destroy` can find their way
|
|
5
|
+
// back without re-prompting.
|
|
6
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import os from "os";
|
|
10
|
+
export function getStateDir() {
|
|
11
|
+
return path.join(os.homedir(), ".ayoune", "provision");
|
|
12
|
+
}
|
|
13
|
+
export function getStatePath(provider) {
|
|
14
|
+
return path.join(getStateDir(), `${provider}.json`);
|
|
15
|
+
}
|
|
16
|
+
export async function readState(provider) {
|
|
17
|
+
const file = getStatePath(provider);
|
|
18
|
+
if (!existsSync(file))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(file, "utf-8");
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch (_a) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function writeState(state) {
|
|
29
|
+
const dir = getStateDir();
|
|
30
|
+
if (!existsSync(dir))
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
const file = getStatePath(state.provider);
|
|
33
|
+
state.updatedAt = new Date().toISOString();
|
|
34
|
+
await writeFile(file, JSON.stringify(state, null, 2), "utf-8");
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared param-collection wizard. Each provider subcommand starts here to
|
|
2
|
+
// gather the cross-cutting fields (domain, license key, modules, contact
|
|
3
|
+
// email) before tacking on its provider-specific questions. Keeps the
|
|
4
|
+
// per-provider files focused on what's actually different between them.
|
|
5
|
+
export const COMMON_MODULES = [
|
|
6
|
+
"crm",
|
|
7
|
+
"marketing",
|
|
8
|
+
"hr",
|
|
9
|
+
"ecommerce",
|
|
10
|
+
"pm",
|
|
11
|
+
"automation",
|
|
12
|
+
"accounting",
|
|
13
|
+
"support",
|
|
14
|
+
"reporting",
|
|
15
|
+
"monitoring",
|
|
16
|
+
];
|
|
17
|
+
export async function runBaseWizard(opts) {
|
|
18
|
+
const inquirer = (await import("inquirer")).default;
|
|
19
|
+
return inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: "input",
|
|
22
|
+
name: "domain",
|
|
23
|
+
message: "Domain to deploy under (e.g. ayoune.example.com):",
|
|
24
|
+
validate: (v) => v.length > 3 || "Domain is required",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "input",
|
|
28
|
+
name: "contactEmail",
|
|
29
|
+
message: "Contact email (used for cert-manager / billing):",
|
|
30
|
+
validate: (v) => /.+@.+\..+/.test(v) || "Valid email required",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "input",
|
|
34
|
+
name: "licenseKey",
|
|
35
|
+
message: "License key (leave blank for 14-day trial):",
|
|
36
|
+
default: "",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "checkbox",
|
|
40
|
+
name: "modules",
|
|
41
|
+
message: "Select modules to enable:",
|
|
42
|
+
choices: COMMON_MODULES,
|
|
43
|
+
default: ["crm"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: opts.regionChoices ? "list" : "input",
|
|
47
|
+
name: "region",
|
|
48
|
+
message: "Region:",
|
|
49
|
+
choices: opts.regionChoices,
|
|
50
|
+
default: opts.defaultRegion,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: opts.instanceChoices ? "list" : "input",
|
|
54
|
+
name: "instanceSize",
|
|
55
|
+
message: "Instance size:",
|
|
56
|
+
choices: opts.instanceChoices,
|
|
57
|
+
default: opts.defaultInstance,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// `ay provision aws` — deploy aYOUne to AWS via the marketplace
|
|
2
|
+
// CloudFormation template at infrastructure/marketplace/aws/cloudformation.yaml.
|
|
3
|
+
//
|
|
4
|
+
// Far thinner than the Hetzner subcommand because CloudFormation handles all
|
|
5
|
+
// the heavy lifting — we just collect parameters and shell out to
|
|
6
|
+
// `aws cloudformation deploy`.
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { spinner } from "../../../index.js";
|
|
12
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
13
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
14
|
+
import { requireTool } from "./_detectTools.js";
|
|
15
|
+
import { runBaseWizard } from "./_wizard.js";
|
|
16
|
+
import { writeState } from "./_stateFile.js";
|
|
17
|
+
const AWS_REGIONS = ["us-east-1", "us-west-2", "eu-central-1", "eu-west-1", "ap-southeast-1"];
|
|
18
|
+
const AWS_INSTANCE_TYPES = ["t3.medium", "t3.large", "m6i.large", "m6i.xlarge", "c6i.xlarge"];
|
|
19
|
+
function streamCmd(cmd, args) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const child = spawn(cmd, args, {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
shell: process.platform === "win32",
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", (code) => resolve(code !== null && code !== void 0 ? code : 0));
|
|
26
|
+
child.on("error", () => resolve(1));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function addAwsSubcommand(prov) {
|
|
30
|
+
prov
|
|
31
|
+
.command("aws")
|
|
32
|
+
.description("Provision aYOUne to AWS via CloudFormation")
|
|
33
|
+
.option("--stack-name <name>", "CloudFormation stack name", "ayoune")
|
|
34
|
+
.option("--region <region>", "AWS region")
|
|
35
|
+
.option("--dry-run", "Show what would be deployed without applying", false)
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
var _a;
|
|
38
|
+
try {
|
|
39
|
+
requireTool("aws");
|
|
40
|
+
const template = findMarketplaceFile("aws", "cloudformation.yaml");
|
|
41
|
+
if (!template) {
|
|
42
|
+
cliError("Could not find infrastructure/marketplace/aws/cloudformation.yaml — run from the monorepo.", EXIT_GENERAL_ERROR);
|
|
43
|
+
}
|
|
44
|
+
const base = await runBaseWizard({
|
|
45
|
+
regionChoices: AWS_REGIONS,
|
|
46
|
+
defaultRegion: (_a = options.region) !== null && _a !== void 0 ? _a : "us-east-1",
|
|
47
|
+
instanceChoices: AWS_INSTANCE_TYPES,
|
|
48
|
+
defaultInstance: "t3.large",
|
|
49
|
+
});
|
|
50
|
+
const params = [
|
|
51
|
+
`Domain=${base.domain}`,
|
|
52
|
+
`ContactEmail=${base.contactEmail}`,
|
|
53
|
+
`LicenseKey=${base.licenseKey || "trial"}`,
|
|
54
|
+
`InstanceType=${base.instanceSize}`,
|
|
55
|
+
`EnabledModules=${base.modules.join(",")}`,
|
|
56
|
+
];
|
|
57
|
+
if (options.dryRun) {
|
|
58
|
+
console.log(chalk.yellow("\n --dry-run set — would run:"));
|
|
59
|
+
console.log(chalk.dim(` aws cloudformation deploy --template-file ${template} --stack-name ${options.stackName} --region ${base.region} --parameter-overrides ${params.join(" ")}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
spinner.start({ text: "Deploying CloudFormation stack...", color: "cyan" });
|
|
63
|
+
const code = await streamCmd("aws", [
|
|
64
|
+
"cloudformation",
|
|
65
|
+
"deploy",
|
|
66
|
+
"--template-file",
|
|
67
|
+
template,
|
|
68
|
+
"--stack-name",
|
|
69
|
+
options.stackName,
|
|
70
|
+
"--region",
|
|
71
|
+
base.region,
|
|
72
|
+
"--capabilities",
|
|
73
|
+
"CAPABILITY_NAMED_IAM",
|
|
74
|
+
"--parameter-overrides",
|
|
75
|
+
...params,
|
|
76
|
+
]);
|
|
77
|
+
if (code !== 0)
|
|
78
|
+
cliError("CloudFormation deploy failed", EXIT_GENERAL_ERROR);
|
|
79
|
+
spinner.success({ text: "Stack deployed" });
|
|
80
|
+
const file = await writeState({
|
|
81
|
+
provider: "aws",
|
|
82
|
+
createdAt: new Date().toISOString(),
|
|
83
|
+
updatedAt: new Date().toISOString(),
|
|
84
|
+
region: base.region,
|
|
85
|
+
domain: base.domain,
|
|
86
|
+
modules: base.modules,
|
|
87
|
+
params: { stackName: options.stackName, instanceType: base.instanceSize },
|
|
88
|
+
});
|
|
89
|
+
console.log(chalk.dim(` State file: ${file}`));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
cliError(e.message || "AWS provisioning failed", EXIT_GENERAL_ERROR);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function findMarketplaceFile(provider, file) {
|
|
97
|
+
let dir = process.cwd();
|
|
98
|
+
while (true) {
|
|
99
|
+
const candidate = path.join(dir, "infrastructure", "marketplace", provider, file);
|
|
100
|
+
if (existsSync(candidate))
|
|
101
|
+
return candidate;
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir)
|
|
104
|
+
return null;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `ay provision azure` — deploy aYOUne to Azure via the marketplace ARM
|
|
2
|
+
// template at infrastructure/marketplace/azure/azuredeploy.json.
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import { spinner } from "../../../index.js";
|
|
8
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
9
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
10
|
+
import { requireTool } from "./_detectTools.js";
|
|
11
|
+
import { runBaseWizard } from "./_wizard.js";
|
|
12
|
+
import { writeState } from "./_stateFile.js";
|
|
13
|
+
const AZURE_REGIONS = ["westeurope", "northeurope", "eastus", "westus2", "southeastasia"];
|
|
14
|
+
const AZURE_VM_SIZES = ["Standard_D2s_v5", "Standard_D4s_v5", "Standard_D8s_v5"];
|
|
15
|
+
function streamCmd(cmd, args) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const child = spawn(cmd, args, {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
shell: process.platform === "win32",
|
|
20
|
+
});
|
|
21
|
+
child.on("exit", (code) => resolve(code !== null && code !== void 0 ? code : 0));
|
|
22
|
+
child.on("error", () => resolve(1));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function addAzureSubcommand(prov) {
|
|
26
|
+
prov
|
|
27
|
+
.command("azure")
|
|
28
|
+
.description("Provision aYOUne to Azure via ARM template")
|
|
29
|
+
.option("--resource-group <rg>", "Resource group name", "ayoune")
|
|
30
|
+
.option("--region <region>", "Azure region")
|
|
31
|
+
.option("--dry-run", "Print commands without running them", false)
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
var _a;
|
|
34
|
+
try {
|
|
35
|
+
requireTool("az");
|
|
36
|
+
const template = findMarketplaceFile("azure", "azuredeploy.json");
|
|
37
|
+
if (!template) {
|
|
38
|
+
cliError("Could not find infrastructure/marketplace/azure/azuredeploy.json — run from the monorepo.", EXIT_GENERAL_ERROR);
|
|
39
|
+
}
|
|
40
|
+
const base = await runBaseWizard({
|
|
41
|
+
regionChoices: AZURE_REGIONS,
|
|
42
|
+
defaultRegion: (_a = options.region) !== null && _a !== void 0 ? _a : "westeurope",
|
|
43
|
+
instanceChoices: AZURE_VM_SIZES,
|
|
44
|
+
defaultInstance: "Standard_D4s_v5",
|
|
45
|
+
});
|
|
46
|
+
const params = [
|
|
47
|
+
`domain=${base.domain}`,
|
|
48
|
+
`contactEmail=${base.contactEmail}`,
|
|
49
|
+
`licenseKey=${base.licenseKey || "trial"}`,
|
|
50
|
+
`vmSize=${base.instanceSize}`,
|
|
51
|
+
`enabledModules=${base.modules.join(",")}`,
|
|
52
|
+
];
|
|
53
|
+
if (options.dryRun) {
|
|
54
|
+
console.log(chalk.yellow("\n --dry-run set — would run:"));
|
|
55
|
+
console.log(chalk.dim(` az group create --name ${options.resourceGroup} --location ${base.region}`));
|
|
56
|
+
console.log(chalk.dim(` az deployment group create --resource-group ${options.resourceGroup} --template-file ${template} --parameters ${params.join(" ")}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
spinner.start({ text: "Creating resource group...", color: "cyan" });
|
|
60
|
+
const groupCode = await streamCmd("az", [
|
|
61
|
+
"group",
|
|
62
|
+
"create",
|
|
63
|
+
"--name",
|
|
64
|
+
options.resourceGroup,
|
|
65
|
+
"--location",
|
|
66
|
+
base.region,
|
|
67
|
+
]);
|
|
68
|
+
if (groupCode !== 0)
|
|
69
|
+
cliError("Resource group create failed", EXIT_GENERAL_ERROR);
|
|
70
|
+
spinner.success({ text: "Resource group ready" });
|
|
71
|
+
spinner.start({ text: "Deploying ARM template...", color: "cyan" });
|
|
72
|
+
const deployCode = await streamCmd("az", [
|
|
73
|
+
"deployment",
|
|
74
|
+
"group",
|
|
75
|
+
"create",
|
|
76
|
+
"--resource-group",
|
|
77
|
+
options.resourceGroup,
|
|
78
|
+
"--template-file",
|
|
79
|
+
template,
|
|
80
|
+
"--parameters",
|
|
81
|
+
...params,
|
|
82
|
+
]);
|
|
83
|
+
if (deployCode !== 0)
|
|
84
|
+
cliError("ARM deployment failed", EXIT_GENERAL_ERROR);
|
|
85
|
+
spinner.success({ text: "ARM deployment complete" });
|
|
86
|
+
const file = await writeState({
|
|
87
|
+
provider: "azure",
|
|
88
|
+
createdAt: new Date().toISOString(),
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
90
|
+
region: base.region,
|
|
91
|
+
domain: base.domain,
|
|
92
|
+
modules: base.modules,
|
|
93
|
+
params: { resourceGroup: options.resourceGroup, vmSize: base.instanceSize },
|
|
94
|
+
});
|
|
95
|
+
console.log(chalk.dim(` State file: ${file}`));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
cliError(e.message || "Azure provisioning failed", EXIT_GENERAL_ERROR);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function findMarketplaceFile(provider, file) {
|
|
103
|
+
let dir = process.cwd();
|
|
104
|
+
while (true) {
|
|
105
|
+
const candidate = path.join(dir, "infrastructure", "marketplace", provider, file);
|
|
106
|
+
if (existsSync(candidate))
|
|
107
|
+
return candidate;
|
|
108
|
+
const parent = path.dirname(dir);
|
|
109
|
+
if (parent === dir)
|
|
110
|
+
return null;
|
|
111
|
+
dir = parent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// `ay provision destroy <provider>` — tear down a provisioned environment.
|
|
2
|
+
//
|
|
3
|
+
// Reads the saved state file, then dispatches to the provider-native
|
|
4
|
+
// teardown command. Always confirms first (unless --yes) — this is the
|
|
5
|
+
// most destructive command in the CLI.
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { spinner } from "../../../index.js";
|
|
9
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
10
|
+
import { EXIT_GENERAL_ERROR, EXIT_MISUSE } from "../../exitCodes.js";
|
|
11
|
+
import { readState } from "./_stateFile.js";
|
|
12
|
+
import { requireTool } from "./_detectTools.js";
|
|
13
|
+
const PROVIDERS = ["hetzner", "aws", "gcp", "azure", "digitalocean"];
|
|
14
|
+
function streamCmd(cmd, args, cwd) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = spawn(cmd, args, {
|
|
17
|
+
cwd,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
shell: process.platform === "win32",
|
|
20
|
+
});
|
|
21
|
+
child.on("exit", (code) => resolve(code !== null && code !== void 0 ? code : 0));
|
|
22
|
+
child.on("error", () => resolve(1));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function addDestroySubcommand(prov) {
|
|
26
|
+
prov
|
|
27
|
+
.command("destroy <provider>")
|
|
28
|
+
.description("Tear down a provisioned environment for the given provider")
|
|
29
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
30
|
+
.action(async (provider, options) => {
|
|
31
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
32
|
+
try {
|
|
33
|
+
if (!PROVIDERS.includes(provider)) {
|
|
34
|
+
cliError(`Unknown provider "${provider}" — expected one of: ${PROVIDERS.join(", ")}`, EXIT_MISUSE);
|
|
35
|
+
}
|
|
36
|
+
const state = await readState(provider);
|
|
37
|
+
if (!state) {
|
|
38
|
+
cliError(`No provisioning state for "${provider}". Nothing to destroy.`, EXIT_MISUSE);
|
|
39
|
+
}
|
|
40
|
+
if (!options.yes && process.stdin.isTTY) {
|
|
41
|
+
const inquirer = (await import("inquirer")).default;
|
|
42
|
+
const { ok } = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: "confirm",
|
|
45
|
+
name: "ok",
|
|
46
|
+
message: chalk.red(`DESTROY the ${provider} deployment for ${state.domain}? This is irreversible.`),
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
if (!ok)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let code;
|
|
54
|
+
switch (provider) {
|
|
55
|
+
case "hetzner": {
|
|
56
|
+
requireTool("terraform");
|
|
57
|
+
const workDir = (_a = state.params) === null || _a === void 0 ? void 0 : _a.workDir;
|
|
58
|
+
if (!workDir)
|
|
59
|
+
cliError("Missing terraform working dir in state file", EXIT_GENERAL_ERROR);
|
|
60
|
+
spinner.start({ text: "Running `terraform destroy`...", color: "red" });
|
|
61
|
+
code = await streamCmd("terraform", ["destroy", "-auto-approve"], workDir);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "aws": {
|
|
65
|
+
requireTool("aws");
|
|
66
|
+
const stackName = (_c = (_b = state.params) === null || _b === void 0 ? void 0 : _b.stackName) !== null && _c !== void 0 ? _c : "ayoune";
|
|
67
|
+
spinner.start({ text: "Deleting CloudFormation stack...", color: "red" });
|
|
68
|
+
code = await streamCmd("aws", [
|
|
69
|
+
"cloudformation",
|
|
70
|
+
"delete-stack",
|
|
71
|
+
"--stack-name",
|
|
72
|
+
stackName,
|
|
73
|
+
"--region",
|
|
74
|
+
(_d = state.region) !== null && _d !== void 0 ? _d : "",
|
|
75
|
+
]);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "gcp": {
|
|
79
|
+
requireTool("gcloud");
|
|
80
|
+
const cluster = (_f = (_e = state.params) === null || _e === void 0 ? void 0 : _e.cluster) !== null && _f !== void 0 ? _f : "ayoune";
|
|
81
|
+
const project = (_g = state.params) === null || _g === void 0 ? void 0 : _g.project;
|
|
82
|
+
spinner.start({ text: "Deleting GKE cluster...", color: "red" });
|
|
83
|
+
code = await streamCmd("gcloud", [
|
|
84
|
+
"container",
|
|
85
|
+
"clusters",
|
|
86
|
+
"delete",
|
|
87
|
+
cluster,
|
|
88
|
+
"--region",
|
|
89
|
+
(_h = state.region) !== null && _h !== void 0 ? _h : "",
|
|
90
|
+
"--project",
|
|
91
|
+
project,
|
|
92
|
+
"--quiet",
|
|
93
|
+
]);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "azure": {
|
|
97
|
+
requireTool("az");
|
|
98
|
+
const rg = (_k = (_j = state.params) === null || _j === void 0 ? void 0 : _j.resourceGroup) !== null && _k !== void 0 ? _k : "ayoune";
|
|
99
|
+
spinner.start({ text: "Deleting resource group...", color: "red" });
|
|
100
|
+
code = await streamCmd("az", ["group", "delete", "--name", rg, "--yes"]);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "digitalocean": {
|
|
104
|
+
requireTool("doctl");
|
|
105
|
+
console.log(chalk.yellow(" doctl does not store the app id locally. Run `doctl apps list` and `doctl apps delete <id>` manually."));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (code !== 0) {
|
|
110
|
+
spinner.error({ text: "Destroy failed" });
|
|
111
|
+
cliError("Destroy failed", EXIT_GENERAL_ERROR);
|
|
112
|
+
}
|
|
113
|
+
spinner.success({ text: `${provider} environment destroyed` });
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
cliError(e.message || "Destroy failed", EXIT_GENERAL_ERROR);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `ay provision digitalocean` — deploy aYOUne to DO App Platform via the
|
|
2
|
+
// marketplace app spec at infrastructure/marketplace/digitalocean/app-spec.yaml.
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import { spinner } from "../../../index.js";
|
|
8
|
+
import { cliError } from "../../helpers/cliError.js";
|
|
9
|
+
import { EXIT_GENERAL_ERROR } from "../../exitCodes.js";
|
|
10
|
+
import { requireTool } from "./_detectTools.js";
|
|
11
|
+
import { runBaseWizard } from "./_wizard.js";
|
|
12
|
+
import { writeState } from "./_stateFile.js";
|
|
13
|
+
const DO_REGIONS = ["fra1", "ams3", "lon1", "nyc3", "sfo3", "sgp1"];
|
|
14
|
+
const DO_INSTANCE_SIZES = ["basic-xs", "basic-s", "basic-m", "basic-l", "professional-xs", "professional-s"];
|
|
15
|
+
function streamCmd(cmd, args) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const child = spawn(cmd, args, {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
shell: process.platform === "win32",
|
|
20
|
+
});
|
|
21
|
+
child.on("exit", (code) => resolve(code !== null && code !== void 0 ? code : 0));
|
|
22
|
+
child.on("error", () => resolve(1));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function addDigitalOceanSubcommand(prov) {
|
|
26
|
+
prov
|
|
27
|
+
.command("digitalocean")
|
|
28
|
+
.alias("do")
|
|
29
|
+
.description("Provision aYOUne to DigitalOcean App Platform")
|
|
30
|
+
.option("--dry-run", "Print commands without running them", false)
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
try {
|
|
33
|
+
requireTool("doctl");
|
|
34
|
+
const spec = findMarketplaceFile("digitalocean", "app-spec.yaml");
|
|
35
|
+
if (!spec) {
|
|
36
|
+
cliError("Could not find infrastructure/marketplace/digitalocean/app-spec.yaml — run from the monorepo.", EXIT_GENERAL_ERROR);
|
|
37
|
+
}
|
|
38
|
+
const base = await runBaseWizard({
|
|
39
|
+
regionChoices: DO_REGIONS,
|
|
40
|
+
defaultRegion: "fra1",
|
|
41
|
+
instanceChoices: DO_INSTANCE_SIZES,
|
|
42
|
+
defaultInstance: "basic-m",
|
|
43
|
+
});
|
|
44
|
+
if (options.dryRun) {
|
|
45
|
+
console.log(chalk.yellow("\n --dry-run set — would run:"));
|
|
46
|
+
console.log(chalk.dim(` doctl apps create --spec ${spec}`));
|
|
47
|
+
console.log(chalk.dim(` (with domain=${base.domain}, region=${base.region}, size=${base.instanceSize})`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
spinner.start({ text: "Creating DigitalOcean app...", color: "cyan" });
|
|
51
|
+
const code = await streamCmd("doctl", ["apps", "create", "--spec", spec]);
|
|
52
|
+
if (code !== 0)
|
|
53
|
+
cliError("App create failed", EXIT_GENERAL_ERROR);
|
|
54
|
+
spinner.success({ text: "App created" });
|
|
55
|
+
const file = await writeState({
|
|
56
|
+
provider: "digitalocean",
|
|
57
|
+
createdAt: new Date().toISOString(),
|
|
58
|
+
updatedAt: new Date().toISOString(),
|
|
59
|
+
region: base.region,
|
|
60
|
+
domain: base.domain,
|
|
61
|
+
modules: base.modules,
|
|
62
|
+
params: { instanceSize: base.instanceSize },
|
|
63
|
+
});
|
|
64
|
+
console.log(chalk.dim(` State file: ${file}`));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
cliError(e.message || "DigitalOcean provisioning failed", EXIT_GENERAL_ERROR);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function findMarketplaceFile(provider, file) {
|
|
72
|
+
let dir = process.cwd();
|
|
73
|
+
while (true) {
|
|
74
|
+
const candidate = path.join(dir, "infrastructure", "marketplace", provider, file);
|
|
75
|
+
if (existsSync(candidate))
|
|
76
|
+
return candidate;
|
|
77
|
+
const parent = path.dirname(dir);
|
|
78
|
+
if (parent === dir)
|
|
79
|
+
return null;
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
}
|