blodemd 0.0.11 → 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 +708 -120
  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 +1 -1
  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 +6 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  67. package/packages/@repo/models/dist/docs-config.js +1 -0
  68. package/packages/@repo/models/src/docs-config.ts +1 -0
  69. package/packages/@repo/previewing/dist/index.d.ts +3 -0
  70. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  71. package/packages/@repo/previewing/dist/index.js +48 -0
  72. package/packages/@repo/previewing/src/index.ts +56 -0
  73. package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
  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,11 +12,34 @@ 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";
23
45
  const BLODE_PROJECT_ENV = "BLODEMD_PROJECT";
@@ -31,30 +53,6 @@ const getDefaultConfigBaseDir = () => {
31
53
  const CONFIG_DIR = join(getDefaultConfigBaseDir(), CLI_NAME);
32
54
  const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
33
55
  //#endregion
34
- //#region src/jwt.ts
35
- const parseJwtBase64Url = (input) => {
36
- const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
37
- const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
38
- return Buffer.from(padded, "base64").toString("utf8");
39
- };
40
- const parseJwtClaims = (token) => {
41
- const payloadPart = token.split(".").at(1);
42
- if (!payloadPart) return null;
43
- try {
44
- const payload = parseJwtBase64Url(payloadPart);
45
- const parsed = JSON.parse(payload);
46
- if (typeof parsed !== "object" || parsed === null) return null;
47
- const claims = parsed;
48
- return {
49
- email: typeof claims.email === "string" ? claims.email : void 0,
50
- exp: typeof claims.exp === "number" ? claims.exp : void 0,
51
- sub: typeof claims.sub === "string" ? claims.sub : void 0
52
- };
53
- } catch {
54
- return null;
55
- }
56
- };
57
- //#endregion
58
56
  //#region src/errors.ts
59
57
  const EXIT_CODES = {
60
58
  AUTH_REQUIRED: 4,
@@ -150,14 +148,6 @@ const parseStoredAuthSession = (value) => {
150
148
  user
151
149
  };
152
150
  };
153
- const parseApiKeyCredentials = (value) => {
154
- if (!isRecord(value)) return null;
155
- if (typeof value.apiKey !== "string") return null;
156
- return {
157
- apiKey: value.apiKey,
158
- type: "api-key"
159
- };
160
- };
161
151
  const createInvalidCredentialsError = (detail) => new CliError(detail ? `Invalid credentials format in ${CREDENTIALS_FILE}: ${detail}` : `Invalid credentials format in ${CREDENTIALS_FILE}`, EXIT_CODES.ERROR);
162
152
  const parseAuthFile = (raw) => {
163
153
  let parsed;
@@ -168,13 +158,9 @@ const parseAuthFile = (raw) => {
168
158
  }
169
159
  if (!isRecord(parsed) || parsed.version !== 1) throw createInvalidCredentialsError();
170
160
  const hasSession = Object.hasOwn(parsed, "session");
171
- const hasApiKey = Object.hasOwn(parsed, "apiKey");
172
161
  const session = hasSession && parsed.session !== void 0 ? parseStoredAuthSession(parsed.session) : void 0;
173
- const apiKey = hasApiKey && parsed.apiKey !== void 0 ? parseApiKeyCredentials(parsed.apiKey) : void 0;
174
162
  if (hasSession && parsed.session !== void 0 && !session) throw createInvalidCredentialsError("stored session is malformed.");
175
- if (hasApiKey && parsed.apiKey !== void 0 && !apiKey) throw createInvalidCredentialsError("stored API key is malformed.");
176
163
  return {
177
- apiKey: apiKey ?? void 0,
178
164
  session: session ?? void 0,
179
165
  version: 1
180
166
  };
@@ -204,16 +190,34 @@ const writeStoredAuthSession = async (session) => {
204
190
  version: 1
205
191
  });
206
192
  };
207
- const writeStoredApiKey = async (apiKey) => {
208
- await writeAuthFile({
209
- apiKey,
210
- version: 1
211
- });
212
- };
213
193
  const clearStoredCredentials = async () => {
214
194
  await rm(CREDENTIALS_FILE, { force: true });
215
195
  };
216
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
217
221
  //#region src/supabase.ts
218
222
  const resolveSupabaseConfig = () => {
219
223
  return { url: process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "https://bwnxwgkgyklzzmpbzuoz.supabase.co" };
@@ -254,53 +258,30 @@ const shouldRefresh = (session) => {
254
258
  const ms = expiresInMs(session);
255
259
  return ms !== null && ms <= 6e4;
256
260
  };
257
- const tokenFromRaw = (token, source) => {
258
- const claims = parseJwtClaims(token);
259
- return {
260
- expiresAt: typeof claims?.exp === "number" ? (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString() : null,
261
- source,
262
- token,
263
- user: claims?.sub || claims?.email ? {
264
- email: claims.email ?? null,
265
- id: claims.sub ?? "unknown"
266
- } : null
267
- };
268
- };
269
261
  const sessionToResolvedToken = (session) => ({
270
262
  expiresAt: session.expiresAt,
271
263
  source: "stored",
272
264
  token: session.accessToken,
273
265
  user: session.user
274
266
  });
275
- const resolveAuthToken = async (optApiKey) => {
276
- const envToken = (optApiKey ?? process.env["BLODEMD_API_KEY"])?.trim();
277
- if (envToken) return tokenFromRaw(envToken, optApiKey ? "flag" : "environment");
278
- const data = await readAuthFile();
279
- const session = data?.session;
280
- if (session) {
281
- if (!(shouldRefresh(session) || isExpired(session))) return sessionToResolvedToken(session);
282
- if (session.refreshToken) try {
283
- const { tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
284
- const updatedSession = tokenResponseToStoredSession(await refreshAccessToken({
285
- clientId: OAUTH_CLIENT_ID,
286
- tokenUrl
287
- }, session.refreshToken));
288
- await writeStoredAuthSession(updatedSession);
289
- return sessionToResolvedToken(updatedSession);
290
- } catch {}
291
- if (isExpired(session)) {
292
- await clearStoredCredentials();
293
- return null;
294
- }
295
- 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;
296
283
  }
297
- if (data?.apiKey) return {
298
- expiresAt: null,
299
- source: "stored",
300
- token: data.apiKey.apiKey,
301
- user: null
302
- };
303
- return null;
284
+ return sessionToResolvedToken(session);
304
285
  };
305
286
  const resolveTokenStatus = (token) => {
306
287
  if (!token.expiresAt) return {
@@ -334,6 +315,642 @@ const parsePort = (value, label = "Port") => {
334
315
  return parsed;
335
316
  };
336
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
337
954
  //#region src/site-config.ts
338
955
  const CONFIG_FILE$2 = "docs.json";
339
956
  const getSiteConfigHint = (errors) => {
@@ -1616,7 +2233,7 @@ const resolvePushConfig = async (config, options) => {
1616
2233
  envProject: process.env[BLODE_PROJECT_ENV]
1617
2234
  });
1618
2235
  const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
1619
- const authToken = (await resolveAuthToken(options.apiKey))?.token;
2236
+ const authToken = (await resolveAuthToken())?.token;
1620
2237
  const branch = options.branch ?? process.env["BLODEMD_BRANCH"] ?? process.env.GITHUB_REF_NAME ?? readGitValue([
1621
2238
  "rev-parse",
1622
2239
  "--abbrev-ref",
@@ -1633,7 +2250,7 @@ const resolvePushConfig = async (config, options) => {
1633
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}`);
1634
2251
  throw new Error(`Invalid project slug "${project}". ${projectSlugError}`);
1635
2252
  }
1636
- if (!authToken) throw new Error("Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.");
2253
+ if (!authToken) throw new Error("Not logged in. Run \"blodemd login\" to authenticate.");
1637
2254
  return {
1638
2255
  apiUrl,
1639
2256
  authToken,
@@ -1656,8 +2273,7 @@ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers) =
1656
2273
  headers,
1657
2274
  method: "POST"
1658
2275
  }, "Failed to create project");
1659
- log.success(`Project ${chalk.cyan(createResult.project.slug)} created`);
1660
- log.info(`API key for CI: ${chalk.dim(createResult.token)}`);
2276
+ log.success(`Project ${chalk.cyan(createResult.slug)} created`);
1661
2277
  return true;
1662
2278
  };
1663
2279
  const scaffoldDocsSite = async (directory, options) => {
@@ -1727,29 +2343,9 @@ program.name("blodemd").description("Blode.md CLI").version(cliVersion);
1727
2343
  program.hook("preAction", () => {
1728
2344
  assertSupportedNodeVersion();
1729
2345
  });
1730
- 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) => {
1731
2347
  intro(chalk.bold("blodemd login"));
1732
2348
  try {
1733
- if (options.token) {
1734
- const apiKey = await password({
1735
- message: "Enter your API key",
1736
- validate: (value) => {
1737
- if (!value) return "API key is required.";
1738
- }
1739
- });
1740
- if (isCancel(apiKey)) {
1741
- log.warn("Cancelled");
1742
- return;
1743
- }
1744
- await writeStoredApiKey({
1745
- apiKey,
1746
- type: "api-key"
1747
- });
1748
- const prefix = apiKey.split(".")[0] ?? apiKey.slice(0, 12);
1749
- log.success(`Authenticated as ${chalk.cyan(prefix)}`);
1750
- log.info("Done");
1751
- return;
1752
- }
1753
2349
  const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
1754
2350
  const clientId = OAUTH_CLIENT_ID;
1755
2351
  const port = parsePort(options.port);
@@ -1766,6 +2362,7 @@ program.command("login").description("Authenticate with Blode.md").option("--tok
1766
2362
  authUrl.searchParams.set("code_challenge_method", "S256");
1767
2363
  authUrl.searchParams.set("state", state);
1768
2364
  authUrl.searchParams.set("scope", "openid email profile");
2365
+ authUrl.searchParams.set("provider", "github");
1769
2366
  const callbackPromise = waitForOAuthCode({
1770
2367
  expectedState: state,
1771
2368
  redirectUrl,
@@ -1818,15 +2415,6 @@ program.command("whoami").description("Show current authentication").action(asyn
1818
2415
  log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
1819
2416
  return;
1820
2417
  }
1821
- if (resolved.source === "environment") {
1822
- log.info("Authenticated via BLODEMD_API_KEY environment variable");
1823
- return;
1824
- }
1825
- if (!resolved.expiresAt && !resolved.user) {
1826
- const prefix = resolved.token.split(".")[0] ?? resolved.token.slice(0, 12);
1827
- log.info(`Logged in with API key ${chalk.cyan(prefix)}`);
1828
- return;
1829
- }
1830
2418
  const status = resolveTokenStatus(resolved);
1831
2419
  const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
1832
2420
  if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
@@ -1866,7 +2454,7 @@ program.command("validate").description("Validate docs.json").argument("[dir]",
1866
2454
  reportCommandError("Validation failed", error);
1867
2455
  }
1868
2456
  });
1869
- 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) => {
1870
2458
  intro(chalk.bold("blodemd push"));
1871
2459
  const s = spinner();
1872
2460
  try {