@mesh-tech/mesh-cli 0.35.2 → 0.37.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.
Files changed (107) hide show
  1. package/assets/docs-preview.css +1599 -0
  2. package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js +48 -0
  3. package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js.map +6 -0
  4. package/dist/bin/mesh.capture-history-A4IAVVIR.js +38 -0
  5. package/dist/bin/mesh.capture-history-A4IAVVIR.js.map +6 -0
  6. package/dist/bin/mesh.chunk-5A5OFSYR.js +187 -0
  7. package/dist/bin/mesh.chunk-5A5OFSYR.js.map +6 -0
  8. package/dist/bin/mesh.chunk-7DMAFUBQ.js +7715 -0
  9. package/dist/bin/mesh.chunk-7DMAFUBQ.js.map +6 -0
  10. package/dist/bin/mesh.chunk-A4DGMGHF.js +303 -0
  11. package/dist/bin/mesh.chunk-A4DGMGHF.js.map +6 -0
  12. package/dist/bin/mesh.chunk-CQ2IC4OP.js +217 -0
  13. package/dist/bin/mesh.chunk-CQ2IC4OP.js.map +6 -0
  14. package/dist/bin/mesh.chunk-F4654EPX.js +383 -0
  15. package/dist/bin/mesh.chunk-F4654EPX.js.map +6 -0
  16. package/dist/bin/mesh.chunk-IQOBTL42.js +60 -0
  17. package/dist/bin/mesh.chunk-IQOBTL42.js.map +6 -0
  18. package/dist/bin/mesh.chunk-J6DVWKRW.js +647 -0
  19. package/dist/bin/mesh.chunk-J6DVWKRW.js.map +6 -0
  20. package/dist/bin/mesh.chunk-JB7FWYSP.js +1751 -0
  21. package/dist/bin/mesh.chunk-JB7FWYSP.js.map +6 -0
  22. package/dist/bin/mesh.chunk-MCBMHWXP.js +837 -0
  23. package/dist/bin/mesh.chunk-MCBMHWXP.js.map +6 -0
  24. package/dist/bin/mesh.chunk-OW5EMVG5.js +507 -0
  25. package/dist/bin/mesh.chunk-OW5EMVG5.js.map +6 -0
  26. package/dist/bin/mesh.chunk-R5VVTXG4.js +10092 -0
  27. package/dist/bin/mesh.chunk-R5VVTXG4.js.map +6 -0
  28. package/dist/bin/mesh.chunk-VKV6Z3UI.js +1146 -0
  29. package/dist/bin/mesh.chunk-VKV6Z3UI.js.map +6 -0
  30. package/dist/bin/mesh.chunk-WFFDF346.js +59 -0
  31. package/dist/bin/mesh.chunk-WFFDF346.js.map +6 -0
  32. package/dist/bin/mesh.chunk-WOJOTUFZ.js +835 -0
  33. package/dist/bin/mesh.chunk-WOJOTUFZ.js.map +6 -0
  34. package/dist/bin/mesh.cli-reference-H3TOPIET.js +158 -0
  35. package/dist/bin/mesh.cli-reference-H3TOPIET.js.map +6 -0
  36. package/dist/bin/mesh.convert-KK6ZP256.js +274 -0
  37. package/dist/bin/mesh.convert-KK6ZP256.js.map +6 -0
  38. package/dist/bin/mesh.create-app-A5F45PWZ.js +39 -0
  39. package/dist/bin/mesh.create-app-A5F45PWZ.js.map +6 -0
  40. package/dist/bin/mesh.dev-token-server-GYDYPXYH.js +61 -0
  41. package/dist/bin/mesh.dev-token-server-GYDYPXYH.js.map +6 -0
  42. package/dist/bin/mesh.js +20 -26414
  43. package/dist/bin/mesh.js.map +3 -3
  44. package/dist/bin/mesh.login-JNHPVMCT.js +91 -0
  45. package/dist/bin/mesh.login-JNHPVMCT.js.map +6 -0
  46. package/dist/bin/mesh.platform-setup-EB64EFRN.js +37 -0
  47. package/dist/bin/mesh.platform-setup-EB64EFRN.js.map +6 -0
  48. package/dist/bin/mesh.preview-3IP34TDO.js +2410 -0
  49. package/dist/bin/mesh.preview-3IP34TDO.js.map +6 -0
  50. package/dist/bin/mesh.program-DWOKHNMQ.js +23 -0
  51. package/dist/bin/mesh.program-DWOKHNMQ.js.map +6 -0
  52. package/dist/bin/mesh.registry-OXF57OQW.js +47 -0
  53. package/dist/bin/mesh.registry-OXF57OQW.js.map +6 -0
  54. package/dist/bin/mesh.registry-identity-2HSGQGJC.js +42 -0
  55. package/dist/bin/mesh.registry-identity-2HSGQGJC.js.map +6 -0
  56. package/dist/bin/mesh.skills-MNDSMBIE.js +23 -0
  57. package/dist/bin/mesh.skills-MNDSMBIE.js.map +6 -0
  58. package/dist/bin/mesh.src-5D5DFSLX.js +1302 -0
  59. package/dist/bin/mesh.src-5D5DFSLX.js.map +6 -0
  60. package/dist/bin/mesh.src-G7TJBLNJ.js +40 -0
  61. package/dist/bin/mesh.src-G7TJBLNJ.js.map +6 -0
  62. package/dist/bin/mesh.stack-EFTFAOCV.js +64 -0
  63. package/dist/bin/mesh.stack-EFTFAOCV.js.map +6 -0
  64. package/dist/bin/mesh.start-ARIIY6CV.js +454 -0
  65. package/dist/bin/mesh.start-ARIIY6CV.js.map +6 -0
  66. package/dist/bin/mesh.wizard-ROAQAGSF.js +500 -0
  67. package/dist/bin/mesh.wizard-ROAQAGSF.js.map +6 -0
  68. package/dist/build-info.json +2 -2
  69. package/dist/src/commands/app-check.d.ts +2 -1
  70. package/dist/src/commands/app-check.js +32 -0
  71. package/dist/src/commands/create-app.d.ts +1 -0
  72. package/dist/src/commands/create-app.js +4 -0
  73. package/dist/src/commands/dev.d.ts +6 -0
  74. package/dist/src/commands/dev.js +152 -3
  75. package/dist/src/commands/docs.js +33 -0
  76. package/dist/src/commands/local/act-as-local.d.ts +5 -0
  77. package/dist/src/commands/local/act-as-local.js +41 -0
  78. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  79. package/dist/src/commands/local/auth-provision.js +1 -0
  80. package/dist/src/commands/local/index.js +4 -0
  81. package/dist/src/commands/local/local-api-key.d.ts +44 -0
  82. package/dist/src/commands/local/local-api-key.js +115 -0
  83. package/dist/src/commands/local/local-spicedb.d.ts +95 -0
  84. package/dist/src/commands/local/local-spicedb.js +304 -0
  85. package/dist/src/commands/local/publish-docs.d.ts +81 -0
  86. package/dist/src/commands/local/publish-docs.js +328 -0
  87. package/dist/src/commands/local/seed-zitadel.js +2 -0
  88. package/dist/src/commands/temporal.js +191 -61
  89. package/dist/src/commands/temporal.js.map +3 -3
  90. package/dist/src/docs/convert.d.ts +35 -0
  91. package/dist/src/docs/convert.js +276 -0
  92. package/dist/src/docs/preview-source.d.ts +13 -0
  93. package/dist/src/docs/preview-source.js +58 -0
  94. package/dist/src/docs/preview.d.ts +32 -0
  95. package/dist/src/docs/preview.js +279 -0
  96. package/dist/src/utils/scaffold-versions.d.ts +1 -0
  97. package/dist/src/utils/scaffold-versions.js +1 -0
  98. package/fragments/agents/AGENTS.md.hbs +27 -27
  99. package/fragments/base/README.md.hbs +5 -2
  100. package/fragments/base/index.ts.hbs +22 -33
  101. package/fragments/base/package.json.hbs +5 -1
  102. package/fragments/service/api/openapi.json.hbs +1 -1
  103. package/fragments/service/docs/docs.yaml.hbs +10 -0
  104. package/fragments/service/docs/pages/getting-started.md.hbs +6 -9
  105. package/package.json +19 -3
  106. package/skills/mesh/SKILL.md +2 -2
  107. package/stack/docker-compose.hub.yml +6 -0
@@ -0,0 +1,58 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { buildDocsSource } from "@mesh-tech/api-registry/docs-source-build";
4
+ export function resolveDocsConfigPath(target) {
5
+ const abs = resolve(target);
6
+ let stat;
7
+ try {
8
+ stat = statSync(abs);
9
+ }
10
+ catch {
11
+ throw new Error(`No docs source at ${abs} — pass the directory that holds docs.yaml, or docs.yaml itself`);
12
+ }
13
+ if (stat.isFile()) {
14
+ if (basename(abs) !== "docs.yaml")
15
+ throw new Error(`${abs} is not a docs.yaml`);
16
+ return abs;
17
+ }
18
+ const configPath = join(abs, "docs.yaml");
19
+ try {
20
+ if (statSync(configPath).isFile())
21
+ return configPath;
22
+ }
23
+ catch {
24
+ }
25
+ throw new Error(`No docs.yaml in ${abs}`);
26
+ }
27
+ function missingEventsCatalogProblem(service) {
28
+ return `docs.yaml names events "${service}", but no events catalog was supplied`;
29
+ }
30
+ export function readPreviewSource(configPath, opts = {}) {
31
+ const built = buildDocsSource({ configPath, ...(opts.specPath ? { specPath: resolve(opts.specPath) } : {}) });
32
+ const { config } = built.bundle;
33
+ const notes = [];
34
+ let problems = built.problems;
35
+ if (config.events) {
36
+ const expected = missingEventsCatalogProblem(config.events);
37
+ if (problems.includes(expected)) {
38
+ problems = problems.filter((p) => p !== expected);
39
+ notes.push(`events: the catalog of ${config.events} is built from the app's event definitions at deploy, so the preview has no Events section`);
40
+ }
41
+ }
42
+ notes.push("availability: each operation's availability is published by the deploy, so the preview shows none");
43
+ let specText;
44
+ try {
45
+ specText = readFileSync(built.specPath, "utf8");
46
+ }
47
+ catch {
48
+ specText = undefined;
49
+ }
50
+ return {
51
+ configPath: resolve(configPath),
52
+ specPath: built.specPath,
53
+ bundle: built.bundle,
54
+ ...(specText !== undefined ? { specText } : {}),
55
+ problems,
56
+ notes,
57
+ };
58
+ }
@@ -0,0 +1,32 @@
1
+ import { type DocsEngine } from "@mesh-tech/api-docs-engine";
2
+ import { type PreviewSource } from "./preview-source.js";
3
+ export declare const PREVIEW_CSS_PATH = "/docs-preview.css";
4
+ export declare function previewCssFile(): string;
5
+ export interface PreviewPage {
6
+ status: number;
7
+ html: string;
8
+ }
9
+ export declare function renderPreviewPage(input: {
10
+ source: PreviewSource;
11
+ engine: DocsEngine | undefined;
12
+ search: URLSearchParams;
13
+ }): PreviewPage;
14
+ export interface PreviewServer {
15
+ url: string;
16
+ port: number;
17
+ close(): Promise<void>;
18
+ }
19
+ export declare function startPreviewServer(args: {
20
+ configPath: string;
21
+ specPath?: string;
22
+ port: number;
23
+ cssFile?: string;
24
+ }): Promise<PreviewServer>;
25
+ export interface DocsPreviewOptions {
26
+ target: string;
27
+ port: string;
28
+ out?: string;
29
+ check: boolean;
30
+ spec?: string;
31
+ }
32
+ export declare function runDocsPreview(opts: DocsPreviewOptions): Promise<boolean>;
@@ -0,0 +1,279 @@
1
+ import { createServer } from "node:http";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import * as React from "react";
5
+ import { renderToString } from "react-dom/server";
6
+ import { buildEngine } from "@mesh-tech/api-docs-engine";
7
+ import { docsPage } from "@mesh-tech/api-registry";
8
+ import { DocsSourceView, locationFromParams, locationToParams } from "@mesh-tech/canvas-api-docs/docs";
9
+ import { logError, logInfo, logSuccess, logWarn } from "../utils/log.js";
10
+ import { findPackageRoot } from "../commands/local/stack.js";
11
+ import { readPreviewSource, resolveDocsConfigPath } from "./preview-source.js";
12
+ globalThis.React ??= React;
13
+ export const PREVIEW_CSS_PATH = "/docs-preview.css";
14
+ export function previewCssFile() {
15
+ return join(findPackageRoot(), "assets", "docs-preview.css");
16
+ }
17
+ function displayPath(file) {
18
+ const rel = relative(process.cwd(), file);
19
+ return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : file;
20
+ }
21
+ function escapeHtml(text) {
22
+ return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
23
+ }
24
+ function engineCache() {
25
+ let last;
26
+ return (specText) => {
27
+ if (specText === undefined)
28
+ return undefined;
29
+ if (last?.text === specText)
30
+ return last.engine;
31
+ let engine;
32
+ try {
33
+ engine = buildEngine(JSON.parse(specText));
34
+ }
35
+ catch {
36
+ engine = undefined;
37
+ }
38
+ last = { text: specText, engine };
39
+ return engine;
40
+ };
41
+ }
42
+ export function renderPreviewPage(input) {
43
+ const { source, engine, search } = input;
44
+ const { bundle } = source;
45
+ const title = bundle.config.title;
46
+ const notices = [];
47
+ let status = 200;
48
+ let location = locationFromParams(search, bundle);
49
+ const askedPage = search.get("page");
50
+ if (askedPage && !(location.kind === "page" && location.page === askedPage)) {
51
+ status = 404;
52
+ notices.push(`This source has no page "${askedPage}".`);
53
+ }
54
+ if (search.get("section") === "events" && !bundle.events) {
55
+ notices.push(bundle.config.events
56
+ ? `The events of ${bundle.config.events} are documented from the catalog the deploy builds; the preview has none.`
57
+ : "This source documents no events.");
58
+ }
59
+ let op;
60
+ if (location.kind === "reference" && location.op) {
61
+ op = engine?.getOp(location.op) ?? undefined;
62
+ if (!op) {
63
+ status = 404;
64
+ notices.push(`The spec has no operation "${location.op}".`);
65
+ location = { kind: "reference" };
66
+ }
67
+ }
68
+ const view = React.createElement(DocsSourceView, {
69
+ bundle,
70
+ title,
71
+ index: engine?.index() ?? [],
72
+ tags: engine?.tags() ?? [],
73
+ location,
74
+ onNavigate: () => { },
75
+ hrefFor: (loc) => `?${locationToParams(loc).toString()}`,
76
+ host: {
77
+ loadOp: (id) => {
78
+ const slice = engine?.getOp(id);
79
+ return slice ? Promise.resolve(slice) : Promise.reject(new Error(`no operation ${id}`));
80
+ },
81
+ },
82
+ ...(op ? { preloadedOp: op } : {}),
83
+ });
84
+ const heading = location.kind === "page"
85
+ ? (docsPage(bundle, location.page)?.title ?? title)
86
+ : location.kind === "events"
87
+ ? "Events"
88
+ : op
89
+ ? (op.summary ?? op.name)
90
+ : ((bundle.config.reference || undefined)?.label ?? "API Reference");
91
+ return {
92
+ status,
93
+ html: previewDocument({
94
+ title: heading === title ? title : `${heading} · ${title}`,
95
+ source,
96
+ notices,
97
+ body: renderToString(view),
98
+ }),
99
+ };
100
+ }
101
+ function previewChrome(source) {
102
+ const where = displayPath(source.configPath);
103
+ return `<header class="border-b bg-muted/40">
104
+ <div class="mx-auto flex max-w-7xl flex-wrap items-center gap-x-4 gap-y-1 px-6 py-3 text-sm">
105
+ <span class="font-semibold">Docs preview</span>
106
+ <code class="font-mono text-xs text-muted-foreground">${escapeHtml(where)}</code>
107
+ <span class="ml-auto text-xs text-muted-foreground" data-preview-try-it>Try-it runs in the Hub — this preview renders the docs only.</span>
108
+ </div>
109
+ </header>`;
110
+ }
111
+ function list(items) {
112
+ return `<ul class="mt-2 list-disc space-y-1 pl-5">${items.map((i) => `<li>${escapeHtml(i)}</li>`).join("")}</ul>`;
113
+ }
114
+ function previewDocument(input) {
115
+ const { source, notices } = input;
116
+ const problems = source.problems.length
117
+ ? `<section role="alert" class="mx-auto max-w-7xl px-6 pt-4" data-preview-problems="${source.problems.length}">
118
+ <div class="rounded-md border border-destructive/50 bg-destructive/5 p-4 text-sm">
119
+ <p class="font-semibold text-destructive">${source.problems.length} problem${source.problems.length === 1 ? "" : "s"} — the deploy refuses this source until ${source.problems.length === 1 ? "it is" : "they are"} fixed</p>
120
+ ${list(source.problems)}
121
+ </div>
122
+ </section>`
123
+ : "";
124
+ const noticeBlock = notices.length
125
+ ? `<section class="mx-auto max-w-7xl px-6 pt-4" data-preview-notices>
126
+ <div class="rounded-md border bg-muted/40 p-4 text-sm">${list(notices)}</div>
127
+ </section>`
128
+ : "";
129
+ const notes = source.notes.length
130
+ ? `<details class="mx-auto max-w-7xl px-6 pt-4 text-xs text-muted-foreground">
131
+ <summary class="cursor-pointer select-none">What the preview does not show</summary>
132
+ ${list(source.notes)}
133
+ </details>`
134
+ : "";
135
+ return `<!doctype html>
136
+ <html lang="en">
137
+ <head>
138
+ <meta charset="utf-8">
139
+ <meta name="viewport" content="width=device-width, initial-scale=1">
140
+ <title>${escapeHtml(input.title)} — docs preview</title>
141
+ <link rel="stylesheet" href="${PREVIEW_CSS_PATH}">
142
+ </head>
143
+ <body class="bg-background text-foreground antialiased">
144
+ ${previewChrome(source)}
145
+ ${problems}
146
+ ${noticeBlock}
147
+ ${notes}
148
+ <main class="mx-auto max-w-7xl px-6 py-6">${input.body}</main>
149
+ </body>
150
+ </html>
151
+ `;
152
+ }
153
+ function errorDocument(configPath, heading, message) {
154
+ return `<!doctype html>
155
+ <html lang="en">
156
+ <head>
157
+ <meta charset="utf-8">
158
+ <meta name="viewport" content="width=device-width, initial-scale=1">
159
+ <title>${escapeHtml(heading)} — docs preview</title>
160
+ <link rel="stylesheet" href="${PREVIEW_CSS_PATH}">
161
+ </head>
162
+ <body class="bg-background text-foreground antialiased">
163
+ ${previewChrome({ configPath })}
164
+ <section role="alert" class="mx-auto max-w-7xl px-6 pt-4">
165
+ <div class="rounded-md border border-destructive/50 bg-destructive/5 p-4 text-sm">
166
+ <p class="font-semibold text-destructive">${escapeHtml(heading)}</p>
167
+ <pre class="mt-2 whitespace-pre-wrap font-mono text-xs">${escapeHtml(message)}</pre>
168
+ </div>
169
+ </section>
170
+ </body>
171
+ </html>
172
+ `;
173
+ }
174
+ export async function startPreviewServer(args) {
175
+ const engineFor = engineCache();
176
+ const cssFile = args.cssFile ?? previewCssFile();
177
+ const message = (error) => (error instanceof Error ? error.message : String(error));
178
+ const pageFor = (search) => {
179
+ let source;
180
+ try {
181
+ source = readPreviewSource(args.configPath, { specPath: args.specPath });
182
+ }
183
+ catch (error) {
184
+ return { status: 500, html: errorDocument(args.configPath, "The source cannot be read", message(error)) };
185
+ }
186
+ try {
187
+ return renderPreviewPage({ source, engine: engineFor(source.specText), search });
188
+ }
189
+ catch (error) {
190
+ return { status: 500, html: errorDocument(args.configPath, "The page cannot be rendered", message(error)) };
191
+ }
192
+ };
193
+ const handle = (req, res) => {
194
+ const url = new URL(req.url ?? "/", "http://localhost");
195
+ if (req.method !== "GET" && req.method !== "HEAD") {
196
+ res.writeHead(405, { allow: "GET, HEAD", "content-type": "text/plain; charset=utf-8" }).end("Method not allowed\n");
197
+ return;
198
+ }
199
+ if (url.pathname === PREVIEW_CSS_PATH) {
200
+ let css;
201
+ try {
202
+ css = readFileSync(cssFile, "utf8");
203
+ }
204
+ catch {
205
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("Stylesheet not found\n");
206
+ return;
207
+ }
208
+ res.writeHead(200, { "content-type": "text/css; charset=utf-8", "cache-control": "no-cache" }).end(css);
209
+ return;
210
+ }
211
+ if (url.pathname !== "/") {
212
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("Not found — the preview is served at /\n");
213
+ return;
214
+ }
215
+ const page = pageFor(url.searchParams);
216
+ res.writeHead(page.status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
217
+ res.end(req.method === "HEAD" ? undefined : page.html);
218
+ };
219
+ const server = createServer(handle);
220
+ await new Promise((resolvePromise, reject) => {
221
+ server.once("error", (error) => {
222
+ reject(error.code === "EADDRINUSE"
223
+ ? new Error(`Port ${args.port} is already in use — pass --port <n> for another one (0 picks a free port)`)
224
+ : error);
225
+ });
226
+ server.listen(args.port, "127.0.0.1", () => resolvePromise());
227
+ });
228
+ const address = server.address();
229
+ const port = typeof address === "object" && address ? address.port : args.port;
230
+ return {
231
+ url: `http://localhost:${port}`,
232
+ port,
233
+ close: () => new Promise((resolvePromise) => server.close(() => resolvePromise())),
234
+ };
235
+ }
236
+ function reportProblems(source) {
237
+ if (source.problems.length === 0) {
238
+ logSuccess(`${source.bundle.config.title}: no problems`);
239
+ }
240
+ else {
241
+ logWarn(`${source.bundle.config.title}: ${source.problems.length} problem${source.problems.length === 1 ? "" : "s"} — the deploy refuses this source until ${source.problems.length === 1 ? "it is" : "they are"} fixed:`);
242
+ for (const problem of source.problems)
243
+ console.error(` - ${problem}`);
244
+ }
245
+ for (const note of source.notes)
246
+ logInfo(`note — ${note}`);
247
+ }
248
+ export async function runDocsPreview(opts) {
249
+ const configPath = resolveDocsConfigPath(opts.target);
250
+ if (opts.check || opts.out) {
251
+ const source = readPreviewSource(configPath, { specPath: opts.spec });
252
+ reportProblems(source);
253
+ if (opts.out) {
254
+ const outPath = resolve(opts.out);
255
+ mkdirSync(dirname(outPath), { recursive: true });
256
+ writeFileSync(outPath, `${JSON.stringify(source.bundle, null, 2)}\n`);
257
+ logSuccess(`Docs source bundle → ${displayPath(outPath)}`);
258
+ }
259
+ return !opts.check || source.problems.length === 0;
260
+ }
261
+ const port = Number(opts.port);
262
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
263
+ throw new Error(`--port must be a port number (0 picks a free one), not "${opts.port}"`);
264
+ }
265
+ try {
266
+ reportProblems(readPreviewSource(configPath, { specPath: opts.spec }));
267
+ }
268
+ catch (error) {
269
+ logError(error instanceof Error ? error.message : String(error));
270
+ }
271
+ const server = await startPreviewServer({ configPath, ...(opts.spec ? { specPath: opts.spec } : {}), port });
272
+ logSuccess(`Docs preview → ${server.url}/ (re-reads ${displayPath(configPath)} on every request; Ctrl-C to stop)`);
273
+ await new Promise((resolvePromise) => {
274
+ const stop = () => void server.close().then(resolvePromise);
275
+ process.once("SIGINT", stop);
276
+ process.once("SIGTERM", stop);
277
+ });
278
+ return true;
279
+ }
@@ -2,6 +2,7 @@ export declare const SCAFFOLD_PACKAGE_LINES: {
2
2
  readonly "@mesh-tech/app-kit": "^1.45.0";
3
3
  readonly "@mesh-tech/esm-bundle": "^0.1.1";
4
4
  readonly "@mesh-tech/events-service": "^0.1.0";
5
+ readonly "@mesh-tech/events-ui": "^0.1.0";
5
6
  readonly "@mesh-tech/authz": "^1.19.0";
6
7
  };
7
8
  export type ScaffoldPackage = keyof typeof SCAFFOLD_PACKAGE_LINES;
@@ -3,6 +3,7 @@ export const SCAFFOLD_PACKAGE_LINES = {
3
3
  "@mesh-tech/app-kit": "^1.45.0",
4
4
  "@mesh-tech/esm-bundle": "^0.1.1",
5
5
  "@mesh-tech/events-service": "^0.1.0",
6
+ "@mesh-tech/events-ui": "^0.1.0",
6
7
  "@mesh-tech/authz": "^1.19.0",
7
8
  };
8
9
  export const SCAFFOLD_TOOLCHAIN = {
@@ -18,33 +18,33 @@ Every mesh app follows 5 steps in `index.ts`:
18
18
  {{#if service}}
19
19
  ## API docs
20
20
 
21
- `api/openapi.json` is the API's OpenAPI document and `docs/pages/` holds its
22
- guides; `index.ts` declares a `mesh.apps.ApiDocs` site from both, reachable
23
- under `mesh dev`. **The Hub docs door is the path a docs site is published
24
- on**: declare the API as a `mesh.apps.AppApiSurface` whose `docs` block builds
25
- the site, and the Hub serves it through that door to the readers the app's
26
- `docs` role admits. Giving the site a hostname of its own instead — `ingress`,
27
- with the `auth` proxy and the in-page `authentication` sign-in — is the
28
- deprecated public shape: it still deploys, as the compatibility path for the
29
- sites that are public today, and warns at every `pulumi preview`. Nothing here
30
- writes one, and a new app has no reason to. A site declared on a
31
- `mesh.apps.VendorApiSurface` (a third-party core's raw reference) is read
32
- through the same door by holders of the Hub's `vendor-docs` add-on only —
33
- never by a partner — and declaring `audience: { roles: [] }` on one fails
34
- the deploy. Add `actAs: true` on an app surface to let Hub operators execute
35
- requests against the deployed API as one of the app's API keys from the Docs
36
- tab (the Hub's act-as playground) — the surface then verifies the Hub's
37
- signed assertion and runs the request as the key. `mesh app check` flags a
38
- docs site put on an ingress (`DOCS_SITE_PUBLIC`). A vendor surface's site is
39
- built from the enabled-ops cut of the core's spec, not the whole document, and
40
- a spec over 1 MiB is split into per-entity variants at build time
41
- (`docs.split: { above }` moves the threshold, `false` turns it off) — an app's
42
- own spec is already the shape it serves and gets neither. Keep the spec in
43
- step with the routes in
44
- `api/src/index.ts` — the site renders what the file says. Keep pages `.md`,
45
- not `.mdx`: a site an API surface declares is served by the Hub under its own
46
- origin, which carries data and never code, so an `.mdx` page fails that
47
- deploy (the bare `ApiDocs` here accepts it, which is exactly the trap).
21
+ `api/openapi.json` is the API's OpenAPI document, `docs/pages/` holds its
22
+ guides, and `docs/docs.yaml` arranges them — a **native docs source** (title,
23
+ landing page, nav). **The Hub renders it on the app's Docs tab**, with its own
24
+ components, once the API is declared as a `mesh.apps.AppApiSurface` whose
25
+ `docs` block names it: `docs: { spec: "./api/openapi.json", config:
26
+ "./docs/docs.yaml" }`. No docs site, container or door is built, and no app
27
+ code reaches the Hub. Run `mesh docs preview docs` to see it as the Hub shows
28
+ it, with every problem the deploy would refuse (`--check` for CI). A page's
29
+ front-matter `title` is its heading, so a page never opens with its own `#`
30
+ heading, and pages are `.md` — Markdown data, never code. An image is a PNG,
31
+ JPEG, GIF or WebP file under `docs/pages/`, referenced relatively
32
+ (`![Flow](img/flow.png)`): it is bundled with the source and rendered from
33
+ those bytes, while an external image is never loaded. Under
34
+ `mesh dev --local`, `package.json`'s `mesh.docs` publishes the same source to
35
+ the local Hub's Docs tab.
36
+
37
+ A `docs` block that builds a Zudoku site instead (`pages`, `landingPage`,
38
+ `playground`, `audience`, `site`) is **deprecated**: it deploys, with a preview
39
+ warning, for one more minor release and is then removed with the Hub docs
40
+ door. `mesh docs convert --spec … --pages … --landing … --fix-pages` writes the
41
+ `docs.yaml` from such a block and prints the edit; `mesh app check` names it
42
+ (`DOCS_SITE_DEPRECATED`), and a site on a hostname of its own
43
+ (`DOCS_SITE_PUBLIC`). Add `actAs: true` on an app surface to let Hub operators
44
+ execute requests against the deployed API as one of the app's API keys from
45
+ the Docs tab (the Hub's act-as playground) — the surface then verifies the
46
+ Hub's signed assertion and runs the request as the key. Keep the spec in step
47
+ with the routes in `api/src/index.ts` — the docs render what the file says.
48
48
  {{/if}}
49
49
 
50
50
  {{#if temporal}}
@@ -4,8 +4,11 @@ A Mesh app for tenant `{{tenant}}`. The Pulumi program is `index.ts`; run it
4
4
  locally with `mesh dev`, deploy it with `mesh deploy up`.
5
5
  {{#if service}}
6
6
 
7
- The API's reference site is built by the platform from `api/openapi.json` and
8
- the guides in `docs/pages/` — this README is its landing page.
7
+ The API's docs are `api/openapi.json` and the guides in `docs/pages/`, arranged
8
+ by `docs/docs.yaml` — a native docs source, which the Hub renders on the app's
9
+ Docs tab once an API surface names it. `mesh docs preview docs` shows it
10
+ locally, and `mesh dev --local` publishes it to the local Hub's Docs tab
11
+ (`mesh.docs` in `package.json`).
9
12
  {{/if}}
10
13
 
11
14
  ## The version this app reports
@@ -113,8 +113,10 @@ const config = new mesh.apps.AppConfig("config", {
113
113
  // for endpoint credentials, the publisher key, the Zitadel introspection app,
114
114
  // the delivery task queue, the worker registration that lists the app's
115
115
  // Temporal namespace on the Hub's Workflows page (with the service's own
116
- // housekeeping workflows marked internal, so only staff see them there), and
117
- // the registry surface — all children of this one declaration.
116
+ // housekeeping workflows marked internal, so only staff see them there), the
117
+ // registry surface, and the operator UI (`@mesh-tech/events-ui`, installed in
118
+ // package.json) as its own signed-in Service at `{app}-events-ui` — all
119
+ // children of this one declaration.
118
120
  //
119
121
  // Its database's schema migration gates the deploy: `pulumi up` waits for the
120
122
  // migration Job and fails when it fails, and the service's pods are held until
@@ -127,7 +129,7 @@ const config = new mesh.apps.AppConfig("config", {
127
129
  // that as "use the default".
128
130
  const events = new mesh.apps.EventsService("events", {
129
131
  env,
130
- identity: { connector, projectId: identity.projectId },
132
+ identity: { connector, projectId: identity.projectId, dependsOn: [identity] },
131
133
  config,
132
134
  // "thin" sends identifiers only, so nothing the API would mask reaches a
133
135
  // third-party endpoint and the consumer follows the identifiers back to the
@@ -197,23 +199,23 @@ const worker = new mesh.apps.TemporalWorker("worker", {
197
199
  {{#if service}}
198
200
 
199
201
  // =============================================================================
200
- // Docs — the API's reference site, built by the platform from the spec and
201
- // the guides in docs/pages/ (this README is its landing page — `landingPage`
202
- // names another file, or `false` — and every guide is listed after it, in
203
- // path order). The Hub docs door is the path a docs site is published on:
204
- // here the site is reachable under `mesh dev`, and to publish it you declare
205
- // the API as a `mesh.apps.AppApiSurface` whose `docs` block builds this same
206
- // site (`docs: { spec, pages }`) — the Hub then serves it through that door.
207
- // A site given a hostname of its own instead (`ingress`, with the `auth`
208
- // proxy and the in-page `authentication` sign-in) is the deprecated public
209
- // shape: it still deploys, for the sites that are public today, and warns at
210
- // every preview. This declaration names none of it, and a new app needs
211
- // none. The site's readers hold this app's `docs` project role: declare the
212
- // role on the ZitadelAppIdentity and grant it from Access → Users; that same
213
- // role is what the Hub reads when a partner holding the Hub's `docs:<tenant>`
214
- // role opens this app's Docs tab, and the app then appears in their Apps list.
215
- // `mesh app check` flags a docs site put on an ingress instead
216
- // (`DOCS_SITE_PUBLIC`). Declaring `actAs: true`
202
+ // Docs — the spec (api/openapi.json) and the guides in docs/pages/, arranged
203
+ // by docs/docs.yaml: a native docs source. To publish it, declare the API as
204
+ // a `mesh.apps.AppApiSurface` whose docs block names it —
205
+ // `docs: { spec: "./api/openapi.json", config: "./docs/docs.yaml" }` — and
206
+ // the Hub renders it on the app's Docs tab itself: no docs site, container
207
+ // or door. `mesh docs preview docs` shows it locally, with every problem the
208
+ // deploy would refuse, and under `mesh dev --local` the app's package.json
209
+ // (`mesh.docs`) publishes it to the local Hub's Docs tab. A docs block that builds a
210
+ // site instead (`pages`, `landingPage`, `playground`, `audience`) is
211
+ // deprecated — `mesh docs convert` moves one, and `mesh app check` names it
212
+ // (`DOCS_SITE_DEPRECATED`); a site given a hostname of its own (`ingress`,
213
+ // with the `auth` proxy and the in-page `authentication` sign-in) is the
214
+ // older public shape (`DOCS_SITE_PUBLIC`). The readers hold this app's
215
+ // `docs` project role: declare the role on the ZitadelAppIdentity and grant
216
+ // it from Access → Users; that same role is what the Hub reads when a
217
+ // partner holding the Hub's `docs:<tenant>` role opens this app's Docs tab,
218
+ // and the app then appears in their Apps list. Declaring `actAs: true`
217
219
  // on that same surface opens the Hub's act-as playground: an operator picks
218
220
  // one of this app's API keys on the Docs tab and the Hub executes the request
219
221
  // against the deployed API as that key, over the cluster network, with a
@@ -228,19 +230,6 @@ const worker = new mesh.apps.TemporalWorker("worker", {
228
230
  // `externalService`; its registry entry publishes the name, and the Hub reads
229
231
  // the app's Core connection from the vendor's shared health measurement.
230
232
  // =============================================================================
231
-
232
- new mesh.apps.ApiDocs("docs", {
233
- env,
234
- sources: { openapi: "./api/openapi.json" },
235
- pages: "./docs/pages",
236
- serverUrl: api.url,
237
- zudoku: {
238
- metadata: {
239
- title: "{{titleCase name}} API",
240
- description: "HTTP API for {{titleCase name}}",
241
- },
242
- },
243
- });
244
233
  {{/if}}
245
234
 
246
235
  // =============================================================================
@@ -12,6 +12,7 @@
12
12
  {{#if events}}
13
13
  "@mesh-tech/authz": "{{#if workspaceDeps}}workspace:*{{else}}{{authzRange}}{{/if}}",
14
14
  "@mesh-tech/events-service": "{{#if workspaceDeps}}workspace:*{{else}}{{eventsServiceRange}}{{/if}}",
15
+ "@mesh-tech/events-ui": "{{#if workspaceDeps}}workspace:*{{else}}{{eventsUiRange}}{{/if}}",
15
16
  {{/if}}
16
17
  "@mesh-tech/infra-components": "{{#if workspaceDeps}}workspace:*{{else}}{{meshRange}}{{/if}}",
17
18
  {{#if events}}
@@ -28,5 +29,8 @@
28
29
  {{/if}}
29
30
  "tsx": "{{#if workspaceDeps}}catalog:{{else}}{{tsxRange}}{{/if}}",
30
31
  "typescript": "{{#if workspaceDeps}}catalog:{{else}}{{typescriptRange}}{{/if}}"
31
- }
32
+ }{{#if service}},
33
+ "mesh": {
34
+ "docs": { "config": "docs/docs.yaml", "service": "api" }
35
+ }{{/if}}
32
36
  }
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "{{titleCase name}} API",
5
5
  "version": "0.0.1",
6
- "description": "HTTP API for {{titleCase name}}. This document is what the docs site renders — keep it in step with api/src/index.ts."
6
+ "description": "HTTP API for {{titleCase name}}. This document is the API reference of the app's docs source (docs/docs.yaml) — keep it in step with api/src/index.ts."
7
7
  },
8
8
  "paths": {
9
9
  "/": {
@@ -0,0 +1,10 @@
1
+ # The {{titleCase name}} API's docs source: the guides under pages/ and the API
2
+ # reference from the spec, as data the Hub renders on the app's Docs tab once
3
+ # an API surface names this file (docs: { spec, config }). Check it with
4
+ # `mesh docs preview docs`.
5
+ title: {{titleCase name}} API
6
+ spec: ../api/openapi.json
7
+ pages: pages
8
+ landing: getting-started
9
+ reference:
10
+ label: API Reference
@@ -2,15 +2,12 @@
2
2
  title: Getting started
3
3
  ---
4
4
 
5
- # Getting started with the {{titleCase name}} API
6
-
7
- Guides for this API live in `docs/pages/` as Markdown and are published next to
8
- the reference generated from `api/openapi.json`. Add a page per topic — each
9
- is listed in the sidebar after the README landing page, in path order, under
10
- the `title` in its front matter; the platform builds the site on every deploy.
11
- Keep pages as `.md`, not `.mdx`: a
12
- site an API surface declares is also read inside the Hub (the docs door),
13
- which serves data, never code — an `.mdx` page fails that deploy.
5
+ Guides for the {{titleCase name}} API live in `docs/pages/` as Markdown and are
6
+ published next to the reference generated from `api/openapi.json`; add a page
7
+ per topic and list it in `docs/docs.yaml`'s `nav`. A page's `title` in its
8
+ front matter is also its heading, so a page does not open with its own `#`
9
+ heading. Keep pages as `.md`, not `.mdx`: the Hub renders them as Markdown
10
+ data and never runs code. `mesh docs preview docs` shows the result.
14
11
 
15
12
  ## Calling the API
16
13