@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.
- package/assets/docs-preview.css +1599 -0
- package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js +48 -0
- package/dist/bin/mesh.auth-preflight-JFKL4Y4X.js.map +6 -0
- package/dist/bin/mesh.capture-history-A4IAVVIR.js +38 -0
- package/dist/bin/mesh.capture-history-A4IAVVIR.js.map +6 -0
- package/dist/bin/mesh.chunk-5A5OFSYR.js +187 -0
- package/dist/bin/mesh.chunk-5A5OFSYR.js.map +6 -0
- package/dist/bin/mesh.chunk-7DMAFUBQ.js +7715 -0
- package/dist/bin/mesh.chunk-7DMAFUBQ.js.map +6 -0
- package/dist/bin/mesh.chunk-A4DGMGHF.js +303 -0
- package/dist/bin/mesh.chunk-A4DGMGHF.js.map +6 -0
- package/dist/bin/mesh.chunk-CQ2IC4OP.js +217 -0
- package/dist/bin/mesh.chunk-CQ2IC4OP.js.map +6 -0
- package/dist/bin/mesh.chunk-F4654EPX.js +383 -0
- package/dist/bin/mesh.chunk-F4654EPX.js.map +6 -0
- package/dist/bin/mesh.chunk-IQOBTL42.js +60 -0
- package/dist/bin/mesh.chunk-IQOBTL42.js.map +6 -0
- package/dist/bin/mesh.chunk-J6DVWKRW.js +647 -0
- package/dist/bin/mesh.chunk-J6DVWKRW.js.map +6 -0
- package/dist/bin/mesh.chunk-JB7FWYSP.js +1751 -0
- package/dist/bin/mesh.chunk-JB7FWYSP.js.map +6 -0
- package/dist/bin/mesh.chunk-MCBMHWXP.js +837 -0
- package/dist/bin/mesh.chunk-MCBMHWXP.js.map +6 -0
- package/dist/bin/mesh.chunk-OW5EMVG5.js +507 -0
- package/dist/bin/mesh.chunk-OW5EMVG5.js.map +6 -0
- package/dist/bin/mesh.chunk-R5VVTXG4.js +10092 -0
- package/dist/bin/mesh.chunk-R5VVTXG4.js.map +6 -0
- package/dist/bin/mesh.chunk-VKV6Z3UI.js +1146 -0
- package/dist/bin/mesh.chunk-VKV6Z3UI.js.map +6 -0
- package/dist/bin/mesh.chunk-WFFDF346.js +59 -0
- package/dist/bin/mesh.chunk-WFFDF346.js.map +6 -0
- package/dist/bin/mesh.chunk-WOJOTUFZ.js +835 -0
- package/dist/bin/mesh.chunk-WOJOTUFZ.js.map +6 -0
- package/dist/bin/mesh.cli-reference-H3TOPIET.js +158 -0
- package/dist/bin/mesh.cli-reference-H3TOPIET.js.map +6 -0
- package/dist/bin/mesh.convert-KK6ZP256.js +274 -0
- package/dist/bin/mesh.convert-KK6ZP256.js.map +6 -0
- package/dist/bin/mesh.create-app-A5F45PWZ.js +39 -0
- package/dist/bin/mesh.create-app-A5F45PWZ.js.map +6 -0
- package/dist/bin/mesh.dev-token-server-GYDYPXYH.js +61 -0
- package/dist/bin/mesh.dev-token-server-GYDYPXYH.js.map +6 -0
- package/dist/bin/mesh.js +20 -26418
- package/dist/bin/mesh.js.map +3 -3
- package/dist/bin/mesh.login-JNHPVMCT.js +91 -0
- package/dist/bin/mesh.login-JNHPVMCT.js.map +6 -0
- package/dist/bin/mesh.platform-setup-EB64EFRN.js +37 -0
- package/dist/bin/mesh.platform-setup-EB64EFRN.js.map +6 -0
- package/dist/bin/mesh.preview-3IP34TDO.js +2410 -0
- package/dist/bin/mesh.preview-3IP34TDO.js.map +6 -0
- package/dist/bin/mesh.program-DWOKHNMQ.js +23 -0
- package/dist/bin/mesh.program-DWOKHNMQ.js.map +6 -0
- package/dist/bin/mesh.registry-OXF57OQW.js +47 -0
- package/dist/bin/mesh.registry-OXF57OQW.js.map +6 -0
- package/dist/bin/mesh.registry-identity-2HSGQGJC.js +42 -0
- package/dist/bin/mesh.registry-identity-2HSGQGJC.js.map +6 -0
- package/dist/bin/mesh.skills-MNDSMBIE.js +23 -0
- package/dist/bin/mesh.skills-MNDSMBIE.js.map +6 -0
- package/dist/bin/mesh.src-5D5DFSLX.js +1302 -0
- package/dist/bin/mesh.src-5D5DFSLX.js.map +6 -0
- package/dist/bin/mesh.src-G7TJBLNJ.js +40 -0
- package/dist/bin/mesh.src-G7TJBLNJ.js.map +6 -0
- package/dist/bin/mesh.stack-EFTFAOCV.js +64 -0
- package/dist/bin/mesh.stack-EFTFAOCV.js.map +6 -0
- package/dist/bin/mesh.start-ARIIY6CV.js +454 -0
- package/dist/bin/mesh.start-ARIIY6CV.js.map +6 -0
- package/dist/bin/mesh.wizard-ROAQAGSF.js +500 -0
- package/dist/bin/mesh.wizard-ROAQAGSF.js.map +6 -0
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.d.ts +2 -1
- package/dist/src/commands/app-check.js +32 -0
- package/dist/src/commands/dev.d.ts +6 -0
- package/dist/src/commands/dev.js +152 -3
- package/dist/src/commands/docs.js +33 -0
- package/dist/src/commands/local/act-as-local.d.ts +5 -0
- package/dist/src/commands/local/act-as-local.js +41 -0
- package/dist/src/commands/local/auth-provision.d.ts +1 -0
- package/dist/src/commands/local/auth-provision.js +1 -0
- package/dist/src/commands/local/index.js +4 -0
- package/dist/src/commands/local/local-api-key.d.ts +44 -0
- package/dist/src/commands/local/local-api-key.js +115 -0
- package/dist/src/commands/local/local-spicedb.d.ts +95 -0
- package/dist/src/commands/local/local-spicedb.js +304 -0
- package/dist/src/commands/local/publish-docs.d.ts +81 -0
- package/dist/src/commands/local/publish-docs.js +328 -0
- package/dist/src/commands/local/seed-zitadel.js +2 -0
- package/dist/src/commands/temporal.js +191 -61
- package/dist/src/commands/temporal.js.map +3 -3
- package/dist/src/docs/convert.d.ts +35 -0
- package/dist/src/docs/convert.js +276 -0
- package/dist/src/docs/preview-source.d.ts +13 -0
- package/dist/src/docs/preview-source.js +58 -0
- package/dist/src/docs/preview.d.ts +32 -0
- package/dist/src/docs/preview.js +279 -0
- package/fragments/agents/AGENTS.md.hbs +27 -27
- package/fragments/base/README.md.hbs +5 -2
- package/fragments/base/index.ts.hbs +17 -30
- package/fragments/base/package.json.hbs +4 -1
- package/fragments/service/api/openapi.json.hbs +1 -1
- package/fragments/service/docs/docs.yaml.hbs +10 -0
- package/fragments/service/docs/pages/getting-started.md.hbs +6 -9
- package/package.json +19 -3
- package/skills/mesh/SKILL.md +2 -2
- 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
|
|
22
|
-
guides
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
(``): 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
|
|
8
|
-
|
|
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
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// role
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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",
|