@mesh-tech/mesh-cli 0.25.0 → 0.27.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/README.md +74 -246
- package/dist/bin/mesh.js +2179 -1328
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/db/axes.d.ts +19 -0
- package/dist/src/commands/db/axes.js +35 -0
- package/dist/src/commands/db/connect.d.ts +2 -4
- package/dist/src/commands/db/connect.js +4 -19
- package/dist/src/commands/db/credentials.d.ts +2 -5
- package/dist/src/commands/db/credentials.js +7 -23
- package/dist/src/commands/db/env.d.ts +2 -5
- package/dist/src/commands/db/env.js +7 -23
- package/dist/src/commands/db/exec.js +7 -24
- package/dist/src/commands/db/psql.d.ts +2 -5
- package/dist/src/commands/db/psql.js +8 -23
- package/dist/src/commands/init/platform-setup.d.ts +68 -0
- package/dist/src/commands/init/platform-setup.js +203 -0
- package/dist/src/commands/init/wizard.d.ts +20 -0
- package/dist/src/commands/init/wizard.js +89 -7
- package/dist/src/commands/init.js +42 -36
- package/dist/src/commands/local/index.d.ts +5 -3
- package/dist/src/commands/local/index.js +61 -23
- package/dist/src/commands/local/mock-idps.d.ts +48 -0
- package/dist/src/commands/local/mock-idps.js +119 -0
- package/dist/src/commands/local/seed-zitadel.d.ts +6 -0
- package/dist/src/commands/local/seed-zitadel.js +76 -0
- package/dist/src/commands/local/stack.d.ts +11 -3
- package/dist/src/commands/local/stack.js +29 -15
- package/dist/src/commands/login.d.ts +4 -0
- package/dist/src/commands/login.js +16 -2
- package/dist/src/commands/registry.d.ts +2 -2
- package/dist/src/commands/registry.js +11 -6
- package/dist/src/commands/secrets/context.d.ts +5 -0
- package/dist/src/commands/secrets/context.js +5 -0
- package/dist/src/commands/secrets/index.js +3 -0
- package/dist/src/commands/secrets/migrate-config.d.ts +1 -0
- package/dist/src/commands/secrets/migrate-config.js +2 -3
- package/dist/src/commands/secrets/reindex.d.ts +1 -0
- package/dist/src/commands/secrets/reindex.js +2 -2
- package/dist/src/commands/secrets/set.d.ts +1 -0
- package/dist/src/commands/secrets/set.js +2 -2
- package/dist/src/commands/temporal.js +35 -8
- package/dist/src/commands/temporal.js.map +2 -2
- package/dist/src/commands/tunnel/index.d.ts +8 -0
- package/dist/src/commands/tunnel/index.js +3 -3
- package/dist/src/index.js +324 -62
- package/dist/src/index.js.map +3 -3
- package/dist/src/utils/context.d.ts +6 -1
- package/dist/src/utils/context.js +302 -39
- package/dist/src/utils/registry-identity.d.ts +5 -0
- package/dist/src/utils/registry-identity.js +16 -0
- package/fragments/agents/AGENTS.md.hbs +10 -2
- package/fragments/base/index.ts.hbs +15 -5
- package/package.json +2 -2
- package/skills/core/SKILL.md +74 -7
- package/stack/docker-compose.idps.yml +104 -0
- package/stack/idps/dex.yaml +53 -0
- package/templates/api-auth/index.ts.hbs +5 -4
- package/templates/api-role-gating/index.ts.hbs +5 -4
- package/templates/external-service/index.ts.hbs +5 -4
- package/templates/platform-repo/README.md.hbs +17 -3
- package/templates/platform-repo/core/Pulumi.{{tenant}}-{{env}}.yaml.hbs +7 -0
- package/templates/workflow/index.ts.hbs +5 -4
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface DbAxisOptions {
|
|
2
|
+
tenant?: string;
|
|
3
|
+
env?: string;
|
|
4
|
+
appTenant?: string;
|
|
5
|
+
appStage?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PlatformAxis {
|
|
8
|
+
tenant: string;
|
|
9
|
+
env: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AppAxis {
|
|
12
|
+
tenant: string;
|
|
13
|
+
stage: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function platformAxis(options: DbAxisOptions, usage: string, detect?: () => {
|
|
16
|
+
tenant: string;
|
|
17
|
+
platformEnv: string;
|
|
18
|
+
}): () => PlatformAxis;
|
|
19
|
+
export declare function appAxis(options: DbAxisOptions, platform: () => PlatformAxis): AppAxis;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { detectContext } from "../../utils/context.js";
|
|
2
|
+
import { MeshCliError } from "../../utils/errors.js";
|
|
3
|
+
import { logInfo } from "../../utils/log.js";
|
|
4
|
+
export function platformAxis(options, usage, detect = detectContext) {
|
|
5
|
+
if ((options.tenant && !options.env) || (!options.tenant && options.env)) {
|
|
6
|
+
throw new MeshCliError("Both --tenant and --env are required", {
|
|
7
|
+
remediation: { command: usage },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
let resolved;
|
|
11
|
+
return () => {
|
|
12
|
+
if (resolved)
|
|
13
|
+
return resolved;
|
|
14
|
+
if (options.tenant && options.env) {
|
|
15
|
+
resolved = { tenant: options.tenant, env: options.env };
|
|
16
|
+
logInfo(`Platform: Tenant=${resolved.tenant}, Env=${resolved.env}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const ctx = detect();
|
|
20
|
+
resolved = { tenant: ctx.tenant, env: ctx.platformEnv };
|
|
21
|
+
logInfo(`Auto-detected: Tenant=${resolved.tenant}, Env=${resolved.env}`);
|
|
22
|
+
}
|
|
23
|
+
return resolved;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function appAxis(options, platform) {
|
|
27
|
+
if (options.appTenant && options.appStage) {
|
|
28
|
+
return { tenant: options.appTenant, stage: options.appStage };
|
|
29
|
+
}
|
|
30
|
+
const resolved = platform();
|
|
31
|
+
return {
|
|
32
|
+
tenant: options.appTenant ?? resolved.tenant,
|
|
33
|
+
stage: options.appStage ?? resolved.env,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
env?: string;
|
|
4
|
-
}
|
|
1
|
+
import { type DbAxisOptions } from "./axes.js";
|
|
2
|
+
type ConnectOptions = Pick<DbAxisOptions, "tenant" | "env">;
|
|
5
3
|
export declare function connectCommand(options: ConnectOptions): Promise<void>;
|
|
6
4
|
export {};
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { getPlatformBastionInfo, logInfo, logError } from "../../utils/index.js";
|
|
3
|
+
import { platformAxis } from "./axes.js";
|
|
4
|
+
const USAGE = "mesh db connect --tenant mesh --env dev-temporal";
|
|
3
5
|
export async function connectCommand(options) {
|
|
4
|
-
|
|
5
|
-
let platformEnv;
|
|
6
|
-
if (options.tenant && options.env) {
|
|
7
|
-
tenant = options.tenant;
|
|
8
|
-
platformEnv = options.env;
|
|
9
|
-
logInfo(`Explicit mode: Tenant=${tenant}, Env=${platformEnv}`);
|
|
10
|
-
}
|
|
11
|
-
else if (options.tenant || options.env) {
|
|
12
|
-
logError("Both --tenant and --env are required");
|
|
13
|
-
logInfo("Usage: mesh db connect --tenant mesh --env dev-temporal");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
const ctx = detectContext();
|
|
18
|
-
tenant = ctx.tenant;
|
|
19
|
-
platformEnv = ctx.platformEnv;
|
|
20
|
-
logInfo(`Auto-detected: Tenant=${tenant}, Env=${platformEnv}`);
|
|
21
|
-
}
|
|
6
|
+
const { tenant, env: platformEnv } = platformAxis(options, USAGE)();
|
|
22
7
|
const bastion = await getPlatformBastionInfo(tenant, platformEnv);
|
|
23
8
|
const rdsService = bastion.services.rds;
|
|
24
9
|
if (!rdsService) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
env?: string;
|
|
1
|
+
import { type DbAxisOptions } from "./axes.js";
|
|
2
|
+
interface CredentialsOptions extends DbAxisOptions {
|
|
4
3
|
app?: string;
|
|
5
|
-
appTenant?: string;
|
|
6
|
-
appStage?: string;
|
|
7
4
|
}
|
|
8
5
|
export declare function credentialsCommand(options: CredentialsOptions): Promise<void>;
|
|
9
6
|
export {};
|
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDatabaseUrl, logInfo } from "../../utils/index.js";
|
|
2
|
+
import { appAxis, platformAxis } from "./axes.js";
|
|
3
|
+
const USAGE = "mesh db credentials --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev";
|
|
2
4
|
export async function credentialsCommand(options) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (options.tenant && options.env) {
|
|
6
|
-
platformTenant = options.tenant;
|
|
7
|
-
platformEnv = options.env;
|
|
8
|
-
logInfo(`Platform: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
9
|
-
}
|
|
10
|
-
else if (options.tenant || options.env) {
|
|
11
|
-
logError("Both --tenant and --env are required");
|
|
12
|
-
logInfo("Usage: mesh db credentials --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev");
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
const ctx = detectContext();
|
|
17
|
-
platformTenant = ctx.tenant;
|
|
18
|
-
platformEnv = ctx.platformEnv;
|
|
19
|
-
logInfo(`Auto-detected: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
20
|
-
}
|
|
21
|
-
const appTenant = options.appTenant ?? platformTenant;
|
|
22
|
-
const appStage = options.appStage ?? platformEnv;
|
|
5
|
+
const platform = platformAxis(options, USAGE);
|
|
6
|
+
const app = appAxis(options, platform);
|
|
23
7
|
const appName = options.app;
|
|
24
8
|
if (appName) {
|
|
25
|
-
logInfo(`App credentials: tenant=${
|
|
9
|
+
logInfo(`App credentials: tenant=${app.tenant}, stage=${app.stage}, app=${appName}`);
|
|
26
10
|
}
|
|
27
|
-
const databaseUrl = await getDatabaseUrl(
|
|
11
|
+
const databaseUrl = await getDatabaseUrl(app.tenant, app.stage, { app: appName });
|
|
28
12
|
const parsed = new URL(databaseUrl);
|
|
29
13
|
console.log("");
|
|
30
14
|
console.log("Database Credentials:");
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
env?: string;
|
|
1
|
+
import { type DbAxisOptions } from "./axes.js";
|
|
2
|
+
interface EnvOptions extends DbAxisOptions {
|
|
4
3
|
app?: string;
|
|
5
|
-
appTenant?: string;
|
|
6
|
-
appStage?: string;
|
|
7
4
|
}
|
|
8
5
|
export declare function envCommand(options: EnvOptions): Promise<void>;
|
|
9
6
|
export {};
|
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDatabaseUrl, rewriteDatabaseUrl } from "../../utils/index.js";
|
|
2
|
+
import { appAxis, platformAxis } from "./axes.js";
|
|
3
|
+
const USAGE = "eval $(mesh db env --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev)";
|
|
2
4
|
export async function envCommand(options) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (options.tenant && options.env) {
|
|
6
|
-
platformTenant = options.tenant;
|
|
7
|
-
platformEnv = options.env;
|
|
8
|
-
console.error(`ℹ Platform: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
9
|
-
}
|
|
10
|
-
else if (options.tenant || options.env) {
|
|
11
|
-
logError("Both --tenant and --env are required");
|
|
12
|
-
logInfo("Usage: eval $(mesh db env --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev)");
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
const ctx = detectContext();
|
|
17
|
-
platformTenant = ctx.tenant;
|
|
18
|
-
platformEnv = ctx.platformEnv;
|
|
19
|
-
console.error(`ℹ Auto-detected: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
20
|
-
}
|
|
21
|
-
const appTenant = options.appTenant ?? platformTenant;
|
|
22
|
-
const appStage = options.appStage ?? platformEnv;
|
|
5
|
+
const platform = platformAxis(options, USAGE);
|
|
6
|
+
const app = appAxis(options, platform);
|
|
23
7
|
const appName = options.app;
|
|
24
8
|
if (appName) {
|
|
25
|
-
console.error(`ℹ App credentials: tenant=${
|
|
9
|
+
console.error(`ℹ App credentials: tenant=${app.tenant}, stage=${app.stage}, app=${appName}`);
|
|
26
10
|
}
|
|
27
|
-
const rawUrl = await getDatabaseUrl(
|
|
11
|
+
const rawUrl = await getDatabaseUrl(app.tenant, app.stage, { app: appName });
|
|
28
12
|
const tunnelUrl = rewriteDatabaseUrl(rawUrl, { endpoint: "localhost:5432" });
|
|
29
13
|
const parsed = new URL(tunnelUrl);
|
|
30
14
|
console.log(`export DATABASE_URL="${tunnelUrl}"`);
|
|
@@ -3,7 +3,8 @@ import * as net from "net";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager";
|
|
6
|
-
import {
|
|
6
|
+
import { getPlatformBastionInfo, getDatabaseUrl, rewriteDatabaseUrl, logInfo, logSuccess, logError, logWarn, } from "../../utils/index.js";
|
|
7
|
+
import { appAxis, platformAxis } from "./axes.js";
|
|
7
8
|
const DEFAULT_LOCAL_PORT = 5432;
|
|
8
9
|
const MAX_RETRIES = 12;
|
|
9
10
|
const RETRY_DELAY_MS = 5000;
|
|
@@ -170,30 +171,11 @@ export async function execCommand(commandArgs, options) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (options.tenant && options.env) {
|
|
176
|
-
platformTenant = options.tenant;
|
|
177
|
-
platformEnv = options.env;
|
|
178
|
-
logInfo(`Explicit mode: Platform Tenant=${platformTenant}, Platform Env=${platformEnv}`);
|
|
179
|
-
}
|
|
180
|
-
else if (options.tenant || options.env) {
|
|
181
|
-
logError("Both --tenant and --env are required when using explicit mode");
|
|
182
|
-
logInfo("Usage: mesh db exec --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev -- <command>");
|
|
183
|
-
logInfo("Or omit both to use auto-detection");
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
const ctx = detectContext();
|
|
188
|
-
platformTenant = ctx.tenant;
|
|
189
|
-
platformEnv = ctx.platformEnv;
|
|
190
|
-
logInfo(`Discovery mode: Platform=${platformEnv}, Tenant=${platformTenant}`);
|
|
191
|
-
}
|
|
192
|
-
const appTenant = options.appTenant ?? platformTenant;
|
|
193
|
-
const appStage = options.appStage ?? platformEnv;
|
|
174
|
+
const platform = platformAxis(options, "mesh db exec --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev -- <command>");
|
|
175
|
+
const app = appAxis(options, platform);
|
|
194
176
|
const appName = options.app;
|
|
195
177
|
if (appName) {
|
|
196
|
-
logInfo(`App credentials: tenant=${
|
|
178
|
+
logInfo(`App credentials: tenant=${app.tenant}, stage=${app.stage}, app=${appName}`);
|
|
197
179
|
}
|
|
198
180
|
const tunnelExists = await checkPort(localPort);
|
|
199
181
|
const sstOutputs = findSstOutputs();
|
|
@@ -215,6 +197,7 @@ export async function execCommand(commandArgs, options) {
|
|
|
215
197
|
}
|
|
216
198
|
if (!databaseUrl) {
|
|
217
199
|
if (!tunnelExists) {
|
|
200
|
+
const { tenant: platformTenant, env: platformEnv } = platform();
|
|
218
201
|
const bastion = await getPlatformBastionInfo(platformTenant, platformEnv);
|
|
219
202
|
const rdsService = bastion.services.rds;
|
|
220
203
|
if (!rdsService) {
|
|
@@ -225,7 +208,7 @@ export async function execCommand(commandArgs, options) {
|
|
|
225
208
|
else {
|
|
226
209
|
logSuccess(`Using existing tunnel on port ${localPort}`);
|
|
227
210
|
}
|
|
228
|
-
const rawUrl = await getDatabaseUrl(
|
|
211
|
+
const rawUrl = await getDatabaseUrl(app.tenant, app.stage, { app: appName });
|
|
229
212
|
databaseUrl = rewriteDatabaseUrl(rawUrl, {
|
|
230
213
|
endpoint: `localhost:${localPort}`,
|
|
231
214
|
sslMode: options.ssl,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
env?: string;
|
|
1
|
+
import { type DbAxisOptions } from "./axes.js";
|
|
2
|
+
interface PsqlOptions extends DbAxisOptions {
|
|
4
3
|
app?: string;
|
|
5
|
-
appTenant?: string;
|
|
6
|
-
appStage?: string;
|
|
7
4
|
}
|
|
8
5
|
export declare function psqlCommand(options: PsqlOptions): Promise<void>;
|
|
9
6
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import * as net from "net";
|
|
3
|
-
import {
|
|
3
|
+
import { getPlatformBastionInfo, getDatabaseUrl, logInfo, logSuccess, logError, } from "../../utils/index.js";
|
|
4
|
+
import { appAxis, platformAxis } from "./axes.js";
|
|
5
|
+
const USAGE = "mesh db psql --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev";
|
|
4
6
|
async function startTunnelBackground(instanceId, rdsHost, rdsPort, localPort) {
|
|
5
7
|
logInfo("Starting tunnel in background...");
|
|
6
8
|
const tunnel = spawn("aws", [
|
|
@@ -46,29 +48,12 @@ function sleep(ms) {
|
|
|
46
48
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
49
|
}
|
|
48
50
|
export async function psqlCommand(options) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
platformTenant = options.tenant;
|
|
53
|
-
platformEnv = options.env;
|
|
54
|
-
logInfo(`Platform: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
55
|
-
}
|
|
56
|
-
else if (options.tenant || options.env) {
|
|
57
|
-
logError("Both --tenant and --env are required");
|
|
58
|
-
logInfo("Usage: mesh db psql --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev");
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
const ctx = detectContext();
|
|
63
|
-
platformTenant = ctx.tenant;
|
|
64
|
-
platformEnv = ctx.platformEnv;
|
|
65
|
-
logInfo(`Auto-detected: Tenant=${platformTenant}, Env=${platformEnv}`);
|
|
66
|
-
}
|
|
67
|
-
const appTenant = options.appTenant ?? platformTenant;
|
|
68
|
-
const appStage = options.appStage ?? platformEnv;
|
|
51
|
+
const platform = platformAxis(options, USAGE);
|
|
52
|
+
const { tenant: platformTenant, env: platformEnv } = platform();
|
|
53
|
+
const app = appAxis(options, platform);
|
|
69
54
|
const appName = options.app;
|
|
70
55
|
if (appName) {
|
|
71
|
-
logInfo(`App credentials: tenant=${
|
|
56
|
+
logInfo(`App credentials: tenant=${app.tenant}, stage=${app.stage}, app=${appName}`);
|
|
72
57
|
}
|
|
73
58
|
const psqlCheck = spawn("which", ["psql"]);
|
|
74
59
|
await new Promise((resolve, reject) => {
|
|
@@ -96,7 +81,7 @@ export async function psqlCommand(options) {
|
|
|
96
81
|
process.on("exit", cleanup);
|
|
97
82
|
process.on("SIGINT", cleanup);
|
|
98
83
|
process.on("SIGTERM", cleanup);
|
|
99
|
-
const databaseUrl = await getDatabaseUrl(
|
|
84
|
+
const databaseUrl = await getDatabaseUrl(app.tenant, app.stage, { app: appName });
|
|
100
85
|
const parsed = new URL(databaseUrl);
|
|
101
86
|
const username = parsed.username;
|
|
102
87
|
const password = decodeURIComponent(parsed.password);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface SetupPrompts {
|
|
2
|
+
input(args: {
|
|
3
|
+
message: string;
|
|
4
|
+
default?: string;
|
|
5
|
+
validate?: (v: string) => true | string;
|
|
6
|
+
}): Promise<string>;
|
|
7
|
+
confirm(args: {
|
|
8
|
+
message: string;
|
|
9
|
+
default?: boolean;
|
|
10
|
+
}): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export interface PlatformSetupFlags {
|
|
13
|
+
env?: string;
|
|
14
|
+
region?: string;
|
|
15
|
+
domain?: string;
|
|
16
|
+
profile?: string;
|
|
17
|
+
stateBucket?: string;
|
|
18
|
+
dir?: string;
|
|
19
|
+
createStateBucket?: boolean;
|
|
20
|
+
yes?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface PlatformSetup {
|
|
23
|
+
tenant: string;
|
|
24
|
+
env: string;
|
|
25
|
+
region: string;
|
|
26
|
+
domain: string;
|
|
27
|
+
profile: string;
|
|
28
|
+
stateBucket: string;
|
|
29
|
+
dir: string;
|
|
30
|
+
createStateBucket: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare const PLATFORM_SETUP_DEFAULTS: {
|
|
33
|
+
readonly env: "dev";
|
|
34
|
+
readonly region: "us-east-2";
|
|
35
|
+
readonly profile: "default";
|
|
36
|
+
};
|
|
37
|
+
export declare const DOMAIN_REQUIRED_MESSAGE: string;
|
|
38
|
+
export declare function defaultStateBucket(tenant: string): string;
|
|
39
|
+
export declare function defaultPlatformDir(tenant: string): string;
|
|
40
|
+
export declare function validateEnv(value: string): true | string;
|
|
41
|
+
export declare function validateRegion(value: string): true | string;
|
|
42
|
+
export declare function validateDomain(value: string): true | string;
|
|
43
|
+
export declare function validateStateBucket(value: string): true | string;
|
|
44
|
+
export declare function resolveTargetDir(tenant: string, flags: PlatformSetupFlags, cwd: string): string;
|
|
45
|
+
export declare function resolvePlatformSetup(tenant: string, flags: PlatformSetupFlags, deps: {
|
|
46
|
+
prompts?: SetupPrompts;
|
|
47
|
+
interactive: boolean;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
}): Promise<PlatformSetup>;
|
|
50
|
+
export declare function stateBucketCreateCommand(setup: Pick<PlatformSetup, "stateBucket" | "region" | "profile">): string;
|
|
51
|
+
export interface StateBucketOutcome {
|
|
52
|
+
created: boolean;
|
|
53
|
+
present: boolean;
|
|
54
|
+
detail: string;
|
|
55
|
+
}
|
|
56
|
+
export interface StateBucketDeps {
|
|
57
|
+
headBucket(setup: PlatformSetup): boolean;
|
|
58
|
+
createBucket(setup: PlatformSetup): void;
|
|
59
|
+
enableVersioning(setup: PlatformSetup): void;
|
|
60
|
+
}
|
|
61
|
+
export declare const realStateBucketDeps: StateBucketDeps;
|
|
62
|
+
export declare function ensureStateBucket(setup: PlatformSetup, deps?: StateBucketDeps): Promise<StateBucketOutcome>;
|
|
63
|
+
export declare function exportCredentialsCommand(profile: string): string;
|
|
64
|
+
export declare function renderPlatformNextSteps(setup: PlatformSetup, opts?: {
|
|
65
|
+
bucket?: StateBucketOutcome;
|
|
66
|
+
relativeDir?: string;
|
|
67
|
+
}): string[];
|
|
68
|
+
export declare function scaffoldPlatformRepo(setup: PlatformSetup): Promise<void>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { MeshCliError } from "../../utils/errors.js";
|
|
5
|
+
export const PLATFORM_SETUP_DEFAULTS = {
|
|
6
|
+
env: "dev",
|
|
7
|
+
region: "us-east-2",
|
|
8
|
+
profile: "default",
|
|
9
|
+
};
|
|
10
|
+
export const DOMAIN_REQUIRED_MESSAGE = "--domain is required: the core stack creates the public Route 53 zone <env>.<domain>, " +
|
|
11
|
+
"and a placeholder domain scaffolds a zone that can never validate (the deploy stalls on the ACM certificate).";
|
|
12
|
+
export function defaultStateBucket(tenant) {
|
|
13
|
+
return `${tenant}-mesh-platform-pulumi-state`;
|
|
14
|
+
}
|
|
15
|
+
export function defaultPlatformDir(tenant) {
|
|
16
|
+
return `${tenant}-mesh-platform`;
|
|
17
|
+
}
|
|
18
|
+
export function validateEnv(value) {
|
|
19
|
+
return /^[a-z][a-z0-9-]*$/.test(value.trim())
|
|
20
|
+
? true
|
|
21
|
+
: "lowercase letters, digits and dashes, starting with a letter";
|
|
22
|
+
}
|
|
23
|
+
export function validateRegion(value) {
|
|
24
|
+
return /^[a-z]{2}(-[a-z]+)+-\d$/.test(value.trim()) ? true : "an AWS region, e.g. us-east-2 or eu-west-1";
|
|
25
|
+
}
|
|
26
|
+
export function validateDomain(value) {
|
|
27
|
+
const domain = value.trim().toLowerCase().replace(/\.$/, "");
|
|
28
|
+
if (!domain)
|
|
29
|
+
return "a public domain you control, e.g. q2poc.pub";
|
|
30
|
+
if (!/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/.test(domain)) {
|
|
31
|
+
return "a public domain you control, e.g. q2poc.pub";
|
|
32
|
+
}
|
|
33
|
+
if (/(^|\.)(example\.(com|org|net)|invalid|localhost|test)$/.test(domain)) {
|
|
34
|
+
return `${domain} is a placeholder — the deploy stalls on an ACM certificate for a zone nobody can delegate. Use a domain you control.`;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
export function validateStateBucket(value) {
|
|
39
|
+
const bucket = value.trim();
|
|
40
|
+
return /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) && !bucket.includes("..")
|
|
41
|
+
? true
|
|
42
|
+
: "3–63 characters: lowercase letters, digits, dots and dashes, starting and ending alphanumeric";
|
|
43
|
+
}
|
|
44
|
+
export function resolveTargetDir(tenant, flags, cwd) {
|
|
45
|
+
const dir = path.resolve(cwd, flags.dir ?? defaultPlatformDir(tenant));
|
|
46
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0) {
|
|
47
|
+
throw new MeshCliError(`Target directory is not empty: ${dir}`, {
|
|
48
|
+
remediation: { command: `mesh init platform ${tenant} --dir <empty-dir>` },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
export async function resolvePlatformSetup(tenant, flags, deps) {
|
|
54
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
55
|
+
const dir = resolveTargetDir(tenant, flags, cwd);
|
|
56
|
+
const ask = deps.interactive && !flags.yes ? deps.prompts : undefined;
|
|
57
|
+
const env = flags.env ?? (ask ? await ask.input({ message: "Environment", default: PLATFORM_SETUP_DEFAULTS.env, validate: validateEnv }) : PLATFORM_SETUP_DEFAULTS.env);
|
|
58
|
+
if (validateEnv(env) !== true)
|
|
59
|
+
throw new MeshCliError(`Invalid environment '${env}' — ${validateEnv(env)}.`);
|
|
60
|
+
const region = flags.region ??
|
|
61
|
+
(ask
|
|
62
|
+
? await ask.input({ message: "AWS region", default: PLATFORM_SETUP_DEFAULTS.region, validate: validateRegion })
|
|
63
|
+
: PLATFORM_SETUP_DEFAULTS.region);
|
|
64
|
+
if (validateRegion(region) !== true)
|
|
65
|
+
throw new MeshCliError(`Invalid region '${region}' — ${validateRegion(region)}.`);
|
|
66
|
+
let domain = flags.domain;
|
|
67
|
+
if (domain === undefined) {
|
|
68
|
+
if (!ask)
|
|
69
|
+
throw new MeshCliError(DOMAIN_REQUIRED_MESSAGE, { remediation: { command: `mesh init platform ${tenant} --domain <your-domain>` } });
|
|
70
|
+
domain = await ask.input({
|
|
71
|
+
message: `Public base domain (the core stack creates the Route 53 zone ${env}.<domain>; its NS records must be delegated from the parent before the deploy can finish)`,
|
|
72
|
+
validate: validateDomain,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const domainCheck = validateDomain(domain);
|
|
76
|
+
if (domainCheck !== true)
|
|
77
|
+
throw new MeshCliError(`Invalid --domain '${domain}' — ${domainCheck}`);
|
|
78
|
+
domain = domain.trim().toLowerCase().replace(/\.$/, "");
|
|
79
|
+
const profile = flags.profile ??
|
|
80
|
+
(ask
|
|
81
|
+
? await ask.input({ message: "AWS profile the stack config references", default: PLATFORM_SETUP_DEFAULTS.profile })
|
|
82
|
+
: PLATFORM_SETUP_DEFAULTS.profile);
|
|
83
|
+
const bucketDefault = defaultStateBucket(tenant);
|
|
84
|
+
const stateBucket = flags.stateBucket ??
|
|
85
|
+
(ask
|
|
86
|
+
? await ask.input({ message: "Pulumi state S3 bucket (holds this platform's state — created below if it does not exist)", default: bucketDefault, validate: validateStateBucket })
|
|
87
|
+
: bucketDefault);
|
|
88
|
+
const bucketCheck = validateStateBucket(stateBucket);
|
|
89
|
+
if (bucketCheck !== true)
|
|
90
|
+
throw new MeshCliError(`Invalid --state-bucket '${stateBucket}' — ${bucketCheck}`);
|
|
91
|
+
let createStateBucket = flags.createStateBucket ?? false;
|
|
92
|
+
if (flags.createStateBucket === undefined && ask) {
|
|
93
|
+
createStateBucket = await ask.confirm({
|
|
94
|
+
message: `Create s3://${stateBucket} in ${region} now (versioned), if it does not exist?`,
|
|
95
|
+
default: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
tenant,
|
|
100
|
+
env,
|
|
101
|
+
region,
|
|
102
|
+
domain,
|
|
103
|
+
profile,
|
|
104
|
+
stateBucket: stateBucket.trim(),
|
|
105
|
+
dir,
|
|
106
|
+
createStateBucket,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function stateBucketCreateCommand(setup) {
|
|
110
|
+
const location = setup.region === "us-east-1" ? "" : ` --create-bucket-configuration LocationConstraint=${setup.region}`;
|
|
111
|
+
return `aws s3api create-bucket --bucket ${setup.stateBucket} --region ${setup.region}${location} --profile ${setup.profile}`;
|
|
112
|
+
}
|
|
113
|
+
function awsJson(args, profile) {
|
|
114
|
+
return execFileSync("aws", [...args, "--profile", profile], {
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export const realStateBucketDeps = {
|
|
120
|
+
headBucket(setup) {
|
|
121
|
+
try {
|
|
122
|
+
awsJson(["s3api", "head-bucket", "--bucket", setup.stateBucket, "--region", setup.region], setup.profile);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
createBucket(setup) {
|
|
130
|
+
const args = ["s3api", "create-bucket", "--bucket", setup.stateBucket, "--region", setup.region];
|
|
131
|
+
if (setup.region !== "us-east-1")
|
|
132
|
+
args.push("--create-bucket-configuration", `LocationConstraint=${setup.region}`);
|
|
133
|
+
awsJson(args, setup.profile);
|
|
134
|
+
},
|
|
135
|
+
enableVersioning(setup) {
|
|
136
|
+
awsJson(["s3api", "put-bucket-versioning", "--bucket", setup.stateBucket, "--region", setup.region, "--versioning-configuration", "Status=Enabled"], setup.profile);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
export async function ensureStateBucket(setup, deps = realStateBucketDeps) {
|
|
140
|
+
if (!setup.createStateBucket) {
|
|
141
|
+
return { created: false, present: false, detail: "not requested" };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (deps.headBucket(setup)) {
|
|
145
|
+
return { created: false, present: true, detail: `s3://${setup.stateBucket} already exists` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
deps.createBucket(setup);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const why = errorDetail(err);
|
|
155
|
+
return { created: false, present: false, detail: `could not create s3://${setup.stateBucket} — ${why}` };
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
deps.enableVersioning(setup);
|
|
159
|
+
return { created: true, present: true, detail: `created s3://${setup.stateBucket} in ${setup.region} (versioning on)` };
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
return { created: true, present: true, detail: `created s3://${setup.stateBucket} in ${setup.region}, but versioning could not be enabled — ${errorDetail(err)}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function errorDetail(err) {
|
|
166
|
+
const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr ?? "") : "";
|
|
167
|
+
const text = stderr.trim() || (err instanceof Error ? err.message : String(err));
|
|
168
|
+
return text.split("\n").filter(Boolean).slice(-1)[0] ?? "unknown error";
|
|
169
|
+
}
|
|
170
|
+
export function exportCredentialsCommand(profile) {
|
|
171
|
+
return (`eval "$(aws configure export-credentials --profile ${profile} --format env)"` +
|
|
172
|
+
" # mesh deploy reads AWS_ACCESS_KEY_ID; a platform stack carries no mesh:deployerRole");
|
|
173
|
+
}
|
|
174
|
+
export function renderPlatformNextSteps(setup, opts = {}) {
|
|
175
|
+
const dir = opts.relativeDir ?? setup.dir;
|
|
176
|
+
const steps = [];
|
|
177
|
+
if (!opts.bucket?.present) {
|
|
178
|
+
steps.push(`${stateBucketCreateCommand(setup)} # the Pulumi state bucket, once`);
|
|
179
|
+
}
|
|
180
|
+
steps.push(`cd ${dir}`, "pnpm install", `aws sso login --profile ${setup.profile} # if this profile is SSO`, exportCredentialsCommand(setup.profile), `cd core && mesh deploy up # the VPC, EKS, RDS and the public zone ${setup.env}.${setup.domain}`, `# → then delegate the NS records of ${setup.env}.${setup.domain} from ${setup.domain}; the deploy waits on the ACM certificate until you do`, "cd ../platform && mesh deploy up # Temporal, Zitadel, the Hub (needs Docker — it builds service images)", `mesh login ${setup.tenant}.${setup.env} # your platform sign-in, once it is up`, "", "Then, in your apps repo, for the hello-world app:", ` mesh init --tenant ${setup.tenant} --platform ${setup.env}`, " mesh create-app --name hello --primitives service", " mesh deploy up # from the app directory");
|
|
181
|
+
return steps;
|
|
182
|
+
}
|
|
183
|
+
export async function scaffoldPlatformRepo(setup) {
|
|
184
|
+
if (fs.existsSync(setup.dir) && fs.readdirSync(setup.dir).length > 0) {
|
|
185
|
+
throw new MeshCliError(`Target directory is not empty: ${setup.dir}`, {
|
|
186
|
+
remediation: { command: `mesh init platform ${setup.tenant} --dir <empty-dir>` },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const { copyTemplate } = await import("../create-app.js");
|
|
190
|
+
const { findPackageRoot } = await import("../local/stack.js");
|
|
191
|
+
const { syncSkills } = await import("../skills.js");
|
|
192
|
+
const templateDir = path.join(findPackageRoot(), "templates", "platform-repo");
|
|
193
|
+
fs.mkdirSync(setup.dir, { recursive: true });
|
|
194
|
+
copyTemplate(templateDir, setup.dir, {
|
|
195
|
+
tenant: setup.tenant,
|
|
196
|
+
env: setup.env,
|
|
197
|
+
region: setup.region,
|
|
198
|
+
domain: setup.domain,
|
|
199
|
+
profile: setup.profile,
|
|
200
|
+
stateBucket: setup.stateBucket,
|
|
201
|
+
});
|
|
202
|
+
syncSkills(setup.dir);
|
|
203
|
+
}
|