@mesh-tech/mesh-cli 0.36.0 → 0.38.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-AQJ5TQES.js +48 -0
- package/dist/bin/mesh.auth-preflight-AQJ5TQES.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-2QIVBQX5.js +647 -0
- package/dist/bin/mesh.chunk-2QIVBQX5.js.map +6 -0
- package/dist/bin/mesh.chunk-2YRWJH5X.js +1747 -0
- package/dist/bin/mesh.chunk-2YRWJH5X.js.map +6 -0
- package/dist/bin/mesh.chunk-BF2SH6XT.js +303 -0
- package/dist/bin/mesh.chunk-BF2SH6XT.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-HPYZWPRM.js +1146 -0
- package/dist/bin/mesh.chunk-HPYZWPRM.js.map +6 -0
- package/dist/bin/mesh.chunk-I6NXLXGC.js +7715 -0
- package/dist/bin/mesh.chunk-I6NXLXGC.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-IU2IWH6O.js +835 -0
- package/dist/bin/mesh.chunk-IU2IWH6O.js.map +6 -0
- package/dist/bin/mesh.chunk-KN4BZOYH.js +187 -0
- package/dist/bin/mesh.chunk-KN4BZOYH.js.map +6 -0
- package/dist/bin/mesh.chunk-LMYPTJ2F.js +10092 -0
- package/dist/bin/mesh.chunk-LMYPTJ2F.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-TZAGJTKU.js +217 -0
- package/dist/bin/mesh.chunk-TZAGJTKU.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.cli-reference-H3TOPIET.js +158 -0
- package/dist/bin/mesh.cli-reference-H3TOPIET.js.map +6 -0
- package/dist/bin/mesh.convert-7N2H2SVK.js +274 -0
- package/dist/bin/mesh.convert-7N2H2SVK.js.map +6 -0
- package/dist/bin/mesh.create-app-NGXACFAU.js +39 -0
- package/dist/bin/mesh.create-app-NGXACFAU.js.map +6 -0
- package/dist/bin/mesh.dev-token-server-YP2VBP2S.js +61 -0
- package/dist/bin/mesh.dev-token-server-YP2VBP2S.js.map +6 -0
- package/dist/bin/mesh.js +20 -26418
- package/dist/bin/mesh.js.map +3 -3
- package/dist/bin/mesh.login-7SIAKFXJ.js +91 -0
- package/dist/bin/mesh.login-7SIAKFXJ.js.map +6 -0
- package/dist/bin/mesh.platform-setup-CJXE3BCP.js +37 -0
- package/dist/bin/mesh.platform-setup-CJXE3BCP.js.map +6 -0
- package/dist/bin/mesh.preview-RJGTCTY3.js +2410 -0
- package/dist/bin/mesh.preview-RJGTCTY3.js.map +6 -0
- package/dist/bin/mesh.program-BIDQBSSI.js +23 -0
- package/dist/bin/mesh.program-BIDQBSSI.js.map +6 -0
- package/dist/bin/mesh.registry-7P4MGXEK.js +47 -0
- package/dist/bin/mesh.registry-7P4MGXEK.js.map +6 -0
- package/dist/bin/mesh.registry-identity-JW4KO7N7.js +42 -0
- package/dist/bin/mesh.registry-identity-JW4KO7N7.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-MPQFIEMH.js +454 -0
- package/dist/bin/mesh.start-MPQFIEMH.js.map +6 -0
- package/dist/bin/mesh.wizard-ZJE4S6PC.js +500 -0
- package/dist/bin/mesh.wizard-ZJE4S6PC.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/create-app.d.ts +0 -1
- package/dist/src/commands/create-app.js +0 -4
- 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/dist/src/utils/scaffold-versions.d.ts +0 -1
- package/dist/src/utils/scaffold-versions.js +0 -1
- package/fragments/agents/AGENTS.md.hbs +27 -27
- package/fragments/base/README.md.hbs +5 -2
- package/fragments/base/index.ts.hbs +19 -33
- package/fragments/base/package.json.hbs +4 -2
- 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,1146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logError,
|
|
3
|
+
logInfo,
|
|
4
|
+
logSuccess,
|
|
5
|
+
logWarn
|
|
6
|
+
} from "./mesh.chunk-WFFDF346.js";
|
|
7
|
+
|
|
8
|
+
// packages/mesh-cli/src/docs/discover.ts
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
// packages/mesh-cli/src/docs/schema.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var RESERVED_SEGMENTS = [
|
|
15
|
+
"plans",
|
|
16
|
+
"designs",
|
|
17
|
+
"proposals",
|
|
18
|
+
"incidents",
|
|
19
|
+
"handoffs",
|
|
20
|
+
"reviews",
|
|
21
|
+
"runbooks",
|
|
22
|
+
"hran",
|
|
23
|
+
"superpowers",
|
|
24
|
+
"templates",
|
|
25
|
+
"archive",
|
|
26
|
+
"internal",
|
|
27
|
+
"drafts",
|
|
28
|
+
"node_modules",
|
|
29
|
+
"__tests__",
|
|
30
|
+
".claude",
|
|
31
|
+
".pi"
|
|
32
|
+
];
|
|
33
|
+
var docsJsonSchema = z.object({
|
|
34
|
+
/** Section label in the nav, e.g. "Building apps". */
|
|
35
|
+
title: z.string().min(1),
|
|
36
|
+
/**
|
|
37
|
+
* Section position in the nav. Ties break alphabetically by title, so two
|
|
38
|
+
* sections can never fight over a slot nondeterministically.
|
|
39
|
+
*/
|
|
40
|
+
order: z.number().int().default(50),
|
|
41
|
+
/**
|
|
42
|
+
* Route prefix for the section. Defaults to the slugified title. The
|
|
43
|
+
* empty string is legal and means "this section lives at the site root" —
|
|
44
|
+
* exactly one root should do that (the front door, `docs/portal`).
|
|
45
|
+
*/
|
|
46
|
+
slug: z.string().regex(
|
|
47
|
+
/^$|^[a-z0-9]+(?:-[a-z0-9]+)*$/,
|
|
48
|
+
'slug must be lower-kebab-case or ""'
|
|
49
|
+
).optional(),
|
|
50
|
+
/**
|
|
51
|
+
* Escape hatch: explicit repo-relative paths to publish that live outside
|
|
52
|
+
* any natural doc root (a package README, a repo-root policy file). Expected
|
|
53
|
+
* to hold a handful of entries across the whole repo — adding one is a
|
|
54
|
+
* deliberate, reviewable act. Paths under a reserved segment are refused.
|
|
55
|
+
*/
|
|
56
|
+
include: z.array(z.string().min(1)).default([])
|
|
57
|
+
}).strict();
|
|
58
|
+
function isReservedSegment(segment) {
|
|
59
|
+
const lowered = segment.toLowerCase();
|
|
60
|
+
return RESERVED_SEGMENTS.some((reserved) => reserved === lowered);
|
|
61
|
+
}
|
|
62
|
+
function hasReservedSegment(relPath) {
|
|
63
|
+
return relPath.split("/").some(isReservedSegment);
|
|
64
|
+
}
|
|
65
|
+
function slugify(value) {
|
|
66
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "index";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// packages/mesh-cli/src/docs/discover.ts
|
|
70
|
+
var ROOT_WALK_SKIP = /* @__PURE__ */ new Set([
|
|
71
|
+
".git",
|
|
72
|
+
"node_modules",
|
|
73
|
+
"dist",
|
|
74
|
+
".turbo",
|
|
75
|
+
".next",
|
|
76
|
+
".deploy",
|
|
77
|
+
".generated",
|
|
78
|
+
"coverage",
|
|
79
|
+
".portal",
|
|
80
|
+
".vercel",
|
|
81
|
+
".wrangler"
|
|
82
|
+
]);
|
|
83
|
+
function routePathFor(relPath) {
|
|
84
|
+
const withoutExt = relPath.replace(/\.md$/i, "");
|
|
85
|
+
const parts = withoutExt.split("/").map(slugify);
|
|
86
|
+
if (parts[parts.length - 1] === "index") parts.pop();
|
|
87
|
+
return parts.join("/");
|
|
88
|
+
}
|
|
89
|
+
function walkMarkdown(absDir, relDir, errors, rootDir) {
|
|
90
|
+
const out = [];
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
94
|
+
} catch {
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
99
|
+
if (hasReservedSegment(entry.name)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const abs = path.join(absDir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
105
|
+
out.push(...walkMarkdown(abs, rel, errors, rootDir));
|
|
106
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
107
|
+
if (entry.name.toLowerCase() === "readme.md") continue;
|
|
108
|
+
out.push(rel);
|
|
109
|
+
} else if (entry.isFile() && entry.name === "docs.json" && relDir !== "") {
|
|
110
|
+
errors.push(
|
|
111
|
+
`nested doc root: ${rootDir}/.../${rel} \u2014 a docs.json inside another doc root is ambiguous; give it its own sibling root or remove it`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function discoverDocRoots(repoRoot) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
const roots = [];
|
|
120
|
+
const findRoots = (absDir, relDir, insideReserved) => {
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const hasDocsJson = entries.some(
|
|
128
|
+
(e) => e.isFile() && e.name === "docs.json"
|
|
129
|
+
);
|
|
130
|
+
if (hasDocsJson) {
|
|
131
|
+
const rel = relDir || ".";
|
|
132
|
+
if (insideReserved || relDir && hasReservedSegment(relDir)) {
|
|
133
|
+
errors.push(
|
|
134
|
+
`doc root ${rel}/ is under a reserved segment \u2014 it publishes nothing. Move the content out of the reserved directory instead.`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
const root = loadRoot(absDir, rel, errors);
|
|
138
|
+
if (root) roots.push(root);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.isDirectory()) continue;
|
|
144
|
+
if (ROOT_WALK_SKIP.has(entry.name)) continue;
|
|
145
|
+
if (entry.name.startsWith(".")) continue;
|
|
146
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
147
|
+
findRoots(
|
|
148
|
+
path.join(absDir, entry.name),
|
|
149
|
+
rel,
|
|
150
|
+
insideReserved || hasReservedSegment(entry.name)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
findRoots(repoRoot, "", false);
|
|
155
|
+
const pages = [];
|
|
156
|
+
for (const root of roots) {
|
|
157
|
+
const absRoot = path.join(repoRoot, root.dir);
|
|
158
|
+
const relFiles = walkMarkdown(absRoot, "", errors, root.dir);
|
|
159
|
+
for (const relPath of relFiles.sort()) {
|
|
160
|
+
pages.push({
|
|
161
|
+
source: root.dir === "." ? relPath : `${root.dir}/${relPath}`,
|
|
162
|
+
root,
|
|
163
|
+
routePath: routePathFor(relPath),
|
|
164
|
+
included: false
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
for (const includePath of root.config.include) {
|
|
168
|
+
const normalized = includePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
169
|
+
if (hasReservedSegment(normalized)) {
|
|
170
|
+
errors.push(
|
|
171
|
+
`${root.dir}/docs.json includes ${normalized}, which is under a reserved segment \u2014 include[] cannot pull internal material into the site`
|
|
172
|
+
);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!normalized.toLowerCase().endsWith(".md")) {
|
|
176
|
+
errors.push(
|
|
177
|
+
`${root.dir}/docs.json includes ${normalized}, which is not a markdown file`
|
|
178
|
+
);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (!existsSync(path.join(repoRoot, normalized))) {
|
|
182
|
+
errors.push(
|
|
183
|
+
`${root.dir}/docs.json includes ${normalized}, which does not exist`
|
|
184
|
+
);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (pages.some((p) => p.source === normalized)) continue;
|
|
188
|
+
const stem = path.posix.basename(normalized).replace(/\.md$/i, "");
|
|
189
|
+
pages.push({
|
|
190
|
+
source: normalized,
|
|
191
|
+
root,
|
|
192
|
+
// An included README is the section's index page — "a package README
|
|
193
|
+
// is its natural overview page". Anything else routes by its stem.
|
|
194
|
+
routePath: stem.toLowerCase() === "readme" ? "" : slugify(stem),
|
|
195
|
+
included: true
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const seenRoutes = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const page of pages) {
|
|
201
|
+
const route = [page.root.slug, page.routePath].filter(Boolean).join("/");
|
|
202
|
+
const existing = seenRoutes.get(route);
|
|
203
|
+
if (existing) {
|
|
204
|
+
errors.push(
|
|
205
|
+
`route collision: ${page.source} and ${existing} both map to /${route || "(site root)"}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
seenRoutes.set(route, page.source);
|
|
209
|
+
}
|
|
210
|
+
roots.sort(
|
|
211
|
+
(a, b) => a.config.order - b.config.order || a.config.title.localeCompare(b.config.title)
|
|
212
|
+
);
|
|
213
|
+
return { roots, pages, errors };
|
|
214
|
+
}
|
|
215
|
+
function loadRoot(absDir, relDir, errors) {
|
|
216
|
+
const configPath = path.join(absDir, "docs.json");
|
|
217
|
+
let raw;
|
|
218
|
+
try {
|
|
219
|
+
raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
errors.push(
|
|
222
|
+
`${relDir}/docs.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
223
|
+
);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const parsed = docsJsonSchema.safeParse(raw);
|
|
227
|
+
if (!parsed.success) {
|
|
228
|
+
for (const issue of parsed.error.issues) {
|
|
229
|
+
errors.push(
|
|
230
|
+
`${relDir}/docs.json: ${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const config = parsed.data;
|
|
236
|
+
const slug = config.slug ?? slugify(config.title);
|
|
237
|
+
return { dir: relDir, config, slug };
|
|
238
|
+
}
|
|
239
|
+
function pathKind(candidate) {
|
|
240
|
+
try {
|
|
241
|
+
const stat = statSync(candidate);
|
|
242
|
+
return stat.isDirectory() ? "dir" : "file";
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// packages/mesh-cli/src/docs/portal.ts
|
|
249
|
+
import { spawn } from "node:child_process";
|
|
250
|
+
import {
|
|
251
|
+
appendFileSync,
|
|
252
|
+
existsSync as existsSync2,
|
|
253
|
+
mkdirSync as mkdirSync2,
|
|
254
|
+
mkdtempSync,
|
|
255
|
+
readFileSync as readFileSync3,
|
|
256
|
+
writeFileSync as writeFileSync2
|
|
257
|
+
} from "node:fs";
|
|
258
|
+
import { tmpdir } from "node:os";
|
|
259
|
+
import path3 from "node:path";
|
|
260
|
+
|
|
261
|
+
// packages/mesh-cli/src/docs/assemble.ts
|
|
262
|
+
import { execFileSync } from "node:child_process";
|
|
263
|
+
import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
264
|
+
import path2 from "node:path";
|
|
265
|
+
import { parse as parseYaml } from "yaml";
|
|
266
|
+
var DEFAULT_REPO_URL = "https://github.com/mesh-tech/mesh-platform";
|
|
267
|
+
var DEFAULT_IGNORE_LINK_PREFIXES = ["node_modules/"];
|
|
268
|
+
function splitFrontMatter(markdown) {
|
|
269
|
+
const normalized = markdown.replace(/^\uFEFF/, "");
|
|
270
|
+
if (!normalized.startsWith("---\n") && !normalized.startsWith("---\r\n")) {
|
|
271
|
+
return { data: {}, body: markdown, hasFrontMatter: false };
|
|
272
|
+
}
|
|
273
|
+
const end = normalized.indexOf("\n---", 4);
|
|
274
|
+
if (end === -1) return { data: {}, body: markdown, hasFrontMatter: false };
|
|
275
|
+
const rawBlock = normalized.slice(4, end);
|
|
276
|
+
const bodyStart = normalized.indexOf("\n", end + 1);
|
|
277
|
+
const body = (bodyStart === -1 ? "" : normalized.slice(bodyStart + 1)).replace(/^\r?\n/, "");
|
|
278
|
+
try {
|
|
279
|
+
const data = parseYaml(rawBlock);
|
|
280
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
281
|
+
return {
|
|
282
|
+
data,
|
|
283
|
+
body,
|
|
284
|
+
hasFrontMatter: true
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
return { data: {}, body, hasFrontMatter: true };
|
|
290
|
+
}
|
|
291
|
+
function joinFrontMatter(data, body) {
|
|
292
|
+
const lines = Object.entries(data).map(([key, value]) => {
|
|
293
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
294
|
+
return `${key}: ${value}`;
|
|
295
|
+
const text = String(value);
|
|
296
|
+
return /^[\w .,'’()/-]+$/.test(text) ? `${key}: ${text}` : `${key}: ${JSON.stringify(text)}`;
|
|
297
|
+
});
|
|
298
|
+
return `---
|
|
299
|
+
${lines.join("\n")}
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
${body.replace(/^\s*\n/, "")}`;
|
|
303
|
+
}
|
|
304
|
+
function firstHeading(body) {
|
|
305
|
+
let inFence = false;
|
|
306
|
+
for (const line of body.split("\n")) {
|
|
307
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
308
|
+
inFence = !inFence;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (inFence) continue;
|
|
312
|
+
const match = /^#\s+(.+?)\s*#*\s*$/.exec(line);
|
|
313
|
+
if (match) return match[1].trim();
|
|
314
|
+
}
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
function titleizeStem(stem) {
|
|
318
|
+
const words = stem.replace(/[-_]+/g, " ").trim();
|
|
319
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
320
|
+
}
|
|
321
|
+
function deriveTitle(frontMatter, body, source) {
|
|
322
|
+
const fmTitle = frontMatter.title;
|
|
323
|
+
if (typeof fmTitle === "string" && fmTitle.trim()) return fmTitle.trim();
|
|
324
|
+
const heading = firstHeading(body);
|
|
325
|
+
if (heading) return heading;
|
|
326
|
+
const stem = path2.posix.basename(source).replace(/\.md$/i, "");
|
|
327
|
+
return titleizeStem(stem);
|
|
328
|
+
}
|
|
329
|
+
function scanLinks(markdown) {
|
|
330
|
+
const hits = [];
|
|
331
|
+
const lines = markdown.split("\n");
|
|
332
|
+
let offset = 0;
|
|
333
|
+
let inFence = null;
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
const fenceMatch = /^(\s{0,3})(`{3,}|~{3,})/.exec(line);
|
|
336
|
+
if (fenceMatch) {
|
|
337
|
+
if (!inFence) {
|
|
338
|
+
inFence = { marker: fenceMatch[2][0] };
|
|
339
|
+
} else if (fenceMatch[2].startsWith(inFence.marker)) {
|
|
340
|
+
inFence = null;
|
|
341
|
+
}
|
|
342
|
+
offset += line.length + 1;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (inFence) {
|
|
346
|
+
offset += line.length + 1;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const refDef = /^ {0,3}\[[^\]]+\]:\s*(\S+)/.exec(line);
|
|
350
|
+
if (refDef) {
|
|
351
|
+
const href = refDef[1];
|
|
352
|
+
const start = offset + line.indexOf(href);
|
|
353
|
+
hits.push({ start, end: start + href.length, href });
|
|
354
|
+
offset += line.length + 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
let i = 0;
|
|
358
|
+
while (i < line.length) {
|
|
359
|
+
const ch = line[i];
|
|
360
|
+
if (ch === "`") {
|
|
361
|
+
const run = /^`+/.exec(line.slice(i))[0];
|
|
362
|
+
const close = line.indexOf(run, i + run.length);
|
|
363
|
+
i = close === -1 ? line.length : close + run.length;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (ch === "<") {
|
|
367
|
+
const close = line.indexOf(">", i);
|
|
368
|
+
i = close === -1 ? line.length : close + 1;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (ch === "!" && line[i + 1] === "[") {
|
|
372
|
+
i = scanBracketLink(line, i + 1, offset, hits);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (ch === "[") {
|
|
376
|
+
i = scanBracketLink(line, i, offset, hits);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
i++;
|
|
380
|
+
}
|
|
381
|
+
offset += line.length + 1;
|
|
382
|
+
}
|
|
383
|
+
return hits;
|
|
384
|
+
}
|
|
385
|
+
function scanBracketLink(line, open, lineOffset, hits) {
|
|
386
|
+
const closeBracket = line.indexOf("]", open);
|
|
387
|
+
if (closeBracket === -1) return line.length;
|
|
388
|
+
if (line[closeBracket + 1] !== "(") return closeBracket + 1;
|
|
389
|
+
let j = closeBracket + 2;
|
|
390
|
+
while (j < line.length && /\s/.test(line[j])) j++;
|
|
391
|
+
if (line[j] === "<") {
|
|
392
|
+
const end = line.indexOf(">", j);
|
|
393
|
+
if (end === -1) return line.length;
|
|
394
|
+
hits.push({
|
|
395
|
+
start: lineOffset + j + 1,
|
|
396
|
+
end: lineOffset + end,
|
|
397
|
+
href: line.slice(j + 1, end)
|
|
398
|
+
});
|
|
399
|
+
return end + 1;
|
|
400
|
+
}
|
|
401
|
+
const closeParen = line.indexOf(")", j);
|
|
402
|
+
if (closeParen === -1) return line.length;
|
|
403
|
+
const inner = line.slice(j, closeParen).trim();
|
|
404
|
+
const space = inner.search(/\s/);
|
|
405
|
+
const href = space === -1 ? inner : inner.slice(0, space);
|
|
406
|
+
if (href) {
|
|
407
|
+
const start = lineOffset + line.indexOf(href, j);
|
|
408
|
+
hits.push({ start, end: start + href.length, href });
|
|
409
|
+
}
|
|
410
|
+
return closeParen + 1;
|
|
411
|
+
}
|
|
412
|
+
function stripHtmlComments(body) {
|
|
413
|
+
const lines = body.split("\n");
|
|
414
|
+
const out = [];
|
|
415
|
+
let inFence = false;
|
|
416
|
+
let inComment = false;
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
419
|
+
if (!inComment) inFence = !inFence;
|
|
420
|
+
if (!inComment) out.push(line);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (inFence) {
|
|
424
|
+
out.push(line);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
let rest = line;
|
|
428
|
+
let rebuilt = "";
|
|
429
|
+
while (rest.length > 0) {
|
|
430
|
+
if (inComment) {
|
|
431
|
+
const end = rest.indexOf("-->");
|
|
432
|
+
if (end === -1) {
|
|
433
|
+
rest = "";
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
inComment = false;
|
|
437
|
+
rest = rest.slice(end + 3);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const start = rest.indexOf("<!--");
|
|
441
|
+
if (start === -1) {
|
|
442
|
+
rebuilt += rest;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
rebuilt += rest.slice(0, start);
|
|
446
|
+
inComment = true;
|
|
447
|
+
rest = rest.slice(start + 4);
|
|
448
|
+
}
|
|
449
|
+
if (rebuilt.trim() !== "" || !inComment)
|
|
450
|
+
out.push(rebuilt.replace(/\s+$/, ""));
|
|
451
|
+
}
|
|
452
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
453
|
+
}
|
|
454
|
+
function isAbsoluteHref(href) {
|
|
455
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href);
|
|
456
|
+
}
|
|
457
|
+
function normalizeRepoPath(value) {
|
|
458
|
+
return path2.posix.normalize(value.replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
459
|
+
}
|
|
460
|
+
function splitHash(href) {
|
|
461
|
+
const index = href.indexOf("#");
|
|
462
|
+
return index === -1 ? [href, ""] : [href.slice(0, index), href.slice(index + 1)];
|
|
463
|
+
}
|
|
464
|
+
function resolveLink(args) {
|
|
465
|
+
const { href, pageRoute: pageRoute2, sourceDir, sourceIndex, routes, repoUrl } = args;
|
|
466
|
+
if (href.startsWith("#") || isAbsoluteHref(href)) return { href };
|
|
467
|
+
if (args.ignoreLinkPrefixes.some((prefix) => href.startsWith(prefix)))
|
|
468
|
+
return { href };
|
|
469
|
+
const [rawTarget, rawHash] = splitHash(href);
|
|
470
|
+
const hash = rawHash ? `#${rawHash}` : "";
|
|
471
|
+
if (rawTarget === "") return { href };
|
|
472
|
+
if (rawTarget.startsWith("/")) {
|
|
473
|
+
const route = rawTarget.replace(/\/$/, "") || "/";
|
|
474
|
+
return routes.has(route) ? { href: `${route}${hash}` } : { href, issue: { kind: "broken", page: pageRoute2, href } };
|
|
475
|
+
}
|
|
476
|
+
const repoPath = normalizeRepoPath(path2.posix.join(sourceDir, rawTarget));
|
|
477
|
+
const page = sourceIndex.get(repoPath);
|
|
478
|
+
if (page) return { href: `${page.route}${hash}` };
|
|
479
|
+
for (const candidate of [`${repoPath}/index.md`]) {
|
|
480
|
+
const indexPage = sourceIndex.get(normalizeRepoPath(candidate));
|
|
481
|
+
if (indexPage) return { href: `${indexPage.route}${hash}` };
|
|
482
|
+
}
|
|
483
|
+
const kind = args.pathKind(repoPath);
|
|
484
|
+
if (kind !== null) {
|
|
485
|
+
const base = repoUrl.replace(/\/$/, "").replace(/\/(blob|tree)$/, "");
|
|
486
|
+
return {
|
|
487
|
+
href: `${base}/${kind === "dir" ? "tree" : "blob"}/main/${repoPath}${hash}`,
|
|
488
|
+
issue: { kind: "external", page: pageRoute2, href }
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { href, issue: { kind: "broken", page: pageRoute2, href } };
|
|
492
|
+
}
|
|
493
|
+
function rewriteLinks(args) {
|
|
494
|
+
const issues = [];
|
|
495
|
+
const hits = scanLinks(args.body);
|
|
496
|
+
if (hits.length === 0) return { body: args.body, issues };
|
|
497
|
+
const sourceDir = path2.posix.dirname(args.page.source);
|
|
498
|
+
let out = "";
|
|
499
|
+
let cursor = 0;
|
|
500
|
+
for (const hit of hits) {
|
|
501
|
+
const resolution = resolveLink({
|
|
502
|
+
href: hit.href,
|
|
503
|
+
pageRoute: args.page.route,
|
|
504
|
+
sourceDir,
|
|
505
|
+
sourceIndex: args.sourceIndex,
|
|
506
|
+
routes: args.routes,
|
|
507
|
+
ignoreLinkPrefixes: args.ignoreLinkPrefixes,
|
|
508
|
+
pathKind: args.pathKind,
|
|
509
|
+
repoUrl: args.repoUrl
|
|
510
|
+
});
|
|
511
|
+
out += args.body.slice(cursor, hit.start) + resolution.href;
|
|
512
|
+
cursor = hit.end;
|
|
513
|
+
if (resolution.issue) issues.push(resolution.issue);
|
|
514
|
+
}
|
|
515
|
+
out += args.body.slice(cursor);
|
|
516
|
+
return { body: out, issues };
|
|
517
|
+
}
|
|
518
|
+
function pageRoute(page) {
|
|
519
|
+
const joined = [page.root.slug, page.routePath].filter(Boolean).join("/");
|
|
520
|
+
return `/${joined}`.replace(/\/$/, "") || "/";
|
|
521
|
+
}
|
|
522
|
+
function pageFilePath(route) {
|
|
523
|
+
return route === "/" ? "index.md" : `${route.slice(1)}.md`;
|
|
524
|
+
}
|
|
525
|
+
function assemblePortal(options) {
|
|
526
|
+
const { repoRoot, discovery, outDir } = options;
|
|
527
|
+
const repoUrl = options.repoUrl ?? DEFAULT_REPO_URL;
|
|
528
|
+
const ignoreLinkPrefixes = options.ignoreLinkPrefixes ?? DEFAULT_IGNORE_LINK_PREFIXES;
|
|
529
|
+
const pages = discovery.pages.map((page) => {
|
|
530
|
+
const route = pageRoute(page);
|
|
531
|
+
return {
|
|
532
|
+
source: page.source,
|
|
533
|
+
route,
|
|
534
|
+
filePath: pageFilePath(route),
|
|
535
|
+
title: "",
|
|
536
|
+
sectionTitle: page.root.config.title,
|
|
537
|
+
sectionOrder: page.root.config.order,
|
|
538
|
+
sectionSlug: page.root.slug
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
const sourceIndex = new Map(
|
|
542
|
+
pages.map((p) => [normalizeRepoPath(p.source), p])
|
|
543
|
+
);
|
|
544
|
+
const routes = new Set(pages.map((p) => p.route));
|
|
545
|
+
const brokenLinks = [];
|
|
546
|
+
const externalLinks = [];
|
|
547
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
548
|
+
mkdirSync(outDir, { recursive: true });
|
|
549
|
+
for (const page of pages) {
|
|
550
|
+
const raw = readFileSync2(path2.join(repoRoot, page.source), "utf-8");
|
|
551
|
+
const frontMatter = splitFrontMatter(raw);
|
|
552
|
+
const title = deriveTitle(frontMatter.data, frontMatter.body, page.source);
|
|
553
|
+
page.title = title;
|
|
554
|
+
const order = frontMatter.data.order;
|
|
555
|
+
if (typeof order === "number" && Number.isFinite(order)) page.order = order;
|
|
556
|
+
const rewritten = rewriteLinks({
|
|
557
|
+
body: stripHtmlComments(frontMatter.body),
|
|
558
|
+
page,
|
|
559
|
+
sourceIndex,
|
|
560
|
+
routes,
|
|
561
|
+
ignoreLinkPrefixes,
|
|
562
|
+
pathKind: (rel) => pathKind(path2.join(repoRoot, rel)),
|
|
563
|
+
repoUrl
|
|
564
|
+
});
|
|
565
|
+
for (const issue of rewritten.issues) {
|
|
566
|
+
(issue.kind === "broken" ? brokenLinks : externalLinks).push(issue);
|
|
567
|
+
}
|
|
568
|
+
const outData = { ...frontMatter.data, title };
|
|
569
|
+
const blobBase = repoUrl.replace(/\/$/, "").replace(/\/(blob|tree)$/, "");
|
|
570
|
+
const sourceNote = `
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
<sub>Source: [\`${page.source}\`](${blobBase}/blob/main/${page.source}) \u2014 edit that file, not this page.</sub>
|
|
575
|
+
`;
|
|
576
|
+
const outFile = path2.join(outDir, page.filePath);
|
|
577
|
+
mkdirSync(path2.dirname(outFile), { recursive: true });
|
|
578
|
+
writeFileSync(
|
|
579
|
+
outFile,
|
|
580
|
+
joinFrontMatter(outData, rewritten.body.trimEnd() + sourceNote)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const navigation = buildNavigation(pages);
|
|
584
|
+
const reservedViolations = pages.map((p) => p.filePath).filter((filePath) => hasReservedSegment(filePath));
|
|
585
|
+
const publishManifest = {
|
|
586
|
+
version: 1,
|
|
587
|
+
count: pages.length,
|
|
588
|
+
sources: pages.map((p) => p.source).sort()
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
outDir,
|
|
592
|
+
pages,
|
|
593
|
+
navigation,
|
|
594
|
+
brokenLinks,
|
|
595
|
+
externalLinks,
|
|
596
|
+
publishManifest,
|
|
597
|
+
reservedViolations
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function titleizeDir(name) {
|
|
601
|
+
return titleizeStem(name);
|
|
602
|
+
}
|
|
603
|
+
function buildNavigation(pages) {
|
|
604
|
+
const sections = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const page of pages) {
|
|
606
|
+
const key = page.sectionSlug;
|
|
607
|
+
if (!sections.has(key)) {
|
|
608
|
+
sections.set(key, {
|
|
609
|
+
title: page.sectionTitle,
|
|
610
|
+
order: page.sectionOrder,
|
|
611
|
+
slug: page.sectionSlug,
|
|
612
|
+
pages: []
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
sections.get(key).pages.push(page);
|
|
616
|
+
}
|
|
617
|
+
const sortedSections = [...sections.values()].sort(
|
|
618
|
+
(a, b) => a.order - b.order || a.title.localeCompare(b.title)
|
|
619
|
+
);
|
|
620
|
+
const sortPages = (list) => [...list].sort(
|
|
621
|
+
(a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER) || a.filePath.localeCompare(b.filePath)
|
|
622
|
+
);
|
|
623
|
+
const docRef = (page) => {
|
|
624
|
+
const file = page.filePath.replace(/\.md$/, "");
|
|
625
|
+
return page.route === `/${file}` ? file : { type: "doc", file, path: page.route };
|
|
626
|
+
};
|
|
627
|
+
const items = [];
|
|
628
|
+
for (const section of sortedSections) {
|
|
629
|
+
const indexPage = section.pages.find(
|
|
630
|
+
(p) => p.route === `/${section.slug}` || section.slug === "" && p.route === "/"
|
|
631
|
+
);
|
|
632
|
+
const rest = section.pages.filter((p) => p !== indexPage);
|
|
633
|
+
const rootPages = [];
|
|
634
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
635
|
+
for (const page of rest) {
|
|
636
|
+
const rel = page.route.replace(/^\//, "");
|
|
637
|
+
const sectionPrefix = section.slug ? `${section.slug}/` : "";
|
|
638
|
+
const relParts = (sectionPrefix && rel.startsWith(sectionPrefix) ? rel.slice(sectionPrefix.length) : rel).split("/").filter(Boolean);
|
|
639
|
+
if (relParts.length <= 1) {
|
|
640
|
+
rootPages.push(page);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const dirKey = relParts.slice(0, -1).join("/");
|
|
644
|
+
if (!dirs.has(dirKey)) {
|
|
645
|
+
dirs.set(dirKey, {
|
|
646
|
+
label: titleizeDir(relParts[relParts.length - 2]),
|
|
647
|
+
routePath: relParts.slice(0, -1).join("/"),
|
|
648
|
+
pages: [],
|
|
649
|
+
dirs: /* @__PURE__ */ new Map()
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
dirs.get(dirKey).pages.push(page);
|
|
653
|
+
}
|
|
654
|
+
const byRoute = new Map(section.pages.map((p) => [p.route, p]));
|
|
655
|
+
const childItems = sortPages(rootPages).map(docRef);
|
|
656
|
+
for (const dir of [...dirs.values()].sort(
|
|
657
|
+
(a, b) => a.routePath.localeCompare(b.routePath)
|
|
658
|
+
)) {
|
|
659
|
+
const dirRoute = `/${[section.slug, dir.routePath].filter(Boolean).join("/")}`.replace(
|
|
660
|
+
/\/$/,
|
|
661
|
+
""
|
|
662
|
+
) || "/";
|
|
663
|
+
const dirIndex = byRoute.get(dirRoute);
|
|
664
|
+
const grandchildren = sortPages(
|
|
665
|
+
dir.pages.filter((p) => p !== dirIndex)
|
|
666
|
+
).map(docRef);
|
|
667
|
+
if (grandchildren.length === 0 && dirIndex) {
|
|
668
|
+
childItems.push(docRef(dirIndex));
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
childItems.push({
|
|
672
|
+
type: "category",
|
|
673
|
+
label: dir.label,
|
|
674
|
+
...dirIndex ? { link: docRef(dirIndex) } : {},
|
|
675
|
+
items: grandchildren
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
items.push({
|
|
679
|
+
type: "category",
|
|
680
|
+
label: section.title,
|
|
681
|
+
collapsible: false,
|
|
682
|
+
...indexPage ? { link: docRef(indexPage) } : {},
|
|
683
|
+
items: childItems
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return [
|
|
687
|
+
{
|
|
688
|
+
type: "category",
|
|
689
|
+
label: "Documentation",
|
|
690
|
+
link: { type: "doc", file: "index", path: "/" },
|
|
691
|
+
items
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
}
|
|
695
|
+
function renderZudokuConfig(args) {
|
|
696
|
+
const versionLine = args.baseline ? `Documents @mesh-tech/* ${args.baseline} \xB7 built from ${args.commit ?? "unknown"}` : `Local build \xB7 ${args.commit ?? "unknown"}`;
|
|
697
|
+
const config = {
|
|
698
|
+
metadata: {
|
|
699
|
+
title: args.title,
|
|
700
|
+
description: args.description
|
|
701
|
+
},
|
|
702
|
+
site: {
|
|
703
|
+
title: args.title,
|
|
704
|
+
logoUrl: "/",
|
|
705
|
+
footer: {
|
|
706
|
+
copyright: `${versionLine} \xB7 /version.json`
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
theme: "THEME_IMPORT",
|
|
710
|
+
navigation: args.navigation,
|
|
711
|
+
docs: {
|
|
712
|
+
files: ["/content/**/*.{md,mdx}"],
|
|
713
|
+
defaultOptions: {
|
|
714
|
+
toc: true,
|
|
715
|
+
showLastModified: false
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
redirects: []
|
|
719
|
+
// Search is Zudoku's built-in Pagefind index — no config needed.
|
|
720
|
+
};
|
|
721
|
+
const json = JSON.stringify(config, null, 2).replace(
|
|
722
|
+
`"THEME_IMPORT"`,
|
|
723
|
+
"portalTheme"
|
|
724
|
+
);
|
|
725
|
+
return `/**
|
|
726
|
+
* GENERATED by \`mesh docs portal\` \u2014 do not edit.
|
|
727
|
+
*
|
|
728
|
+
* The navigation array is derived from the repo's docs.json roots and the
|
|
729
|
+
* markdown tree beneath them. To change what the site publishes, change the
|
|
730
|
+
* tree; to change section labels or order, change the root's docs.json.
|
|
731
|
+
* Regenerate: pnpm exec mesh docs portal --assemble-only
|
|
732
|
+
*/
|
|
733
|
+
import type { ZudokuConfig } from "zudoku";
|
|
734
|
+
import { portalTheme } from "./theme.js";
|
|
735
|
+
|
|
736
|
+
const config: ZudokuConfig = ${json};
|
|
737
|
+
|
|
738
|
+
export default config;
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
function renderVersionJson(args) {
|
|
742
|
+
return `${JSON.stringify(
|
|
743
|
+
{
|
|
744
|
+
meshBaseline: args.baseline ?? null,
|
|
745
|
+
commit: args.commit,
|
|
746
|
+
builtAt: args.builtAt
|
|
747
|
+
},
|
|
748
|
+
null,
|
|
749
|
+
2
|
|
750
|
+
)}
|
|
751
|
+
`;
|
|
752
|
+
}
|
|
753
|
+
function currentCommit(repoRoot) {
|
|
754
|
+
try {
|
|
755
|
+
return execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
756
|
+
cwd: repoRoot,
|
|
757
|
+
encoding: "utf-8"
|
|
758
|
+
}).trim();
|
|
759
|
+
} catch {
|
|
760
|
+
return "unknown";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function currentBaseline(repoRoot) {
|
|
764
|
+
try {
|
|
765
|
+
const pkg = JSON.parse(
|
|
766
|
+
readFileSync2(path2.join(repoRoot, "packages/app-kit/package.json"), "utf-8")
|
|
767
|
+
);
|
|
768
|
+
return pkg.version;
|
|
769
|
+
} catch {
|
|
770
|
+
return void 0;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function publishSetAtRef(repoRoot, ref) {
|
|
774
|
+
const git = (gitArgs) => execFileSync("git", gitArgs, {
|
|
775
|
+
cwd: repoRoot,
|
|
776
|
+
encoding: "utf-8",
|
|
777
|
+
maxBuffer: 64 * 1024 * 1024
|
|
778
|
+
});
|
|
779
|
+
const allFiles = git(["ls-tree", "-r", "--name-only", ref]).split("\n").filter(Boolean);
|
|
780
|
+
const docsJsonPaths = allFiles.filter(
|
|
781
|
+
(file) => file === "docs.json" || file.endsWith("/docs.json")
|
|
782
|
+
);
|
|
783
|
+
const published = /* @__PURE__ */ new Set();
|
|
784
|
+
for (const docsJsonPath of docsJsonPaths) {
|
|
785
|
+
const rootDir = docsJsonPath === "docs.json" ? "" : docsJsonPath.slice(0, -"/docs.json".length);
|
|
786
|
+
if (rootDir && hasReservedSegment(rootDir)) continue;
|
|
787
|
+
let config;
|
|
788
|
+
try {
|
|
789
|
+
config = JSON.parse(git(["show", `${ref}:${docsJsonPath}`]));
|
|
790
|
+
} catch {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const prefix = rootDir ? `${rootDir}/` : "";
|
|
794
|
+
for (const file of allFiles) {
|
|
795
|
+
if (!file.startsWith(prefix) || !file.toLowerCase().endsWith(".md"))
|
|
796
|
+
continue;
|
|
797
|
+
const rel = file.slice(prefix.length);
|
|
798
|
+
if (hasReservedSegment(rel)) continue;
|
|
799
|
+
if (path2.posix.basename(file).toLowerCase() === "readme.md") continue;
|
|
800
|
+
published.add(file);
|
|
801
|
+
}
|
|
802
|
+
for (const include of config.include ?? []) {
|
|
803
|
+
const normalized = include.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
804
|
+
if (hasReservedSegment(normalized)) continue;
|
|
805
|
+
if (allFiles.includes(normalized)) published.add(normalized);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return [...published].sort();
|
|
809
|
+
}
|
|
810
|
+
function diffPublishSets(base, head) {
|
|
811
|
+
const baseSet = new Set(base);
|
|
812
|
+
const headSet = new Set(head);
|
|
813
|
+
return {
|
|
814
|
+
added: head.filter((source) => !baseSet.has(source)),
|
|
815
|
+
removed: base.filter((source) => !headSet.has(source))
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function renderPublishSetSummary(manifest, diff) {
|
|
819
|
+
const lines = [
|
|
820
|
+
`## Docs portal publish set`,
|
|
821
|
+
``,
|
|
822
|
+
`**${manifest.count} pages publish** from this tree.`,
|
|
823
|
+
``
|
|
824
|
+
];
|
|
825
|
+
if (diff && (diff.added.length > 0 || diff.removed.length > 0)) {
|
|
826
|
+
lines.push(`### Changes vs the merge base`, ``);
|
|
827
|
+
for (const source of diff.added) lines.push(`- \u2795 \`${source}\``);
|
|
828
|
+
for (const source of diff.removed) lines.push(`- \u2796 \`${source}\``);
|
|
829
|
+
lines.push(
|
|
830
|
+
``,
|
|
831
|
+
`Growing the public surface is a deliberate act \u2014 a reviewer should see this list.`
|
|
832
|
+
);
|
|
833
|
+
} else if (diff) {
|
|
834
|
+
lines.push(`No change vs the merge base.`);
|
|
835
|
+
}
|
|
836
|
+
lines.push(``, `<details><summary>Full publish set</summary>`, ``);
|
|
837
|
+
for (const source of manifest.sources) lines.push(`- \`${source}\``);
|
|
838
|
+
lines.push(``, `</details>`);
|
|
839
|
+
return `${lines.join("\n")}
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// packages/mesh-cli/src/docs/portal.ts
|
|
844
|
+
var DOCS_APP_DIR = "apps/docs";
|
|
845
|
+
var CONTENT_DIR = "content";
|
|
846
|
+
var GENERATED_CONFIG = "zudoku.config.ts";
|
|
847
|
+
var PUBLISH_MANIFEST = "publish-manifest.json";
|
|
848
|
+
var VERSION_JSON_DIR = "public";
|
|
849
|
+
var VERSION_JSON = "version.json";
|
|
850
|
+
var SITE_TITLE = "Mesh Developer Portal";
|
|
851
|
+
var SITE_DESCRIPTION = "Build and run financial-services apps on Mesh Platform with the mesh developer CLI.";
|
|
852
|
+
function runAssemble(args) {
|
|
853
|
+
const discovery = discoverDocRoots(args.repoRoot);
|
|
854
|
+
const assembled = assemblePortal({
|
|
855
|
+
repoRoot: args.repoRoot,
|
|
856
|
+
discovery,
|
|
857
|
+
outDir: args.outDir,
|
|
858
|
+
meshBaseline: currentBaseline(args.repoRoot),
|
|
859
|
+
commit: currentCommit(args.repoRoot)
|
|
860
|
+
});
|
|
861
|
+
const errors = [...discovery.errors];
|
|
862
|
+
if (args.writeFiles !== false) {
|
|
863
|
+
const manifestPath = args.outDir.includes(tmpdir()) ? path3.join(args.outDir, PUBLISH_MANIFEST) : path3.join(args.outDir, "..", PUBLISH_MANIFEST);
|
|
864
|
+
writeFileSync2(
|
|
865
|
+
manifestPath,
|
|
866
|
+
`${JSON.stringify(assembled.publishManifest, null, 2)}
|
|
867
|
+
`
|
|
868
|
+
);
|
|
869
|
+
if (args.configOut) {
|
|
870
|
+
writeFileSync2(
|
|
871
|
+
args.configOut,
|
|
872
|
+
renderZudokuConfig({
|
|
873
|
+
navigation: assembled.navigation,
|
|
874
|
+
title: SITE_TITLE,
|
|
875
|
+
description: SITE_DESCRIPTION,
|
|
876
|
+
baseline: currentBaseline(args.repoRoot),
|
|
877
|
+
commit: currentCommit(args.repoRoot)
|
|
878
|
+
})
|
|
879
|
+
);
|
|
880
|
+
const versionDir = path3.join(
|
|
881
|
+
path3.dirname(args.configOut),
|
|
882
|
+
VERSION_JSON_DIR
|
|
883
|
+
);
|
|
884
|
+
mkdirSync2(versionDir, { recursive: true });
|
|
885
|
+
writeFileSync2(
|
|
886
|
+
path3.join(versionDir, VERSION_JSON),
|
|
887
|
+
renderVersionJson({
|
|
888
|
+
baseline: currentBaseline(args.repoRoot),
|
|
889
|
+
commit: currentCommit(args.repoRoot),
|
|
890
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
891
|
+
})
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return { assembled, errors };
|
|
896
|
+
}
|
|
897
|
+
function reportAssembly(result) {
|
|
898
|
+
const { assembled } = result;
|
|
899
|
+
logSuccess(
|
|
900
|
+
`Assembled ${assembled.pages.length} pages from ${new Set(assembled.pages.map((p) => p.sectionTitle)).size} sections \u2192 ${assembled.outDir}`
|
|
901
|
+
);
|
|
902
|
+
if (assembled.externalLinks.length > 0) {
|
|
903
|
+
const unique = new Set(
|
|
904
|
+
assembled.externalLinks.map((issue) => `${issue.page} \u2192 ${issue.href}`)
|
|
905
|
+
);
|
|
906
|
+
logInfo(
|
|
907
|
+
`${unique.size} link${unique.size === 1 ? "" : "s"} point at repo files outside the portal and were rewritten to blob links`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
if (assembled.brokenLinks.length > 0) {
|
|
911
|
+
logWarn(`${assembled.brokenLinks.length} link(s) resolve to nothing:`);
|
|
912
|
+
for (const issue of assembled.brokenLinks) {
|
|
913
|
+
logWarn(` ${issue.page}: ${issue.href}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function checkPortal(args) {
|
|
918
|
+
const scratch = mkdtempSync(path3.join(tmpdir(), "mesh-docs-check-"));
|
|
919
|
+
const result = runAssemble({ repoRoot: args.repoRoot, outDir: scratch });
|
|
920
|
+
const failures = [...result.errors];
|
|
921
|
+
for (const violation of result.assembled.reservedViolations) {
|
|
922
|
+
failures.push(
|
|
923
|
+
`assembled output path contains a reserved segment: ${violation}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
for (const issue of result.assembled.brokenLinks) {
|
|
927
|
+
failures.push(`broken link on ${issue.page}: ${issue.href}`);
|
|
928
|
+
}
|
|
929
|
+
const cliReferenceError = await args.checkCliReference();
|
|
930
|
+
if (cliReferenceError) failures.push(cliReferenceError);
|
|
931
|
+
reportAssembly(result);
|
|
932
|
+
if (args.printManifest) {
|
|
933
|
+
process.stdout.write(
|
|
934
|
+
`${JSON.stringify(result.assembled.publishManifest, null, 2)}
|
|
935
|
+
`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (args.manifestOut) {
|
|
939
|
+
mkdirSync2(path3.dirname(args.manifestOut), { recursive: true });
|
|
940
|
+
writeFileSync2(
|
|
941
|
+
args.manifestOut,
|
|
942
|
+
`${JSON.stringify(result.assembled.publishManifest, null, 2)}
|
|
943
|
+
`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
if (args.diffBase) {
|
|
947
|
+
writePublishSetSummary(
|
|
948
|
+
args.repoRoot,
|
|
949
|
+
args.diffBase,
|
|
950
|
+
result.assembled.publishManifest
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
return failures;
|
|
954
|
+
}
|
|
955
|
+
function writePublishSetSummary(repoRoot, diffBase, manifest) {
|
|
956
|
+
let diff = null;
|
|
957
|
+
try {
|
|
958
|
+
const base = publishSetAtRef(repoRoot, diffBase);
|
|
959
|
+
diff = diffPublishSets(base, manifest.sources);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
logWarn(
|
|
962
|
+
`could not compute the publish-set diff against ${diffBase}: ${error instanceof Error ? error.message : String(error)}`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const summary = renderPublishSetSummary(manifest, diff);
|
|
966
|
+
process.stdout.write(`
|
|
967
|
+
${summary}`);
|
|
968
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
969
|
+
if (summaryPath) {
|
|
970
|
+
try {
|
|
971
|
+
appendFileSync(summaryPath, summary);
|
|
972
|
+
} catch {
|
|
973
|
+
logWarn(`GITHUB_STEP_SUMMARY is set but not writable (${summaryPath})`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function runZudoku(args) {
|
|
978
|
+
const cliArgs = args.mode === "dev" ? ["dev", "--port", String(args.port ?? 3e3)] : ["build"];
|
|
979
|
+
logInfo(`Running \`zudoku ${cliArgs.join(" ")}\` in ${args.appDir}`);
|
|
980
|
+
return new Promise((resolve, reject) => {
|
|
981
|
+
const child = spawn("pnpm", ["exec", "zudoku", ...cliArgs], {
|
|
982
|
+
cwd: args.appDir,
|
|
983
|
+
stdio: "inherit",
|
|
984
|
+
env: process.env
|
|
985
|
+
});
|
|
986
|
+
child.on("error", reject);
|
|
987
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
async function runPortalCommand(opts, deps) {
|
|
991
|
+
const { repoRoot } = deps;
|
|
992
|
+
if (opts.check) {
|
|
993
|
+
const failures = await checkPortal({
|
|
994
|
+
repoRoot,
|
|
995
|
+
diffBase: opts.diffBase,
|
|
996
|
+
printManifest: opts.printManifest ?? false,
|
|
997
|
+
manifestOut: opts.manifestOut,
|
|
998
|
+
checkCliReference: deps.checkCliReference
|
|
999
|
+
});
|
|
1000
|
+
if (failures.length > 0) {
|
|
1001
|
+
for (const failure of failures) logError(failure);
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`docs portal check failed with ${failures.length} problem(s)`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
logSuccess("Docs portal check passed");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const appDir = path3.join(repoRoot, DOCS_APP_DIR);
|
|
1010
|
+
const outDir = opts.out ? path3.resolve(repoRoot, opts.out) : path3.join(appDir, CONTENT_DIR);
|
|
1011
|
+
const result = runAssemble({
|
|
1012
|
+
repoRoot,
|
|
1013
|
+
outDir,
|
|
1014
|
+
configOut: path3.join(appDir, GENERATED_CONFIG)
|
|
1015
|
+
});
|
|
1016
|
+
reportAssembly(result);
|
|
1017
|
+
if (result.errors.length > 0) {
|
|
1018
|
+
for (const error of result.errors) logError(error);
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`docs portal assembly failed with ${result.errors.length} problem(s)`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
if (opts.assembleOnly || opts.out) {
|
|
1024
|
+
logInfo(
|
|
1025
|
+
`Assembled content at ${path3.relative(repoRoot, outDir)} \u2014 build with: pnpm --filter @mesh-tech/docs build`
|
|
1026
|
+
);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const mode = opts.serve ? "dev" : "build";
|
|
1030
|
+
let port;
|
|
1031
|
+
if (opts.serve && opts.port) {
|
|
1032
|
+
port = Number.parseInt(opts.port, 10);
|
|
1033
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1034
|
+
throw new Error(`Invalid --port: ${opts.port}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const code = await runZudoku({
|
|
1038
|
+
appDir,
|
|
1039
|
+
mode,
|
|
1040
|
+
...port !== void 0 ? { port } : {}
|
|
1041
|
+
});
|
|
1042
|
+
if (code !== 0) throw new Error(`zudoku ${mode} exited with code ${code}`);
|
|
1043
|
+
if (mode === "build") {
|
|
1044
|
+
logSuccess(
|
|
1045
|
+
`Portal built \u2192 ${path3.join(path3.relative(repoRoot, appDir), "dist")}`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async function cliReferenceStaleness(repoRoot) {
|
|
1050
|
+
const [
|
|
1051
|
+
{ createProgram },
|
|
1052
|
+
{ extractCliReference, renderCliReferenceMarkdown }
|
|
1053
|
+
] = await Promise.all([
|
|
1054
|
+
import("./mesh.program-BIDQBSSI.js"),
|
|
1055
|
+
import("./mesh.cli-reference-H3TOPIET.js")
|
|
1056
|
+
]);
|
|
1057
|
+
const outPath = path3.join(repoRoot, "docs/portal/generated/cli-reference.md");
|
|
1058
|
+
const markdown = renderCliReferenceMarkdown(
|
|
1059
|
+
extractCliReference(createProgram())
|
|
1060
|
+
);
|
|
1061
|
+
const existing = existsSync2(outPath) ? readFileSync3(outPath, "utf-8") : null;
|
|
1062
|
+
if (existing === markdown) return null;
|
|
1063
|
+
return `${existing === null ? "CLI reference is missing" : "CLI reference is stale"}: docs/portal/generated/cli-reference.md \u2014 the commander tree changed; regenerate and commit with \`pnpm exec mesh docs cli-reference\``;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// packages/mesh-cli/src/utils/build-info.ts
|
|
1067
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1068
|
+
import * as fs from "fs";
|
|
1069
|
+
import * as path4 from "path";
|
|
1070
|
+
import { fileURLToPath } from "url";
|
|
1071
|
+
function detectRuntimeMode(modulePath) {
|
|
1072
|
+
return modulePath.split(/[\\/]/).includes("dist") ? "dist" : "source";
|
|
1073
|
+
}
|
|
1074
|
+
function formatVersionLine(info) {
|
|
1075
|
+
if (info.mode === "source") {
|
|
1076
|
+
const at = [info.commit, info.builtAt].filter(Boolean).join(" ");
|
|
1077
|
+
return `${info.version} (source${at ? ` @ ${at}` : ""})`;
|
|
1078
|
+
}
|
|
1079
|
+
const built = info.builtAt ? ` ${info.builtAt.slice(0, 10)}` : "";
|
|
1080
|
+
const commit = info.commit ? `, commit ${info.commit}` : "";
|
|
1081
|
+
const detail = built || commit ? `${built}${commit}` : " \u2014 build metadata unavailable";
|
|
1082
|
+
return `${info.version} (published build${detail})`;
|
|
1083
|
+
}
|
|
1084
|
+
function findCliPackageRoot(startDir) {
|
|
1085
|
+
let dir = startDir;
|
|
1086
|
+
for (let i = 0; i < 8; i++) {
|
|
1087
|
+
try {
|
|
1088
|
+
const pkg = JSON.parse(fs.readFileSync(path4.join(dir, "package.json"), "utf-8"));
|
|
1089
|
+
if (pkg.name === "@mesh-tech/mesh-cli") return dir;
|
|
1090
|
+
} catch {
|
|
1091
|
+
}
|
|
1092
|
+
const parent = path4.dirname(dir);
|
|
1093
|
+
if (parent === dir) break;
|
|
1094
|
+
dir = parent;
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
function resolveCliRuntime(opts) {
|
|
1099
|
+
const vcs = opts?.vcs ?? true;
|
|
1100
|
+
const here = path4.dirname(fileURLToPath(import.meta.url));
|
|
1101
|
+
const mode = detectRuntimeMode(here);
|
|
1102
|
+
const root = findCliPackageRoot(here);
|
|
1103
|
+
let version = "0.0.0";
|
|
1104
|
+
if (root) {
|
|
1105
|
+
try {
|
|
1106
|
+
version = JSON.parse(fs.readFileSync(path4.join(root, "package.json"), "utf-8")).version ?? version;
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const info = { version, mode };
|
|
1111
|
+
if (!root) return info;
|
|
1112
|
+
if (mode === "dist") {
|
|
1113
|
+
try {
|
|
1114
|
+
const meta = JSON.parse(
|
|
1115
|
+
fs.readFileSync(path4.join(root, "dist", "build-info.json"), "utf-8")
|
|
1116
|
+
);
|
|
1117
|
+
info.commit = meta.commit;
|
|
1118
|
+
info.builtAt = meta.builtAt;
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
return info;
|
|
1122
|
+
}
|
|
1123
|
+
if (!vcs) return info;
|
|
1124
|
+
try {
|
|
1125
|
+
const git = (args) => execFileSync2("git", args, {
|
|
1126
|
+
cwd: root,
|
|
1127
|
+
encoding: "utf-8",
|
|
1128
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1129
|
+
}).trim();
|
|
1130
|
+
info.commit = git(["rev-parse", "--short", "HEAD"]);
|
|
1131
|
+
info.builtAt = git(["log", "-1", "--format=%cs"]);
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
return info;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export {
|
|
1138
|
+
discoverDocRoots,
|
|
1139
|
+
DOCS_APP_DIR,
|
|
1140
|
+
runAssemble,
|
|
1141
|
+
reportAssembly,
|
|
1142
|
+
runPortalCommand,
|
|
1143
|
+
cliReferenceStaleness,
|
|
1144
|
+
formatVersionLine,
|
|
1145
|
+
resolveCliRuntime
|
|
1146
|
+
};
|