@mesh-tech/mesh-cli 0.31.1 → 0.33.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 CHANGED
@@ -6912,7 +6912,7 @@ var init_src = __esm({
6912
6912
  z.object({ op: z.string().trim().min(1) }),
6913
6913
  z.object({ unavailable: z.string().trim().min(1) })
6914
6914
  ]);
6915
- apiSurfaceKindSchema = z.enum(["app", "vendor"]);
6915
+ apiSurfaceKindSchema = z.enum(["app", "vendor", "events"]);
6916
6916
  apiDocsSiteAudienceSchema = z.discriminatedUnion("readers", [
6917
6917
  z.object({ readers: z.literal("partner"), roles: z.array(z.string().min(1)) }),
6918
6918
  z.object({ readers: z.literal("operator") })
@@ -11846,6 +11846,53 @@ function statedKind(decl, args) {
11846
11846
  if (decl.ctor === "VendorApiSurface") return "vendor";
11847
11847
  return /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
11848
11848
  }
11849
+ function programIdentities(text) {
11850
+ const identities = [];
11851
+ for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
11852
+ const options = optionsObject(d.body, text);
11853
+ if (!options) continue;
11854
+ const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find(
11855
+ (m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1
11856
+ )?.[1];
11857
+ identities.push({ binding, options: topLevelProperties(options) });
11858
+ }
11859
+ return identities;
11860
+ }
11861
+ function declaredProjectRoles(identity) {
11862
+ const rolesText = identity.options.get("roles");
11863
+ const coarseText = identity.options.get("coarseRoles");
11864
+ if (rolesText !== void 0) return stringList(rolesText);
11865
+ if (coarseText !== void 0) return stringList(coarseText);
11866
+ return [];
11867
+ }
11868
+ function eventsRoleFindings(text) {
11869
+ const findings = [];
11870
+ const identities = programIdentities(text);
11871
+ for (const decl of surfaceDeclarations(text, RE_EVENTS_NEW)) {
11872
+ const options = optionsObject(decl.body, text);
11873
+ if (!options) continue;
11874
+ const args = topLevelProperties(options);
11875
+ if (args.get("enabled") === "false") continue;
11876
+ if (identities.length === 0) {
11877
+ continue;
11878
+ }
11879
+ const identityText = objectLiteralOf(args.get("identity"), text);
11880
+ const projectExpr = identityText ? topLevelProperties(identityText).get("projectId") : void 0;
11881
+ const projectRoot = projectExpr ? /^([A-Za-z_$][\w$]*)\.projectId\b/.exec(projectExpr.trim())?.[1] : void 0;
11882
+ const identity = (projectRoot ? identities.find((id) => id.binding === projectRoot) : void 0) ?? (identities.length === 1 ? identities[0] : void 0);
11883
+ if (!identity) continue;
11884
+ const declared = declaredProjectRoles(identity);
11885
+ if (!declared) continue;
11886
+ const missing = EVENTS_PROJECT_ROLES.filter((r) => !declared.includes(r));
11887
+ if (missing.length === 0) continue;
11888
+ findings.push({
11889
+ code: "EVENTS_ROLES_UNDECLARED",
11890
+ line: decl.line,
11891
+ why: `mesh.apps.EventsService is declared but the app's ZitadelAppIdentity does not declare ${missing.map((r) => `"${r}"`).join(" or ")} \u2014 the events service authorizes bearer callers by these project roles, and a role the identity never declares cannot be granted from the Hub's Access tab, so no subscriber can be admitted`
11892
+ });
11893
+ }
11894
+ return findings;
11895
+ }
11849
11896
  function docsAudienceUngrantable(text, decl, args, docs, httpText) {
11850
11897
  const findings = [];
11851
11898
  let roles;
@@ -11856,22 +11903,14 @@ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
11856
11903
  roles = DEFAULT_PARTNER_DOCS_ROLES;
11857
11904
  }
11858
11905
  if (!roles || roles.length === 0) return findings;
11859
- const identities = [];
11860
- for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
11861
- const options = optionsObject(d.body, text);
11862
- if (!options) continue;
11863
- const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
11864
- identities.push({ binding, options: topLevelProperties(options) });
11865
- }
11906
+ const identities = programIdentities(text);
11866
11907
  if (identities.length === 0) return findings;
11867
11908
  const docsAuth = objectLiteralOf(docs.get("auth"), text);
11868
11909
  const clientExpr = (docsAuth ? topLevelProperties(docsAuth).get("clientId") : void 0) ?? (httpText ? topLevelProperties(objectLiteralOf(topLevelProperties(httpText).get("auth"), text) ?? "{}").get("clientId") : void 0);
11869
11910
  const clientRoot = clientExpr ? /^([A-Za-z_$][\w$]*)\.(?:clientIds|applications)\./.exec(clientExpr.trim())?.[1] : void 0;
11870
11911
  const identity = (clientRoot ? identities.find((id) => id.binding === clientRoot) : void 0) ?? (identities.length === 1 ? identities[0] : void 0);
11871
11912
  if (!identity) return findings;
11872
- const rolesText = identity.options.get("roles");
11873
- const coarseText = identity.options.get("coarseRoles");
11874
- const declared = rolesText !== void 0 ? stringList(rolesText) : coarseText !== void 0 ? stringList(coarseText) : [];
11913
+ const declared = declaredProjectRoles(identity);
11875
11914
  if (declared) {
11876
11915
  for (const role of roles) {
11877
11916
  if (declared.includes(role)) continue;
@@ -11942,6 +11981,7 @@ function scanApp(root, appRel, resolution = probeResolution(root, appRel)) {
11942
11981
  hit("NO_REGISTER", indexRel, "never calls env.register({\u2026}) \u2014 the Hub cannot list this app");
11943
11982
  }
11944
11983
  for (const f of docsSiteFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why);
11984
+ for (const f of eventsRoleFindings(readText(index))) hit(f.code, `${indexRel}:${f.line}`, f.why);
11945
11985
  for (const svc of serviceDirs(app)) {
11946
11986
  for (const f of srcFiles(svc)) {
11947
11987
  const text = readText(f);
@@ -12103,7 +12143,7 @@ function registerAppCommands(program2) {
12103
12143
  }
12104
12144
  });
12105
12145
  }
12106
- var APP_CHECK_RULES, APP_CHECK_CODES, SRC_EXT, SKIP_DIRS, NON_SERVICE_DIRS, RE_HTML, RE_OIDC, RE_COOKIE, RE_LOGGER, RE_EXPOSURE, RE_POINTER, RE_METADATA, RE_IAC_GRANT, RE_POINTER_OVERRIDE, RE_PASSWORD, RE_ALLOWLIST, RE_USERS_TABLE, RE_SURFACE_NEW, RE_APIDOCS_NEW, RE_IDENTITY_NEW, RE_IDENTITY_BINDING, DEFAULT_PARTNER_DOCS_ROLES, DOCS_SITE_FIELDS, DOCS_SITE_RETIRED_FIELDS, RE_PROPERTY;
12146
+ var APP_CHECK_RULES, APP_CHECK_CODES, SRC_EXT, SKIP_DIRS, NON_SERVICE_DIRS, RE_HTML, RE_OIDC, RE_COOKIE, RE_LOGGER, RE_EXPOSURE, RE_POINTER, RE_METADATA, RE_IAC_GRANT, RE_POINTER_OVERRIDE, RE_PASSWORD, RE_ALLOWLIST, RE_USERS_TABLE, RE_SURFACE_NEW, RE_APIDOCS_NEW, RE_IDENTITY_NEW, RE_EVENTS_NEW, EVENTS_PROJECT_ROLES, RE_IDENTITY_BINDING, DEFAULT_PARTNER_DOCS_ROLES, DOCS_SITE_FIELDS, DOCS_SITE_RETIRED_FIELDS, RE_PROPERTY;
12107
12147
  var init_app_check = __esm({
12108
12148
  "libs/mesh-cli/src/commands/app-check.ts"() {
12109
12149
  "use strict";
@@ -12242,6 +12282,13 @@ var init_app_check = __esm({
12242
12282
  remediation: `declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it from the app's Access tab \u2014 the audience is published on the surface's registry entry as the reader set for the Hub to admit by, and a role nobody can be granted admits nobody`,
12243
12283
  scope: "app"
12244
12284
  },
12285
+ EVENTS_ROLES_UNDECLARED: {
12286
+ gate: "H",
12287
+ title: "Events roles grantable from the Hub",
12288
+ severity: "block",
12289
+ remediation: "spread mesh.apps.EVENTS_PROJECT_ROLES into the ZitadelAppIdentity's roles (roles: [...schemaDef.coarseRoles, ...mesh.apps.EVENTS_PROJECT_ROLES]) so events:subscriber and events:admin are project roles the Hub's Access tab can grant \u2014 the primitive cannot add them, because the identity builds its roles before the events service exists and a role added afterwards is absent from the coarseRoles the Hub reads",
12290
+ scope: "app"
12291
+ },
12245
12292
  DOCS_VENDOR_AUDIENCE_PARTNER: {
12246
12293
  gate: "H",
12247
12294
  title: "Vendor reference never partner-readable",
@@ -12273,7 +12320,7 @@ var init_app_check = __esm({
12273
12320
  RE_OIDC = /openid-client|code_verifier|authorization_code|buildAuthorizeUrl|end_session/;
12274
12321
  RE_COOKIE = /setCookie\(|hono\/cookie|Set-Cookie/;
12275
12322
  RE_LOGGER = /from ["'](pino|hono\/logger|winston|bunyan)["']|require\(["'](pino|winston|bunyan)["']\)|^[^/*]*\bconsole\.(log|info|warn|error)\(/;
12276
- RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(/;
12323
+ RE_EXPOSURE = /ZitadelAppIdentity|createOAuthMiddleware|createAuthorizationMiddleware|zitadelJwtScheme|createAuthzMiddleware|bearerAuth|type: *"oidc"|^\s+auth: *\{|_ROLES *= *\{|coarseRoles|ZITADEL_(AUDIENCE|CLIENT_ID)|OIDC_CLIENT_ID|zitadel\.(ApplicationOidc|ApplicationApi|Project|UserGrant)\(|mesh\.apps\.EventsService\(/;
12277
12324
  RE_POINTER = /AppAuthzPointer/;
12278
12325
  RE_METADATA = /compileOpsHubMetadata|ops-hub-metadata|SpiceDBSchema/;
12279
12326
  RE_IAC_GRANT = /userId: *"[0-9]{6,}"/;
@@ -12284,6 +12331,8 @@ var init_app_check = __esm({
12284
12331
  RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
12285
12332
  RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
12286
12333
  RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
12334
+ RE_EVENTS_NEW = /new\s+mesh\.apps\.(EventsService)\s*\(/g;
12335
+ EVENTS_PROJECT_ROLES = ["events:subscriber", "events:admin"];
12287
12336
  RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
12288
12337
  DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
12289
12338
  DOCS_SITE_FIELDS = ["pages", "playground", "audience", "basePath", "ingress", "auth"];
@@ -13507,7 +13556,13 @@ var init_scaffold_versions = __esm({
13507
13556
  "use strict";
13508
13557
  SCAFFOLD_PACKAGE_LINES = {
13509
13558
  "@mesh-tech/app-kit": "^1.45.0",
13510
- "@mesh-tech/esm-bundle": "^0.1.1"
13559
+ "@mesh-tech/esm-bundle": "^0.1.1",
13560
+ // The events service versions with the primitive that deploys it, not with
13561
+ // the lockstep group, so it carries its own floor.
13562
+ "@mesh-tech/events-service": "^0.1.0",
13563
+ // Likewise authz: it is not in the `fixed` group in .changeset/config.json,
13564
+ // so the lockstep range resolves to a major line it has never published.
13565
+ "@mesh-tech/authz": "^1.19.0"
13511
13566
  };
13512
13567
  SCAFFOLD_TOOLCHAIN = {
13513
13568
  "@types/node": "^24.2.1",
@@ -13676,6 +13731,10 @@ async function promptForOptions(options) {
13676
13731
  primitives.push("service");
13677
13732
  logInfo("Added 'service' \u2014 bucket requires an API service for S3 helpers.");
13678
13733
  }
13734
+ if (primitives.includes("events") && !primitives.includes("service")) {
13735
+ primitives.push("service");
13736
+ logInfo("Added 'service' \u2014 the events publisher lives in the api/ directory.");
13737
+ }
13679
13738
  return { tenant, name, primitives };
13680
13739
  }
13681
13740
  function copyTemplate(srcDir, destDir, context) {
@@ -13787,6 +13846,13 @@ function generateComposableApp(appDir, context) {
13787
13846
  }
13788
13847
  copyTemplate(bucketDir, appDir, context);
13789
13848
  }
13849
+ if (context.events) {
13850
+ const eventsDir = path27.join(fragmentsDir, "events");
13851
+ if (!fs25.existsSync(eventsDir)) {
13852
+ throw new Error(`Fragment directory not found: ${eventsDir}`);
13853
+ }
13854
+ copyTemplate(eventsDir, appDir, context);
13855
+ }
13790
13856
  const agentsDir = path27.join(fragmentsDir, "agents");
13791
13857
  if (fs25.existsSync(agentsDir)) {
13792
13858
  copyTemplate(agentsDir, appDir, context);
@@ -13853,6 +13919,10 @@ function registerCreateAppCommand(program2) {
13853
13919
  primitives.push("service");
13854
13920
  logInfo("Added 'service' \u2014 bucket requires an API service for S3 helpers.");
13855
13921
  }
13922
+ if (primitives.includes("events") && !primitives.includes("service")) {
13923
+ primitives.push("service");
13924
+ logInfo("Added 'service' \u2014 the events publisher lives in the api/ directory.");
13925
+ }
13856
13926
  await runComposable(tenant, name, primitives, options.test);
13857
13927
  });
13858
13928
  }
@@ -13961,6 +14031,8 @@ async function runComposable(tenant, name, primitives, test) {
13961
14031
  const repoRoot2 = resolveTargetRoot(process.cwd());
13962
14032
  const mesh = workspaceDeps ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/app-kit", repoRoot2);
13963
14033
  const esmBundle = workspaceDeps ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/esm-bundle", repoRoot2);
14034
+ const eventsService = workspaceDeps || !primitives.includes("events") ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/events-service", repoRoot2);
14035
+ const authz = workspaceDeps || !primitives.includes("events") ? { range: "", resolved: true } : resolvePublishedRange("@mesh-tech/authz", repoRoot2);
13964
14036
  if (!workspaceDeps) {
13965
14037
  logInfo(
13966
14038
  mesh.resolved ? `@mesh-tech/* pinned to ${mesh.range} (current published line)` : `Registry lookup unavailable \u2014 pinning @mesh-tech/* to ${mesh.range}; run \`pnpm up @mesh-tech/*\` once you have registry auth`
@@ -13973,10 +14045,13 @@ async function runComposable(tenant, name, primitives, test) {
13973
14045
  database: primitives.includes("database"),
13974
14046
  temporal: primitives.includes("temporal"),
13975
14047
  bucket: primitives.includes("bucket"),
14048
+ events: primitives.includes("events"),
13976
14049
  region,
13977
14050
  workspaceDeps,
13978
14051
  meshRange: mesh.range,
13979
14052
  esmBundleRange: esmBundle.range,
14053
+ eventsServiceRange: eventsService.range,
14054
+ authzRange: authz.range,
13980
14055
  typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
13981
14056
  tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
13982
14057
  typescriptRange: SCAFFOLD_TOOLCHAIN.typescript,
@@ -14091,7 +14166,8 @@ var init_create_app = __esm({
14091
14166
  service: "HTTP Service (API with Hono)",
14092
14167
  database: "Database (PostgreSQL via Prisma)",
14093
14168
  temporal: "Temporal (workflow orchestration)",
14094
- bucket: "S3 Bucket (file storage)"
14169
+ bucket: "S3 Bucket (file storage)",
14170
+ events: "Events and webhooks (publish, subscriptions, signed delivery)"
14095
14171
  };
14096
14172
  HUB_ACCOUNTS = {
14097
14173
  mesh: "159923586610"
@@ -16931,6 +17007,31 @@ import * as net12 from "node:net";
16931
17007
  import * as os9 from "node:os";
16932
17008
  import * as path36 from "node:path";
16933
17009
  import chalk3 from "chalk";
17010
+ function hubSessionName(apiPort) {
17011
+ return `${HUB_SESSION_PREFIX}-${apiPort}`;
17012
+ }
17013
+ function isHubSession(name) {
17014
+ return HUB_SESSION_RE.test(name);
17015
+ }
17016
+ function listHubSessions() {
17017
+ try {
17018
+ return execFileSync24("tmux", ["list-sessions", "-F", "#{session_name}"], {
17019
+ encoding: "utf8",
17020
+ stdio: ["ignore", "pipe", "ignore"]
17021
+ }).split("\n").map((line) => line.trim()).filter(isHubSession);
17022
+ } catch {
17023
+ return [];
17024
+ }
17025
+ }
17026
+ function killTargets(live, named) {
17027
+ return named ? live.filter((name) => name === named) : live;
17028
+ }
17029
+ function killInstructions(targets) {
17030
+ return targets.map((name) => {
17031
+ const port = name.slice(HUB_SESSION_PREFIX.length + 1);
17032
+ return port ? ` ${name}: mesh hub dev --kill --api-port ${port}` : ` ${name}: tmux kill-session -t ${name}`;
17033
+ });
17034
+ }
16934
17035
  function devSessionsDir() {
16935
17036
  return path36.join(os9.tmpdir(), "mesh-dev-sessions");
16936
17037
  }
@@ -17158,8 +17259,8 @@ function isPortListening2(port) {
17158
17259
  socket.once("error", () => done(false));
17159
17260
  });
17160
17261
  }
17161
- function hubEnvDir() {
17162
- return path36.join(os9.tmpdir(), "mesh-dev-sessions", HUB_SESSION);
17262
+ function hubEnvDir(sessionName) {
17263
+ return path36.join(os9.tmpdir(), "mesh-dev-sessions", sessionName);
17163
17264
  }
17164
17265
  function parsePort(raw, fallback, source) {
17165
17266
  if (raw === void 0) return fallback;
@@ -17181,17 +17282,28 @@ function redactEnv(env) {
17181
17282
  }
17182
17283
  async function hubDevAction(opts) {
17183
17284
  if (opts.kill) {
17184
- if (tmuxSessionExists(HUB_SESSION)) {
17185
- execFileSync24("tmux", ["kill-session", "-t", HUB_SESSION], { stdio: "ignore" });
17186
- fs29.rmSync(hubEnvDir(), { recursive: true, force: true });
17187
- logSuccess(`Killed Hub session '${HUB_SESSION}'.`);
17188
- } else {
17189
- logInfo(`No '${HUB_SESSION}' tmux session running.`);
17285
+ const namedPort = opts.apiPort || process.env.MESH_HUB_DEV_API_PORT;
17286
+ const named = namedPort ? hubSessionName(parsePort(namedPort, DEFAULT_HUB_API_PORT, opts.apiPort ? "--api-port" : "MESH_HUB_DEV_API_PORT")) : null;
17287
+ const targets = killTargets(listHubSessions(), named);
17288
+ if (targets.length === 0) {
17289
+ logInfo(named ? `No '${named}' tmux session running.` : "No `mesh hub dev` session running.");
17290
+ return;
17190
17291
  }
17292
+ if (targets.length > 1) {
17293
+ throw new MeshCliError(
17294
+ `${targets.length} Hub sessions are running \u2014 name the one to stop.
17295
+ ${killInstructions(targets).join("\n")}`,
17296
+ { remediation: { command: "mesh hub dev --kill --api-port <port> # the API port that Hub was started on" } }
17297
+ );
17298
+ }
17299
+ const target = targets[0];
17300
+ execFileSync24("tmux", ["kill-session", "-t", target], { stdio: "ignore" });
17301
+ fs29.rmSync(hubEnvDir(target), { recursive: true, force: true });
17302
+ logSuccess(`Killed Hub session '${target}'.`);
17191
17303
  return;
17192
17304
  }
17193
17305
  const uiPort = opts.port ? parsePort(opts.port, DEFAULT_HUB_UI_PORT, "--port") : parsePort(process.env.MESH_HUB_DEV_PORT, DEFAULT_HUB_UI_PORT, "MESH_HUB_DEV_PORT");
17194
- const apiPort = parsePort(opts.apiPort, DEFAULT_HUB_API_PORT, "--api-port");
17306
+ const apiPort = opts.apiPort ? parsePort(opts.apiPort, DEFAULT_HUB_API_PORT, "--api-port") : parsePort(process.env.MESH_HUB_DEV_API_PORT, DEFAULT_HUB_API_PORT, "MESH_HUB_DEV_API_PORT");
17195
17307
  const platformDir = resolvePlatformDir(opts.platformDir, process.cwd());
17196
17308
  const cwdAppRoot = findAppRoot(process.cwd());
17197
17309
  const session = pickDevSession(listDevSessions(), { session: opts.session, cwdAppRoot });
@@ -17218,9 +17330,10 @@ async function hubDevAction(opts) {
17218
17330
  for (const [k, v] of Object.entries(redactEnv(assembled.uiEnv))) console.log(` ${k}=${v}`);
17219
17331
  return;
17220
17332
  }
17221
- if (tmuxSessionExists(HUB_SESSION)) {
17222
- throw new MeshCliError(`A '${HUB_SESSION}' tmux session is already running.`, {
17223
- remediation: { command: "mesh hub dev --kill # then re-run mesh hub dev" }
17333
+ const hubSession = hubSessionName(apiPort);
17334
+ if (tmuxSessionExists(hubSession)) {
17335
+ throw new MeshCliError(`A '${hubSession}' tmux session is already running.`, {
17336
+ remediation: { command: `mesh hub dev --kill --api-port ${apiPort} # then re-run` }
17224
17337
  });
17225
17338
  }
17226
17339
  for (const [label, port, flag] of [
@@ -17245,20 +17358,20 @@ async function hubDevAction(opts) {
17245
17358
  for (const warning of assembled.warnings) logWarn(warning);
17246
17359
  const apiDir = path36.join(platformDir, "apps", "hub", "api");
17247
17360
  const uiDir = path36.join(platformDir, "apps", "hub", "ui");
17248
- execFileSync24("tmux", ["new-session", "-d", "-s", HUB_SESSION, "-n", "api", "-c", apiDir]);
17361
+ execFileSync24("tmux", ["new-session", "-d", "-s", hubSession, "-n", "api", "-c", apiDir]);
17249
17362
  if (!assembled.apiEnv.DEV_USER_TOKEN_URL && assembled.apiEnv.DEV_USER_ID_TOKEN) {
17250
17363
  const platform = session.state.devOutput.platform;
17251
17364
  const credContext = `mesh.${platform.env}`;
17252
17365
  const tokenPort = await findFreePort4();
17253
17366
  const tokenUrl = `http://127.0.0.1:${tokenPort}`;
17254
- execFileSync24("tmux", ["new-window", "-t", HUB_SESSION, "-n", "token-server", "-c", platformDir]);
17255
- execFileSync24("tmux", ["set-option", "-t", `${HUB_SESSION}:token-server`, "remain-on-exit", "on"], {
17367
+ execFileSync24("tmux", ["new-window", "-t", hubSession, "-n", "token-server", "-c", platformDir]);
17368
+ execFileSync24("tmux", ["set-option", "-t", `${hubSession}:token-server`, "remain-on-exit", "on"], {
17256
17369
  stdio: "ignore"
17257
17370
  });
17258
17371
  execFileSync24("tmux", [
17259
17372
  "send-keys",
17260
17373
  "-t",
17261
- `${HUB_SESSION}:token-server`,
17374
+ `${hubSession}:token-server`,
17262
17375
  `npx mesh dev __token-server ${tokenPort} ${credContext}`,
17263
17376
  "Enter"
17264
17377
  ]);
@@ -17270,15 +17383,15 @@ async function hubDevAction(opts) {
17270
17383
  logSuccess(`Dev-user token-server: ${tokenUrl} (context ${credContext})`);
17271
17384
  }
17272
17385
  const launch = (window, dir, env, cmd, createWindow) => {
17273
- const envFile = path36.join(hubEnvDir(), `${window}.env.sh`);
17386
+ const envFile = path36.join(hubEnvDir(hubSession), `${window}.env.sh`);
17274
17387
  writeEnvFile(envFile, env);
17275
17388
  if (createWindow) {
17276
- execFileSync24("tmux", ["new-window", "-t", HUB_SESSION, "-n", window, "-c", dir]);
17389
+ execFileSync24("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
17277
17390
  }
17278
- execFileSync24("tmux", ["set-option", "-t", `${HUB_SESSION}:${window}`, "remain-on-exit", "on"], {
17391
+ execFileSync24("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
17279
17392
  stdio: "ignore"
17280
17393
  });
17281
- execFileSync24("tmux", ["send-keys", "-t", `${HUB_SESSION}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
17394
+ execFileSync24("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
17282
17395
  };
17283
17396
  launch("api", apiDir, assembled.apiEnv, "pnpm dev", false);
17284
17397
  launch("ui", uiDir, assembled.uiEnv, `pnpm dev --port ${uiPort} --strictPort`, true);
@@ -17288,15 +17401,15 @@ async function hubDevAction(opts) {
17288
17401
  if (!apiUp || !uiUp) {
17289
17402
  throw new MeshCliError(
17290
17403
  `Hub ${apiUp ? "UI" : "API"} did not start listening (api :${apiPort}, ui :${uiPort}).`,
17291
- { remediation: { command: `tmux attach -t ${HUB_SESSION} # inspect the ${apiUp ? "ui" : "api"} window` } }
17404
+ { remediation: { command: `tmux attach -t ${hubSession} # inspect the ${apiUp ? "ui" : "api"} window` } }
17292
17405
  );
17293
17406
  }
17294
17407
  console.log("");
17295
17408
  logSuccess(`Mesh Hub (dev-local scope ${assembled.scope.scopeTenants.join(",")} @ ${assembled.scope.scopeEnv})`);
17296
17409
  console.log(` ${chalk3.bold.cyan(`http://localhost:${uiPort}`)} (also reachable on your tailnet)`);
17297
17410
  console.log(` api http://localhost:${apiPort}`);
17298
- console.log(` logs tmux attach -t ${HUB_SESSION}`);
17299
- console.log(` stop mesh hub dev --kill`);
17411
+ console.log(` logs tmux attach -t ${hubSession}`);
17412
+ console.log(` stop mesh hub dev --kill --api-port ${apiPort}`);
17300
17413
  }
17301
17414
  function registerHubCommands(program2) {
17302
17415
  const hub = program2.command("hub").description("Local Hub over dev-local stacks");
@@ -17305,9 +17418,15 @@ function registerHubCommands(program2) {
17305
17418
  ).option("--session <name>", "dev session to observe (default: auto-detect from cwd)").option("--tenants <list>", "comma-separated HUB_SCOPE_TENANTS override (default: the session's tenant)").option(
17306
17419
  "--audience <who>",
17307
17420
  "who the Hub renders for: `operator` (platform staff \u2014 the default here) or `tenant` (a client's operators). Anything else reads as `tenant`"
17308
- ).option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`).option("--api-port <port>", `Hub API port (default: ${DEFAULT_HUB_API_PORT})`).option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)").option("--print-env", "print the assembled env (secrets redacted) and exit without launching").option("--kill", "tear down the running Hub session").action(hubDevAction);
17309
- }
17310
- var HUB_SESSION, DEFAULT_HUB_UI_PORT, DEFAULT_HUB_API_PORT, FORWARD_PREFIXES, localPlatformEnv2, SENSITIVE_RE;
17421
+ ).option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`).option(
17422
+ "--api-port <port>",
17423
+ `Hub API port, and the key its tmux session is named after \u2014 several Hubs coexist on different ones (default: $MESH_HUB_DEV_API_PORT or ${DEFAULT_HUB_API_PORT})`
17424
+ ).option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)").option("--print-env", "print the assembled env (secrets redacted) and exit without launching").option(
17425
+ "--kill",
17426
+ "tear down a running Hub session; with several up, name it with --api-port (or $MESH_HUB_DEV_API_PORT)"
17427
+ ).action(hubDevAction);
17428
+ }
17429
+ var HUB_SESSION_PREFIX, HUB_SESSION_RE, DEFAULT_HUB_UI_PORT, DEFAULT_HUB_API_PORT, FORWARD_PREFIXES, localPlatformEnv2, SENSITIVE_RE;
17311
17430
  var init_hub = __esm({
17312
17431
  "libs/mesh-cli/src/commands/hub/index.ts"() {
17313
17432
  "use strict";
@@ -17317,7 +17436,8 @@ var init_hub = __esm({
17317
17436
  init_stack();
17318
17437
  init_log();
17319
17438
  init_pulumi();
17320
- HUB_SESSION = "mesh-hub-dev";
17439
+ HUB_SESSION_PREFIX = "mesh-hub-dev";
17440
+ HUB_SESSION_RE = new RegExp(`^${HUB_SESSION_PREFIX}(-\\d+)?$`);
17321
17441
  DEFAULT_HUB_UI_PORT = Number(DEFAULT_HUB_PORT);
17322
17442
  DEFAULT_HUB_API_PORT = 3002;
17323
17443
  FORWARD_PREFIXES = ["AWS_", "DEV_USER_"];