emdash 0.0.0-b → 0.0.2

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 (661) hide show
  1. package/README.md +87 -43
  2. package/dist/adapters-BLMa4JGD.d.mts +106 -0
  3. package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
  4. package/dist/apply-Bjfq_b4-.mjs +1293 -0
  5. package/dist/apply-Bjfq_b4-.mjs.map +1 -0
  6. package/dist/astro/index.d.mts +51 -0
  7. package/dist/astro/index.d.mts.map +1 -0
  8. package/dist/astro/index.mjs +1336 -0
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/middleware/auth.d.mts +31 -0
  11. package/dist/astro/middleware/auth.d.mts.map +1 -0
  12. package/dist/astro/middleware/auth.mjs +654 -0
  13. package/dist/astro/middleware/auth.mjs.map +1 -0
  14. package/dist/astro/middleware/redirect.d.mts +22 -0
  15. package/dist/astro/middleware/redirect.d.mts.map +1 -0
  16. package/dist/astro/middleware/redirect.mjs +63 -0
  17. package/dist/astro/middleware/redirect.mjs.map +1 -0
  18. package/dist/astro/middleware/request-context.d.mts +18 -0
  19. package/dist/astro/middleware/request-context.d.mts.map +1 -0
  20. package/dist/astro/middleware/request-context.mjs +1310 -0
  21. package/dist/astro/middleware/request-context.mjs.map +1 -0
  22. package/dist/astro/middleware/setup.d.mts +20 -0
  23. package/dist/astro/middleware/setup.d.mts.map +1 -0
  24. package/dist/astro/middleware/setup.mjs +47 -0
  25. package/dist/astro/middleware/setup.mjs.map +1 -0
  26. package/dist/astro/middleware.d.mts +13 -0
  27. package/dist/astro/middleware.d.mts.map +1 -0
  28. package/dist/astro/middleware.mjs +1613 -0
  29. package/dist/astro/middleware.mjs.map +1 -0
  30. package/dist/astro/types.d.mts +250 -0
  31. package/dist/astro/types.d.mts.map +1 -0
  32. package/dist/astro/types.mjs +1 -0
  33. package/dist/base64-MBPo9ozB.mjs +59 -0
  34. package/dist/base64-MBPo9ozB.mjs.map +1 -0
  35. package/dist/byline-CL847F26.mjs +213 -0
  36. package/dist/byline-CL847F26.mjs.map +1 -0
  37. package/dist/bylines-C2a-2TGt.mjs +136 -0
  38. package/dist/bylines-C2a-2TGt.mjs.map +1 -0
  39. package/dist/chunk-ClPoSABd.mjs +21 -0
  40. package/dist/cli/index.d.mts +1 -0
  41. package/dist/cli/index.mjs +3909 -0
  42. package/dist/cli/index.mjs.map +1 -0
  43. package/dist/client/cf-access.d.mts +60 -0
  44. package/dist/client/cf-access.d.mts.map +1 -0
  45. package/dist/client/cf-access.mjs +179 -0
  46. package/dist/client/cf-access.mjs.map +1 -0
  47. package/dist/client/index.d.mts +398 -0
  48. package/dist/client/index.d.mts.map +1 -0
  49. package/dist/client/index.mjs +346 -0
  50. package/dist/client/index.mjs.map +1 -0
  51. package/dist/config-CKE8p9xM.mjs +55 -0
  52. package/dist/config-CKE8p9xM.mjs.map +1 -0
  53. package/dist/connection-B4zVnQIa.mjs +40 -0
  54. package/dist/connection-B4zVnQIa.mjs.map +1 -0
  55. package/dist/content-D6C2WsZC.mjs +824 -0
  56. package/dist/content-D6C2WsZC.mjs.map +1 -0
  57. package/dist/db/index.d.mts +4 -0
  58. package/dist/db/index.mjs +62 -0
  59. package/dist/db/index.mjs.map +1 -0
  60. package/dist/db/libsql.d.mts +11 -0
  61. package/dist/db/libsql.d.mts.map +1 -0
  62. package/dist/db/libsql.mjs +17 -0
  63. package/dist/db/libsql.mjs.map +1 -0
  64. package/dist/db/postgres.d.mts +11 -0
  65. package/dist/db/postgres.d.mts.map +1 -0
  66. package/dist/db/postgres.mjs +30 -0
  67. package/dist/db/postgres.mjs.map +1 -0
  68. package/dist/db/sqlite.d.mts +11 -0
  69. package/dist/db/sqlite.d.mts.map +1 -0
  70. package/dist/db/sqlite.mjs +16 -0
  71. package/dist/db/sqlite.mjs.map +1 -0
  72. package/dist/default-Cyi4aAxu.mjs +81 -0
  73. package/dist/default-Cyi4aAxu.mjs.map +1 -0
  74. package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
  75. package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
  76. package/dist/error-Cxz0tQeO.mjs +27 -0
  77. package/dist/error-Cxz0tQeO.mjs.map +1 -0
  78. package/dist/index-C1xF3OGh.d.mts +4527 -0
  79. package/dist/index-C1xF3OGh.d.mts.map +1 -0
  80. package/dist/index.d.mts +16 -0
  81. package/dist/index.mjs +30 -0
  82. package/dist/load-yOOlckBj.mjs +28 -0
  83. package/dist/load-yOOlckBj.mjs.map +1 -0
  84. package/dist/loader-fz8Q_3EO.mjs +447 -0
  85. package/dist/loader-fz8Q_3EO.mjs.map +1 -0
  86. package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
  87. package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
  88. package/dist/media/index.d.mts +26 -0
  89. package/dist/media/index.d.mts.map +1 -0
  90. package/dist/media/index.mjs +55 -0
  91. package/dist/media/index.mjs.map +1 -0
  92. package/dist/media/local-runtime.d.mts +39 -0
  93. package/dist/media/local-runtime.d.mts.map +1 -0
  94. package/dist/media/local-runtime.mjs +133 -0
  95. package/dist/media/local-runtime.mjs.map +1 -0
  96. package/dist/media-DqHVh136.mjs +200 -0
  97. package/dist/media-DqHVh136.mjs.map +1 -0
  98. package/dist/mode-C2EzN1uE.mjs +23 -0
  99. package/dist/mode-C2EzN1uE.mjs.map +1 -0
  100. package/dist/page/index.d.mts +140 -0
  101. package/dist/page/index.d.mts.map +1 -0
  102. package/dist/page/index.mjs +416 -0
  103. package/dist/page/index.mjs.map +1 -0
  104. package/dist/placeholder-CmGAmqeO.d.mts +276 -0
  105. package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
  106. package/dist/placeholder-SmpOx-_v.mjs +243 -0
  107. package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
  108. package/dist/plugin-utils.d.mts +58 -0
  109. package/dist/plugin-utils.d.mts.map +1 -0
  110. package/dist/plugin-utils.mjs +78 -0
  111. package/dist/plugin-utils.mjs.map +1 -0
  112. package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
  113. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
  114. package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
  115. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
  116. package/dist/query-CS_iSj34.mjs +460 -0
  117. package/dist/query-CS_iSj34.mjs.map +1 -0
  118. package/dist/redirect-DIfIni3r.mjs +329 -0
  119. package/dist/redirect-DIfIni3r.mjs.map +1 -0
  120. package/dist/registry-D_w5HW4G.mjs +863 -0
  121. package/dist/registry-D_w5HW4G.mjs.map +1 -0
  122. package/dist/request-context.d.mts +49 -0
  123. package/dist/request-context.d.mts.map +1 -0
  124. package/dist/request-context.mjs +43 -0
  125. package/dist/request-context.mjs.map +1 -0
  126. package/dist/runner-C0hCbYnD.mjs +1412 -0
  127. package/dist/runner-C0hCbYnD.mjs.map +1 -0
  128. package/dist/runner-EAtf0ZIe.d.mts +27 -0
  129. package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
  130. package/dist/runtime.d.mts +26 -0
  131. package/dist/runtime.d.mts.map +1 -0
  132. package/dist/runtime.mjs +42 -0
  133. package/dist/runtime.mjs.map +1 -0
  134. package/dist/search-DG603UrT.mjs +9211 -0
  135. package/dist/search-DG603UrT.mjs.map +1 -0
  136. package/dist/seed/index.d.mts +3 -0
  137. package/dist/seed/index.mjs +15 -0
  138. package/dist/seo/index.d.mts +70 -0
  139. package/dist/seo/index.d.mts.map +1 -0
  140. package/dist/seo/index.mjs +70 -0
  141. package/dist/seo/index.mjs.map +1 -0
  142. package/dist/storage/local.d.mts +39 -0
  143. package/dist/storage/local.d.mts.map +1 -0
  144. package/dist/storage/local.mjs +166 -0
  145. package/dist/storage/local.mjs.map +1 -0
  146. package/dist/storage/s3.d.mts +32 -0
  147. package/dist/storage/s3.d.mts.map +1 -0
  148. package/dist/storage/s3.mjs +175 -0
  149. package/dist/storage/s3.mjs.map +1 -0
  150. package/dist/tokens-DpgrkrXK.mjs +171 -0
  151. package/dist/tokens-DpgrkrXK.mjs.map +1 -0
  152. package/dist/transport-BFGblqwG.d.mts +42 -0
  153. package/dist/transport-BFGblqwG.d.mts.map +1 -0
  154. package/dist/transport-yxiQsi8I.mjs +418 -0
  155. package/dist/transport-yxiQsi8I.mjs.map +1 -0
  156. package/dist/types-BRuPJGdV.d.mts +102 -0
  157. package/dist/types-BRuPJGdV.d.mts.map +1 -0
  158. package/dist/types-C4-fAxN3.d.mts +182 -0
  159. package/dist/types-C4-fAxN3.d.mts.map +1 -0
  160. package/dist/types-CMMN0pNg.mjs +31 -0
  161. package/dist/types-CMMN0pNg.mjs.map +1 -0
  162. package/dist/types-CUBbjgmP.mjs +16 -0
  163. package/dist/types-CUBbjgmP.mjs.map +1 -0
  164. package/dist/types-DRjfYOEv.d.mts +426 -0
  165. package/dist/types-DRjfYOEv.d.mts.map +1 -0
  166. package/dist/types-DY5zk5HN.mjs +73 -0
  167. package/dist/types-DY5zk5HN.mjs.map +1 -0
  168. package/dist/types-DaNLHo_T.d.mts +184 -0
  169. package/dist/types-DaNLHo_T.d.mts.map +1 -0
  170. package/dist/types-DvhsUmSJ.d.mts +1111 -0
  171. package/dist/types-DvhsUmSJ.d.mts.map +1 -0
  172. package/dist/validate-CpBtVMsD.d.mts +378 -0
  173. package/dist/validate-CpBtVMsD.d.mts.map +1 -0
  174. package/dist/validate-CqRJb_xU.mjs +97 -0
  175. package/dist/validate-CqRJb_xU.mjs.map +1 -0
  176. package/dist/validate-O7PWmlnq.mjs +328 -0
  177. package/dist/validate-O7PWmlnq.mjs.map +1 -0
  178. package/locals.d.ts +46 -0
  179. package/package.json +233 -19
  180. package/src/api/authorize.ts +63 -0
  181. package/src/api/csrf.ts +48 -0
  182. package/src/api/error.ts +99 -0
  183. package/src/api/errors.ts +445 -0
  184. package/src/api/escape.ts +9 -0
  185. package/src/api/handlers/api-tokens.ts +240 -0
  186. package/src/api/handlers/comments.ts +314 -0
  187. package/src/api/handlers/content.ts +1315 -0
  188. package/src/api/handlers/dashboard.ts +205 -0
  189. package/src/api/handlers/device-flow.ts +684 -0
  190. package/src/api/handlers/index.ts +163 -0
  191. package/src/api/handlers/manifest.ts +158 -0
  192. package/src/api/handlers/marketplace.ts +930 -0
  193. package/src/api/handlers/media.ts +207 -0
  194. package/src/api/handlers/menus.ts +493 -0
  195. package/src/api/handlers/oauth-authorization.ts +429 -0
  196. package/src/api/handlers/oauth-clients.ts +349 -0
  197. package/src/api/handlers/oauth-user-lookup.ts +39 -0
  198. package/src/api/handlers/plugins.ts +254 -0
  199. package/src/api/handlers/redirects.ts +360 -0
  200. package/src/api/handlers/revision.ts +145 -0
  201. package/src/api/handlers/schema.ts +534 -0
  202. package/src/api/handlers/sections.ts +289 -0
  203. package/src/api/handlers/seo.ts +115 -0
  204. package/src/api/handlers/settings.ts +49 -0
  205. package/src/api/handlers/snapshot.ts +350 -0
  206. package/src/api/handlers/taxonomies.ts +523 -0
  207. package/src/api/index.ts +6 -0
  208. package/src/api/openapi/document.ts +2368 -0
  209. package/src/api/openapi/index.ts +1 -0
  210. package/src/api/parse.ts +139 -0
  211. package/src/api/redirect.ts +14 -0
  212. package/src/api/rev.ts +67 -0
  213. package/src/api/schemas/auth.ts +112 -0
  214. package/src/api/schemas/bylines.ts +85 -0
  215. package/src/api/schemas/comments.ts +117 -0
  216. package/src/api/schemas/common.ts +89 -0
  217. package/src/api/schemas/content.ts +191 -0
  218. package/src/api/schemas/import.ts +52 -0
  219. package/src/api/schemas/index.ts +17 -0
  220. package/src/api/schemas/media.ts +116 -0
  221. package/src/api/schemas/menus.ts +111 -0
  222. package/src/api/schemas/redirects.ts +155 -0
  223. package/src/api/schemas/schema.ts +203 -0
  224. package/src/api/schemas/search.ts +63 -0
  225. package/src/api/schemas/sections.ts +67 -0
  226. package/src/api/schemas/settings.ts +63 -0
  227. package/src/api/schemas/setup.ts +37 -0
  228. package/src/api/schemas/taxonomies.ts +113 -0
  229. package/src/api/schemas/users.ts +96 -0
  230. package/src/api/schemas/widgets.ts +80 -0
  231. package/src/api/site-url.ts +25 -0
  232. package/src/api/types.ts +82 -0
  233. package/src/astro/index.ts +27 -0
  234. package/src/astro/integration/index.ts +303 -0
  235. package/src/astro/integration/routes.ts +834 -0
  236. package/src/astro/integration/runtime.ts +338 -0
  237. package/src/astro/integration/virtual-modules.ts +469 -0
  238. package/src/astro/integration/vite-config.ts +335 -0
  239. package/src/astro/middleware/auth.ts +743 -0
  240. package/src/astro/middleware/redirect.ts +89 -0
  241. package/src/astro/middleware/request-context.ts +129 -0
  242. package/src/astro/middleware/setup.ts +89 -0
  243. package/src/astro/middleware.ts +398 -0
  244. package/src/astro/routes/PluginRegistry.tsx +15 -0
  245. package/src/astro/routes/admin.astro +81 -0
  246. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  247. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  248. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  249. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  250. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  251. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  252. package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
  253. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  254. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  255. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  256. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  257. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  258. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  259. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  260. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  261. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  262. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  263. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  264. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  265. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  266. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  267. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
  268. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  269. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
  270. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  271. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  272. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  273. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
  274. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  275. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  276. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  277. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  278. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  279. package/src/astro/routes/api/admin/users/index.ts +66 -0
  280. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  281. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  282. package/src/astro/routes/api/auth/invite/complete.ts +84 -0
  283. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  284. package/src/astro/routes/api/auth/logout.ts +40 -0
  285. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  286. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  287. package/src/astro/routes/api/auth/me.ts +60 -0
  288. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
  289. package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
  290. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  291. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  292. package/src/astro/routes/api/auth/passkey/options.ts +82 -0
  293. package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
  294. package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
  295. package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
  296. package/src/astro/routes/api/auth/signup/complete.ts +85 -0
  297. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  298. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  299. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
  300. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  301. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  302. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  303. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  304. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  305. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  306. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  307. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  308. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
  309. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  310. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  311. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  312. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  313. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  314. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  315. package/src/astro/routes/api/dashboard.ts +32 -0
  316. package/src/astro/routes/api/dev/emails.ts +36 -0
  317. package/src/astro/routes/api/import/probe.ts +47 -0
  318. package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
  319. package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
  320. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  321. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  322. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  323. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  324. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  325. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
  326. package/src/astro/routes/api/manifest.ts +62 -0
  327. package/src/astro/routes/api/mcp.ts +124 -0
  328. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  329. package/src/astro/routes/api/media/[id].ts +145 -0
  330. package/src/astro/routes/api/media/file/[key].ts +79 -0
  331. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  332. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  333. package/src/astro/routes/api/media/providers/index.ts +30 -0
  334. package/src/astro/routes/api/media/upload-url.ts +137 -0
  335. package/src/astro/routes/api/media.ts +190 -0
  336. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  337. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  338. package/src/astro/routes/api/menus/[name].ts +65 -0
  339. package/src/astro/routes/api/menus/index.ts +47 -0
  340. package/src/astro/routes/api/oauth/authorize.ts +412 -0
  341. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  342. package/src/astro/routes/api/oauth/device/code.ts +51 -0
  343. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  344. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  345. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  346. package/src/astro/routes/api/oauth/token.ts +184 -0
  347. package/src/astro/routes/api/openapi.json.ts +32 -0
  348. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  349. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  350. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  351. package/src/astro/routes/api/redirects/[id].ts +84 -0
  352. package/src/astro/routes/api/redirects/index.ts +52 -0
  353. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  354. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
  355. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  356. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  357. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  358. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  359. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  360. package/src/astro/routes/api/schema/index.ts +109 -0
  361. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  362. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  363. package/src/astro/routes/api/search/enable.ts +64 -0
  364. package/src/astro/routes/api/search/index.ts +55 -0
  365. package/src/astro/routes/api/search/rebuild.ts +72 -0
  366. package/src/astro/routes/api/search/stats.ts +35 -0
  367. package/src/astro/routes/api/search/suggest.ts +53 -0
  368. package/src/astro/routes/api/sections/[slug].ts +84 -0
  369. package/src/astro/routes/api/sections/index.ts +52 -0
  370. package/src/astro/routes/api/settings/email.ts +150 -0
  371. package/src/astro/routes/api/settings.ts +67 -0
  372. package/src/astro/routes/api/setup/admin-verify.ts +100 -0
  373. package/src/astro/routes/api/setup/admin.ts +94 -0
  374. package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
  375. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  376. package/src/astro/routes/api/setup/index.ts +126 -0
  377. package/src/astro/routes/api/setup/status.ts +122 -0
  378. package/src/astro/routes/api/snapshot.ts +75 -0
  379. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  380. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  381. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  382. package/src/astro/routes/api/themes/preview.ts +77 -0
  383. package/src/astro/routes/api/typegen.ts +114 -0
  384. package/src/astro/routes/api/well-known/auth.ts +68 -0
  385. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
  386. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
  387. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
  388. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  389. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  390. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  391. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  392. package/src/astro/routes/api/widget-components.ts +22 -0
  393. package/src/astro/routes/robots.txt.ts +77 -0
  394. package/src/astro/routes/sitemap.xml.ts +97 -0
  395. package/src/astro/storage/adapters.ts +74 -0
  396. package/src/astro/storage/index.ts +19 -0
  397. package/src/astro/storage/types.ts +60 -0
  398. package/src/astro/types.ts +346 -0
  399. package/src/auth/api-tokens.ts +25 -0
  400. package/src/auth/challenge-store.ts +80 -0
  401. package/src/auth/mode.ts +96 -0
  402. package/src/auth/oauth-state-store.ts +96 -0
  403. package/src/auth/passkey-config.ts +27 -0
  404. package/src/auth/rate-limit.ts +158 -0
  405. package/src/auth/scopes.ts +33 -0
  406. package/src/auth/types.ts +104 -0
  407. package/src/aws-sdk.d.ts +100 -0
  408. package/src/bylines/index.ts +237 -0
  409. package/src/cleanup.ts +153 -0
  410. package/src/cli/client-factory.ts +100 -0
  411. package/src/cli/commands/auth.ts +46 -0
  412. package/src/cli/commands/bundle-utils.ts +247 -0
  413. package/src/cli/commands/bundle.ts +609 -0
  414. package/src/cli/commands/content.ts +442 -0
  415. package/src/cli/commands/dev.ts +191 -0
  416. package/src/cli/commands/doctor.ts +211 -0
  417. package/src/cli/commands/export-seed.ts +630 -0
  418. package/src/cli/commands/import/wordpress.ts +1056 -0
  419. package/src/cli/commands/init.ts +192 -0
  420. package/src/cli/commands/login.ts +547 -0
  421. package/src/cli/commands/media.ts +165 -0
  422. package/src/cli/commands/menu.ts +67 -0
  423. package/src/cli/commands/plugin-init.ts +291 -0
  424. package/src/cli/commands/plugin-validate.ts +31 -0
  425. package/src/cli/commands/plugin.ts +33 -0
  426. package/src/cli/commands/publish.ts +697 -0
  427. package/src/cli/commands/schema.ts +233 -0
  428. package/src/cli/commands/search-cmd.ts +54 -0
  429. package/src/cli/commands/seed.ts +286 -0
  430. package/src/cli/commands/taxonomy.ts +128 -0
  431. package/src/cli/commands/types.ts +68 -0
  432. package/src/cli/credentials.ts +236 -0
  433. package/src/cli/index.ts +70 -0
  434. package/src/cli/output.ts +75 -0
  435. package/src/cli/wxr/parser.ts +969 -0
  436. package/src/client/cf-access.ts +193 -0
  437. package/src/client/index.ts +854 -0
  438. package/src/client/portable-text.ts +413 -0
  439. package/src/client/transport.ts +200 -0
  440. package/src/comments/moderator.ts +46 -0
  441. package/src/comments/notifications.ts +144 -0
  442. package/src/comments/query.ts +105 -0
  443. package/src/comments/service.ts +213 -0
  444. package/src/components/Break.astro +45 -0
  445. package/src/components/Button.astro +71 -0
  446. package/src/components/Buttons.astro +49 -0
  447. package/src/components/Code.astro +59 -0
  448. package/src/components/Columns.astro +59 -0
  449. package/src/components/CommentForm.astro +315 -0
  450. package/src/components/Comments.astro +232 -0
  451. package/src/components/Cover.astro +128 -0
  452. package/src/components/EmDashBodyEnd.astro +32 -0
  453. package/src/components/EmDashBodyStart.astro +32 -0
  454. package/src/components/EmDashHead.astro +53 -0
  455. package/src/components/EmDashImage.astro +178 -0
  456. package/src/components/EmDashMedia.astro +167 -0
  457. package/src/components/Embed.astro +128 -0
  458. package/src/components/File.astro +122 -0
  459. package/src/components/Gallery.astro +93 -0
  460. package/src/components/HtmlBlock.astro +33 -0
  461. package/src/components/Image.astro +178 -0
  462. package/src/components/InlineEditor.astro +27 -0
  463. package/src/components/InlinePortableTextEditor.tsx +1905 -0
  464. package/src/components/LiveSearch.astro +614 -0
  465. package/src/components/PortableText.astro +51 -0
  466. package/src/components/Pullquote.astro +51 -0
  467. package/src/components/Table.astro +108 -0
  468. package/src/components/WidgetArea.astro +22 -0
  469. package/src/components/WidgetRenderer.astro +72 -0
  470. package/src/components/index.ts +116 -0
  471. package/src/components/marks/Link.astro +31 -0
  472. package/src/components/marks/StrikeThrough.astro +7 -0
  473. package/src/components/marks/Subscript.astro +7 -0
  474. package/src/components/marks/Superscript.astro +7 -0
  475. package/src/components/marks/Underline.astro +7 -0
  476. package/src/components/widgets/Archives.astro +65 -0
  477. package/src/components/widgets/Categories.astro +35 -0
  478. package/src/components/widgets/RecentPosts.astro +51 -0
  479. package/src/components/widgets/Search.astro +18 -0
  480. package/src/components/widgets/Tags.astro +38 -0
  481. package/src/content/converters/index.ts +9 -0
  482. package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
  483. package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
  484. package/src/content/converters/types.ts +120 -0
  485. package/src/content/index.ts +5 -0
  486. package/src/database/connection.ts +67 -0
  487. package/src/database/dialect-helpers.ts +138 -0
  488. package/src/database/index.ts +5 -0
  489. package/src/database/migrations/001_initial.ts +170 -0
  490. package/src/database/migrations/002_media_status.ts +26 -0
  491. package/src/database/migrations/003_schema_registry.ts +79 -0
  492. package/src/database/migrations/004_plugins.ts +62 -0
  493. package/src/database/migrations/005_menus.ts +67 -0
  494. package/src/database/migrations/006_taxonomy_defs.ts +51 -0
  495. package/src/database/migrations/007_widgets.ts +42 -0
  496. package/src/database/migrations/008_auth.ts +194 -0
  497. package/src/database/migrations/009_user_disabled.ts +27 -0
  498. package/src/database/migrations/011_sections.ts +65 -0
  499. package/src/database/migrations/012_search.ts +25 -0
  500. package/src/database/migrations/013_scheduled_publishing.ts +51 -0
  501. package/src/database/migrations/014_draft_revisions.ts +72 -0
  502. package/src/database/migrations/015_indexes.ts +82 -0
  503. package/src/database/migrations/016_api_tokens.ts +89 -0
  504. package/src/database/migrations/017_authorization_codes.ts +45 -0
  505. package/src/database/migrations/018_seo.ts +56 -0
  506. package/src/database/migrations/019_i18n.ts +618 -0
  507. package/src/database/migrations/020_collection_url_pattern.ts +23 -0
  508. package/src/database/migrations/021_remove_section_categories.ts +43 -0
  509. package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
  510. package/src/database/migrations/023_plugin_metadata.ts +33 -0
  511. package/src/database/migrations/024_media_placeholders.ts +32 -0
  512. package/src/database/migrations/025_oauth_clients.ts +28 -0
  513. package/src/database/migrations/026_cron_tasks.ts +49 -0
  514. package/src/database/migrations/027_comments.ts +87 -0
  515. package/src/database/migrations/028_drop_author_url.ts +9 -0
  516. package/src/database/migrations/029_redirects.ts +67 -0
  517. package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
  518. package/src/database/migrations/031_bylines.ts +90 -0
  519. package/src/database/migrations/032_rate_limits.ts +39 -0
  520. package/src/database/migrations/runner.ts +170 -0
  521. package/src/database/repositories/audit.ts +294 -0
  522. package/src/database/repositories/byline.ts +387 -0
  523. package/src/database/repositories/comment.ts +458 -0
  524. package/src/database/repositories/content.ts +1144 -0
  525. package/src/database/repositories/index.ts +30 -0
  526. package/src/database/repositories/media.ts +347 -0
  527. package/src/database/repositories/options.ts +150 -0
  528. package/src/database/repositories/plugin-storage.ts +373 -0
  529. package/src/database/repositories/redirect.ts +480 -0
  530. package/src/database/repositories/revision.ts +200 -0
  531. package/src/database/repositories/seo.ts +176 -0
  532. package/src/database/repositories/taxonomy.ts +294 -0
  533. package/src/database/repositories/types.ts +132 -0
  534. package/src/database/repositories/user.ts +258 -0
  535. package/src/database/transaction.ts +54 -0
  536. package/src/database/types.ts +501 -0
  537. package/src/database/validate.ts +138 -0
  538. package/src/db/adapters.ts +125 -0
  539. package/src/db/index.ts +37 -0
  540. package/src/db/libsql.ts +23 -0
  541. package/src/db/postgres.ts +30 -0
  542. package/src/db/sqlite.ts +27 -0
  543. package/src/emdash-runtime.ts +2096 -0
  544. package/src/fields/boolean.ts +34 -0
  545. package/src/fields/datetime.ts +44 -0
  546. package/src/fields/file.ts +41 -0
  547. package/src/fields/image.ts +34 -0
  548. package/src/fields/index.ts +42 -0
  549. package/src/fields/integer.ts +50 -0
  550. package/src/fields/json.ts +37 -0
  551. package/src/fields/multiselect.ts +48 -0
  552. package/src/fields/number.ts +52 -0
  553. package/src/fields/portable-text.ts +33 -0
  554. package/src/fields/reference.ts +29 -0
  555. package/src/fields/richtext.ts +31 -0
  556. package/src/fields/select.ts +46 -0
  557. package/src/fields/slug.ts +38 -0
  558. package/src/fields/text.ts +55 -0
  559. package/src/fields/textarea.ts +52 -0
  560. package/src/fields/types.ts +64 -0
  561. package/src/i18n/config.ts +68 -0
  562. package/src/import/index.ts +90 -0
  563. package/src/import/menus.ts +436 -0
  564. package/src/import/registry.ts +111 -0
  565. package/src/import/sections.ts +103 -0
  566. package/src/import/settings.ts +281 -0
  567. package/src/import/sources/wordpress-plugin.ts +641 -0
  568. package/src/import/sources/wordpress-rest.ts +191 -0
  569. package/src/import/sources/wxr.ts +330 -0
  570. package/src/import/ssrf.ts +260 -0
  571. package/src/import/types.ts +418 -0
  572. package/src/import/utils.ts +412 -0
  573. package/src/index.ts +481 -0
  574. package/src/loader.ts +770 -0
  575. package/src/mcp/server.ts +1463 -0
  576. package/src/media/index.ts +32 -0
  577. package/src/media/local-runtime.ts +213 -0
  578. package/src/media/local.ts +46 -0
  579. package/src/media/normalize.ts +190 -0
  580. package/src/media/placeholder.ts +150 -0
  581. package/src/media/provider-loader.ts +78 -0
  582. package/src/media/types.ts +279 -0
  583. package/src/menus/index.ts +324 -0
  584. package/src/menus/types.ts +112 -0
  585. package/src/page/context.ts +93 -0
  586. package/src/page/fragments.ts +89 -0
  587. package/src/page/index.ts +58 -0
  588. package/src/page/jsonld.ts +94 -0
  589. package/src/page/metadata.ts +185 -0
  590. package/src/page/seo-contributions.ts +136 -0
  591. package/src/plugin-utils.ts +80 -0
  592. package/src/plugins/adapt-sandbox-entry.ts +207 -0
  593. package/src/plugins/context.ts +833 -0
  594. package/src/plugins/cron.ts +361 -0
  595. package/src/plugins/define-plugin.ts +259 -0
  596. package/src/plugins/email-console.ts +73 -0
  597. package/src/plugins/email.ts +209 -0
  598. package/src/plugins/hooks.ts +1273 -0
  599. package/src/plugins/index.ts +193 -0
  600. package/src/plugins/manager.ts +595 -0
  601. package/src/plugins/manifest-schema.ts +230 -0
  602. package/src/plugins/marketplace.ts +460 -0
  603. package/src/plugins/request-meta.ts +139 -0
  604. package/src/plugins/routes.ts +302 -0
  605. package/src/plugins/sandbox/index.ts +18 -0
  606. package/src/plugins/sandbox/noop.ts +76 -0
  607. package/src/plugins/sandbox/types.ts +173 -0
  608. package/src/plugins/scheduler/node.ts +122 -0
  609. package/src/plugins/scheduler/piggyback.ts +71 -0
  610. package/src/plugins/scheduler/types.ts +27 -0
  611. package/src/plugins/state.ts +208 -0
  612. package/src/plugins/storage-indexes.ts +326 -0
  613. package/src/plugins/storage-query.ts +240 -0
  614. package/src/plugins/types.ts +1284 -0
  615. package/src/preview/helpers.ts +27 -0
  616. package/src/preview/index.ts +40 -0
  617. package/src/preview/tokens.ts +279 -0
  618. package/src/preview/urls.ts +118 -0
  619. package/src/query.ts +674 -0
  620. package/src/redirects/patterns.ts +224 -0
  621. package/src/request-context.ts +67 -0
  622. package/src/runtime.ts +21 -0
  623. package/src/schema/index.ts +29 -0
  624. package/src/schema/query.ts +44 -0
  625. package/src/schema/registry.ts +965 -0
  626. package/src/schema/types.ts +276 -0
  627. package/src/schema/zod-generator.ts +413 -0
  628. package/src/search/fts-manager.ts +452 -0
  629. package/src/search/index.ts +26 -0
  630. package/src/search/query.ts +396 -0
  631. package/src/search/text-extraction.ts +162 -0
  632. package/src/search/types.ts +114 -0
  633. package/src/sections/index.ts +226 -0
  634. package/src/sections/types.ts +86 -0
  635. package/src/seed/apply.ts +1141 -0
  636. package/src/seed/default.ts +86 -0
  637. package/src/seed/index.ts +28 -0
  638. package/src/seed/load.ts +35 -0
  639. package/src/seed/types.ts +341 -0
  640. package/src/seed/validate.ts +642 -0
  641. package/src/seo/index.ts +179 -0
  642. package/src/settings/index.ts +203 -0
  643. package/src/settings/types.ts +58 -0
  644. package/src/storage/index.ts +28 -0
  645. package/src/storage/local.ts +249 -0
  646. package/src/storage/s3.ts +263 -0
  647. package/src/storage/types.ts +204 -0
  648. package/src/taxonomies/index.ts +309 -0
  649. package/src/taxonomies/types.ts +61 -0
  650. package/src/ui.ts +75 -0
  651. package/src/utils/base64.ts +73 -0
  652. package/src/utils/hash.ts +36 -0
  653. package/src/utils/sanitize.ts +20 -0
  654. package/src/utils/slugify.ts +29 -0
  655. package/src/utils/url.ts +48 -0
  656. package/src/virtual-modules.d.ts +111 -0
  657. package/src/visual-editing/editable.ts +108 -0
  658. package/src/visual-editing/toolbar.ts +1229 -0
  659. package/src/widgets/components.ts +105 -0
  660. package/src/widgets/index.ts +131 -0
  661. package/src/widgets/types.ts +81 -0
@@ -0,0 +1,3909 @@
1
+ #!/usr/bin/env node
2
+ import { t as __exportAll } from "../chunk-ClPoSABd.mjs";
3
+ import { n as createDatabase } from "../connection-B4zVnQIa.mjs";
4
+ import { s as listTablesLike } from "../dialect-helpers-B9uSp2GJ.mjs";
5
+ import { r as runMigrations, t as getMigrationStatus } from "../runner-C0hCbYnD.mjs";
6
+ import { t as ContentRepository } from "../content-D6C2WsZC.mjs";
7
+ import { i as encodeBase64url } from "../base64-MBPo9ozB.mjs";
8
+ import "../types-CMMN0pNg.mjs";
9
+ import { t as MediaRepository } from "../media-DqHVh136.mjs";
10
+ import { d as TaxonomyRepository, t as applySeed, u as OptionsRepository } from "../apply-Bjfq_b4-.mjs";
11
+ import { n as SchemaRegistry } from "../registry-D_w5HW4G.mjs";
12
+ import "../redirect-DIfIni3r.mjs";
13
+ import "../byline-CL847F26.mjs";
14
+ import { i as isI18nEnabled } from "../config-CKE8p9xM.mjs";
15
+ import "../loader-fz8Q_3EO.mjs";
16
+ import { i as pluginManifestSchema } from "../manifest-schema-Dcl0R6nM.mjs";
17
+ import { t as validateSeed } from "../validate-O7PWmlnq.mjs";
18
+ import { LocalStorage } from "../storage/local.mjs";
19
+ import { createHeaderAwareFetch, customHeadersInterceptor, getCachedAccessToken, isAccessRedirect, resolveCustomHeaders, runCloudflaredLogin } from "../client/cf-access.mjs";
20
+ import { EmDashClient } from "../client/index.mjs";
21
+ import { createGzipDecoder, unpackTar } from "modern-tar";
22
+ import { imageSize } from "image-size";
23
+ import { basename, dirname, extname, join, resolve } from "node:path";
24
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
26
+ import { defineCommand, runCommand, runMain } from "citty";
27
+ import consola, { consola as consola$1 } from "consola";
28
+ import pc from "picocolors";
29
+ import { spawn } from "node:child_process";
30
+ import { homedir } from "node:os";
31
+ import { createHash } from "node:crypto";
32
+ import { pipeline } from "node:stream/promises";
33
+ import { packTar } from "modern-tar/fs";
34
+
35
+ //#region src/cli/commands/auth.ts
36
+ /**
37
+ * Auth CLI commands
38
+ */
39
+ /**
40
+ * Generate a cryptographically secure auth secret
41
+ */
42
+ function generateAuthSecret() {
43
+ const bytes = new Uint8Array(32);
44
+ crypto.getRandomValues(bytes);
45
+ return encodeBase64url(bytes);
46
+ }
47
+ const secretCommand = defineCommand({
48
+ meta: {
49
+ name: "secret",
50
+ description: "Generate a secure auth secret"
51
+ },
52
+ run() {
53
+ const secret = generateAuthSecret();
54
+ consola$1.log("");
55
+ consola$1.log(pc.bold("Generated auth secret:"));
56
+ consola$1.log("");
57
+ consola$1.log(` ${pc.cyan("EMDASH_AUTH_SECRET")}=${pc.green(secret)}`);
58
+ consola$1.log("");
59
+ consola$1.log(pc.dim("Add this to your environment variables."));
60
+ consola$1.log("");
61
+ }
62
+ });
63
+ const authCommand = defineCommand({
64
+ meta: {
65
+ name: "auth",
66
+ description: "Authentication utilities"
67
+ },
68
+ subCommands: { secret: secretCommand }
69
+ });
70
+
71
+ //#endregion
72
+ //#region src/cli/credentials.ts
73
+ /**
74
+ * Credential storage for CLI auth tokens.
75
+ *
76
+ * Stores OAuth tokens in ~/.config/emdash/auth.json.
77
+ * Remote URLs are keyed by origin, local dev by project path.
78
+ */
79
+ function getConfigDir() {
80
+ const xdg = process.env["XDG_CONFIG_HOME"];
81
+ if (xdg) return join(xdg, "emdash");
82
+ return join(homedir(), ".config", "emdash");
83
+ }
84
+ function getCredentialPath() {
85
+ return join(getConfigDir(), "auth.json");
86
+ }
87
+ /**
88
+ * Resolve the credential key for a given URL.
89
+ *
90
+ * Remote URLs are keyed by origin (e.g. "https://my-site.pages.dev").
91
+ * Local dev instances are keyed by project path (e.g. "path:/Users/matt/sites/blog").
92
+ */
93
+ function resolveCredentialKey(baseUrl) {
94
+ try {
95
+ const url = new URL(baseUrl);
96
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]") {
97
+ const projectPath = findProjectRoot(process.cwd());
98
+ if (projectPath) return `path:${projectPath}`;
99
+ return url.origin;
100
+ }
101
+ return url.origin;
102
+ } catch {
103
+ return baseUrl;
104
+ }
105
+ }
106
+ /**
107
+ * Walk up from cwd to find the project root (directory containing astro.config.*).
108
+ */
109
+ function findProjectRoot(from) {
110
+ let dir = resolve(from);
111
+ const root = resolve("/");
112
+ while (dir !== root) {
113
+ for (const name of [
114
+ "astro.config.ts",
115
+ "astro.config.mts",
116
+ "astro.config.js",
117
+ "astro.config.mjs"
118
+ ]) if (existsSync(join(dir, name))) return dir;
119
+ const parent = resolve(dir, "..");
120
+ if (parent === dir) break;
121
+ dir = parent;
122
+ }
123
+ return null;
124
+ }
125
+ function readStore() {
126
+ const path = getCredentialPath();
127
+ try {
128
+ if (existsSync(path)) {
129
+ const content = readFileSync(path, "utf-8");
130
+ return JSON.parse(content);
131
+ }
132
+ } catch {}
133
+ return {};
134
+ }
135
+ function writeStore(store) {
136
+ mkdirSync(getConfigDir(), { recursive: true });
137
+ writeFileSync(getCredentialPath(), JSON.stringify(store, null, " "), {
138
+ encoding: "utf-8",
139
+ mode: 384
140
+ });
141
+ }
142
+ /**
143
+ * Get stored credentials for a URL.
144
+ */
145
+ function getCredentials(baseUrl) {
146
+ const key = resolveCredentialKey(baseUrl);
147
+ const cred = readStore()[key];
148
+ if (!cred || !("accessToken" in cred)) return null;
149
+ return cred;
150
+ }
151
+ /**
152
+ * Save credentials for a URL.
153
+ */
154
+ function saveCredentials(baseUrl, cred) {
155
+ const key = resolveCredentialKey(baseUrl);
156
+ const store = readStore();
157
+ store[key] = cred;
158
+ writeStore(store);
159
+ }
160
+ /**
161
+ * Remove credentials for a URL.
162
+ */
163
+ function removeCredentials(baseUrl) {
164
+ const key = resolveCredentialKey(baseUrl);
165
+ const store = readStore();
166
+ if (key in store) {
167
+ delete store[key];
168
+ writeStore(store);
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+ function marketplaceKey(registryUrl) {
174
+ try {
175
+ return `marketplace:${new URL(registryUrl).origin}`;
176
+ } catch {
177
+ return `marketplace:${registryUrl}`;
178
+ }
179
+ }
180
+ /**
181
+ * Get stored marketplace credential for a registry URL.
182
+ */
183
+ function getMarketplaceCredential(registryUrl) {
184
+ const key = marketplaceKey(registryUrl);
185
+ const cred = readStore()[key];
186
+ if (!cred || !("token" in cred)) return null;
187
+ if (new Date(cred.expiresAt) < /* @__PURE__ */ new Date()) return null;
188
+ return cred;
189
+ }
190
+ /**
191
+ * Save marketplace credential for a registry URL.
192
+ */
193
+ function saveMarketplaceCredential(registryUrl, cred) {
194
+ const key = marketplaceKey(registryUrl);
195
+ const store = readStore();
196
+ store[key] = cred;
197
+ writeStore(store);
198
+ }
199
+ /**
200
+ * Remove marketplace credential for a registry URL.
201
+ */
202
+ function removeMarketplaceCredential(registryUrl) {
203
+ const key = marketplaceKey(registryUrl);
204
+ const store = readStore();
205
+ if (key in store) {
206
+ delete store[key];
207
+ writeStore(store);
208
+ return true;
209
+ }
210
+ return false;
211
+ }
212
+
213
+ //#endregion
214
+ //#region src/cli/client-factory.ts
215
+ var client_factory_exports = /* @__PURE__ */ __exportAll({
216
+ connectionArgs: () => connectionArgs,
217
+ createClientFromArgs: () => createClientFromArgs
218
+ });
219
+ /**
220
+ * Shared connection args for all CLI commands that talk to an EmDash instance.
221
+ * Spread into each command's `args` definition.
222
+ */
223
+ const connectionArgs = {
224
+ url: {
225
+ type: "string",
226
+ alias: "u",
227
+ description: "EmDash instance URL",
228
+ default: "http://localhost:4321"
229
+ },
230
+ token: {
231
+ type: "string",
232
+ alias: "t",
233
+ description: "Auth token"
234
+ },
235
+ header: {
236
+ type: "string",
237
+ alias: "H",
238
+ description: "Custom header \"Name: Value\" (repeatable, or use EMDASH_HEADERS env)"
239
+ },
240
+ json: {
241
+ type: "boolean",
242
+ description: "Output as JSON"
243
+ }
244
+ };
245
+ /**
246
+ * Create an EmDashClient from CLI args, env vars, and stored credentials.
247
+ *
248
+ * Auth resolution order:
249
+ * 1. --token flag
250
+ * 2. EMDASH_TOKEN env var
251
+ * 3. Stored credentials (~/.config/emdash/auth.json)
252
+ * 4. Dev bypass (if URL is localhost)
253
+ *
254
+ * Custom headers are merged from (in priority order):
255
+ * 1. Stored credentials (persisted during `emdash login --header`)
256
+ * 2. EMDASH_HEADERS env var
257
+ * 3. --header CLI flags
258
+ */
259
+ function createClientFromArgs(args) {
260
+ const baseUrl = args.url || process.env["EMDASH_URL"] || "http://localhost:4321";
261
+ let token = args.token || process.env["EMDASH_TOKEN"];
262
+ const isLocal = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
263
+ const cred = !token ? getCredentials(baseUrl) : null;
264
+ const customHeaders = {
265
+ ...cred?.customHeaders,
266
+ ...resolveCustomHeaders()
267
+ };
268
+ const extraInterceptors = [];
269
+ if (Object.keys(customHeaders).length > 0) extraInterceptors.push(customHeadersInterceptor(customHeaders));
270
+ if (!token && cred) if (new Date(cred.expiresAt) > /* @__PURE__ */ new Date()) token = cred.accessToken;
271
+ else return new EmDashClient({
272
+ baseUrl,
273
+ token: cred.accessToken,
274
+ refreshToken: cred.refreshToken,
275
+ onTokenRefresh: (newAccessToken, expiresIn) => {
276
+ saveCredentials(baseUrl, {
277
+ ...cred,
278
+ accessToken: newAccessToken,
279
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
280
+ });
281
+ },
282
+ interceptors: extraInterceptors
283
+ });
284
+ return new EmDashClient({
285
+ baseUrl,
286
+ token,
287
+ devBypass: !token && isLocal,
288
+ interceptors: extraInterceptors
289
+ });
290
+ }
291
+
292
+ //#endregion
293
+ //#region src/cli/output.ts
294
+ /**
295
+ * Output data as JSON or pretty-printed.
296
+ *
297
+ * If stdout is not a TTY or --json is set, outputs JSON.
298
+ * Otherwise, outputs a formatted representation.
299
+ */
300
+ function output(data, args) {
301
+ if (args.json || !process.stdout.isTTY) process.stdout.write(JSON.stringify(data, null, 2) + "\n");
302
+ else prettyPrint(data);
303
+ }
304
+ function prettyPrint(data, indent = 0) {
305
+ if (data === null || data === void 0) {
306
+ consola$1.log("(empty)");
307
+ return;
308
+ }
309
+ if (Array.isArray(data)) {
310
+ if (data.length === 0) {
311
+ consola$1.log("(no items)");
312
+ return;
313
+ }
314
+ for (const item of data) {
315
+ prettyPrint(item, indent);
316
+ if (indent === 0) consola$1.log("---");
317
+ }
318
+ return;
319
+ }
320
+ if (typeof data === "object") {
321
+ const obj = Object(data);
322
+ if ("items" in obj && Array.isArray(obj.items)) {
323
+ prettyPrint(obj.items, indent);
324
+ if (typeof obj.nextCursor === "string") consola$1.log(`\nNext cursor: ${obj.nextCursor}`);
325
+ return;
326
+ }
327
+ const prefix = " ".repeat(indent);
328
+ for (const [key, value] of Object.entries(obj)) {
329
+ if (value === null || value === void 0) continue;
330
+ if (typeof value === "object" && !Array.isArray(value)) {
331
+ consola$1.log(`${prefix}${key}:`);
332
+ prettyPrint(value, indent + 1);
333
+ } else if (Array.isArray(value)) consola$1.log(`${prefix}${key}: [${value.length} items]`);
334
+ else {
335
+ const str = typeof value === "string" ? value : JSON.stringify(value);
336
+ const display = str.length > 80 ? str.slice(0, 77) + "..." : str;
337
+ consola$1.log(`${prefix}${key}: ${display}`);
338
+ }
339
+ }
340
+ return;
341
+ }
342
+ consola$1.log(typeof data === "string" ? data : JSON.stringify(data));
343
+ }
344
+
345
+ //#endregion
346
+ //#region src/cli/commands/content.ts
347
+ /**
348
+ * emdash content
349
+ *
350
+ * CRUD commands for managing content items via the EmDash REST API.
351
+ */
352
+ /** Read content data from --data, --file, or --stdin */
353
+ async function readInputData(args) {
354
+ if (args.data) try {
355
+ return JSON.parse(args.data);
356
+ } catch {
357
+ throw new Error("Invalid JSON in --data argument");
358
+ }
359
+ if (args.file) try {
360
+ const content = await readFile(args.file, "utf-8");
361
+ return JSON.parse(content);
362
+ } catch (error) {
363
+ if (error instanceof SyntaxError) throw new Error(`Invalid JSON in file: ${args.file}`, { cause: error });
364
+ throw error;
365
+ }
366
+ if (args.stdin) {
367
+ const chunks = [];
368
+ for await (const chunk of process.stdin) chunks.push(chunk);
369
+ const content = Buffer.concat(chunks).toString("utf-8");
370
+ try {
371
+ return JSON.parse(content);
372
+ } catch {
373
+ throw new Error("Invalid JSON from stdin");
374
+ }
375
+ }
376
+ throw new Error("Provide content data via --data, --file, or --stdin");
377
+ }
378
+ const listCommand$4 = defineCommand({
379
+ meta: {
380
+ name: "list",
381
+ description: "List content items"
382
+ },
383
+ args: {
384
+ collection: {
385
+ type: "positional",
386
+ description: "Collection slug",
387
+ required: true
388
+ },
389
+ status: {
390
+ type: "string",
391
+ description: "Filter by status"
392
+ },
393
+ locale: {
394
+ type: "string",
395
+ description: "Filter by locale"
396
+ },
397
+ limit: {
398
+ type: "string",
399
+ description: "Maximum items to return"
400
+ },
401
+ cursor: {
402
+ type: "string",
403
+ description: "Pagination cursor"
404
+ },
405
+ ...connectionArgs
406
+ },
407
+ async run({ args }) {
408
+ try {
409
+ const result = await createClientFromArgs(args).list(args.collection, {
410
+ status: args.status,
411
+ locale: args.locale,
412
+ limit: args.limit ? parseInt(args.limit, 10) : void 0,
413
+ cursor: args.cursor
414
+ });
415
+ output({
416
+ items: result.items.map((item) => ({
417
+ id: item.id,
418
+ slug: item.slug,
419
+ locale: item.locale,
420
+ status: item.status,
421
+ title: typeof item.data?.title === "string" ? item.data.title : void 0,
422
+ updatedAt: item.updatedAt
423
+ })),
424
+ nextCursor: result.nextCursor
425
+ }, args);
426
+ } catch (error) {
427
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
428
+ process.exit(1);
429
+ }
430
+ }
431
+ });
432
+ const getCommand$3 = defineCommand({
433
+ meta: {
434
+ name: "get",
435
+ description: "Get a single content item"
436
+ },
437
+ args: {
438
+ collection: {
439
+ type: "positional",
440
+ description: "Collection slug",
441
+ required: true
442
+ },
443
+ id: {
444
+ type: "positional",
445
+ description: "Content item ID or slug",
446
+ required: true
447
+ },
448
+ locale: {
449
+ type: "string",
450
+ description: "Locale for slug resolution"
451
+ },
452
+ raw: {
453
+ type: "boolean",
454
+ description: "Return raw Portable Text (skip markdown conversion)"
455
+ },
456
+ published: {
457
+ type: "boolean",
458
+ description: "Return published data only (ignore pending draft)"
459
+ },
460
+ ...connectionArgs
461
+ },
462
+ async run({ args }) {
463
+ try {
464
+ const client = createClientFromArgs(args);
465
+ const item = await client.get(args.collection, args.id, {
466
+ raw: args.raw,
467
+ locale: args.locale
468
+ });
469
+ if (!args.published && item.draftRevisionId) {
470
+ const comparison = await client.compare(args.collection, args.id);
471
+ if (comparison.hasChanges && comparison.draft) item.data = comparison.draft;
472
+ }
473
+ output(item, args);
474
+ } catch (error) {
475
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
476
+ process.exit(1);
477
+ }
478
+ }
479
+ });
480
+ const createCommand$1 = defineCommand({
481
+ meta: {
482
+ name: "create",
483
+ description: "Create a content item"
484
+ },
485
+ args: {
486
+ collection: {
487
+ type: "positional",
488
+ description: "Collection slug",
489
+ required: true
490
+ },
491
+ data: {
492
+ type: "string",
493
+ description: "Content data as JSON string"
494
+ },
495
+ file: {
496
+ type: "string",
497
+ description: "Read content data from a JSON file"
498
+ },
499
+ stdin: {
500
+ type: "boolean",
501
+ description: "Read content data from stdin"
502
+ },
503
+ slug: {
504
+ type: "string",
505
+ description: "Content slug"
506
+ },
507
+ locale: {
508
+ type: "string",
509
+ description: "Content locale"
510
+ },
511
+ "translation-of": {
512
+ type: "string",
513
+ description: "ID of content item to link as translation"
514
+ },
515
+ draft: {
516
+ type: "boolean",
517
+ description: "Keep as draft instead of auto-publishing"
518
+ },
519
+ ...connectionArgs
520
+ },
521
+ async run({ args }) {
522
+ try {
523
+ const data = await readInputData(args);
524
+ const client = createClientFromArgs(args);
525
+ const item = await client.create(args.collection, {
526
+ data,
527
+ slug: args.slug,
528
+ locale: args.locale,
529
+ translationOf: args["translation-of"]
530
+ });
531
+ if (!args.draft) await client.publish(args.collection, item.id);
532
+ output(await client.get(args.collection, item.id), args);
533
+ } catch (error) {
534
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
535
+ process.exit(1);
536
+ }
537
+ }
538
+ });
539
+ const updateCommand = defineCommand({
540
+ meta: {
541
+ name: "update",
542
+ description: "Update a content item"
543
+ },
544
+ args: {
545
+ collection: {
546
+ type: "positional",
547
+ description: "Collection slug",
548
+ required: true
549
+ },
550
+ id: {
551
+ type: "positional",
552
+ description: "Content item ID or slug",
553
+ required: true
554
+ },
555
+ data: {
556
+ type: "string",
557
+ description: "Content data as JSON string"
558
+ },
559
+ file: {
560
+ type: "string",
561
+ description: "Read content data from a JSON file"
562
+ },
563
+ rev: {
564
+ type: "string",
565
+ description: "Revision token from get (prevents overwriting unseen changes)",
566
+ required: true
567
+ },
568
+ draft: {
569
+ type: "boolean",
570
+ description: "Keep as draft instead of auto-publishing"
571
+ },
572
+ ...connectionArgs
573
+ },
574
+ async run({ args }) {
575
+ try {
576
+ const data = await readInputData(args);
577
+ const client = createClientFromArgs(args);
578
+ const updated = await client.update(args.collection, args.id, {
579
+ data,
580
+ _rev: args.rev
581
+ });
582
+ if (!args.draft && updated.draftRevisionId) await client.publish(args.collection, args.id);
583
+ output(await client.get(args.collection, args.id), args);
584
+ } catch (error) {
585
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
586
+ process.exit(1);
587
+ }
588
+ }
589
+ });
590
+ const deleteCommand$2 = defineCommand({
591
+ meta: {
592
+ name: "delete",
593
+ description: "Delete a content item"
594
+ },
595
+ args: {
596
+ collection: {
597
+ type: "positional",
598
+ description: "Collection slug",
599
+ required: true
600
+ },
601
+ id: {
602
+ type: "positional",
603
+ description: "Content item ID or slug",
604
+ required: true
605
+ },
606
+ ...connectionArgs
607
+ },
608
+ async run({ args }) {
609
+ try {
610
+ await createClientFromArgs(args).delete(args.collection, args.id);
611
+ consola$1.success(`Deleted ${args.collection}/${args.id}`);
612
+ } catch (error) {
613
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
614
+ process.exit(1);
615
+ }
616
+ }
617
+ });
618
+ const publishCommand$1 = defineCommand({
619
+ meta: {
620
+ name: "publish",
621
+ description: "Publish a content item"
622
+ },
623
+ args: {
624
+ collection: {
625
+ type: "positional",
626
+ description: "Collection slug",
627
+ required: true
628
+ },
629
+ id: {
630
+ type: "positional",
631
+ description: "Content item ID or slug",
632
+ required: true
633
+ },
634
+ ...connectionArgs
635
+ },
636
+ async run({ args }) {
637
+ try {
638
+ await createClientFromArgs(args).publish(args.collection, args.id);
639
+ consola$1.success(`Published ${args.collection}/${args.id}`);
640
+ } catch (error) {
641
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
642
+ process.exit(1);
643
+ }
644
+ }
645
+ });
646
+ const unpublishCommand = defineCommand({
647
+ meta: {
648
+ name: "unpublish",
649
+ description: "Unpublish a content item"
650
+ },
651
+ args: {
652
+ collection: {
653
+ type: "positional",
654
+ description: "Collection slug",
655
+ required: true
656
+ },
657
+ id: {
658
+ type: "positional",
659
+ description: "Content item ID or slug",
660
+ required: true
661
+ },
662
+ ...connectionArgs
663
+ },
664
+ async run({ args }) {
665
+ try {
666
+ await createClientFromArgs(args).unpublish(args.collection, args.id);
667
+ consola$1.success(`Unpublished ${args.collection}/${args.id}`);
668
+ } catch (error) {
669
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
670
+ process.exit(1);
671
+ }
672
+ }
673
+ });
674
+ const scheduleCommand = defineCommand({
675
+ meta: {
676
+ name: "schedule",
677
+ description: "Schedule content for publishing"
678
+ },
679
+ args: {
680
+ collection: {
681
+ type: "positional",
682
+ description: "Collection slug",
683
+ required: true
684
+ },
685
+ id: {
686
+ type: "positional",
687
+ description: "Content item ID or slug",
688
+ required: true
689
+ },
690
+ at: {
691
+ type: "string",
692
+ description: "ISO 8601 datetime to publish at",
693
+ required: true
694
+ },
695
+ ...connectionArgs
696
+ },
697
+ async run({ args }) {
698
+ try {
699
+ await createClientFromArgs(args).schedule(args.collection, args.id, { at: args.at });
700
+ consola$1.success(`Scheduled ${args.collection}/${args.id} for ${args.at}`);
701
+ } catch (error) {
702
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
703
+ process.exit(1);
704
+ }
705
+ }
706
+ });
707
+ const restoreCommand = defineCommand({
708
+ meta: {
709
+ name: "restore",
710
+ description: "Restore a trashed content item"
711
+ },
712
+ args: {
713
+ collection: {
714
+ type: "positional",
715
+ description: "Collection slug",
716
+ required: true
717
+ },
718
+ id: {
719
+ type: "positional",
720
+ description: "Content item ID or slug",
721
+ required: true
722
+ },
723
+ ...connectionArgs
724
+ },
725
+ async run({ args }) {
726
+ try {
727
+ await createClientFromArgs(args).restore(args.collection, args.id);
728
+ consola$1.success(`Restored ${args.collection}/${args.id}`);
729
+ } catch (error) {
730
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
731
+ process.exit(1);
732
+ }
733
+ }
734
+ });
735
+ const translationsCommand = defineCommand({
736
+ meta: {
737
+ name: "translations",
738
+ description: "List translations for a content item"
739
+ },
740
+ args: {
741
+ collection: {
742
+ type: "positional",
743
+ description: "Collection slug",
744
+ required: true
745
+ },
746
+ id: {
747
+ type: "positional",
748
+ description: "Content item ID or slug",
749
+ required: true
750
+ },
751
+ ...connectionArgs
752
+ },
753
+ async run({ args }) {
754
+ try {
755
+ output(await createClientFromArgs(args).translations(args.collection, args.id), args);
756
+ } catch (error) {
757
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
758
+ process.exit(1);
759
+ }
760
+ }
761
+ });
762
+ const contentCommand = defineCommand({
763
+ meta: {
764
+ name: "content",
765
+ description: "Manage content"
766
+ },
767
+ subCommands: {
768
+ list: listCommand$4,
769
+ get: getCommand$3,
770
+ create: createCommand$1,
771
+ update: updateCommand,
772
+ delete: deleteCommand$2,
773
+ publish: publishCommand$1,
774
+ unpublish: unpublishCommand,
775
+ schedule: scheduleCommand,
776
+ restore: restoreCommand,
777
+ translations: translationsCommand
778
+ }
779
+ });
780
+
781
+ //#endregion
782
+ //#region src/cli/commands/dev.ts
783
+ /**
784
+ * emdash dev
785
+ *
786
+ * Start development server with optional schema sync from remote
787
+ */
788
+ async function readPackageJson$2(cwd) {
789
+ const pkgPath = resolve(cwd, "package.json");
790
+ try {
791
+ const content = await readFile(pkgPath, "utf-8");
792
+ return JSON.parse(content);
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
797
+ async function fileExists$4(path) {
798
+ try {
799
+ await access(path);
800
+ return true;
801
+ } catch {
802
+ return false;
803
+ }
804
+ }
805
+ const devCommand = defineCommand({
806
+ meta: {
807
+ name: "dev",
808
+ description: "Start dev server with local database"
809
+ },
810
+ args: {
811
+ database: {
812
+ type: "string",
813
+ alias: "d",
814
+ description: "Database path (default: ./data.db)",
815
+ default: "./data.db"
816
+ },
817
+ types: {
818
+ type: "boolean",
819
+ alias: "t",
820
+ description: "Generate types from remote before starting",
821
+ default: false
822
+ },
823
+ port: {
824
+ type: "string",
825
+ alias: "p",
826
+ description: "Port for dev server",
827
+ default: "4321"
828
+ },
829
+ cwd: {
830
+ type: "string",
831
+ description: "Working directory",
832
+ default: process.cwd()
833
+ }
834
+ },
835
+ async run({ args }) {
836
+ const cwd = resolve(args.cwd);
837
+ const pkg = await readPackageJson$2(cwd);
838
+ if (!pkg) {
839
+ consola.error("No package.json found");
840
+ process.exit(1);
841
+ }
842
+ const dbPath = resolve(cwd, args.database);
843
+ if (!await fileExists$4(dbPath)) consola.start("Database not found, initializing...");
844
+ const db = createDatabase({ url: `file:${dbPath}` });
845
+ try {
846
+ consola.start("Checking database migrations...");
847
+ const { applied } = await runMigrations(db);
848
+ if (applied.length > 0) consola.success(`Applied ${applied.length} migrations`);
849
+ else consola.info("Database up to date");
850
+ } catch (error) {
851
+ consola.error("Migration failed:", error);
852
+ await db.destroy();
853
+ process.exit(1);
854
+ }
855
+ await db.destroy();
856
+ if (args.types) {
857
+ const remoteUrl = pkg.emdash?.url || process.env.EMDASH_URL;
858
+ if (!remoteUrl) consola.warn("No remote URL configured. Set EMDASH_URL or emdash.url in package.json");
859
+ else try {
860
+ const { createClientFromArgs } = await Promise.resolve().then(() => client_factory_exports);
861
+ const client = createClientFromArgs({ url: remoteUrl });
862
+ const schema = await client.schemaExport();
863
+ const types = await client.schemaTypes();
864
+ const { writeFile, mkdir } = await import("node:fs/promises");
865
+ const { resolve: resolvePath, dirname } = await import("node:path");
866
+ const outputPath = resolvePath(cwd, ".emdash/types.ts");
867
+ await mkdir(dirname(outputPath), { recursive: true });
868
+ await writeFile(outputPath, types, "utf-8");
869
+ await writeFile(resolvePath(dirname(outputPath), "schema.json"), JSON.stringify(schema, null, 2), "utf-8");
870
+ consola.success(`Generated types for ${schema.collections.length} collections`);
871
+ } catch (error) {
872
+ consola.warn("Type generation failed:", error instanceof Error ? error.message : error);
873
+ }
874
+ }
875
+ consola.start("Starting Astro dev server...");
876
+ const astroArgs = [
877
+ "astro",
878
+ "dev",
879
+ "--port",
880
+ args.port
881
+ ];
882
+ const pnpmLockExists = await fileExists$4(resolve(cwd, "pnpm-lock.yaml"));
883
+ const yarnLockExists = await fileExists$4(resolve(cwd, "yarn.lock"));
884
+ let cmd;
885
+ let cmdArgs;
886
+ if (pnpmLockExists) {
887
+ cmd = "pnpm";
888
+ cmdArgs = astroArgs;
889
+ } else if (yarnLockExists) {
890
+ cmd = "yarn";
891
+ cmdArgs = astroArgs;
892
+ } else {
893
+ cmd = "npx";
894
+ cmdArgs = astroArgs;
895
+ }
896
+ consola.info(`Running: ${cmd} ${cmdArgs.join(" ")}`);
897
+ const child = spawn(cmd, cmdArgs, {
898
+ cwd,
899
+ stdio: "inherit",
900
+ env: {
901
+ ...process.env,
902
+ EMDASH_DATABASE_URL: `file:${dbPath}`
903
+ }
904
+ });
905
+ child.on("error", (error) => {
906
+ consola.error("Failed to start dev server:", error);
907
+ process.exit(1);
908
+ });
909
+ child.on("exit", (code) => {
910
+ process.exit(code ?? 0);
911
+ });
912
+ const cleanup = () => {
913
+ child.kill("SIGTERM");
914
+ };
915
+ process.on("SIGINT", cleanup);
916
+ process.on("SIGTERM", cleanup);
917
+ }
918
+ });
919
+
920
+ //#endregion
921
+ //#region src/cli/commands/doctor.ts
922
+ /**
923
+ * emdash doctor
924
+ *
925
+ * Diagnose database health: connection, migrations, schema integrity.
926
+ */
927
+ async function fileExists$3(path) {
928
+ try {
929
+ await access(path);
930
+ return true;
931
+ } catch {
932
+ return false;
933
+ }
934
+ }
935
+ function printResult(result) {
936
+ (result.status === "pass" ? consola.success : result.status === "warn" ? consola.warn : consola.error)(`${result.name}: ${result.message}`);
937
+ }
938
+ async function checkDatabase(dbPath) {
939
+ const results = [];
940
+ if (!await fileExists$3(dbPath)) {
941
+ results.push({
942
+ name: "database",
943
+ status: "fail",
944
+ message: `not found at ${dbPath} — run "emdash init"`
945
+ });
946
+ return results;
947
+ }
948
+ results.push({
949
+ name: "database",
950
+ status: "pass",
951
+ message: dbPath
952
+ });
953
+ let db;
954
+ try {
955
+ db = createDatabase({ url: `file:${dbPath}` });
956
+ const { applied, pending } = await getMigrationStatus(db);
957
+ if (pending.length === 0) results.push({
958
+ name: "migrations",
959
+ status: "pass",
960
+ message: `${applied.length} applied, none pending`
961
+ });
962
+ else results.push({
963
+ name: "migrations",
964
+ status: "warn",
965
+ message: `${applied.length} applied, ${pending.length} pending — run "emdash init"`
966
+ });
967
+ const { sql } = await import("kysely");
968
+ try {
969
+ const count = (await sql`SELECT COUNT(id) as count FROM _emdash_collections`.execute(db)).rows[0]?.count ?? 0;
970
+ results.push({
971
+ name: "collections",
972
+ status: count > 0 ? "pass" : "warn",
973
+ message: count > 0 ? `${count} collections defined` : "no collections — seed or create via admin"
974
+ });
975
+ } catch {
976
+ results.push({
977
+ name: "collections",
978
+ status: "fail",
979
+ message: "could not query collections table — migrations may not have run"
980
+ });
981
+ }
982
+ try {
983
+ const tableNames = await listTablesLike(db, "ec_%");
984
+ const collectionsResult = await sql`SELECT slug FROM _emdash_collections`.execute(db);
985
+ const registeredSlugs = new Set(collectionsResult.rows.map((r) => `ec_${r.slug}`));
986
+ const orphaned = tableNames.filter((name) => !registeredSlugs.has(name));
987
+ if (orphaned.length > 0) results.push({
988
+ name: "orphaned tables",
989
+ status: "warn",
990
+ message: `found ${orphaned.length}: ${orphaned.join(", ")}`
991
+ });
992
+ } catch {}
993
+ try {
994
+ const count = (await sql`SELECT COUNT(id) as count FROM _emdash_users`.execute(db)).rows[0]?.count ?? 0;
995
+ results.push({
996
+ name: "users",
997
+ status: count > 0 ? "pass" : "warn",
998
+ message: count > 0 ? `${count} users` : "no users — complete setup wizard at /_emdash/admin"
999
+ });
1000
+ } catch {
1001
+ results.push({
1002
+ name: "users",
1003
+ status: "warn",
1004
+ message: "could not query users table"
1005
+ });
1006
+ }
1007
+ } catch (error) {
1008
+ results.push({
1009
+ name: "database connection",
1010
+ status: "fail",
1011
+ message: error instanceof Error ? error.message : "failed to connect"
1012
+ });
1013
+ } finally {
1014
+ if (db) await db.destroy();
1015
+ }
1016
+ return results;
1017
+ }
1018
+ const doctorCommand = defineCommand({
1019
+ meta: {
1020
+ name: "doctor",
1021
+ description: "Check database health and diagnose issues"
1022
+ },
1023
+ args: {
1024
+ database: {
1025
+ type: "string",
1026
+ alias: "d",
1027
+ description: "Database path (default: ./data.db)",
1028
+ default: "./data.db"
1029
+ },
1030
+ cwd: {
1031
+ type: "string",
1032
+ description: "Working directory",
1033
+ default: process.cwd()
1034
+ },
1035
+ json: {
1036
+ type: "boolean",
1037
+ description: "Output results as JSON",
1038
+ default: false
1039
+ }
1040
+ },
1041
+ async run({ args }) {
1042
+ const results = await checkDatabase(resolve(resolve(args.cwd), args.database));
1043
+ if (args.json) {
1044
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
1045
+ return;
1046
+ }
1047
+ consola.start("EmDash Doctor\n");
1048
+ for (const result of results) printResult(result);
1049
+ const fails = results.filter((r) => r.status === "fail");
1050
+ const warns = results.filter((r) => r.status === "warn");
1051
+ consola.log("");
1052
+ if (fails.length === 0 && warns.length === 0) consola.success("All checks passed");
1053
+ else if (fails.length === 0) consola.info(`All critical checks passed (${warns.length} warnings)`);
1054
+ else {
1055
+ consola.error(`${fails.length} issues found`);
1056
+ process.exitCode = 1;
1057
+ }
1058
+ }
1059
+ });
1060
+
1061
+ //#endregion
1062
+ //#region src/cli/commands/export-seed.ts
1063
+ /**
1064
+ * emdash export-seed
1065
+ *
1066
+ * Export current database schema (and optionally content) as a seed file
1067
+ */
1068
+ const SETTINGS_PREFIX = "site:";
1069
+ const exportSeedCommand = defineCommand({
1070
+ meta: {
1071
+ name: "export-seed",
1072
+ description: "Export database schema and content as a seed file"
1073
+ },
1074
+ args: {
1075
+ database: {
1076
+ type: "string",
1077
+ alias: "d",
1078
+ description: "Database path",
1079
+ default: "./data.db"
1080
+ },
1081
+ cwd: {
1082
+ type: "string",
1083
+ description: "Working directory",
1084
+ default: process.cwd()
1085
+ },
1086
+ "with-content": {
1087
+ type: "string",
1088
+ description: "Include content (all or comma-separated collection names)",
1089
+ required: false
1090
+ },
1091
+ pretty: {
1092
+ type: "boolean",
1093
+ description: "Pretty print JSON output",
1094
+ default: true
1095
+ }
1096
+ },
1097
+ async run({ args }) {
1098
+ const dbPath = resolve(resolve(args.cwd), args.database);
1099
+ consola.info(`Database: ${dbPath}`);
1100
+ const db = createDatabase({ url: `file:${dbPath}` });
1101
+ try {
1102
+ await runMigrations(db);
1103
+ } catch (error) {
1104
+ consola.error("Migration failed:", error);
1105
+ await db.destroy();
1106
+ process.exit(1);
1107
+ }
1108
+ try {
1109
+ const seed = await exportSeed(db, args["with-content"]);
1110
+ const output = args.pretty ? JSON.stringify(seed, null, " ") : JSON.stringify(seed);
1111
+ console.log(output);
1112
+ } catch (error) {
1113
+ consola.error("Export failed:", error);
1114
+ await db.destroy();
1115
+ process.exit(1);
1116
+ }
1117
+ await db.destroy();
1118
+ }
1119
+ });
1120
+ /**
1121
+ * Export database to seed file format
1122
+ */
1123
+ async function exportSeed(db, withContent) {
1124
+ const seed = {
1125
+ $schema: "https://emdashcms.com/seed.schema.json",
1126
+ version: "1",
1127
+ meta: {
1128
+ name: "Exported Seed",
1129
+ description: "Exported from existing EmDash database"
1130
+ }
1131
+ };
1132
+ seed.settings = await exportSettings(db);
1133
+ seed.collections = await exportCollections(db);
1134
+ seed.taxonomies = await exportTaxonomies(db);
1135
+ seed.menus = await exportMenus(db);
1136
+ seed.widgetAreas = await exportWidgetAreas(db);
1137
+ if (withContent !== void 0) {
1138
+ const collections = withContent === "" || withContent === "true" ? null : withContent.split(",").map((s) => s.trim()).filter(Boolean);
1139
+ seed.content = await exportContent(db, seed.collections || [], collections);
1140
+ }
1141
+ return seed;
1142
+ }
1143
+ /**
1144
+ * Export site settings
1145
+ */
1146
+ async function exportSettings(db) {
1147
+ const allOptions = await new OptionsRepository(db).getByPrefix(SETTINGS_PREFIX);
1148
+ const settings = {};
1149
+ for (const [key, value] of allOptions) {
1150
+ const settingKey = key.replace(SETTINGS_PREFIX, "");
1151
+ settings[settingKey] = value;
1152
+ }
1153
+ return Object.keys(settings).length > 0 ? settings : void 0;
1154
+ }
1155
+ /**
1156
+ * Export collections and their fields
1157
+ */
1158
+ async function exportCollections(db) {
1159
+ const registry = new SchemaRegistry(db);
1160
+ const collections = await registry.listCollections();
1161
+ const result = [];
1162
+ for (const collection of collections) {
1163
+ const fields = await registry.listFields(collection.id);
1164
+ const seedCollection = {
1165
+ slug: collection.slug,
1166
+ label: collection.label,
1167
+ labelSingular: collection.labelSingular || void 0,
1168
+ description: collection.description || void 0,
1169
+ icon: collection.icon || void 0,
1170
+ supports: collection.supports.length > 0 ? collection.supports : void 0,
1171
+ urlPattern: collection.urlPattern || void 0,
1172
+ fields: fields.map((field) => ({
1173
+ slug: field.slug,
1174
+ label: field.label,
1175
+ type: field.type,
1176
+ required: field.required || void 0,
1177
+ unique: field.unique || void 0,
1178
+ searchable: field.searchable || void 0,
1179
+ defaultValue: field.defaultValue,
1180
+ validation: field.validation ? { ...field.validation } : void 0,
1181
+ widget: field.widget || void 0,
1182
+ options: field.options || void 0
1183
+ }))
1184
+ };
1185
+ result.push(seedCollection);
1186
+ }
1187
+ return result;
1188
+ }
1189
+ /**
1190
+ * Export taxonomy definitions and terms
1191
+ */
1192
+ async function exportTaxonomies(db) {
1193
+ const defs = await db.selectFrom("_emdash_taxonomy_defs").selectAll().execute();
1194
+ const result = [];
1195
+ const termRepo = new TaxonomyRepository(db);
1196
+ for (const def of defs) {
1197
+ const terms = await termRepo.findByName(def.name);
1198
+ const seedTerms = [];
1199
+ const idToSlug = /* @__PURE__ */ new Map();
1200
+ for (const term of terms) idToSlug.set(term.id, term.slug);
1201
+ for (const term of terms) {
1202
+ const seedTerm = {
1203
+ slug: term.slug,
1204
+ label: term.label,
1205
+ description: typeof term.data?.description === "string" ? term.data.description : void 0
1206
+ };
1207
+ if (term.parentId) seedTerm.parent = idToSlug.get(term.parentId);
1208
+ seedTerms.push(seedTerm);
1209
+ }
1210
+ const taxonomy = {
1211
+ name: def.name,
1212
+ label: def.label,
1213
+ labelSingular: def.label_singular || void 0,
1214
+ hierarchical: def.hierarchical === 1,
1215
+ collections: def.collections ? JSON.parse(def.collections) : []
1216
+ };
1217
+ if (seedTerms.length > 0) taxonomy.terms = seedTerms;
1218
+ result.push(taxonomy);
1219
+ }
1220
+ return result;
1221
+ }
1222
+ /**
1223
+ * Export menus with their items
1224
+ */
1225
+ async function exportMenus(db) {
1226
+ const menus = await db.selectFrom("_emdash_menus").selectAll().execute();
1227
+ const result = [];
1228
+ for (const menu of menus) {
1229
+ const seedItems = buildMenuItemTree(await db.selectFrom("_emdash_menu_items").selectAll().where("menu_id", "=", menu.id).orderBy("sort_order", "asc").execute());
1230
+ result.push({
1231
+ name: menu.name,
1232
+ label: menu.label,
1233
+ items: seedItems
1234
+ });
1235
+ }
1236
+ return result;
1237
+ }
1238
+ /** Type guard for valid widget types */
1239
+ function isWidgetType(t) {
1240
+ return t === "content" || t === "menu" || t === "component";
1241
+ }
1242
+ /**
1243
+ * Build hierarchical menu item tree from flat array
1244
+ */
1245
+ function buildMenuItemTree(items) {
1246
+ const childMap = /* @__PURE__ */ new Map();
1247
+ for (const item of items) {
1248
+ const parentId = item.parent_id;
1249
+ if (!childMap.has(parentId)) childMap.set(parentId, []);
1250
+ childMap.get(parentId).push(item);
1251
+ }
1252
+ function buildLevel(parentId) {
1253
+ return (childMap.get(parentId) || []).map((item) => {
1254
+ const seedItem = {
1255
+ type: item.type,
1256
+ label: item.label || void 0
1257
+ };
1258
+ if (item.type === "custom") seedItem.url = item.custom_url || void 0;
1259
+ else {
1260
+ seedItem.ref = item.reference_id || void 0;
1261
+ seedItem.collection = item.reference_collection || void 0;
1262
+ }
1263
+ if (item.target === "_blank") seedItem.target = "_blank";
1264
+ if (item.title_attr) seedItem.titleAttr = item.title_attr;
1265
+ if (item.css_classes) seedItem.cssClasses = item.css_classes;
1266
+ const itemChildren = buildLevel(item.id);
1267
+ if (itemChildren.length > 0) seedItem.children = itemChildren;
1268
+ return seedItem;
1269
+ });
1270
+ }
1271
+ return buildLevel(null);
1272
+ }
1273
+ /**
1274
+ * Export widget areas with their widgets
1275
+ */
1276
+ async function exportWidgetAreas(db) {
1277
+ const areas = await db.selectFrom("_emdash_widget_areas").selectAll().execute();
1278
+ const result = [];
1279
+ for (const area of areas) {
1280
+ const seedWidgets = (await db.selectFrom("_emdash_widgets").selectAll().where("area_id", "=", area.id).orderBy("sort_order", "asc").execute()).filter((w) => isWidgetType(w.type)).map((widget) => {
1281
+ const seedWidget = { type: isWidgetType(widget.type) ? widget.type : "content" };
1282
+ if (widget.title) seedWidget.title = widget.title;
1283
+ if (widget.type === "content" && widget.content) seedWidget.content = JSON.parse(widget.content);
1284
+ else if (widget.type === "menu" && widget.menu_name) seedWidget.menuName = widget.menu_name;
1285
+ else if (widget.type === "component") {
1286
+ if (widget.component_id) seedWidget.componentId = widget.component_id;
1287
+ if (widget.component_props) seedWidget.props = JSON.parse(widget.component_props);
1288
+ }
1289
+ return seedWidget;
1290
+ });
1291
+ result.push({
1292
+ name: area.name,
1293
+ label: area.label,
1294
+ description: area.description || void 0,
1295
+ widgets: seedWidgets
1296
+ });
1297
+ }
1298
+ return result;
1299
+ }
1300
+ /**
1301
+ * Export content from collections
1302
+ */
1303
+ async function exportContent(db, collections, includeCollections) {
1304
+ const content = {};
1305
+ const contentRepo = new ContentRepository(db);
1306
+ const taxonomyRepo = new TaxonomyRepository(db);
1307
+ const mediaRepo = new MediaRepository(db);
1308
+ const mediaMap = /* @__PURE__ */ new Map();
1309
+ try {
1310
+ let cursor;
1311
+ do {
1312
+ const result = await mediaRepo.findMany({
1313
+ limit: 100,
1314
+ cursor,
1315
+ status: "all"
1316
+ });
1317
+ for (const media of result.items) mediaMap.set(media.id, {
1318
+ url: `/_emdash/api/media/file/${media.storageKey}`,
1319
+ filename: media.filename,
1320
+ alt: media.alt || void 0,
1321
+ caption: media.caption || void 0
1322
+ });
1323
+ cursor = result.nextCursor;
1324
+ } while (cursor);
1325
+ } catch {}
1326
+ const i18nEnabled = isI18nEnabled();
1327
+ for (const collection of collections) {
1328
+ if (includeCollections && !includeCollections.includes(collection.slug)) continue;
1329
+ const entries = [];
1330
+ let cursor;
1331
+ const translationGroupToSeedId = /* @__PURE__ */ new Map();
1332
+ do {
1333
+ const result = await contentRepo.findMany(collection.slug, {
1334
+ limit: 100,
1335
+ cursor
1336
+ });
1337
+ for (const item of result.items) {
1338
+ const seedId = item.slug ? i18nEnabled && item.locale ? `${collection.slug}:${item.slug}:${item.locale}` : `${collection.slug}:${item.slug}` : item.id;
1339
+ const processedData = processDataForExport(item.data, collection.fields, mediaMap);
1340
+ const entry = {
1341
+ id: seedId,
1342
+ slug: item.slug || item.id,
1343
+ status: item.status === "published" || item.status === "draft" ? item.status : void 0,
1344
+ data: processedData
1345
+ };
1346
+ if (i18nEnabled && item.locale) {
1347
+ entry.locale = item.locale;
1348
+ if (item.translationGroup) {
1349
+ const sourceSeedId = translationGroupToSeedId.get(item.translationGroup);
1350
+ if (sourceSeedId) entry.translationOf = sourceSeedId;
1351
+ else translationGroupToSeedId.set(item.translationGroup, seedId);
1352
+ }
1353
+ }
1354
+ const taxonomies = await getTaxonomyAssignments(taxonomyRepo, collection.slug, item.id);
1355
+ if (Object.keys(taxonomies).length > 0) entry.taxonomies = taxonomies;
1356
+ entries.push(entry);
1357
+ }
1358
+ cursor = result.nextCursor;
1359
+ } while (cursor);
1360
+ if (i18nEnabled && entries.length > 0) entries.sort((a, b) => {
1361
+ if (a.translationOf && !b.translationOf) return 1;
1362
+ if (!a.translationOf && b.translationOf) return -1;
1363
+ return 0;
1364
+ });
1365
+ if (entries.length > 0) content[collection.slug] = entries;
1366
+ }
1367
+ return content;
1368
+ }
1369
+ /**
1370
+ * Process content data for export, converting image fields to $media syntax
1371
+ */
1372
+ function processDataForExport(data, fields, mediaMap) {
1373
+ const result = {};
1374
+ const fieldTypes = /* @__PURE__ */ new Map();
1375
+ for (const field of fields) fieldTypes.set(field.slug, field.type);
1376
+ for (const [key, value] of Object.entries(data)) {
1377
+ const fieldType = fieldTypes.get(key);
1378
+ if (fieldType === "image" && value && typeof value === "object") {
1379
+ const imageValue = value;
1380
+ if (imageValue.id) {
1381
+ const mediaInfo = mediaMap.get(imageValue.id);
1382
+ if (mediaInfo) {
1383
+ result[key] = { $media: {
1384
+ url: mediaInfo.url,
1385
+ filename: mediaInfo.filename,
1386
+ alt: imageValue.alt || mediaInfo.alt,
1387
+ caption: mediaInfo.caption
1388
+ } };
1389
+ continue;
1390
+ }
1391
+ }
1392
+ result[key] = value;
1393
+ } else if (fieldType === "reference" && typeof value === "string") result[key] = `$ref:${value}`;
1394
+ else if (Array.isArray(value)) result[key] = value.map((item) => {
1395
+ if (typeof item === "string" && fieldType === "reference") return `$ref:${item}`;
1396
+ return item;
1397
+ });
1398
+ else result[key] = value;
1399
+ }
1400
+ return result;
1401
+ }
1402
+ /**
1403
+ * Get taxonomy term assignments for a content entry
1404
+ */
1405
+ async function getTaxonomyAssignments(taxonomyRepo, collection, entryId) {
1406
+ const terms = await taxonomyRepo.getTermsForEntry(collection, entryId);
1407
+ const result = {};
1408
+ for (const term of terms) {
1409
+ if (!result[term.name]) result[term.name] = [];
1410
+ result[term.name].push(term.slug);
1411
+ }
1412
+ return result;
1413
+ }
1414
+
1415
+ //#endregion
1416
+ //#region src/cli/commands/init.ts
1417
+ /**
1418
+ * emdash init
1419
+ *
1420
+ * Initialize database from template config in package.json
1421
+ */
1422
+ async function fileExists$2(path) {
1423
+ try {
1424
+ await access(path);
1425
+ return true;
1426
+ } catch {
1427
+ return false;
1428
+ }
1429
+ }
1430
+ async function readPackageJson$1(cwd) {
1431
+ const pkgPath = resolve(cwd, "package.json");
1432
+ try {
1433
+ const content = await readFile(pkgPath, "utf-8");
1434
+ return JSON.parse(content);
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ }
1439
+ async function runSqlFile(db, filePath) {
1440
+ const statements = (await readFile(filePath, "utf-8")).split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1441
+ for (const statement of statements) await db.executeQuery({
1442
+ sql: statement,
1443
+ parameters: [],
1444
+ query: {
1445
+ kind: "RawNode",
1446
+ sqlFragments: [statement],
1447
+ parameters: []
1448
+ }
1449
+ });
1450
+ }
1451
+ /**
1452
+ * Check if database has already been initialized with template schema
1453
+ */
1454
+ async function isAlreadyInitialized(db) {
1455
+ try {
1456
+ const { sql } = await import("kysely");
1457
+ const row = (await sql`SELECT COUNT(id) as count FROM _emdash_collections`.execute(db)).rows[0];
1458
+ return row ? row.count > 0 : false;
1459
+ } catch {
1460
+ return false;
1461
+ }
1462
+ }
1463
+ const initCommand = defineCommand({
1464
+ meta: {
1465
+ name: "init",
1466
+ description: "Initialize database from template config"
1467
+ },
1468
+ args: {
1469
+ database: {
1470
+ type: "string",
1471
+ alias: "d",
1472
+ description: "Database path (default: ./data.db)",
1473
+ default: "./data.db"
1474
+ },
1475
+ cwd: {
1476
+ type: "string",
1477
+ description: "Working directory",
1478
+ default: process.cwd()
1479
+ },
1480
+ force: {
1481
+ type: "boolean",
1482
+ alias: "f",
1483
+ description: "Force re-initialization",
1484
+ default: false
1485
+ }
1486
+ },
1487
+ async run({ args }) {
1488
+ const cwd = resolve(args.cwd);
1489
+ consola.start("Initializing EmDash...");
1490
+ const pkg = await readPackageJson$1(cwd);
1491
+ if (!pkg) {
1492
+ consola.error("No package.json found in", cwd);
1493
+ process.exit(1);
1494
+ }
1495
+ const config = pkg.emdash;
1496
+ consola.info(`Project: ${pkg.name || "unknown"}`);
1497
+ if (config?.label) consola.info(`Template: ${config.label}`);
1498
+ const dbPath = resolve(cwd, args.database);
1499
+ consola.info(`Database: ${dbPath}`);
1500
+ const db = createDatabase({ url: `file:${dbPath}` });
1501
+ consola.start("Running migrations...");
1502
+ try {
1503
+ const { applied } = await runMigrations(db);
1504
+ if (applied.length > 0) {
1505
+ consola.success(`Applied ${applied.length} migrations`);
1506
+ for (const name of applied) consola.info(` - ${name}`);
1507
+ } else consola.info("Migrations already up to date");
1508
+ } catch (error) {
1509
+ consola.error("Migration failed:", error);
1510
+ await db.destroy();
1511
+ process.exit(1);
1512
+ }
1513
+ const alreadyInitialized = await isAlreadyInitialized(db);
1514
+ if (alreadyInitialized && !args.force) {
1515
+ await db.destroy();
1516
+ consola.success("Already initialized. Use --force to re-run schema/seed.");
1517
+ return;
1518
+ }
1519
+ if (alreadyInitialized && args.force) consola.warn("Re-initializing (--force)...");
1520
+ if (config?.schema) {
1521
+ const schemaPath = resolve(cwd, config.schema);
1522
+ if (await fileExists$2(schemaPath)) {
1523
+ consola.start(`Running schema: ${config.schema}`);
1524
+ try {
1525
+ await runSqlFile(db, schemaPath);
1526
+ consola.success("Schema applied");
1527
+ } catch (error) {
1528
+ consola.error("Schema failed:", error);
1529
+ await db.destroy();
1530
+ process.exit(1);
1531
+ }
1532
+ } else consola.warn(`Schema file not found: ${config.schema}`);
1533
+ }
1534
+ await db.destroy();
1535
+ consola.success("EmDash initialized successfully!");
1536
+ consola.info("Run `pnpm dev` to start the development server");
1537
+ }
1538
+ });
1539
+
1540
+ //#endregion
1541
+ //#region src/cli/commands/login.ts
1542
+ /**
1543
+ * Login/logout/whoami CLI commands
1544
+ *
1545
+ * Login uses the OAuth Device Flow (RFC 8628):
1546
+ * 1. POST /oauth/device/code → get device_code + user_code
1547
+ * 2. Display URL + code to user
1548
+ * 3. Poll POST /oauth/device/token until authorized
1549
+ * 4. Save tokens to ~/.config/emdash/auth.json
1550
+ *
1551
+ * Custom headers (--header / EMDASH_HEADERS) are sent with every request
1552
+ * and persisted to credentials so subsequent commands inherit them.
1553
+ * This supports sites behind reverse proxies like Cloudflare Access.
1554
+ */
1555
+ async function pollForToken(tokenEndpoint, deviceCode, interval, expiresIn, fetchFn) {
1556
+ const deadline = Date.now() + expiresIn * 1e3;
1557
+ let currentInterval = interval;
1558
+ while (Date.now() < deadline) {
1559
+ await new Promise((resolve) => setTimeout(resolve, currentInterval * 1e3));
1560
+ const res = await fetchFn(tokenEndpoint, {
1561
+ method: "POST",
1562
+ headers: { "Content-Type": "application/json" },
1563
+ body: JSON.stringify({
1564
+ device_code: deviceCode,
1565
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1566
+ })
1567
+ });
1568
+ if (res.ok) return await res.json();
1569
+ const body = await res.json();
1570
+ if (body.error === "authorization_pending") continue;
1571
+ if (body.error === "slow_down") {
1572
+ currentInterval = body.interval ?? currentInterval + 5;
1573
+ continue;
1574
+ }
1575
+ if (body.error === "expired_token") throw new Error("Device code expired. Please try again.");
1576
+ if (body.error === "access_denied") throw new Error("Authorization was denied.");
1577
+ throw new Error(`Token exchange failed: ${body.error || res.statusText}`);
1578
+ }
1579
+ throw new Error("Device code expired (timeout). Please try again.");
1580
+ }
1581
+ /**
1582
+ * Handle a Cloudflare Access redirect during login.
1583
+ *
1584
+ * 1. Try `cloudflared access token` for a cached JWT
1585
+ * 2. Try `cloudflared access login` to do the browser flow
1586
+ * 3. If cloudflared isn't available, print instructions for service tokens
1587
+ *
1588
+ * Returns the Access JWT, or null if auth couldn't be resolved.
1589
+ */
1590
+ async function handleAccessRedirect(baseUrl) {
1591
+ consola$1.info("This site is behind Cloudflare Access.");
1592
+ const cached = await getCachedAccessToken(baseUrl);
1593
+ if (cached) {
1594
+ consola$1.success("Using cached Cloudflare Access token from cloudflared.");
1595
+ return cached;
1596
+ }
1597
+ consola$1.info("Launching browser for Cloudflare Access login...");
1598
+ if (await runCloudflaredLogin(baseUrl)) {
1599
+ const token = await getCachedAccessToken(baseUrl);
1600
+ if (token) {
1601
+ consola$1.success("Cloudflare Access authentication successful.");
1602
+ return token;
1603
+ }
1604
+ }
1605
+ console.log();
1606
+ consola$1.info("Could not authenticate with Cloudflare Access automatically.");
1607
+ consola$1.info("You have two options:");
1608
+ console.log();
1609
+ consola$1.info(` ${pc.bold("Option 1:")} Install cloudflared and run:`);
1610
+ console.log(` ${pc.cyan(`cloudflared access login ${baseUrl}`)}`);
1611
+ console.log(` ${pc.cyan(`emdash login --url ${baseUrl}`)}`);
1612
+ console.log();
1613
+ consola$1.info(` ${pc.bold("Option 2:")} Use a service token:`);
1614
+ console.log(` ${pc.cyan(`emdash login --url ${baseUrl} -H "CF-Access-Client-Id: <id>" -H "CF-Access-Client-Secret: <secret>"`)}`);
1615
+ console.log();
1616
+ return null;
1617
+ }
1618
+ const loginCommand = defineCommand({
1619
+ meta: {
1620
+ name: "login",
1621
+ description: "Log in to an EmDash instance"
1622
+ },
1623
+ args: {
1624
+ url: {
1625
+ type: "string",
1626
+ alias: "u",
1627
+ description: "EmDash instance URL",
1628
+ default: "http://localhost:4321"
1629
+ },
1630
+ header: {
1631
+ type: "string",
1632
+ alias: "H",
1633
+ description: "Custom header \"Name: Value\" (repeatable, or use EMDASH_HEADERS env)"
1634
+ }
1635
+ },
1636
+ async run({ args }) {
1637
+ const baseUrl = args.url || "http://localhost:4321";
1638
+ consola$1.start(`Connecting to ${baseUrl}...`);
1639
+ const customHeaders = resolveCustomHeaders();
1640
+ let headerFetch = createHeaderAwareFetch(customHeaders);
1641
+ try {
1642
+ const discoveryUrl = new URL("/_emdash/.well-known/auth", baseUrl);
1643
+ let res = await headerFetch(discoveryUrl, { redirect: "manual" });
1644
+ if (isAccessRedirect(res)) {
1645
+ const accessToken = await handleAccessRedirect(baseUrl);
1646
+ if (!accessToken) return;
1647
+ customHeaders["cf-access-token"] = accessToken;
1648
+ headerFetch = createHeaderAwareFetch(customHeaders);
1649
+ res = await headerFetch(discoveryUrl);
1650
+ } else if (res.status === 301 || res.status === 302) res = await headerFetch(discoveryUrl);
1651
+ if (!res.ok) {
1652
+ if (res.status === 404) {
1653
+ if (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1")) {
1654
+ consola$1.info("Auth discovery not available. Trying dev bypass...");
1655
+ const bypassRes = await fetch(new URL("/_emdash/api/auth/dev-bypass", baseUrl), { redirect: "manual" });
1656
+ if (bypassRes.status === 302 || bypassRes.ok) consola$1.success("Dev bypass available. Client will authenticate automatically.");
1657
+ else consola$1.error("Could not authenticate. Is the dev server running?");
1658
+ } else consola$1.error("Auth discovery endpoint not found. Is this an EmDash instance?");
1659
+ return;
1660
+ }
1661
+ consola$1.error(`Discovery failed: ${res.status} ${res.statusText}`);
1662
+ process.exit(2);
1663
+ }
1664
+ const discovery = await res.json();
1665
+ consola$1.success(`Connected to ${discovery.instance?.name || "EmDash"}`);
1666
+ const deviceFlow = discovery.auth?.methods?.device_flow;
1667
+ if (!deviceFlow) {
1668
+ consola$1.info("Device Flow is not available for this instance.");
1669
+ consola$1.info("Generate an API token in Settings > API Tokens");
1670
+ consola$1.info(`Then run: ${pc.cyan(`emdash --token <token> --url ${baseUrl}`)}`);
1671
+ return;
1672
+ }
1673
+ const codeUrl = new URL(deviceFlow.device_authorization_endpoint, baseUrl);
1674
+ const codeRes = await headerFetch(codeUrl, {
1675
+ method: "POST",
1676
+ headers: {
1677
+ "Content-Type": "application/json",
1678
+ "X-EmDash-Request": "1"
1679
+ },
1680
+ body: JSON.stringify({ client_id: "emdash-cli" })
1681
+ });
1682
+ if (!codeRes.ok) {
1683
+ consola$1.error(`Failed to request device code: ${codeRes.status}`);
1684
+ process.exit(2);
1685
+ }
1686
+ const deviceCode = await codeRes.json();
1687
+ console.log();
1688
+ consola$1.info(`Open your browser to:`);
1689
+ console.log(` ${pc.cyan(pc.bold(deviceCode.verification_uri))}`);
1690
+ console.log();
1691
+ consola$1.info(`Enter code: ${pc.yellow(pc.bold(deviceCode.user_code))}`);
1692
+ console.log();
1693
+ try {
1694
+ const { execFile } = await import("node:child_process");
1695
+ if (process.platform === "darwin") execFile("open", [deviceCode.verification_uri]);
1696
+ else if (process.platform === "win32") execFile("cmd", [
1697
+ "/c",
1698
+ "start",
1699
+ "",
1700
+ deviceCode.verification_uri
1701
+ ]);
1702
+ else execFile("xdg-open", [deviceCode.verification_uri]);
1703
+ } catch {}
1704
+ consola$1.start("Waiting for authorization...");
1705
+ const tokenResult = await pollForToken(new URL(deviceFlow.token_endpoint, baseUrl).toString(), deviceCode.device_code, deviceCode.interval, deviceCode.expires_in, headerFetch);
1706
+ let userEmail = "unknown";
1707
+ let userRole = "unknown";
1708
+ try {
1709
+ const meRes = await headerFetch(new URL("/_emdash/api/auth/me", baseUrl), { headers: { Authorization: `Bearer ${tokenResult.access_token}` } });
1710
+ if (meRes.ok) {
1711
+ const me = (await meRes.json()).data;
1712
+ userEmail = me.email || "unknown";
1713
+ userRole = (me.role ? {
1714
+ 10: "subscriber",
1715
+ 20: "contributor",
1716
+ 30: "author",
1717
+ 40: "editor",
1718
+ 50: "admin"
1719
+ }[me.role] : void 0) || "unknown";
1720
+ }
1721
+ } catch {}
1722
+ const expiresAt = new Date(Date.now() + tokenResult.expires_in * 1e3).toISOString();
1723
+ const hasCustomHeaders = Object.keys(customHeaders).length > 0;
1724
+ saveCredentials(baseUrl, {
1725
+ accessToken: tokenResult.access_token,
1726
+ refreshToken: tokenResult.refresh_token,
1727
+ expiresAt,
1728
+ ...hasCustomHeaders ? { customHeaders } : {},
1729
+ user: {
1730
+ email: userEmail,
1731
+ role: userRole
1732
+ }
1733
+ });
1734
+ consola$1.success(`Logged in as ${pc.bold(userEmail)} (${userRole})`);
1735
+ consola$1.info(`Token saved to ${pc.dim(resolveCredentialKey(baseUrl))}`);
1736
+ } catch (error) {
1737
+ consola$1.error(error instanceof Error ? error.message : "Login failed");
1738
+ process.exit(2);
1739
+ }
1740
+ }
1741
+ });
1742
+ const logoutCommand = defineCommand({
1743
+ meta: {
1744
+ name: "logout",
1745
+ description: "Log out of an EmDash instance"
1746
+ },
1747
+ args: { url: {
1748
+ type: "string",
1749
+ alias: "u",
1750
+ description: "EmDash instance URL",
1751
+ default: "http://localhost:4321"
1752
+ } },
1753
+ async run({ args }) {
1754
+ const baseUrl = args.url || "http://localhost:4321";
1755
+ const cred = getCredentials(baseUrl);
1756
+ if (!cred) {
1757
+ consola$1.info("No stored credentials found for this instance.");
1758
+ return;
1759
+ }
1760
+ const headerFetch = createHeaderAwareFetch(cred.customHeaders ?? {});
1761
+ try {
1762
+ await headerFetch(new URL("/_emdash/api/oauth/token/revoke", baseUrl), {
1763
+ method: "POST",
1764
+ headers: { "Content-Type": "application/json" },
1765
+ body: JSON.stringify({ token: cred.refreshToken })
1766
+ });
1767
+ } catch {}
1768
+ removeCredentials(baseUrl);
1769
+ consola$1.success("Logged out successfully.");
1770
+ }
1771
+ });
1772
+ const whoamiCommand = defineCommand({
1773
+ meta: {
1774
+ name: "whoami",
1775
+ description: "Show current user and auth method"
1776
+ },
1777
+ args: {
1778
+ url: {
1779
+ type: "string",
1780
+ alias: "u",
1781
+ description: "EmDash instance URL",
1782
+ default: "http://localhost:4321"
1783
+ },
1784
+ token: {
1785
+ type: "string",
1786
+ alias: "t",
1787
+ description: "Auth token"
1788
+ },
1789
+ json: {
1790
+ type: "boolean",
1791
+ description: "Output as JSON"
1792
+ }
1793
+ },
1794
+ async run({ args }) {
1795
+ const baseUrl = args.url || "http://localhost:4321";
1796
+ let token = args.token || process.env["EMDASH_TOKEN"];
1797
+ let authMethod = token ? "token" : "none";
1798
+ let storedHeaders = {};
1799
+ if (!token) {
1800
+ const cred = getCredentials(baseUrl);
1801
+ if (cred) {
1802
+ token = cred.accessToken;
1803
+ authMethod = "stored";
1804
+ storedHeaders = cred.customHeaders ?? {};
1805
+ if (new Date(cred.expiresAt) < /* @__PURE__ */ new Date()) {
1806
+ const headerFetch = createHeaderAwareFetch(storedHeaders);
1807
+ try {
1808
+ const refreshRes = await headerFetch(new URL("/_emdash/api/oauth/token/refresh", baseUrl), {
1809
+ method: "POST",
1810
+ headers: { "Content-Type": "application/json" },
1811
+ body: JSON.stringify({
1812
+ refresh_token: cred.refreshToken,
1813
+ grant_type: "refresh_token"
1814
+ })
1815
+ });
1816
+ if (refreshRes.ok) {
1817
+ const refreshed = await refreshRes.json();
1818
+ token = refreshed.access_token;
1819
+ saveCredentials(baseUrl, {
1820
+ ...cred,
1821
+ accessToken: refreshed.access_token,
1822
+ expiresAt: new Date(Date.now() + refreshed.expires_in * 1e3).toISOString()
1823
+ });
1824
+ } else {
1825
+ consola$1.warn("Stored token expired and refresh failed. Run: emdash login");
1826
+ process.exit(2);
1827
+ }
1828
+ } catch {
1829
+ consola$1.warn("Stored token expired. Run: emdash login");
1830
+ process.exit(2);
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ if (!token) {
1836
+ if (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1")) {
1837
+ authMethod = "dev-bypass";
1838
+ consola$1.info(`Auth method: ${pc.cyan("dev-bypass")}`);
1839
+ consola$1.info("No stored credentials. Client will use dev bypass for localhost.");
1840
+ return;
1841
+ }
1842
+ consola$1.error("Not logged in. Run: emdash login");
1843
+ process.exit(2);
1844
+ }
1845
+ const headerFetch = createHeaderAwareFetch(storedHeaders);
1846
+ try {
1847
+ const meRes = await headerFetch(new URL("/_emdash/api/auth/me", baseUrl), { headers: { Authorization: `Bearer ${token}` } });
1848
+ if (!meRes.ok) {
1849
+ if (meRes.status === 401) {
1850
+ consola$1.error("Token is invalid or expired. Run: emdash login");
1851
+ process.exit(1);
1852
+ }
1853
+ consola$1.error(`Failed to fetch user info: ${meRes.status}`);
1854
+ process.exit(1);
1855
+ }
1856
+ const me = (await meRes.json()).data;
1857
+ const roleNames = {
1858
+ 10: "subscriber",
1859
+ 20: "contributor",
1860
+ 30: "author",
1861
+ 40: "editor",
1862
+ 50: "admin"
1863
+ };
1864
+ if (args.json) console.log(JSON.stringify({
1865
+ id: me.id,
1866
+ email: me.email,
1867
+ name: me.name,
1868
+ role: roleNames[me.role] || `unknown (${me.role})`,
1869
+ authMethod,
1870
+ url: baseUrl
1871
+ }));
1872
+ else {
1873
+ consola$1.info(`Email: ${pc.bold(me.email)}`);
1874
+ if (me.name) consola$1.info(`Name: ${me.name}`);
1875
+ consola$1.info(`Role: ${pc.cyan(roleNames[me.role] || `unknown (${me.role})`)}`);
1876
+ consola$1.info(`Auth: ${pc.dim(authMethod)}`);
1877
+ consola$1.info(`URL: ${pc.dim(baseUrl)}`);
1878
+ }
1879
+ } catch (error) {
1880
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
1881
+ process.exit(1);
1882
+ }
1883
+ }
1884
+ });
1885
+
1886
+ //#endregion
1887
+ //#region src/cli/commands/media.ts
1888
+ /**
1889
+ * emdash media
1890
+ *
1891
+ * Manage media items via the EmDash API
1892
+ */
1893
+ const listCommand$3 = defineCommand({
1894
+ meta: {
1895
+ name: "list",
1896
+ description: "List media items"
1897
+ },
1898
+ args: {
1899
+ ...connectionArgs,
1900
+ mime: {
1901
+ type: "string",
1902
+ description: "Filter by MIME type (e.g., image/png)"
1903
+ },
1904
+ limit: {
1905
+ type: "string",
1906
+ description: "Number of items to return"
1907
+ },
1908
+ cursor: {
1909
+ type: "string",
1910
+ description: "Pagination cursor"
1911
+ }
1912
+ },
1913
+ async run({ args }) {
1914
+ const client = createClientFromArgs(args);
1915
+ try {
1916
+ output(await client.mediaList({
1917
+ mimeType: args.mime,
1918
+ limit: args.limit ? Number(args.limit) : void 0,
1919
+ cursor: args.cursor
1920
+ }), args);
1921
+ } catch (error) {
1922
+ consola$1.error("Failed to list media:", error instanceof Error ? error.message : error);
1923
+ process.exit(1);
1924
+ }
1925
+ }
1926
+ });
1927
+ const uploadCommand = defineCommand({
1928
+ meta: {
1929
+ name: "upload",
1930
+ description: "Upload a media file"
1931
+ },
1932
+ args: {
1933
+ file: {
1934
+ type: "positional",
1935
+ description: "Path to the file to upload",
1936
+ required: true
1937
+ },
1938
+ ...connectionArgs,
1939
+ alt: {
1940
+ type: "string",
1941
+ description: "Alt text for the media item"
1942
+ },
1943
+ caption: {
1944
+ type: "string",
1945
+ description: "Caption for the media item"
1946
+ }
1947
+ },
1948
+ async run({ args }) {
1949
+ const client = createClientFromArgs(args);
1950
+ const filename = basename(args.file);
1951
+ consola$1.start(`Uploading ${filename}...`);
1952
+ try {
1953
+ const buffer = await readFile(args.file);
1954
+ const result = await client.mediaUpload(buffer, filename, {
1955
+ alt: args.alt,
1956
+ caption: args.caption
1957
+ });
1958
+ consola$1.success(`Uploaded ${filename}`);
1959
+ output(result, args);
1960
+ } catch (error) {
1961
+ consola$1.error("Failed to upload:", error instanceof Error ? error.message : error);
1962
+ process.exit(1);
1963
+ }
1964
+ }
1965
+ });
1966
+ const getCommand$2 = defineCommand({
1967
+ meta: {
1968
+ name: "get",
1969
+ description: "Get a media item"
1970
+ },
1971
+ args: {
1972
+ id: {
1973
+ type: "positional",
1974
+ description: "Media item ID",
1975
+ required: true
1976
+ },
1977
+ ...connectionArgs
1978
+ },
1979
+ async run({ args }) {
1980
+ const client = createClientFromArgs(args);
1981
+ try {
1982
+ output(await client.mediaGet(args.id), args);
1983
+ } catch (error) {
1984
+ consola$1.error("Failed to get media:", error instanceof Error ? error.message : error);
1985
+ process.exit(1);
1986
+ }
1987
+ }
1988
+ });
1989
+ const deleteCommand$1 = defineCommand({
1990
+ meta: {
1991
+ name: "delete",
1992
+ description: "Delete a media item"
1993
+ },
1994
+ args: {
1995
+ id: {
1996
+ type: "positional",
1997
+ description: "Media item ID",
1998
+ required: true
1999
+ },
2000
+ ...connectionArgs
2001
+ },
2002
+ async run({ args }) {
2003
+ const client = createClientFromArgs(args);
2004
+ try {
2005
+ await client.mediaDelete(args.id);
2006
+ if (args.json) output({ deleted: true }, args);
2007
+ else consola$1.success(`Deleted media item ${args.id}`);
2008
+ } catch (error) {
2009
+ consola$1.error("Failed to delete media:", error instanceof Error ? error.message : error);
2010
+ process.exit(1);
2011
+ }
2012
+ }
2013
+ });
2014
+ const mediaCommand = defineCommand({
2015
+ meta: {
2016
+ name: "media",
2017
+ description: "Manage media items"
2018
+ },
2019
+ subCommands: {
2020
+ list: listCommand$3,
2021
+ upload: uploadCommand,
2022
+ get: getCommand$2,
2023
+ delete: deleteCommand$1
2024
+ }
2025
+ });
2026
+
2027
+ //#endregion
2028
+ //#region src/cli/commands/menu.ts
2029
+ /**
2030
+ * emdash menu
2031
+ *
2032
+ * Manage menus via the EmDash REST API.
2033
+ */
2034
+ const listCommand$2 = defineCommand({
2035
+ meta: {
2036
+ name: "list",
2037
+ description: "List all menus"
2038
+ },
2039
+ args: { ...connectionArgs },
2040
+ async run({ args }) {
2041
+ try {
2042
+ output(await createClientFromArgs(args).menus(), args);
2043
+ } catch (error) {
2044
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
2045
+ process.exit(1);
2046
+ }
2047
+ }
2048
+ });
2049
+ const getCommand$1 = defineCommand({
2050
+ meta: {
2051
+ name: "get",
2052
+ description: "Get a menu with its items"
2053
+ },
2054
+ args: {
2055
+ name: {
2056
+ type: "positional",
2057
+ description: "Menu name",
2058
+ required: true
2059
+ },
2060
+ ...connectionArgs
2061
+ },
2062
+ async run({ args }) {
2063
+ try {
2064
+ output(await createClientFromArgs(args).menu(args.name), args);
2065
+ } catch (error) {
2066
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
2067
+ process.exit(1);
2068
+ }
2069
+ }
2070
+ });
2071
+ const menuCommand = defineCommand({
2072
+ meta: {
2073
+ name: "menu",
2074
+ description: "Manage menus"
2075
+ },
2076
+ subCommands: {
2077
+ list: listCommand$2,
2078
+ get: getCommand$1
2079
+ }
2080
+ });
2081
+
2082
+ //#endregion
2083
+ //#region src/cli/commands/bundle-utils.ts
2084
+ /**
2085
+ * Bundle utility functions
2086
+ *
2087
+ * Shared logic extracted from the bundle command so it can be tested
2088
+ * without the CLI harness and tsdown dependency.
2089
+ */
2090
+ const MAX_BUNDLE_SIZE = 5 * 1024 * 1024;
2091
+ const MAX_SCREENSHOTS = 5;
2092
+ const MAX_SCREENSHOT_WIDTH = 1920;
2093
+ const MAX_SCREENSHOT_HEIGHT = 1080;
2094
+ const ICON_SIZE = 256;
2095
+ /** Matches require("node:xxx") / require("xxx") / import("node:xxx") in bundled output */
2096
+ const NODE_BUILTIN_IMPORT_RE = /(?:import|require)\s*\(?["'](?:node:)?([a-z_]+)["']\)?/g;
2097
+ const LEADING_DOT_SLASH_RE = /^\.\//;
2098
+ const DIST_PREFIX_RE = /^dist\//;
2099
+ const MJS_EXT_RE = /\.m?js$/;
2100
+ const TS_TO_TSX_RE = /\.ts$/;
2101
+ /** Node.js built-in modules that shouldn't appear in sandbox code */
2102
+ const NODE_BUILTINS = new Set([
2103
+ "assert",
2104
+ "buffer",
2105
+ "child_process",
2106
+ "cluster",
2107
+ "crypto",
2108
+ "dgram",
2109
+ "dns",
2110
+ "domain",
2111
+ "events",
2112
+ "fs",
2113
+ "http",
2114
+ "http2",
2115
+ "https",
2116
+ "inspector",
2117
+ "module",
2118
+ "net",
2119
+ "os",
2120
+ "path",
2121
+ "perf_hooks",
2122
+ "process",
2123
+ "punycode",
2124
+ "querystring",
2125
+ "readline",
2126
+ "repl",
2127
+ "stream",
2128
+ "string_decoder",
2129
+ "sys",
2130
+ "timers",
2131
+ "tls",
2132
+ "trace_events",
2133
+ "tty",
2134
+ "url",
2135
+ "util",
2136
+ "v8",
2137
+ "vm",
2138
+ "wasi",
2139
+ "worker_threads",
2140
+ "zlib"
2141
+ ]);
2142
+ async function fileExists$1(path) {
2143
+ try {
2144
+ await access(path);
2145
+ return true;
2146
+ } catch {
2147
+ return false;
2148
+ }
2149
+ }
2150
+ /**
2151
+ * Read image dimensions from a buffer.
2152
+ * Returns [width, height] or null if the format is unrecognized.
2153
+ */
2154
+ function readImageDimensions(buf) {
2155
+ try {
2156
+ const result = imageSize(buf);
2157
+ if (result.width != null && result.height != null) return [result.width, result.height];
2158
+ return null;
2159
+ } catch {
2160
+ return null;
2161
+ }
2162
+ }
2163
+ /**
2164
+ * Extract manifest metadata from a ResolvedPlugin.
2165
+ * Strips functions (hooks, route handlers) and keeps only serializable metadata.
2166
+ */
2167
+ function extractManifest(plugin) {
2168
+ const hooks = [];
2169
+ for (const [name, resolved] of Object.entries(plugin.hooks)) {
2170
+ if (!resolved) continue;
2171
+ if (resolved.exclusive || resolved.priority !== 100 || resolved.timeout !== 5e3) {
2172
+ const entry = { name };
2173
+ if (resolved.exclusive) entry.exclusive = true;
2174
+ if (resolved.priority !== 100) entry.priority = resolved.priority;
2175
+ if (resolved.timeout !== 5e3) entry.timeout = resolved.timeout;
2176
+ hooks.push(entry);
2177
+ } else hooks.push(name);
2178
+ }
2179
+ return {
2180
+ id: plugin.id,
2181
+ version: plugin.version,
2182
+ capabilities: plugin.capabilities,
2183
+ allowedHosts: plugin.allowedHosts,
2184
+ storage: plugin.storage,
2185
+ hooks,
2186
+ routes: Object.keys(plugin.routes),
2187
+ admin: {
2188
+ settingsSchema: plugin.admin.settingsSchema,
2189
+ pages: plugin.admin.pages,
2190
+ widgets: plugin.admin.widgets
2191
+ }
2192
+ };
2193
+ }
2194
+ /**
2195
+ * Scan bundled code for Node.js built-in imports.
2196
+ * Matches require("node:xxx"), require("xxx"), import("node:xxx") — the patterns
2197
+ * that appear in bundled ESM/CJS output (not source-level named imports).
2198
+ * Returns deduplicated array of built-in module names found.
2199
+ */
2200
+ function findNodeBuiltinImports(code) {
2201
+ const found = [];
2202
+ NODE_BUILTIN_IMPORT_RE.lastIndex = 0;
2203
+ let match;
2204
+ while ((match = NODE_BUILTIN_IMPORT_RE.exec(code)) !== null) {
2205
+ const mod = match[1];
2206
+ if (NODE_BUILTINS.has(mod)) found.push(mod);
2207
+ }
2208
+ return [...new Set(found)];
2209
+ }
2210
+ /**
2211
+ * Find a build output file by base name, checking common extensions.
2212
+ * tsdown may output .mjs, .js, or .cjs depending on format and config.
2213
+ */
2214
+ async function findBuildOutput(dir, baseName) {
2215
+ for (const ext of [
2216
+ ".mjs",
2217
+ ".js",
2218
+ ".cjs"
2219
+ ]) {
2220
+ const candidate = join(dir, `${baseName}${ext}`);
2221
+ if (await fileExists$1(candidate)) return candidate;
2222
+ }
2223
+ }
2224
+ /**
2225
+ * Resolve a dist/built path back to its source .ts/.tsx equivalent.
2226
+ * E.g., "./dist/index.mjs" → "src/index.ts"
2227
+ */
2228
+ async function resolveSourceEntry(pluginDir, distPath) {
2229
+ const cleaned = distPath.replace(LEADING_DOT_SLASH_RE, "");
2230
+ const direct = resolve(pluginDir, cleaned);
2231
+ if (await fileExists$1(direct)) return direct;
2232
+ const srcPath = cleaned.replace(DIST_PREFIX_RE, "src/").replace(MJS_EXT_RE, ".ts");
2233
+ const srcFull = resolve(pluginDir, srcPath);
2234
+ if (await fileExists$1(srcFull)) return srcFull;
2235
+ const tsxFull = resolve(pluginDir, srcPath.replace(TS_TO_TSX_RE, ".tsx"));
2236
+ if (await fileExists$1(tsxFull)) return tsxFull;
2237
+ }
2238
+ /**
2239
+ * Recursively calculate the total size of all files in a directory.
2240
+ */
2241
+ async function calculateDirectorySize(dir) {
2242
+ let total = 0;
2243
+ const items = await readdir(dir, { withFileTypes: true });
2244
+ for (const item of items) {
2245
+ const fullPath = join(dir, item.name);
2246
+ if (item.isFile()) {
2247
+ const s = await stat(fullPath);
2248
+ total += s.size;
2249
+ } else if (item.isDirectory()) total += await calculateDirectorySize(fullPath);
2250
+ }
2251
+ return total;
2252
+ }
2253
+ /**
2254
+ * Create a gzipped tarball from a directory.
2255
+ */
2256
+ async function createTarball(sourceDir, outputPath) {
2257
+ const { createGzip } = await import("node:zlib");
2258
+ await pipeline(packTar(sourceDir), createGzip({ level: 9 }), createWriteStream(outputPath));
2259
+ }
2260
+
2261
+ //#endregion
2262
+ //#region src/cli/commands/bundle.ts
2263
+ /**
2264
+ * emdash plugin bundle
2265
+ *
2266
+ * Produces a publishable plugin tarball from a plugin source directory.
2267
+ *
2268
+ * Steps:
2269
+ * 1. Resolve plugin entrypoint (finds definePlugin() export)
2270
+ * 2. Bundle backend code with tsdown → backend.js (single ES module, tree-shaken)
2271
+ * 3. Bundle admin code if present → admin.js
2272
+ * 4. Extract manifest from definePlugin() → manifest.json
2273
+ * 5. Collect assets (README.md, icon.png, screenshots/)
2274
+ * 6. Validate bundle (manifest schema, size limits, no Node.js builtins)
2275
+ * 7. Create tarball ({id}-{version}.tar.gz)
2276
+ */
2277
+ var bundle_exports = /* @__PURE__ */ __exportAll({ bundleCommand: () => bundleCommand });
2278
+ const TS_EXT_RE = /\.tsx?$/;
2279
+ const SLASH_RE = /\//g;
2280
+ const LEADING_AT_RE = /^@/;
2281
+ const emdash_SCOPE_RE = /^@emdash-cms\//;
2282
+ const bundleCommand = defineCommand({
2283
+ meta: {
2284
+ name: "bundle",
2285
+ description: "Bundle a plugin for marketplace distribution"
2286
+ },
2287
+ args: {
2288
+ dir: {
2289
+ type: "string",
2290
+ description: "Plugin directory (default: current directory)",
2291
+ default: process.cwd()
2292
+ },
2293
+ outDir: {
2294
+ type: "string",
2295
+ alias: "o",
2296
+ description: "Output directory for the tarball (default: ./dist)",
2297
+ default: "dist"
2298
+ },
2299
+ validateOnly: {
2300
+ type: "boolean",
2301
+ description: "Run validation only, skip tarball creation",
2302
+ default: false
2303
+ }
2304
+ },
2305
+ async run({ args }) {
2306
+ const pluginDir = resolve(args.dir);
2307
+ const outDir = resolve(pluginDir, args.outDir);
2308
+ const validateOnly = args.validateOnly;
2309
+ consola.start(validateOnly ? "Validating plugin..." : "Bundling plugin...");
2310
+ const pkgPath = join(pluginDir, "package.json");
2311
+ if (!await fileExists$1(pkgPath)) {
2312
+ consola.error("No package.json found in", pluginDir);
2313
+ process.exit(1);
2314
+ }
2315
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
2316
+ let backendEntry;
2317
+ let adminEntry;
2318
+ if (pkg.exports) {
2319
+ const sandboxExport = pkg.exports["./sandbox"];
2320
+ if (typeof sandboxExport === "string") backendEntry = await resolveSourceEntry(pluginDir, sandboxExport);
2321
+ else if (sandboxExport && typeof sandboxExport === "object" && "import" in sandboxExport) backendEntry = await resolveSourceEntry(pluginDir, sandboxExport.import);
2322
+ const adminExport = pkg.exports["./admin"];
2323
+ if (typeof adminExport === "string") adminEntry = await resolveSourceEntry(pluginDir, adminExport);
2324
+ else if (adminExport && typeof adminExport === "object" && "import" in adminExport) adminEntry = await resolveSourceEntry(pluginDir, adminExport.import);
2325
+ }
2326
+ if (!backendEntry) {
2327
+ const defaultSandbox = join(pluginDir, "src/sandbox-entry.ts");
2328
+ if (await fileExists$1(defaultSandbox)) backendEntry = defaultSandbox;
2329
+ }
2330
+ let mainEntry;
2331
+ if (pkg.exports?.["."] !== void 0) {
2332
+ const mainExport = pkg.exports["."];
2333
+ if (typeof mainExport === "string") mainEntry = await resolveSourceEntry(pluginDir, mainExport);
2334
+ else if (mainExport && typeof mainExport === "object" && "import" in mainExport) mainEntry = await resolveSourceEntry(pluginDir, mainExport.import);
2335
+ }
2336
+ if (!mainEntry && pkg.main) mainEntry = await resolveSourceEntry(pluginDir, pkg.main);
2337
+ if (!mainEntry) {
2338
+ const defaultMain = join(pluginDir, "src/index.ts");
2339
+ if (await fileExists$1(defaultMain)) mainEntry = defaultMain;
2340
+ }
2341
+ if (!mainEntry) {
2342
+ consola.error("Cannot find plugin entrypoint. Expected src/index.ts or main/exports in package.json");
2343
+ process.exit(1);
2344
+ }
2345
+ consola.info(`Main entry: ${mainEntry}`);
2346
+ if (backendEntry) consola.info(`Backend entry: ${backendEntry}`);
2347
+ if (adminEntry) consola.info(`Admin entry: ${adminEntry}`);
2348
+ consola.start("Extracting plugin manifest...");
2349
+ const { build } = await import("tsdown");
2350
+ const tmpDir = join(pluginDir, ".emdash-bundle-tmp");
2351
+ try {
2352
+ await mkdir(tmpDir, { recursive: true });
2353
+ const mainOutDir = join(tmpDir, "main");
2354
+ await build({
2355
+ config: false,
2356
+ entry: [mainEntry],
2357
+ format: "esm",
2358
+ outDir: mainOutDir,
2359
+ dts: false,
2360
+ platform: "node",
2361
+ external: ["emdash", emdash_SCOPE_RE]
2362
+ });
2363
+ const pluginNodeModules = join(pluginDir, "node_modules");
2364
+ const tmpNodeModules = join(mainOutDir, "node_modules");
2365
+ if (await fileExists$1(pluginNodeModules)) await symlink(pluginNodeModules, tmpNodeModules, "junction");
2366
+ const mainOutputPath = await findBuildOutput(mainOutDir, basename(mainEntry).replace(TS_EXT_RE, ""));
2367
+ if (!mainOutputPath) {
2368
+ consola.error("Failed to build main entry — no output found in", mainOutDir);
2369
+ process.exit(1);
2370
+ }
2371
+ const pluginModule = await import(mainOutputPath);
2372
+ let resolvedPlugin;
2373
+ if (typeof pluginModule.createPlugin === "function") resolvedPlugin = pluginModule.createPlugin();
2374
+ else if (typeof pluginModule.default === "function") resolvedPlugin = pluginModule.default();
2375
+ else if (typeof pluginModule.default === "object" && pluginModule.default !== null) {
2376
+ const defaultExport = pluginModule.default;
2377
+ if ("id" in defaultExport && "version" in defaultExport) resolvedPlugin = defaultExport;
2378
+ }
2379
+ if (!resolvedPlugin) for (const [key, value] of Object.entries(pluginModule)) {
2380
+ if (key === "default" || typeof value !== "function") continue;
2381
+ try {
2382
+ const result = value();
2383
+ if (result && typeof result === "object" && "id" in result && "version" in result) {
2384
+ resolvedPlugin = {
2385
+ id: result.id,
2386
+ version: result.version,
2387
+ capabilities: result.capabilities ?? [],
2388
+ allowedHosts: result.allowedHosts ?? [],
2389
+ storage: result.storage ?? {},
2390
+ hooks: {},
2391
+ routes: {},
2392
+ admin: {
2393
+ pages: result.adminPages,
2394
+ widgets: result.adminWidgets
2395
+ }
2396
+ };
2397
+ if (backendEntry) {
2398
+ const backendProbeDir = join(tmpDir, "backend-probe");
2399
+ const probeShimDir = join(tmpDir, "probe-shims");
2400
+ await mkdir(probeShimDir, { recursive: true });
2401
+ await writeFile(join(probeShimDir, "emdash.mjs"), "export const definePlugin = (d) => d;\n");
2402
+ await build({
2403
+ config: false,
2404
+ entry: [backendEntry],
2405
+ format: "esm",
2406
+ outDir: backendProbeDir,
2407
+ dts: false,
2408
+ platform: "neutral",
2409
+ external: [],
2410
+ alias: { emdash: join(probeShimDir, "emdash.mjs") },
2411
+ treeshake: true
2412
+ });
2413
+ const backendProbePath = await findBuildOutput(backendProbeDir, basename(backendEntry).replace(TS_EXT_RE, ""));
2414
+ if (backendProbePath) {
2415
+ const standardDef = (await import(backendProbePath)).default ?? {};
2416
+ const hooks = standardDef.hooks;
2417
+ const routes = standardDef.routes;
2418
+ if (hooks) for (const hookName of Object.keys(hooks)) {
2419
+ const hookEntry = hooks[hookName];
2420
+ const isConfig = typeof hookEntry === "object" && hookEntry !== null && "handler" in hookEntry;
2421
+ const config = isConfig ? hookEntry : {};
2422
+ resolvedPlugin.hooks[hookName] = {
2423
+ handler: isConfig ? hookEntry.handler : hookEntry,
2424
+ priority: config.priority ?? 100,
2425
+ timeout: config.timeout ?? 5e3,
2426
+ dependencies: config.dependencies ?? [],
2427
+ errorPolicy: config.errorPolicy ?? "abort",
2428
+ exclusive: config.exclusive ?? false,
2429
+ pluginId: result.id
2430
+ };
2431
+ }
2432
+ if (routes) for (const [name, route] of Object.entries(routes)) {
2433
+ const routeObj = route;
2434
+ resolvedPlugin.routes[name] = {
2435
+ handler: routeObj.handler,
2436
+ public: routeObj.public
2437
+ };
2438
+ }
2439
+ }
2440
+ }
2441
+ break;
2442
+ }
2443
+ } catch {}
2444
+ }
2445
+ if (!resolvedPlugin?.id || !resolvedPlugin?.version) {
2446
+ consola.error("Could not extract plugin definition. Expected one of:\n - createPlugin() export (native format)\n - Descriptor factory function returning { id, version, ... } (standard format)");
2447
+ process.exit(1);
2448
+ }
2449
+ const manifest = extractManifest(resolvedPlugin);
2450
+ if (resolvedPlugin.admin?.entry) {
2451
+ consola.error("Plugin declares adminEntry — React admin components require native/trusted mode. Use Block Kit for sandboxed admin pages, or remove adminEntry.");
2452
+ process.exit(1);
2453
+ }
2454
+ if (resolvedPlugin.admin?.portableTextBlocks && resolvedPlugin.admin.portableTextBlocks.length > 0) {
2455
+ consola.error("Plugin declares portableTextBlocks — these require native/trusted mode and cannot be bundled for the marketplace.");
2456
+ process.exit(1);
2457
+ }
2458
+ consola.success(`Plugin: ${manifest.id}@${manifest.version}`);
2459
+ consola.info(` Capabilities: ${manifest.capabilities.length > 0 ? manifest.capabilities.join(", ") : "(none)"}`);
2460
+ consola.info(` Hooks: ${manifest.hooks.length > 0 ? manifest.hooks.map((h) => typeof h === "string" ? h : h.name).join(", ") : "(none)"}`);
2461
+ consola.info(` Routes: ${manifest.routes.length > 0 ? manifest.routes.map((r) => typeof r === "string" ? r : r.name).join(", ") : "(none)"}`);
2462
+ const bundleDir = join(tmpDir, "bundle");
2463
+ await mkdir(bundleDir, { recursive: true });
2464
+ if (backendEntry) {
2465
+ consola.start("Bundling backend...");
2466
+ const shimDir = join(tmpDir, "shims");
2467
+ await mkdir(shimDir, { recursive: true });
2468
+ await writeFile(join(shimDir, "emdash.mjs"), "export const definePlugin = (d) => d;\n");
2469
+ await build({
2470
+ config: false,
2471
+ entry: [backendEntry],
2472
+ format: "esm",
2473
+ outDir: join(tmpDir, "backend"),
2474
+ dts: false,
2475
+ platform: "neutral",
2476
+ external: [],
2477
+ alias: { emdash: join(shimDir, "emdash.mjs") },
2478
+ minify: true,
2479
+ treeshake: true
2480
+ });
2481
+ const backendBaseName = basename(backendEntry).replace(TS_EXT_RE, "");
2482
+ const backendOutputPath = await findBuildOutput(join(tmpDir, "backend"), backendBaseName);
2483
+ if (backendOutputPath) {
2484
+ await copyFile(backendOutputPath, join(bundleDir, "backend.js"));
2485
+ consola.success("Built backend.js");
2486
+ } else {
2487
+ consola.error("Backend build produced no output");
2488
+ process.exit(1);
2489
+ }
2490
+ } else {
2491
+ consola.warn("No sandbox entry found — bundle will have no backend.js");
2492
+ consola.warn(" Add a \"sandbox-entry.ts\" in src/ or a \"./sandbox\" export in package.json");
2493
+ }
2494
+ if (adminEntry) {
2495
+ consola.start("Bundling admin...");
2496
+ await build({
2497
+ config: false,
2498
+ entry: [adminEntry],
2499
+ format: "esm",
2500
+ outDir: join(tmpDir, "admin"),
2501
+ dts: false,
2502
+ platform: "neutral",
2503
+ external: [],
2504
+ minify: true,
2505
+ treeshake: true
2506
+ });
2507
+ const adminBaseName = basename(adminEntry).replace(TS_EXT_RE, "");
2508
+ const adminOutputPath = await findBuildOutput(join(tmpDir, "admin"), adminBaseName);
2509
+ if (adminOutputPath) {
2510
+ await copyFile(adminOutputPath, join(bundleDir, "admin.js"));
2511
+ consola.success("Built admin.js");
2512
+ }
2513
+ }
2514
+ await writeFile(join(bundleDir, "manifest.json"), JSON.stringify(manifest, null, 2));
2515
+ consola.start("Collecting assets...");
2516
+ const readmePath = join(pluginDir, "README.md");
2517
+ if (await fileExists$1(readmePath)) {
2518
+ await copyFile(readmePath, join(bundleDir, "README.md"));
2519
+ consola.success("Included README.md");
2520
+ }
2521
+ const iconPath = join(pluginDir, "icon.png");
2522
+ if (await fileExists$1(iconPath)) {
2523
+ const dims = readImageDimensions(await readFile(iconPath));
2524
+ if (!dims) consola.warn("icon.png is not a valid PNG — skipping");
2525
+ else if (dims[0] !== ICON_SIZE || dims[1] !== ICON_SIZE) {
2526
+ consola.warn(`icon.png is ${dims[0]}x${dims[1]}, expected ${ICON_SIZE}x${ICON_SIZE} — including anyway`);
2527
+ await copyFile(iconPath, join(bundleDir, "icon.png"));
2528
+ } else {
2529
+ await copyFile(iconPath, join(bundleDir, "icon.png"));
2530
+ consola.success("Included icon.png");
2531
+ }
2532
+ }
2533
+ const screenshotsDir = join(pluginDir, "screenshots");
2534
+ if (await fileExists$1(screenshotsDir)) {
2535
+ const screenshotFiles = (await readdir(screenshotsDir)).filter((f) => {
2536
+ const ext = extname(f).toLowerCase();
2537
+ return ext === ".png" || ext === ".jpg" || ext === ".jpeg";
2538
+ }).toSorted().slice(0, MAX_SCREENSHOTS);
2539
+ if (screenshotFiles.length > 0) {
2540
+ await mkdir(join(bundleDir, "screenshots"), { recursive: true });
2541
+ for (const file of screenshotFiles) {
2542
+ const filePath = join(screenshotsDir, file);
2543
+ const dims = readImageDimensions(await readFile(filePath));
2544
+ if (!dims) {
2545
+ consola.warn(`screenshots/${file} — cannot read dimensions, skipping`);
2546
+ continue;
2547
+ }
2548
+ if (dims[0] > MAX_SCREENSHOT_WIDTH || dims[1] > MAX_SCREENSHOT_HEIGHT) consola.warn(`screenshots/${file} is ${dims[0]}x${dims[1]}, max ${MAX_SCREENSHOT_WIDTH}x${MAX_SCREENSHOT_HEIGHT} — including anyway`);
2549
+ await copyFile(filePath, join(bundleDir, "screenshots", file));
2550
+ }
2551
+ consola.success(`Included ${screenshotFiles.length} screenshot(s)`);
2552
+ }
2553
+ }
2554
+ consola.start("Validating bundle...");
2555
+ let hasErrors = false;
2556
+ const backendPath = join(bundleDir, "backend.js");
2557
+ if (await fileExists$1(backendPath)) {
2558
+ const builtins = findNodeBuiltinImports(await readFile(backendPath, "utf-8"));
2559
+ if (builtins.length > 0) {
2560
+ consola.error(`backend.js imports Node.js built-in modules: ${builtins.join(", ")}`);
2561
+ consola.error("Sandboxed plugins cannot use Node.js APIs");
2562
+ hasErrors = true;
2563
+ }
2564
+ }
2565
+ if (manifest.capabilities.includes("network:fetch:any")) consola.warn("Plugin declares unrestricted network access (network:fetch:any) — it can make requests to any host");
2566
+ else if (manifest.capabilities.includes("network:fetch") && manifest.allowedHosts.length === 0) consola.warn("Plugin declares network:fetch capability but no allowedHosts — all fetch requests will be blocked");
2567
+ if (resolvedPlugin.admin?.portableTextBlocks && resolvedPlugin.admin.portableTextBlocks.length > 0) consola.warn("Plugin declares portableTextBlocks — these require trusted mode and will be ignored in sandboxed plugins");
2568
+ if (resolvedPlugin.admin?.entry) consola.warn("Plugin declares admin.entry — custom React components require trusted mode. Use Block Kit for sandboxed admin pages");
2569
+ if (resolvedPlugin.hooks["page:fragments"]) consola.warn("Plugin declares page:fragments hook — this is trusted-only and will not work in sandboxed mode");
2570
+ const hasAdminPages = (manifest.admin?.pages?.length ?? 0) > 0;
2571
+ const hasAdminWidgets = (manifest.admin?.widgets?.length ?? 0) > 0;
2572
+ if (hasAdminPages || hasAdminWidgets) {
2573
+ if (!manifest.routes.map((r) => typeof r === "string" ? r : r.name).includes("admin")) {
2574
+ consola.error(`Plugin declares ${hasAdminPages ? "adminPages" : ""}${hasAdminPages && hasAdminWidgets ? " and " : ""}${hasAdminWidgets ? "adminWidgets" : ""} but the sandbox entry has no "admin" route. Add an admin route handler to serve Block Kit pages.`);
2575
+ hasErrors = true;
2576
+ }
2577
+ }
2578
+ const totalSize = await calculateDirectorySize(bundleDir);
2579
+ if (totalSize > MAX_BUNDLE_SIZE) {
2580
+ const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
2581
+ consola.error(`Bundle size ${sizeMB}MB exceeds maximum of 5MB`);
2582
+ hasErrors = true;
2583
+ } else {
2584
+ const sizeKB = (totalSize / 1024).toFixed(1);
2585
+ consola.info(`Bundle size: ${sizeKB}KB`);
2586
+ }
2587
+ if (hasErrors) {
2588
+ consola.error("Bundle validation failed");
2589
+ process.exit(1);
2590
+ }
2591
+ consola.success("Validation passed");
2592
+ if (validateOnly) return;
2593
+ await mkdir(outDir, { recursive: true });
2594
+ const tarballName = `${manifest.id.replace(SLASH_RE, "-").replace(LEADING_AT_RE, "")}-${manifest.version}.tar.gz`;
2595
+ const tarballPath = join(outDir, tarballName);
2596
+ consola.start("Creating tarball...");
2597
+ await createTarball(bundleDir, tarballPath);
2598
+ const tarballSizeKB = ((await stat(tarballPath)).size / 1024).toFixed(1);
2599
+ const tarballBuf = await readFile(tarballPath);
2600
+ const checksum = createHash("sha256").update(tarballBuf).digest("hex");
2601
+ consola.success(`Created ${tarballName} (${tarballSizeKB}KB)`);
2602
+ consola.info(` SHA-256: ${checksum}`);
2603
+ consola.info(` Path: ${tarballPath}`);
2604
+ } finally {
2605
+ if (tmpDir.endsWith(".emdash-bundle-tmp")) await rm(tmpDir, {
2606
+ recursive: true,
2607
+ force: true
2608
+ });
2609
+ }
2610
+ }
2611
+ });
2612
+
2613
+ //#endregion
2614
+ //#region src/cli/commands/plugin-init.ts
2615
+ /**
2616
+ * emdash plugin init
2617
+ *
2618
+ * Scaffold a new EmDash plugin. Generates the standard-format boilerplate:
2619
+ * src/index.ts -- descriptor factory
2620
+ * src/sandbox-entry.ts -- definePlugin({ hooks, routes })
2621
+ * package.json
2622
+ * tsconfig.json
2623
+ *
2624
+ * Use --native to generate native-format boilerplate instead (createPlugin + React admin).
2625
+ *
2626
+ */
2627
+ const SLUG_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2628
+ const SCOPE_RE = /^@[^/]+\//;
2629
+ const pluginInitCommand = defineCommand({
2630
+ meta: {
2631
+ name: "init",
2632
+ description: "Scaffold a new plugin"
2633
+ },
2634
+ args: {
2635
+ dir: {
2636
+ type: "string",
2637
+ description: "Directory to create the plugin in (default: current directory)",
2638
+ default: "."
2639
+ },
2640
+ name: {
2641
+ type: "string",
2642
+ description: "Plugin name/id (e.g. my-plugin or @org/my-plugin)"
2643
+ },
2644
+ native: {
2645
+ type: "boolean",
2646
+ description: "Generate native-format plugin (createPlugin + React admin)",
2647
+ default: false
2648
+ }
2649
+ },
2650
+ async run({ args }) {
2651
+ const targetDir = resolve(args.dir);
2652
+ const isNative = args.native;
2653
+ let pluginName = args.name || basename(targetDir);
2654
+ if (!pluginName || pluginName === ".") pluginName = basename(resolve("."));
2655
+ const slug = pluginName.replace(SCOPE_RE, "");
2656
+ if (!SLUG_RE.test(slug)) {
2657
+ consola.error(`Invalid plugin name "${pluginName}". Use lowercase letters, numbers, and hyphens (e.g. my-plugin).`);
2658
+ process.exit(1);
2659
+ }
2660
+ const srcDir = join(targetDir, "src");
2661
+ if (await fileExists$1(join(targetDir, "package.json"))) {
2662
+ consola.error(`package.json already exists in ${targetDir}`);
2663
+ process.exit(1);
2664
+ }
2665
+ consola.start(`Scaffolding ${isNative ? "native" : "standard"} plugin: ${pluginName}`);
2666
+ await mkdir(srcDir, { recursive: true });
2667
+ if (isNative) await scaffoldNative(targetDir, srcDir, pluginName, slug);
2668
+ else await scaffoldStandard(targetDir, srcDir, pluginName, slug);
2669
+ consola.success(`Plugin scaffolded in ${targetDir}`);
2670
+ consola.info("Next steps:");
2671
+ if (args.dir !== ".") consola.info(` 1. cd ${args.dir}`);
2672
+ consola.info(` ${args.dir !== "." ? "2" : "1"}. pnpm install`);
2673
+ if (isNative) consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/index.ts to add hooks and routes`);
2674
+ else consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/sandbox-entry.ts to add hooks and routes`);
2675
+ consola.info(` ${args.dir !== "." ? "4" : "3"}. emdash plugin validate --dir .`);
2676
+ }
2677
+ });
2678
+ async function scaffoldStandard(targetDir, srcDir, pluginName, slug) {
2679
+ const fnName = slug.split("-").map((s, i) => i === 0 ? s : s[0].toUpperCase() + s.slice(1)).join("");
2680
+ await writeFile(join(targetDir, "package.json"), JSON.stringify({
2681
+ name: pluginName,
2682
+ version: "0.1.0",
2683
+ type: "module",
2684
+ exports: {
2685
+ ".": "./src/index.ts",
2686
+ "./sandbox": "./src/sandbox-entry.ts"
2687
+ },
2688
+ files: ["src"],
2689
+ peerDependencies: { emdash: "*" }
2690
+ }, null, " ") + "\n");
2691
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
2692
+ compilerOptions: {
2693
+ target: "ES2022",
2694
+ module: "preserve",
2695
+ moduleResolution: "bundler",
2696
+ strict: true,
2697
+ esModuleInterop: true,
2698
+ declaration: true,
2699
+ outDir: "./dist",
2700
+ rootDir: "./src"
2701
+ },
2702
+ include: ["src/**/*"],
2703
+ exclude: ["node_modules", "dist"]
2704
+ }, null, " ") + "\n");
2705
+ await writeFile(join(srcDir, "index.ts"), `import type { PluginDescriptor } from "emdash";
2706
+
2707
+ export function ${fnName}Plugin(): PluginDescriptor {
2708
+ \treturn {
2709
+ \t\tid: "${pluginName}",
2710
+ \t\tversion: "0.1.0",
2711
+ \t\tformat: "standard",
2712
+ \t\tentrypoint: "${pluginName}/sandbox",
2713
+ \t\tcapabilities: [],
2714
+ \t};
2715
+ }
2716
+ `);
2717
+ await writeFile(join(srcDir, "sandbox-entry.ts"), `import { definePlugin } from "emdash";
2718
+ import type { PluginContext } from "emdash";
2719
+
2720
+ export default definePlugin({
2721
+ \thooks: {
2722
+ \t\t"content:afterSave": {
2723
+ \t\t\thandler: async (event: any, ctx: PluginContext) => {
2724
+ \t\t\t\tctx.log.info("Content saved", {
2725
+ \t\t\t\t\tcollection: event.collection,
2726
+ \t\t\t\t\tid: event.content.id,
2727
+ \t\t\t\t});
2728
+ \t\t\t},
2729
+ \t\t},
2730
+ \t},
2731
+ });
2732
+ `);
2733
+ }
2734
+ async function scaffoldNative(targetDir, srcDir, pluginName, slug) {
2735
+ const fnName = slug.split("-").map((s, i) => i === 0 ? s : s[0].toUpperCase() + s.slice(1)).join("");
2736
+ await writeFile(join(targetDir, "package.json"), JSON.stringify({
2737
+ name: pluginName,
2738
+ version: "0.1.0",
2739
+ type: "module",
2740
+ exports: { ".": "./src/index.ts" },
2741
+ files: ["src"],
2742
+ peerDependencies: { emdash: "*" }
2743
+ }, null, " ") + "\n");
2744
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
2745
+ compilerOptions: {
2746
+ target: "ES2022",
2747
+ module: "preserve",
2748
+ moduleResolution: "bundler",
2749
+ strict: true,
2750
+ esModuleInterop: true,
2751
+ declaration: true,
2752
+ outDir: "./dist",
2753
+ rootDir: "./src"
2754
+ },
2755
+ include: ["src/**/*"],
2756
+ exclude: ["node_modules", "dist"]
2757
+ }, null, " ") + "\n");
2758
+ await writeFile(join(srcDir, "index.ts"), `import { definePlugin } from "emdash";
2759
+ import type { PluginDescriptor } from "emdash";
2760
+
2761
+ export function ${fnName}Plugin(): PluginDescriptor {
2762
+ \treturn {
2763
+ \t\tid: "${pluginName}",
2764
+ \t\tversion: "0.1.0",
2765
+ \t\tformat: "native",
2766
+ \t\tentrypoint: "${pluginName}",
2767
+ \t\toptions: {},
2768
+ \t};
2769
+ }
2770
+
2771
+ export function createPlugin() {
2772
+ \treturn definePlugin({
2773
+ \t\tid: "${pluginName}",
2774
+ \t\tversion: "0.1.0",
2775
+
2776
+ \t\thooks: {
2777
+ \t\t\t"content:afterSave": async (event, ctx) => {
2778
+ \t\t\t\tctx.log.info("Content saved", {
2779
+ \t\t\t\t\tcollection: event.collection,
2780
+ \t\t\t\t\tid: event.content.id,
2781
+ \t\t\t\t});
2782
+ \t\t\t},
2783
+ \t\t},
2784
+ \t});
2785
+ }
2786
+
2787
+ export default createPlugin;
2788
+ `);
2789
+ }
2790
+
2791
+ //#endregion
2792
+ //#region src/cli/commands/plugin-validate.ts
2793
+ /**
2794
+ * emdash plugin validate
2795
+ *
2796
+ * Runs bundle validation without producing a tarball.
2797
+ * Thin wrapper around `emdash plugin bundle --validate-only`.
2798
+ *
2799
+ */
2800
+ const pluginValidateCommand = defineCommand({
2801
+ meta: {
2802
+ name: "validate",
2803
+ description: "Validate a plugin without producing a tarball (same checks as bundle)"
2804
+ },
2805
+ args: { dir: {
2806
+ type: "string",
2807
+ description: "Plugin directory (default: current directory)",
2808
+ default: "."
2809
+ } },
2810
+ async run({ args }) {
2811
+ await runCommand(bundleCommand, { rawArgs: [
2812
+ "--dir",
2813
+ args.dir,
2814
+ "--validateOnly"
2815
+ ] });
2816
+ }
2817
+ });
2818
+
2819
+ //#endregion
2820
+ //#region src/cli/commands/publish.ts
2821
+ /**
2822
+ * emdash plugin publish
2823
+ *
2824
+ * Publishes a plugin tarball to the EmDash Marketplace.
2825
+ *
2826
+ * Flow:
2827
+ * 1. Resolve tarball (from --tarball path, or build via `emdash plugin bundle`)
2828
+ * 2. Read manifest.json from tarball to show summary
2829
+ * 3. Authenticate (stored credential or GitHub device flow)
2830
+ * 4. Pre-publish validation (check plugin exists, version not published)
2831
+ * 5. Upload via multipart POST
2832
+ * 6. Display audit results
2833
+ */
2834
+ const DEFAULT_REGISTRY = "https://marketplace.emdashcms.com";
2835
+ /**
2836
+ * Authenticate with the marketplace via GitHub Device Flow.
2837
+ * Returns the marketplace JWT and author info.
2838
+ */
2839
+ async function authenticateViaDeviceFlow(registryUrl) {
2840
+ consola.start("Fetching auth configuration...");
2841
+ const discoveryRes = await fetch(new URL("/api/v1/auth/discovery", registryUrl));
2842
+ if (!discoveryRes.ok) throw new Error(`Marketplace unreachable: ${discoveryRes.status} ${discoveryRes.statusText}`);
2843
+ const discovery = await discoveryRes.json();
2844
+ const deviceRes = await fetch(discovery.github.deviceAuthorizationEndpoint, {
2845
+ method: "POST",
2846
+ headers: {
2847
+ "Content-Type": "application/json",
2848
+ Accept: "application/json"
2849
+ },
2850
+ body: JSON.stringify({
2851
+ client_id: discovery.github.clientId,
2852
+ scope: "read:user user:email"
2853
+ })
2854
+ });
2855
+ if (!deviceRes.ok) throw new Error(`GitHub device flow failed: ${deviceRes.status}`);
2856
+ const deviceCode = await deviceRes.json();
2857
+ console.log();
2858
+ consola.info("Open your browser to:");
2859
+ console.log(` ${pc.cyan(pc.bold(deviceCode.verification_uri))}`);
2860
+ console.log();
2861
+ consola.info(`Enter code: ${pc.yellow(pc.bold(deviceCode.user_code))}`);
2862
+ console.log();
2863
+ try {
2864
+ const { execFile } = await import("node:child_process");
2865
+ if (process.platform === "darwin") execFile("open", [deviceCode.verification_uri]);
2866
+ else if (process.platform === "win32") execFile("cmd", [
2867
+ "/c",
2868
+ "start",
2869
+ "",
2870
+ deviceCode.verification_uri
2871
+ ]);
2872
+ else execFile("xdg-open", [deviceCode.verification_uri]);
2873
+ } catch {}
2874
+ consola.start("Waiting for authorization...");
2875
+ const githubToken = await pollGitHubDeviceFlow(discovery.github.tokenEndpoint, discovery.github.clientId, deviceCode.device_code, deviceCode.interval, deviceCode.expires_in);
2876
+ consola.start("Authenticating with marketplace...");
2877
+ const deviceTokenUrl = new URL(discovery.marketplace.deviceTokenEndpoint, registryUrl);
2878
+ const authRes = await fetch(deviceTokenUrl, {
2879
+ method: "POST",
2880
+ headers: { "Content-Type": "application/json" },
2881
+ body: JSON.stringify({ access_token: githubToken })
2882
+ });
2883
+ if (!authRes.ok) {
2884
+ const body = await authRes.json().catch(() => ({}));
2885
+ throw new Error(`Marketplace auth failed: ${body.error ?? authRes.statusText}`);
2886
+ }
2887
+ return await authRes.json();
2888
+ }
2889
+ async function pollGitHubDeviceFlow(tokenEndpoint, clientId, deviceCode, interval, expiresIn) {
2890
+ const deadline = Date.now() + expiresIn * 1e3;
2891
+ let currentInterval = interval;
2892
+ while (Date.now() < deadline) {
2893
+ await new Promise((r) => setTimeout(r, currentInterval * 1e3));
2894
+ const body = await (await fetch(tokenEndpoint, {
2895
+ method: "POST",
2896
+ headers: {
2897
+ "Content-Type": "application/json",
2898
+ Accept: "application/json"
2899
+ },
2900
+ body: JSON.stringify({
2901
+ client_id: clientId,
2902
+ device_code: deviceCode,
2903
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2904
+ })
2905
+ })).json();
2906
+ if (body.access_token) return body.access_token;
2907
+ if (body.error === "authorization_pending") continue;
2908
+ if (body.error === "slow_down") {
2909
+ currentInterval = body.interval ?? currentInterval + 5;
2910
+ continue;
2911
+ }
2912
+ if (body.error === "expired_token") throw new Error("Device code expired. Please try again.");
2913
+ if (body.error === "access_denied") throw new Error("Authorization was denied.");
2914
+ throw new Error(`GitHub token exchange failed: ${body.error ?? "unknown error"}`);
2915
+ }
2916
+ throw new Error("Device code expired (timeout). Please try again.");
2917
+ }
2918
+ const manifestSummarySchema = pluginManifestSchema.pick({
2919
+ id: true,
2920
+ version: true,
2921
+ capabilities: true,
2922
+ allowedHosts: true
2923
+ });
2924
+ /**
2925
+ * Read manifest.json from a tarball without fully extracting it.
2926
+ */
2927
+ async function readManifestFromTarball(tarballPath) {
2928
+ const data = await readFile(tarballPath);
2929
+ const manifest = (await unpackTar(new ReadableStream({ start(controller) {
2930
+ controller.enqueue(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
2931
+ controller.close();
2932
+ } }).pipeThrough(createGzipDecoder()), { filter: (header) => header.name === "manifest.json" })).find((e) => e.header.name === "manifest.json");
2933
+ if (!manifest?.data) throw new Error("Tarball does not contain manifest.json");
2934
+ const content = new TextDecoder().decode(manifest.data);
2935
+ const parsed = JSON.parse(content);
2936
+ const result = manifestSummarySchema.safeParse(parsed);
2937
+ if (!result.success) throw new Error(`Invalid manifest.json: ${result.error.message}`);
2938
+ return result.data;
2939
+ }
2940
+ const POLL_INTERVAL_MS = 3e3;
2941
+ const POLL_TIMEOUT_MS = 12e4;
2942
+ /**
2943
+ * Poll the version endpoint until status leaves "pending" or timeout.
2944
+ * Returns the final version data, or null on timeout.
2945
+ */
2946
+ async function pollVersionStatus(versionUrl, token) {
2947
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
2948
+ while (Date.now() < deadline) {
2949
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
2950
+ try {
2951
+ const res = await fetch(versionUrl, { headers: { Authorization: `Bearer ${token}` } });
2952
+ if (!res.ok) continue;
2953
+ const data = await res.json();
2954
+ if (data.status !== "pending") return data;
2955
+ } catch {}
2956
+ }
2957
+ return null;
2958
+ }
2959
+ function displayAuditResults(version) {
2960
+ const statusColor = version.status === "published" ? pc.green : version.status === "flagged" ? pc.yellow : pc.red;
2961
+ consola.info(` Status: ${statusColor(version.status)}`);
2962
+ if (version.audit_verdict) {
2963
+ const verdictColor = version.audit_verdict === "pass" ? pc.green : version.audit_verdict === "warn" ? pc.yellow : pc.red;
2964
+ consola.info(` Audit: ${verdictColor(version.audit_verdict)}`);
2965
+ }
2966
+ if (version.image_audit_verdict) {
2967
+ const verdictColor = version.image_audit_verdict === "pass" ? pc.green : version.image_audit_verdict === "warn" ? pc.yellow : pc.red;
2968
+ consola.info(` Image audit: ${verdictColor(version.image_audit_verdict)}`);
2969
+ }
2970
+ }
2971
+ function displayInlineAuditResults(audit, imageAudit) {
2972
+ const verdictColor = audit.verdict === "pass" ? pc.green : audit.verdict === "warn" ? pc.yellow : pc.red;
2973
+ consola.info(` Audit: ${verdictColor(audit.verdict)} (risk: ${audit.riskScore}/100)`);
2974
+ if (audit.findings.length > 0) for (const finding of audit.findings) {
2975
+ const icon = finding.severity === "high" ? pc.red("!") : pc.yellow("~");
2976
+ consola.info(` ${icon} [${finding.category}] ${finding.description}`);
2977
+ }
2978
+ if (imageAudit) {
2979
+ const imgColor = imageAudit.verdict === "pass" ? pc.green : imageAudit.verdict === "warn" ? pc.yellow : pc.red;
2980
+ consola.info(` Image audit: ${imgColor(imageAudit.verdict)}`);
2981
+ }
2982
+ }
2983
+ const publishCommand = defineCommand({
2984
+ meta: {
2985
+ name: "publish",
2986
+ description: "Publish a plugin to the EmDash Marketplace"
2987
+ },
2988
+ args: {
2989
+ tarball: {
2990
+ type: "string",
2991
+ description: "Path to plugin tarball (default: build first via `emdash plugin bundle`)"
2992
+ },
2993
+ dir: {
2994
+ type: "string",
2995
+ description: "Plugin directory (used with --build, default: current directory)",
2996
+ default: process.cwd()
2997
+ },
2998
+ build: {
2999
+ type: "boolean",
3000
+ description: "Build the plugin before publishing",
3001
+ default: false
3002
+ },
3003
+ registry: {
3004
+ type: "string",
3005
+ description: "Marketplace registry URL",
3006
+ default: DEFAULT_REGISTRY
3007
+ },
3008
+ "no-wait": {
3009
+ type: "boolean",
3010
+ description: "Exit immediately after upload without waiting for audit (useful for CI)",
3011
+ default: false
3012
+ }
3013
+ },
3014
+ async run({ args }) {
3015
+ const registryUrl = args.registry;
3016
+ let tarballPath;
3017
+ if (args.tarball) tarballPath = resolve(args.tarball);
3018
+ else if (args.build) {
3019
+ consola.start("Building plugin...");
3020
+ const pluginDir = resolve(args.dir);
3021
+ try {
3022
+ const { runCommand } = await import("citty");
3023
+ const { bundleCommand } = await Promise.resolve().then(() => bundle_exports);
3024
+ await runCommand(bundleCommand, { rawArgs: ["--dir", pluginDir] });
3025
+ } catch {
3026
+ consola.error("Build failed");
3027
+ process.exit(1);
3028
+ }
3029
+ const { readdir } = await import("node:fs/promises");
3030
+ const distDir = resolve(pluginDir, "dist");
3031
+ const tarball = (await readdir(distDir)).find((f) => f.endsWith(".tar.gz"));
3032
+ if (!tarball) {
3033
+ consola.error("Build succeeded but no .tar.gz found in dist/");
3034
+ process.exit(1);
3035
+ }
3036
+ tarballPath = resolve(distDir, tarball);
3037
+ } else {
3038
+ const pluginDir = resolve(args.dir);
3039
+ const { readdir } = await import("node:fs/promises");
3040
+ try {
3041
+ const distDir = resolve(pluginDir, "dist");
3042
+ const tarball = (await readdir(distDir)).find((f) => f.endsWith(".tar.gz"));
3043
+ if (tarball) tarballPath = resolve(distDir, tarball);
3044
+ else {
3045
+ consola.error("No tarball found. Run `emdash plugin bundle` first or use --build.");
3046
+ process.exit(1);
3047
+ }
3048
+ } catch {
3049
+ consola.error("No dist/ directory found. Run `emdash plugin bundle` first or use --build.");
3050
+ process.exit(1);
3051
+ }
3052
+ }
3053
+ const sizeKB = ((await stat(tarballPath)).size / 1024).toFixed(1);
3054
+ consola.info(`Tarball: ${pc.dim(tarballPath)} (${sizeKB}KB)`);
3055
+ const manifest = await readManifestFromTarball(tarballPath);
3056
+ console.log();
3057
+ consola.info(`Plugin: ${pc.bold(`${manifest.id}@${manifest.version}`)}`);
3058
+ if (manifest.capabilities.length > 0) consola.info(`Capabilities: ${manifest.capabilities.join(", ")}`);
3059
+ if (manifest.allowedHosts?.length) consola.info(`Allowed hosts: ${manifest.allowedHosts.join(", ")}`);
3060
+ console.log();
3061
+ let token;
3062
+ const envToken = process.env.EMDASH_MARKETPLACE_TOKEN;
3063
+ const stored = !envToken ? getMarketplaceCredential(registryUrl) : null;
3064
+ if (envToken) {
3065
+ token = envToken;
3066
+ consola.info("Using EMDASH_MARKETPLACE_TOKEN for authentication");
3067
+ } else if (stored) {
3068
+ token = stored.token;
3069
+ consola.info(`Authenticated as ${pc.bold(stored.author?.name ?? "unknown")}`);
3070
+ } else {
3071
+ consola.info("Not logged in to marketplace. Starting GitHub authentication...");
3072
+ const result = await authenticateViaDeviceFlow(registryUrl);
3073
+ token = result.token;
3074
+ saveMarketplaceCredential(registryUrl, {
3075
+ token: result.token,
3076
+ expiresAt: new Date(Date.now() + 30 * 86400 * 1e3).toISOString(),
3077
+ author: {
3078
+ id: result.author.id,
3079
+ name: result.author.name
3080
+ }
3081
+ });
3082
+ consola.success(`Authenticated as ${pc.bold(result.author.name)}`);
3083
+ }
3084
+ consola.start("Checking marketplace...");
3085
+ const pluginRes = await fetch(new URL(`/api/v1/plugins/${manifest.id}`, registryUrl));
3086
+ if (pluginRes.status === 404 && !envToken) {
3087
+ consola.info(`Plugin ${pc.bold(manifest.id)} not found in marketplace. Registering...`);
3088
+ const createRes = await fetch(new URL("/api/v1/plugins", registryUrl), {
3089
+ method: "POST",
3090
+ headers: {
3091
+ "Content-Type": "application/json",
3092
+ Authorization: `Bearer ${token}`
3093
+ },
3094
+ body: JSON.stringify({
3095
+ id: manifest.id,
3096
+ name: manifest.id,
3097
+ capabilities: manifest.capabilities
3098
+ })
3099
+ });
3100
+ if (!createRes.ok) {
3101
+ const body = await createRes.json().catch(() => ({}));
3102
+ if (createRes.status === 401) {
3103
+ removeMarketplaceCredential(registryUrl);
3104
+ consola.error("Authentication expired. Please run `emdash plugin publish` again to re-authenticate.");
3105
+ process.exit(1);
3106
+ }
3107
+ consola.error(`Failed to register plugin: ${body.error ?? createRes.statusText}`);
3108
+ process.exit(1);
3109
+ }
3110
+ consola.success(`Registered ${pc.bold(manifest.id)}`);
3111
+ } else if (pluginRes.status === 404 && envToken) consola.info(`Plugin ${pc.bold(manifest.id)} will be auto-registered on publish`);
3112
+ else if (!pluginRes.ok) {
3113
+ consola.error(`Marketplace error: ${pluginRes.status}`);
3114
+ process.exit(1);
3115
+ }
3116
+ consola.start(`Publishing ${manifest.id}@${manifest.version}...`);
3117
+ const tarballData = await readFile(tarballPath);
3118
+ const formData = new FormData();
3119
+ formData.append("bundle", new Blob([tarballData], { type: "application/gzip" }), basename(tarballPath));
3120
+ const uploadUrl = new URL(`/api/v1/plugins/${manifest.id}/versions`, registryUrl);
3121
+ const uploadRes = await fetch(uploadUrl, {
3122
+ method: "POST",
3123
+ headers: { Authorization: `Bearer ${token}` },
3124
+ body: formData
3125
+ });
3126
+ if (!uploadRes.ok && uploadRes.status !== 202) {
3127
+ const body = await uploadRes.json().catch(() => ({}));
3128
+ if (uploadRes.status === 401) {
3129
+ if (envToken) consola.error("EMDASH_MARKETPLACE_TOKEN was rejected by the marketplace.");
3130
+ else {
3131
+ removeMarketplaceCredential(registryUrl);
3132
+ consola.error("Authentication expired. Please run `emdash plugin publish` again.");
3133
+ }
3134
+ process.exit(1);
3135
+ }
3136
+ if (uploadRes.status === 409) {
3137
+ if (body.latestVersion) consola.error(`Version ${manifest.version} must be greater than ${body.latestVersion}`);
3138
+ else consola.error(body.error ?? "Version conflict");
3139
+ process.exit(1);
3140
+ }
3141
+ if (uploadRes.status === 422 && body.audit) {
3142
+ consola.error("Plugin failed security audit:");
3143
+ consola.error(` Verdict: ${pc.red(body.audit.verdict)}`);
3144
+ consola.error(` Summary: ${body.audit.summary}`);
3145
+ process.exit(1);
3146
+ }
3147
+ consola.error(`Publish failed: ${body.error ?? uploadRes.statusText}`);
3148
+ process.exit(1);
3149
+ }
3150
+ const result = await uploadRes.json();
3151
+ console.log();
3152
+ consola.success(`Uploaded ${pc.bold(`${manifest.id}@${result.version}`)}`);
3153
+ consola.info(` Checksum: ${pc.dim(result.checksum)}`);
3154
+ consola.info(` Size: ${(result.bundleSize / 1024).toFixed(1)}KB`);
3155
+ if (uploadRes.status === 202) {
3156
+ consola.info(` Status: ${pc.yellow("pending")} (audit running in background)`);
3157
+ if (args["no-wait"]) {
3158
+ consola.info("Skipping audit wait (--no-wait). Check status later.");
3159
+ console.log();
3160
+ return;
3161
+ }
3162
+ consola.start("Waiting for security audit to complete...");
3163
+ const versionUrl = new URL(`/api/v1/plugins/${manifest.id}/versions/${manifest.version}`, registryUrl);
3164
+ const finalStatus = await pollVersionStatus(versionUrl.toString(), token);
3165
+ if (finalStatus) displayAuditResults(finalStatus);
3166
+ else {
3167
+ consola.warn("Audit did not complete within timeout. Check status later with:");
3168
+ consola.info(` ${pc.dim(`curl ${versionUrl.toString()}`)}`);
3169
+ }
3170
+ } else {
3171
+ if (result.audit) displayInlineAuditResults(result.audit, result.imageAudit ?? null);
3172
+ consola.info(` Status: ${pc.green(result.status ?? "published")}`);
3173
+ }
3174
+ console.log();
3175
+ }
3176
+ });
3177
+ const marketplaceLoginCommand = defineCommand({
3178
+ meta: {
3179
+ name: "login",
3180
+ description: "Log in to the EmDash Marketplace via GitHub"
3181
+ },
3182
+ args: { registry: {
3183
+ type: "string",
3184
+ description: "Marketplace registry URL",
3185
+ default: DEFAULT_REGISTRY
3186
+ } },
3187
+ async run({ args }) {
3188
+ const registryUrl = args.registry;
3189
+ const existing = getMarketplaceCredential(registryUrl);
3190
+ if (existing) {
3191
+ consola.info(`Already logged in as ${pc.bold(existing.author?.name ?? "unknown")}`);
3192
+ consola.info("Use `emdash plugin logout` to log out first.");
3193
+ return;
3194
+ }
3195
+ const result = await authenticateViaDeviceFlow(registryUrl);
3196
+ saveMarketplaceCredential(registryUrl, {
3197
+ token: result.token,
3198
+ expiresAt: new Date(Date.now() + 30 * 86400 * 1e3).toISOString(),
3199
+ author: {
3200
+ id: result.author.id,
3201
+ name: result.author.name
3202
+ }
3203
+ });
3204
+ consola.success(`Logged in as ${pc.bold(result.author.name)}`);
3205
+ }
3206
+ });
3207
+ const marketplaceLogoutCommand = defineCommand({
3208
+ meta: {
3209
+ name: "logout",
3210
+ description: "Log out of the EmDash Marketplace"
3211
+ },
3212
+ args: { registry: {
3213
+ type: "string",
3214
+ description: "Marketplace registry URL",
3215
+ default: DEFAULT_REGISTRY
3216
+ } },
3217
+ async run({ args }) {
3218
+ if (removeMarketplaceCredential(args.registry)) consola.success("Logged out of marketplace.");
3219
+ else consola.info("No marketplace credentials found.");
3220
+ }
3221
+ });
3222
+
3223
+ //#endregion
3224
+ //#region src/cli/commands/plugin.ts
3225
+ /**
3226
+ * emdash plugin
3227
+ *
3228
+ * Plugin management commands grouped under a single namespace.
3229
+ *
3230
+ * Subcommands:
3231
+ * - init: Scaffold a new plugin
3232
+ * - bundle: Bundle a plugin for marketplace distribution
3233
+ * - validate: Run bundle validation without producing a tarball
3234
+ * - publish: Publish a plugin to the marketplace
3235
+ * - login: Log in to the marketplace via GitHub
3236
+ * - logout: Log out of the marketplace
3237
+ *
3238
+ */
3239
+ const pluginCommand = defineCommand({
3240
+ meta: {
3241
+ name: "plugin",
3242
+ description: "Manage plugins"
3243
+ },
3244
+ subCommands: {
3245
+ init: pluginInitCommand,
3246
+ bundle: bundleCommand,
3247
+ validate: pluginValidateCommand,
3248
+ publish: publishCommand,
3249
+ login: marketplaceLoginCommand,
3250
+ logout: marketplaceLogoutCommand
3251
+ }
3252
+ });
3253
+
3254
+ //#endregion
3255
+ //#region src/cli/commands/schema.ts
3256
+ /**
3257
+ * emdash schema
3258
+ *
3259
+ * Manage collections and fields via the remote API
3260
+ */
3261
+ const listCommand$1 = defineCommand({
3262
+ meta: {
3263
+ name: "list",
3264
+ description: "List all collections"
3265
+ },
3266
+ args: { ...connectionArgs },
3267
+ async run({ args }) {
3268
+ try {
3269
+ output(await createClientFromArgs(args).collections(), args);
3270
+ } catch (error) {
3271
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3272
+ process.exit(1);
3273
+ }
3274
+ }
3275
+ });
3276
+ const getCommand = defineCommand({
3277
+ meta: {
3278
+ name: "get",
3279
+ description: "Get collection with fields"
3280
+ },
3281
+ args: {
3282
+ collection: {
3283
+ type: "positional",
3284
+ description: "Collection slug",
3285
+ required: true
3286
+ },
3287
+ ...connectionArgs
3288
+ },
3289
+ async run({ args }) {
3290
+ try {
3291
+ output(await createClientFromArgs(args).collection(args.collection), args);
3292
+ } catch (error) {
3293
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3294
+ process.exit(1);
3295
+ }
3296
+ }
3297
+ });
3298
+ const createCommand = defineCommand({
3299
+ meta: {
3300
+ name: "create",
3301
+ description: "Create a collection"
3302
+ },
3303
+ args: {
3304
+ collection: {
3305
+ type: "positional",
3306
+ description: "Collection slug",
3307
+ required: true
3308
+ },
3309
+ label: {
3310
+ type: "string",
3311
+ description: "Collection label",
3312
+ required: true
3313
+ },
3314
+ "label-singular": {
3315
+ type: "string",
3316
+ description: "Singular label (defaults to label)"
3317
+ },
3318
+ description: {
3319
+ type: "string",
3320
+ description: "Collection description"
3321
+ },
3322
+ ...connectionArgs
3323
+ },
3324
+ async run({ args }) {
3325
+ try {
3326
+ const data = await createClientFromArgs(args).createCollection({
3327
+ slug: args.collection,
3328
+ label: args.label,
3329
+ labelSingular: args["label-singular"] || args.label,
3330
+ description: args.description
3331
+ });
3332
+ consola$1.success(`Created collection "${args.collection}"`);
3333
+ output(data, args);
3334
+ } catch (error) {
3335
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3336
+ process.exit(1);
3337
+ }
3338
+ }
3339
+ });
3340
+ const deleteCommand = defineCommand({
3341
+ meta: {
3342
+ name: "delete",
3343
+ description: "Delete a collection"
3344
+ },
3345
+ args: {
3346
+ collection: {
3347
+ type: "positional",
3348
+ description: "Collection slug",
3349
+ required: true
3350
+ },
3351
+ force: {
3352
+ type: "boolean",
3353
+ description: "Skip confirmation"
3354
+ },
3355
+ ...connectionArgs
3356
+ },
3357
+ async run({ args }) {
3358
+ try {
3359
+ if (!args.force) {
3360
+ if (!await consola$1.prompt(`Delete collection "${args.collection}"?`, { type: "confirm" })) {
3361
+ consola$1.info("Cancelled");
3362
+ return;
3363
+ }
3364
+ }
3365
+ await createClientFromArgs(args).deleteCollection(args.collection);
3366
+ consola$1.success(`Deleted collection "${args.collection}"`);
3367
+ } catch (error) {
3368
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3369
+ process.exit(1);
3370
+ }
3371
+ }
3372
+ });
3373
+ const addFieldCommand = defineCommand({
3374
+ meta: {
3375
+ name: "add-field",
3376
+ description: "Add a field to a collection"
3377
+ },
3378
+ args: {
3379
+ collection: {
3380
+ type: "positional",
3381
+ description: "Collection slug",
3382
+ required: true
3383
+ },
3384
+ field: {
3385
+ type: "positional",
3386
+ description: "Field slug",
3387
+ required: true
3388
+ },
3389
+ type: {
3390
+ type: "string",
3391
+ description: "Field type (string, text, number, integer, boolean, datetime, image, reference, portableText, json)",
3392
+ required: true
3393
+ },
3394
+ label: {
3395
+ type: "string",
3396
+ description: "Field label"
3397
+ },
3398
+ required: {
3399
+ type: "boolean",
3400
+ description: "Whether the field is required"
3401
+ },
3402
+ ...connectionArgs
3403
+ },
3404
+ async run({ args }) {
3405
+ try {
3406
+ const data = await createClientFromArgs(args).createField(args.collection, {
3407
+ slug: args.field,
3408
+ type: args.type,
3409
+ label: args.label || args.field,
3410
+ required: args.required
3411
+ });
3412
+ consola$1.success(`Added field "${args.field}" to "${args.collection}"`);
3413
+ output(data, args);
3414
+ } catch (error) {
3415
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3416
+ process.exit(1);
3417
+ }
3418
+ }
3419
+ });
3420
+ const removeFieldCommand = defineCommand({
3421
+ meta: {
3422
+ name: "remove-field",
3423
+ description: "Remove a field from a collection"
3424
+ },
3425
+ args: {
3426
+ collection: {
3427
+ type: "positional",
3428
+ description: "Collection slug",
3429
+ required: true
3430
+ },
3431
+ field: {
3432
+ type: "positional",
3433
+ description: "Field slug",
3434
+ required: true
3435
+ },
3436
+ ...connectionArgs
3437
+ },
3438
+ async run({ args }) {
3439
+ try {
3440
+ await createClientFromArgs(args).deleteField(args.collection, args.field);
3441
+ consola$1.success(`Removed field "${args.field}" from "${args.collection}"`);
3442
+ } catch (error) {
3443
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3444
+ process.exit(1);
3445
+ }
3446
+ }
3447
+ });
3448
+ const schemaCommand = defineCommand({
3449
+ meta: {
3450
+ name: "schema",
3451
+ description: "Manage collections and fields"
3452
+ },
3453
+ subCommands: {
3454
+ list: listCommand$1,
3455
+ get: getCommand,
3456
+ create: createCommand,
3457
+ delete: deleteCommand,
3458
+ "add-field": addFieldCommand,
3459
+ "remove-field": removeFieldCommand
3460
+ }
3461
+ });
3462
+
3463
+ //#endregion
3464
+ //#region src/cli/commands/search-cmd.ts
3465
+ /**
3466
+ * emdash search
3467
+ *
3468
+ * Full-text search across content
3469
+ */
3470
+ const searchCommand = defineCommand({
3471
+ meta: {
3472
+ name: "search",
3473
+ description: "Full-text search across content"
3474
+ },
3475
+ args: {
3476
+ query: {
3477
+ type: "positional",
3478
+ description: "Search query",
3479
+ required: true
3480
+ },
3481
+ collection: {
3482
+ type: "string",
3483
+ alias: "c",
3484
+ description: "Filter by collection"
3485
+ },
3486
+ locale: {
3487
+ type: "string",
3488
+ description: "Filter by locale"
3489
+ },
3490
+ limit: {
3491
+ type: "string",
3492
+ alias: "l",
3493
+ description: "Maximum results to return"
3494
+ },
3495
+ ...connectionArgs
3496
+ },
3497
+ async run({ args }) {
3498
+ try {
3499
+ output(await createClientFromArgs(args).search(args.query, {
3500
+ collection: args.collection,
3501
+ locale: args.locale,
3502
+ limit: args.limit ? parseInt(args.limit, 10) : void 0
3503
+ }), args);
3504
+ } catch (error) {
3505
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3506
+ process.exit(1);
3507
+ }
3508
+ }
3509
+ });
3510
+
3511
+ //#endregion
3512
+ //#region src/cli/commands/seed.ts
3513
+ /**
3514
+ * emdash seed
3515
+ *
3516
+ * Apply a seed file to the database
3517
+ */
3518
+ async function fileExists(path) {
3519
+ try {
3520
+ await access(path);
3521
+ return true;
3522
+ } catch {
3523
+ return false;
3524
+ }
3525
+ }
3526
+ async function readPackageJson(cwd) {
3527
+ const pkgPath = resolve(cwd, "package.json");
3528
+ try {
3529
+ const content = await readFile(pkgPath, "utf-8");
3530
+ return JSON.parse(content);
3531
+ } catch {
3532
+ return null;
3533
+ }
3534
+ }
3535
+ /**
3536
+ * Resolve seed file path from:
3537
+ * 1. Positional argument (if provided)
3538
+ * 2. .emdash/seed.json (convention)
3539
+ * 3. package.json emdash.seed (config)
3540
+ */
3541
+ async function resolveSeedPath(cwd, positional) {
3542
+ if (positional) {
3543
+ const resolved = resolve(cwd, positional);
3544
+ if (await fileExists(resolved)) return resolved;
3545
+ consola.error(`Seed file not found: ${positional}`);
3546
+ return null;
3547
+ }
3548
+ const conventionPath = resolve(cwd, ".emdash", "seed.json");
3549
+ if (await fileExists(conventionPath)) return conventionPath;
3550
+ const pkg = await readPackageJson(cwd);
3551
+ if (pkg?.emdash?.seed) {
3552
+ const pkgSeedPath = resolve(cwd, pkg.emdash.seed);
3553
+ if (await fileExists(pkgSeedPath)) return pkgSeedPath;
3554
+ consola.warn(`Seed file from package.json not found: ${pkg.emdash.seed}`);
3555
+ }
3556
+ return null;
3557
+ }
3558
+ const seedCommand = defineCommand({
3559
+ meta: {
3560
+ name: "seed",
3561
+ description: "Apply a seed file to the database"
3562
+ },
3563
+ args: {
3564
+ path: {
3565
+ type: "positional",
3566
+ description: "Path to seed file (default: .emdash/seed.json)",
3567
+ required: false
3568
+ },
3569
+ database: {
3570
+ type: "string",
3571
+ alias: "d",
3572
+ description: "Database path",
3573
+ default: "./data.db"
3574
+ },
3575
+ cwd: {
3576
+ type: "string",
3577
+ description: "Working directory",
3578
+ default: process.cwd()
3579
+ },
3580
+ validate: {
3581
+ type: "boolean",
3582
+ description: "Validate only, don't apply",
3583
+ default: false
3584
+ },
3585
+ "no-content": {
3586
+ type: "boolean",
3587
+ description: "Skip sample content",
3588
+ default: false
3589
+ },
3590
+ "on-conflict": {
3591
+ type: "string",
3592
+ description: "Conflict handling: skip, update, error",
3593
+ default: "skip"
3594
+ },
3595
+ "uploads-dir": {
3596
+ type: "string",
3597
+ description: "Directory for media uploads",
3598
+ default: "./uploads"
3599
+ },
3600
+ "media-base-url": {
3601
+ type: "string",
3602
+ description: "Base URL for media files",
3603
+ default: "/_emdash/api/media/file"
3604
+ }
3605
+ },
3606
+ async run({ args }) {
3607
+ const cwd = resolve(args.cwd);
3608
+ consola.start("Loading seed file...");
3609
+ const seedPath = await resolveSeedPath(cwd, args.path);
3610
+ if (!seedPath) {
3611
+ consola.error("No seed file found");
3612
+ consola.info("Provide a path, create .emdash/seed.json, or set emdash.seed in package.json");
3613
+ process.exit(1);
3614
+ }
3615
+ consola.info(`Seed file: ${seedPath}`);
3616
+ let seed;
3617
+ try {
3618
+ const content = await readFile(seedPath, "utf-8");
3619
+ seed = JSON.parse(content);
3620
+ } catch (error) {
3621
+ consola.error("Failed to parse seed file:", error);
3622
+ process.exit(1);
3623
+ }
3624
+ consola.start("Validating seed file...");
3625
+ const validation = validateSeed(seed);
3626
+ if (validation.warnings.length > 0) for (const warning of validation.warnings) consola.warn(warning);
3627
+ if (!validation.valid) {
3628
+ consola.error("Seed validation failed:");
3629
+ for (const error of validation.errors) consola.error(` - ${error}`);
3630
+ process.exit(1);
3631
+ }
3632
+ consola.success("Seed file is valid");
3633
+ if (args.validate) {
3634
+ consola.success("Validation complete");
3635
+ return;
3636
+ }
3637
+ const dbPath = resolve(cwd, args.database);
3638
+ consola.info(`Database: ${dbPath}`);
3639
+ const db = createDatabase({ url: `file:${dbPath}` });
3640
+ consola.start("Running migrations...");
3641
+ try {
3642
+ const { applied } = await runMigrations(db);
3643
+ if (applied.length > 0) consola.success(`Applied ${applied.length} migrations`);
3644
+ else consola.info("Database up to date");
3645
+ } catch (error) {
3646
+ consola.error("Migration failed:", error);
3647
+ await db.destroy();
3648
+ process.exit(1);
3649
+ }
3650
+ const uploadsDir = resolve(cwd, args["uploads-dir"]);
3651
+ await mkdir(uploadsDir, { recursive: true });
3652
+ const storage = new LocalStorage({
3653
+ directory: uploadsDir,
3654
+ baseUrl: args["media-base-url"]
3655
+ });
3656
+ const onConflictRaw = args["on-conflict"];
3657
+ if (onConflictRaw !== "skip" && onConflictRaw !== "update" && onConflictRaw !== "error") {
3658
+ consola.error(`Invalid --on-conflict value: ${onConflictRaw}`);
3659
+ consola.info("Use: skip, update, or error");
3660
+ await db.destroy();
3661
+ process.exit(1);
3662
+ }
3663
+ const options = {
3664
+ includeContent: !args["no-content"],
3665
+ onConflict: onConflictRaw,
3666
+ storage
3667
+ };
3668
+ consola.start("Applying seed...");
3669
+ try {
3670
+ const result = await applySeed(db, seed, options);
3671
+ consola.success("Seed applied successfully!");
3672
+ consola.log("");
3673
+ if (result.settings.applied > 0) consola.info(`Settings: ${result.settings.applied} applied`);
3674
+ if (result.collections.created > 0 || result.collections.skipped > 0 || result.collections.updated > 0) consola.info(`Collections: ${result.collections.created} created, ${result.collections.skipped} skipped, ${result.collections.updated} updated`);
3675
+ if (result.fields.created > 0 || result.fields.skipped > 0 || result.fields.updated > 0) consola.info(`Fields: ${result.fields.created} created, ${result.fields.skipped} skipped, ${result.fields.updated} updated`);
3676
+ if (result.taxonomies.created > 0 || result.taxonomies.terms > 0) consola.info(`Taxonomies: ${result.taxonomies.created} created, ${result.taxonomies.terms} terms`);
3677
+ if (result.bylines.created > 0 || result.bylines.skipped > 0 || result.bylines.updated > 0) consola.info(`Bylines: ${result.bylines.created} created, ${result.bylines.skipped} skipped, ${result.bylines.updated} updated`);
3678
+ if (result.menus.created > 0 || result.menus.items > 0) consola.info(`Menus: ${result.menus.created} created, ${result.menus.items} items`);
3679
+ if (result.widgetAreas.created > 0 || result.widgetAreas.widgets > 0) consola.info(`Widget Areas: ${result.widgetAreas.created} created, ${result.widgetAreas.widgets} widgets`);
3680
+ if (result.content.created > 0 || result.content.skipped > 0 || result.content.updated > 0) consola.info(`Content: ${result.content.created} created, ${result.content.skipped} skipped, ${result.content.updated} updated`);
3681
+ if (result.media.created > 0 || result.media.skipped > 0) consola.info(`Media: ${result.media.created} created, ${result.media.skipped} skipped`);
3682
+ } catch (error) {
3683
+ consola.error("Seed failed:", error instanceof Error ? error.message : error);
3684
+ await db.destroy();
3685
+ process.exit(1);
3686
+ }
3687
+ await db.destroy();
3688
+ consola.success("Done!");
3689
+ }
3690
+ });
3691
+
3692
+ //#endregion
3693
+ //#region src/cli/commands/taxonomy.ts
3694
+ /**
3695
+ * emdash taxonomy
3696
+ *
3697
+ * Manage taxonomies and terms via the EmDash REST API.
3698
+ */
3699
+ /** Pattern to replace whitespace with hyphens for slug generation */
3700
+ const WHITESPACE_PATTERN = /\s+/g;
3701
+ const listCommand = defineCommand({
3702
+ meta: {
3703
+ name: "list",
3704
+ description: "List all taxonomies"
3705
+ },
3706
+ args: { ...connectionArgs },
3707
+ async run({ args }) {
3708
+ try {
3709
+ output(await createClientFromArgs(args).taxonomies(), args);
3710
+ } catch (error) {
3711
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3712
+ process.exit(1);
3713
+ }
3714
+ }
3715
+ });
3716
+ const termsCommand = defineCommand({
3717
+ meta: {
3718
+ name: "terms",
3719
+ description: "List terms in a taxonomy"
3720
+ },
3721
+ args: {
3722
+ name: {
3723
+ type: "positional",
3724
+ description: "Taxonomy name",
3725
+ required: true
3726
+ },
3727
+ limit: {
3728
+ type: "string",
3729
+ alias: "l",
3730
+ description: "Maximum terms to return"
3731
+ },
3732
+ cursor: {
3733
+ type: "string",
3734
+ description: "Pagination cursor"
3735
+ },
3736
+ ...connectionArgs
3737
+ },
3738
+ async run({ args }) {
3739
+ try {
3740
+ output(await createClientFromArgs(args).terms(args.name, {
3741
+ limit: args.limit ? parseInt(args.limit, 10) : void 0,
3742
+ cursor: args.cursor
3743
+ }), args);
3744
+ } catch (error) {
3745
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3746
+ process.exit(1);
3747
+ }
3748
+ }
3749
+ });
3750
+ const addTermCommand = defineCommand({
3751
+ meta: {
3752
+ name: "add-term",
3753
+ description: "Create a term in a taxonomy"
3754
+ },
3755
+ args: {
3756
+ taxonomy: {
3757
+ type: "positional",
3758
+ description: "Taxonomy name",
3759
+ required: true
3760
+ },
3761
+ name: {
3762
+ type: "string",
3763
+ description: "Term label",
3764
+ required: true
3765
+ },
3766
+ slug: {
3767
+ type: "string",
3768
+ description: "Term slug (defaults to slugified name)"
3769
+ },
3770
+ parent: {
3771
+ type: "string",
3772
+ description: "Parent term ID"
3773
+ },
3774
+ ...connectionArgs
3775
+ },
3776
+ async run({ args }) {
3777
+ try {
3778
+ const client = createClientFromArgs(args);
3779
+ const label = args.name;
3780
+ const slug = args.slug || label.toLowerCase().replace(WHITESPACE_PATTERN, "-");
3781
+ const term = await client.createTerm(args.taxonomy, {
3782
+ slug,
3783
+ label,
3784
+ parentId: args.parent
3785
+ });
3786
+ consola$1.success(`Created term "${label}" in ${args.taxonomy}`);
3787
+ output(term, args);
3788
+ } catch (error) {
3789
+ consola$1.error(error instanceof Error ? error.message : "Unknown error");
3790
+ process.exit(1);
3791
+ }
3792
+ }
3793
+ });
3794
+ const taxonomyCommand = defineCommand({
3795
+ meta: {
3796
+ name: "taxonomy",
3797
+ description: "Manage taxonomies and terms"
3798
+ },
3799
+ subCommands: {
3800
+ list: listCommand,
3801
+ terms: termsCommand,
3802
+ "add-term": addTermCommand
3803
+ }
3804
+ });
3805
+
3806
+ //#endregion
3807
+ //#region src/cli/commands/types.ts
3808
+ /**
3809
+ * emdash types
3810
+ *
3811
+ * Fetch schema from an EmDash instance and generate TypeScript types
3812
+ */
3813
+ const typesCommand = defineCommand({
3814
+ meta: {
3815
+ name: "types",
3816
+ description: "Generate TypeScript types from schema"
3817
+ },
3818
+ args: {
3819
+ ...connectionArgs,
3820
+ output: {
3821
+ type: "string",
3822
+ alias: "o",
3823
+ description: "Output path for generated types",
3824
+ default: ".emdash/types.ts"
3825
+ },
3826
+ cwd: {
3827
+ type: "string",
3828
+ description: "Working directory",
3829
+ default: process.cwd()
3830
+ }
3831
+ },
3832
+ async run({ args }) {
3833
+ const cwd = resolve(args.cwd);
3834
+ consola.start("Fetching schema...");
3835
+ try {
3836
+ const client = createClientFromArgs(args);
3837
+ const schema = await client.schemaExport();
3838
+ consola.success(`Found ${schema.collections.length} collections`);
3839
+ const types = await client.schemaTypes();
3840
+ const outputPath = resolve(cwd, args.output);
3841
+ await mkdir(dirname(outputPath), { recursive: true });
3842
+ await writeFile(outputPath, types, "utf-8");
3843
+ consola.success(`Generated ${args.output}`);
3844
+ await writeFile(resolve(dirname(outputPath), "schema.json"), JSON.stringify(schema, null, 2), "utf-8");
3845
+ consola.info(`Schema version: ${schema.version}`);
3846
+ consola.box({
3847
+ title: "Types generated",
3848
+ message: `${schema.collections.length} collections\n\nTypes: ${args.output}\nSchema: .emdash/schema.json`
3849
+ });
3850
+ } catch (error) {
3851
+ consola.error("Failed to fetch schema:", error instanceof Error ? error.message : error);
3852
+ process.exit(1);
3853
+ }
3854
+ }
3855
+ });
3856
+
3857
+ //#endregion
3858
+ //#region src/cli/index.ts
3859
+ /**
3860
+ * EmDash CLI
3861
+ *
3862
+ * Built with citty + clack (same stack as Nuxt CLI)
3863
+ *
3864
+ * Commands:
3865
+ * - init: Bootstrap database from template config, or interactive setup
3866
+ * - types: Generate TypeScript types from schema
3867
+ * - dev: Run dev server with local D1
3868
+ * - seed: Apply a seed file to the database
3869
+ * - export-seed: Export database schema and content as a seed file
3870
+ * - auth: Authentication utilities (secret generation)
3871
+ * - login/logout/whoami: Session management
3872
+ * - content: Create, read, update, delete content
3873
+ * - schema: Manage collections and fields
3874
+ * - media: Upload and manage media
3875
+ * - search: Full-text search
3876
+ * - taxonomy: Manage taxonomies and terms
3877
+ * - menu: Manage navigation menus
3878
+ * - plugin: Plugin management (init, bundle, validate, publish, login, logout)
3879
+ */
3880
+ runMain(defineCommand({
3881
+ meta: {
3882
+ name: "emdash",
3883
+ version: "0.0.0",
3884
+ description: "CLI for EmDash CMS"
3885
+ },
3886
+ subCommands: {
3887
+ init: initCommand,
3888
+ types: typesCommand,
3889
+ dev: devCommand,
3890
+ doctor: doctorCommand,
3891
+ seed: seedCommand,
3892
+ "export-seed": exportSeedCommand,
3893
+ auth: authCommand,
3894
+ login: loginCommand,
3895
+ logout: logoutCommand,
3896
+ whoami: whoamiCommand,
3897
+ content: contentCommand,
3898
+ schema: schemaCommand,
3899
+ media: mediaCommand,
3900
+ search: searchCommand,
3901
+ taxonomy: taxonomyCommand,
3902
+ menu: menuCommand,
3903
+ plugin: pluginCommand
3904
+ }
3905
+ }));
3906
+
3907
+ //#endregion
3908
+ export { };
3909
+ //# sourceMappingURL=index.mjs.map