@mesh-tech/mesh-cli 0.27.0 → 0.28.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 +27 -4
- package/dist/bin/mesh.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +2 -1
- package/dist/src/commands/hub/index.d.ts +2 -0
- package/dist/src/commands/hub/index.js +16 -1
- package/fragments/base/index.ts.hbs +13 -1
- package/package.json +2 -2
- package/templates/api-auth/index.ts.hbs +5 -1
- package/templates/api-role-gating/index.ts.hbs +5 -1
- package/templates/external-service/index.ts.hbs +5 -1
- package/templates/platform-repo/AGENTS.md.hbs +20 -4
- package/templates/workflow/index.ts.hbs +5 -1
package/dist/build-info.json
CHANGED
|
@@ -23,7 +23,8 @@ export const APP_CHECK_RULES = {
|
|
|
23
23
|
gate: "0.2",
|
|
24
24
|
title: "App registers with the Hub",
|
|
25
25
|
severity: "block",
|
|
26
|
-
remediation: "end index.ts with `export const app = env.register({ description })`"
|
|
26
|
+
remediation: "end index.ts with `export const app = env.register({ displayName, description })` — " +
|
|
27
|
+
"one sentence a non-technical operator can act on; put the caveats in `operatorNote`",
|
|
27
28
|
scope: "app",
|
|
28
29
|
},
|
|
29
30
|
CUSTOM_SESSION_AUTH: {
|
|
@@ -19,7 +19,9 @@ export interface HubDevEnvOptions {
|
|
|
19
19
|
tenants?: string;
|
|
20
20
|
uiPort: number;
|
|
21
21
|
apiPort: number;
|
|
22
|
+
audience?: string;
|
|
22
23
|
}
|
|
24
|
+
export declare function resolveHubAudience(raw: string | undefined): "operator" | "tenant";
|
|
23
25
|
export interface AssembledHubEnv {
|
|
24
26
|
apiEnv: Record<string, string>;
|
|
25
27
|
uiEnv: Record<string, string>;
|
|
@@ -99,6 +99,11 @@ export function forwardedSessionVars(sessionEnv) {
|
|
|
99
99
|
}
|
|
100
100
|
return out;
|
|
101
101
|
}
|
|
102
|
+
export function resolveHubAudience(raw) {
|
|
103
|
+
if (raw === undefined || raw === "")
|
|
104
|
+
return "operator";
|
|
105
|
+
return raw.trim().toLowerCase() === "operator" ? "operator" : "tenant";
|
|
106
|
+
}
|
|
102
107
|
export function assembleHubEnv(session, sessionEnv, opts) {
|
|
103
108
|
const warnings = [];
|
|
104
109
|
const notes = [];
|
|
@@ -141,6 +146,7 @@ export function assembleHubEnv(session, sessionEnv, opts) {
|
|
|
141
146
|
if (!temporalAddress) {
|
|
142
147
|
throw new MeshCliError(`Dev session '${session.name}' exposes no Temporal tunnel — the Hub can't reach the stack's Temporal.`, { remediation: { command: "mesh dev # relaunch; the session records tunnels.temporal in its state" } });
|
|
143
148
|
}
|
|
149
|
+
const audienceEnv = { HUB_AUDIENCE: resolveHubAudience(opts.audience) };
|
|
144
150
|
const apiEnv = {
|
|
145
151
|
...forwarded,
|
|
146
152
|
PORT: String(opts.apiPort),
|
|
@@ -148,6 +154,7 @@ export function assembleHubEnv(session, sessionEnv, opts) {
|
|
|
148
154
|
HUB_TENANT: hubTenant,
|
|
149
155
|
HUB_SCOPE_ENV: scopeEnv,
|
|
150
156
|
HUB_SCOPE_TENANTS: scopeTenants.join(","),
|
|
157
|
+
...audienceEnv,
|
|
151
158
|
...(isLocalPlatform ? localPlatformEnv(sessionEnv) : {}),
|
|
152
159
|
};
|
|
153
160
|
const uiEnv = {
|
|
@@ -157,6 +164,7 @@ export function assembleHubEnv(session, sessionEnv, opts) {
|
|
|
157
164
|
HUB_TENANT: hubTenant,
|
|
158
165
|
DEFAULT_ORG: scopeTenants[0],
|
|
159
166
|
DEFAULT_ENV: scopeEnv,
|
|
167
|
+
...audienceEnv,
|
|
160
168
|
};
|
|
161
169
|
return { apiEnv, uiEnv, scope: { hubTenant, scopeEnv, scopeTenants }, temporalAddress, warnings, notes };
|
|
162
170
|
}
|
|
@@ -282,7 +290,12 @@ async function hubDevAction(opts) {
|
|
|
282
290
|
throw new MeshCliError(`Dev session '${session.name}' has a state file but no live tmux session — its tunnels and token-server are gone.`, { remediation: { command: `mesh dev # from ${session.state.appRoot}` } });
|
|
283
291
|
}
|
|
284
292
|
const sessionEnv = readTmuxSessionEnv(session.name);
|
|
285
|
-
const assembled = assembleHubEnv(session, sessionEnv, {
|
|
293
|
+
const assembled = assembleHubEnv(session, sessionEnv, {
|
|
294
|
+
tenants: opts.tenants,
|
|
295
|
+
uiPort,
|
|
296
|
+
apiPort,
|
|
297
|
+
audience: opts.audience ?? process.env.HUB_AUDIENCE,
|
|
298
|
+
});
|
|
286
299
|
if (opts.printEnv) {
|
|
287
300
|
console.log(chalk.bold("\nhub-api env:"));
|
|
288
301
|
for (const [k, v] of Object.entries(redactEnv(assembled.apiEnv)))
|
|
@@ -308,6 +321,7 @@ async function hubDevAction(opts) {
|
|
|
308
321
|
logInfo(`Dev session : ${session.name} (${session.state.appRoot})`);
|
|
309
322
|
logInfo(`Hub source : ${platformDir}`);
|
|
310
323
|
logInfo(`Scope : HUB_TENANT=${assembled.scope.hubTenant} HUB_SCOPE_ENV=${assembled.scope.scopeEnv} HUB_SCOPE_TENANTS=${assembled.scope.scopeTenants.join(",")}`);
|
|
324
|
+
logInfo(`Audience : ${assembled.apiEnv.HUB_AUDIENCE} (--audience tenant for a client's view)`);
|
|
311
325
|
logInfo(`Temporal : ${assembled.temporalAddress}`);
|
|
312
326
|
for (const note of assembled.notes)
|
|
313
327
|
logInfo(note);
|
|
@@ -372,6 +386,7 @@ export function registerHubCommands(program) {
|
|
|
372
386
|
.description("Launch the current-code Hub (api + ui) against a running `mesh dev` session — env auto-assembled, zero hand-set vars")
|
|
373
387
|
.option("--session <name>", "dev session to observe (default: auto-detect from cwd)")
|
|
374
388
|
.option("--tenants <list>", "comma-separated HUB_SCOPE_TENANTS override (default: the session's tenant)")
|
|
389
|
+
.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`")
|
|
375
390
|
.option("--port <port>", `Hub UI host port (default: $MESH_HUB_DEV_PORT or ${DEFAULT_HUB_UI_PORT})`)
|
|
376
391
|
.option("--api-port <port>", `Hub API port (default: ${DEFAULT_HUB_API_PORT})`)
|
|
377
392
|
.option("--platform-dir <dir>", "mesh-platform checkout to run the Hub from (default: $MESH_PLATFORM_DIR or walk up from cwd)")
|
|
@@ -145,7 +145,19 @@ new mesh.apps.ApiDocs("docs", {
|
|
|
145
145
|
// =============================================================================
|
|
146
146
|
|
|
147
147
|
export const app = env.register({
|
|
148
|
-
|
|
148
|
+
// The line under this app's name on the Hub's Apps list, read by the
|
|
149
|
+
// client's own operators more often than by anyone on the team. One
|
|
150
|
+
// sentence in THEIR words — what it does, not how. No file paths, no ticket
|
|
151
|
+
// ids, no platform vocabulary; `operatorNote` below is where those go.
|
|
152
|
+
displayName: "{{titleCase name}}",
|
|
153
|
+
description: "{{titleCase name}}.",
|
|
154
|
+
// Engineer to engineer. A Hub deployed for a client never serializes this —
|
|
155
|
+
// the key is dropped from the response — so it is the right home for the
|
|
156
|
+
// caveat, the runbook pointer and the ticket.
|
|
157
|
+
// operatorNote: "Shares a database with the importer; see the runbook.",
|
|
158
|
+
//
|
|
159
|
+
// `visibility: "platform"` would hide this app from a client's Hub entirely.
|
|
160
|
+
// That is for platform components, not for an app a client operates.
|
|
149
161
|
});
|
|
150
162
|
|
|
151
163
|
// Exported so the bindings above are used, and because a stack's URLs are the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mesh-tech/mesh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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.18.0",
|
|
46
46
|
"@mesh-tech/workflow-viz": "0.3.0",
|
|
47
47
|
"@tanstack/intent": "^0.3.6",
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
@@ -115,7 +115,11 @@ new mesh.auth.AppAuthzPointer("authz", { env, mode: "policy-engine", opsHubMetad
|
|
|
115
115
|
// --- Outputs ---
|
|
116
116
|
|
|
117
117
|
export const app = env.register({
|
|
118
|
-
|
|
118
|
+
displayName: "{{titleCase name}}",
|
|
119
|
+
// What a client operator reads on the Apps list. One sentence, their words.
|
|
120
|
+
description: "Serves the {{titleCase name}} API to signed-in callers.",
|
|
121
|
+
// Engineer to engineer; a Hub deployed for a client never serializes it.
|
|
122
|
+
operatorNote: "Platform-authenticated API — authn (Zitadel JWT) plus authz (SpiceDB policy engine).",
|
|
119
123
|
});
|
|
120
124
|
|
|
121
125
|
/** The app's own Zitadel project id (grant people and API keys against this). */
|
|
@@ -107,7 +107,11 @@ new mesh.auth.AppAuthzPointer("authz", {
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
export const app = env.register({
|
|
110
|
-
|
|
110
|
+
displayName: "{{titleCase name}}",
|
|
111
|
+
// What a client operator reads on the Apps list. One sentence, their words.
|
|
112
|
+
description: "Serves the {{titleCase name}} API, with access decided by each caller's role.",
|
|
113
|
+
// Engineer to engineer; a Hub deployed for a client never serializes it.
|
|
114
|
+
operatorNote: "Role-gating mode — roles are enforced in-process from the Zitadel JWT, no SpiceDB.",
|
|
111
115
|
});
|
|
112
116
|
|
|
113
117
|
/** The app's own Zitadel project id (grant API keys against this). */
|
|
@@ -42,7 +42,11 @@ const vendorpay = new mesh.apps.ExternalService("vendorpay", {
|
|
|
42
42
|
env,
|
|
43
43
|
type: "payment",
|
|
44
44
|
kind: "connection",
|
|
45
|
-
|
|
45
|
+
// One sentence a client operator can act on, in their words. This renders on
|
|
46
|
+
// the Hub's Integrations row, which a credit union's own staff read.
|
|
47
|
+
description: "Gets price quotes from our payments vendor.",
|
|
48
|
+
// Engineer to engineer: never serialized by a Hub deployed for a client.
|
|
49
|
+
operatorNote: "Example vendor wired the external-service way — credentials in Secrets Manager, no runtime to probe.",
|
|
46
50
|
credentials: {
|
|
47
51
|
fields: {
|
|
48
52
|
baseUrl: { name: "Base URL", description: "Vendor API endpoint" },
|
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
- Invoke the CLI as `pnpm mesh <cmd>` or (cwd-independent) `pnpm exec mesh <cmd>` —
|
|
4
4
|
a root `"mesh"` script + the `@mesh-tech/mesh-cli` devDependency make both work
|
|
5
5
|
after `pnpm install`. For a bare `mesh` on PATH, run once: `pnpm exec mesh install-shim`.
|
|
6
|
-
- Operations are `mesh` commands, never raw `
|
|
7
|
-
`
|
|
8
|
-
|
|
6
|
+
- Operations are `mesh` commands, never raw `pulumi`/`kubectl`: `mesh deploy
|
|
7
|
+
preview|up` (run inside `core/` or `platform/`), `mesh init app-tenant`
|
|
8
|
+
(doctor). **One exception:** a platform stack carries no `mesh:deployerRole`
|
|
9
|
+
— only app stacks do — so `mesh deploy` reads credentials from the
|
|
10
|
+
environment, on the first deploy and on every day-2 run:
|
|
11
|
+
`aws sso login --profile {{profile}}`, then
|
|
12
|
+
`eval "$(aws configure export-credentials --profile {{profile}} --format env)"`.
|
|
13
|
+
- **`mesh login {{tenant}}.{{env}}` comes LAST, after the first deploy.** The
|
|
14
|
+
Zitadel it signs in to is what the `platform/` layer creates; run earlier it
|
|
15
|
+
fails with `No configuration found for "{{tenant}}.{{env}}"`. Package access
|
|
16
|
+
is a separate, global sign-in (`mesh registry login`) and is not needed in
|
|
17
|
+
this repo at all.
|
|
9
18
|
- Deploy order matters: `core/` before `platform/` (platform imports core
|
|
10
|
-
from the SSM registry)
|
|
19
|
+
from the SSM registry), and the NS delegation of the public zone is a manual
|
|
20
|
+
step **in the middle of the `core/` deploy** — that deploy blocks on its ACM
|
|
21
|
+
certificate until you delegate the new zone's NS records from the parent, so
|
|
22
|
+
a `core/` deploy that looks hung is waiting on you. README.md holds the full
|
|
23
|
+
first-deploy sequence, the one `mesh init` printed when it scaffolded this
|
|
24
|
+
repo. Do not invent a different one.
|
|
25
|
+
- `platform/` builds service images, so it needs Docker; `core/` does not.
|
|
26
|
+
That is why a Docker-less container can deploy `core/` and stop there.
|
|
11
27
|
- Capabilities are config, not code: enable/disable platform services in
|
|
12
28
|
`platform/Pulumi.{{tenant}}-{{env}}.yaml` (temporal/zitadel/hub/tenants/…);
|
|
13
29
|
the `index.ts` layer programs should rarely change.
|
|
@@ -64,5 +64,9 @@ const worker = new mesh.apps.TemporalWorker("worker", {
|
|
|
64
64
|
// --- Outputs ---
|
|
65
65
|
|
|
66
66
|
export const app = env.register({
|
|
67
|
-
|
|
67
|
+
displayName: "{{titleCase name}}",
|
|
68
|
+
// What a client operator reads on the Apps list. One sentence, their words.
|
|
69
|
+
description: "Runs {{titleCase name}}'s scheduled and long-running jobs.",
|
|
70
|
+
// Engineer to engineer; a Hub deployed for a client never serializes it.
|
|
71
|
+
operatorNote: "Temporal workflow app scaffolded by `mesh create-app --template workflow`.",
|
|
68
72
|
});
|