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.
Files changed (78) hide show
  1. package/README.md +11 -47
  2. package/dev-server/app/layout.tsx +1 -1
  3. package/dist/cli.mjs +1078 -406
  4. package/dist/cli.mjs.map +1 -1
  5. package/docs/app/globals.css +15 -1
  6. package/docs/components/api/api-playground.tsx +2 -2
  7. package/docs/components/docs/copy-page-menu.tsx +55 -27
  8. package/docs/components/docs/doc-header.tsx +1 -1
  9. package/docs/components/docs/doc-shell.tsx +89 -88
  10. package/docs/components/docs/doc-sidebar.tsx +6 -3
  11. package/docs/components/docs/doc-toc.tsx +1 -1
  12. package/docs/components/docs/mobile-nav.tsx +8 -16
  13. package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
  14. package/docs/components/git/repo-picker.tsx +526 -0
  15. package/docs/components/mdx/agent-instructions.tsx +17 -0
  16. package/docs/components/mdx/code-block.tsx +6 -1
  17. package/docs/components/mdx/code-group.tsx +1 -1
  18. package/docs/components/mdx/iframe.tsx +62 -0
  19. package/docs/components/mdx/index.tsx +4 -0
  20. package/docs/components/mdx/tabs.tsx +5 -5
  21. package/docs/components/mdx/video.tsx +45 -12
  22. package/docs/components/third-parties.tsx +29 -0
  23. package/docs/components/ui/badge.tsx +61 -0
  24. package/docs/components/ui/breadcrumb.tsx +61 -41
  25. package/docs/components/ui/button-group.tsx +83 -0
  26. package/docs/components/ui/button.tsx +30 -55
  27. package/docs/components/ui/command.tsx +32 -4
  28. package/docs/components/ui/copy-button.tsx +12 -19
  29. package/docs/components/ui/dialog.tsx +50 -1
  30. package/docs/components/ui/input.tsx +16 -97
  31. package/docs/components/ui/kbd.tsx +98 -0
  32. package/docs/components/ui/morph-icon.tsx +79 -0
  33. package/docs/components/ui/popover.tsx +225 -30
  34. package/docs/components/ui/search.tsx +0 -9
  35. package/docs/components/ui/sheet.tsx +30 -1
  36. package/docs/components/ui/sidebar.tsx +332 -7
  37. package/docs/components/ui/site-footer.tsx +6 -4
  38. package/docs/components/ui/skeleton.tsx +11 -0
  39. package/docs/components/ui/switch.tsx +32 -0
  40. package/docs/components/ui/tabs.tsx +138 -0
  41. package/docs/lib/api-client.ts +72 -0
  42. package/docs/lib/contextual-options.ts +9 -0
  43. package/docs/lib/dashboard-session.ts +167 -0
  44. package/docs/lib/db.ts +13 -0
  45. package/docs/lib/env.ts +4 -3
  46. package/docs/lib/etag.ts +22 -0
  47. package/docs/lib/github-install.ts +33 -0
  48. package/docs/lib/project-authz.ts +46 -0
  49. package/docs/lib/routes.ts +5 -1
  50. package/docs/lib/supabase.ts +30 -6
  51. package/docs/lib/tenancy.ts +1 -0
  52. package/docs/lib/tenant-static.ts +206 -4
  53. package/docs/lib/tenants.ts +5 -1
  54. package/docs/lib/time-ago.ts +24 -0
  55. package/docs/lib/use-tab-observer.ts +71 -0
  56. package/package.json +2 -2
  57. package/packages/@repo/contracts/dist/git.d.ts +28 -0
  58. package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
  59. package/packages/@repo/contracts/dist/git.js +24 -0
  60. package/packages/@repo/contracts/dist/index.d.ts +1 -1
  61. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  62. package/packages/@repo/contracts/dist/index.js +1 -1
  63. package/packages/@repo/contracts/src/git.ts +31 -0
  64. package/packages/@repo/contracts/src/index.ts +1 -1
  65. package/packages/@repo/models/dist/docs-config.d.ts +9 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  67. package/packages/@repo/models/dist/docs-config.js +7 -0
  68. package/packages/@repo/models/src/docs-config.ts +7 -0
  69. package/packages/@repo/previewing/dist/index.d.ts +4 -0
  70. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  71. package/packages/@repo/previewing/dist/index.js +53 -2
  72. package/packages/@repo/previewing/src/index.ts +64 -2
  73. package/packages/@repo/validation/src/blodemd-docs-schema.json +8 -1
  74. package/scripts/prepare-package.mjs +14 -0
  75. package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
  76. package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
  77. package/packages/@repo/contracts/dist/api-key.js +0 -20
  78. 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, password, select, spinner, text } from "@clack/prompts";
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 { createFsSource, loadSiteConfig } from "@repo/previewing";
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 (optApiKey) => {
275
- const envToken = (optApiKey ?? process.env["BLODEMD_API_KEY"])?.trim();
276
- if (envToken) return tokenFromRaw(envToken, optApiKey ? "flag" : "environment");
277
- const data = await readAuthFile();
278
- const session = data?.session;
279
- if (session) {
280
- if (!(shouldRefresh(session) || isExpired(session))) return sessionToResolvedToken(session);
281
- if (session.refreshToken) try {
282
- const { tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
283
- const updatedSession = tokenResponseToStoredSession(await refreshAccessToken({
284
- clientId: OAUTH_CLIENT_ID,
285
- tokenUrl
286
- }, session.refreshToken));
287
- await writeStoredAuthSession(updatedSession);
288
- return sessionToResolvedToken(updatedSession);
289
- } catch {}
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
- if (data?.apiKey) return {
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
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 validateProjectSlug = (value) => {
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: projectSlug,
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: `${projectSlug} logo`,
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: projectSlug,
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
- content: stringifyJson(createStarterDocsJson(projectSlug)),
940
- path: "docs.json"
941
- },
942
- {
943
- content: [
944
- "---",
945
- "title: Welcome",
946
- "description: Start here.",
947
- "---",
948
- "",
949
- "# Welcome",
950
- "",
951
- "This starter gives you branded assets, repo helper files, and a small docs structure you can rewrite quickly.",
952
- "",
953
- "![Starter illustration](images/hero-light.svg)",
954
- "",
955
- "## What is included",
956
- "",
957
- "- A starter `docs.json` with branding, contextual actions, and navigation.",
958
- "- Placeholder brand assets in `/logo` and `/images`.",
959
- "- Repo helper files like `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`.",
960
- "",
961
- "## Next steps",
962
- "",
963
- "- Confirm the `name` field in `docs.json` matches your project slug.",
964
- "- Set `description` in `docs.json` to explain your product.",
965
- "- Replace the files in `/logo` and `/images` with your own brand assets.",
966
- "- Rewrite `CLAUDE.md` with your terminology and writing standards.",
967
- "- Update this page, then preview locally with `blodemd dev`.",
968
- "",
969
- "## Included pages",
970
- "",
971
- "- [Quickstart](quickstart)",
972
- "- [Development](development)",
973
- ""
974
- ].join("\n"),
975
- path: "index.mdx"
976
- },
977
- {
978
- content: [
979
- "---",
980
- "title: Quickstart",
981
- "description: Get your docs running fast.",
982
- "---",
983
- "",
984
- "# Quickstart",
985
- "",
986
- "![Setup checklist](images/checks-passed.svg)",
987
- "",
988
- "1. Confirm the `name` field in `docs.json`.",
989
- "2. Update the `description` field to match your product.",
990
- "3. Replace the assets in `/logo` and `/images`.",
991
- "4. Run `blodemd dev` to preview locally.",
992
- "5. Run `blodemd push` when you are ready to publish.",
993
- ""
994
- ].join("\n"),
995
- path: "quickstart.mdx"
996
- },
997
- {
998
- content: [
999
- "---",
1000
- "title: Development",
1001
- "description: Work on your docs locally.",
1002
- "---",
1003
- "",
1004
- "# Development",
1005
- "",
1006
- "![Dark preview illustration](images/hero-dark.svg)",
1007
- "",
1008
- "Preview locally with:",
1009
- "",
1010
- "```bash",
1011
- "blodemd dev",
1012
- "```",
1013
- "",
1014
- "Validate your configuration with:",
1015
- "",
1016
- "```bash",
1017
- "blodemd validate",
1018
- "```",
1019
- "",
1020
- "Keep `CLAUDE.md` current as your product terminology and writing rules evolve.",
1021
- ""
1022
- ].join("\n"),
1023
- path: "development.mdx"
1024
- },
1025
- {
1026
- content: [
1027
- "# Documentation starter",
1028
- "",
1029
- "This directory was scaffolded with `blodemd new --template starter`.",
1030
- "",
1031
- "## What is included",
1032
- "",
1033
- "- `docs.json` with branding, contextual actions, and starter navigation",
1034
- "- `index.mdx`, `quickstart.mdx`, and `development.mdx`",
1035
- "- Placeholder brand assets in `/logo` and `/images`",
1036
- "- Repo helper files: `.gitignore`, `README.md`, `AGENTS.md`, and `CLAUDE.md`",
1037
- "",
1038
- "## Commands",
1039
- "",
1040
- "```bash",
1041
- "blodemd dev",
1042
- "blodemd validate",
1043
- "blodemd push",
1044
- "```",
1045
- "",
1046
- "## Customize",
1047
- "",
1048
- "- Confirm the project slug and set the description in `docs.json`.",
1049
- "- Replace the assets in `/logo` and `/images`.",
1050
- "- Rewrite `CLAUDE.md` with project-specific terminology and writing rules.",
1051
- "- Rewrite the starter pages to match your product.",
1052
- "- Add a `LICENSE` file deliberately if this repo will be public.",
1053
- ""
1054
- ].join("\n"),
1055
- path: "README.md"
1056
- },
1057
- {
1058
- fallbackContent: claudeInstructions,
1059
- path: "AGENTS.md",
1060
- target: "CLAUDE.md",
1061
- type: "symlink"
1062
- },
1063
- {
1064
- content: claudeInstructions,
1065
- path: "CLAUDE.md"
1066
- },
1067
- {
1068
- content: [
1069
- "# dependencies",
1070
- "node_modules/",
1071
- "",
1072
- "# local env files",
1073
- ".env*",
1074
- "!.env.example",
1075
- "",
1076
- "# build and cache",
1077
- ".next/",
1078
- ".turbo/",
1079
- "coverage/",
1080
- "dist/",
1081
- ".vercel/",
1082
- "*.tsbuildinfo",
1083
- "",
1084
- "# logs",
1085
- "*.log",
1086
- "",
1087
- "# misc",
1088
- ".DS_Store",
1089
- ""
1090
- ].join("\n"),
1091
- path: ".gitignore"
1092
- },
1093
- {
1094
- content: [
1095
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" fill=\"none\">",
1096
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0D9373\"/>",
1097
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1098
- " <path d=\"M28 26h6c5.523 0 10 4.477 10 10s-4.477 10-10 10h-6V26Z\" fill=\"#0C3A33\"/>",
1099
- "</svg>",
1100
- ""
1101
- ].join("\n"),
1102
- path: "favicon.svg"
1103
- },
1104
- {
1105
- content: [
1106
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1107
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#0C3A33\"/>",
1108
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#CFF6EE\"/>",
1109
- ` <text x="84" y="41" fill="#111827" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
1110
- "</svg>",
1111
- ""
1112
- ].join("\n"),
1113
- path: "logo/light.svg"
1114
- },
1115
- {
1116
- content: [
1117
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 240 64\" fill=\"none\">",
1118
- " <rect width=\"64\" height=\"64\" rx=\"16\" fill=\"#CFF6EE\"/>",
1119
- " <path d=\"M20 18h14c8.837 0 16 7.163 16 16s-7.163 16-16 16H20V18Z\" fill=\"#0C3A33\"/>",
1120
- ` <text x="84" y="41" fill="#F9FAFB" font-family="Arial, sans-serif" font-size="28" font-weight="700">${projectSlug}</text>`,
1121
- "</svg>",
1122
- ""
1123
- ].join("\n"),
1124
- path: "logo/dark.svg"
1125
- },
1126
- {
1127
- content: [
1128
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1129
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F4FBF8\"/>",
1130
- " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#E1F4EE\"/>",
1131
- " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#0D9373\" opacity=\".25\"/>",
1132
- " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1133
- " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0D9373\" opacity=\".18\"/>",
1134
- " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1135
- " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#CFF6EE\"/>",
1136
- " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#CFF6EE\" opacity=\".7\"/>",
1137
- " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#FFFFFF\"/>",
1138
- " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".18\"/>",
1139
- " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1140
- " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1141
- " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".85\"/>",
1142
- " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".45\"/>",
1143
- " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1144
- "</svg>",
1145
- ""
1146
- ].join("\n"),
1147
- path: "images/hero-light.svg"
1148
- },
1149
- {
1150
- content: [
1151
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1152
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#071715\"/>",
1153
- " <rect x=\"48\" y=\"48\" width=\"260\" height=\"424\" rx=\"24\" fill=\"#0F2E28\"/>",
1154
- " <rect x=\"96\" y=\"120\" width=\"164\" height=\"20\" rx=\"10\" fill=\"#CFF6EE\" opacity=\".18\"/>",
1155
- " <rect x=\"96\" y=\"164\" width=\"132\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1156
- " <rect x=\"96\" y=\"204\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".14\"/>",
1157
- " <rect x=\"356\" y=\"80\" width=\"556\" height=\"104\" rx=\"24\" fill=\"#0D9373\"/>",
1158
- " <rect x=\"388\" y=\"116\" width=\"220\" height=\"18\" rx=\"9\" fill=\"#E8FFF9\"/>",
1159
- " <rect x=\"388\" y=\"148\" width=\"156\" height=\"14\" rx=\"7\" fill=\"#E8FFF9\" opacity=\".6\"/>",
1160
- " <rect x=\"356\" y=\"216\" width=\"268\" height=\"256\" rx=\"24\" fill=\"#0C3A33\"/>",
1161
- " <rect x=\"388\" y=\"260\" width=\"168\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".22\"/>",
1162
- " <rect x=\"388\" y=\"292\" width=\"196\" height=\"16\" rx=\"8\" fill=\"#CFF6EE\" opacity=\".16\"/>",
1163
- " <rect x=\"656\" y=\"216\" width=\"256\" height=\"256\" rx=\"24\" fill=\"#E9FFF8\"/>",
1164
- " <rect x=\"692\" y=\"260\" width=\"128\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".24\"/>",
1165
- " <rect x=\"692\" y=\"292\" width=\"152\" height=\"16\" rx=\"8\" fill=\"#0C3A33\" opacity=\".12\"/>",
1166
- " <circle cx=\"804\" cy=\"380\" r=\"52\" fill=\"#0D9373\"/>",
1167
- "</svg>",
1168
- ""
1169
- ].join("\n"),
1170
- path: "images/hero-dark.svg"
1171
- },
1172
- {
1173
- content: [
1174
- "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 960 520\" fill=\"none\">",
1175
- " <rect width=\"960\" height=\"520\" rx=\"32\" fill=\"#F8FCFA\"/>",
1176
- " <rect x=\"60\" y=\"76\" width=\"840\" height=\"368\" rx=\"28\" fill=\"#FFFFFF\" stroke=\"#D7ECE6\" stroke-width=\"4\"/>",
1177
- " <rect x=\"108\" y=\"124\" width=\"96\" height=\"96\" rx=\"24\" fill=\"#0D9373\"/>",
1178
- " <path d=\"M136 172l18 18 38-48\" stroke=\"#CFF6EE\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"18\"/>",
1179
- " <rect x=\"244\" y=\"132\" width=\"280\" height=\"24\" rx=\"12\" fill=\"#0C3A33\"/>",
1180
- " <rect x=\"244\" y=\"176\" width=\"416\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".16\"/>",
1181
- " <rect x=\"244\" y=\"214\" width=\"340\" height=\"18\" rx=\"9\" fill=\"#0C3A33\" opacity=\".12\"/>",
1182
- " <rect x=\"108\" y=\"280\" width=\"744\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1183
- " <rect x=\"108\" y=\"326\" width=\"520\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1184
- " <rect x=\"108\" y=\"372\" width=\"612\" height=\"22\" rx=\"11\" fill=\"#0D9373\" opacity=\".12\"/>",
1185
- "</svg>",
1186
- ""
1187
- ].join("\n"),
1188
- path: "images/checks-passed.svg"
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
+ "![Starter illustration](images/hero-light.svg)",
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
+ "![Setup checklist](images/checks-passed.svg)",
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
+ "![Dark preview illustration](images/hero-dark.svg)",
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
- return template === "starter" ? createStarterFiles(projectSlug) : createMinimalFiles(projectSlug);
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 (providedName, directory, shouldPrompt) => {
2199
+ const resolveProjectSlug = async (providedSlug, directory, shouldPrompt) => {
1531
2200
  const defaultProjectSlug = deriveDefaultProjectSlug(directory, process.cwd());
1532
- if (providedName) return providedName;
2201
+ if (providedSlug) return providedSlug;
1533
2202
  if (!shouldPrompt) return defaultProjectSlug;
1534
2203
  return await promptForProjectSlug(defaultProjectSlug);
1535
2204
  };
1536
- const writeScaffoldFiles = async (root, template, projectSlug) => {
1537
- for (const file of getScaffoldFiles(template, { projectSlug })) {
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 project = options.project ?? process.env["BLODEMD_PROJECT"] ?? config.name;
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(options.apiKey))?.token;
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 \"name\" in docs.json, pass --project, or set BLODEMD_PROJECT.");
1569
- if (!authToken) throw new Error("Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.");
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: project,
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.project.slug)} created`);
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, projectSlug);
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").option("--token", "Paste an API key instead of using browser login").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) => {
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("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
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("--name <slug>", "project slug for docs.json", parseProjectSlug).option("-t, --template <template>", `scaffold template (${SCAFFOLD_TEMPLATES.join(", ")})`, parseScaffoldTemplate, "minimal").option("-y, --yes", "accept defaults without prompting").action(async (directory, options) => {
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("--api-key <token>", "API key (env: BLODEMD_API_KEY)").option("--branch <name>", "git branch (env: BLODEMD_BRANCH)").option("--message <msg>", "deploy message (env: BLODEMD_COMMIT_MESSAGE)").action(async (dir, options) => {
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
  }