blodemd 0.0.11 → 0.0.13

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 (86) hide show
  1. package/README.md +11 -47
  2. package/dev-server/app/layout.tsx +1 -1
  3. package/dev-server/next.config.js +19 -9
  4. package/dev-server/tsconfig.json +0 -3
  5. package/dist/cli.mjs +732 -123
  6. package/dist/cli.mjs.map +1 -1
  7. package/docs/app/globals.css +15 -1
  8. package/docs/components/api/api-playground.tsx +2 -2
  9. package/docs/components/docs/copy-page-menu.tsx +55 -27
  10. package/docs/components/docs/doc-header.tsx +1 -1
  11. package/docs/components/docs/doc-shell.tsx +89 -88
  12. package/docs/components/docs/doc-sidebar.tsx +6 -3
  13. package/docs/components/docs/doc-toc.tsx +1 -1
  14. package/docs/components/docs/mobile-nav.tsx +8 -16
  15. package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
  16. package/docs/components/git/repo-picker.tsx +526 -0
  17. package/docs/components/mdx/agent-instructions.tsx +17 -0
  18. package/docs/components/mdx/code-block.tsx +6 -1
  19. package/docs/components/mdx/code-group.tsx +1 -1
  20. package/docs/components/mdx/iframe.tsx +62 -0
  21. package/docs/components/mdx/index.tsx +4 -0
  22. package/docs/components/mdx/tabs.tsx +5 -5
  23. package/docs/components/mdx/video.tsx +45 -12
  24. package/docs/components/third-parties.tsx +29 -0
  25. package/docs/components/ui/badge.tsx +61 -0
  26. package/docs/components/ui/breadcrumb.tsx +61 -41
  27. package/docs/components/ui/button-group.tsx +83 -0
  28. package/docs/components/ui/button.tsx +30 -55
  29. package/docs/components/ui/command.tsx +32 -4
  30. package/docs/components/ui/copy-button.tsx +12 -19
  31. package/docs/components/ui/dialog.tsx +50 -1
  32. package/docs/components/ui/input.tsx +16 -97
  33. package/docs/components/ui/kbd.tsx +98 -0
  34. package/docs/components/ui/morph-icon.tsx +79 -0
  35. package/docs/components/ui/popover.tsx +225 -30
  36. package/docs/components/ui/search.tsx +0 -9
  37. package/docs/components/ui/sheet.tsx +30 -1
  38. package/docs/components/ui/sidebar.tsx +332 -7
  39. package/docs/components/ui/site-footer.tsx +6 -4
  40. package/docs/components/ui/skeleton.tsx +11 -0
  41. package/docs/components/ui/switch.tsx +32 -0
  42. package/docs/components/ui/tabs.tsx +138 -0
  43. package/docs/lib/api-client.ts +72 -0
  44. package/docs/lib/contextual-options.ts +9 -0
  45. package/docs/lib/dashboard-session.ts +167 -0
  46. package/docs/lib/db.ts +13 -0
  47. package/docs/lib/env.ts +4 -3
  48. package/docs/lib/etag.ts +22 -0
  49. package/docs/lib/github-install.ts +33 -0
  50. package/docs/lib/project-authz.ts +46 -0
  51. package/docs/lib/routes.ts +5 -1
  52. package/docs/lib/supabase.ts +30 -6
  53. package/docs/lib/tenancy.ts +1 -0
  54. package/docs/lib/tenant-static.ts +206 -4
  55. package/docs/lib/tenants.ts +5 -1
  56. package/docs/lib/time-ago.ts +24 -0
  57. package/docs/lib/use-tab-observer.ts +71 -0
  58. package/package.json +3 -1
  59. package/packages/@repo/common/package.json +2 -2
  60. package/packages/@repo/contracts/dist/git.d.ts +28 -0
  61. package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
  62. package/packages/@repo/contracts/dist/git.js +24 -0
  63. package/packages/@repo/contracts/dist/index.d.ts +1 -1
  64. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  65. package/packages/@repo/contracts/dist/index.js +1 -1
  66. package/packages/@repo/contracts/package.json +2 -2
  67. package/packages/@repo/contracts/src/git.ts +31 -0
  68. package/packages/@repo/contracts/src/index.ts +1 -1
  69. package/packages/@repo/models/dist/docs-config.d.ts +6 -0
  70. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  71. package/packages/@repo/models/dist/docs-config.js +1 -0
  72. package/packages/@repo/models/package.json +2 -2
  73. package/packages/@repo/models/src/docs-config.ts +1 -0
  74. package/packages/@repo/prebuild/package.json +2 -2
  75. package/packages/@repo/previewing/dist/index.d.ts +3 -0
  76. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  77. package/packages/@repo/previewing/dist/index.js +48 -0
  78. package/packages/@repo/previewing/package.json +2 -2
  79. package/packages/@repo/previewing/src/index.ts +56 -0
  80. package/packages/@repo/validation/package.json +2 -2
  81. package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
  82. package/scripts/prepare-package.mjs +14 -0
  83. package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
  84. package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
  85. package/packages/@repo/contracts/dist/api-key.js +0 -20
  86. 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) => {
@@ -579,6 +1196,19 @@ const createStandaloneRuntimeRoot = async (configDir = CONFIG_DIR) => {
579
1196
  await cleanupStandaloneRuntimeRoots(configDir);
580
1197
  return await fs.mkdtemp(path.join(configDir, STANDALONE_RUNTIME_PREFIX));
581
1198
  };
1199
+ /**
1200
+ * Locate the `node_modules` directory that actually contains the CLI's
1201
+ * transitive dependencies. Package managers like npm/yarn-classic (and
1202
+ * `npx` caches) hoist shared deps above the package directory, so
1203
+ * `<cliPackageRoot>/node_modules` may not exist or may not contain `next`.
1204
+ * Resolve `next/package.json` and use the directory that owns it.
1205
+ */
1206
+ const resolveRuntimeNodeModules = async (cliPackageRoot) => {
1207
+ const localNodeModules = path.join(cliPackageRoot, "node_modules");
1208
+ if (await fileExists(path.join(localNodeModules, "next", "package.json"))) return localNodeModules;
1209
+ const nextPkgPath = createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
1210
+ return path.dirname(path.dirname(nextPkgPath));
1211
+ };
582
1212
  const materializeStandaloneRuntime = async (cliPackageRoot) => {
583
1213
  const runtimeRoot = await createStandaloneRuntimeRoot();
584
1214
  try {
@@ -587,9 +1217,17 @@ const materializeStandaloneRuntime = async (cliPackageRoot) => {
587
1217
  "docs",
588
1218
  "packages"
589
1219
  ]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
590
- await fs.symlink(path.join(cliPackageRoot, "node_modules"), path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
591
- await fs.mkdir(path.join(runtimeRoot, "dev-server", "node_modules"), { recursive: true });
592
- await fs.symlink(path.join(runtimeRoot, "packages", "@repo"), path.join(runtimeRoot, "dev-server", "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
1220
+ const runtimeNodeModules = await resolveRuntimeNodeModules(cliPackageRoot);
1221
+ await fs.symlink(runtimeNodeModules, path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
1222
+ const linkTarget = path.join(runtimeRoot, "packages", "@repo");
1223
+ for (const consumer of [
1224
+ "dev-server",
1225
+ "docs",
1226
+ "packages"
1227
+ ]) {
1228
+ await fs.mkdir(path.join(runtimeRoot, consumer, "node_modules"), { recursive: true });
1229
+ await fs.symlink(linkTarget, path.join(runtimeRoot, consumer, "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
1230
+ }
593
1231
  await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
594
1232
  dependencies: {
595
1233
  next: "16.2.1",
@@ -1616,7 +2254,7 @@ const resolvePushConfig = async (config, options) => {
1616
2254
  envProject: process.env[BLODE_PROJECT_ENV]
1617
2255
  });
1618
2256
  const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
1619
- const authToken = (await resolveAuthToken(options.apiKey))?.token;
2257
+ const authToken = (await resolveAuthToken())?.token;
1620
2258
  const branch = options.branch ?? process.env["BLODEMD_BRANCH"] ?? process.env.GITHUB_REF_NAME ?? readGitValue([
1621
2259
  "rev-parse",
1622
2260
  "--abbrev-ref",
@@ -1633,7 +2271,7 @@ const resolvePushConfig = async (config, options) => {
1633
2271
  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
2272
  throw new Error(`Invalid project slug "${project}". ${projectSlugError}`);
1635
2273
  }
1636
- if (!authToken) throw new Error("Missing credentials. Run \"blodemd login\", pass --api-key, or set BLODEMD_API_KEY.");
2274
+ if (!authToken) throw new Error("Not logged in. Run \"blodemd login\" to authenticate.");
1637
2275
  return {
1638
2276
  apiUrl,
1639
2277
  authToken,
@@ -1656,8 +2294,7 @@ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers) =
1656
2294
  headers,
1657
2295
  method: "POST"
1658
2296
  }, "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)}`);
2297
+ log.success(`Project ${chalk.cyan(createResult.slug)} created`);
1661
2298
  return true;
1662
2299
  };
1663
2300
  const scaffoldDocsSite = async (directory, options) => {
@@ -1727,29 +2364,9 @@ program.name("blodemd").description("Blode.md CLI").version(cliVersion);
1727
2364
  program.hook("preAction", () => {
1728
2365
  assertSupportedNodeVersion();
1729
2366
  });
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) => {
2367
+ 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
2368
  intro(chalk.bold("blodemd login"));
1732
2369
  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
2370
  const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
1754
2371
  const clientId = OAUTH_CLIENT_ID;
1755
2372
  const port = parsePort(options.port);
@@ -1766,6 +2383,7 @@ program.command("login").description("Authenticate with Blode.md").option("--tok
1766
2383
  authUrl.searchParams.set("code_challenge_method", "S256");
1767
2384
  authUrl.searchParams.set("state", state);
1768
2385
  authUrl.searchParams.set("scope", "openid email profile");
2386
+ authUrl.searchParams.set("provider", "github");
1769
2387
  const callbackPromise = waitForOAuthCode({
1770
2388
  expectedState: state,
1771
2389
  redirectUrl,
@@ -1818,15 +2436,6 @@ program.command("whoami").description("Show current authentication").action(asyn
1818
2436
  log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
1819
2437
  return;
1820
2438
  }
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
2439
  const status = resolveTokenStatus(resolved);
1831
2440
  const email = resolved.user?.email ?? await fetchUserEmail(process.env["BLODEMD_API_URL"] ?? "https://api.blode.md", resolved.token);
1832
2441
  if (email) log.info(`Logged in as ${chalk.cyan(email)}`);
@@ -1866,7 +2475,7 @@ program.command("validate").description("Validate docs.json").argument("[dir]",
1866
2475
  reportCommandError("Validation failed", error);
1867
2476
  }
1868
2477
  });
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) => {
2478
+ 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
2479
  intro(chalk.bold("blodemd push"));
1871
2480
  const s = spinner();
1872
2481
  try {