@mesh-tech/mesh-cli 0.24.1 → 0.26.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 +1451 -847
- package/dist/bin/mesh.js.map +3 -3
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.d.ts +2 -1
- package/dist/src/commands/app-check.js +27 -8
- 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/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 +34 -10
- 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 +14 -2
- package/fragments/base/index.ts.hbs +15 -5
- package/package.json +2 -2
- package/skills/core/SKILL.md +74 -7
- 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
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { probeRegistryToken } from "../../utils/auth-preflight.js";
|
|
2
2
|
import { type RegistryLoginOptions } from "../registry.js";
|
|
3
3
|
import { type MeshJson } from "../../utils/mesh-json.js";
|
|
4
|
+
import { scaffoldPlatformRepo, type PlatformSetupFlags, type StateBucketDeps } from "./platform-setup.js";
|
|
4
5
|
export interface WizardOptions {
|
|
5
6
|
tenant?: string;
|
|
6
7
|
local?: boolean;
|
|
7
8
|
platform?: string;
|
|
9
|
+
newPlatform?: string | boolean;
|
|
10
|
+
region?: string;
|
|
11
|
+
domain?: string;
|
|
12
|
+
stateBucket?: string;
|
|
13
|
+
createStateBucket?: boolean;
|
|
14
|
+
dir?: string;
|
|
8
15
|
device?: boolean;
|
|
9
16
|
profile?: string;
|
|
10
17
|
skipRepo?: boolean;
|
|
@@ -18,9 +25,15 @@ export interface StepResult {
|
|
|
18
25
|
detail: string;
|
|
19
26
|
fix?: string;
|
|
20
27
|
blocking?: boolean;
|
|
28
|
+
next?: string[];
|
|
21
29
|
}
|
|
22
30
|
export type WizardMode = "local" | {
|
|
23
31
|
platform: string;
|
|
32
|
+
} | {
|
|
33
|
+
newPlatform: PlatformSetupFlags;
|
|
34
|
+
};
|
|
35
|
+
export declare function isNewPlatform(mode: WizardMode): mode is {
|
|
36
|
+
newPlatform: PlatformSetupFlags;
|
|
24
37
|
};
|
|
25
38
|
export interface WizardCtx {
|
|
26
39
|
cwd: string;
|
|
@@ -40,6 +53,8 @@ export interface WizardSeams {
|
|
|
40
53
|
probe?: typeof probeRegistryToken;
|
|
41
54
|
runRegistryLogin?: (opts: RegistryLoginOptions) => Promise<void>;
|
|
42
55
|
gitInit?: (dir: string) => void;
|
|
56
|
+
scaffoldPlatform?: typeof scaffoldPlatformRepo;
|
|
57
|
+
stateBucket?: StateBucketDeps;
|
|
43
58
|
}
|
|
44
59
|
export interface WizardStep {
|
|
45
60
|
name: "registry" | "platform" | "repo" | "skills";
|
|
@@ -72,7 +87,9 @@ export interface Prompts {
|
|
|
72
87
|
}
|
|
73
88
|
export declare const NO_SESSION_NO_TTY_MESSAGE: string;
|
|
74
89
|
export declare const TENANT_REQUIRED_MESSAGE = "--tenant is required (no TTY for interactive mode)";
|
|
90
|
+
export declare const REGISTRY_WHY: string;
|
|
75
91
|
export declare const INTERRUPTED_EXIT_CODE = 130;
|
|
92
|
+
export declare const PLATFORM_SETUP_LABEL = "Platform setup";
|
|
76
93
|
export declare function wizardExitCode(steps: ReadonlyArray<Pick<StepResult, "status">>): number;
|
|
77
94
|
export declare function wizardNextCommands(mode: WizardMode, steps?: ReadonlyArray<StepResult>): string[];
|
|
78
95
|
export declare function stepBlocks(result: StepResult): boolean;
|
|
@@ -85,7 +102,9 @@ export declare function resolveAnswers(opts: WizardOptions, recorded: MeshJson |
|
|
|
85
102
|
mode?: WizardMode;
|
|
86
103
|
open: Array<"tenant" | "mode">;
|
|
87
104
|
};
|
|
105
|
+
export declare function platformSetupFlags(opts: WizardOptions): PlatformSetupFlags;
|
|
88
106
|
export declare const registryStep: WizardStep;
|
|
107
|
+
export declare const newPlatformStep: WizardStep;
|
|
89
108
|
export type RepoKind = "platform-monorepo" | "empty-apps-repo" | "apps-repo" | "other-repo" | "empty-dir" | "no-repo";
|
|
90
109
|
export declare function classifyRepo(cwd: string): {
|
|
91
110
|
kind: RepoKind;
|
|
@@ -93,6 +112,7 @@ export declare function classifyRepo(cwd: string): {
|
|
|
93
112
|
};
|
|
94
113
|
export declare const INIT_REPO_FIX = "git init && mesh init";
|
|
95
114
|
export declare const repoStep: WizardStep;
|
|
115
|
+
export declare const skillsStep: WizardStep;
|
|
96
116
|
export declare const WIZARD_STEPS: readonly WizardStep[];
|
|
97
117
|
export declare function runWizardSteps(ctx: WizardCtx, steps: readonly WizardStep[]): Promise<StepResult[]>;
|
|
98
118
|
export declare function runInitWizard(opts: WizardOptions, deps?: {
|
|
@@ -12,10 +12,17 @@ import { discoverConfigForContext, isRemoteEnvironment, readCredentials, renderN
|
|
|
12
12
|
import { bootstrapAppsRepo, isInsidePlatformMonorepo, shouldBootstrapAppsRepo } from "../create-app.js";
|
|
13
13
|
import { resolveTargetRoot, syncSkills } from "../skills.js";
|
|
14
14
|
import { detectLocalTenant } from "../local/dev-local.js";
|
|
15
|
+
import { PLATFORM_SETUP_DEFAULTS, ensureStateBucket, renderPlatformNextSteps, resolvePlatformSetup, scaffoldPlatformRepo, } from "./platform-setup.js";
|
|
16
|
+
export function isNewPlatform(mode) {
|
|
17
|
+
return typeof mode === "object" && "newPlatform" in mode;
|
|
18
|
+
}
|
|
15
19
|
export const NO_SESSION_NO_TTY_MESSAGE = "No registry session and no terminal to sign in with.\n" +
|
|
16
20
|
" On a headless box: mesh registry login --device (one-time, prints a code)\n" +
|
|
17
21
|
" In CI with AWS creds: mesh registry login --ci";
|
|
18
22
|
export const TENANT_REQUIRED_MESSAGE = "--tenant is required (no TTY for interactive mode)";
|
|
23
|
+
export const REGISTRY_WHY = "Package access — a sign-in to the Mesh package REGISTRY, which is what makes `@mesh-tech/*` installable.\n" +
|
|
24
|
+
" It is one global identity: no AWS account, no platform, no <tenant>.<env> context.\n" +
|
|
25
|
+
" (`mesh login <tenant>.<env>` is the other sign-in — that one is only for deploying to, or operating, a platform.)";
|
|
19
26
|
export const INTERRUPTED_EXIT_CODE = 130;
|
|
20
27
|
const STEP_LABELS = {
|
|
21
28
|
registry: "Registry access",
|
|
@@ -23,10 +30,14 @@ const STEP_LABELS = {
|
|
|
23
30
|
repo: "Repo",
|
|
24
31
|
skills: "Agent skills",
|
|
25
32
|
};
|
|
33
|
+
export const PLATFORM_SETUP_LABEL = "Platform setup";
|
|
26
34
|
export function wizardExitCode(steps) {
|
|
27
35
|
return steps.some((s) => s.status === "fail") ? 1 : 0;
|
|
28
36
|
}
|
|
29
37
|
export function wizardNextCommands(mode, steps = []) {
|
|
38
|
+
const owned = steps.find((s) => s.next?.length)?.next;
|
|
39
|
+
if (owned)
|
|
40
|
+
return owned;
|
|
30
41
|
const repo = steps.find((s) => s.name === STEP_LABELS.repo);
|
|
31
42
|
const first = repo && repo.status !== "pass" && repo.fix ? repo.fix : "mesh create-app";
|
|
32
43
|
return mode === "local" ? [first] : [first, "mesh deploy up # from the app directory, once it exists"];
|
|
@@ -35,7 +46,11 @@ export function stepBlocks(result) {
|
|
|
35
46
|
return result.status === "fail" && result.blocking !== false;
|
|
36
47
|
}
|
|
37
48
|
export function describeMode(mode) {
|
|
38
|
-
|
|
49
|
+
if (mode === "local")
|
|
50
|
+
return "local only";
|
|
51
|
+
if (isNewPlatform(mode))
|
|
52
|
+
return `a new platform (${mode.newPlatform.env ?? PLATFORM_SETUP_DEFAULTS.env})`;
|
|
53
|
+
return `deployed platform (${mode.platform})`;
|
|
39
54
|
}
|
|
40
55
|
export function renderWizardSummary(outcome, opts = {}) {
|
|
41
56
|
const paint = opts.color === false ? { green: (s) => s, red: (s) => s, yellow: (s) => s, dim: (s) => s, cyan: (s) => s } : chalk;
|
|
@@ -65,10 +80,13 @@ export function resolveAnswers(opts, recorded, interactive) {
|
|
|
65
80
|
open.push("tenant");
|
|
66
81
|
}
|
|
67
82
|
let mode;
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
const chosen = [opts.local && "--local", opts.platform && "--platform", opts.newPlatform && "--new-platform"].filter(Boolean);
|
|
84
|
+
if (chosen.length > 1) {
|
|
85
|
+
throw new MeshCliError(`${chosen.join(", ")} are mutually exclusive — a tenant runs in exactly one of those three places.`);
|
|
70
86
|
}
|
|
71
|
-
if (opts.
|
|
87
|
+
if (opts.newPlatform)
|
|
88
|
+
mode = { newPlatform: platformSetupFlags(opts) };
|
|
89
|
+
else if (opts.platform)
|
|
72
90
|
mode = { platform: opts.platform };
|
|
73
91
|
else if (opts.local)
|
|
74
92
|
mode = "local";
|
|
@@ -80,6 +98,18 @@ export function resolveAnswers(opts, recorded, interactive) {
|
|
|
80
98
|
}
|
|
81
99
|
return { tenant, mode, open };
|
|
82
100
|
}
|
|
101
|
+
export function platformSetupFlags(opts) {
|
|
102
|
+
return {
|
|
103
|
+
env: typeof opts.newPlatform === "string" ? opts.newPlatform : undefined,
|
|
104
|
+
region: opts.region,
|
|
105
|
+
domain: opts.domain,
|
|
106
|
+
profile: opts.profile,
|
|
107
|
+
stateBucket: opts.stateBucket,
|
|
108
|
+
createStateBucket: opts.createStateBucket,
|
|
109
|
+
dir: opts.dir,
|
|
110
|
+
yes: opts.yes,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
83
113
|
export const registryStep = {
|
|
84
114
|
name: "registry",
|
|
85
115
|
async run(ctx) {
|
|
@@ -102,6 +132,7 @@ export const registryStep = {
|
|
|
102
132
|
return { name, status: "fail", detail: NO_SESSION_NO_TTY_MESSAGE.split("\n")[0], fix: "mesh registry login --device", blocking: true };
|
|
103
133
|
}
|
|
104
134
|
try {
|
|
135
|
+
logInfo(REGISTRY_WHY);
|
|
105
136
|
const login = ctx.seams?.runRegistryLogin ??
|
|
106
137
|
(async (opts) => {
|
|
107
138
|
const { runRegistryLogin } = await import("../registry.js");
|
|
@@ -132,10 +163,51 @@ export const registryStep = {
|
|
|
132
163
|
return { name, status: "pass", detail: `Registry access OK (${session ? `signed in as ${session.email}` : "token written"})` };
|
|
133
164
|
},
|
|
134
165
|
};
|
|
166
|
+
export const newPlatformStep = {
|
|
167
|
+
name: "platform",
|
|
168
|
+
async run(ctx) {
|
|
169
|
+
const name = PLATFORM_SETUP_LABEL;
|
|
170
|
+
if (!isNewPlatform(ctx.mode))
|
|
171
|
+
return { name, status: "skip", detail: "no new platform requested" };
|
|
172
|
+
let setup;
|
|
173
|
+
try {
|
|
174
|
+
setup = await resolvePlatformSetup(ctx.tenant, { ...ctx.mode.newPlatform, yes: ctx.yes }, {
|
|
175
|
+
prompts: ctx.prompts,
|
|
176
|
+
interactive: ctx.interactive,
|
|
177
|
+
cwd: ctx.cwd,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return {
|
|
182
|
+
name,
|
|
183
|
+
status: "fail",
|
|
184
|
+
detail: err.message.split("\n")[0],
|
|
185
|
+
fix: `mesh init --tenant ${ctx.tenant} --new-platform --domain <your-domain>`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await (ctx.seams?.scaffoldPlatform ?? scaffoldPlatformRepo)(setup);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
return { name, status: "fail", detail: `platform repo not scaffolded: ${err.message.split("\n")[0]}`, fix: `mesh init platform ${ctx.tenant} --dir <empty-dir>` };
|
|
193
|
+
}
|
|
194
|
+
const bucket = await ensureStateBucket(setup, ctx.seams?.stateBucket);
|
|
195
|
+
const next = renderPlatformNextSteps(setup, { bucket, relativeDir: path.relative(ctx.cwd, setup.dir) || "." });
|
|
196
|
+
return {
|
|
197
|
+
name,
|
|
198
|
+
status: setup.createStateBucket && !bucket.present ? "warn" : "pass",
|
|
199
|
+
detail: `platform repo scaffolded at ${setup.dir} (${setup.tenant}.${setup.env}, ${setup.region}, zone ${setup.env}.${setup.domain})` +
|
|
200
|
+
(setup.createStateBucket ? ` — state bucket: ${bucket.detail}` : ` — state bucket ${setup.stateBucket} not created (see Next)`),
|
|
201
|
+
next,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
135
205
|
const platformStep = {
|
|
136
206
|
name: "platform",
|
|
137
207
|
async run(ctx) {
|
|
138
208
|
const name = STEP_LABELS.platform;
|
|
209
|
+
if (isNewPlatform(ctx.mode))
|
|
210
|
+
return newPlatformStep.run(ctx);
|
|
139
211
|
if (!ctx.context)
|
|
140
212
|
return { name, status: "skip", detail: "local only — no platform to sign in to" };
|
|
141
213
|
const context = ctx.context;
|
|
@@ -192,6 +264,9 @@ export const repoStep = {
|
|
|
192
264
|
const name = STEP_LABELS.repo;
|
|
193
265
|
if (ctx.skipRepo)
|
|
194
266
|
return { name, status: "skip", detail: "--skip-repo" };
|
|
267
|
+
if (isNewPlatform(ctx.mode)) {
|
|
268
|
+
return { name, status: "skip", detail: "setting up a platform — the apps repo comes after the first deploy (see Next)" };
|
|
269
|
+
}
|
|
195
270
|
const { kind, root } = classifyRepo(ctx.cwd);
|
|
196
271
|
switch (kind) {
|
|
197
272
|
case "platform-monorepo":
|
|
@@ -259,12 +334,15 @@ export const repoStep = {
|
|
|
259
334
|
}
|
|
260
335
|
},
|
|
261
336
|
};
|
|
262
|
-
const skillsStep = {
|
|
337
|
+
export const skillsStep = {
|
|
263
338
|
name: "skills",
|
|
264
339
|
async run(ctx) {
|
|
265
340
|
const name = STEP_LABELS.skills;
|
|
266
341
|
if (ctx.skipRepo)
|
|
267
342
|
return { name, status: "skip", detail: "--skip-repo" };
|
|
343
|
+
if (isNewPlatform(ctx.mode)) {
|
|
344
|
+
return { name, status: "skip", detail: "synced into the new platform repo by the scaffold" };
|
|
345
|
+
}
|
|
268
346
|
const { kind, root } = classifyRepo(ctx.cwd);
|
|
269
347
|
if (kind !== "apps-repo" && kind !== "empty-apps-repo") {
|
|
270
348
|
return { name, status: "skip", detail: "no apps repo to install skills into" };
|
|
@@ -341,11 +419,15 @@ export async function runInitWizard(opts, deps = {}) {
|
|
|
341
419
|
choices: [
|
|
342
420
|
{ name: "Local only (mesh start / mesh dev — no deployed platform yet)", value: "local" },
|
|
343
421
|
{ name: `A deployed Mesh platform (I have a ${tenant}.<env> context)`, value: "deployed" },
|
|
422
|
+
{ name: "Nothing yet — set up a new Mesh platform in my AWS account", value: "new" },
|
|
344
423
|
],
|
|
345
424
|
default: recorded && recorded.platform !== "local" ? "deployed" : "local",
|
|
346
425
|
});
|
|
347
426
|
if (choice === "local")
|
|
348
427
|
mode = "local";
|
|
428
|
+
else if (choice === "new") {
|
|
429
|
+
mode = { newPlatform: platformSetupFlags({ ...opts, newPlatform: true }) };
|
|
430
|
+
}
|
|
349
431
|
else {
|
|
350
432
|
const env = await prompts.input({
|
|
351
433
|
message: "Environment",
|
|
@@ -359,7 +441,7 @@ export async function runInitWizard(opts, deps = {}) {
|
|
|
359
441
|
cwd,
|
|
360
442
|
tenant,
|
|
361
443
|
mode,
|
|
362
|
-
context: mode === "local" ? null : `${tenant}.${mode.platform}`,
|
|
444
|
+
context: mode === "local" || isNewPlatform(mode) ? null : `${tenant}.${mode.platform}`,
|
|
363
445
|
device: opts.device,
|
|
364
446
|
profile: opts.profile,
|
|
365
447
|
interactive,
|
|
@@ -372,7 +454,7 @@ export async function runInitWizard(opts, deps = {}) {
|
|
|
372
454
|
logInfo(`Setting up tenant '${tenant}' (${describeMode(mode)})…`);
|
|
373
455
|
const steps = await runWizardSteps(ctx, deps.steps ?? WIZARD_STEPS);
|
|
374
456
|
const { kind, root } = classifyRepo(cwd);
|
|
375
|
-
if (!opts.skipRepo && (kind === "apps-repo" || kind === "empty-apps-repo")) {
|
|
457
|
+
if (!opts.skipRepo && !isNewPlatform(mode) && (kind === "apps-repo" || kind === "empty-apps-repo")) {
|
|
376
458
|
const written = writeMeshJson(root, { tenant, platform: mode === "local" ? "local" : mode.platform });
|
|
377
459
|
logInfo(`Recorded tenant + mode in ${written}`);
|
|
378
460
|
}
|
|
@@ -8,6 +8,14 @@ import { readCredentials } from "./login.js";
|
|
|
8
8
|
import { resolveTargetRoot, syncSkills } from "./skills.js";
|
|
9
9
|
import { detectLocalTenant } from "./local/dev-local.js";
|
|
10
10
|
import { LOGIN_CONTEXT } from "./local/seed-zitadel.js";
|
|
11
|
+
import { PLATFORM_SETUP_DEFAULTS } from "./init/platform-setup.js";
|
|
12
|
+
async function setupPrompts() {
|
|
13
|
+
const [{ default: input }, { default: confirm }] = await Promise.all([
|
|
14
|
+
import("@inquirer/input"),
|
|
15
|
+
import("@inquirer/confirm"),
|
|
16
|
+
]);
|
|
17
|
+
return { input: (args) => input(args), confirm: (args) => confirm(args) };
|
|
18
|
+
}
|
|
11
19
|
const LOCAL_HUB_API = "http://localhost:4568";
|
|
12
20
|
function isLocalContext(context) {
|
|
13
21
|
return context === LOGIN_CONTEXT;
|
|
@@ -211,12 +219,19 @@ export function registerInitCommands(program) {
|
|
|
211
219
|
const init = program
|
|
212
220
|
.command("init")
|
|
213
221
|
.enablePositionalOptions()
|
|
214
|
-
.description("Set up this machine and repo for a tenant — a guided wizard with no subcommand
|
|
222
|
+
.description("Set up this machine and repo for a tenant — a guided wizard with no subcommand. It starts from nothing: it asks where the tenant runs, and 'nowhere yet' is one of the answers (it scaffolds you a platform). Then package-registry sign-in, repo, skills. `init platform` is the same scaffold on its own; `init app-tenant` is the apps-repo doctor")
|
|
215
223
|
.option("--tenant <name>", "app tenant this repo belongs to (prompted when omitted on a TTY)")
|
|
216
224
|
.option("--local", "local only — mesh start / mesh dev, no deployed platform (the default)")
|
|
217
|
-
.option("--platform <env>", "a deployed platform's environment; signs in to
|
|
225
|
+
.option("--platform <env>", "a deployed platform's environment; signs in to `<tenant>.<env>`")
|
|
226
|
+
.option("--new-platform [env]", `nothing deployed yet — scaffold a tenant platform repo for \`<tenant>.<env>\` (default env: ${PLATFORM_SETUP_DEFAULTS.env}) and print its deploy sequence`)
|
|
227
|
+
.option("--region <region>", `--new-platform: AWS region (default: ${PLATFORM_SETUP_DEFAULTS.region})`)
|
|
228
|
+
.option("--domain <domain>", "--new-platform: base public domain you control; the core stack creates the zone {env}.{domain}. No default — a placeholder scaffolds a zone that can never validate")
|
|
229
|
+
.option("--state-bucket <bucket>", "--new-platform: Pulumi state S3 bucket (default: {tenant}-mesh-platform-pulumi-state)")
|
|
230
|
+
.option("--create-state-bucket", "--new-platform: create the state bucket (versioned) if it does not exist")
|
|
231
|
+
.option("--no-create-state-bucket", "--new-platform: never create the state bucket; print the aws command instead")
|
|
232
|
+
.option("--dir <path>", "--new-platform: where to scaffold the platform repo (default: ./{tenant}-mesh-platform)")
|
|
218
233
|
.option("--device", "device-code sign-in for the registry and platform steps (headless / SSH)")
|
|
219
|
-
.option("--profile <aws-profile>", "
|
|
234
|
+
.option("--profile <aws-profile>", "AWS profile this machine uses: read the registry without the broker (deployers only), and the profile a --new-platform stack config references")
|
|
220
235
|
.option("--skip-repo", "do not bootstrap, check or record anything in the current repo", false)
|
|
221
236
|
.option("--yes", "accept every default and never prompt", false)
|
|
222
237
|
.option("--json", "one JSON document on stdout ({tenant, mode, steps, next}); human output on stderr", false)
|
|
@@ -228,47 +243,38 @@ export function registerInitCommands(program) {
|
|
|
228
243
|
});
|
|
229
244
|
init
|
|
230
245
|
.command("platform <tenant>")
|
|
231
|
-
.description("Scaffold a tenant PLATFORM repo (core + platform Pulumi layers on @mesh-tech/infra-components) that deploys with `mesh deploy` unmodified — the proven mesh-sandbox shape, annotated.")
|
|
232
|
-
.option("--env <env>",
|
|
233
|
-
.option("--region <region>",
|
|
234
|
-
.option("--domain <domain>", "base public domain
|
|
235
|
-
.option("--profile <profile>",
|
|
236
|
-
.option("--state-bucket <bucket>", "Pulumi state S3 bucket (
|
|
246
|
+
.description("Scaffold a tenant PLATFORM repo (core + platform Pulumi layers on @mesh-tech/infra-components) that deploys with `mesh deploy` unmodified — the proven mesh-sandbox shape, annotated. On a terminal it ASKS for the values that differ per install (env, region, domain, state bucket, AWS profile), each pre-filled with its default; every answer is also a flag.")
|
|
247
|
+
.option("--env <env>", `environment the first stack targets (default: ${PLATFORM_SETUP_DEFAULTS.env})`)
|
|
248
|
+
.option("--region <region>", `AWS region (default: ${PLATFORM_SETUP_DEFAULTS.region})`)
|
|
249
|
+
.option("--domain <domain>", "base public domain you control — the core stack creates the Route 53 zone {env}.{domain} and waits on its NS delegation. Required: it has no default, because a placeholder scaffolds a zone that can never validate")
|
|
250
|
+
.option("--profile <profile>", `AWS profile the stack config references (default: ${PLATFORM_SETUP_DEFAULTS.profile})`)
|
|
251
|
+
.option("--state-bucket <bucket>", "Pulumi state S3 bucket (default: {tenant}-mesh-platform-pulumi-state)")
|
|
252
|
+
.option("--create-state-bucket", "create the state bucket (versioned) if it does not exist — prompted on a terminal")
|
|
253
|
+
.option("--no-create-state-bucket", "never create the state bucket; print the aws command instead")
|
|
237
254
|
.option("--dir <path>", "target directory (default: ./{tenant}-mesh-platform)")
|
|
255
|
+
.option("--yes", "accept every default and never prompt", false)
|
|
238
256
|
.action(async (tenant, opts) => {
|
|
239
257
|
if (!/^[a-z][a-z0-9-]*$/.test(tenant)) {
|
|
240
258
|
throw new MeshCliError(`Invalid tenant name '${tenant}' (lowercase alphanumeric + dashes).`);
|
|
241
259
|
}
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
env: opts.env,
|
|
254
|
-
region: opts.region,
|
|
255
|
-
domain: opts.domain,
|
|
256
|
-
profile: opts.profile,
|
|
257
|
-
stateBucket: opts.stateBucket ?? `${tenant}-mesh-platform-pulumi-state`,
|
|
258
|
-
};
|
|
259
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
260
|
-
copyTemplate(templateDir, targetDir, context);
|
|
261
|
-
syncSkills(targetDir);
|
|
262
|
-
logInfo(`Platform repo scaffolded at ${targetDir}`);
|
|
260
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
261
|
+
const { resolvePlatformSetup, scaffoldPlatformRepo, ensureStateBucket, renderPlatformNextSteps } = await import("./init/platform-setup.js");
|
|
262
|
+
const setup = await resolvePlatformSetup(tenant, opts, {
|
|
263
|
+
prompts: interactive && !opts.yes ? await setupPrompts() : undefined,
|
|
264
|
+
interactive,
|
|
265
|
+
});
|
|
266
|
+
await scaffoldPlatformRepo(setup);
|
|
267
|
+
logInfo(`Platform repo scaffolded at ${setup.dir}`);
|
|
268
|
+
const bucket = await ensureStateBucket(setup);
|
|
269
|
+
if (bucket.detail !== "not requested")
|
|
270
|
+
logInfo(`Pulumi state bucket: ${bucket.detail}`);
|
|
263
271
|
console.log("");
|
|
264
272
|
console.log("Next steps:");
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
console.log(" cd core && mesh deploy up");
|
|
269
|
-
console.log(" cd ../platform && mesh deploy up");
|
|
273
|
+
for (const line of renderPlatformNextSteps(setup, { bucket, relativeDir: path.relative(process.cwd(), setup.dir) || "." })) {
|
|
274
|
+
console.log(line ? ` ${line}` : "");
|
|
275
|
+
}
|
|
270
276
|
console.log("");
|
|
271
|
-
logInfo(`Review the annotated stack configs (Pulumi.${tenant}-${
|
|
277
|
+
logInfo(`Review the annotated stack configs (Pulumi.${tenant}-${setup.env}.yaml) before the first deploy.`);
|
|
272
278
|
});
|
|
273
279
|
init
|
|
274
280
|
.command("app-tenant")
|
|
@@ -50,6 +50,10 @@ export declare function registerLoginCommand(program: Command): void;
|
|
|
50
50
|
export declare const RESERVED_REGISTRY_CONTEXT = "registry";
|
|
51
51
|
export declare const NO_REGISTRY_DEPRECATED = "--no-registry is deprecated and ignored: mesh login no longer touches the registry.";
|
|
52
52
|
export declare const REGISTRY_SEPARATE_HINT = "Package registry access is separate from platform sign-in \u2014 run: mesh registry login";
|
|
53
|
+
export declare function shorthandContext(context: string): {
|
|
54
|
+
tenant: string;
|
|
55
|
+
env: string;
|
|
56
|
+
} | null;
|
|
53
57
|
export declare function renderNoConfigHelp(context: string): string;
|
|
54
58
|
export declare function tokenStillValid(expiresAt: string, marginMs?: number, now?: number): boolean;
|
|
55
59
|
declare function getValidToken(context: string, opts?: {
|
|
@@ -622,7 +622,7 @@ async function showStatus(context, config) {
|
|
|
622
622
|
export function registerLoginCommand(program) {
|
|
623
623
|
program
|
|
624
624
|
.command("login")
|
|
625
|
-
.description("
|
|
625
|
+
.description("PLATFORM access: sign in to a deployed Mesh platform (`<tenant>.<env>`) — what `mesh deploy`, `mesh stack`, the Hub and the VPN use. It never touches your ~/.npmrc, and you do not need it to install packages: that is `mesh registry login`. OIDC PKCE, or device code with --device.")
|
|
626
626
|
.argument("<context>", 'Platform context (e.g., "mesh.dev")')
|
|
627
627
|
.option("--status", "Show current authentication status — the Zitadel session if one is cached, otherwise the AWS identity the credential chain resolves (SSO profile or key pair). Exits non-zero only when neither works.")
|
|
628
628
|
.option("--device", "Force device code flow (no callback server needed)")
|
|
@@ -668,7 +668,7 @@ export function registerLoginCommand(program) {
|
|
|
668
668
|
});
|
|
669
669
|
program
|
|
670
670
|
.command("logout")
|
|
671
|
-
.description("Clear cached
|
|
671
|
+
.description("Clear a platform context's cached credentials (the package-registry session under `mesh registry` is untouched)")
|
|
672
672
|
.argument("<context>", 'Platform context (e.g., "mesh.dev")')
|
|
673
673
|
.action((context) => {
|
|
674
674
|
refuseReservedRegistryContext(context, "logout");
|
|
@@ -698,8 +698,20 @@ async function hintRegistryIfMissing() {
|
|
|
698
698
|
catch {
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
|
+
export function shorthandContext(context) {
|
|
702
|
+
const parts = context.split(".");
|
|
703
|
+
if (parts.length !== 2)
|
|
704
|
+
return null;
|
|
705
|
+
const [tenant, env] = parts;
|
|
706
|
+
return tenant && env ? { tenant, env } : null;
|
|
707
|
+
}
|
|
701
708
|
export function renderNoConfigHelp(context) {
|
|
702
709
|
const pinned = firstPartyDomainFor(context);
|
|
710
|
+
const shorthand = shorthandContext(context);
|
|
711
|
+
const createOne = shorthand
|
|
712
|
+
? `mesh init --tenant ${shorthand.tenant} --new-platform ${shorthand.env}`
|
|
713
|
+
: "mesh init --tenant <tenant> --new-platform <env>";
|
|
714
|
+
const scaffoldOne = `mesh init platform ${shorthand?.tenant ?? "<tenant>"} --domain <a domain you control>`;
|
|
703
715
|
return (`No configuration found for "${context}".\n\n` +
|
|
704
716
|
`Tried:\n` +
|
|
705
717
|
` 1. SSM: /mesh-platform/${context.split(".")[0]}/${context.split(".")[1]}/platform/zitadel\n` +
|
|
@@ -709,6 +721,8 @@ export function renderNoConfigHelp(context) {
|
|
|
709
721
|
: ` 2. HTTPS: https://cli.${context}/.well-known/mesh.json\n` +
|
|
710
722
|
` (only attempted for a full domain, or a known first-party context)\n\n`) +
|
|
711
723
|
`To fix:\n` +
|
|
724
|
+
` - No platform deployed yet? Create one: ${createOne}\n` +
|
|
725
|
+
` (or the scaffold on its own: ${scaffoldOne})\n` +
|
|
712
726
|
` - Use the platform's full domain: mesh login <env>.<tenant>.meshtech.io\n` +
|
|
713
727
|
` - Or, if you have AWS access, ensure SSO is active: aws sso login --profile <profile>\n` +
|
|
714
728
|
` - Or add config manually to ${CONFIG_FILE}:\n\n` +
|