@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/build-info.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { type ProbeResult } from "./app-check-resolve.js";
|
|
3
3
|
import { jsonReport, type Check, type CheckResult, type CheckStatus } from "./dev-doctor.js";
|
|
4
|
-
export type AppCheckCode = "UNRESOLVABLE_IMPORT" | "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER";
|
|
4
|
+
export type AppCheckCode = "UNRESOLVABLE_IMPORT" | "UI_IN_API" | "NO_UI_SERVICE" | "NO_REGISTER" | "CUSTOM_SESSION_AUTH" | "NON_PLATFORM_LOGGER" | "LOCAL_AUTH_LIB" | "NO_AUTHZ_POINTER" | "METADATA_UNPUBLISHED" | "IAC_GRANTS" | "POINTER_PROJECT_OVERRIDE" | "PASSWORD_STORE" | "EMAIL_ALLOWLIST" | "USERS_TABLE" | "DOCS_SITE_PUBLIC" | "DOCS_AUDIENCE_IMPLICIT" | "DOCS_AUDIENCE_UNGRANTABLE" | "DOCS_VENDOR_AUDIENCE_PARTNER" | "EVENTS_ROLES_UNDECLARED";
|
|
5
5
|
export interface AppCheckRule {
|
|
6
6
|
gate: string;
|
|
7
7
|
title: string;
|
|
@@ -35,6 +35,11 @@ export declare function docsSiteFindings(text: string): Array<{
|
|
|
35
35
|
why: string;
|
|
36
36
|
}>;
|
|
37
37
|
export declare function statedKind(decl: Pick<SurfaceDeclaration, "ctor">, args: Map<string, string>): string | undefined;
|
|
38
|
+
export declare function eventsRoleFindings(text: string): Array<{
|
|
39
|
+
code: AppCheckCode;
|
|
40
|
+
line: number;
|
|
41
|
+
why: string;
|
|
42
|
+
}>;
|
|
38
43
|
export declare function listApps(root: string): string[];
|
|
39
44
|
export declare function probeResolution(root: string, appRel: string): ProbeResult;
|
|
40
45
|
export declare function scanApp(root: string, appRel: string, resolution?: ProbeResult): Finding[];
|
|
@@ -127,6 +127,13 @@ export const APP_CHECK_RULES = {
|
|
|
127
127
|
remediation: 'declare every audience role on the ZitadelAppIdentity (roles: ["docs"]) so the Hub can grant it from the app\'s Access tab — 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',
|
|
128
128
|
scope: "app",
|
|
129
129
|
},
|
|
130
|
+
EVENTS_ROLES_UNDECLARED: {
|
|
131
|
+
gate: "H",
|
|
132
|
+
title: "Events roles grantable from the Hub",
|
|
133
|
+
severity: "block",
|
|
134
|
+
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 — 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",
|
|
135
|
+
scope: "app",
|
|
136
|
+
},
|
|
130
137
|
DOCS_VENDOR_AUDIENCE_PARTNER: {
|
|
131
138
|
gate: "H",
|
|
132
139
|
title: "Vendor reference never partner-readable",
|
|
@@ -257,7 +264,7 @@ const RE_HTML = /from ["']hono\/(html|jsx)["']|react-dom\/server|\bc\.html\(|<!d
|
|
|
257
264
|
const RE_OIDC = /openid-client|code_verifier|authorization_code|buildAuthorizeUrl|end_session/;
|
|
258
265
|
const RE_COOKIE = /setCookie\(|hono\/cookie|Set-Cookie/;
|
|
259
266
|
const RE_LOGGER = /from ["'](pino|hono\/logger|winston|bunyan)["']|require\(["'](pino|winston|bunyan)["']\)|^[^/*]*\bconsole\.(log|info|warn|error)\(/;
|
|
260
|
-
const 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)\(/;
|
|
267
|
+
const 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\(/;
|
|
261
268
|
const RE_POINTER = /AppAuthzPointer/;
|
|
262
269
|
const RE_METADATA = /compileOpsHubMetadata|ops-hub-metadata|SpiceDBSchema/;
|
|
263
270
|
const RE_IAC_GRANT = /userId: *"[0-9]{6,}"/;
|
|
@@ -268,6 +275,8 @@ const RE_USERS_TABLE = /create table (if not exists )?"?(users|roles|user_roles|
|
|
|
268
275
|
const RE_SURFACE_NEW = /new\s+mesh\.apps\.(ApiSurface|AppApiSurface|VendorApiSurface|Integration)\s*\(/g;
|
|
269
276
|
const RE_APIDOCS_NEW = /new\s+mesh\.apps\.ApiDocs\s*\(/g;
|
|
270
277
|
const RE_IDENTITY_NEW = /new\s+mesh\.auth\.(ZitadelAppIdentity)\s*\(/g;
|
|
278
|
+
const RE_EVENTS_NEW = /new\s+mesh\.apps\.(EventsService)\s*\(/g;
|
|
279
|
+
const EVENTS_PROJECT_ROLES = ["events:subscriber", "events:admin"];
|
|
271
280
|
const RE_IDENTITY_BINDING = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+mesh\.auth\.ZitadelAppIdentity\s*\(/g;
|
|
272
281
|
const DEFAULT_PARTNER_DOCS_ROLES = ["docs"];
|
|
273
282
|
export function stringList(value) {
|
|
@@ -557,6 +566,62 @@ export function statedKind(decl, args) {
|
|
|
557
566
|
return "vendor";
|
|
558
567
|
return /^["']([a-z]+)["']$/.exec(args.get("kind") ?? "")?.[1];
|
|
559
568
|
}
|
|
569
|
+
function programIdentities(text) {
|
|
570
|
+
const identities = [];
|
|
571
|
+
for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
|
|
572
|
+
const options = optionsObject(d.body, text);
|
|
573
|
+
if (!options)
|
|
574
|
+
continue;
|
|
575
|
+
const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
|
|
576
|
+
identities.push({ binding, options: topLevelProperties(options) });
|
|
577
|
+
}
|
|
578
|
+
return identities;
|
|
579
|
+
}
|
|
580
|
+
function declaredProjectRoles(identity) {
|
|
581
|
+
const rolesText = identity.options.get("roles");
|
|
582
|
+
const coarseText = identity.options.get("coarseRoles");
|
|
583
|
+
if (rolesText !== undefined)
|
|
584
|
+
return stringList(rolesText);
|
|
585
|
+
if (coarseText !== undefined)
|
|
586
|
+
return stringList(coarseText);
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
export function eventsRoleFindings(text) {
|
|
590
|
+
const findings = [];
|
|
591
|
+
const identities = programIdentities(text);
|
|
592
|
+
for (const decl of surfaceDeclarations(text, RE_EVENTS_NEW)) {
|
|
593
|
+
const options = optionsObject(decl.body, text);
|
|
594
|
+
if (!options)
|
|
595
|
+
continue;
|
|
596
|
+
const args = topLevelProperties(options);
|
|
597
|
+
if (args.get("enabled") === "false")
|
|
598
|
+
continue;
|
|
599
|
+
if (identities.length === 0) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const identityText = objectLiteralOf(args.get("identity"), text);
|
|
603
|
+
const projectExpr = identityText ? topLevelProperties(identityText).get("projectId") : undefined;
|
|
604
|
+
const projectRoot = projectExpr ? /^([A-Za-z_$][\w$]*)\.projectId\b/.exec(projectExpr.trim())?.[1] : undefined;
|
|
605
|
+
const identity = (projectRoot ? identities.find((id) => id.binding === projectRoot) : undefined) ??
|
|
606
|
+
(identities.length === 1 ? identities[0] : undefined);
|
|
607
|
+
if (!identity)
|
|
608
|
+
continue;
|
|
609
|
+
const declared = declaredProjectRoles(identity);
|
|
610
|
+
if (!declared)
|
|
611
|
+
continue;
|
|
612
|
+
const missing = EVENTS_PROJECT_ROLES.filter((r) => !declared.includes(r));
|
|
613
|
+
if (missing.length === 0)
|
|
614
|
+
continue;
|
|
615
|
+
findings.push({
|
|
616
|
+
code: "EVENTS_ROLES_UNDECLARED",
|
|
617
|
+
line: decl.line,
|
|
618
|
+
why: `mesh.apps.EventsService is declared but the app's ZitadelAppIdentity does not declare ${missing
|
|
619
|
+
.map((r) => `"${r}"`)
|
|
620
|
+
.join(" or ")} — 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`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return findings;
|
|
624
|
+
}
|
|
560
625
|
function docsAudienceUngrantable(text, decl, args, docs, httpText) {
|
|
561
626
|
const findings = [];
|
|
562
627
|
let roles;
|
|
@@ -569,14 +634,7 @@ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
|
|
|
569
634
|
}
|
|
570
635
|
if (!roles || roles.length === 0)
|
|
571
636
|
return findings;
|
|
572
|
-
const identities =
|
|
573
|
-
for (const d of surfaceDeclarations(text, RE_IDENTITY_NEW)) {
|
|
574
|
-
const options = optionsObject(d.body, text);
|
|
575
|
-
if (!options)
|
|
576
|
-
continue;
|
|
577
|
-
const binding = [...text.matchAll(RE_IDENTITY_BINDING)].find((m) => text.indexOf(d.body, m.index) === m.index + m[0].length - 1)?.[1];
|
|
578
|
-
identities.push({ binding, options: topLevelProperties(options) });
|
|
579
|
-
}
|
|
637
|
+
const identities = programIdentities(text);
|
|
580
638
|
if (identities.length === 0)
|
|
581
639
|
return findings;
|
|
582
640
|
const docsAuth = objectLiteralOf(docs.get("auth"), text);
|
|
@@ -587,9 +645,7 @@ function docsAudienceUngrantable(text, decl, args, docs, httpText) {
|
|
|
587
645
|
(identities.length === 1 ? identities[0] : undefined);
|
|
588
646
|
if (!identity)
|
|
589
647
|
return findings;
|
|
590
|
-
const
|
|
591
|
-
const coarseText = identity.options.get("coarseRoles");
|
|
592
|
-
const declared = rolesText !== undefined ? stringList(rolesText) : coarseText !== undefined ? stringList(coarseText) : [];
|
|
648
|
+
const declared = declaredProjectRoles(identity);
|
|
593
649
|
if (declared) {
|
|
594
650
|
for (const role of roles) {
|
|
595
651
|
if (declared.includes(role))
|
|
@@ -668,6 +724,8 @@ export function scanApp(root, appRel, resolution = probeResolution(root, appRel)
|
|
|
668
724
|
}
|
|
669
725
|
for (const f of docsSiteFindings(readText(index)))
|
|
670
726
|
hit(f.code, `${indexRel}:${f.line}`, f.why);
|
|
727
|
+
for (const f of eventsRoleFindings(readText(index)))
|
|
728
|
+
hit(f.code, `${indexRel}:${f.line}`, f.why);
|
|
671
729
|
for (const svc of serviceDirs(app)) {
|
|
672
730
|
for (const f of srcFiles(svc)) {
|
|
673
731
|
const text = readText(f);
|
|
@@ -27,10 +27,13 @@ export declare function generateComposableApp(appDir: string, context: {
|
|
|
27
27
|
database: boolean;
|
|
28
28
|
temporal: boolean;
|
|
29
29
|
bucket: boolean;
|
|
30
|
+
events: boolean;
|
|
30
31
|
region: string;
|
|
31
32
|
workspaceDeps: boolean;
|
|
32
33
|
meshRange: string;
|
|
33
34
|
esmBundleRange: string;
|
|
35
|
+
eventsServiceRange: string;
|
|
36
|
+
authzRange: string;
|
|
34
37
|
typesNodeRange: string;
|
|
35
38
|
tsxRange: string;
|
|
36
39
|
typescriptRange: string;
|
|
@@ -30,6 +30,7 @@ const PRIMITIVES = {
|
|
|
30
30
|
database: "Database (PostgreSQL via Prisma)",
|
|
31
31
|
temporal: "Temporal (workflow orchestration)",
|
|
32
32
|
bucket: "S3 Bucket (file storage)",
|
|
33
|
+
events: "Events and webhooks (publish, subscriptions, signed delivery)",
|
|
33
34
|
};
|
|
34
35
|
const HUB_ACCOUNTS = {
|
|
35
36
|
mesh: "159923586610",
|
|
@@ -198,6 +199,10 @@ async function promptForOptions(options) {
|
|
|
198
199
|
primitives.push("service");
|
|
199
200
|
logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
|
|
200
201
|
}
|
|
202
|
+
if (primitives.includes("events") && !primitives.includes("service")) {
|
|
203
|
+
primitives.push("service");
|
|
204
|
+
logInfo("Added 'service' — the events publisher lives in the api/ directory.");
|
|
205
|
+
}
|
|
201
206
|
return { tenant, name, primitives };
|
|
202
207
|
}
|
|
203
208
|
export function copyTemplate(srcDir, destDir, context) {
|
|
@@ -311,6 +316,13 @@ export function generateComposableApp(appDir, context) {
|
|
|
311
316
|
}
|
|
312
317
|
copyTemplate(bucketDir, appDir, context);
|
|
313
318
|
}
|
|
319
|
+
if (context.events) {
|
|
320
|
+
const eventsDir = path.join(fragmentsDir, "events");
|
|
321
|
+
if (!fs.existsSync(eventsDir)) {
|
|
322
|
+
throw new Error(`Fragment directory not found: ${eventsDir}`);
|
|
323
|
+
}
|
|
324
|
+
copyTemplate(eventsDir, appDir, context);
|
|
325
|
+
}
|
|
314
326
|
const agentsDir = path.join(fragmentsDir, "agents");
|
|
315
327
|
if (fs.existsSync(agentsDir)) {
|
|
316
328
|
copyTemplate(agentsDir, appDir, context);
|
|
@@ -375,6 +387,10 @@ export function registerCreateAppCommand(program) {
|
|
|
375
387
|
primitives.push("service");
|
|
376
388
|
logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
|
|
377
389
|
}
|
|
390
|
+
if (primitives.includes("events") && !primitives.includes("service")) {
|
|
391
|
+
primitives.push("service");
|
|
392
|
+
logInfo("Added 'service' — the events publisher lives in the api/ directory.");
|
|
393
|
+
}
|
|
378
394
|
await runComposable(tenant, name, primitives, options.test);
|
|
379
395
|
});
|
|
380
396
|
}
|
|
@@ -496,6 +512,12 @@ async function runComposable(tenant, name, primitives, test) {
|
|
|
496
512
|
const esmBundle = workspaceDeps
|
|
497
513
|
? { range: "", resolved: true }
|
|
498
514
|
: resolvePublishedRange("@mesh-tech/esm-bundle", repoRoot);
|
|
515
|
+
const eventsService = workspaceDeps || !primitives.includes("events")
|
|
516
|
+
? { range: "", resolved: true }
|
|
517
|
+
: resolvePublishedRange("@mesh-tech/events-service", repoRoot);
|
|
518
|
+
const authz = workspaceDeps || !primitives.includes("events")
|
|
519
|
+
? { range: "", resolved: true }
|
|
520
|
+
: resolvePublishedRange("@mesh-tech/authz", repoRoot);
|
|
499
521
|
if (!workspaceDeps) {
|
|
500
522
|
logInfo(mesh.resolved
|
|
501
523
|
? `@mesh-tech/* pinned to ${mesh.range} (current published line)`
|
|
@@ -508,10 +530,13 @@ async function runComposable(tenant, name, primitives, test) {
|
|
|
508
530
|
database: primitives.includes("database"),
|
|
509
531
|
temporal: primitives.includes("temporal"),
|
|
510
532
|
bucket: primitives.includes("bucket"),
|
|
533
|
+
events: primitives.includes("events"),
|
|
511
534
|
region,
|
|
512
535
|
workspaceDeps,
|
|
513
536
|
meshRange: mesh.range,
|
|
514
537
|
esmBundleRange: esmBundle.range,
|
|
538
|
+
eventsServiceRange: eventsService.range,
|
|
539
|
+
authzRange: authz.range,
|
|
515
540
|
typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
|
|
516
541
|
tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
|
|
517
542
|
typescriptRange: SCAFFOLD_TOOLCHAIN.typescript,
|
|
@@ -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() {
|
|
@@ -248,8 +278,8 @@ function isPortListening(port) {
|
|
|
248
278
|
socket.once("error", () => done(false));
|
|
249
279
|
});
|
|
250
280
|
}
|
|
251
|
-
function hubEnvDir() {
|
|
252
|
-
return path.join(os.tmpdir(), "mesh-dev-sessions",
|
|
281
|
+
function hubEnvDir(sessionName) {
|
|
282
|
+
return path.join(os.tmpdir(), "mesh-dev-sessions", sessionName);
|
|
253
283
|
}
|
|
254
284
|
export function parsePort(raw, fallback, source) {
|
|
255
285
|
if (raw === undefined)
|
|
@@ -273,20 +303,30 @@ export function redactEnv(env) {
|
|
|
273
303
|
}
|
|
274
304
|
async function hubDevAction(opts) {
|
|
275
305
|
if (opts.kill) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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;
|
|
280
314
|
}
|
|
281
|
-
|
|
282
|
-
|
|
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" } });
|
|
283
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}'.`);
|
|
284
322
|
return;
|
|
285
323
|
}
|
|
286
324
|
const uiPort = opts.port
|
|
287
325
|
? parsePort(opts.port, DEFAULT_HUB_UI_PORT, "--port")
|
|
288
326
|
: parsePort(process.env.MESH_HUB_DEV_PORT, DEFAULT_HUB_UI_PORT, "MESH_HUB_DEV_PORT");
|
|
289
|
-
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");
|
|
290
330
|
const platformDir = resolvePlatformDir(opts.platformDir, process.cwd());
|
|
291
331
|
const cwdAppRoot = findAppRoot(process.cwd());
|
|
292
332
|
const session = pickDevSession(listDevSessions(), { session: opts.session, cwdAppRoot });
|
|
@@ -309,9 +349,10 @@ async function hubDevAction(opts) {
|
|
|
309
349
|
console.log(` ${k}=${v}`);
|
|
310
350
|
return;
|
|
311
351
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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` },
|
|
315
356
|
});
|
|
316
357
|
}
|
|
317
358
|
for (const [label, port, flag] of [
|
|
@@ -333,20 +374,20 @@ async function hubDevAction(opts) {
|
|
|
333
374
|
logWarn(warning);
|
|
334
375
|
const apiDir = path.join(platformDir, "apps", "hub", "api");
|
|
335
376
|
const uiDir = path.join(platformDir, "apps", "hub", "ui");
|
|
336
|
-
execFileSync("tmux", ["new-session", "-d", "-s",
|
|
377
|
+
execFileSync("tmux", ["new-session", "-d", "-s", hubSession, "-n", "api", "-c", apiDir]);
|
|
337
378
|
if (!assembled.apiEnv.DEV_USER_TOKEN_URL && assembled.apiEnv.DEV_USER_ID_TOKEN) {
|
|
338
379
|
const platform = session.state.devOutput.platform;
|
|
339
380
|
const credContext = `mesh.${platform.env}`;
|
|
340
381
|
const tokenPort = await findFreePort();
|
|
341
382
|
const tokenUrl = `http://127.0.0.1:${tokenPort}`;
|
|
342
|
-
execFileSync("tmux", ["new-window", "-t",
|
|
343
|
-
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"], {
|
|
344
385
|
stdio: "ignore",
|
|
345
386
|
});
|
|
346
387
|
execFileSync("tmux", [
|
|
347
388
|
"send-keys",
|
|
348
389
|
"-t",
|
|
349
|
-
`${
|
|
390
|
+
`${hubSession}:token-server`,
|
|
350
391
|
`npx mesh dev __token-server ${tokenPort} ${credContext}`,
|
|
351
392
|
"Enter",
|
|
352
393
|
]);
|
|
@@ -358,15 +399,15 @@ async function hubDevAction(opts) {
|
|
|
358
399
|
logSuccess(`Dev-user token-server: ${tokenUrl} (context ${credContext})`);
|
|
359
400
|
}
|
|
360
401
|
const launch = (window, dir, env, cmd, createWindow) => {
|
|
361
|
-
const envFile = path.join(hubEnvDir(), `${window}.env.sh`);
|
|
402
|
+
const envFile = path.join(hubEnvDir(hubSession), `${window}.env.sh`);
|
|
362
403
|
writeEnvFile(envFile, env);
|
|
363
404
|
if (createWindow) {
|
|
364
|
-
execFileSync("tmux", ["new-window", "-t",
|
|
405
|
+
execFileSync("tmux", ["new-window", "-t", hubSession, "-n", window, "-c", dir]);
|
|
365
406
|
}
|
|
366
|
-
execFileSync("tmux", ["set-option", "-t", `${
|
|
407
|
+
execFileSync("tmux", ["set-option", "-t", `${hubSession}:${window}`, "remain-on-exit", "on"], {
|
|
367
408
|
stdio: "ignore",
|
|
368
409
|
});
|
|
369
|
-
execFileSync("tmux", ["send-keys", "-t", `${
|
|
410
|
+
execFileSync("tmux", ["send-keys", "-t", `${hubSession}:${window}`, buildLaunchCommand(envFile, dir, cmd), "Enter"]);
|
|
370
411
|
};
|
|
371
412
|
launch("api", apiDir, assembled.apiEnv, "pnpm dev", false);
|
|
372
413
|
launch("ui", uiDir, assembled.uiEnv, `pnpm dev --port ${uiPort} --strictPort`, true);
|
|
@@ -374,14 +415,14 @@ async function hubDevAction(opts) {
|
|
|
374
415
|
const apiUp = await waitForPort("127.0.0.1", apiPort, 90_000);
|
|
375
416
|
const uiUp = apiUp && (await waitForPort("127.0.0.1", uiPort, 90_000));
|
|
376
417
|
if (!apiUp || !uiUp) {
|
|
377
|
-
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` } });
|
|
378
419
|
}
|
|
379
420
|
console.log("");
|
|
380
421
|
logSuccess(`Mesh Hub (dev-local scope ${assembled.scope.scopeTenants.join(",")} @ ${assembled.scope.scopeEnv})`);
|
|
381
422
|
console.log(` ${chalk.bold.cyan(`http://localhost:${uiPort}`)} (also reachable on your tailnet)`);
|
|
382
423
|
console.log(` api http://localhost:${apiPort}`);
|
|
383
|
-
console.log(` logs tmux attach -t ${
|
|
384
|
-
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}`);
|
|
385
426
|
}
|
|
386
427
|
export function registerHubCommands(program) {
|
|
387
428
|
const hub = program.command("hub").description("Local Hub over dev-local stacks");
|
|
@@ -392,9 +433,9 @@ export function registerHubCommands(program) {
|
|
|
392
433
|
.option("--tenants <list>", "comma-separated HUB_SCOPE_TENANTS override (default: the session's tenant)")
|
|
393
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`")
|
|
394
435
|
.option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`)
|
|
395
|
-
.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})`)
|
|
396
437
|
.option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)")
|
|
397
438
|
.option("--print-env", "print the assembled env (secrets redacted) and exit without launching")
|
|
398
|
-
.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)")
|
|
399
440
|
.action(hubDevAction);
|
|
400
441
|
}
|
|
@@ -533,7 +533,7 @@ var init_src = __esm({
|
|
|
533
533
|
z.object({ op: z.string().trim().min(1) }),
|
|
534
534
|
z.object({ unavailable: z.string().trim().min(1) })
|
|
535
535
|
]);
|
|
536
|
-
apiSurfaceKindSchema = z.enum(["app", "vendor"]);
|
|
536
|
+
apiSurfaceKindSchema = z.enum(["app", "vendor", "events"]);
|
|
537
537
|
apiDocsSiteAudienceSchema = z.discriminatedUnion("readers", [
|
|
538
538
|
z.object({ readers: z.literal("partner"), roles: z.array(z.string().min(1)) }),
|
|
539
539
|
z.object({ readers: z.literal("operator") })
|