@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
@@ -91,20 +91,20 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
91
91
  |---|---|---|---|
92
92
  | `mesh login <context>` | (also `mesh logout <context>`) | Zitadel OIDC PKCE or device-code auth; caches creds in `~/.config/mesh/`. Context like `mesh.dev`. Auto-discovers config via SSM then HTTPS `.well-known/mesh.json`. | `--status` (reports the cached Zitadel session, or — when there isn't one — the AWS identity the credential chain resolves, so an SSO profile or a key/secret pair passes too; non-zero only when neither works), `--device`, `--export` (print AWS `export`s for `eval`), `--role <arn>`, `--region <r>` |
93
93
  | `mesh registry` | `login [context]`, `logout`, `status`, `publish` | CodeArtifact auth → refreshes `~/.npmrc` token (12h) + adds `@mesh-tech` scope to project `.npmrc` + **strips an unscoped CodeArtifact `registry=` line from `~/.npmrc`** (after writing `~/.npmrc.bak-<ts>`) — that line is what a raw CodeArtifact sign-in *without* `--namespace @mesh-tech` leaves behind, and it makes CodeArtifact your DEFAULT registry so every *public* install 401s at the 12h expiry. Skipped under `--ci`, where the unscoped login is deliberate. `status` probes the registry rather than grepping the file: expired/missing token or a hijack exits 1, an unreachable registry warns and exits 0. **Step 0 is the Zitadel-gated broker:** the registry is ONE global identity (`utils/registry-identity.ts`, seeded from the compiled-in first-party alias table, session cached under the reserved key `registry`), so no context is needed — `mesh registry login` signs in to the registry's issuer if nothing is cached (browser; `--device` when headless; reuses a `mesh.dev` session on the same issuer) and POSTs the id token to the platform's `registry-auth` service, which returns a CodeArtifact read token minted with its OWN AWS identity — so this path needs no AWS account, CLI or profile at all. It writes the same two `~/.npmrc` lines the AWS path does. Falls back to the AWS chain when the platform publishes no broker, when the broker is unreachable, or with `--no-broker`; a 403 (a valid Mesh account without the `registry:read` role) is fatal and does **not** fall through. `--publish` and `--profile` deliberately skip the broker — both are AWS-identity paths. `--profile mesh-dev` auto-runs the AWS SSO login when the session is stale. `mesh login` never touches the registry (it prints a one-line pointer to `mesh registry login` when the machine has no registry access); `logout` clears the registry session only. A `[context]` on the read path is ignored with a notice; it still selects the SSM export for `--publish`. `login --publish` assumes the registry's publisher role (from `--role` or the registry SSM export); `publish` pushes package snapshots. | `login`: `--device`, `--no-broker`, `--profile <sso>`, `--role <arn>`, `--ci`, `--skip-npmrc`, `--publish`; `publish`: `--snapshot` |
94
- | `mesh db` | `connect`, `credentials`(alias `creds`), `env`, `psql`, `exec <command...>` | Access tenant DBs through an SSM bastion tunnel. `env` prints `export`s for `eval`; `exec` runs a command with `DATABASE_URL` injected. | `-t/--tenant`, `-e/--env`, `-a/--app`, `--app-tenant`, `--app-stage`; `exec` also: `--secret <arn>`, `--ssl <require\|no-verify\|disable>`, `--port <n>` |
94
+ | `mesh db` | `connect`, `credentials`(alias `creds`), `env`, `psql`, `exec <command...>` | Access tenant DBs through an SSM bastion tunnel. `env` prints `export`s for `eval`; `exec` runs a command with `DATABASE_URL` injected. Outside an app directory pass `-t` and `-e` together — detection fails closed. | `-t/--tenant`, `-e/--env`, `-a/--app`, `--app-tenant`, `--app-stage`; `exec` also: `--secret <arn>`, `--ssl <require\|no-verify\|disable>`, `--port <n>` |
95
95
  | `mesh deploy ...` | passthrough | Runs `pulumi` with the stack's `mesh:deployerRole` assumed. Unknown opts/args pass through; defaults to `up` if no subcommand. | `-s/--stack <name>` (stripped before pulumi `up`); everything else → pulumi (`preview`, `up --yes`, `destroy`, `stack output …`) |
96
96
  | `mesh dev` | `logs <service>`, `restart <service>`, `list`, `test-user [name]` | Two modes. **Tethered** (deployed stack): reads the Pulumi stack dev output, opens tunnels, injects Secrets Manager secrets; needs VPN/AWS creds. **Local** (`--local`, auto when the app has no `Pulumi.yaml`): synthesizes services from the app layout (subdirs with a `dev` script, plus a synthesized `dg dev` webserver when the app root is a Dagster workspace — `dg.toml`, or `[tool.dg]` in `pyproject.toml`; such an app runs with zero package.json services — see `docs/guides/local-dagster.md`) and wires them to the `mesh start` platform (Temporal `local-dev`, app DB, Zitadel, ministack via `AWS_ENDPOINT_URL`) — zero AWS/VPN/Pulumi. `--externals` (local mode; `--mock` is an alias) also realizes the app's declared external services (package.json → `mesh.externals`; legacy `mesh.mocks` still read). Each declaration runs in one of three MODES — `mock` (the service is emulated: `openapi` → a Prism mock server from a spec, or `src` → a mock process), `local` (a local version of the real service: a `compose` file + published `port`, e.g. a vendor-shaped DB — up `--wait` before the app's services; a realization another checkout already started is **adopted** instead of failing — declared port already served **by a container this compose file defines** → adopt as-is (a publisher the file doesn't define — a native install or unrelated container — is a hard **conflict**, never adopted, since credentials would seed against the wrong service); a stopped foreign container holding the compose `container_name` → `docker start` + adopt — so startup order across checkouts doesn't matter; adopted externals stay out of session state and `--kill` tears down only externals this session created, volumes kept), `remote` (connect to the ACTUAL service: inline vendor `credentials`, or the credentials the app's tenant environment is configured with via `remote.env`). A declaration may carry SEVERAL realizations; `mode` picks the default and `--externals name=mode` overrides it per run (`--externals plaid-db=remote,plaid`). Remote externals get a blackbox uptime probe against their real endpoint. Failing to realize a remote external is fatal only when you named it; a bare `--externals` warns and keeps going, so the local loop still works with no AWS identity. Every realization seeds its ExternalService credential secret (`{{url}}`/`{{host}}`/`{{port}}` from the stand-in's endpoint, `{{env:VAR}}` from the shell) so `resolveCredentials()` runs the deployed code path against the stand-in. Runners: tmux (default) or `--runner docker` (CI/headless — services run as a docker compose project with host networking; parity `--status`/`--kill`/`logs`/`restart`). | root: `--local`, `--externals [name[=mode],…]` (alias `--mock`), `--runner <tmux\|docker>`, `--app <path>`, `--stage`, `--headless`, `--kill`, `--status`, `--json`, `--session <name>`, `--profile <sso>`; `logs`: `--tail <n>`; `test-user`: `--tenant/--env/--region` |
97
97
  | `mesh start` / `mesh stop` / `mesh status` | — | Full-local Mesh platform via docker compose (project `mesh-local`, assets ship in the CLI package): Postgres, Temporal (+UI), Zitadel, SpiceDB, ministack (local AWS fabric: SSM/Secrets/S3 registry; also seeds a general-purpose `mesh-local-data` bucket with sample data — see `docs/guides/local-dagster.md`), memcached, OpenSearch, a **local mailbox** (Mailpit — SMTP :1025 wired into Zitadel, inbox at http://localhost:8025, so user-activation and password-reset mail is testable locally instead of vanishing), **and the hosted observability backends** (OTel collector + Loki + Tempo + Prometheus — the Hub's logs/traces/metrics views run unchanged). Seeds tenant `local`/env `dev` on first boot and reconciles every Zitadel-provisioned app tenant into the registry. **No AWS creds, VPN, or Pulumi state needed — Docker only.** The **Hub is included by default** — three overlay services, `hub-api` + `hub-ui` + `hub-auth` (an oauth2-proxy; it is what binds the front-door port and proxies to `hub-ui`, so front-door 401s/redirect loops are in ITS logs, not `hub-api`'s) — the front door at http://localhost:9000 — `MESH_HUB_PORT=<port>` publishes it elsewhere when 9000 is taken; export it for `mesh status` too (images build once from the published @mesh-tech/hub tarball; needs `mesh registry login` the first time; `--no-hub` opts out). Auth contract: `--with-hub` refreshes to the latest published hub after preflighting the CodeArtifact token — with an expired/missing token it falls back to the already-built local images (warn + fixing command), or fails fast naming `mesh registry login --profile mesh-dev` when none exist; the default (no-flag) path with an expired/missing token starts hub-less and names the same fix. `--hub-from-source` sidesteps the registry entirely — it builds the Hub images from THIS checkout's `apps/hub`, which is the only way to run an unpublished Hub change in the containerized stack. Non-TTY runs (CI, agents, piped output) get an `[HH:MM:SS]` prefix on every step line plus a heartbeat during long fetches/builds — `MESH_LOG_TIMESTAMPS=1/0` forces the prefix on/off (default: on when stderr is not a TTY). | `start`: `--no-seed`, `--no-hub`, `--with-hub`, `--hub-from-source`, `--takeover`, `--skip-port-check`; `stop`: `--destroy` (drops volumes/seeds), `--force`; `status`: `--json` |
98
98
  | `mesh hub` | `dev` | One-command local Hub over a running dev-local stack. Launches the CURRENT-CODE Hub (apps/hub api+ui from a mesh-platform checkout) pointed at an existing `mesh dev` session — auto-assembles `HUB_TENANT`/`HUB_SCOPE_ENV`/`HUB_SCOPE_TENANTS` (session platform context), `TEMPORAL_ADDRESS` (session tunnel), `AWS_*` (session scratch profile), `DEV_USER_*`+`DEV_USER_TOKEN_URL` (session dev identity; starts its own token-server if the session predates it, so tokens stay fresh past ~1h). Refuses to launch with an empty tenant scope (which the Hub renders as silently blank). Runs in tmux session `mesh-hub-dev`. | `dev`: `--session <name>`, `--tenants <csv>`, `--port` (or `MESH_HUB_DEV_PORT`, default 9000; the containerized `mesh start` Hub uses the separate `MESH_HUB_PORT`), `--api-port <3002>`, `--platform-dir <dir>` (or `MESH_PLATFORM_DIR`), `--print-env`, `--kill` |
99
99
  | `mesh create-app` | — | Scaffold a tenant app. Composable mode (primitives) or legacy template mode. Interactive when TTY + missing flags. Run at the root of an **empty** `{tenant}-mesh-apps` git clone (a `.git` with no `package.json`/`apps/`/`tenants/` and no enclosing pnpm workspace), it first generates the repo-level shape — `pnpm-workspace.yaml` over `apps/*`, private root `package.json`, scoped-registry `.npmrc`, base `tsconfig.json`, `.gitignore`, `README.md`, `apps/` — then scaffolds the app. Never overwrites an existing file. | `--tenant`, `--name`, `--primitives <csv>` (`service,database,temporal,bucket`), `--template <workflow\|api-auth\|api-role-gating\|external-service>`, `--test` (writes to `tests/tenants/`) |
100
100
  | `mesh app` | `check [apps...]` | Checks tenant apps against the **Mesh app contract** (`@mesh-tech/app-kit#apps` → `references/app-contract.md`): a UI is its own Service (`UI_IN_API`, `NO_UI_SERVICE`), the app registers with the Hub (`NO_REGISTER`), sign-in goes through the platform proxy (`CUSTOM_SESSION_AUTH`), logs go through `@mesh-tech/logger` (`NON_PLATFORM_LOGGER`), and people/roles/keys are surfaced through the Hub (`NO_AUTHZ_POINTER`, `METADATA_UNPUBLISHED`, `IAC_GRANTS`, `PASSWORD_STORE`, `EMAIL_ALLOWLIST`, `USERS_TABLE`), and a docs site is read through the Hub rather than on a hostname of its own, with its readers stated and grantable (`DOCS_SITE_PUBLIC` — advisory while the hostname is deprecated — `DOCS_AUDIENCE_IMPLICIT`, `DOCS_AUDIENCE_UNGRANTABLE`, `DOCS_VENDOR_AUDIENCE_PARTNER` — a vendor surface's reference is never partner-readable); a tenant-local auth lib is advisory (`LOCAL_AUTH_LIB`), as is a pointer that names its own Zitadel project instead of binding the env with `zitadelAppProjectId` (`POINTER_PROJECT_OVERRIDE`). One check per code on the `mesh dev doctor` engine; exit 1 on any BLOCK. The SAME gate `mesh create-app` runs after scaffolding, tenant CI runs beside `mesh skills sync --check`, and the platform reviewer runs on a PR. Default: every `apps/*` with a Pulumi program. | `[apps...]` (repo-relative dirs), `--root <dir>`, `--json`, `--verbose` |
101
- | `mesh init` | (bare = wizard), `app-tenant`, `platform <tenant>` | **Bare `mesh init` is the guided setup wizard**: tenant (pre-filled from `mesh.json` / Pulumi `mesh:tenant`), local-only vs deployed platform, registry access (in-process `mesh registry login`; a 403 marks the step ✘ and the wizard continues), platform sign-in on the deployed branch only, repo bootstrap (empty clone, or an empty non-git folder the wizard offers to `git init`) or shape check, skills sync; writes `mesh.json` (`{tenant, platform}`) so `create-app` / `mesh dev` default `--tenant`; flags `--tenant --local\|--platform <env> --device --profile --skip-repo --yes --json`; exit 1 if any step failed, 130 on Ctrl-C. `app-tenant` is the repo doctor (A1–A3): read-only check pipeline — CLI auth, registry access, public npm not hijacked, platform reachable (Hub API), app-tenant registration, deployer role, repo shape, agent skills — each failure names the exact `mesh` fix. **Registry access** is a live probe of the CodeArtifact token, not a grep of `~/.npmrc`: an expired token `fail`s (it used to `pass`), an unreachable registry `warn`s (a network fault is not an auth fault). **Public npm not hijacked** fails when `~/.npmrc` carries an unscoped `registry=<codeartifact>` line, which makes CodeArtifact your default registry — one `mesh registry login` fixes both. Exit 0 iff all pass; re-run any time. `--fix` applies developer-scope fixes (registry token, skills sync); operator actions stay remediations — and `--profile <p>` is the SSO profile `--fix` logs into CodeArtifact with, echoed back in the `fix:` command each check prints so the advertised command is the one that runs. | `app-tenant`: `--tenant <name>`, `--context <ctx>` (default `local`), `--hub-url <url>`, `--fix`, `--profile <sso>`, `--json` |
101
+ | `mesh init` | (bare = wizard), `app-tenant`, `platform <tenant>` | **Bare `mesh init` is the guided setup wizard**: tenant (pre-filled from `mesh.json` / Pulumi `mesh:tenant`), **where the tenant runs — local-only, a deployed platform, or NOTHING YET (scaffold one; §1c)**, registry access (in-process `mesh registry login`; a 403 marks the step ✘ and the wizard continues), platform sign-in on the deployed branch only, repo bootstrap (empty clone, or an empty non-git folder the wizard offers to `git init`) or shape check, skills sync; writes `mesh.json` (`{tenant, platform}`) so `create-app` / `mesh dev` default `--tenant`; flags `--tenant --local\|--platform <env>\|--new-platform [env] --device --profile --skip-repo --yes --json`, plus the scaffold's own under `--new-platform` (`--region --domain --state-bucket --create-state-bucket/--no-create-state-bucket --dir`); exit 1 if any step failed, 130 on Ctrl-C. `platform <tenant>` **prompts** on a TTY for env/region/profile/state bucket (defaults on each) and for `--domain`, which has **no** default because a placeholder scaffolds an unvalidatable zone; it offers to create the Pulumi state bucket (versioned) and prints the deploy sequence including the manual NS delegation. `app-tenant` is the repo doctor (A1–A3): read-only check pipeline — CLI auth, registry access, public npm not hijacked, platform reachable (Hub API), app-tenant registration, deployer role, repo shape, agent skills — each failure names the exact `mesh` fix. **Registry access** is a live probe of the CodeArtifact token, not a grep of `~/.npmrc`: an expired token `fail`s (it used to `pass`), an unreachable registry `warn`s (a network fault is not an auth fault). **Public npm not hijacked** fails when `~/.npmrc` carries an unscoped `registry=<codeartifact>` line, which makes CodeArtifact your default registry — one `mesh registry login` fixes both. Exit 0 iff all pass; re-run any time. `--fix` applies developer-scope fixes (registry token, skills sync); operator actions stay remediations — and `--profile <p>` is the SSO profile `--fix` logs into CodeArtifact with, echoed back in the `fix:` command each check prints so the advertised command is the one that runs. | `app-tenant`: `--tenant <name>`, `--context <ctx>` (default `local`), `--hub-url <url>`, `--fix`, `--profile <sso>`, `--json` |
102
102
  | `mesh skills` | `sync` | Agent-skill distribution (D1/D2): installs the base building-with-Mesh skills into the repo's `.claude/skills/mesh-*/` (managed-marker files) and wires TanStack-Intent discovery for the pattern skills shipping inside `@mesh-tech/*` packages (`.intent/hooks/` gate + `.claude/settings.json` SessionStart hook + AGENTS.md fence — the PR #2356 mechanism, vendored). Idempotent; runs automatically after `mesh create-app`. | `sync`: `--check` (CI/doctor: exit 1 when missing/stale), `--root <path>` |
103
103
  | `mesh docs` | `build`, `portal`, `cli-reference` | `build` builds/validates an **app's** OpenAPI specs from `docs/docs.config.json` (file copy or SymXchange generation). `start`/`stop`/`list` are the simple front door: `start` serves the docs on loopback — DETACHED in a tmux session named `mesh-docs` by default (prints the bare URL once live; `stop` kills the session; `--foreground` or any non-TTY runs in the foreground) — serving the working tree in a mesh-platform checkout, or the published `@mesh-tech/docs` artifact fetched from the role-gated CodeArtifact registry anywhere else (docs version == the @mesh-tech/* baseline it describes; `-v` pins, default latest; cached under `~/.cache/mesh/docs/`). `list` shows the published docs versions. `portal` assembles the **Mesh developer portal** (docs.meshtech.io) from every `docs.json`-opted-in doc root in the repo — a directory publishes its markdown iff it holds a `docs.json`; route/title/order/nav derive from the tree and each file, so adding a doc is adding one file — then hands the assembled tree to Zudoku (`apps/docs/`): default runs `zudoku build`, `--serve` runs `zudoku dev`, `--assemble-only`/`-o` stop after assembly. Links between published files are rewritten to routes; links to unpublished repo files become blob links. Reserved directory names (`plans/`, `designs/`, `incidents/`, … the frozen list in `src/docs/schema.ts`) are excluded at any depth. `cli-reference` regenerates `docs/portal/generated/cli-reference.md` from THIS command tree, so it can't document a flag the binary lacks. `portal --check` is the CI gate (`pnpm check:docs-portal`): docs.json schema validation, reserved-segment fail-close, broken links, and CLI-reference staleness; `--diff-base <ref>` prints the publish-set diff for PR job summaries. Hidden commands and `--version` are excluded from the reference. | `build`: `-c/--config <path>` (default `docs/docs.config.json`); `portal`: `-o/--out <dir>`, `--assemble-only`, `--serve`, `-p/--port <n>` (default `3000`), `--check`, `--print-manifest`, `--diff-base <ref>`, `--manifest-out <path>`; `cli-reference`: `-o/--out <path>`, `--check`; `start`: `-v/--version <v>` (default: latest), `-p/--port <n>` (default `4400`; `0` is foreground-only), `--dev` (foreground HMR), `--foreground`; `stop`: no flags; `list`: no flags |
104
104
  | `mesh stack` | `init`, `rm <name>` | `init` creates a personal dev stack `dev-{github-user}`, copying base-stack config, setting `mesh:deploy: false`, inheriting KMS secrets provider; `--worktree` appends the git-worktree token (`dev-{user}-{token}`) so concurrent worktrees deploy to **distinct** stacks (SSM paths, Temporal namespace, Nexus endpoint all derive from the stack name → one discriminator isolates all three; no-op on the primary checkout). `rm <name>` removes a personal stack (backend state + local config), credentialed. Both assume the deployer role — no raw `pulumi` / manual creds needed. | `init`: `--from <stack>`, `--name <stack>`, `--worktree`; `rm`: `--yes` |
105
105
  | `mesh tenant` | `add <name>`, `list` | Register/inspect app tenants on a platform stack. Run from the **platform layer** of a tenant platform repo (e.g. `mesh-sandbox/platform`). `add` inserts the tenant under `mesh:tenants` in `Pulumi.<stack>.yaml` via a comment-preserving YAML round-trip (untouched lines don't reformat), refuses a tenant that is already declared (an existing entry may carry fields a re-add would drop), derives the `{name}-{env}` subdomain from the stack name (echoed in the success line), and guards against running on the core layer or an app dir; it then prints the deploy + verify sequence — `mesh deploy up` is what makes the registration live. `list` prints what the stack config declares, so "is it registered?" is answerable without opening YAML (warns when the project doesn't look like a platform layer, so an empty result isn't mistaken for "no tenants"). | `add`: `--display-name <name>`, `--subdomain <sub>`, `--zitadel-org-id <id>`, `--stack <stack>`, `--json`; `list`: `--stack <stack>`, `--json` |
106
- | `mesh tunnel [group]` | `external <name>` | SSM port-forward to platform services. Groups: `dev` (temporal+db), `temporal`. **`external <name>`** tunnels to a *registered* `ExternalService`: host/port are read from its credential secret (`mesh/{app-tenant}/{app-stage}/external/{name}`) — the same values deployed apps resolve at runtime — so anything the bastion can reach is tunnelable with no per-service CLI entry and no bastion services-map change. `-t/-e` are **optional everywhere** — they fall back to the same Pulumi/SST context detection `mesh dev` uses, and `AWS_REGION`/`AWS_PROFILE` default from the nearest Pulumi config + the `{tenant}-{env}` profile convention (region falls back to the effective profile's own `region` — the `[default]` profile when none was resolved — before the platform default us-east-2). | root: `-t/--tenant`, `-e/--env`, `-l/--list`, `-s/--services <csv\|group>`, `--db-port`, `--temporal-ui-port`, `--temporal-frontend-port`; `external`: `-p/--port` (default: the remote port), `-k/--key <key>` (multi-instance `credentials.keyedBy` externals), `--app-tenant`/`--app-stage` (credential axis; default to `-t`/`-e`) |
107
- | `mesh secrets` | `exec <command...>`, `set [service]`, `reindex [service]`, `migrate-config` | Manage external-service creds in Secrets Manager. `exec` injects secrets as env; `set` reads field schema from SSM and prompts (or `--json`) — with `--key` it also records the instance in the service's instance index (`{prefix}/.index`, what `listInstances()` reads). `reindex` rebuilds that index from the per-key secrets actually in Secrets Manager (operator-credential `ListSecrets`) — the repair for an index that drifted (out-of-band secret create/delete, concurrent writers, malformed value) and the populate step when rolling the index out to an estate: **deploy the declaring stack first** (creates the managed, empty index), then `reindex`. | `set`: `--key <id>` (multi-instance), `--all`, `--json <str>`, `--stack`, `--region`; `reindex`: `--dry-run`, `--stack`, `--region`; `migrate-config`: `--force`, `--dry-run`, `--stack`, `--region` |
106
+ | `mesh tunnel [group]` | `external <name>` | SSM port-forward to platform services. Groups: `dev` (temporal+db), `temporal`. **`external <name>`** tunnels to a *registered* `ExternalService`: host/port are read from its credential secret (`mesh/{app-tenant}/{app-stage}/external/{name}`) — the same values deployed apps resolve at runtime — so anything the bastion can reach is tunnelable with no per-service CLI entry and no bastion services-map change. `-t/-e` are **optional inside an app directory** — they fall back to the same Pulumi/SST context detection `mesh dev` uses, which fails closed elsewhere, so pass `-t` outside an app dir (`-t` alone is enough; `-e` alone is not). `-e` names a platform env, not a stack, so it no longer doubles as the stage hint: the credential axis `external` keys on stays the app's real stack name. And `AWS_REGION`/`AWS_PROFILE` default from the nearest Pulumi config + the `{tenant}-{env}` profile convention (region falls back to the effective profile's own `region` — the `[default]` profile when none was resolved — before the platform default us-east-2). | root: `-t/--tenant`, `-e/--env`, `-l/--list`, `-s/--services <csv\|group>`, `--db-port`, `--temporal-ui-port`, `--temporal-frontend-port`; `external`: `-p/--port` (default: the remote port), `-k/--key <key>` (multi-instance `credentials.keyedBy` externals), `--app-tenant`/`--app-stage` (credential axis; default to `-t`/`-e`) |
107
+ | `mesh secrets` | `exec <command...>`, `set [service]`, `reindex [service]`, `migrate-config` | Manage external-service creds in Secrets Manager. `exec` injects secrets as env; `set` reads field schema from SSM and prompts (or `--json`) — with `--key` it also records the instance in the service's instance index (`{prefix}/.index`, what `listInstances()` reads). `reindex` rebuilds that index from the per-key secrets actually in Secrets Manager (operator-credential `ListSecrets`) — the repair for an index that drifted (out-of-band secret create/delete, concurrent writers, malformed value) and the populate step when rolling the index out to an estate: **deploy the declaring stack first** (creates the managed, empty index), then `reindex`. All three resolve tenant + platform env from the cwd stack file and the app's `config.ts`; `-t/--tenant` overrides the tenant for one invocation, and with several `Pulumi.<stack>.yaml` files present pass `--stack` rather than relying on the base-stack pick. | `set`: `--key <id>` (multi-instance), `--all`, `--json <str>`, `-t/--tenant`, `--stack`, `--region`; `reindex`: `--dry-run`, `-t/--tenant`, `--stack`, `--region`; `migrate-config`: `--force`, `--dry-run`, `-t/--tenant`, `--stack`, `--region` |
108
108
  | `mesh artifacts` | `get <ref>` | Download workflow artifact files from an AI-agent conversation. Ref `<conversationId>:<artifactId>` (artifactId defaults to `workflow-artifact`). | `-o/--output <dir>`, `--target <name>`, `--api-url <url>`, `--context <ctx>` (default `mesh.dev`) |
109
109
  | `mesh conversations` (alias `conv`) | `list`, `show <id>`, `artifacts <id>` | Read a deployed Mesh agent's conversations: list the caller's conversations, render a transcript, or list a conversation's artifacts. Resolves against the same `agent-targets` registry the MCP `agent_*` tools use. | all three: `--target <name>`, `--api-url <url>` (overrides `--target`), `--context <ctx>` (used with `--api-url`, default `mesh.dev`), `--json` (default when stdout isn't a TTY) |
110
110
  | `mesh vpn` | `connect <context>`, `disconnect`, `status`, `tunnel <up\|down\|status>`, `api-key`, `pre-auth-key`, `users` | Headscale VPN mgmt via Tailscale + kubectl. **`connect` now defaults to the userspace `tailscaled`** (brew formula, NOT the GUI Tailscale.app) — the same daemon `mesh dev --transport tailscale` / `mesh vpn tunnel` use, so it **runs headless / under the sandbox** and exposes a local SOCKS5 proxy (route a tool via `ALL_PROXY=socks5://127.0.0.1:<port>`). It relays a one-time browser-register URL for Zitadel auth. `--system` opts into the whole-machine GUI Tailscale.app (system TUN) — needs the app installed and **cannot run headless/sandboxed** (Sparkle.framework abort). For auto-forwarded VPC services (Temporal/RDS) prefer **`mesh vpn tunnel up`** or `mesh dev`. `tunnel status` reports runner **ownership** (`Runner: owned (verified)` vs `NOT OWNED (<reason>)` — a port answering a dial is not proof it's mesh's); `--json` adds `owned` (bool) + `ownership` (reason string). Admin subcommands `exec` into the `headscale-0` pod. | root: `-t/--tenant` (default `mesh`), `-e/--env` (default `dev`), `-n/--namespace`; `connect`/`disconnect`: `--system`; `tunnel up`: `--context <ctx>`; `tunnel down`: `--stop` (stop daemon, keep login), `--logout`; `tunnel status`: `--json`; `pre-auth-key`: `-u/--user` (**required**), `--expiration <24h>`, `--reusable`, `--ephemeral` |
@@ -121,11 +121,44 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
121
121
  ```bash
122
122
  mesh registry login # ONE command: signs in (browser) + registry token via the Zitadel broker + project .npmrc scope — no AWS, no platform context
123
123
  mesh registry login --device # same, device-code sign-in (browser callback times out over SSH/tailscale)
124
- mesh init # guided: tenant, local-only vs deployed, registry access, repo bootstrap/doctor, skills → mesh.json
124
+ mesh init # guided: tenant, WHERE IT RUNS (local / a deployed platform / nothing yet — set one up), registry access, repo bootstrap/doctor, skills → mesh.json
125
125
  pnpm install # now resolves @mesh-tech/* from CodeArtifact
126
126
  # mesh login <tenant>.<env> is for a DEPLOYED platform only (mesh deploy / Hub / VPN) — never needed for packages
127
127
  ```
128
128
 
129
+ **Answer 3 is the one that used to be missing.** "Nothing yet — set up a new Mesh
130
+ platform in my AWS account" scaffolds the platform repo and prints its deploy
131
+ sequence; see §1c. Before it existed, a user with no platform picked "deployed",
132
+ hit `No configuration found for <tenant>.<env>`, and there was nothing in that
133
+ message about *creating* one — the dead end both the Q2 PoC and a Trabian
134
+ engineer landed on.
135
+
136
+ ### 1c. Set up a platform from nothing (a fresh AWS account)
137
+
138
+ `mesh init platform <tenant>` is the scaffolder; bare `mesh init` reaches the
139
+ same code from its third answer. On a TTY it **asks** for the values that differ
140
+ per install, each pre-filled with its default — Enter accepts. Do not interrogate
141
+ the user in free text: run the command and let it ask.
142
+
143
+ ```bash
144
+ mesh init platform acme --domain a-domain-you-control.com # the only value with NO default
145
+ # prompts, all Enter-able: Environment (dev) · AWS region (us-east-2) · AWS profile (default)
146
+ # Pulumi state bucket ({tenant}-mesh-platform-pulumi-state)
147
+ # "Create s3://… now (versioned)?" (yes)
148
+ ```
149
+
150
+ - **`--domain` is required and has no default.** The core stack creates the
151
+ public zone `{env}.{domain}`; a placeholder scaffolds a zone that can never
152
+ validate, and the failure surfaces late as a stalled ACM certificate. A
153
+ non-TTY run without `--domain` stops rather than inventing one.
154
+ - **The NS delegation is a manual step in the middle of the core deploy** —
155
+ the new zone's NS records must be delegated from the parent, or the deploy
156
+ waits on the certificate indefinitely. The printed Next block names it.
157
+ - **The platform layer needs Docker** (it builds service images); the core
158
+ layer does not. That is why a Docker-less container can deploy core and stop.
159
+ - Every answer is a flag, so the agent path is one line:
160
+ `mesh init --tenant acme --new-platform dev --domain … --region … --state-bucket … --create-state-bucket --yes`
161
+
129
162
  ### 1b. Run the platform fully locally (no cloud)
130
163
 
131
164
  ```bash
@@ -314,6 +347,16 @@ mesh secrets reindex external/symitar # rebuild {prefix}/.in
314
347
  mesh secrets exec -- node ./script.js # secrets injected as env
315
348
  ```
316
349
 
350
+ `set`/`reindex`/`migrate-config` resolve tenant + env from the cwd stack file's
351
+ `mesh:tenant`, the app's shared `config.ts`, or `MESH_TENANT`; there is no
352
+ default tenant, so run them from the app or integrations directory that
353
+ declares it — from anywhere else they refuse instead of silently targeting
354
+ `mesh`, the live hub. `-t/--tenant <tenant>` is the per-invocation override
355
+ when you must run from elsewhere; it redirects the paths but not the AWS
356
+ credentials, and says so. The env half is the config's
357
+ `platform.envMap[stage] ?? defaultEnv`, so a stage mapped to another
358
+ environment writes where the app actually reads.
359
+
317
360
  ### 7. Read a remote agent's conversation context
318
361
 
319
362
  ```bash
@@ -360,7 +403,8 @@ re-reads stack/SSM output; if the *topology* changed, run `mesh deploy up` first
360
403
 
361
404
  | Need | Source |
362
405
  |---|---|
363
- | Tenant / env / stage | flags (`-t/-e`, `--stage`) → `MESH_TENANT`/`MESH_STAGE`/`SST_STAGE` env → Pulumi config in cwd/parents → defaults (`mesh`/`dev`) |
406
+ | Tenant / env / stage | flags (`-t` for tenant, `-e` for env, `--stack` for stage) → `MESH_TENANT`/`MESH_STAGE`/`SST_STAGE` env → `mesh:tenant` in the cwd stack file → the app's shared `config.ts` (beside an `sst.config.ts` found upward, else one level above the Pulumi project; it **fills in** what the stack file left unset and never overrides it) → a `tenants/<name>/` path segment → `mesh.json` → stage defaults to `dev`. Every file source is bounded to the checkout, and `mesh:tenant` under `Pulumi.yaml`'s own project-level `config:` block is **not** a source. `--stack` names the one stack file that may supply the tenant — a name that matches no file reads none, never another stack's, and refuses outright only when the config does not map it as a stage either (a stack file is written only when a stack carries per-stack config, so a mapped stage need not have one); outside a Pulumi project it is just the stage selector; a bare stage hint may select a stack file that exists but never requires one. With several stack files and no `--stack` the pick is the base stack (shortest stage), warning when that is genuinely arbitrary. **The tenant has no default**: when nothing declares one, `secrets`, `tunnel`, and the `db` commands that read the platform axis (`psql`, `connect`, and `exec` when it must start a tunnel) refuse with the remediation rather than assume `mesh` (the live hub). `db credentials`/`db env` with both `--app-tenant` and `--app-stage`, and `db exec` with both over a listening tunnel, never read it and run from anywhere |
407
+ | Platform env (the `{env}` in SSM/Secrets paths) | the shared `config.ts`'s `platform.envMap[stage] ?? platform.defaultEnv` — the same expression the config's own `getPlatform()` evaluates, so the CLI addresses the env the stack was deployed against (legacy top-level `platformEnvMap`/`defaultPlatformEnv` still read). The full stage is tried before the tenant-stripped one. A stage the map does not list falls to `defaultEnv` and says so — personal `dev-<user>` stacks rely on that. With no config — or when `--tenant`/`MESH_TENANT` has redirected the paths to another tenant, whose mapping this config does not describe — the stage minus its tenant prefix |
364
408
  | AWS creds | ambient env → cached Zitadel JWT (`mesh login`) → AWS SSO profile; region from `AWS_REGION`/`AWS_DEFAULT_REGION` else `us-east-2` |
365
409
  | Login config | `~/.config/mesh/config.json`; creds cache `~/.config/mesh/credentials.json` |
366
410
  | Deploy role | `mesh:deployerRole` (and optional `mesh:adminDeployerRole`) in `Pulumi.<stack>.yaml` |
@@ -404,6 +448,29 @@ const bastion = await getPlatformBastionInfo(ctx.tenant, ctx.platformEnv);
404
448
  Exports include `detectContext`, `getPlatformBastionInfo` (bastion), credentials,
405
449
  pulumi, vpn, and log helpers.
406
450
 
451
+ `detectContext(stageArg?, { tenant?, stack? })` fails closed: when no source
452
+ declares a tenant it throws a `MeshCliError` naming what it looked at, instead
453
+ of silently defaulting to the hub tenant `"mesh"`. The shared `config.ts` must
454
+ export the documented shape — `export const config = { tenant: … }` or
455
+ `export default { … }`; a renamed or re-exported const is not scanned, and a
456
+ `tenant:` in a comment, in a nested ref, or in a second export cannot supply
457
+ one. `mesh tunnel` and the `secrets` commands forward their `--tenant` through
458
+ `options.tenant`; `db` subcommands take `--tenant` and `--env` together,
459
+ bypassing detection. Embedders should pass `options.tenant` or catch the error.
460
+
461
+ The two options are not interchangeable. `options.stack` is a real stack
462
+ selection — it names the one `Pulumi.<stack>.yaml` allowed to supply
463
+ `mesh:tenant`, and no match means none is read. `stageArg` is only a hint:
464
+ `mesh tunnel -e <env>` passes a platform env, which names no stack file, so
465
+ tunnel passes no hint at all and lets the stack file supply the stage.
466
+
467
+ The returned `platformEnv` is `envMap[stage] ?? defaultEnv` from the config's
468
+ `platform` block — the same expression the config's own `getPlatform()`
469
+ evaluates, so the CLI addresses the environment the stack was deployed
470
+ against. `detectedTenant` carries what detection produced before any override,
471
+ and a `--tenant`/`MESH_TENANT` that disagrees with it warns: paths follow the
472
+ override, implicit AWS credentials do not.
473
+
407
474
  ## See also
408
475
 
409
476
  - `@mesh-tech/app-kit#core` — app-kit's `mesh` bin is a shim onto this CLI, which is an **optional peer**: declare `@mesh-tech/mesh-cli` in your repo root's devDependencies. Consumers then invoke it as `pnpm mesh <cmd>`
@@ -98,10 +98,11 @@ const api = new mesh.apps.Service("api", {
98
98
  ZITADEL_PROJECT_ID: identity.projectId,
99
99
  SPICEDB_HTTP_ENDPOINT: spicedb.httpEndpoint,
100
100
  },
101
- ingress: env.buildIngress({
102
- subdomain: "{{name}}-api",
103
- healthCheckPath: "/health",
104
- }),
101
+ ingress: env.buildIngress({ subdomain: "{{name}}-api" }),
102
+ // Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
103
+ // `ingress.healthCheckPath` sets the ALB target group's path only, and a
104
+ // gateway (HTTPRoute) tenant has no target group — so the path belongs here.
105
+ healthCheck: "/health",
105
106
  replicas: 1,
106
107
  resources: { cpu: "100m", memory: "128Mi" },
107
108
  });
@@ -81,10 +81,11 @@ const api = new mesh.apps.Service("api", {
81
81
  ZITADEL_ISSUER: pulumi.output(env.zitadel).apply((z) => z?.issuer ?? ""),
82
82
  ZITADEL_PROJECT_ID: identity.projectId,
83
83
  },
84
- ingress: env.buildIngress({
85
- subdomain: "{{name}}-api",
86
- healthCheckPath: "/health",
87
- }),
84
+ ingress: env.buildIngress({ subdomain: "{{name}}-api" }),
85
+ // Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
86
+ // `ingress.healthCheckPath` sets the ALB target group's path only, and a
87
+ // gateway (HTTPRoute) tenant has no target group — so the path belongs here.
88
+ healthCheck: "/health",
88
89
  replicas: 1,
89
90
  resources: { cpu: "100m", memory: "128Mi" },
90
91
  });
@@ -67,10 +67,11 @@ const api = new mesh.apps.Service("api", {
67
67
  // credential secret; the api calls resolveCredentials("vendorpay") at
68
68
  // runtime.
69
69
  link: [vendorpay.link()],
70
- ingress: env.buildIngress({
71
- subdomain: "{{name}}-api",
72
- healthCheckPath: "/health",
73
- }),
70
+ ingress: env.buildIngress({ subdomain: "{{name}}-api" }),
71
+ // Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
72
+ // `ingress.healthCheckPath` sets the ALB target group's path only, and a
73
+ // gateway (HTTPRoute) tenant has no target group — so the path belongs here.
74
+ healthCheck: "/health",
74
75
  replicas: 1,
75
76
  resources: { cpu: "100m", memory: "128Mi" },
76
77
  });
@@ -11,14 +11,28 @@ apps and the Hub discover the platform from there — no hardcoded wiring.
11
11
 
12
12
  ## Deploy
13
13
 
14
+ In this order — `mesh init` printed the same sequence when it scaffolded this
15
+ repo:
16
+
14
17
  ```bash
15
- mesh login {{tenant}}.{{env}} # or your platform context; registry auth runs automatically
16
18
  pnpm install
17
19
 
18
- cd core && mesh deploy up # ~20 min first run (VPC + EKS)
19
- cd ../platform && mesh deploy up # Temporal + Zitadel + Hub + tenant envs
20
+ aws sso login --profile {{profile}} # if this profile is SSO
21
+ eval "$(aws configure export-credentials --profile {{profile}} --format env)"
22
+ # mesh deploy reads AWS_ACCESS_KEY_ID; a platform stack carries no mesh:deployerRole
23
+
24
+ cd core && mesh deploy up # ~20 min first run: VPC, EKS, RDS and the public zone {{env}}.{{domain}}
25
+ # → then delegate the NS records of {{env}}.{{domain}} from {{domain}};
26
+ # the deploy waits on the ACM certificate until you do
27
+ cd ../platform && mesh deploy up # Temporal, Zitadel, the Hub (needs Docker — it builds service images)
28
+
29
+ mesh login {{tenant}}.{{env}} # your platform sign-in, once it is up
20
30
  ```
21
31
 
32
+ `mesh login {{tenant}}.{{env}}` comes **last**: the Zitadel it signs in to is
33
+ what the platform layer above creates. Package access is a separate, global
34
+ sign-in (`mesh registry login`) and is not needed here at all.
35
+
22
36
  The stack configs (`Pulumi.{{tenant}}-{{env}}.yaml`) are annotated — every
23
37
  platform capability is a config block; uncomment to enable.
24
38
 
@@ -28,6 +28,13 @@ config:
28
28
  certManager: true
29
29
  prometheusStack:
30
30
  enableGrafana: true
31
+ # Alertmanager is the NOTIFICATION plane. Turning it on is not enough
32
+ # to notify anybody: without an `alerting` config it runs the chart's
33
+ # stock routing, whose only receiver is null — alerts evaluate, fire,
34
+ # and are discarded. `alerting` carries Slack webhooks and SMTP
35
+ # credentials, so it is passed programmatically on MeshCore in
36
+ # index.ts, never from this file. The deploy warns while it is
37
+ # missing, and the Hub's Alerts tab shows a detection-only banner.
31
38
  enableAlertManager: false
32
39
  retention: "15d"
33
40
  storageSize: "20Gi"
@@ -43,10 +43,11 @@ const api = new mesh.apps.Service("api", {
43
43
  taskQueue: TASK_QUEUE,
44
44
  }),
45
45
  ],
46
- ingress: env.buildIngress({
47
- subdomain: "{{name}}-api",
48
- healthCheckPath: "/health",
49
- }),
46
+ ingress: env.buildIngress({ subdomain: "{{name}}-api" }),
47
+ // Drives readiness, liveness and the Hub uptime sampler on BOTH ingress kinds.
48
+ // `ingress.healthCheckPath` sets the ALB target group's path only, and a
49
+ // gateway (HTTPRoute) tenant has no target group — so the path belongs here.
50
+ healthCheck: "/health",
50
51
  replicas: 1,
51
52
  resources: { cpu: "100m", memory: "128Mi" },
52
53
  });