blodemd 0.0.10 → 0.0.12
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/README.md +11 -47
- package/dev-server/app/layout.tsx +1 -1
- package/dist/cli.mjs +1078 -406
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +15 -1
- package/docs/components/api/api-playground.tsx +2 -2
- package/docs/components/docs/copy-page-menu.tsx +55 -27
- package/docs/components/docs/doc-header.tsx +1 -1
- package/docs/components/docs/doc-shell.tsx +89 -88
- package/docs/components/docs/doc-sidebar.tsx +6 -3
- package/docs/components/docs/doc-toc.tsx +1 -1
- package/docs/components/docs/mobile-nav.tsx +8 -16
- package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
- package/docs/components/git/repo-picker.tsx +526 -0
- package/docs/components/mdx/agent-instructions.tsx +17 -0
- package/docs/components/mdx/code-block.tsx +6 -1
- package/docs/components/mdx/code-group.tsx +1 -1
- package/docs/components/mdx/iframe.tsx +62 -0
- package/docs/components/mdx/index.tsx +4 -0
- package/docs/components/mdx/tabs.tsx +5 -5
- package/docs/components/mdx/video.tsx +45 -12
- package/docs/components/third-parties.tsx +29 -0
- package/docs/components/ui/badge.tsx +61 -0
- package/docs/components/ui/breadcrumb.tsx +61 -41
- package/docs/components/ui/button-group.tsx +83 -0
- package/docs/components/ui/button.tsx +30 -55
- package/docs/components/ui/command.tsx +32 -4
- package/docs/components/ui/copy-button.tsx +12 -19
- package/docs/components/ui/dialog.tsx +50 -1
- package/docs/components/ui/input.tsx +16 -97
- package/docs/components/ui/kbd.tsx +98 -0
- package/docs/components/ui/morph-icon.tsx +79 -0
- package/docs/components/ui/popover.tsx +225 -30
- package/docs/components/ui/search.tsx +0 -9
- package/docs/components/ui/sheet.tsx +30 -1
- package/docs/components/ui/sidebar.tsx +332 -7
- package/docs/components/ui/site-footer.tsx +6 -4
- package/docs/components/ui/skeleton.tsx +11 -0
- package/docs/components/ui/switch.tsx +32 -0
- package/docs/components/ui/tabs.tsx +138 -0
- package/docs/lib/api-client.ts +72 -0
- package/docs/lib/contextual-options.ts +9 -0
- package/docs/lib/dashboard-session.ts +167 -0
- package/docs/lib/db.ts +13 -0
- package/docs/lib/env.ts +4 -3
- package/docs/lib/etag.ts +22 -0
- package/docs/lib/github-install.ts +33 -0
- package/docs/lib/project-authz.ts +46 -0
- package/docs/lib/routes.ts +5 -1
- package/docs/lib/supabase.ts +30 -6
- package/docs/lib/tenancy.ts +1 -0
- package/docs/lib/tenant-static.ts +206 -4
- package/docs/lib/tenants.ts +5 -1
- package/docs/lib/time-ago.ts +24 -0
- package/docs/lib/use-tab-observer.ts +71 -0
- package/package.json +2 -2
- package/packages/@repo/contracts/dist/git.d.ts +28 -0
- package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/git.js +24 -0
- package/packages/@repo/contracts/dist/index.d.ts +1 -1
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -1
- package/packages/@repo/contracts/src/git.ts +31 -0
- package/packages/@repo/contracts/src/index.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +9 -0
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +7 -0
- package/packages/@repo/models/src/docs-config.ts +7 -0
- package/packages/@repo/previewing/dist/index.d.ts +4 -0
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +53 -2
- package/packages/@repo/previewing/src/index.ts +64 -2
- package/packages/@repo/validation/src/blodemd-docs-schema.json +8 -1
- package/scripts/prepare-package.mjs +14 -0
- package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
- package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
- package/packages/@repo/contracts/dist/api-key.js +0 -20
- package/packages/@repo/contracts/src/api-key.ts +0 -27
package/dist/cli.mjs
CHANGED
|
@@ -3,8 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fs, { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import path, { join } from "node:path";
|
|
6
|
-
import { confirm, intro, isCancel, log,
|
|
7
|
-
import { shouldIgnoreRootDocsFile, slugify } from "@repo/common";
|
|
6
|
+
import { confirm, intro, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
8
7
|
import chalk from "chalk";
|
|
9
8
|
import { Command, InvalidArgumentError } from "commander";
|
|
10
9
|
import open from "open";
|
|
@@ -13,13 +12,37 @@ import { once } from "node:events";
|
|
|
13
12
|
import { createServer } from "node:net";
|
|
14
13
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
14
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import "yaml";
|
|
17
17
|
import { watch } from "chokidar";
|
|
18
18
|
import { createServer as createServer$1 } from "node:http";
|
|
19
19
|
import { createHash, randomBytes } from "node:crypto";
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
+
//#region ../../packages/common/dist/index.js
|
|
22
|
+
const BACKSLASH_TO_SLASH_REGEX = /\\/g;
|
|
23
|
+
const TRAILING_SLASHES_REGEX = /\/+$/g;
|
|
24
|
+
const LEADING_SLASHES_REGEX = /^\/+/;
|
|
25
|
+
const normalizePath = (value) => {
|
|
26
|
+
return value.replace(BACKSLASH_TO_SLASH_REGEX, "/").replace(TRAILING_SLASHES_REGEX, "").replace(LEADING_SLASHES_REGEX, "");
|
|
27
|
+
};
|
|
28
|
+
const slugify = (value) => value.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(^-|-$)+/g, "");
|
|
29
|
+
const IGNORED_ROOT_DOCS_FILES = new Set([
|
|
30
|
+
".gitignore",
|
|
31
|
+
"AGENTS.md",
|
|
32
|
+
"CLAUDE.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"LICENSE.md",
|
|
35
|
+
"README.md"
|
|
36
|
+
]);
|
|
37
|
+
const shouldIgnoreRootDocsFile = (value) => {
|
|
38
|
+
const normalized = normalizePath(value);
|
|
39
|
+
if (!normalized || normalized.includes("/")) return false;
|
|
40
|
+
return IGNORED_ROOT_DOCS_FILES.has(normalized);
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
21
43
|
//#region src/constants.ts
|
|
22
44
|
const CLI_NAME = "blodemd";
|
|
45
|
+
const BLODE_PROJECT_ENV = "BLODEMD_PROJECT";
|
|
23
46
|
const OAUTH_CLIENT_ID = "6b5f9860-fe96-4a83-b1ad-266260523c91";
|
|
24
47
|
const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
|
|
25
48
|
const DEFAULT_OAUTH_CALLBACK_PATH = "/auth/callback";
|
|
@@ -30,30 +53,6 @@ const getDefaultConfigBaseDir = () => {
|
|
|
30
53
|
const CONFIG_DIR = join(getDefaultConfigBaseDir(), CLI_NAME);
|
|
31
54
|
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
32
55
|
//#endregion
|
|
33
|
-
//#region src/jwt.ts
|
|
34
|
-
const parseJwtBase64Url = (input) => {
|
|
35
|
-
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
36
|
-
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
37
|
-
return Buffer.from(padded, "base64").toString("utf8");
|
|
38
|
-
};
|
|
39
|
-
const parseJwtClaims = (token) => {
|
|
40
|
-
const payloadPart = token.split(".").at(1);
|
|
41
|
-
if (!payloadPart) return null;
|
|
42
|
-
try {
|
|
43
|
-
const payload = parseJwtBase64Url(payloadPart);
|
|
44
|
-
const parsed = JSON.parse(payload);
|
|
45
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
46
|
-
const claims = parsed;
|
|
47
|
-
return {
|
|
48
|
-
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
49
|
-
exp: typeof claims.exp === "number" ? claims.exp : void 0,
|
|
50
|
-
sub: typeof claims.sub === "string" ? claims.sub : void 0
|
|
51
|
-
};
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
//#endregion
|
|
57
56
|
//#region src/errors.ts
|
|
58
57
|
const EXIT_CODES = {
|
|
59
58
|
AUTH_REQUIRED: 4,
|
|
@@ -149,14 +148,6 @@ const parseStoredAuthSession = (value) => {
|
|
|
149
148
|
user
|
|
150
149
|
};
|
|
151
150
|
};
|
|
152
|
-
const parseApiKeyCredentials = (value) => {
|
|
153
|
-
if (!isRecord(value)) return null;
|
|
154
|
-
if (typeof value.apiKey !== "string") return null;
|
|
155
|
-
return {
|
|
156
|
-
apiKey: value.apiKey,
|
|
157
|
-
type: "api-key"
|
|
158
|
-
};
|
|
159
|
-
};
|
|
160
151
|
const createInvalidCredentialsError = (detail) => new CliError(detail ? `Invalid credentials format in ${CREDENTIALS_FILE}: ${detail}` : `Invalid credentials format in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
|
|
161
152
|
const parseAuthFile = (raw) => {
|
|
162
153
|
let parsed;
|
|
@@ -167,13 +158,9 @@ const parseAuthFile = (raw) => {
|
|
|
167
158
|
}
|
|
168
159
|
if (!isRecord(parsed) || parsed.version !== 1) throw createInvalidCredentialsError();
|
|
169
160
|
const hasSession = Object.hasOwn(parsed, "session");
|
|
170
|
-
const hasApiKey = Object.hasOwn(parsed, "apiKey");
|
|
171
161
|
const session = hasSession && parsed.session !== void 0 ? parseStoredAuthSession(parsed.session) : void 0;
|
|
172
|
-
const apiKey = hasApiKey && parsed.apiKey !== void 0 ? parseApiKeyCredentials(parsed.apiKey) : void 0;
|
|
173
162
|
if (hasSession && parsed.session !== void 0 && !session) throw createInvalidCredentialsError("stored session is malformed.");
|
|
174
|
-
if (hasApiKey && parsed.apiKey !== void 0 && !apiKey) throw createInvalidCredentialsError("stored API key is malformed.");
|
|
175
163
|
return {
|
|
176
|
-
apiKey: apiKey ?? void 0,
|
|
177
164
|
session: session ?? void 0,
|
|
178
165
|
version: 1
|
|
179
166
|
};
|
|
@@ -203,16 +190,34 @@ const writeStoredAuthSession = async (session) => {
|
|
|
203
190
|
version: 1
|
|
204
191
|
});
|
|
205
192
|
};
|
|
206
|
-
const writeStoredApiKey = async (apiKey) => {
|
|
207
|
-
await writeAuthFile({
|
|
208
|
-
apiKey,
|
|
209
|
-
version: 1
|
|
210
|
-
});
|
|
211
|
-
};
|
|
212
193
|
const clearStoredCredentials = async () => {
|
|
213
194
|
await rm(CREDENTIALS_FILE, { force: true });
|
|
214
195
|
};
|
|
215
196
|
//#endregion
|
|
197
|
+
//#region src/jwt.ts
|
|
198
|
+
const parseJwtBase64Url = (input) => {
|
|
199
|
+
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
200
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
201
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
202
|
+
};
|
|
203
|
+
const parseJwtClaims = (token) => {
|
|
204
|
+
const payloadPart = token.split(".").at(1);
|
|
205
|
+
if (!payloadPart) return null;
|
|
206
|
+
try {
|
|
207
|
+
const payload = parseJwtBase64Url(payloadPart);
|
|
208
|
+
const parsed = JSON.parse(payload);
|
|
209
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
210
|
+
const claims = parsed;
|
|
211
|
+
return {
|
|
212
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
213
|
+
exp: typeof claims.exp === "number" ? claims.exp : void 0,
|
|
214
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
216
221
|
//#region src/supabase.ts
|
|
217
222
|
const resolveSupabaseConfig = () => {
|
|
218
223
|
return { url: process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "https://bwnxwgkgyklzzmpbzuoz.supabase.co" };
|
|
@@ -253,53 +258,30 @@ const shouldRefresh = (session) => {
|
|
|
253
258
|
const ms = expiresInMs(session);
|
|
254
259
|
return ms !== null && ms <= 6e4;
|
|
255
260
|
};
|
|
256
|
-
const tokenFromRaw = (token, source) => {
|
|
257
|
-
const claims = parseJwtClaims(token);
|
|
258
|
-
return {
|
|
259
|
-
expiresAt: typeof claims?.exp === "number" ? (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString() : null,
|
|
260
|
-
source,
|
|
261
|
-
token,
|
|
262
|
-
user: claims?.sub || claims?.email ? {
|
|
263
|
-
email: claims.email ?? null,
|
|
264
|
-
id: claims.sub ?? "unknown"
|
|
265
|
-
} : null
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
261
|
const sessionToResolvedToken = (session) => ({
|
|
269
262
|
expiresAt: session.expiresAt,
|
|
270
263
|
source: "stored",
|
|
271
264
|
token: session.accessToken,
|
|
272
265
|
user: session.user
|
|
273
266
|
});
|
|
274
|
-
const resolveAuthToken = async (
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (isExpired(session)) {
|
|
291
|
-
await clearStoredCredentials();
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
return sessionToResolvedToken(session);
|
|
267
|
+
const resolveAuthToken = async () => {
|
|
268
|
+
const session = (await readAuthFile())?.session;
|
|
269
|
+
if (!session) return null;
|
|
270
|
+
if (!(shouldRefresh(session) || isExpired(session))) return sessionToResolvedToken(session);
|
|
271
|
+
if (session.refreshToken) try {
|
|
272
|
+
const { tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
273
|
+
const updatedSession = tokenResponseToStoredSession(await refreshAccessToken({
|
|
274
|
+
clientId: OAUTH_CLIENT_ID,
|
|
275
|
+
tokenUrl
|
|
276
|
+
}, session.refreshToken));
|
|
277
|
+
await writeStoredAuthSession(updatedSession);
|
|
278
|
+
return sessionToResolvedToken(updatedSession);
|
|
279
|
+
} catch {}
|
|
280
|
+
if (isExpired(session)) {
|
|
281
|
+
await clearStoredCredentials();
|
|
282
|
+
return null;
|
|
295
283
|
}
|
|
296
|
-
|
|
297
|
-
expiresAt: null,
|
|
298
|
-
source: "stored",
|
|
299
|
-
token: data.apiKey.apiKey,
|
|
300
|
-
user: null
|
|
301
|
-
};
|
|
302
|
-
return null;
|
|
284
|
+
return sessionToResolvedToken(session);
|
|
303
285
|
};
|
|
304
286
|
const resolveTokenStatus = (token) => {
|
|
305
287
|
if (!token.expiresAt) return {
|
|
@@ -333,6 +315,642 @@ const parsePort = (value, label = "Port") => {
|
|
|
333
315
|
return parsed;
|
|
334
316
|
};
|
|
335
317
|
//#endregion
|
|
318
|
+
//#region ../../packages/models/dist/docs-config.js
|
|
319
|
+
const UrlOrPathSchema = z.string().min(1);
|
|
320
|
+
const SlugSchema = z.string().min(1).regex(/^[a-z0-9-]+$/);
|
|
321
|
+
const DocsColorsSchema = z.object({
|
|
322
|
+
background: z.string().optional(),
|
|
323
|
+
border: z.string().optional(),
|
|
324
|
+
dark: z.string().optional(),
|
|
325
|
+
light: z.string().optional(),
|
|
326
|
+
muted: z.string().optional(),
|
|
327
|
+
primary: z.string().min(1),
|
|
328
|
+
surface: z.string().optional()
|
|
329
|
+
}).strict();
|
|
330
|
+
const DocsFontsSchema = z.object({
|
|
331
|
+
body: z.string().optional(),
|
|
332
|
+
cssUrl: z.string().optional(),
|
|
333
|
+
heading: z.string().optional(),
|
|
334
|
+
mono: z.string().optional(),
|
|
335
|
+
provider: z.enum([
|
|
336
|
+
"google",
|
|
337
|
+
"local",
|
|
338
|
+
"custom"
|
|
339
|
+
]).optional()
|
|
340
|
+
}).strict();
|
|
341
|
+
const DocsLogoSchema = z.object({
|
|
342
|
+
alt: z.string().optional(),
|
|
343
|
+
dark: UrlOrPathSchema.optional(),
|
|
344
|
+
href: z.string().min(1).optional(),
|
|
345
|
+
light: UrlOrPathSchema.optional()
|
|
346
|
+
}).strict();
|
|
347
|
+
const DocsNavLinkSchema = z.object({
|
|
348
|
+
href: z.string().min(1),
|
|
349
|
+
label: z.string().min(1)
|
|
350
|
+
}).strict();
|
|
351
|
+
const DocsNavAnchorSchema = z.object({
|
|
352
|
+
href: z.string().min(1),
|
|
353
|
+
label: z.string().min(1)
|
|
354
|
+
}).strict();
|
|
355
|
+
const DocsNavLocaleSchema = z.object({
|
|
356
|
+
label: z.string().min(1),
|
|
357
|
+
locale: z.string().optional(),
|
|
358
|
+
url: z.string().min(1)
|
|
359
|
+
}).strict();
|
|
360
|
+
const DocsNavVersionSchema = z.object({
|
|
361
|
+
label: z.string().min(1),
|
|
362
|
+
url: z.string().min(1)
|
|
363
|
+
}).strict();
|
|
364
|
+
const DocsOpenApiSourceSchema = z.object({
|
|
365
|
+
basePath: z.string().optional(),
|
|
366
|
+
directory: z.string().optional(),
|
|
367
|
+
include: z.array(z.string()).optional(),
|
|
368
|
+
source: z.string().min(1)
|
|
369
|
+
}).strict();
|
|
370
|
+
const DocsNavGroupSchema = z.object({
|
|
371
|
+
expanded: z.boolean().optional(),
|
|
372
|
+
group: z.string().optional(),
|
|
373
|
+
hidden: z.boolean().optional(),
|
|
374
|
+
openapi: z.union([z.string().min(1), DocsOpenApiSourceSchema]).optional(),
|
|
375
|
+
pages: z.array(z.string()).optional()
|
|
376
|
+
}).strict();
|
|
377
|
+
const DocsNavTabSchema = z.object({
|
|
378
|
+
groups: z.array(DocsNavGroupSchema).optional(),
|
|
379
|
+
href: z.string().min(1).optional(),
|
|
380
|
+
icon: z.string().optional(),
|
|
381
|
+
label: z.string().min(1),
|
|
382
|
+
pages: z.array(z.string()).optional()
|
|
383
|
+
}).strict().refine((value) => Boolean(value.groups?.length || value.pages?.length || value.href), {
|
|
384
|
+
message: "tab must define groups, pages, or href",
|
|
385
|
+
path: []
|
|
386
|
+
});
|
|
387
|
+
const DocsNavigationSchema = z.object({
|
|
388
|
+
global: z.object({
|
|
389
|
+
anchors: z.array(DocsNavAnchorSchema).optional(),
|
|
390
|
+
links: z.array(DocsNavLinkSchema).optional()
|
|
391
|
+
}).strict().optional(),
|
|
392
|
+
groups: z.array(DocsNavGroupSchema).optional(),
|
|
393
|
+
hidden: z.array(z.string()).optional(),
|
|
394
|
+
languages: z.array(DocsNavLocaleSchema).optional(),
|
|
395
|
+
pages: z.array(z.string()).optional(),
|
|
396
|
+
tabs: z.array(DocsNavTabSchema).optional(),
|
|
397
|
+
versions: z.array(DocsNavVersionSchema).optional()
|
|
398
|
+
}).strict();
|
|
399
|
+
const DocsScriptsSchema = z.object({
|
|
400
|
+
body: z.array(z.string()).optional(),
|
|
401
|
+
head: z.array(z.string()).optional()
|
|
402
|
+
}).strict();
|
|
403
|
+
const DocsSeoSchema = z.object({ indexing: z.enum(["all", "default"]).optional() }).strict();
|
|
404
|
+
const DocsFeatureFlagsSchema = z.object({
|
|
405
|
+
rightToc: z.boolean().optional(),
|
|
406
|
+
search: z.boolean().optional(),
|
|
407
|
+
themeToggle: z.boolean().optional(),
|
|
408
|
+
toc: z.boolean().optional()
|
|
409
|
+
}).strict();
|
|
410
|
+
const DocsOpenApiProxySchema = z.object({
|
|
411
|
+
allowedHosts: z.array(z.string()).optional(),
|
|
412
|
+
enabled: z.boolean().optional()
|
|
413
|
+
}).strict();
|
|
414
|
+
const MintlifyLogoSchema = z.union([UrlOrPathSchema, z.object({
|
|
415
|
+
dark: UrlOrPathSchema,
|
|
416
|
+
href: z.string().min(1).optional(),
|
|
417
|
+
light: UrlOrPathSchema
|
|
418
|
+
}).strict()]);
|
|
419
|
+
const MintlifyFaviconSchema = z.union([UrlOrPathSchema, z.object({
|
|
420
|
+
dark: UrlOrPathSchema,
|
|
421
|
+
light: UrlOrPathSchema
|
|
422
|
+
}).strict()]);
|
|
423
|
+
const MintlifyNavbarLinkSchema = z.object({
|
|
424
|
+
href: z.string().min(1),
|
|
425
|
+
icon: z.string().optional(),
|
|
426
|
+
iconType: z.string().optional(),
|
|
427
|
+
label: z.string().optional(),
|
|
428
|
+
type: z.enum(["discord", "github"]).optional()
|
|
429
|
+
}).strict();
|
|
430
|
+
const MintlifyNavbarPrimarySchema = z.object({
|
|
431
|
+
href: z.string().min(1),
|
|
432
|
+
label: z.string().optional(),
|
|
433
|
+
type: z.enum([
|
|
434
|
+
"button",
|
|
435
|
+
"discord",
|
|
436
|
+
"github"
|
|
437
|
+
])
|
|
438
|
+
}).strict();
|
|
439
|
+
const MintlifyNavbarSchema = z.object({
|
|
440
|
+
links: z.array(MintlifyNavbarLinkSchema).optional(),
|
|
441
|
+
primary: MintlifyNavbarPrimarySchema.optional()
|
|
442
|
+
}).strict();
|
|
443
|
+
const MintlifyNavigationGlobalSchema = z.object({ anchors: z.array(z.object({
|
|
444
|
+
anchor: z.string().min(1),
|
|
445
|
+
color: z.object({
|
|
446
|
+
dark: z.string().optional(),
|
|
447
|
+
light: z.string().optional()
|
|
448
|
+
}).strict().optional(),
|
|
449
|
+
hidden: z.boolean().optional(),
|
|
450
|
+
href: z.string().min(1),
|
|
451
|
+
icon: z.string().optional(),
|
|
452
|
+
iconType: z.string().optional()
|
|
453
|
+
}).strict()).optional() }).strict();
|
|
454
|
+
const MintlifyNavigationGroupSchema = z.object({
|
|
455
|
+
expanded: z.boolean().optional(),
|
|
456
|
+
group: z.string().min(1),
|
|
457
|
+
hidden: z.boolean().optional(),
|
|
458
|
+
icon: z.string().optional(),
|
|
459
|
+
pages: z.array(z.string()).optional(),
|
|
460
|
+
root: z.string().optional(),
|
|
461
|
+
tag: z.string().optional()
|
|
462
|
+
}).strict();
|
|
463
|
+
const MintlifyNavTabSchema = z.object({
|
|
464
|
+
groups: z.array(MintlifyNavigationGroupSchema).optional(),
|
|
465
|
+
hidden: z.boolean().optional(),
|
|
466
|
+
href: z.string().min(1).optional(),
|
|
467
|
+
icon: z.string().optional(),
|
|
468
|
+
pages: z.array(z.string()).optional(),
|
|
469
|
+
tab: z.string().min(1)
|
|
470
|
+
}).strict();
|
|
471
|
+
const MintlifyNavigationSchema = z.object({
|
|
472
|
+
global: MintlifyNavigationGlobalSchema.optional(),
|
|
473
|
+
groups: z.array(MintlifyNavigationGroupSchema).optional(),
|
|
474
|
+
languages: z.array(z.object({
|
|
475
|
+
default: z.boolean().optional(),
|
|
476
|
+
hidden: z.boolean().optional(),
|
|
477
|
+
href: z.string().min(1),
|
|
478
|
+
language: z.string().min(1)
|
|
479
|
+
}).strict()).optional(),
|
|
480
|
+
pages: z.array(z.string()).optional(),
|
|
481
|
+
tabs: z.array(MintlifyNavTabSchema).optional(),
|
|
482
|
+
versions: z.array(z.object({
|
|
483
|
+
default: z.boolean().optional(),
|
|
484
|
+
hidden: z.boolean().optional(),
|
|
485
|
+
href: z.string().min(1),
|
|
486
|
+
version: z.string().min(1)
|
|
487
|
+
}).strict()).optional()
|
|
488
|
+
}).strict().refine((value) => Boolean(value.groups?.length || value.pages?.length || value.tabs?.length || value.languages?.length || value.versions?.length), {
|
|
489
|
+
message: "navigation must define at least one of groups, pages, tabs, languages, or versions",
|
|
490
|
+
path: []
|
|
491
|
+
});
|
|
492
|
+
const MintlifyApiSchema = z.object({
|
|
493
|
+
asyncapi: z.union([
|
|
494
|
+
z.string(),
|
|
495
|
+
z.array(z.string()),
|
|
496
|
+
DocsOpenApiSourceSchema
|
|
497
|
+
]).optional(),
|
|
498
|
+
examples: z.object({
|
|
499
|
+
autogenerate: z.boolean().optional(),
|
|
500
|
+
defaults: z.enum(["all", "required"]).optional(),
|
|
501
|
+
languages: z.array(z.string()).optional(),
|
|
502
|
+
prefill: z.boolean().optional()
|
|
503
|
+
}).strict().optional(),
|
|
504
|
+
mdx: z.object({
|
|
505
|
+
auth: z.object({
|
|
506
|
+
method: z.enum([
|
|
507
|
+
"basic",
|
|
508
|
+
"bearer",
|
|
509
|
+
"cobo",
|
|
510
|
+
"key"
|
|
511
|
+
]).optional(),
|
|
512
|
+
name: z.string().optional()
|
|
513
|
+
}).strict().optional(),
|
|
514
|
+
server: z.union([z.string().min(1), z.array(z.string().min(1))]).optional()
|
|
515
|
+
}).strict().optional(),
|
|
516
|
+
openapi: z.union([
|
|
517
|
+
z.string(),
|
|
518
|
+
z.array(z.string()),
|
|
519
|
+
DocsOpenApiSourceSchema
|
|
520
|
+
]).optional(),
|
|
521
|
+
params: z.object({ expanded: z.enum(["all", "closed"]).optional() }).strict().optional(),
|
|
522
|
+
playground: z.object({
|
|
523
|
+
credentials: z.boolean().optional(),
|
|
524
|
+
display: z.enum([
|
|
525
|
+
"auth",
|
|
526
|
+
"interactive",
|
|
527
|
+
"none",
|
|
528
|
+
"simple"
|
|
529
|
+
]).optional(),
|
|
530
|
+
proxy: z.boolean().optional()
|
|
531
|
+
}).strict().optional(),
|
|
532
|
+
url: z.literal("full").optional()
|
|
533
|
+
}).strict();
|
|
534
|
+
const MintlifyAppearanceSchema = z.object({
|
|
535
|
+
default: z.enum([
|
|
536
|
+
"dark",
|
|
537
|
+
"light",
|
|
538
|
+
"system"
|
|
539
|
+
]).optional(),
|
|
540
|
+
strict: z.boolean().optional()
|
|
541
|
+
}).strict();
|
|
542
|
+
const MintlifyMetadataSchema = z.object({ timestamp: z.boolean().optional() }).strict();
|
|
543
|
+
const MintlifySearchSchema = z.object({ prompt: z.string().optional() }).strict();
|
|
544
|
+
const ContextualBuiltinOptionSchema = z.enum([
|
|
545
|
+
"add-mcp",
|
|
546
|
+
"aistudio",
|
|
547
|
+
"assistant",
|
|
548
|
+
"chatgpt",
|
|
549
|
+
"claude",
|
|
550
|
+
"copy",
|
|
551
|
+
"cursor",
|
|
552
|
+
"devin",
|
|
553
|
+
"devin-mcp",
|
|
554
|
+
"gemini",
|
|
555
|
+
"grok",
|
|
556
|
+
"mcp",
|
|
557
|
+
"perplexity",
|
|
558
|
+
"view",
|
|
559
|
+
"vscode",
|
|
560
|
+
"windsurf"
|
|
561
|
+
]);
|
|
562
|
+
const ContextualCustomHrefQuerySchema = z.object({
|
|
563
|
+
key: z.string().min(1),
|
|
564
|
+
value: z.string().min(1)
|
|
565
|
+
}).strict();
|
|
566
|
+
const ContextualCustomHrefObjectSchema = z.object({
|
|
567
|
+
base: z.string().min(1),
|
|
568
|
+
query: z.array(ContextualCustomHrefQuerySchema)
|
|
569
|
+
}).strict();
|
|
570
|
+
const ContextualCustomOptionSchema = z.object({
|
|
571
|
+
description: z.string().min(1),
|
|
572
|
+
href: z.union([z.string().min(1), ContextualCustomHrefObjectSchema]),
|
|
573
|
+
icon: z.string().min(1),
|
|
574
|
+
iconType: z.string().optional(),
|
|
575
|
+
title: z.string().min(1)
|
|
576
|
+
}).strict();
|
|
577
|
+
const ContextualOptionSchema = z.union([ContextualBuiltinOptionSchema, ContextualCustomOptionSchema]);
|
|
578
|
+
const DocsContextualSchema = z.object({
|
|
579
|
+
display: z.enum(["header", "toc"]).optional(),
|
|
580
|
+
options: z.array(ContextualOptionSchema)
|
|
581
|
+
}).strict();
|
|
582
|
+
const DocsConfigSchema = z.object({
|
|
583
|
+
$schema: z.string().optional(),
|
|
584
|
+
api: MintlifyApiSchema.optional(),
|
|
585
|
+
appearance: MintlifyAppearanceSchema.optional(),
|
|
586
|
+
contextual: DocsContextualSchema.optional(),
|
|
587
|
+
description: z.string().optional(),
|
|
588
|
+
favicon: MintlifyFaviconSchema.optional(),
|
|
589
|
+
logo: MintlifyLogoSchema.optional(),
|
|
590
|
+
metadata: MintlifyMetadataSchema.optional(),
|
|
591
|
+
name: z.string().min(1),
|
|
592
|
+
navbar: MintlifyNavbarSchema.optional(),
|
|
593
|
+
navigation: MintlifyNavigationSchema,
|
|
594
|
+
search: MintlifySearchSchema.optional(),
|
|
595
|
+
seo: DocsSeoSchema.optional(),
|
|
596
|
+
slug: SlugSchema.optional()
|
|
597
|
+
}).strict();
|
|
598
|
+
const ContentTypeSchema = z.enum([
|
|
599
|
+
"site",
|
|
600
|
+
"blog",
|
|
601
|
+
"docs",
|
|
602
|
+
"courses",
|
|
603
|
+
"products",
|
|
604
|
+
"notes",
|
|
605
|
+
"forms",
|
|
606
|
+
"sheets",
|
|
607
|
+
"slides",
|
|
608
|
+
"todos"
|
|
609
|
+
]);
|
|
610
|
+
const FrontmatterBaseSchema = z.object({
|
|
611
|
+
description: z.string().optional(),
|
|
612
|
+
hidden: z.boolean().optional(),
|
|
613
|
+
title: z.string().min(1)
|
|
614
|
+
}).passthrough();
|
|
615
|
+
FrontmatterBaseSchema.extend({
|
|
616
|
+
date: z.string().min(1),
|
|
617
|
+
tags: z.array(z.string()).optional()
|
|
618
|
+
}).passthrough();
|
|
619
|
+
FrontmatterBaseSchema.extend({ order: z.number() }).passthrough();
|
|
620
|
+
FrontmatterBaseSchema.extend({
|
|
621
|
+
currency: z.string().min(1),
|
|
622
|
+
price: z.number(),
|
|
623
|
+
sku: z.string().min(1)
|
|
624
|
+
}).passthrough();
|
|
625
|
+
FrontmatterBaseSchema.extend({ date: z.string().min(1) }).passthrough();
|
|
626
|
+
const FormFieldSchema = z.object({
|
|
627
|
+
id: z.string().min(1),
|
|
628
|
+
label: z.string().min(1),
|
|
629
|
+
options: z.array(z.string()).optional(),
|
|
630
|
+
required: z.boolean().optional(),
|
|
631
|
+
type: z.string().min(1)
|
|
632
|
+
}).passthrough();
|
|
633
|
+
FrontmatterBaseSchema.extend({ fields: z.array(FormFieldSchema).min(1) }).passthrough();
|
|
634
|
+
FrontmatterBaseSchema.extend({ columns: z.array(z.string()).min(1) }).passthrough();
|
|
635
|
+
FrontmatterBaseSchema.extend({ date: z.string().min(1) }).passthrough();
|
|
636
|
+
const PageModeSchema = z.enum([
|
|
637
|
+
"default",
|
|
638
|
+
"wide",
|
|
639
|
+
"custom",
|
|
640
|
+
"frame",
|
|
641
|
+
"center"
|
|
642
|
+
]);
|
|
643
|
+
FrontmatterBaseSchema.extend({
|
|
644
|
+
deprecated: z.boolean().optional(),
|
|
645
|
+
hideApiMarker: z.boolean().optional(),
|
|
646
|
+
hideFooterPagination: z.boolean().optional(),
|
|
647
|
+
icon: z.string().optional(),
|
|
648
|
+
iconType: z.enum([
|
|
649
|
+
"regular",
|
|
650
|
+
"solid",
|
|
651
|
+
"light",
|
|
652
|
+
"thin",
|
|
653
|
+
"sharp-solid",
|
|
654
|
+
"duotone",
|
|
655
|
+
"brands"
|
|
656
|
+
]).optional(),
|
|
657
|
+
keywords: z.array(z.string()).optional(),
|
|
658
|
+
mode: PageModeSchema.optional(),
|
|
659
|
+
noindex: z.boolean().optional(),
|
|
660
|
+
sidebarTitle: z.string().optional(),
|
|
661
|
+
tag: z.string().optional(),
|
|
662
|
+
url: z.string().url().optional()
|
|
663
|
+
}).passthrough();
|
|
664
|
+
const CollectionIndexSchema = z.object({
|
|
665
|
+
description: z.string().optional(),
|
|
666
|
+
hidden: z.boolean().optional(),
|
|
667
|
+
slug: z.string().min(1),
|
|
668
|
+
title: z.string().optional()
|
|
669
|
+
}).strict();
|
|
670
|
+
const CollectionSortSchema = z.object({
|
|
671
|
+
direction: z.enum(["asc", "desc"]).optional(),
|
|
672
|
+
field: z.enum([
|
|
673
|
+
"date",
|
|
674
|
+
"order",
|
|
675
|
+
"title",
|
|
676
|
+
"price"
|
|
677
|
+
]).optional()
|
|
678
|
+
}).strict();
|
|
679
|
+
const CollectionConfigSchema = z.object({
|
|
680
|
+
id: z.string().min(1),
|
|
681
|
+
index: CollectionIndexSchema.optional(),
|
|
682
|
+
navigation: DocsNavigationSchema.optional(),
|
|
683
|
+
openapi: z.union([
|
|
684
|
+
z.string(),
|
|
685
|
+
z.array(z.string()),
|
|
686
|
+
DocsOpenApiSourceSchema
|
|
687
|
+
]).optional(),
|
|
688
|
+
root: z.string().optional(),
|
|
689
|
+
slugPrefix: z.string().optional(),
|
|
690
|
+
sort: CollectionSortSchema.optional(),
|
|
691
|
+
type: ContentTypeSchema
|
|
692
|
+
}).strict();
|
|
693
|
+
const SiteConfigSchema = z.object({
|
|
694
|
+
collections: z.array(CollectionConfigSchema).min(1),
|
|
695
|
+
colors: DocsColorsSchema.optional(),
|
|
696
|
+
contextual: DocsContextualSchema.optional(),
|
|
697
|
+
description: z.string().optional(),
|
|
698
|
+
favicon: UrlOrPathSchema.optional(),
|
|
699
|
+
features: DocsFeatureFlagsSchema.optional(),
|
|
700
|
+
fonts: DocsFontsSchema.optional(),
|
|
701
|
+
logo: DocsLogoSchema.optional(),
|
|
702
|
+
metadata: z.object({
|
|
703
|
+
defaultTitle: z.string().optional(),
|
|
704
|
+
ogImage: UrlOrPathSchema.optional(),
|
|
705
|
+
titleTemplate: z.string().optional()
|
|
706
|
+
}).strict().optional(),
|
|
707
|
+
name: z.string().min(1),
|
|
708
|
+
navigation: DocsNavigationSchema.optional(),
|
|
709
|
+
openapiProxy: DocsOpenApiProxySchema.optional(),
|
|
710
|
+
scripts: DocsScriptsSchema.optional(),
|
|
711
|
+
seo: DocsSeoSchema.optional(),
|
|
712
|
+
slug: SlugSchema.optional(),
|
|
713
|
+
theme: z.string().optional()
|
|
714
|
+
}).strict();
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region ../../packages/validation/dist/index.js
|
|
717
|
+
const formatIssues = (issues) => issues.map((issue) => {
|
|
718
|
+
return `${issue.path.length ? issue.path.map(String).join(".") : "root"}: ${issue.message}`;
|
|
719
|
+
});
|
|
720
|
+
const validateSiteConfig = (input) => {
|
|
721
|
+
const result = SiteConfigSchema.safeParse(input);
|
|
722
|
+
if (result.success) return {
|
|
723
|
+
data: result.data,
|
|
724
|
+
success: true
|
|
725
|
+
};
|
|
726
|
+
return {
|
|
727
|
+
errors: formatIssues(result.error.issues),
|
|
728
|
+
success: false
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
const validateDocsConfig = (input) => {
|
|
732
|
+
const result = DocsConfigSchema.safeParse(input);
|
|
733
|
+
if (result.success) return {
|
|
734
|
+
data: result.data,
|
|
735
|
+
success: true
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
errors: formatIssues(result.error.issues),
|
|
739
|
+
success: false
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region ../../packages/previewing/dist/fs-source.js
|
|
744
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
745
|
+
"app",
|
|
746
|
+
"lib",
|
|
747
|
+
"node_modules",
|
|
748
|
+
"public"
|
|
749
|
+
]);
|
|
750
|
+
const isNotFoundError = (error) => Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
751
|
+
const isWithinRoot = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
752
|
+
const resolveWithinRoot = (root, relativePath) => {
|
|
753
|
+
const normalized = normalizePath(relativePath);
|
|
754
|
+
const absolutePath = path.resolve(root, normalized);
|
|
755
|
+
if (!isWithinRoot(root, absolutePath)) throw new Error(`Path "${relativePath}" escapes the content source root.`);
|
|
756
|
+
return absolutePath;
|
|
757
|
+
};
|
|
758
|
+
const walkFiles = async (directory, prefix) => {
|
|
759
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
760
|
+
const files = [];
|
|
761
|
+
for (const entry of entries) {
|
|
762
|
+
if (entry.name.startsWith(".")) continue;
|
|
763
|
+
const absolutePath = path.join(directory, entry.name);
|
|
764
|
+
const relativePath = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
765
|
+
if (entry.isDirectory()) {
|
|
766
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
767
|
+
files.push(...await walkFiles(absolutePath, relativePath));
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (entry.isFile()) {
|
|
771
|
+
if (!prefix && shouldIgnoreRootDocsFile(entry.name)) continue;
|
|
772
|
+
files.push(normalizePath(relativePath));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return files;
|
|
776
|
+
};
|
|
777
|
+
var FsContentSource = class {
|
|
778
|
+
root;
|
|
779
|
+
constructor(root) {
|
|
780
|
+
this.root = path.resolve(root);
|
|
781
|
+
}
|
|
782
|
+
async readFile(relativePath) {
|
|
783
|
+
return await fs.readFile(resolveWithinRoot(this.root, relativePath), "utf8");
|
|
784
|
+
}
|
|
785
|
+
async listFiles(directory) {
|
|
786
|
+
return await walkFiles(resolveWithinRoot(this.root, directory), "");
|
|
787
|
+
}
|
|
788
|
+
async exists(relativePath) {
|
|
789
|
+
try {
|
|
790
|
+
await fs.access(resolveWithinRoot(this.root, relativePath));
|
|
791
|
+
return true;
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (isNotFoundError(error)) return false;
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
resolveUrl() {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
const createFsSource = (root) => new FsContentSource(root);
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region ../../packages/previewing/dist/index.js
|
|
804
|
+
const LEGACY_PROJECT_NAME_FALLBACK_WARNING$1 = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
|
|
805
|
+
new Set(PageModeSchema.options);
|
|
806
|
+
const DOCS_CONFIG_FILE = "docs.json";
|
|
807
|
+
const defaultLinkLabel = (input) => {
|
|
808
|
+
if (input.label) return input.label;
|
|
809
|
+
if (input.type === "github") return "GitHub";
|
|
810
|
+
if (input.type === "discord") return "Discord";
|
|
811
|
+
try {
|
|
812
|
+
return new URL(input.href).hostname;
|
|
813
|
+
} catch {
|
|
814
|
+
return input.href;
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
const mapDocsConfig = (docs) => {
|
|
818
|
+
const navigation = {
|
|
819
|
+
global: docs.navbar?.links?.length || docs.navigation.global?.anchors?.length ? {
|
|
820
|
+
anchors: docs.navigation.global?.anchors?.map((anchor) => ({
|
|
821
|
+
href: anchor.href,
|
|
822
|
+
label: anchor.anchor
|
|
823
|
+
})),
|
|
824
|
+
links: docs.navbar?.links?.map((link) => ({
|
|
825
|
+
href: link.href,
|
|
826
|
+
label: defaultLinkLabel(link)
|
|
827
|
+
}))
|
|
828
|
+
} : void 0,
|
|
829
|
+
groups: docs.navigation.groups?.map((group) => ({
|
|
830
|
+
expanded: group.expanded,
|
|
831
|
+
group: group.group,
|
|
832
|
+
hidden: group.hidden,
|
|
833
|
+
pages: group.root ? [group.root, ...(group.pages ?? []).filter((page) => page !== group.root)] : group.pages
|
|
834
|
+
})),
|
|
835
|
+
languages: docs.navigation.languages?.map((language) => ({
|
|
836
|
+
label: language.language,
|
|
837
|
+
locale: language.language,
|
|
838
|
+
url: language.href
|
|
839
|
+
})),
|
|
840
|
+
pages: docs.navigation.pages,
|
|
841
|
+
tabs: docs.navigation.tabs?.map((tab) => ({
|
|
842
|
+
groups: tab.groups?.map((group) => ({
|
|
843
|
+
expanded: group.expanded,
|
|
844
|
+
group: group.group,
|
|
845
|
+
hidden: group.hidden,
|
|
846
|
+
pages: group.root ? [group.root, ...(group.pages ?? []).filter((page) => page !== group.root)] : group.pages
|
|
847
|
+
})),
|
|
848
|
+
href: tab.href,
|
|
849
|
+
icon: tab.icon,
|
|
850
|
+
label: tab.tab,
|
|
851
|
+
pages: tab.pages
|
|
852
|
+
})),
|
|
853
|
+
versions: docs.navigation.versions?.map((version) => ({
|
|
854
|
+
label: version.version,
|
|
855
|
+
url: version.href
|
|
856
|
+
}))
|
|
857
|
+
};
|
|
858
|
+
return {
|
|
859
|
+
collections: [{
|
|
860
|
+
id: "docs",
|
|
861
|
+
navigation,
|
|
862
|
+
openapi: docs.api?.openapi,
|
|
863
|
+
root: "",
|
|
864
|
+
type: "docs"
|
|
865
|
+
}],
|
|
866
|
+
contextual: docs.contextual,
|
|
867
|
+
description: docs.description,
|
|
868
|
+
favicon: typeof docs.favicon === "string" ? docs.favicon : docs.favicon?.light,
|
|
869
|
+
features: {
|
|
870
|
+
rightToc: true,
|
|
871
|
+
search: true,
|
|
872
|
+
themeToggle: docs.appearance?.strict !== true,
|
|
873
|
+
toc: true
|
|
874
|
+
},
|
|
875
|
+
logo: docs.logo ? {
|
|
876
|
+
dark: typeof docs.logo === "string" ? docs.logo : docs.logo.dark,
|
|
877
|
+
href: typeof docs.logo === "string" ? void 0 : docs.logo.href,
|
|
878
|
+
light: typeof docs.logo === "string" ? docs.logo : docs.logo.light
|
|
879
|
+
} : void 0,
|
|
880
|
+
name: docs.name,
|
|
881
|
+
navigation,
|
|
882
|
+
openapiProxy: { enabled: docs.api?.playground?.proxy !== false && Boolean(docs.api?.openapi || docs.api?.asyncapi) },
|
|
883
|
+
seo: docs.seo,
|
|
884
|
+
slug: docs.slug
|
|
885
|
+
};
|
|
886
|
+
};
|
|
887
|
+
const getProjectWarnings = (config) => config.slug ? [] : [LEGACY_PROJECT_NAME_FALLBACK_WARNING$1];
|
|
888
|
+
const readJsonConfig = async (source, relativePath) => JSON.parse(await source.readFile(relativePath));
|
|
889
|
+
const normalizeRefPath = (baseDirectory, reference) => {
|
|
890
|
+
if (reference.startsWith("/") || reference.startsWith("\\") || reference.startsWith("http://") || reference.startsWith("https://")) throw new Error(`Invalid $ref "${reference}". Only relative JSON files are supported.`);
|
|
891
|
+
const normalized = normalizePath(path.posix.join(baseDirectory, reference));
|
|
892
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) throw new Error(`Invalid $ref "${reference}".`);
|
|
893
|
+
return normalized;
|
|
894
|
+
};
|
|
895
|
+
const resolveJsonRefs = async (source, value, baseDirectory, seen) => {
|
|
896
|
+
if (Array.isArray(value)) return await Promise.all(value.map((item) => resolveJsonRefs(source, item, baseDirectory, seen)));
|
|
897
|
+
if (!value || typeof value !== "object") return value;
|
|
898
|
+
const record = value;
|
|
899
|
+
const reference = record.$ref;
|
|
900
|
+
if (typeof reference === "string") {
|
|
901
|
+
const resolvedPath = normalizeRefPath(baseDirectory, reference);
|
|
902
|
+
if (seen.has(resolvedPath)) throw new Error(`Circular $ref detected for "${resolvedPath}".`);
|
|
903
|
+
const nextSeen = new Set(seen);
|
|
904
|
+
nextSeen.add(resolvedPath);
|
|
905
|
+
const referencedValue = await resolveJsonRefs(source, await readJsonConfig(source, resolvedPath), path.posix.dirname(resolvedPath) === "." ? "" : normalizePath(path.posix.dirname(resolvedPath)), nextSeen);
|
|
906
|
+
const siblingEntries = Object.entries(record).filter(([key]) => key !== "$ref");
|
|
907
|
+
if (!siblingEntries.length || !referencedValue || typeof referencedValue !== "object" || Array.isArray(referencedValue)) return referencedValue;
|
|
908
|
+
const siblingValue = await resolveJsonRefs(source, Object.fromEntries(siblingEntries), baseDirectory, seen);
|
|
909
|
+
return {
|
|
910
|
+
...referencedValue,
|
|
911
|
+
...siblingValue
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const resolvedEntries = await Promise.all(Object.entries(record).map(async ([key, entryValue]) => [key, await resolveJsonRefs(source, entryValue, baseDirectory, seen)]));
|
|
915
|
+
return Object.fromEntries(resolvedEntries);
|
|
916
|
+
};
|
|
917
|
+
const readResolvedJsonConfig = async (source, relativePath) => await resolveJsonRefs(source, await readJsonConfig(source, relativePath), path.posix.dirname(relativePath) === "." ? "" : normalizePath(path.posix.dirname(relativePath)), new Set([relativePath]));
|
|
918
|
+
const loadDocsConfig = async (source) => {
|
|
919
|
+
if (!await source.exists(DOCS_CONFIG_FILE)) return null;
|
|
920
|
+
try {
|
|
921
|
+
const parsed = await readResolvedJsonConfig(source, DOCS_CONFIG_FILE);
|
|
922
|
+
const siteResult = validateSiteConfig(parsed);
|
|
923
|
+
if (siteResult.success) return {
|
|
924
|
+
config: siteResult.data,
|
|
925
|
+
ok: true,
|
|
926
|
+
warnings: getProjectWarnings(siteResult.data)
|
|
927
|
+
};
|
|
928
|
+
const docsResult = validateDocsConfig(parsed);
|
|
929
|
+
if (docsResult.success) return {
|
|
930
|
+
config: mapDocsConfig(docsResult.data),
|
|
931
|
+
ok: true,
|
|
932
|
+
warnings: getProjectWarnings(docsResult.data)
|
|
933
|
+
};
|
|
934
|
+
return {
|
|
935
|
+
errors: docsResult.errors,
|
|
936
|
+
ok: false
|
|
937
|
+
};
|
|
938
|
+
} catch (error) {
|
|
939
|
+
return {
|
|
940
|
+
errors: [error instanceof Error ? error.message : `Failed to load ${DOCS_CONFIG_FILE}`],
|
|
941
|
+
ok: false
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const loadSiteConfig = async (source) => {
|
|
946
|
+
const docsConfig = await loadDocsConfig(source);
|
|
947
|
+
if (docsConfig) return docsConfig;
|
|
948
|
+
return {
|
|
949
|
+
errors: [`${DOCS_CONFIG_FILE} not found.`],
|
|
950
|
+
ok: false
|
|
951
|
+
};
|
|
952
|
+
};
|
|
953
|
+
//#endregion
|
|
336
954
|
//#region src/site-config.ts
|
|
337
955
|
const CONFIG_FILE$2 = "docs.json";
|
|
338
956
|
const getSiteConfigHint = (errors) => {
|
|
@@ -831,10 +1449,48 @@ const findExistingPaths = async (root, relativePaths) => {
|
|
|
831
1449
|
}))).filter((relativePath) => relativePath !== null).toSorted((left, right) => left.localeCompare(right));
|
|
832
1450
|
};
|
|
833
1451
|
//#endregion
|
|
1452
|
+
//#region src/project-config.ts
|
|
1453
|
+
const LEGACY_PROJECT_NAME_FALLBACK_WARNING = "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
|
|
1454
|
+
const validateProjectSlug = (value) => {
|
|
1455
|
+
const trimmed = value?.trim();
|
|
1456
|
+
if (!trimmed) return "Project slug is required.";
|
|
1457
|
+
const normalized = slugify(trimmed);
|
|
1458
|
+
if (!normalized) return "Use at least one letter or number.";
|
|
1459
|
+
if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
|
|
1460
|
+
};
|
|
1461
|
+
const deriveDisplayNameFromProjectSlug = (projectSlug) => projectSlug.split("-").filter(Boolean).map((segment) => segment[0]?.toUpperCase() + segment.slice(1)).join(" ");
|
|
1462
|
+
const resolveProjectTarget = (options) => {
|
|
1463
|
+
if (options.cliProject) return {
|
|
1464
|
+
project: options.cliProject,
|
|
1465
|
+
usedLegacyNameFallback: false
|
|
1466
|
+
};
|
|
1467
|
+
if (options.envProject) return {
|
|
1468
|
+
project: options.envProject,
|
|
1469
|
+
usedLegacyNameFallback: false
|
|
1470
|
+
};
|
|
1471
|
+
if (options.config.slug) return {
|
|
1472
|
+
project: options.config.slug,
|
|
1473
|
+
usedLegacyNameFallback: false
|
|
1474
|
+
};
|
|
1475
|
+
if (options.config.name) return {
|
|
1476
|
+
project: options.config.name,
|
|
1477
|
+
usedLegacyNameFallback: true
|
|
1478
|
+
};
|
|
1479
|
+
return {
|
|
1480
|
+
project: void 0,
|
|
1481
|
+
usedLegacyNameFallback: false
|
|
1482
|
+
};
|
|
1483
|
+
};
|
|
1484
|
+
const getProjectSlugError = (project) => {
|
|
1485
|
+
if (!project) return;
|
|
1486
|
+
return validateProjectSlug(project);
|
|
1487
|
+
};
|
|
1488
|
+
//#endregion
|
|
834
1489
|
//#region src/scaffold.ts
|
|
835
1490
|
const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
|
|
836
1491
|
const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
|
|
837
1492
|
const stringifyJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
1493
|
+
const escapeXmlText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
838
1494
|
const isScaffoldTemplate = (value) => SCAFFOLD_TEMPLATES.includes(value);
|
|
839
1495
|
const normalizeProjectSlug = (value) => slugify(value) || "my-project";
|
|
840
1496
|
const resolveScaffoldDirectory = (directory) => directory?.trim() || "docs";
|
|
@@ -843,22 +1499,16 @@ const deriveDefaultProjectSlug = (directory, cwd) => {
|
|
|
843
1499
|
if (resolvedDirectory === "." || resolvedDirectory === "docs") return normalizeProjectSlug(path.basename(cwd));
|
|
844
1500
|
return normalizeProjectSlug(path.basename(path.resolve(cwd, resolvedDirectory)));
|
|
845
1501
|
};
|
|
846
|
-
const
|
|
847
|
-
const trimmed = value?.trim();
|
|
848
|
-
if (!trimmed) return "Project slug is required.";
|
|
849
|
-
const normalized = slugify(trimmed);
|
|
850
|
-
if (!normalized) return "Use at least one letter or number.";
|
|
851
|
-
if (normalized !== trimmed) return `Use lowercase letters, numbers, and hyphens. Try "${normalized}".`;
|
|
852
|
-
};
|
|
853
|
-
const createMinimalDocsJson = (projectSlug) => ({
|
|
1502
|
+
const createMinimalDocsJson = (projectSlug, displayName) => ({
|
|
854
1503
|
$schema: "https://blode.md/docs.json",
|
|
855
|
-
name:
|
|
1504
|
+
name: displayName,
|
|
856
1505
|
navigation: { groups: [{
|
|
857
1506
|
group: "Getting Started",
|
|
858
1507
|
pages: ["index"]
|
|
859
|
-
}] }
|
|
1508
|
+
}] },
|
|
1509
|
+
slug: projectSlug
|
|
860
1510
|
});
|
|
861
|
-
const createStarterDocsJson = (projectSlug) => ({
|
|
1511
|
+
const createStarterDocsJson = (projectSlug, displayName) => ({
|
|
862
1512
|
$schema: "https://blode.md/docs.json",
|
|
863
1513
|
appearance: { default: "system" },
|
|
864
1514
|
contextual: { options: [
|
|
@@ -870,12 +1520,12 @@ const createStarterDocsJson = (projectSlug) => ({
|
|
|
870
1520
|
description: "Ship documentation from your terminal.",
|
|
871
1521
|
favicon: "/favicon.svg",
|
|
872
1522
|
logo: {
|
|
873
|
-
alt: `${
|
|
1523
|
+
alt: `${displayName} logo`,
|
|
874
1524
|
dark: "/logo/dark.svg",
|
|
875
1525
|
light: "/logo/light.svg"
|
|
876
1526
|
},
|
|
877
1527
|
metadata: { timestamp: true },
|
|
878
|
-
name:
|
|
1528
|
+
name: displayName,
|
|
879
1529
|
navigation: { groups: [{
|
|
880
1530
|
group: "Getting Started",
|
|
881
1531
|
pages: [
|
|
@@ -883,7 +1533,8 @@ const createStarterDocsJson = (projectSlug) => ({
|
|
|
883
1533
|
"quickstart",
|
|
884
1534
|
"development"
|
|
885
1535
|
]
|
|
886
|
-
}] }
|
|
1536
|
+
}] },
|
|
1537
|
+
slug: projectSlug
|
|
887
1538
|
});
|
|
888
1539
|
const claudeInstructions = [
|
|
889
1540
|
"> **First-time setup**: Customize this file for your project. Prompt the user to update terminology, style preferences, and content boundaries before drafting large amounts of docs.",
|
|
@@ -927,270 +1578,276 @@ const claudeInstructions = [
|
|
|
927
1578
|
"- Run `blodemd validate` before publishing.",
|
|
928
1579
|
""
|
|
929
1580
|
].join("\n");
|
|
930
|
-
const createMinimalFiles = (projectSlug) => [{
|
|
931
|
-
content: stringifyJson(createMinimalDocsJson(projectSlug)),
|
|
1581
|
+
const createMinimalFiles = (projectSlug, displayName) => [{
|
|
1582
|
+
content: stringifyJson(createMinimalDocsJson(projectSlug, displayName)),
|
|
932
1583
|
path: "docs.json"
|
|
933
1584
|
}, {
|
|
934
1585
|
content: "---\ntitle: Welcome\n---\n\nStart writing your docs here.\n",
|
|
935
1586
|
path: "index.mdx"
|
|
936
1587
|
}];
|
|
937
|
-
const createStarterFiles = (projectSlug) =>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
"
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1588
|
+
const createStarterFiles = (projectSlug, displayName) => {
|
|
1589
|
+
const escapedDisplayName = escapeXmlText(displayName);
|
|
1590
|
+
return [
|
|
1591
|
+
{
|
|
1592
|
+
content: stringifyJson(createStarterDocsJson(projectSlug, displayName)),
|
|
1593
|
+
path: "docs.json"
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
content: [
|
|
1597
|
+
"---",
|
|
1598
|
+
"title: Welcome",
|
|
1599
|
+
"description: Start here.",
|
|
1600
|
+
"---",
|
|
1601
|
+
"",
|
|
1602
|
+
"# Welcome",
|
|
1603
|
+
"",
|
|
1604
|
+
"This starter gives you branded assets, repo helper files, and a small docs structure you can rewrite quickly.",
|
|
1605
|
+
"",
|
|
1606
|
+
"",
|
|
1607
|
+
"",
|
|
1608
|
+
"## What is included",
|
|
1609
|
+
"",
|
|
1610
|
+
"- A starter `docs.json` with branding, contextual actions, and navigation.",
|
|
1611
|
+
"- Placeholder brand assets in `/logo` and `/images`.",
|
|
1612
|
+
"- Repo helper files like `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`.",
|
|
1613
|
+
"",
|
|
1614
|
+
"## Next steps",
|
|
1615
|
+
"",
|
|
1616
|
+
"- Confirm `slug` in `docs.json` matches your deployment target.",
|
|
1617
|
+
"- Update `name` in `docs.json` to match the visible product or docs brand.",
|
|
1618
|
+
"- Set `description` in `docs.json` to explain your product.",
|
|
1619
|
+
"- Replace the files in `/logo` and `/images` with your own brand assets.",
|
|
1620
|
+
"- Rewrite `CLAUDE.md` with your terminology and writing standards.",
|
|
1621
|
+
"- Update this page, then preview locally with `blodemd dev`.",
|
|
1622
|
+
"",
|
|
1623
|
+
"## Included pages",
|
|
1624
|
+
"",
|
|
1625
|
+
"- [Quickstart](quickstart)",
|
|
1626
|
+
"- [Development](development)",
|
|
1627
|
+
""
|
|
1628
|
+
].join("\n"),
|
|
1629
|
+
path: "index.mdx"
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
content: [
|
|
1633
|
+
"---",
|
|
1634
|
+
"title: Quickstart",
|
|
1635
|
+
"description: Get your docs running fast.",
|
|
1636
|
+
"---",
|
|
1637
|
+
"",
|
|
1638
|
+
"# Quickstart",
|
|
1639
|
+
"",
|
|
1640
|
+
"",
|
|
1641
|
+
"",
|
|
1642
|
+
"1. Confirm `slug` in `docs.json` matches your deployment target.",
|
|
1643
|
+
"2. Update `name` in `docs.json` to match your visible docs brand.",
|
|
1644
|
+
"3. Update the `description` field to match your product.",
|
|
1645
|
+
"4. Replace the assets in `/logo` and `/images`.",
|
|
1646
|
+
"5. Run `blodemd dev` to preview locally.",
|
|
1647
|
+
"6. Run `blodemd push` when you are ready to publish.",
|
|
1648
|
+
""
|
|
1649
|
+
].join("\n"),
|
|
1650
|
+
path: "quickstart.mdx"
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
content: [
|
|
1654
|
+
"---",
|
|
1655
|
+
"title: Development",
|
|
1656
|
+
"description: Work on your docs locally.",
|
|
1657
|
+
"---",
|
|
1658
|
+
"",
|
|
1659
|
+
"# Development",
|
|
1660
|
+
"",
|
|
1661
|
+
"",
|
|
1662
|
+
"",
|
|
1663
|
+
"Preview locally with:",
|
|
1664
|
+
"",
|
|
1665
|
+
"```bash",
|
|
1666
|
+
"blodemd dev",
|
|
1667
|
+
"```",
|
|
1668
|
+
"",
|
|
1669
|
+
"Validate your configuration with:",
|
|
1670
|
+
"",
|
|
1671
|
+
"```bash",
|
|
1672
|
+
"blodemd validate",
|
|
1673
|
+
"```",
|
|
1674
|
+
"",
|
|
1675
|
+
"Keep `CLAUDE.md` current as your product terminology and writing rules evolve.",
|
|
1676
|
+
""
|
|
1677
|
+
].join("\n"),
|
|
1678
|
+
path: "development.mdx"
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
content: [
|
|
1682
|
+
"# Documentation starter",
|
|
1683
|
+
"",
|
|
1684
|
+
"This directory was scaffolded with `blodemd new --template starter`.",
|
|
1685
|
+
"",
|
|
1686
|
+
"## What is included",
|
|
1687
|
+
"",
|
|
1688
|
+
"- `docs.json` with branding, contextual actions, and starter navigation",
|
|
1689
|
+
"- `index.mdx`, `quickstart.mdx`, and `development.mdx`",
|
|
1690
|
+
"- Placeholder brand assets in `/logo` and `/images`",
|
|
1691
|
+
"- Repo helper files: `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`",
|
|
1692
|
+
"",
|
|
1693
|
+
"## Commands",
|
|
1694
|
+
"",
|
|
1695
|
+
"```bash",
|
|
1696
|
+
"blodemd dev",
|
|
1697
|
+
"blodemd validate",
|
|
1698
|
+
"blodemd push",
|
|
1699
|
+
"```",
|
|
1700
|
+
"",
|
|
1701
|
+
"## Customize",
|
|
1702
|
+
"",
|
|
1703
|
+
"- Confirm `slug` in `docs.json` and set the display `name` and description.",
|
|
1704
|
+
"- Replace the assets in `/logo` and `/images`.",
|
|
1705
|
+
"- Rewrite `CLAUDE.md` with project-specific terminology and writing rules.",
|
|
1706
|
+
"- Rewrite the starter pages to match your product.",
|
|
1707
|
+
"- Add a `LICENSE` file deliberately if this repo will be public.",
|
|
1708
|
+
""
|
|
1709
|
+
].join("\n"),
|
|
1710
|
+
path: "README.md"
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
fallbackContent: claudeInstructions,
|
|
1714
|
+
path: "AGENTS.md",
|
|
1715
|
+
target: "CLAUDE.md",
|
|
1716
|
+
type: "symlink"
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
content: claudeInstructions,
|
|
1720
|
+
path: "CLAUDE.md"
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
content: [
|
|
1724
|
+
"# dependencies",
|
|
1725
|
+
"node_modules/",
|
|
1726
|
+
"",
|
|
1727
|
+
"# local env files",
|
|
1728
|
+
".env*",
|
|
1729
|
+
"!.env.example",
|
|
1730
|
+
"",
|
|
1731
|
+
"# build and cache",
|
|
1732
|
+
".next/",
|
|
1733
|
+
".turbo/",
|
|
1734
|
+
"coverage/",
|
|
1735
|
+
"dist/",
|
|
1736
|
+
".vercel/",
|
|
1737
|
+
"*.tsbuildinfo",
|
|
1738
|
+
"",
|
|
1739
|
+
"# logs",
|
|
1740
|
+
"*.log",
|
|
1741
|
+
"",
|
|
1742
|
+
"# misc",
|
|
1743
|
+
".DS_Store",
|
|
1744
|
+
""
|
|
1745
|
+
].join("\n"),
|
|
1746
|
+
path: ".gitignore"
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
content: [
|
|
1750
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\">",
|
|
1751
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0D9373\"/>",
|
|
1752
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
|
|
1753
|
+
" <path d=\"M28 26h6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-6V26Z\" fill=\"#0C3A33\"/>",
|
|
1754
|
+
"</svg>",
|
|
1755
|
+
""
|
|
1756
|
+
].join("\n"),
|
|
1757
|
+
path: "favicon.svg"
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
content: [
|
|
1761
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
|
|
1762
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0C3A33\"/>",
|
|
1763
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
|
|
1764
|
+
` <text x="84" y="41" fill="#111827" font-family="Arial, sans-serif" font-size="28" font-weight="700">${escapedDisplayName}</text>`,
|
|
1765
|
+
"</svg>",
|
|
1766
|
+
""
|
|
1767
|
+
].join("\n"),
|
|
1768
|
+
path: "logo/light.svg"
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
content: [
|
|
1772
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
|
|
1773
|
+
" <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#CFF6EE\"/>",
|
|
1774
|
+
" <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#0C3A33\"/>",
|
|
1775
|
+
` <text x="84" y="41" fill="#F9FAFB" font-family="Arial, sans-serif" font-size="28" font-weight="700">${escapedDisplayName}</text>`,
|
|
1776
|
+
"</svg>",
|
|
1777
|
+
""
|
|
1778
|
+
].join("\n"),
|
|
1779
|
+
path: "logo/dark.svg"
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
content: [
|
|
1783
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1784
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F4FBF8\"/>",
|
|
1785
|
+
" <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#E1F4EE\"/>",
|
|
1786
|
+
" <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#0D9373\" opacity=\".25\"/>",
|
|
1787
|
+
" <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
|
|
1788
|
+
" <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
|
|
1789
|
+
" <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1790
|
+
" <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#CFF6EE\"/>",
|
|
1791
|
+
" <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#CFF6EE\" opacity=\".7\"/>",
|
|
1792
|
+
" <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#FFFFFF\"/>",
|
|
1793
|
+
" <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".18\"/>",
|
|
1794
|
+
" <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1795
|
+
" <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
|
|
1796
|
+
" <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".85\"/>",
|
|
1797
|
+
" <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".45\"/>",
|
|
1798
|
+
" <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
|
|
1799
|
+
"</svg>",
|
|
1800
|
+
""
|
|
1801
|
+
].join("\n"),
|
|
1802
|
+
path: "images/hero-light.svg"
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
content: [
|
|
1806
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1807
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#071715\"/>",
|
|
1808
|
+
" <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#0F2E28\"/>",
|
|
1809
|
+
" <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#CFF6EE\" opacity=\".18\"/>",
|
|
1810
|
+
" <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
|
|
1811
|
+
" <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
|
|
1812
|
+
" <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1813
|
+
" <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#E8FFF9\"/>",
|
|
1814
|
+
" <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#E8FFF9\" opacity=\".6\"/>",
|
|
1815
|
+
" <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
|
|
1816
|
+
" <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".22\"/>",
|
|
1817
|
+
" <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".16\"/>",
|
|
1818
|
+
" <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#E9FFF8\"/>",
|
|
1819
|
+
" <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".24\"/>",
|
|
1820
|
+
" <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1821
|
+
" <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
|
|
1822
|
+
"</svg>",
|
|
1823
|
+
""
|
|
1824
|
+
].join("\n"),
|
|
1825
|
+
path: "images/hero-dark.svg"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
content: [
|
|
1829
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
|
|
1830
|
+
" <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F8FCFA\"/>",
|
|
1831
|
+
" <rect x=\"60\" y=\"76\" width=\"840\" height=\"368\" rx=\"28\" fill=\"#FFFFFF\" stroke=\"#D7ECE6\" stroke-width=\"4\"/>",
|
|
1832
|
+
" <rect x=\"108\" y=\"124\" width=\"96\" height=\"96\" rx=\"24\" fill=\"#0D9373\"/>",
|
|
1833
|
+
" <path d=\"M136 172l18 18 38-48\" stroke=\"#CFF6EE\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"18\"/>",
|
|
1834
|
+
" <rect x=\"244\" y=\"132\" width=\"280\" height=\"24\" rx=\"12\" fill=\"#0C3A33\"/>",
|
|
1835
|
+
" <rect x=\"244\" y=\"176\" width=\"416\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".16\"/>",
|
|
1836
|
+
" <rect x=\"244\" y=\"214\" width=\"340\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".12\"/>",
|
|
1837
|
+
" <rect x=\"108\" y=\"280\" width=\"744\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1838
|
+
" <rect x=\"108\" y=\"326\" width=\"520\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1839
|
+
" <rect x=\"108\" y=\"372\" width=\"612\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
|
|
1840
|
+
"</svg>",
|
|
1841
|
+
""
|
|
1842
|
+
].join("\n"),
|
|
1843
|
+
path: "images/checks-passed.svg"
|
|
1844
|
+
}
|
|
1845
|
+
];
|
|
1846
|
+
};
|
|
1191
1847
|
const getScaffoldFiles = (template, options) => {
|
|
1192
1848
|
const projectSlug = options?.projectSlug ?? "my-project";
|
|
1193
|
-
|
|
1849
|
+
const displayName = options?.displayName ?? deriveDisplayNameFromProjectSlug(projectSlug);
|
|
1850
|
+
return template === "starter" ? createStarterFiles(projectSlug, displayName) : createMinimalFiles(projectSlug, displayName);
|
|
1194
1851
|
};
|
|
1195
1852
|
//#endregion
|
|
1196
1853
|
//#region src/new-flow.ts
|
|
@@ -1497,6 +2154,18 @@ const promptForProjectSlug = async (initialValue) => {
|
|
|
1497
2154
|
if (isCancel(projectSlug)) return;
|
|
1498
2155
|
return projectSlug.trim();
|
|
1499
2156
|
};
|
|
2157
|
+
const promptForDisplayName = async (initialValue) => {
|
|
2158
|
+
const displayName = await text({
|
|
2159
|
+
initialValue,
|
|
2160
|
+
message: "Display name",
|
|
2161
|
+
placeholder: initialValue,
|
|
2162
|
+
validate: (value) => {
|
|
2163
|
+
if (!value?.trim()) return "Display name is required.";
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
if (isCancel(displayName)) return;
|
|
2167
|
+
return displayName.trim();
|
|
2168
|
+
};
|
|
1500
2169
|
const resolveRequestedDirectory = async (directory, shouldPrompt) => {
|
|
1501
2170
|
let currentDirectoryEntries = [];
|
|
1502
2171
|
if (!directory && shouldPrompt) currentDirectoryEntries = await fs.readdir(process.cwd());
|
|
@@ -1527,14 +2196,20 @@ const confirmScaffoldTarget = async (root, template, shouldPrompt, options) => {
|
|
|
1527
2196
|
const shouldContinue = await confirm({ message: `Scaffold into the non-empty directory ${root}? Existing files will be left untouched.` });
|
|
1528
2197
|
return !isCancel(shouldContinue) && shouldContinue;
|
|
1529
2198
|
};
|
|
1530
|
-
const resolveProjectSlug = async (
|
|
2199
|
+
const resolveProjectSlug = async (providedSlug, directory, shouldPrompt) => {
|
|
1531
2200
|
const defaultProjectSlug = deriveDefaultProjectSlug(directory, process.cwd());
|
|
1532
|
-
if (
|
|
2201
|
+
if (providedSlug) return providedSlug;
|
|
1533
2202
|
if (!shouldPrompt) return defaultProjectSlug;
|
|
1534
2203
|
return await promptForProjectSlug(defaultProjectSlug);
|
|
1535
2204
|
};
|
|
1536
|
-
const
|
|
1537
|
-
|
|
2205
|
+
const resolveDisplayName = async (providedDisplayName, projectSlug, shouldPrompt) => {
|
|
2206
|
+
const defaultDisplayName = deriveDisplayNameFromProjectSlug(projectSlug);
|
|
2207
|
+
if (providedDisplayName?.trim()) return providedDisplayName.trim();
|
|
2208
|
+
if (!shouldPrompt) return defaultDisplayName;
|
|
2209
|
+
return await promptForDisplayName(defaultDisplayName);
|
|
2210
|
+
};
|
|
2211
|
+
const writeScaffoldFiles = async (root, template, options) => {
|
|
2212
|
+
for (const file of getScaffoldFiles(template, options)) {
|
|
1538
2213
|
const filePath = path.join(root, file.path);
|
|
1539
2214
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1540
2215
|
if (file.type === "symlink") {
|
|
@@ -1552,9 +2227,13 @@ const fetchUserEmail = async (apiUrl, token) => {
|
|
|
1552
2227
|
}
|
|
1553
2228
|
};
|
|
1554
2229
|
const resolvePushConfig = async (config, options) => {
|
|
1555
|
-
const
|
|
2230
|
+
const { project, usedLegacyNameFallback } = resolveProjectTarget({
|
|
2231
|
+
cliProject: options.project,
|
|
2232
|
+
config,
|
|
2233
|
+
envProject: process.env[BLODE_PROJECT_ENV]
|
|
2234
|
+
});
|
|
1556
2235
|
const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
|
|
1557
|
-
const authToken = (await resolveAuthToken(
|
|
2236
|
+
const authToken = (await resolveAuthToken())?.token;
|
|
1558
2237
|
const branch = options.branch ?? process.env["BLODEMD_BRANCH"] ?? process.env.GITHUB_REF_NAME ?? readGitValue([
|
|
1559
2238
|
"rev-parse",
|
|
1560
2239
|
"--abbrev-ref",
|
|
@@ -1565,35 +2244,42 @@ const resolvePushConfig = async (config, options) => {
|
|
|
1565
2244
|
"-1",
|
|
1566
2245
|
"--pretty=%s"
|
|
1567
2246
|
]);
|
|
1568
|
-
if (!project) throw new Error("Missing project slug. Set \"
|
|
1569
|
-
|
|
2247
|
+
if (!project) throw new Error("Missing project slug. Set \"slug\" in docs.json, pass --project, or set BLODEMD_PROJECT.");
|
|
2248
|
+
const projectSlugError = getProjectSlugError(project);
|
|
2249
|
+
if (projectSlugError) {
|
|
2250
|
+
if (usedLegacyNameFallback) throw new Error(`docs.json.name is not a valid deployment slug. Add "slug" to docs.json, pass --project, or set BLODEMD_PROJECT. ${projectSlugError}`);
|
|
2251
|
+
throw new Error(`Invalid project slug "${project}". ${projectSlugError}`);
|
|
2252
|
+
}
|
|
2253
|
+
if (!authToken) throw new Error("Not logged in. Run \"blodemd login\" to authenticate.");
|
|
1570
2254
|
return {
|
|
1571
2255
|
apiUrl,
|
|
1572
2256
|
authToken,
|
|
1573
2257
|
branch,
|
|
1574
2258
|
commitMessage,
|
|
1575
|
-
project
|
|
2259
|
+
project,
|
|
2260
|
+
projectDisplayName: config.name?.trim() || project,
|
|
2261
|
+
usedLegacyNameFallback
|
|
1576
2262
|
};
|
|
1577
2263
|
};
|
|
1578
|
-
const autoCreateProject = async (project, apiUrl, headers) => {
|
|
2264
|
+
const autoCreateProject = async (project, projectDisplayName, apiUrl, headers) => {
|
|
1579
2265
|
if (!(await readAuthFile())?.session) throw new Error(`Project "${project}" not found. Create it at blode.md or login with "blodemd login" to auto-create.`);
|
|
1580
2266
|
const shouldCreate = await confirm({ message: `Project "${project}" doesn't exist. Create it?` });
|
|
1581
2267
|
if (isCancel(shouldCreate) || !shouldCreate) return false;
|
|
1582
2268
|
const createResult = await requestJson(new URL("/projects", apiUrl).toString(), {
|
|
1583
2269
|
body: JSON.stringify({
|
|
1584
|
-
name:
|
|
2270
|
+
name: projectDisplayName,
|
|
1585
2271
|
slug: project
|
|
1586
2272
|
}),
|
|
1587
2273
|
headers,
|
|
1588
2274
|
method: "POST"
|
|
1589
2275
|
}, "Failed to create project");
|
|
1590
|
-
log.success(`Project ${chalk.cyan(createResult.
|
|
1591
|
-
log.info(`API key for CI: ${chalk.dim(createResult.token)}`);
|
|
2276
|
+
log.success(`Project ${chalk.cyan(createResult.slug)} created`);
|
|
1592
2277
|
return true;
|
|
1593
2278
|
};
|
|
1594
2279
|
const scaffoldDocsSite = async (directory, options) => {
|
|
1595
2280
|
intro(chalk.bold("blodemd new"));
|
|
1596
2281
|
if (options?.deprecatedCommand) log.warn(`"${options.deprecatedCommand}" is deprecated. Use ${chalk.cyan("blodemd new")} instead.`);
|
|
2282
|
+
if (options?.name && !options.slug) log.warn(`"${chalk.cyan("--name")}" is deprecated. Use ${chalk.cyan("--slug")} instead.`);
|
|
1597
2283
|
try {
|
|
1598
2284
|
const template = options?.template ?? "minimal";
|
|
1599
2285
|
const shouldPrompt = isInteractiveTerminal() && !options?.yes;
|
|
@@ -1608,15 +2294,24 @@ const scaffoldDocsSite = async (directory, options) => {
|
|
|
1608
2294
|
log.warn("Cancelled");
|
|
1609
2295
|
return;
|
|
1610
2296
|
}
|
|
1611
|
-
const projectSlug = await resolveProjectSlug(options?.name, resolvedDirectory, shouldPrompt);
|
|
2297
|
+
const projectSlug = await resolveProjectSlug(options?.slug ?? options?.name, resolvedDirectory, shouldPrompt);
|
|
1612
2298
|
if (!projectSlug) {
|
|
1613
2299
|
log.warn("Cancelled");
|
|
1614
2300
|
return;
|
|
1615
2301
|
}
|
|
2302
|
+
const displayName = await resolveDisplayName(options?.displayName, projectSlug, shouldPrompt);
|
|
2303
|
+
if (!displayName) {
|
|
2304
|
+
log.warn("Cancelled");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
1616
2307
|
await fs.mkdir(root, { recursive: true });
|
|
1617
|
-
await writeScaffoldFiles(root, template,
|
|
2308
|
+
await writeScaffoldFiles(root, template, {
|
|
2309
|
+
displayName,
|
|
2310
|
+
projectSlug
|
|
2311
|
+
});
|
|
1618
2312
|
log.success(`Docs scaffolded in ${chalk.cyan(root)}`);
|
|
1619
2313
|
if (template === "starter") log.info("Starter template includes brand assets and helper files.");
|
|
2314
|
+
log.info(`Display name: ${chalk.cyan(displayName)}`);
|
|
1620
2315
|
log.info(`Project slug: ${chalk.cyan(projectSlug)}`);
|
|
1621
2316
|
log.info("Done");
|
|
1622
2317
|
} catch (error) {
|
|
@@ -1648,29 +2343,9 @@ program.name("blodemd").description("Blode.md CLI").version(cliVersion);
|
|
|
1648
2343
|
program.hook("preAction", () => {
|
|
1649
2344
|
assertSupportedNodeVersion();
|
|
1650
2345
|
});
|
|
1651
|
-
program.command("login").description("Authenticate with Blode.md
|
|
2346
|
+
program.command("login").description("Authenticate with Blode.md via GitHub in your browser").option("--port <port>", "Loopback callback port", String(DEFAULT_OAUTH_CALLBACK_PORT)).option("--timeout <seconds>", "OAuth timeout in seconds", String(180)).option("--no-open", "Print URL instead of opening the browser").action(async (options) => {
|
|
1652
2347
|
intro(chalk.bold("blodemd login"));
|
|
1653
2348
|
try {
|
|
1654
|
-
if (options.token) {
|
|
1655
|
-
const apiKey = await password({
|
|
1656
|
-
message: "Enter your API key",
|
|
1657
|
-
validate: (value) => {
|
|
1658
|
-
if (!value) return "API key is required.";
|
|
1659
|
-
}
|
|
1660
|
-
});
|
|
1661
|
-
if (isCancel(apiKey)) {
|
|
1662
|
-
log.warn("Cancelled");
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
await writeStoredApiKey({
|
|
1666
|
-
apiKey,
|
|
1667
|
-
type: "api-key"
|
|
1668
|
-
});
|
|
1669
|
-
const prefix = apiKey.split(".")[0] ?? apiKey.slice(0, 12);
|
|
1670
|
-
log.success(`Authenticated as ${chalk.cyan(prefix)}`);
|
|
1671
|
-
log.info("Done");
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
2349
|
const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
1675
2350
|
const clientId = OAUTH_CLIENT_ID;
|
|
1676
2351
|
const port = parsePort(options.port);
|
|
@@ -1687,6 +2362,7 @@ program.command("login").description("Authenticate with Blode.md").option("--tok
|
|
|
1687
2362
|
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1688
2363
|
authUrl.searchParams.set("state", state);
|
|
1689
2364
|
authUrl.searchParams.set("scope", "openid email profile");
|
|
2365
|
+
authUrl.searchParams.set("provider", "github");
|
|
1690
2366
|
const callbackPromise = waitForOAuthCode({
|
|
1691
2367
|
expectedState: state,
|
|
1692
2368
|
redirectUrl,
|
|
@@ -1739,15 +2415,6 @@ program.command("whoami").description("Show current authentication").action(asyn
|
|
|
1739
2415
|
log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
|
|
1740
2416
|
return;
|
|
1741
2417
|
}
|
|
1742
|
-
if (resolved.source === "environment") {
|
|
1743
|
-
log.info("Authenticated via BLODEMD_API_KEY environment variable");
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
if (!resolved.expiresAt && !resolved.user) {
|
|
1747
|
-
const prefix = resolved.token.split(".")[0] ?? resolved.token.slice(0, 12);
|
|
1748
|
-
log.info(`Logged in with API key ${chalk.cyan(prefix)}`);
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
2418
|
const status = resolveTokenStatus(resolved);
|
|
1752
2419
|
const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
|
|
1753
2420
|
if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
|
|
@@ -1757,17 +2424,21 @@ program.command("whoami").description("Show current authentication").action(asyn
|
|
|
1757
2424
|
reportCommandError("Whoami failed", error);
|
|
1758
2425
|
}
|
|
1759
2426
|
});
|
|
1760
|
-
program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--
|
|
2427
|
+
program.command("new").description("Create a new blode.md documentation site").argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
1761
2428
|
await scaffoldDocsSite(directory, {
|
|
2429
|
+
displayName: options.displayName,
|
|
1762
2430
|
name: options.name,
|
|
2431
|
+
slug: options.slug ?? options.name,
|
|
1763
2432
|
template: options.template,
|
|
1764
2433
|
yes: options.yes
|
|
1765
2434
|
});
|
|
1766
2435
|
});
|
|
1767
|
-
program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--
|
|
2436
|
+
program.command("init", { hidden: true }).argument("[directory]", "target directory").option("--slug <slug>", "project slug for docs.json", parseProjectSlug).option("--name <slug>", "deprecated alias for --slug", parseProjectSlug).option("--display-name <name>", "display name for docs.json").option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
|
|
1768
2437
|
await scaffoldDocsSite(directory, {
|
|
1769
2438
|
deprecatedCommand: "blodemd init",
|
|
2439
|
+
displayName: options.displayName,
|
|
1770
2440
|
name: options.name,
|
|
2441
|
+
slug: options.slug ?? options.name,
|
|
1771
2442
|
template: options.template,
|
|
1772
2443
|
yes: options.yes
|
|
1773
2444
|
});
|
|
@@ -1783,7 +2454,7 @@ program.command("validate").description("Validate docs.json").argument("[dir]",
|
|
|
1783
2454
|
reportCommandError("Validation failed", error);
|
|
1784
2455
|
}
|
|
1785
2456
|
});
|
|
1786
|
-
program.command("push").description("Deploy docs").argument("[dir]", "docs directory").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").option("--
|
|
2457
|
+
program.command("push").description("Deploy docs").argument("[dir]", "docs directory").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").option("--branch <name>", "git branch (env: BLODEMD_BRANCH)").option("--message <msg>", "deploy message (env: BLODEMD_COMMIT_MESSAGE)").action(async (dir, options) => {
|
|
1787
2458
|
intro(chalk.bold("blodemd push"));
|
|
1788
2459
|
const s = spinner();
|
|
1789
2460
|
try {
|
|
@@ -1792,7 +2463,8 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
|
|
|
1792
2463
|
const { config, warnings } = await loadValidatedSiteConfig(root);
|
|
1793
2464
|
s.stop("Configuration valid");
|
|
1794
2465
|
for (const warning of warnings) log.warn(warning);
|
|
1795
|
-
const { project, apiUrl, authToken, branch, commitMessage } = await resolvePushConfig(config, options);
|
|
2466
|
+
const { project, projectDisplayName, apiUrl, authToken, branch, commitMessage, usedLegacyNameFallback } = await resolvePushConfig(config, options);
|
|
2467
|
+
if (usedLegacyNameFallback) log.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
|
|
1796
2468
|
s.start("Collecting files");
|
|
1797
2469
|
const files = await collectFiles(root);
|
|
1798
2470
|
if (files.length === 0) throw new Error("No files found to deploy.");
|
|
@@ -1817,7 +2489,7 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
|
|
|
1817
2489
|
} catch (error) {
|
|
1818
2490
|
if (!(error instanceof Error ? error.message : "").includes("404")) throw error;
|
|
1819
2491
|
s.stop("Project not found");
|
|
1820
|
-
if (!await autoCreateProject(project, apiUrl, headers)) {
|
|
2492
|
+
if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers)) {
|
|
1821
2493
|
log.info("Cancelled");
|
|
1822
2494
|
return;
|
|
1823
2495
|
}
|