@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.
Files changed (53) hide show
  1. package/README.md +74 -246
  2. package/dist/bin/mesh.js +1415 -842
  3. package/dist/bin/mesh.js.map +3 -3
  4. package/dist/build-info.json +2 -2
  5. package/dist/src/commands/db/axes.d.ts +19 -0
  6. package/dist/src/commands/db/axes.js +35 -0
  7. package/dist/src/commands/db/connect.d.ts +2 -4
  8. package/dist/src/commands/db/connect.js +4 -19
  9. package/dist/src/commands/db/credentials.d.ts +2 -5
  10. package/dist/src/commands/db/credentials.js +7 -23
  11. package/dist/src/commands/db/env.d.ts +2 -5
  12. package/dist/src/commands/db/env.js +7 -23
  13. package/dist/src/commands/db/exec.js +7 -24
  14. package/dist/src/commands/db/psql.d.ts +2 -5
  15. package/dist/src/commands/db/psql.js +8 -23
  16. package/dist/src/commands/init/platform-setup.d.ts +68 -0
  17. package/dist/src/commands/init/platform-setup.js +203 -0
  18. package/dist/src/commands/init/wizard.d.ts +20 -0
  19. package/dist/src/commands/init/wizard.js +89 -7
  20. package/dist/src/commands/init.js +42 -36
  21. package/dist/src/commands/login.d.ts +4 -0
  22. package/dist/src/commands/login.js +16 -2
  23. package/dist/src/commands/registry.d.ts +2 -2
  24. package/dist/src/commands/registry.js +11 -6
  25. package/dist/src/commands/secrets/context.d.ts +5 -0
  26. package/dist/src/commands/secrets/context.js +5 -0
  27. package/dist/src/commands/secrets/index.js +3 -0
  28. package/dist/src/commands/secrets/migrate-config.d.ts +1 -0
  29. package/dist/src/commands/secrets/migrate-config.js +2 -3
  30. package/dist/src/commands/secrets/reindex.d.ts +1 -0
  31. package/dist/src/commands/secrets/reindex.js +2 -2
  32. package/dist/src/commands/secrets/set.d.ts +1 -0
  33. package/dist/src/commands/secrets/set.js +2 -2
  34. package/dist/src/commands/temporal.js +25 -8
  35. package/dist/src/commands/temporal.js.map +2 -2
  36. package/dist/src/commands/tunnel/index.d.ts +8 -0
  37. package/dist/src/commands/tunnel/index.js +3 -3
  38. package/dist/src/index.js +324 -62
  39. package/dist/src/index.js.map +3 -3
  40. package/dist/src/utils/context.d.ts +6 -1
  41. package/dist/src/utils/context.js +302 -39
  42. package/dist/src/utils/registry-identity.d.ts +5 -0
  43. package/dist/src/utils/registry-identity.js +16 -0
  44. package/fragments/agents/AGENTS.md.hbs +10 -2
  45. package/fragments/base/index.ts.hbs +15 -5
  46. package/package.json +2 -2
  47. package/skills/core/SKILL.md +73 -6
  48. package/templates/api-auth/index.ts.hbs +5 -4
  49. package/templates/api-role-gating/index.ts.hbs +5 -4
  50. package/templates/external-service/index.ts.hbs +5 -4
  51. package/templates/platform-repo/README.md.hbs +17 -3
  52. package/templates/platform-repo/core/Pulumi.{{tenant}}-{{env}}.yaml.hbs +7 -0
  53. 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
- return mode === "local" ? "local only" : `deployed platform (${mode.platform})`;
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
- if (opts.local && opts.platform) {
69
- throw new MeshCliError("--local and --platform are mutually exclusive.");
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.platform)
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 (tenant, local or deployed, package-registry sign-in, repo, skills); `init platform` scaffolds a platform repo, `init app-tenant` is the apps-repo doctor")
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 <tenant>.<env>")
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>", "deployers only: AWS profile that can read the registry (skips the broker)")
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>", "environment the first stack targets", "dev")
233
- .option("--region <region>", "AWS region", "us-east-2")
234
- .option("--domain <domain>", "base public domain (ingress zone becomes {env}.{domain})", "example.com")
235
- .option("--profile <profile>", "AWS profile the stack config references", "default")
236
- .option("--state-bucket <bucket>", "Pulumi state S3 bucket (created by an operator)", undefined)
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 targetDir = path.resolve(opts.dir ?? `${tenant}-mesh-platform`);
243
- if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
244
- throw new MeshCliError(`Target directory is not empty: ${targetDir}`, {
245
- remediation: { command: `mesh init platform ${tenant} --dir <empty-dir>` },
246
- });
247
- }
248
- const { copyTemplate } = await import("./create-app.js");
249
- const { findPackageRoot } = await import("./local/stack.js");
250
- const templateDir = path.join(findPackageRoot(), "templates", "platform-repo");
251
- const context = {
252
- tenant,
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
- console.log(` cd ${path.relative(process.cwd(), targetDir) || "."}`);
266
- console.log(` mesh login ${tenant}.${opts.env} # or an operator context with access`);
267
- console.log(" pnpm install");
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}-${opts.env}.yaml) before the first deploy.`);
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("Authenticate with Zitadel (OIDC PKCE or Device Code)")
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 Zitadel credentials")
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("Manage npm registry auth for @mesh-tech packages");
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 { type MeshContext } from "../../utils/context.js";
2
+ import { type StackSelector } from "../../utils/stack-flag.js";
3
+ export declare function resolveSecretsContext(opts: StackSelector & {
4
+ tenant?: string;
5
+ }, stackOpt?: string | undefined): MeshContext;
@@ -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,6 +1,7 @@
1
1
  export declare function migrateConfigCommand(opts: {
2
2
  force?: boolean;
3
3
  dryRun?: boolean;
4
+ tenant?: string;
4
5
  stack?: string;
5
6
  stage?: string;
6
7
  region?: string;
@@ -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 { resolveStackOption } from "../../utils/stack-flag.js";
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 = detectContext(resolveStackOption(opts));
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 = detectContext(stackOpt);
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 });
@@ -33,6 +33,7 @@ export declare function setCommand(servicePath: string | undefined, opts: {
33
33
  key?: string;
34
34
  all?: boolean;
35
35
  json?: string;
36
+ tenant?: string;
36
37
  stack?: string;
37
38
  stage?: string;
38
39
  region?: string;