@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 +162 -42
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.d.ts +6 -1
- package/dist/src/commands/app-check.js +70 -12
- package/dist/src/commands/create-app.d.ts +3 -0
- package/dist/src/commands/create-app.js +25 -0
- package/dist/src/commands/hub/index.d.ts +6 -1
- package/dist/src/commands/hub/index.js +67 -26
- package/dist/src/commands/temporal.js +1 -1
- package/dist/src/commands/temporal.js.map +1 -1
- package/dist/src/utils/scaffold-versions.d.ts +2 -0
- package/dist/src/utils/scaffold-versions.js +2 -0
- package/fragments/base/index.ts.hbs +104 -0
- package/fragments/base/package.json.hbs +10 -0
- package/fragments/events/api/src/events.ts.hbs +161 -0
- package/fragments/events/schema.ts.hbs +23 -0
- package/fragments/service/api/src/index.ts.hbs +27 -0
- package/package.json +2 -2
- package/skills/core/SKILL.md +3 -2
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
|
|
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",
|
|
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
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
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
|
-
|
|
17222
|
-
|
|
17223
|
-
|
|
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",
|
|
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",
|
|
17255
|
-
execFileSync24("tmux", ["set-option", "-t", `${
|
|
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
|
-
`${
|
|
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",
|
|
17389
|
+
execFileSync24("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
|
|
17277
17390
|
}
|
|
17278
|
-
execFileSync24("tmux", ["set-option", "-t", `${
|
|
17391
|
+
execFileSync24("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
|
|
17279
17392
|
stdio: "ignore"
|
|
17280
17393
|
});
|
|
17281
|
-
execFileSync24("tmux", ["send-keys", "-t", `${
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
17309
|
-
|
|
17310
|
-
|
|
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
|
-
|
|
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_"];
|