@mesh-tech/mesh-cli 0.25.0 → 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 +1415 -842
- 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/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 +25 -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 +73 -6
- 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
|
@@ -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` +
|
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { MeshCliError } from "../utils/errors.js";
|
|
3
3
|
import type { RegistryTokenState } from "../utils/auth-preflight.js";
|
|
4
4
|
import { type RegistryBrokerFailure, type RegistryGrant } from "../utils/registry-broker.js";
|
|
5
|
-
import { readRegistrySession } from "../utils/registry-identity.js";
|
|
5
|
+
import { readRegistrySession, type RegistryAccountState } from "../utils/registry-identity.js";
|
|
6
6
|
export declare function codeartifactLogin(env?: Record<string, string>): boolean;
|
|
7
7
|
export declare function resolvePublisherRoleArn({ roleArnFlag, ssmArn, }: {
|
|
8
8
|
roleArnFlag?: string;
|
|
@@ -12,7 +12,7 @@ export declare function resolveCredentials(context?: string, roleArn?: string):
|
|
|
12
12
|
export declare function applyGrantToUserNpmrc(grant: RegistryGrant, npmrcPath?: string): {
|
|
13
13
|
path: string;
|
|
14
14
|
};
|
|
15
|
-
export declare function describeBrokerFailure(failure: RegistryBrokerFailure): {
|
|
15
|
+
export declare function describeBrokerFailure(failure: RegistryBrokerFailure, account?: RegistryAccountState): {
|
|
16
16
|
message: string;
|
|
17
17
|
hint?: string;
|
|
18
18
|
fatal: boolean;
|
|
@@ -5,7 +5,7 @@ import { logInfo, logSuccess, logWarn } from "../utils/log.js";
|
|
|
5
5
|
import { MeshCliError } from "../utils/errors.js";
|
|
6
6
|
import { awsConfigHasProfile, findUnscopedCodeArtifactRegistry, homeNpmrcPath, npmrcAuthKeyForEndpoint, probeRegistryToken, readAwsConfig, SSO_SESSION_SNIPPET, stripUnscopedCodeArtifactRegistry, upsertNpmrcLines, } from "../utils/auth-preflight.js";
|
|
7
7
|
import { fetchRegistryGrant, realGrantDeps, PUBLISHED_BROKER_TIMEOUT_MS, } from "../utils/registry-broker.js";
|
|
8
|
-
import { clearRegistrySession, describeSessionExpiry, ensureRegistrySession, identityChanged, logRegistryIdentity, readRegistrySession, refreshRegistrySession, resolveRegistryIdentity, } from "../utils/registry-identity.js";
|
|
8
|
+
import { clearRegistrySession, describeSessionExpiry, ensureRegistrySession, identityChanged, logRegistryIdentity, readRegistrySession, refreshRegistrySession, registryAccountState, resolveRegistryIdentity, } from "../utils/registry-identity.js";
|
|
9
9
|
import { readCredentials } from "./login.js";
|
|
10
10
|
import { registerRegistryPublish } from "./registry-publish.js";
|
|
11
11
|
const CA_DOMAIN = "mesh-platform";
|
|
@@ -222,7 +222,11 @@ export function applyGrantToUserNpmrc(grant, npmrcPath = homeNpmrcPath()) {
|
|
|
222
222
|
}
|
|
223
223
|
return { path: npmrcPath };
|
|
224
224
|
}
|
|
225
|
-
export function describeBrokerFailure(failure) {
|
|
225
|
+
export function describeBrokerFailure(failure, account = {}) {
|
|
226
|
+
const unverified = account.emailVerified === false
|
|
227
|
+
? `\n Your Mesh account${account.email ? ` (${account.email})` : ""} has never confirmed its email address, and an unconfirmed account cannot hold a registry grant.` +
|
|
228
|
+
`\n This is not something a role fixes: ask your Mesh contact to verify the address on your account (a self-registered, passkey-only sign-up lands here), then run: mesh registry logout && mesh registry login`
|
|
229
|
+
: "";
|
|
226
230
|
switch (failure.kind) {
|
|
227
231
|
case "no-broker":
|
|
228
232
|
return {
|
|
@@ -237,7 +241,7 @@ export function describeBrokerFailure(failure) {
|
|
|
237
241
|
};
|
|
238
242
|
case "unauthenticated":
|
|
239
243
|
return {
|
|
240
|
-
message: "The registry broker rejected your session (expired or wrong audience).",
|
|
244
|
+
message: "The registry broker rejected your session (expired or wrong audience)." + unverified,
|
|
241
245
|
hint: "mesh registry logout && mesh registry login",
|
|
242
246
|
fatal: false,
|
|
243
247
|
};
|
|
@@ -251,7 +255,8 @@ export function describeBrokerFailure(failure) {
|
|
|
251
255
|
(failure.requestUrl
|
|
252
256
|
? `\n Request access: ${failure.requestUrl}`
|
|
253
257
|
: "\n Ask a Mesh platform admin to grant your account registry access.") +
|
|
254
|
-
`\n ${NOT_AUTHORIZED_RERUN}
|
|
258
|
+
`\n ${NOT_AUTHORIZED_RERUN}` +
|
|
259
|
+
unverified,
|
|
255
260
|
fatal: true,
|
|
256
261
|
};
|
|
257
262
|
case "unavailable":
|
|
@@ -327,7 +332,7 @@ async function tryBrokerLogin(opts) {
|
|
|
327
332
|
}
|
|
328
333
|
if (result.ok)
|
|
329
334
|
return { grant: result.grant };
|
|
330
|
-
const { message, hint, fatal } = describeBrokerFailure(result.failure);
|
|
335
|
+
const { message, hint, fatal } = describeBrokerFailure(result.failure, registryAccountState(readCredentials(session.context)?.idToken));
|
|
331
336
|
if (fatal) {
|
|
332
337
|
throw new RegistryNotAuthorizedError(message);
|
|
333
338
|
}
|
|
@@ -627,7 +632,7 @@ export function describeAwsChainFailure(outcome) {
|
|
|
627
632
|
export function registerRegistryCommands(program) {
|
|
628
633
|
const registry = program
|
|
629
634
|
.command("registry")
|
|
630
|
-
.description("
|
|
635
|
+
.description("PACKAGE access: the sign-in that makes @mesh-tech/* installable. One global identity — no AWS account, no platform, no `<tenant>.<env>` context. (`mesh login <tenant>.<env>` is the other one; that is for deploying to or operating a platform.)");
|
|
631
636
|
registry
|
|
632
637
|
.command("login")
|
|
633
638
|
.description("Sign in to the Mesh package registry (browser, or device code with --device) and configure ~/.npmrc + the project .npmrc. Needs no platform context and no AWS account.")
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { detectContext } from "../../utils/context.js";
|
|
2
|
+
import { resolveStackOption } from "../../utils/stack-flag.js";
|
|
3
|
+
export function resolveSecretsContext(opts, stackOpt = resolveStackOption(opts)) {
|
|
4
|
+
return detectContext(stackOpt, { tenant: opts.tenant, stack: stackOpt });
|
|
5
|
+
}
|
|
@@ -21,6 +21,7 @@ export function registerSecretsCommands(program) {
|
|
|
21
21
|
.option("--key <key>", "Instance key for multi-instance services (e.g., FI ID)")
|
|
22
22
|
.option("--all", "Prompt for all fields including optional ones")
|
|
23
23
|
.option("--json <json>", "Non-interactive: provide values as JSON")
|
|
24
|
+
.option("-t, --tenant <tenant>", "Tenant to address (override detection)")
|
|
24
25
|
.option("--stack <stack>", "Pulumi stack name (override detection)")
|
|
25
26
|
.addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
|
|
26
27
|
.option("--region <region>", "AWS region (default: us-east-2)")
|
|
@@ -31,6 +32,7 @@ export function registerSecretsCommands(program) {
|
|
|
31
32
|
.command("reindex [service]")
|
|
32
33
|
.description("Rebuild a multi-instance external service's instance index (the {prefix}/.index secret listInstances() reads) from the per-key secrets actually in Secrets Manager — the repair for an instance secret created or deleted out of band")
|
|
33
34
|
.option("--dry-run", "Show what would change without writing")
|
|
35
|
+
.option("-t, --tenant <tenant>", "Tenant to address (override detection)")
|
|
34
36
|
.option("--stack <stack>", "Pulumi stack name (override detection)")
|
|
35
37
|
.addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
|
|
36
38
|
.option("--region <region>", "AWS region (default: us-east-2)")
|
|
@@ -42,6 +44,7 @@ export function registerSecretsCommands(program) {
|
|
|
42
44
|
.description("Backfill .config mirror secrets for all external services")
|
|
43
45
|
.option("--force", "Overwrite existing .config mirrors")
|
|
44
46
|
.option("--dry-run", "Show what would be written without writing")
|
|
47
|
+
.option("-t, --tenant <tenant>", "Tenant to address (override detection)")
|
|
45
48
|
.option("--stack <stack>", "Pulumi stack name (override detection)")
|
|
46
49
|
.addOption(new Option("--stage <stack>", "Deprecated alias for --stack").hideHelp())
|
|
47
50
|
.option("--region <region>", "AWS region (default: us-east-2)")
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { SSMClient, GetParametersByPathCommand, } from "@aws-sdk/client-ssm";
|
|
2
2
|
import { SecretsManagerClient, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, ListSecretsCommand, } from "@aws-sdk/client-secrets-manager";
|
|
3
|
-
import { detectContext } from "../../utils/context.js";
|
|
4
3
|
import { logInfo, logSuccess, logError } from "../../utils/log.js";
|
|
5
|
-
import {
|
|
4
|
+
import { resolveSecretsContext } from "./context.js";
|
|
6
5
|
function normalizeEntry(raw) {
|
|
7
6
|
if (raw.type === "group" && raw.fields) {
|
|
8
7
|
return {
|
|
@@ -198,7 +197,7 @@ async function migrateService(smClient, meta, schema, opts) {
|
|
|
198
197
|
return { migrated, skipped, errors };
|
|
199
198
|
}
|
|
200
199
|
export async function migrateConfigCommand(opts) {
|
|
201
|
-
const context =
|
|
200
|
+
const context = resolveSecretsContext(opts);
|
|
202
201
|
const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
|
|
203
202
|
const ssmClient = new SSMClient({ region });
|
|
204
203
|
const smClient = new SecretsManagerClient({ region });
|
|
@@ -2,6 +2,7 @@ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
|
2
2
|
export declare function scanInstanceKeys(client: SecretsManagerClient, secretPrefix: string): Promise<string[]>;
|
|
3
3
|
export declare function indexAlreadyMatches(missing: string[], stale: string[], indexExists: boolean, malformed: boolean): boolean;
|
|
4
4
|
export declare function reindexCommand(servicePath: string | undefined, opts: {
|
|
5
|
+
tenant?: string;
|
|
5
6
|
stack?: string;
|
|
6
7
|
stage?: string;
|
|
7
8
|
region?: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SSMClient } from "@aws-sdk/client-ssm";
|
|
2
2
|
import { SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand, PutSecretValueCommand, CreateSecretCommand, } from "@aws-sdk/client-secrets-manager";
|
|
3
3
|
import { buildInstanceIndexValue, instanceIndexSecretId, instanceKeyFromSecretName, parseInstanceIndex, } from "@mesh-tech/secrets";
|
|
4
|
-
import { detectContext } from "../../utils/context.js";
|
|
5
4
|
import { logInfo, logSuccess, logError } from "../../utils/log.js";
|
|
6
5
|
import { resolveStackOption } from "../../utils/stack-flag.js";
|
|
6
|
+
import { resolveSecretsContext } from "./context.js";
|
|
7
7
|
import { discoverExternalServices, ensureAwsCredentialsForStack, isResourceNotFound, } from "./set.js";
|
|
8
8
|
export async function scanInstanceKeys(client, secretPrefix) {
|
|
9
9
|
const keys = [];
|
|
@@ -27,7 +27,7 @@ export function indexAlreadyMatches(missing, stale, indexExists, malformed) {
|
|
|
27
27
|
}
|
|
28
28
|
export async function reindexCommand(servicePath, opts) {
|
|
29
29
|
const stackOpt = resolveStackOption(opts);
|
|
30
|
-
const context =
|
|
30
|
+
const context = resolveSecretsContext(opts, stackOpt);
|
|
31
31
|
const region = opts.region ?? process.env.AWS_REGION ?? "us-east-2";
|
|
32
32
|
await ensureAwsCredentialsForStack(stackOpt ?? context.stage);
|
|
33
33
|
const ssmClient = new SSMClient({ region });
|