@mesh-tech/mesh-cli 0.36.0 → 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 (103) 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 -26418
  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/dev.d.ts +6 -0
  72. package/dist/src/commands/dev.js +152 -3
  73. package/dist/src/commands/docs.js +33 -0
  74. package/dist/src/commands/local/act-as-local.d.ts +5 -0
  75. package/dist/src/commands/local/act-as-local.js +41 -0
  76. package/dist/src/commands/local/auth-provision.d.ts +1 -0
  77. package/dist/src/commands/local/auth-provision.js +1 -0
  78. package/dist/src/commands/local/index.js +4 -0
  79. package/dist/src/commands/local/local-api-key.d.ts +44 -0
  80. package/dist/src/commands/local/local-api-key.js +115 -0
  81. package/dist/src/commands/local/local-spicedb.d.ts +95 -0
  82. package/dist/src/commands/local/local-spicedb.js +304 -0
  83. package/dist/src/commands/local/publish-docs.d.ts +81 -0
  84. package/dist/src/commands/local/publish-docs.js +328 -0
  85. package/dist/src/commands/local/seed-zitadel.js +2 -0
  86. package/dist/src/commands/temporal.js +191 -61
  87. package/dist/src/commands/temporal.js.map +3 -3
  88. package/dist/src/docs/convert.d.ts +35 -0
  89. package/dist/src/docs/convert.js +276 -0
  90. package/dist/src/docs/preview-source.d.ts +13 -0
  91. package/dist/src/docs/preview-source.js +58 -0
  92. package/dist/src/docs/preview.d.ts +32 -0
  93. package/dist/src/docs/preview.js +279 -0
  94. package/fragments/agents/AGENTS.md.hbs +27 -27
  95. package/fragments/base/README.md.hbs +5 -2
  96. package/fragments/base/index.ts.hbs +17 -30
  97. package/fragments/base/package.json.hbs +4 -1
  98. package/fragments/service/api/openapi.json.hbs +1 -1
  99. package/fragments/service/docs/docs.yaml.hbs +10 -0
  100. package/fragments/service/docs/pages/getting-started.md.hbs +6 -9
  101. package/package.json +19 -3
  102. package/skills/mesh/SKILL.md +2 -2
  103. 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
+ }
@@ -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
@@ -199,23 +199,23 @@ const worker = new mesh.apps.TemporalWorker("worker", {
199
199
  {{#if service}}
200
200
 
201
201
  // =============================================================================
202
- // Docs — the API's reference site, built by the platform from the spec and
203
- // the guides in docs/pages/ (this README is its landing page — `landingPage`
204
- // names another file, or `false` — and every guide is listed after it, in
205
- // path order). The Hub docs door is the path a docs site is published on:
206
- // here the site is reachable under `mesh dev`, and to publish it you declare
207
- // the API as a `mesh.apps.AppApiSurface` whose `docs` block builds this same
208
- // site (`docs: { spec, pages }`) — the Hub then serves it through that door.
209
- // A site given a hostname of its own instead (`ingress`, with the `auth`
210
- // proxy and the in-page `authentication` sign-in) is the deprecated public
211
- // shape: it still deploys, for the sites that are public today, and warns at
212
- // every preview. This declaration names none of it, and a new app needs
213
- // none. The site's readers hold this app's `docs` project role: declare the
214
- // role on the ZitadelAppIdentity and grant it from Access → Users; that same
215
- // role is what the Hub reads when a partner holding the Hub's `docs:<tenant>`
216
- // role opens this app's Docs tab, and the app then appears in their Apps list.
217
- // `mesh app check` flags a docs site put on an ingress instead
218
- // (`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`
219
219
  // on that same surface opens the Hub's act-as playground: an operator picks
220
220
  // one of this app's API keys on the Docs tab and the Hub executes the request
221
221
  // against the deployed API as that key, over the cluster network, with a
@@ -230,19 +230,6 @@ const worker = new mesh.apps.TemporalWorker("worker", {
230
230
  // `externalService`; its registry entry publishes the name, and the Hub reads
231
231
  // the app's Core connection from the vendor's shared health measurement.
232
232
  // =============================================================================
233
-
234
- new mesh.apps.ApiDocs("docs", {
235
- env,
236
- sources: { openapi: "./api/openapi.json" },
237
- pages: "./docs/pages",
238
- serverUrl: api.url,
239
- zudoku: {
240
- metadata: {
241
- title: "{{titleCase name}} API",
242
- description: "HTTP API for {{titleCase name}}",
243
- },
244
- },
245
- });
246
233
  {{/if}}
247
234
 
248
235
  // =============================================================================
@@ -29,5 +29,8 @@
29
29
  {{/if}}
30
30
  "tsx": "{{#if workspaceDeps}}catalog:{{else}}{{tsxRange}}{{/if}}",
31
31
  "typescript": "{{#if workspaceDeps}}catalog:{{else}}{{typescriptRange}}{{/if}}"
32
- }
32
+ }{{#if service}},
33
+ "mesh": {
34
+ "docs": { "config": "docs/docs.yaml", "service": "api" }
35
+ }{{/if}}
33
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mesh-tech/mesh-cli",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "CLI for Mesh platform development utilities",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -42,11 +42,13 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@mesh-tech/agent-contracts": "0.2.0",
45
- "@mesh-tech/tsconfig": "2.37.0",
45
+ "@mesh-tech/tsconfig": "2.38.0",
46
46
  "@mesh-tech/workflow-viz": "0.3.1",
47
47
  "@tanstack/intent": "^0.3.6",
48
48
  "@types/node": "^20.0.0",
49
- "tsx": "^4.20.4"
49
+ "@types/react": "^19.1.8",
50
+ "@types/react-dom": "^19.1.6",
51
+ "tailwindcss": "^4.2.2"
50
52
  },
51
53
  "dependencies": {
52
54
  "@aws-sdk/client-route-53": "^3.700.0",
@@ -61,9 +63,23 @@
61
63
  "chalk": "^5.3.0",
62
64
  "commander": "^12.1.0",
63
65
  "handlebars": "^4.7.8",
66
+ "react": "^19.2.0",
67
+ "react-dom": "^19.2.0",
68
+ "react-markdown": "^10.1.0",
69
+ "rehype-highlight": "^7.0.2",
70
+ "remark-gfm": "^4.0.1",
71
+ "tsx": "^4.20.4",
64
72
  "yaml": "^2.8.1",
65
73
  "zod": "^4.2.1"
66
74
  },
75
+ "peerDependencies": {
76
+ "mermaid": "^11.12.2"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "mermaid": {
80
+ "optional": true
81
+ }
82
+ },
67
83
  "optionalDependencies": {
68
84
  "@aws-sdk/client-s3": "^3.864.0",
69
85
  "@temporalio/client": "^1.17.0",