blodemd 0.0.15 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +26 -1
  2. package/dist/cli.mjs +217 -217
  3. package/dist/cli.mjs.map +1 -1
  4. package/package.json +3 -36
  5. package/dev-server/app/[[...slug]]/page.tsx +0 -140
  6. package/dev-server/app/blodemd-dev/invalidate/route.ts +0 -12
  7. package/dev-server/app/blodemd-dev/static/[...path]/route.ts +0 -32
  8. package/dev-server/app/blodemd-dev/version/route.ts +0 -14
  9. package/dev-server/app/blodemd-internal/proxy/route.ts +0 -86
  10. package/dev-server/app/error.tsx +0 -24
  11. package/dev-server/app/favicon.ico +0 -0
  12. package/dev-server/app/globals.css +0 -4
  13. package/dev-server/app/layout.tsx +0 -38
  14. package/dev-server/app/not-found.tsx +0 -18
  15. package/dev-server/app/search/route.ts +0 -17
  16. package/dev-server/components/dev-reload-script.tsx +0 -86
  17. package/dev-server/components/providers.tsx +0 -15
  18. package/dev-server/lib/dev-state.ts +0 -8
  19. package/dev-server/lib/local-content-source.ts +0 -103
  20. package/dev-server/lib/local-runtime.tsx +0 -555
  21. package/dev-server/next-env.d.ts +0 -5
  22. package/dev-server/next.config.js +0 -54
  23. package/dev-server/package.json +0 -61
  24. package/dev-server/postcss.config.mjs +0 -7
  25. package/dev-server/public/glide-variable.woff2 +0 -0
  26. package/dev-server/tsconfig.json +0 -50
  27. package/docs/app/globals.css +0 -471
  28. package/docs/components/animate-ui/primitives/buttons/button.tsx +0 -14
  29. package/docs/components/api/api-playground.tsx +0 -467
  30. package/docs/components/api/api-reference.tsx +0 -157
  31. package/docs/components/content/collection-index.tsx +0 -114
  32. package/docs/components/docs/contextual-menu.tsx +0 -495
  33. package/docs/components/docs/copy-page-menu.tsx +0 -364
  34. package/docs/components/docs/doc-header.tsx +0 -223
  35. package/docs/components/docs/doc-shell.tsx +0 -355
  36. package/docs/components/docs/doc-sidebar.tsx +0 -256
  37. package/docs/components/docs/doc-toc.tsx +0 -46
  38. package/docs/components/docs/mobile-nav.tsx +0 -195
  39. package/docs/components/docs/sidebar-active-highlight.tsx +0 -34
  40. package/docs/components/docs/sidebar-scroll-area.tsx +0 -63
  41. package/docs/components/git/repo-picker.tsx +0 -529
  42. package/docs/components/icons/doc-icon.tsx +0 -96
  43. package/docs/components/mdx/accordion.tsx +0 -83
  44. package/docs/components/mdx/agent-instructions.tsx +0 -17
  45. package/docs/components/mdx/badge.tsx +0 -79
  46. package/docs/components/mdx/callout.tsx +0 -88
  47. package/docs/components/mdx/card.tsx +0 -110
  48. package/docs/components/mdx/code-block.tsx +0 -80
  49. package/docs/components/mdx/code-group.tsx +0 -203
  50. package/docs/components/mdx/color.tsx +0 -87
  51. package/docs/components/mdx/columns.tsx +0 -25
  52. package/docs/components/mdx/expandable.tsx +0 -45
  53. package/docs/components/mdx/field-layout.tsx +0 -77
  54. package/docs/components/mdx/frame.tsx +0 -23
  55. package/docs/components/mdx/get-text-content.ts +0 -18
  56. package/docs/components/mdx/icon.tsx +0 -12
  57. package/docs/components/mdx/iframe.tsx +0 -62
  58. package/docs/components/mdx/index.tsx +0 -121
  59. package/docs/components/mdx/installer.tsx +0 -20
  60. package/docs/components/mdx/panel.tsx +0 -11
  61. package/docs/components/mdx/param-field.tsx +0 -56
  62. package/docs/components/mdx/preview.tsx +0 -36
  63. package/docs/components/mdx/prompt.tsx +0 -63
  64. package/docs/components/mdx/request-example.tsx +0 -27
  65. package/docs/components/mdx/response-field.tsx +0 -42
  66. package/docs/components/mdx/steps.tsx +0 -92
  67. package/docs/components/mdx/tabs.tsx +0 -193
  68. package/docs/components/mdx/tile.tsx +0 -43
  69. package/docs/components/mdx/tooltip.tsx +0 -71
  70. package/docs/components/mdx/tree.tsx +0 -120
  71. package/docs/components/mdx/type-table.tsx +0 -71
  72. package/docs/components/mdx/update.tsx +0 -44
  73. package/docs/components/mdx/video.tsx +0 -45
  74. package/docs/components/mdx/view.tsx +0 -66
  75. package/docs/components/posthog-provider.tsx +0 -42
  76. package/docs/components/providers.tsx +0 -15
  77. package/docs/components/tenant-analytics.tsx +0 -49
  78. package/docs/components/third-parties.tsx +0 -32
  79. package/docs/components/ui/badge.tsx +0 -61
  80. package/docs/components/ui/breadcrumb.tsx +0 -112
  81. package/docs/components/ui/button-group.tsx +0 -83
  82. package/docs/components/ui/button.tsx +0 -65
  83. package/docs/components/ui/card.tsx +0 -92
  84. package/docs/components/ui/command.tsx +0 -167
  85. package/docs/components/ui/copy-button.tsx +0 -129
  86. package/docs/components/ui/dialog.tsx +0 -146
  87. package/docs/components/ui/field.tsx +0 -237
  88. package/docs/components/ui/input.tsx +0 -23
  89. package/docs/components/ui/kbd.tsx +0 -98
  90. package/docs/components/ui/label.tsx +0 -22
  91. package/docs/components/ui/morph-icon.tsx +0 -79
  92. package/docs/components/ui/popover.tsx +0 -267
  93. package/docs/components/ui/search-dialog.tsx +0 -431
  94. package/docs/components/ui/search.tsx +0 -101
  95. package/docs/components/ui/separator.tsx +0 -26
  96. package/docs/components/ui/sheet.tsx +0 -133
  97. package/docs/components/ui/sidebar.tsx +0 -758
  98. package/docs/components/ui/skeleton.tsx +0 -11
  99. package/docs/components/ui/switch.tsx +0 -32
  100. package/docs/components/ui/tabs.tsx +0 -138
  101. package/docs/components/ui/theme-toggle.tsx +0 -62
  102. package/docs/components/ui/tooltip.tsx +0 -53
  103. package/docs/components/web-mcp.tsx +0 -115
  104. package/docs/lib/agent-skills.ts +0 -48
  105. package/docs/lib/api-client.ts +0 -72
  106. package/docs/lib/config.ts +0 -6
  107. package/docs/lib/content-root.ts +0 -33
  108. package/docs/lib/content-source.ts +0 -70
  109. package/docs/lib/contextual-options.ts +0 -222
  110. package/docs/lib/dashboard-session.ts +0 -167
  111. package/docs/lib/db.ts +0 -13
  112. package/docs/lib/docs-collection.ts +0 -7
  113. package/docs/lib/docs-runtime.tsx +0 -745
  114. package/docs/lib/edge-config.ts +0 -95
  115. package/docs/lib/env.ts +0 -23
  116. package/docs/lib/etag.ts +0 -22
  117. package/docs/lib/github-install.ts +0 -33
  118. package/docs/lib/llms-full.ts +0 -82
  119. package/docs/lib/marketing-markdown.ts +0 -79
  120. package/docs/lib/marketing-site.ts +0 -31
  121. package/docs/lib/mdx.ts +0 -98
  122. package/docs/lib/navigation.ts +0 -288
  123. package/docs/lib/openapi-proxy.ts +0 -88
  124. package/docs/lib/openapi.ts +0 -158
  125. package/docs/lib/platform-config.ts +0 -6
  126. package/docs/lib/project-authz.ts +0 -46
  127. package/docs/lib/routes.ts +0 -103
  128. package/docs/lib/server-cache.ts +0 -83
  129. package/docs/lib/shiki.ts +0 -44
  130. package/docs/lib/supabase.ts +0 -37
  131. package/docs/lib/tenancy.ts +0 -364
  132. package/docs/lib/tenant-headers.ts +0 -43
  133. package/docs/lib/tenant-static.ts +0 -705
  134. package/docs/lib/tenant-utility-context.ts +0 -62
  135. package/docs/lib/tenants.ts +0 -72
  136. package/docs/lib/theme.ts +0 -29
  137. package/docs/lib/time-ago.ts +0 -24
  138. package/docs/lib/toc.ts +0 -2
  139. package/docs/lib/use-mobile.ts +0 -19
  140. package/docs/lib/use-tab-observer.ts +0 -71
  141. package/docs/lib/utils.ts +0 -5
  142. package/packages/@repo/common/dist/index.d.ts +0 -16
  143. package/packages/@repo/common/dist/index.d.ts.map +0 -1
  144. package/packages/@repo/common/dist/index.js +0 -84
  145. package/packages/@repo/common/package.json +0 -34
  146. package/packages/@repo/common/src/index.ts +0 -101
  147. package/packages/@repo/contracts/dist/analytics.d.ts +0 -21
  148. package/packages/@repo/contracts/dist/analytics.d.ts.map +0 -1
  149. package/packages/@repo/contracts/dist/analytics.js +0 -18
  150. package/packages/@repo/contracts/dist/dates.d.ts +0 -4
  151. package/packages/@repo/contracts/dist/dates.d.ts.map +0 -1
  152. package/packages/@repo/contracts/dist/dates.js +0 -2
  153. package/packages/@repo/contracts/dist/deployment.d.ts +0 -71
  154. package/packages/@repo/contracts/dist/deployment.d.ts.map +0 -1
  155. package/packages/@repo/contracts/dist/deployment.js +0 -46
  156. package/packages/@repo/contracts/dist/domain.d.ts +0 -94
  157. package/packages/@repo/contracts/dist/domain.d.ts.map +0 -1
  158. package/packages/@repo/contracts/dist/domain.js +0 -36
  159. package/packages/@repo/contracts/dist/git.d.ts +0 -28
  160. package/packages/@repo/contracts/dist/git.d.ts.map +0 -1
  161. package/packages/@repo/contracts/dist/git.js +0 -24
  162. package/packages/@repo/contracts/dist/ids.d.ts +0 -14
  163. package/packages/@repo/contracts/dist/ids.d.ts.map +0 -1
  164. package/packages/@repo/contracts/dist/ids.js +0 -11
  165. package/packages/@repo/contracts/dist/index.d.ts +0 -11
  166. package/packages/@repo/contracts/dist/index.d.ts.map +0 -1
  167. package/packages/@repo/contracts/dist/index.js +0 -12
  168. package/packages/@repo/contracts/dist/pagination.d.ts +0 -23
  169. package/packages/@repo/contracts/dist/pagination.d.ts.map +0 -1
  170. package/packages/@repo/contracts/dist/pagination.js +0 -15
  171. package/packages/@repo/contracts/dist/project.d.ts +0 -42
  172. package/packages/@repo/contracts/dist/project.d.ts.map +0 -1
  173. package/packages/@repo/contracts/dist/project.js +0 -25
  174. package/packages/@repo/contracts/dist/tenant.d.ts +0 -147
  175. package/packages/@repo/contracts/dist/tenant.d.ts.map +0 -1
  176. package/packages/@repo/contracts/dist/tenant.js +0 -58
  177. package/packages/@repo/contracts/dist/user.d.ts +0 -9
  178. package/packages/@repo/contracts/dist/user.d.ts.map +0 -1
  179. package/packages/@repo/contracts/dist/user.js +0 -9
  180. package/packages/@repo/contracts/package.json +0 -37
  181. package/packages/@repo/contracts/src/analytics.ts +0 -33
  182. package/packages/@repo/contracts/src/dates.ts +0 -4
  183. package/packages/@repo/contracts/src/deployment.ts +0 -73
  184. package/packages/@repo/contracts/src/domain.ts +0 -51
  185. package/packages/@repo/contracts/src/git.ts +0 -31
  186. package/packages/@repo/contracts/src/ids.ts +0 -23
  187. package/packages/@repo/contracts/src/index.ts +0 -12
  188. package/packages/@repo/contracts/src/pagination.ts +0 -21
  189. package/packages/@repo/contracts/src/project.ts +0 -32
  190. package/packages/@repo/contracts/src/tenant.ts +0 -94
  191. package/packages/@repo/contracts/src/user.ts +0 -12
  192. package/packages/@repo/models/dist/docs-config.d.ts +0 -961
  193. package/packages/@repo/models/dist/docs-config.d.ts.map +0 -1
  194. package/packages/@repo/models/dist/docs-config.js +0 -531
  195. package/packages/@repo/models/dist/index.d.ts +0 -3
  196. package/packages/@repo/models/dist/index.d.ts.map +0 -1
  197. package/packages/@repo/models/dist/index.js +0 -3
  198. package/packages/@repo/models/dist/tenant.d.ts +0 -37
  199. package/packages/@repo/models/dist/tenant.d.ts.map +0 -1
  200. package/packages/@repo/models/dist/tenant.js +0 -1
  201. package/packages/@repo/models/package.json +0 -37
  202. package/packages/@repo/models/src/docs-config.ts +0 -629
  203. package/packages/@repo/models/src/index.ts +0 -3
  204. package/packages/@repo/models/src/tenant.ts +0 -44
  205. package/packages/@repo/prebuild/dist/index.d.ts +0 -2
  206. package/packages/@repo/prebuild/dist/index.d.ts.map +0 -1
  207. package/packages/@repo/prebuild/dist/index.js +0 -2
  208. package/packages/@repo/prebuild/dist/openapi.d.ts +0 -43
  209. package/packages/@repo/prebuild/dist/openapi.d.ts.map +0 -1
  210. package/packages/@repo/prebuild/dist/openapi.js +0 -86
  211. package/packages/@repo/prebuild/package.json +0 -39
  212. package/packages/@repo/prebuild/src/index.ts +0 -2
  213. package/packages/@repo/prebuild/src/openapi.ts +0 -154
  214. package/packages/@repo/previewing/dist/blob-source.d.ts +0 -16
  215. package/packages/@repo/previewing/dist/blob-source.d.ts.map +0 -1
  216. package/packages/@repo/previewing/dist/blob-source.js +0 -128
  217. package/packages/@repo/previewing/dist/config-loader.d.ts +0 -134
  218. package/packages/@repo/previewing/dist/config-loader.d.ts.map +0 -1
  219. package/packages/@repo/previewing/dist/config-loader.js +0 -231
  220. package/packages/@repo/previewing/dist/constants.d.ts +0 -15
  221. package/packages/@repo/previewing/dist/constants.d.ts.map +0 -1
  222. package/packages/@repo/previewing/dist/constants.js +0 -14
  223. package/packages/@repo/previewing/dist/content-index.d.ts +0 -7
  224. package/packages/@repo/previewing/dist/content-index.d.ts.map +0 -1
  225. package/packages/@repo/previewing/dist/content-index.js +0 -291
  226. package/packages/@repo/previewing/dist/content-source.d.ts +0 -12
  227. package/packages/@repo/previewing/dist/content-source.d.ts.map +0 -1
  228. package/packages/@repo/previewing/dist/content-source.js +0 -1
  229. package/packages/@repo/previewing/dist/fs-source.d.ts +0 -11
  230. package/packages/@repo/previewing/dist/fs-source.d.ts.map +0 -1
  231. package/packages/@repo/previewing/dist/fs-source.js +0 -71
  232. package/packages/@repo/previewing/dist/index.d.ts +0 -16
  233. package/packages/@repo/previewing/dist/index.d.ts.map +0 -1
  234. package/packages/@repo/previewing/dist/index.js +0 -13
  235. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +0 -3
  236. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +0 -1
  237. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +0 -176
  238. package/packages/@repo/previewing/dist/markdown/format.d.ts +0 -8
  239. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +0 -1
  240. package/packages/@repo/previewing/dist/markdown/format.js +0 -43
  241. package/packages/@repo/previewing/dist/markdown/links.d.ts +0 -3
  242. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +0 -1
  243. package/packages/@repo/previewing/dist/markdown/links.js +0 -31
  244. package/packages/@repo/previewing/dist/openapi-pages.d.ts +0 -7
  245. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +0 -1
  246. package/packages/@repo/previewing/dist/openapi-pages.js +0 -200
  247. package/packages/@repo/previewing/dist/search-index.d.ts +0 -4
  248. package/packages/@repo/previewing/dist/search-index.d.ts.map +0 -1
  249. package/packages/@repo/previewing/dist/search-index.js +0 -23
  250. package/packages/@repo/previewing/dist/serialization.d.ts +0 -13
  251. package/packages/@repo/previewing/dist/serialization.d.ts.map +0 -1
  252. package/packages/@repo/previewing/dist/serialization.js +0 -108
  253. package/packages/@repo/previewing/dist/toc-index.d.ts +0 -5
  254. package/packages/@repo/previewing/dist/toc-index.d.ts.map +0 -1
  255. package/packages/@repo/previewing/dist/toc-index.js +0 -53
  256. package/packages/@repo/previewing/dist/types.d.ts +0 -92
  257. package/packages/@repo/previewing/dist/types.d.ts.map +0 -1
  258. package/packages/@repo/previewing/dist/types.js +0 -1
  259. package/packages/@repo/previewing/dist/utility-index.d.ts +0 -7
  260. package/packages/@repo/previewing/dist/utility-index.d.ts.map +0 -1
  261. package/packages/@repo/previewing/dist/utility-index.js +0 -201
  262. package/packages/@repo/previewing/package.json +0 -41
  263. package/packages/@repo/previewing/src/blob-source.ts +0 -185
  264. package/packages/@repo/previewing/src/config-loader.ts +0 -316
  265. package/packages/@repo/previewing/src/constants.ts +0 -16
  266. package/packages/@repo/previewing/src/content-index.ts +0 -384
  267. package/packages/@repo/previewing/src/content-source.ts +0 -12
  268. package/packages/@repo/previewing/src/fs-source.ts +0 -103
  269. package/packages/@repo/previewing/src/index.ts +0 -71
  270. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +0 -254
  271. package/packages/@repo/previewing/src/markdown/format.ts +0 -63
  272. package/packages/@repo/previewing/src/markdown/links.ts +0 -46
  273. package/packages/@repo/previewing/src/openapi-pages.ts +0 -319
  274. package/packages/@repo/previewing/src/search-index.ts +0 -38
  275. package/packages/@repo/previewing/src/serialization.ts +0 -191
  276. package/packages/@repo/previewing/src/toc-index.ts +0 -72
  277. package/packages/@repo/previewing/src/types.ts +0 -105
  278. package/packages/@repo/previewing/src/utility-index.ts +0 -304
  279. package/packages/@repo/validation/dist/index.d.ts +0 -12
  280. package/packages/@repo/validation/dist/index.d.ts.map +0 -1
  281. package/packages/@repo/validation/dist/index.js +0 -30
  282. package/packages/@repo/validation/package.json +0 -38
  283. package/packages/@repo/validation/src/blodemd-docs-schema.json +0 -3575
  284. package/packages/@repo/validation/src/index.ts +0 -59
  285. package/scripts/prepare-package.mjs +0 -53
  286. package/scripts/repo-packages.mjs +0 -8
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - **One-command deploy:** Push your entire docs folder to Blode.md with `blodemd push`.
10
10
  - **Scaffold in seconds:** Generate a ready-to-edit docs folder with `blodemd new`.
11
11
  - **Config validation:** Catch `docs.json` errors before deploying.
12
- - **Zero keys:** Sign in once with GitHub in your browser — the CLI handles the rest.
12
+ - **Zero keys locally:** Sign in once with GitHub in your browser — no keys for local deploys. CI uses a project deploy key.
13
13
 
14
14
  ## Install
15
15
 
@@ -41,6 +41,30 @@ blodemd dev
41
41
  blodemd push
42
42
  ```
43
43
 
44
+ ## Deploy from CI
45
+
46
+ The [GitHub App](#auto-deploy-without-the-cli) is the recommended zero-config path. To run the deploy yourself from GitHub Actions, use a project deploy key stored in the `BLODEMD_API_KEY` secret:
47
+
48
+ ```yaml
49
+ name: Deploy docs
50
+ on:
51
+ push:
52
+ branches: [main]
53
+ jobs:
54
+ deploy:
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v5
58
+ - uses: actions/setup-node@v5
59
+ with:
60
+ node-version: 24
61
+ - run: npx blodemd@latest push --project your-project-slug
62
+ env:
63
+ BLODEMD_API_KEY: ${{ secrets.BLODEMD_API_KEY }}
64
+ ```
65
+
66
+ Create a deploy key in the dashboard under **Settings → Deploy keys**, or copy the `bmd_...` key that `blodemd push` prints the first time it auto-creates a project. Deploy keys are project-scoped and deploy-only — store them as CI secrets, never commit them.
67
+
44
68
  ## Commands
45
69
 
46
70
  ```bash
@@ -57,6 +81,7 @@ blodemd dev [dir] Start the local docs preview server
57
81
 
58
82
  ```
59
83
  --project <slug> Project slug (env: BLODEMD_PROJECT)
84
+ --api-key <token> API key (env: BLODEMD_API_KEY)
60
85
  --api-url <url> API URL (env: BLODEMD_API_URL)
61
86
  --branch <name> Git branch (env: BLODEMD_BRANCH)
62
87
  --message <msg> Deploy message (env: BLODEMD_COMMIT_MESSAGE)
package/dist/cli.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
2
  import { Command, InvalidArgumentError } from "commander";
4
3
  import { confirm, intro, isCancel, log, select, spinner, text } from "@clack/prompts";
5
4
  import chalk from "chalk";
@@ -20,6 +19,7 @@ import { watch } from "chokidar";
20
19
  import { readFileSync } from "node:fs";
21
20
  //#region src/constants.ts
22
21
  const CLI_NAME = "blodemd";
22
+ const BLODE_API_KEY_ENV = "BLODEMD_API_KEY";
23
23
  const BLODE_PROJECT_ENV = "BLODEMD_PROJECT";
24
24
  const OAUTH_CLIENT_ID = "6b5f9860-fe96-4a83-b1ad-266260523c91";
25
25
  const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
@@ -55,6 +55,7 @@ const toCliError = (error) => {
55
55
  if (error instanceof Error) {
56
56
  if (error instanceof TypeError && error.message.includes("fetch")) return new CliError("Cannot connect to Blode.md API.", EXIT_CODES.NETWORK, "Check your internet connection and API URL configuration.");
57
57
  if (error.name === "TimeoutError" || error.name === "AbortError") return new CliError("Request timed out.", EXIT_CODES.NETWORK, "The API may be unavailable. Try again later.");
58
+ if (/\b401\b/.test(error.message)) return new CliError(error.message, EXIT_CODES.AUTH_REQUIRED, "Check your API key or run \"blodemd login\".");
58
59
  return new CliError(error.message, EXIT_CODES.ERROR);
59
60
  }
60
61
  return new CliError("Unknown error", EXIT_CODES.ERROR);
@@ -1220,6 +1221,67 @@ const registerAnalyticsCommand = (program) => {
1220
1221
  });
1221
1222
  };
1222
1223
  //#endregion
1224
+ //#region src/output.ts
1225
+ const isInteractive = (json = false) => !json && Boolean(process.stdout.isTTY) && !process.env.CI;
1226
+ const writeLine = (message) => {
1227
+ process.stderr.write(`${message}\n`);
1228
+ };
1229
+ const createReporter = ({ json = false } = {}) => {
1230
+ const interactive = isInteractive(json);
1231
+ if (!interactive) return {
1232
+ info: writeLine,
1233
+ interactive,
1234
+ json: (payload) => {
1235
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
1236
+ },
1237
+ step: writeLine,
1238
+ stop: (message) => {
1239
+ if (message) writeLine(message);
1240
+ },
1241
+ success: writeLine,
1242
+ warn: writeLine
1243
+ };
1244
+ const s = spinner();
1245
+ let active = false;
1246
+ return {
1247
+ info(message) {
1248
+ if (active) {
1249
+ s.stop();
1250
+ active = false;
1251
+ }
1252
+ log.info(message);
1253
+ },
1254
+ interactive,
1255
+ json() {},
1256
+ step(message) {
1257
+ if (active) s.message(message);
1258
+ else {
1259
+ s.start(message);
1260
+ active = true;
1261
+ }
1262
+ },
1263
+ stop(message) {
1264
+ if (active) {
1265
+ s.stop(message);
1266
+ active = false;
1267
+ }
1268
+ },
1269
+ success(message) {
1270
+ if (active) {
1271
+ s.stop(message);
1272
+ active = false;
1273
+ } else log.success(message);
1274
+ },
1275
+ warn(message) {
1276
+ if (active) {
1277
+ s.stop();
1278
+ active = false;
1279
+ }
1280
+ log.warn(message);
1281
+ }
1282
+ };
1283
+ };
1284
+ //#endregion
1223
1285
  //#region src/scaffold.ts
1224
1286
  const SCAFFOLD_TEMPLATES = ["minimal", "starter"];
1225
1287
  const DEFAULT_SCAFFOLD_DIRECTORY = "docs";
@@ -1617,11 +1679,16 @@ const collectFiles = async (root, directory = root) => {
1617
1679
  }
1618
1680
  return files.toSorted((left, right) => left.localeCompare(right));
1619
1681
  };
1620
- const reportCommandError = (prefix, error) => {
1682
+ const reportCommandError = (prefix, error, options) => {
1621
1683
  const cliError = toCliError(error);
1622
- log.error(`${prefix}: ${cliError.message}`);
1623
- if (cliError.hint) log.info(cliError.hint);
1624
- log.info("Failed");
1684
+ if (isInteractive(options?.json)) {
1685
+ log.error(`${prefix}: ${cliError.message}`);
1686
+ if (cliError.hint) log.info(cliError.hint);
1687
+ log.info("Failed");
1688
+ } else {
1689
+ process.stderr.write(`${prefix}: ${cliError.message}\n`);
1690
+ if (cliError.hint) process.stderr.write(`${cliError.hint}\n`);
1691
+ }
1625
1692
  process.exitCode = cliError.exitCode;
1626
1693
  };
1627
1694
  const parseScaffoldTemplate = (value) => {
@@ -1827,6 +1894,7 @@ const registerAuthCommands = (program) => {
1827
1894
  const resolved = await resolveAuthToken();
1828
1895
  if (!resolved) {
1829
1896
  log.warn("Not logged in. Run \"blodemd login\" to authenticate.");
1897
+ process.exitCode = EXIT_CODES.AUTH_REQUIRED;
1830
1898
  return;
1831
1899
  }
1832
1900
  const status = resolveTokenStatus(resolved);
@@ -1904,51 +1972,7 @@ const DEV_READY_TIMEOUT_MS = 45e3;
1904
1972
  const DEV_PORT_SCAN_LIMIT = 10;
1905
1973
  const DEV_SHUTDOWN_TIMEOUT_MS = 5e3;
1906
1974
  const LOCALHOST = "127.0.0.1";
1907
- const RUNTIME_EXCLUDE_DIRS = new Set([
1908
- ".next",
1909
- ".turbo",
1910
- "node_modules"
1911
- ]);
1912
- const STANDALONE_RUNTIME_MAX_AGE_MS = 1440 * 60 * 1e3;
1913
- const STANDALONE_RUNTIME_PREFIX = "standalone-runtime-";
1914
- const TURBOPACK_ARGS = ["dev", "--turbopack"];
1915
- const fileExists = async (filePath) => {
1916
- try {
1917
- await fs.access(filePath);
1918
- return true;
1919
- } catch {
1920
- return false;
1921
- }
1922
- };
1923
- const resolveCommonAncestor = (pathsToCompare) => {
1924
- const [firstPath, ...restPaths] = pathsToCompare;
1925
- if (!firstPath) return path.sep;
1926
- const first = path.resolve(firstPath);
1927
- const { root } = path.parse(first);
1928
- const firstSegments = first.slice(root.length).split(path.sep).filter(Boolean);
1929
- const sharedSegments = [];
1930
- for (const [index, segment] of firstSegments.entries()) {
1931
- if (!restPaths.every((candidatePath) => {
1932
- const candidate = path.resolve(candidatePath);
1933
- if (path.parse(candidate).root !== root) return false;
1934
- return candidate.slice(root.length).split(path.sep).filter(Boolean)[index] === segment;
1935
- })) break;
1936
- sharedSegments.push(segment);
1937
- }
1938
- return path.join(root, ...sharedSegments);
1939
- };
1940
- const cleanupStandaloneRuntimeRoots = async (configDir, maxAgeMs = STANDALONE_RUNTIME_MAX_AGE_MS) => {
1941
- const cutoff = Date.now() - maxAgeMs;
1942
- const entries = await fs.readdir(configDir, { withFileTypes: true });
1943
- await Promise.all(entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(STANDALONE_RUNTIME_PREFIX)).map(async (entry) => {
1944
- const entryPath = path.join(configDir, entry.name);
1945
- if ((await fs.stat(entryPath)).mtimeMs >= cutoff) return;
1946
- await fs.rm(entryPath, {
1947
- force: true,
1948
- recursive: true
1949
- });
1950
- }));
1951
- };
1975
+ const DEV_PACKAGE_NAME = "blodemd-dev";
1952
1976
  const probePortAvailability = async (port) => {
1953
1977
  const server = createServer$1();
1954
1978
  const listening = (async () => {
@@ -2001,122 +2025,33 @@ const shutdownChildProcess = async (child, timeoutMs = DEV_SHUTDOWN_TIMEOUT_MS)
2001
2025
  clearTimeout(timer);
2002
2026
  });
2003
2027
  };
2004
- /**
2005
- * Derive the CLI npm package root from the running script path.
2006
- * The CLI entry point is at `<pkg-root>/dist/cli.mjs`.
2007
- */
2008
- const resolveCliPackageRoot = (cliFilePath) => path.dirname(path.dirname(cliFilePath));
2009
- const copyStandaloneTree = async (sourceDir, targetDir) => {
2010
- await fs.cp(sourceDir, targetDir, {
2011
- filter: (source) => {
2012
- const relative = path.relative(sourceDir, source);
2013
- if (!relative) return true;
2014
- const topSegment = relative.split(path.sep)[0] ?? "";
2015
- return !RUNTIME_EXCLUDE_DIRS.has(topSegment);
2016
- },
2017
- recursive: true
2018
- });
2019
- };
2020
- const isStandaloneCliInstall = async (cliPackageRoot) => {
2028
+ const fileExists = async (filePath) => {
2021
2029
  try {
2022
- return (await fs.realpath(cliPackageRoot)).split(path.sep).includes("node_modules");
2030
+ await fs.access(filePath);
2031
+ return true;
2023
2032
  } catch {
2024
- return cliPackageRoot.split(path.sep).includes("node_modules");
2033
+ return false;
2025
2034
  }
2026
2035
  };
2027
- const createStandaloneRuntimeRoot = async (configDir = CONFIG_DIR) => {
2028
- await fs.mkdir(configDir, { recursive: true });
2029
- await cleanupStandaloneRuntimeRoots(configDir);
2030
- return await fs.mkdtemp(path.join(configDir, STANDALONE_RUNTIME_PREFIX));
2031
- };
2032
2036
  /**
2033
- * Locate the `node_modules` directory that actually contains the CLI's
2034
- * transitive dependencies. Package managers like npm/yarn-classic (and
2035
- * `npx` caches) hoist shared deps above the package directory, so
2036
- * `<cliPackageRoot>/node_modules` may not exist or may not contain `next`.
2037
- * Resolve `next/package.json` and use the directory that owns it.
2037
+ * Derive the CLI npm package root from the running script path.
2038
+ * The CLI entry point is at `<pkg-root>/dist/cli.mjs`.
2038
2039
  */
2039
- const resolveRuntimeNodeModules = async (cliPackageRoot) => {
2040
- const localNodeModules = path.join(cliPackageRoot, "node_modules");
2041
- if (await fileExists(path.join(localNodeModules, "next", "package.json"))) return localNodeModules;
2042
- const nextPkgPath = createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
2043
- return path.dirname(path.dirname(nextPkgPath));
2044
- };
2045
- const materializeStandaloneRuntime = async (cliPackageRoot) => {
2046
- const runtimeRoot = await createStandaloneRuntimeRoot();
2047
- try {
2048
- for (const dir of [
2049
- "dev-server",
2050
- "docs",
2051
- "packages"
2052
- ]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
2053
- const runtimeNodeModules = await resolveRuntimeNodeModules(cliPackageRoot);
2054
- await fs.symlink(runtimeNodeModules, path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
2055
- const linkTarget = path.join(runtimeRoot, "packages", "@repo");
2056
- for (const consumer of [
2057
- "dev-server",
2058
- "docs",
2059
- "packages"
2060
- ]) {
2061
- await fs.mkdir(path.join(runtimeRoot, consumer, "node_modules"), { recursive: true });
2062
- await fs.symlink(linkTarget, path.join(runtimeRoot, consumer, "node_modules", "@repo"), process.platform === "win32" ? "junction" : "dir");
2063
- }
2064
- await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
2065
- dependencies: {
2066
- next: "16.2.1",
2067
- react: "^19.2.0",
2068
- "react-dom": "^19.2.0"
2069
- },
2070
- devDependencies: {
2071
- "@types/node": "^24.12.2",
2072
- "@types/react": "19.2.14",
2073
- "@types/react-dom": "19.2.3",
2074
- typescript: "6.0.2"
2075
- },
2076
- name: "blodemd-dev-server",
2077
- private: true,
2078
- type: "module"
2079
- }, null, 2)}\n`);
2080
- return {
2081
- devServerDir: path.join(runtimeRoot, "dev-server"),
2082
- runtimeRoot
2083
- };
2084
- } catch (error) {
2085
- await fs.rm(runtimeRoot, {
2086
- force: true,
2087
- recursive: true
2088
- });
2089
- throw error;
2090
- }
2091
- };
2040
+ const resolveCliPackageRoot = (cliFilePath) => path.dirname(path.dirname(cliFilePath));
2092
2041
  /**
2093
- * Check if a shipped dev-server exists alongside an installed CLI package.
2094
- * We only use standalone mode when the package root lives under `node_modules`.
2042
+ * Read the CLI's own version so the delegated `blodemd-dev` invocation is
2043
+ * pinned to the exact matching release (they version together via changesets).
2095
2044
  */
2096
- const findStandaloneDevServer = async (cliPackageRoot) => {
2097
- const devServerDir = path.join(cliPackageRoot, "dev-server");
2098
- if (!await fileExists(path.join(devServerDir, "next.config.js"))) return null;
2099
- if (!await isStandaloneCliInstall(cliPackageRoot)) return null;
2100
- try {
2101
- createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
2102
- } catch {
2103
- return null;
2104
- }
2105
- const runtime = await materializeStandaloneRuntime(cliPackageRoot);
2106
- return {
2107
- devServerDir: runtime.devServerDir,
2108
- mode: "standalone",
2109
- nextPackageRoot: cliPackageRoot,
2110
- runtimeRoot: runtime.runtimeRoot
2111
- };
2045
+ const readCliPackageVersion = async (cliFilePath) => {
2046
+ const packageJsonPath = path.join(resolveCliPackageRoot(cliFilePath), "package.json");
2047
+ const raw = await fs.readFile(packageJsonPath, "utf8");
2048
+ return JSON.parse(raw).version ?? "0.0.0";
2112
2049
  };
2113
2050
  /**
2114
- * Resolve the `next` CLI binary from the blodemd package's own dependencies.
2051
+ * Walk up from the running CLI looking for the blodemd monorepo checkout.
2052
+ * Returns the repo root when found (development mode), otherwise `null`
2053
+ * (installed mode, which delegates to `blodemd-dev`).
2115
2054
  */
2116
- const resolveNextBin = (cliPackageRoot) => {
2117
- const nextPkgPath = createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
2118
- return path.join(path.dirname(nextPkgPath), "dist", "bin", "next");
2119
- };
2120
2055
  const findMonorepoRoot = async (start) => {
2121
2056
  let current = start;
2122
2057
  while (true) {
@@ -2124,35 +2059,50 @@ const findMonorepoRoot = async (start) => {
2124
2059
  if (await fileExists(packageJsonPath)) {
2125
2060
  const raw = await fs.readFile(packageJsonPath, "utf8");
2126
2061
  const workspaces = JSON.parse(raw).workspaces ?? [];
2127
- if (workspaces.includes("apps/*") && workspaces.includes("packages/*")) return current;
2062
+ const hasWorkspaces = workspaces.includes("apps/*") && workspaces.includes("packages/*");
2063
+ const hasDevServer = await fileExists(path.join(current, "apps", "dev-server", "next.config.js"));
2064
+ if (hasWorkspaces && hasDevServer) return current;
2128
2065
  }
2129
2066
  const parent = path.dirname(current);
2130
2067
  if (parent === current) break;
2131
2068
  current = parent;
2132
2069
  }
2133
- throw new CliError("Could not locate the blodemd dev server.", EXIT_CODES.ERROR, "Make sure blodemd is installed correctly (npm i blodemd).");
2070
+ return null;
2134
2071
  };
2135
2072
  const resolveDevServer = async (cliFilePath) => {
2136
- const standalone = await findStandaloneDevServer(resolveCliPackageRoot(cliFilePath));
2137
- if (standalone) return standalone;
2138
- return {
2073
+ const repoRoot = await findMonorepoRoot(path.dirname(cliFilePath));
2074
+ if (repoRoot) return {
2139
2075
  mode: "monorepo",
2140
- repoRoot: await findMonorepoRoot(path.dirname(cliFilePath))
2076
+ repoRoot
2077
+ };
2078
+ return {
2079
+ devPackageVersion: await readCliPackageVersion(cliFilePath),
2080
+ mode: "delegated"
2141
2081
  };
2142
2082
  };
2143
2083
  const buildDevServerLaunch = (server, { root, port }) => {
2144
- if (server.mode === "standalone") return {
2145
- args: [resolveNextBin(server.nextPackageRoot), ...TURBOPACK_ARGS],
2146
- command: process.execPath,
2147
- cwd: server.devServerDir,
2148
- env: {
2149
- ...process.env,
2150
- BLODEMD_PACKAGES_DIR: path.join(server.runtimeRoot, "packages"),
2151
- BLODEMD_TURBOPACK_ROOT: resolveCommonAncestor([server.nextPackageRoot, server.runtimeRoot]),
2152
- DOCS_ROOT: root,
2153
- PORT: String(port)
2154
- }
2155
- };
2084
+ if (server.mode === "delegated") {
2085
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
2086
+ return {
2087
+ args: [
2088
+ "-y",
2089
+ `${DEV_PACKAGE_NAME}@${server.devPackageVersion}`,
2090
+ "dev",
2091
+ "--port",
2092
+ String(port),
2093
+ "--dir",
2094
+ root,
2095
+ "--no-open"
2096
+ ],
2097
+ command: npxCommand,
2098
+ cwd: process.cwd(),
2099
+ env: {
2100
+ ...process.env,
2101
+ DOCS_ROOT: root,
2102
+ PORT: String(port)
2103
+ }
2104
+ };
2105
+ }
2156
2106
  return {
2157
2107
  args: [
2158
2108
  "run",
@@ -2198,7 +2148,6 @@ const defaultDevCommandDependencies = {
2198
2148
  getLog: log,
2199
2149
  getOpen: open,
2200
2150
  parsePortValue: parsePort,
2201
- removeDirectory: fs.rm,
2202
2151
  resolveDevPortValue: resolveDevPort,
2203
2152
  resolveDocsRootValue: resolveDocsRoot,
2204
2153
  resolveServer: resolveDevServer,
@@ -2233,10 +2182,6 @@ const devCommand = async ({ dir, openBrowser, port: portValue }, dependencies =
2233
2182
  watcher = null;
2234
2183
  }
2235
2184
  await dependencies.shutdownChild(child);
2236
- if (server.mode === "standalone") await dependencies.removeDirectory(server.runtimeRoot, {
2237
- force: true,
2238
- recursive: true
2239
- });
2240
2185
  };
2241
2186
  process.once("SIGINT", closeAll);
2242
2187
  process.once("SIGTERM", closeAll);
@@ -2513,6 +2458,31 @@ const registerNewCommand = (program) => {
2513
2458
  });
2514
2459
  };
2515
2460
  //#endregion
2461
+ //#region src/commands/projects.ts
2462
+ const registerProjectsCommand = (program) => {
2463
+ program.command("projects").description("List your projects").option("--api-url <url>", "API URL (env: BLODEMD_API_URL)").option("--json", "output machine-readable JSON (implies non-interactive)").action(async (options) => {
2464
+ const reporter = createReporter({ json: options.json });
2465
+ if (reporter.interactive) intro(chalk.bold("blodemd projects"));
2466
+ try {
2467
+ const resolved = await resolveAuthToken();
2468
+ if (!resolved?.token) throw new CliError("Run \"blodemd login\" to list your projects. API keys cannot list projects.", EXIT_CODES.AUTH_REQUIRED, "Run \"blodemd login\" to authenticate.");
2469
+ const apiUrl = options.apiUrl ?? process.env["BLODEMD_API_URL"] ?? "https://api.blode.md";
2470
+ const projects = await requestJson(new URL("/projects", apiUrl).toString(), { headers: { Authorization: `Bearer ${resolved.token}` } }, "Failed to list projects");
2471
+ reporter.json(projects);
2472
+ if (projects.length === 0) {
2473
+ reporter.info("No projects yet.");
2474
+ return;
2475
+ }
2476
+ for (const project of projects) {
2477
+ const label = project.name ? `${chalk.cyan(project.slug)} — ${project.name}` : chalk.cyan(project.slug);
2478
+ reporter.info(label);
2479
+ }
2480
+ } catch (error) {
2481
+ reportCommandError("Projects failed", error, { json: options.json });
2482
+ }
2483
+ });
2484
+ };
2485
+ //#endregion
2516
2486
  //#region src/upload.ts
2517
2487
  const TEXT_CONTENT_TYPES = {
2518
2488
  ".css": "text/css; charset=utf-8",
@@ -2560,10 +2530,10 @@ const resolveAuthHeaders = async (apiKeyOption) => {
2560
2530
  const apiKey = (apiKeyOption ?? process.env["BLODEMD_API_KEY"])?.trim();
2561
2531
  if (apiKey) return {
2562
2532
  canAutoCreate: false,
2563
- headers: { "x-admin-token": apiKey }
2533
+ headers: { Authorization: `Bearer ${apiKey}` }
2564
2534
  };
2565
2535
  const resolved = await resolveAuthToken();
2566
- if (!resolved?.token) throw new Error("Not logged in. Run \"blodemd login\" to authenticate, pass --api-key, or set BLODEMD_API_KEY.");
2536
+ if (!resolved?.token) throw new Error("Not logged in. Run \"blodemd login\" to authenticate, pass --api-key, or set BLODEMD_API_KEY (see https://blode.md/docs/deployment/ci).");
2567
2537
  return {
2568
2538
  canAutoCreate: true,
2569
2539
  headers: { Authorization: `Bearer ${resolved.token}` }
@@ -2604,8 +2574,9 @@ const resolvePushConfig = async (config, options) => {
2604
2574
  usedLegacyNameFallback
2605
2575
  };
2606
2576
  };
2607
- const autoCreateProject = async (project, projectDisplayName, apiUrl, headers, canAutoCreate) => {
2577
+ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers, canAutoCreate, reporter) => {
2608
2578
  if (!canAutoCreate) throw new Error(`Project "${project}" not found. Create it at blode.md or login with "blodemd login" to auto-create.`);
2579
+ if (!reporter.interactive) throw new Error(`Project "${project}" not found. Create it in the dashboard or run \`blodemd push\` in an interactive terminal to auto-create it.`);
2609
2580
  const shouldCreate = await confirm({ message: `Project "${project}" doesn't exist. Create it?` });
2610
2581
  if (isCancel(shouldCreate) || !shouldCreate) return false;
2611
2582
  const createResult = await requestJson(new URL("/projects", apiUrl).toString(), {
@@ -2616,12 +2587,18 @@ const autoCreateProject = async (project, projectDisplayName, apiUrl, headers, c
2616
2587
  headers,
2617
2588
  method: "POST"
2618
2589
  }, "Failed to create project");
2619
- log.success(`Project ${chalk.cyan(createResult.slug)} created`);
2590
+ reporter.success(`Project ${chalk.cyan(createResult.slug)} created`);
2591
+ const keyResult = await requestJson(new URL(`/projects/${createResult.id}/keys`, apiUrl).toString(), {
2592
+ body: JSON.stringify({ name: "CI deploy key" }),
2593
+ headers,
2594
+ method: "POST"
2595
+ }, "Failed to create deploy key");
2596
+ reporter.info(`Deploy key created (save this — shown once, use as ${BLODE_API_KEY_ENV} in CI): ${chalk.cyan(keyResult.key)}`);
2620
2597
  return true;
2621
2598
  };
2622
2599
  const MAX_BATCH_BYTES = 4 * 1024 * 1024;
2623
- const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
2624
- s.start(`Uploading ${files.length} files`);
2600
+ const uploadFiles = async (files, root, apiPath, deploymentId, headers, reporter) => {
2601
+ reporter.step(`Uploading ${files.length} files`);
2625
2602
  let uploaded = 0;
2626
2603
  for await (const batch of createUploadBatches({
2627
2604
  files,
@@ -2634,26 +2611,26 @@ const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
2634
2611
  method: "POST"
2635
2612
  }, "Failed to upload files");
2636
2613
  uploaded += batch.length;
2637
- s.message(`Uploading files (${uploaded}/${files.length})`);
2614
+ reporter.step(`Uploading files (${uploaded}/${files.length})`);
2638
2615
  }
2639
- s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);
2616
+ reporter.success(`Uploaded ${chalk.cyan(String(files.length))} files`);
2640
2617
  };
2641
2618
  const registerPushCommand = (program) => {
2642
- program.command("push").description("Deploy docs").argument("[dir]", "docs directory").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-key <token>", "API key (env: BLODEMD_API_KEY)").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) => {
2643
- intro(chalk.bold("blodemd push"));
2644
- const s = spinner();
2619
+ program.command("push").description("Deploy docs").argument("[dir]", "docs directory").option("--project <slug>", "project slug (env: BLODEMD_PROJECT)").option("--api-key <token>", "API key (env: BLODEMD_API_KEY)").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)").option("--json", "output machine-readable JSON (implies non-interactive)").action(async (dir, options) => {
2620
+ const reporter = createReporter({ json: options.json });
2621
+ if (reporter.interactive) intro(chalk.bold("blodemd push"));
2645
2622
  try {
2646
2623
  const root = await resolveDocsRoot(dir);
2647
- s.start("Validating configuration");
2624
+ reporter.step("Validating configuration");
2648
2625
  const { config, warnings } = await loadValidatedSiteConfig(root);
2649
- s.stop("Configuration valid");
2650
- for (const warning of warnings) log.warn(warning);
2626
+ reporter.success("Configuration valid");
2627
+ for (const warning of warnings) reporter.warn(warning);
2651
2628
  const { project, projectDisplayName, apiUrl, authHeaders, canAutoCreate, branch, commitMessage, usedLegacyNameFallback } = await resolvePushConfig(config, options);
2652
- if (usedLegacyNameFallback) log.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
2653
- s.start("Collecting files");
2629
+ if (usedLegacyNameFallback) reporter.warn(LEGACY_PROJECT_NAME_FALLBACK_WARNING);
2630
+ reporter.step("Collecting files");
2654
2631
  const files = await collectFiles(root);
2655
2632
  if (files.length === 0) throw new Error("No files found to deploy.");
2656
- s.stop(`Found ${chalk.cyan(String(files.length))} files`);
2633
+ reporter.success(`Found ${chalk.cyan(String(files.length))} files`);
2657
2634
  const headers = {
2658
2635
  ...authHeaders,
2659
2636
  "Content-Type": "application/json"
@@ -2663,7 +2640,7 @@ const registerPushCommand = (program) => {
2663
2640
  branch,
2664
2641
  commitMessage
2665
2642
  });
2666
- s.start("Creating deployment");
2643
+ reporter.step("Creating deployment");
2667
2644
  let deployment;
2668
2645
  try {
2669
2646
  deployment = await requestJson(apiPath(""), {
@@ -2673,34 +2650,39 @@ const registerPushCommand = (program) => {
2673
2650
  }, "Failed to create deployment");
2674
2651
  } catch (error) {
2675
2652
  if (!(error instanceof Error ? error.message : "").includes("404")) throw error;
2676
- s.stop("Project not found");
2677
- if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers, canAutoCreate)) {
2678
- log.info("Cancelled");
2653
+ reporter.stop("Project not found");
2654
+ if (!await autoCreateProject(project, projectDisplayName, apiUrl, headers, canAutoCreate, reporter)) {
2655
+ reporter.info("Cancelled");
2679
2656
  return;
2680
2657
  }
2681
- s.start("Creating deployment");
2658
+ reporter.step("Creating deployment");
2682
2659
  deployment = await requestJson(apiPath(""), {
2683
2660
  body: createDeploymentBody,
2684
2661
  headers,
2685
2662
  method: "POST"
2686
2663
  }, "Failed to create deployment");
2687
2664
  }
2688
- s.stop(`Deployment ${chalk.cyan(deployment.id)} created`);
2689
- await uploadFiles(files, root, apiPath, deployment.id, headers, s);
2690
- s.start("Finalizing deployment");
2665
+ reporter.success(`Deployment ${chalk.cyan(deployment.id)} created`);
2666
+ await uploadFiles(files, root, apiPath, deployment.id, headers, reporter);
2667
+ reporter.step("Finalizing deployment");
2691
2668
  const finalized = await requestJson(apiPath(`/${deployment.id}/finalize`), {
2692
2669
  body: JSON.stringify({ promote: true }),
2693
2670
  headers,
2694
2671
  method: "POST"
2695
2672
  }, "Failed to finalize deployment");
2696
- s.stop("Deployment finalized");
2697
- log.success(`Published ${chalk.cyan(finalized.id)}`);
2698
- if (finalized.manifestUrl) log.info(`Manifest: ${finalized.manifestUrl}`);
2699
- if (typeof finalized.fileCount === "number") log.info(`Files: ${finalized.fileCount}`);
2700
- log.info("Done");
2673
+ reporter.success("Deployment finalized");
2674
+ reporter.success(`Published ${chalk.cyan(finalized.id)}`);
2675
+ if (finalized.manifestUrl) reporter.info(`Manifest: ${finalized.manifestUrl}`);
2676
+ if (typeof finalized.fileCount === "number") reporter.info(`Files: ${finalized.fileCount}`);
2677
+ reporter.info("Done");
2678
+ reporter.json({
2679
+ deploymentId: finalized.id,
2680
+ fileCount: finalized.fileCount ?? files.length,
2681
+ manifestUrl: finalized.manifestUrl ?? null
2682
+ });
2701
2683
  } catch (error) {
2702
- s.stop("Failed");
2703
- reportCommandError("Push failed", error);
2684
+ reporter.stop("Failed");
2685
+ reportCommandError("Push failed", error, { json: options.json });
2704
2686
  }
2705
2687
  });
2706
2688
  };
@@ -2708,15 +2690,24 @@ const registerPushCommand = (program) => {
2708
2690
  //#region src/commands/validate.ts
2709
2691
  const CONFIG_FILE = "docs.json";
2710
2692
  const registerValidateCommand = (program) => {
2711
- program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").action(async (dir) => {
2712
- intro(chalk.bold("blodemd validate"));
2693
+ program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").option("--json", "output machine-readable JSON (implies non-interactive)").action(async (dir, options) => {
2694
+ const reporter = createReporter({ json: options.json });
2695
+ if (reporter.interactive) intro(chalk.bold("blodemd validate"));
2713
2696
  try {
2714
2697
  const { warnings } = await loadValidatedSiteConfig(await resolveDocsRoot(dir));
2715
- for (const warning of warnings) log.warn(warning);
2716
- log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);
2717
- log.info("Done");
2698
+ for (const warning of warnings) reporter.warn(warning);
2699
+ reporter.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);
2700
+ reporter.info("Done");
2701
+ reporter.json({
2702
+ valid: true,
2703
+ warnings
2704
+ });
2718
2705
  } catch (error) {
2719
- reportCommandError("Validation failed", error);
2706
+ if (options.json && error instanceof CliError && error.exitCode === EXIT_CODES.VALIDATION) reporter.json({
2707
+ errors: error.message.split("\n"),
2708
+ valid: false
2709
+ });
2710
+ reportCommandError("Validation failed", error, { json: options.json });
2720
2711
  }
2721
2712
  });
2722
2713
  };
@@ -2770,9 +2761,18 @@ registerAuthCommands(program);
2770
2761
  registerNewCommand(program);
2771
2762
  registerValidateCommand(program);
2772
2763
  registerPushCommand(program);
2764
+ registerProjectsCommand(program);
2773
2765
  registerDevCommand(program);
2774
2766
  registerAnalyticsCommand(program);
2775
- program.parse();
2767
+ program.addHelpText("after", "\nExample:\n $ blodemd push ./docs --project my-docs\n");
2768
+ try {
2769
+ await program.parseAsync();
2770
+ } catch (error) {
2771
+ const cliError = toCliError(error);
2772
+ console.error(cliError.message);
2773
+ if (cliError.hint) console.error(cliError.hint);
2774
+ process.exitCode = cliError.exitCode;
2775
+ }
2776
2776
  //#endregion
2777
2777
  export {};
2778
2778