@mesh-tech/mesh-cli 0.31.0 → 0.32.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/dist/bin/mesh.js +75 -27
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/hub/index.d.ts +6 -1
- package/dist/src/commands/hub/index.js +71 -26
- package/fragments/base/README.md.hbs +15 -0
- package/package.json +2 -2
- package/skills/core/SKILL.md +3 -2
- package/templates/apps-repo/README.md.hbs +8 -0
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type SessionState } from "../dev.js";
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const HUB_SESSION_PREFIX = "mesh-hub-dev";
|
|
4
|
+
export declare function hubSessionName(apiPort: number): string;
|
|
5
|
+
export declare function isHubSession(name: string): boolean;
|
|
6
|
+
export declare function listHubSessions(): string[];
|
|
7
|
+
export declare function killTargets(live: string[], named: string | null): string[];
|
|
8
|
+
export declare function killInstructions(targets: string[]): string[];
|
|
4
9
|
export declare const DEFAULT_HUB_UI_PORT: number;
|
|
5
10
|
export declare const DEFAULT_HUB_API_PORT = 3002;
|
|
6
11
|
export interface DiscoveredSession {
|
|
@@ -10,7 +10,37 @@ import { MeshCliError } from "../../utils/errors.js";
|
|
|
10
10
|
import { DEFAULT_HUB_PORT } from "../local/stack.js";
|
|
11
11
|
import { logInfo, logSuccess, logWarn } from "../../utils/log.js";
|
|
12
12
|
import { findAppRoot } from "../../utils/pulumi.js";
|
|
13
|
-
export const
|
|
13
|
+
export const HUB_SESSION_PREFIX = "mesh-hub-dev";
|
|
14
|
+
export function hubSessionName(apiPort) {
|
|
15
|
+
return `${HUB_SESSION_PREFIX}-${apiPort}`;
|
|
16
|
+
}
|
|
17
|
+
const HUB_SESSION_RE = new RegExp(`^${HUB_SESSION_PREFIX}(-\\d+)?$`);
|
|
18
|
+
export function isHubSession(name) {
|
|
19
|
+
return HUB_SESSION_RE.test(name);
|
|
20
|
+
}
|
|
21
|
+
export function listHubSessions() {
|
|
22
|
+
try {
|
|
23
|
+
return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
26
|
+
})
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.filter(isHubSession);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function killTargets(live, named) {
|
|
36
|
+
return named ? live.filter((name) => name === named) : live;
|
|
37
|
+
}
|
|
38
|
+
export function killInstructions(targets) {
|
|
39
|
+
return targets.map((name) => {
|
|
40
|
+
const port = name.slice(HUB_SESSION_PREFIX.length + 1);
|
|
41
|
+
return port ? ` ${name}: mesh hub dev --kill --api-port ${port}` : ` ${name}: tmux kill-session -t ${name}`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
14
44
|
export const DEFAULT_HUB_UI_PORT = Number(DEFAULT_HUB_PORT);
|
|
15
45
|
export const DEFAULT_HUB_API_PORT = 3002;
|
|
16
46
|
export function devSessionsDir() {
|
|
@@ -162,8 +192,12 @@ export function assembleHubEnv(session, sessionEnv, opts) {
|
|
|
162
192
|
PORT: String(opts.uiPort),
|
|
163
193
|
API_URL: `http://localhost:${opts.apiPort}`,
|
|
164
194
|
HUB_TENANT: hubTenant,
|
|
195
|
+
HUB_SCOPE_ENV: scopeEnv,
|
|
165
196
|
DEFAULT_ORG: scopeTenants[0],
|
|
166
197
|
DEFAULT_ENV: scopeEnv,
|
|
198
|
+
...(process.env.HUB_PEER_ENVIRONMENTS
|
|
199
|
+
? { HUB_PEER_ENVIRONMENTS: process.env.HUB_PEER_ENVIRONMENTS }
|
|
200
|
+
: {}),
|
|
167
201
|
...audienceEnv,
|
|
168
202
|
};
|
|
169
203
|
return { apiEnv, uiEnv, scope: { hubTenant, scopeEnv, scopeTenants }, temporalAddress, warnings, notes };
|
|
@@ -244,8 +278,8 @@ function isPortListening(port) {
|
|
|
244
278
|
socket.once("error", () => done(false));
|
|
245
279
|
});
|
|
246
280
|
}
|
|
247
|
-
function hubEnvDir() {
|
|
248
|
-
return path.join(os.tmpdir(), "mesh-dev-sessions",
|
|
281
|
+
function hubEnvDir(sessionName) {
|
|
282
|
+
return path.join(os.tmpdir(), "mesh-dev-sessions", sessionName);
|
|
249
283
|
}
|
|
250
284
|
export function parsePort(raw, fallback, source) {
|
|
251
285
|
if (raw === undefined)
|
|
@@ -269,20 +303,30 @@ export function redactEnv(env) {
|
|
|
269
303
|
}
|
|
270
304
|
async function hubDevAction(opts) {
|
|
271
305
|
if (opts.kill) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
306
|
+
const namedPort = opts.apiPort || process.env.MESH_HUB_DEV_API_PORT;
|
|
307
|
+
const named = namedPort
|
|
308
|
+
? hubSessionName(parsePort(namedPort, DEFAULT_HUB_API_PORT, opts.apiPort ? "--api-port" : "MESH_HUB_DEV_API_PORT"))
|
|
309
|
+
: null;
|
|
310
|
+
const targets = killTargets(listHubSessions(), named);
|
|
311
|
+
if (targets.length === 0) {
|
|
312
|
+
logInfo(named ? `No '${named}' tmux session running.` : "No `mesh hub dev` session running.");
|
|
313
|
+
return;
|
|
276
314
|
}
|
|
277
|
-
|
|
278
|
-
|
|
315
|
+
if (targets.length > 1) {
|
|
316
|
+
throw new MeshCliError(`${targets.length} Hub sessions are running — name the one to stop.\n${killInstructions(targets).join("\n")}`, { remediation: { command: "mesh hub dev --kill --api-port <port> # the API port that Hub was started on" } });
|
|
279
317
|
}
|
|
318
|
+
const target = targets[0];
|
|
319
|
+
execFileSync("tmux", ["kill-session", "-t", target], { stdio: "ignore" });
|
|
320
|
+
fs.rmSync(hubEnvDir(target), { recursive: true, force: true });
|
|
321
|
+
logSuccess(`Killed Hub session '${target}'.`);
|
|
280
322
|
return;
|
|
281
323
|
}
|
|
282
324
|
const uiPort = opts.port
|
|
283
325
|
? parsePort(opts.port, DEFAULT_HUB_UI_PORT, "--port")
|
|
284
326
|
: parsePort(process.env.MESH_HUB_DEV_PORT, DEFAULT_HUB_UI_PORT, "MESH_HUB_DEV_PORT");
|
|
285
|
-
const apiPort =
|
|
327
|
+
const apiPort = opts.apiPort
|
|
328
|
+
? parsePort(opts.apiPort, DEFAULT_HUB_API_PORT, "--api-port")
|
|
329
|
+
: parsePort(process.env.MESH_HUB_DEV_API_PORT, DEFAULT_HUB_API_PORT, "MESH_HUB_DEV_API_PORT");
|
|
286
330
|
const platformDir = resolvePlatformDir(opts.platformDir, process.cwd());
|
|
287
331
|
const cwdAppRoot = findAppRoot(process.cwd());
|
|
288
332
|
const session = pickDevSession(listDevSessions(), { session: opts.session, cwdAppRoot });
|
|
@@ -305,9 +349,10 @@ async function hubDevAction(opts) {
|
|
|
305
349
|
console.log(` ${k}=${v}`);
|
|
306
350
|
return;
|
|
307
351
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
const hubSession = hubSessionName(apiPort);
|
|
353
|
+
if (tmuxSessionExists(hubSession)) {
|
|
354
|
+
throw new MeshCliError(`A '${hubSession}' tmux session is already running.`, {
|
|
355
|
+
remediation: { command: `mesh hub dev --kill --api-port ${apiPort} # then re-run` },
|
|
311
356
|
});
|
|
312
357
|
}
|
|
313
358
|
for (const [label, port, flag] of [
|
|
@@ -329,20 +374,20 @@ async function hubDevAction(opts) {
|
|
|
329
374
|
logWarn(warning);
|
|
330
375
|
const apiDir = path.join(platformDir, "apps", "hub", "api");
|
|
331
376
|
const uiDir = path.join(platformDir, "apps", "hub", "ui");
|
|
332
|
-
execFileSync("tmux", ["new-session", "-d", "-s",
|
|
377
|
+
execFileSync("tmux", ["new-session", "-d", "-s", hubSession, "-n", "api", "-c", apiDir]);
|
|
333
378
|
if (!assembled.apiEnv.DEV_USER_TOKEN_URL && assembled.apiEnv.DEV_USER_ID_TOKEN) {
|
|
334
379
|
const platform = session.state.devOutput.platform;
|
|
335
380
|
const credContext = `mesh.${platform.env}`;
|
|
336
381
|
const tokenPort = await findFreePort();
|
|
337
382
|
const tokenUrl = `http://127.0.0.1:${tokenPort}`;
|
|
338
|
-
execFileSync("tmux", ["new-window", "-t",
|
|
339
|
-
execFileSync("tmux", ["set-option", "-t", `${
|
|
383
|
+
execFileSync("tmux", ["new-window", "-t", hubSession, "-n", "token-server", "-c", platformDir]);
|
|
384
|
+
execFileSync("tmux", ["set-option", "-t", `${hubSession}:token-server`, "remain-on-exit", "on"], {
|
|
340
385
|
stdio: "ignore",
|
|
341
386
|
});
|
|
342
387
|
execFileSync("tmux", [
|
|
343
388
|
"send-keys",
|
|
344
389
|
"-t",
|
|
345
|
-
`${
|
|
390
|
+
`${hubSession}:token-server`,
|
|
346
391
|
`npx mesh dev __token-server ${tokenPort} ${credContext}`,
|
|
347
392
|
"Enter",
|
|
348
393
|
]);
|
|
@@ -354,15 +399,15 @@ async function hubDevAction(opts) {
|
|
|
354
399
|
logSuccess(`Dev-user token-server: ${tokenUrl} (context ${credContext})`);
|
|
355
400
|
}
|
|
356
401
|
const launch = (window, dir, env, cmd, createWindow) => {
|
|
357
|
-
const envFile = path.join(hubEnvDir(), `${window}.env.sh`);
|
|
402
|
+
const envFile = path.join(hubEnvDir(hubSession), `${window}.env.sh`);
|
|
358
403
|
writeEnvFile(envFile, env);
|
|
359
404
|
if (createWindow) {
|
|
360
|
-
execFileSync("tmux", ["new-window", "-t",
|
|
405
|
+
execFileSync("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
|
|
361
406
|
}
|
|
362
|
-
execFileSync("tmux", ["set-option", "-t", `${
|
|
407
|
+
execFileSync("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
|
|
363
408
|
stdio: "ignore",
|
|
364
409
|
});
|
|
365
|
-
execFileSync("tmux", ["send-keys", "-t", `${
|
|
410
|
+
execFileSync("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
|
|
366
411
|
};
|
|
367
412
|
launch("api", apiDir, assembled.apiEnv, "pnpm dev", false);
|
|
368
413
|
launch("ui", uiDir, assembled.uiEnv, `pnpm dev --port ${uiPort} --strictPort`, true);
|
|
@@ -370,14 +415,14 @@ async function hubDevAction(opts) {
|
|
|
370
415
|
const apiUp = await waitForPort("127.0.0.1", apiPort, 90_000);
|
|
371
416
|
const uiUp = apiUp && (await waitForPort("127.0.0.1", uiPort, 90_000));
|
|
372
417
|
if (!apiUp || !uiUp) {
|
|
373
|
-
throw new MeshCliError(`Hub ${apiUp ? "UI" : "API"} did not start listening (api :${apiPort}, ui :${uiPort}).`, { remediation: { command: `tmux attach -t ${
|
|
418
|
+
throw new MeshCliError(`Hub ${apiUp ? "UI" : "API"} did not start listening (api :${apiPort}, ui :${uiPort}).`, { remediation: { command: `tmux attach -t ${hubSession} # inspect the ${apiUp ? "ui" : "api"} window` } });
|
|
374
419
|
}
|
|
375
420
|
console.log("");
|
|
376
421
|
logSuccess(`Mesh Hub (dev-local scope ${assembled.scope.scopeTenants.join(",")} @ ${assembled.scope.scopeEnv})`);
|
|
377
422
|
console.log(` ${chalk.bold.cyan(`http://localhost:${uiPort}`)} (also reachable on your tailnet)`);
|
|
378
423
|
console.log(` api http://localhost:${apiPort}`);
|
|
379
|
-
console.log(` logs tmux attach -t ${
|
|
380
|
-
console.log(` stop mesh hub dev --kill`);
|
|
424
|
+
console.log(` logs tmux attach -t ${hubSession}`);
|
|
425
|
+
console.log(` stop mesh hub dev --kill --api-port ${apiPort}`);
|
|
381
426
|
}
|
|
382
427
|
export function registerHubCommands(program) {
|
|
383
428
|
const hub = program.command("hub").description("Local Hub over dev-local stacks");
|
|
@@ -388,9 +433,9 @@ export function registerHubCommands(program) {
|
|
|
388
433
|
.option("--tenants <list>", "comma-separated HUB_SCOPE_TENANTS override (default: the session's tenant)")
|
|
389
434
|
.option("--audience <who>", "who the Hub renders for: `operator` (platform staff — the default here) or `tenant` (a client's operators). Anything else reads as `tenant`")
|
|
390
435
|
.option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`)
|
|
391
|
-
.option("--api-port <port>", `Hub API port (default: ${DEFAULT_HUB_API_PORT})`)
|
|
436
|
+
.option("--api-port <port>", `Hub API port, and the key its tmux session is named after — several Hubs coexist on different ones (default: $MESH_HUB_DEV_API_PORT or ${DEFAULT_HUB_API_PORT})`)
|
|
392
437
|
.option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)")
|
|
393
438
|
.option("--print-env", "print the assembled env (secrets redacted) and exit without launching")
|
|
394
|
-
.option("--kill", "tear down
|
|
439
|
+
.option("--kill", "tear down a running Hub session; with several up, name it with --api-port (or $MESH_HUB_DEV_API_PORT)")
|
|
395
440
|
.action(hubDevAction);
|
|
396
441
|
}
|
|
@@ -7,3 +7,18 @@ locally with `mesh dev`, deploy it with `mesh deploy up`.
|
|
|
7
7
|
The API's reference site is built by the platform from `api/openapi.json` and
|
|
8
8
|
the guides in `docs/pages/` — this README is its landing page.
|
|
9
9
|
{{/if}}
|
|
10
|
+
|
|
11
|
+
## The version this app reports
|
|
12
|
+
|
|
13
|
+
`"version": "0.0.1"` in `package.json` is a STARTING POINT, not the number the
|
|
14
|
+
Hub shows. Every deploy derives the app's version from the conventional commits
|
|
15
|
+
that have landed on this directory since that version was last committed: a
|
|
16
|
+
`fix:` moves the patch, a `feat:` moves the minor, a `feat!:`/`BREAKING CHANGE:`
|
|
17
|
+
moves the major. Redeploying unchanged code stamps the same number, so a
|
|
18
|
+
redeploy is not recorded as a new deploy.
|
|
19
|
+
|
|
20
|
+
Nothing to run and nothing to remember — the version simply grows with the app,
|
|
21
|
+
and the Hub's Activity tab shows each deploy with the version it shipped. To
|
|
22
|
+
take the number over yourself, commit the version you want into `package.json`
|
|
23
|
+
(the derivation restarts from it) or pass `versioning: "package"` to
|
|
24
|
+
`AppEnvironment`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "CLI for Mesh platform development utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@mesh-tech/agent-contracts": "0.2.0",
|
|
45
|
-
"@mesh-tech/tsconfig": "2.
|
|
45
|
+
"@mesh-tech/tsconfig": "2.26.0",
|
|
46
46
|
"@mesh-tech/workflow-viz": "0.3.0",
|
|
47
47
|
"@tanstack/intent": "^0.3.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|
package/skills/core/SKILL.md
CHANGED
|
@@ -95,7 +95,7 @@ Top-level commands (each `register*` is wired in `src/program.ts`, which `bin/me
|
|
|
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. `--with-idps` adds two mock identity providers (SAML + OIDC) and federates the local Zitadel with BOTH, which is how enterprise SSO gets exercised without a cloud tenant — the two-provider shape is deliberate, since the instance login policy carries a LIST of IdPs and the failure worth catching is a second federation evicting the first (`docs/guides/local-federation.md`). Its two host ports (4569 SAML, 5556 OIDC — 5556 is Dex's own default) join the start-up port preflight only when the flag is passed, and `mesh status` lists both mocks once they are running. 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`, `--with-idps`, `--takeover`, `--skip-port-check`; `stop`: `--destroy` (drops volumes/seeds), `--force`; `status`: `--json` |
|
|
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
|
|
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-<api-port>`, so several local Hubs coexist once each is given an API port of its own (both ports default the same in every worktree). | `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` (or `MESH_HUB_DEV_API_PORT`, default 3002; also names the tmux session), `--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 published through the Hub docs door rather than on a hostname of its own, with its readers stated and grantable (`DOCS_SITE_PUBLIC` — advisory: the public hostname is the deprecated compatibility path and keeps deploying, so the finding names the door instead — `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`). Gate 0.6 additionally resolves the Pulumi program's imports against the installed tree (`UNRESOLVABLE_IMPORT`) — an optional peer nothing declares installs silently and `skipLibCheck` hides it, so the break otherwise waits for `mesh deploy`; with no `node_modules` the gate reports itself not checked rather than met. 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
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` |
|
|
@@ -245,7 +245,8 @@ mesh hub dev # → local current-code Hub at http://localhost:90
|
|
|
245
245
|
# scoped to this session's tenant: workflow types with
|
|
246
246
|
# live run counts, execution timelines incl. child slices
|
|
247
247
|
mesh hub dev --print-env # inspect the assembled env without launching
|
|
248
|
-
mesh hub dev --
|
|
248
|
+
mesh hub dev --api-port 3102 # move the API off 3002 AND name the session (or export MESH_HUB_DEV_API_PORT)
|
|
249
|
+
mesh hub dev --kill # tear down (tmux session mesh-hub-dev-<api-port>)
|
|
249
250
|
```
|
|
250
251
|
|
|
251
252
|
Zero hand-set env vars: the command reads the `mesh dev` session's state file
|
|
@@ -22,3 +22,11 @@ pnpm install
|
|
|
22
22
|
mesh dev # run locally
|
|
23
23
|
mesh deploy up # deploy via the stack's deployer role
|
|
24
24
|
```
|
|
25
|
+
|
|
26
|
+
## App versions
|
|
27
|
+
|
|
28
|
+
An app's deployed version is derived from its own change set — the conventional
|
|
29
|
+
commits touching `apps/<app>/` since the version in its `package.json` was last
|
|
30
|
+
committed — so it grows on its own and the Hub can show which version each
|
|
31
|
+
deploy shipped. Conventional-commit subjects (`feat:`, `fix:`, `feat!:`) are
|
|
32
|
+
what make the bump meaningful; a subject with no type counts as a patch.
|