emdash 0.0.0-a → 0.0.1

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 -1
  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 +1333 -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-B-u2F2b6.mjs +1412 -0
  127. package/dist/runner-B-u2F2b6.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 +687 -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 +353 -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 +328 -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 +120 -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 +117 -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 +105 -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 +62 -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 +72 -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 +699 -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 +288 -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 +136 -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 +42 -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 +253 -0
  646. package/src/storage/s3.ts +271 -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,1056 @@
1
+ /**
2
+ * WordPress WXR import implementation
3
+ *
4
+ * Two-phase import process:
5
+ * 1. Prepare: Analyze WXR, generate config and suggested live.config.ts
6
+ * 2. Execute: Import content using the generated/edited config
7
+ */
8
+
9
+ import { createReadStream } from "node:fs";
10
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
11
+ import { dirname, join, resolve } from "node:path";
12
+
13
+ import { gutenbergToPortableText } from "@emdash-cms/gutenberg-to-portable-text";
14
+ import pc from "picocolors";
15
+
16
+ import { slugify } from "#utils/slugify.js";
17
+
18
+ import { validateExternalUrl, ssrfSafeFetch } from "../../../import/ssrf.js";
19
+ import { parseWxr, type WxrData, type WxrPost, type WxrAttachment } from "../../wxr/parser.js";
20
+
21
+ // Regex patterns for WordPress import
22
+ const NUMBER_PATTERN = /^-?\d+(\.\d+)?$/;
23
+ const DOT_PATTERN = /\./g;
24
+ const NON_ALPHANUMERIC_UNDERSCORE_PATTERN = /[^a-zA-Z0-9_]/g;
25
+ const TRAILING_SLASH_PATTERN = /\/$/;
26
+ const PHP_STRING_PATTERN = /s:\d+:"(.*)";/;
27
+ const PHP_ARRAY_PATTERN = /s:(\d+):"([^"]+)";(?:s:(\d+):"([^"]+)"|i:(\d+)|b:([01]))/g;
28
+
29
+ /** Type guard for Record<string, unknown> */
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ // ============================================================================
35
+ // Types
36
+ // ============================================================================
37
+
38
+ export interface MigrationConfig {
39
+ /** WordPress site info */
40
+ site: {
41
+ title: string;
42
+ url: string;
43
+ };
44
+ /** Map WordPress post types to EmDash collections */
45
+ collections: Record<string, CollectionMapping>;
46
+ /** Map WordPress meta keys to EmDash field names */
47
+ fields: Record<string, FieldMapping>;
48
+ /** Post types to skip */
49
+ skipPostTypes: string[];
50
+ /** Meta keys to skip (internal WP fields) */
51
+ skipMetaKeys: string[];
52
+ }
53
+
54
+ export interface CollectionMapping {
55
+ /** EmDash collection name */
56
+ collection: string;
57
+ /** Whether to import this type */
58
+ enabled: boolean;
59
+ /** Number of items found */
60
+ count: number;
61
+ }
62
+
63
+ export interface FieldMapping {
64
+ /** EmDash field name (supports dot notation for nested) */
65
+ field: string;
66
+ /** Field type hint */
67
+ type: "string" | "number" | "boolean" | "date" | "reference" | "json";
68
+ /** Whether to import this field */
69
+ enabled: boolean;
70
+ /** Number of posts with this field */
71
+ count: number;
72
+ /** Sample values for reference */
73
+ samples: string[];
74
+ }
75
+
76
+ export interface PrepareOptions {
77
+ outputDir: string;
78
+ configPath: string;
79
+ verbose: boolean;
80
+ dryRun: boolean;
81
+ json: boolean;
82
+ }
83
+
84
+ export interface ExecuteOptions {
85
+ outputDir: string;
86
+ mediaDir?: string;
87
+ configPath: string;
88
+ skipMedia: boolean;
89
+ verbose: boolean;
90
+ dryRun: boolean;
91
+ json: boolean;
92
+ resume: boolean;
93
+ }
94
+
95
+ /** Progress tracking for resumable imports */
96
+ export interface ImportProgress {
97
+ /** ISO timestamp when import started */
98
+ startedAt: string;
99
+ /** ISO timestamp of last update */
100
+ updatedAt: string;
101
+ /** Source WXR file path */
102
+ sourceFile: string;
103
+ /** Config file used */
104
+ configFile: string;
105
+ /** Posts successfully imported (by WP ID) */
106
+ importedPosts: number[];
107
+ /** Attachments successfully downloaded (by WP ID) */
108
+ downloadedMedia: number[];
109
+ /** Items that failed with error messages */
110
+ errors: Array<{ id: number; type: string; error: string }>;
111
+ /** Summary stats */
112
+ stats: {
113
+ totalPosts: number;
114
+ totalMedia: number;
115
+ importedPosts: number;
116
+ downloadedMedia: number;
117
+ skippedPosts: number;
118
+ errorCount: number;
119
+ };
120
+ }
121
+
122
+ /** Structured result for agent-friendly output */
123
+ export interface ImportResult {
124
+ success: boolean;
125
+ phase: "prepare" | "execute";
126
+ dryRun: boolean;
127
+ /** Summary of what was/would be done */
128
+ summary: {
129
+ postsAnalyzed?: number;
130
+ postsImported?: number;
131
+ postsSkipped?: number;
132
+ mediaDownloaded?: number;
133
+ mediaSkipped?: number;
134
+ errors: number;
135
+ };
136
+ /** Files created/would be created */
137
+ files: Array<{
138
+ path: string;
139
+ action: "created" | "skipped" | "would_create";
140
+ }>;
141
+ /** Errors encountered */
142
+ errors: Array<{ id?: number; message: string }>;
143
+ /** Next steps for the user/agent */
144
+ nextSteps: string[];
145
+ }
146
+
147
+ // ============================================================================
148
+ // Phase 1: Prepare
149
+ // ============================================================================
150
+
151
+ export async function prepareWordPressImport(
152
+ filePath: string,
153
+ options: PrepareOptions,
154
+ ): Promise<ImportResult> {
155
+ const result: ImportResult = {
156
+ success: true,
157
+ phase: "prepare",
158
+ dryRun: options.dryRun,
159
+ summary: { errors: 0 },
160
+ files: [],
161
+ errors: [],
162
+ nextSteps: [],
163
+ };
164
+
165
+ const log = (msg: string) => !options.json && console.log(msg);
166
+
167
+ if (options.dryRun) {
168
+ log(pc.yellow("[DRY RUN] ") + pc.cyan("Analyzing WordPress export...\n"));
169
+ } else {
170
+ log(pc.cyan("Analyzing WordPress export...\n"));
171
+ }
172
+ log(pc.dim(`File: ${filePath}`));
173
+
174
+ // Parse WXR
175
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
176
+ const wxr = await parseWxr(stream);
177
+
178
+ // Analyze content
179
+ const analysis = analyzeWxrContent(wxr, options.verbose);
180
+
181
+ // Generate migration config
182
+ const config = generateMigrationConfig(wxr, analysis);
183
+
184
+ result.summary.postsAnalyzed = wxr.posts.length;
185
+
186
+ // Write config file (or report what would be written)
187
+ if (options.dryRun) {
188
+ log(pc.yellow(`\n[DRY RUN] Would write: ${options.configPath}`));
189
+ result.files.push({ path: options.configPath, action: "would_create" });
190
+ } else {
191
+ await mkdir(dirname(options.configPath), { recursive: true });
192
+ await writeFile(options.configPath, JSON.stringify(config, null, 2));
193
+ log(pc.green(`\nWrote migration config: ${options.configPath}`));
194
+ result.files.push({ path: options.configPath, action: "created" });
195
+ }
196
+
197
+ // Generate suggested live.config.ts
198
+ const liveConfigPath = join(options.outputDir, "suggested-live.config.ts");
199
+ const liveConfigContent = generateLiveConfig(config, analysis);
200
+
201
+ if (options.dryRun) {
202
+ log(pc.yellow(`[DRY RUN] Would write: ${liveConfigPath}`));
203
+ result.files.push({ path: liveConfigPath, action: "would_create" });
204
+ } else {
205
+ await mkdir(dirname(liveConfigPath), { recursive: true });
206
+ await writeFile(liveConfigPath, liveConfigContent);
207
+ log(pc.green(`Wrote suggested config: ${liveConfigPath}`));
208
+ result.files.push({ path: liveConfigPath, action: "created" });
209
+ }
210
+
211
+ // Summary
212
+ log(pc.cyan("\n=== Analysis Summary ===\n"));
213
+
214
+ log(pc.bold("Post Types:"));
215
+ for (const [type, mapping] of Object.entries(config.collections)) {
216
+ const status = mapping.enabled ? pc.green("enabled") : pc.yellow("disabled");
217
+ log(` ${type} → ${mapping.collection} (${mapping.count} items) [${status}]`);
218
+ }
219
+
220
+ log(pc.bold("\nCustom Fields:"));
221
+ const enabledFields = Object.entries(config.fields).filter(([_, m]) => m.enabled);
222
+ const disabledFields = Object.entries(config.fields).filter(([_, m]) => !m.enabled);
223
+
224
+ for (const [key, mapping] of enabledFields.slice(0, 10)) {
225
+ log(` ${key} → ${mapping.field} (${mapping.type}, ${mapping.count} posts)`);
226
+ }
227
+ if (enabledFields.length > 10) {
228
+ log(pc.dim(` ... and ${enabledFields.length - 10} more`));
229
+ }
230
+ if (disabledFields.length > 0) {
231
+ log(pc.dim(` (${disabledFields.length} internal fields hidden)`));
232
+ }
233
+
234
+ // Next steps
235
+ result.nextSteps = [
236
+ `Review and edit: ${options.configPath}`,
237
+ `Review suggested config: ${liveConfigPath}`,
238
+ "Copy relevant parts to your src/live.config.ts",
239
+ `Run: emdash import wordpress ${filePath} --execute`,
240
+ ];
241
+
242
+ log(pc.cyan("\n=== Next Steps ===\n"));
243
+ for (const step of result.nextSteps) {
244
+ log(` ${step}`);
245
+ }
246
+ log("");
247
+
248
+ // JSON output for agents
249
+ if (options.json) {
250
+ console.log(JSON.stringify(result, null, 2));
251
+ }
252
+
253
+ return result;
254
+ }
255
+
256
+ interface ContentAnalysis {
257
+ postTypes: Map<string, number>;
258
+ metaKeys: Map<string, MetaKeyInfo>;
259
+ categories: number;
260
+ tags: number;
261
+ attachments: number;
262
+ authors: string[];
263
+ }
264
+
265
+ interface MetaKeyInfo {
266
+ count: number;
267
+ samples: string[];
268
+ isInternal: boolean;
269
+ inferredType: "string" | "number" | "boolean" | "date" | "reference" | "json";
270
+ }
271
+
272
+ function analyzeWxrContent(wxr: WxrData, _verbose: boolean): ContentAnalysis {
273
+ const postTypes = new Map<string, number>();
274
+ const metaKeys = new Map<string, MetaKeyInfo>();
275
+
276
+ // Analyze posts
277
+ for (const post of wxr.posts) {
278
+ // Count post types
279
+ const type = post.postType || "post";
280
+ postTypes.set(type, (postTypes.get(type) || 0) + 1);
281
+
282
+ // Analyze meta keys
283
+ for (const [key, value] of post.meta) {
284
+ const existing = metaKeys.get(key);
285
+ if (existing) {
286
+ existing.count++;
287
+ if (existing.samples.length < 3 && value && !existing.samples.includes(value)) {
288
+ existing.samples.push(value.slice(0, 100));
289
+ }
290
+ } else {
291
+ metaKeys.set(key, {
292
+ count: 1,
293
+ samples: value ? [value.slice(0, 100)] : [],
294
+ isInternal: isInternalMetaKey(key),
295
+ inferredType: inferMetaType(key, value),
296
+ });
297
+ }
298
+ }
299
+ }
300
+
301
+ // Analyze attachments
302
+ for (const attachment of wxr.attachments) {
303
+ for (const [key, value] of attachment.meta) {
304
+ const existing = metaKeys.get(key);
305
+ if (existing) {
306
+ existing.count++;
307
+ } else {
308
+ metaKeys.set(key, {
309
+ count: 1,
310
+ samples: value ? [value.slice(0, 100)] : [],
311
+ isInternal: isInternalMetaKey(key),
312
+ inferredType: inferMetaType(key, value),
313
+ });
314
+ }
315
+ }
316
+ }
317
+
318
+ return {
319
+ postTypes,
320
+ metaKeys,
321
+ categories: wxr.categories.length,
322
+ tags: wxr.tags.length,
323
+ attachments: wxr.attachments.length,
324
+ authors: wxr.authors.map((a) => a.displayName || a.login || "Unknown"),
325
+ };
326
+ }
327
+
328
+ function isInternalMetaKey(key: string): boolean {
329
+ // WordPress internal keys
330
+ if (key.startsWith("_edit_")) return true;
331
+ if (key.startsWith("_wp_")) return true;
332
+ if (key === "_edit_last" || key === "_edit_lock") return true;
333
+ if (key === "_pingme" || key === "_encloseme") return true;
334
+
335
+ // But keep these useful ones
336
+ if (key === "_thumbnail_id") return false;
337
+ if (key.startsWith("_yoast_")) return false;
338
+ if (key.startsWith("_rank_math_")) return false;
339
+ if (key.startsWith("_aioseop_")) return false;
340
+
341
+ // Other underscore prefixes are usually internal
342
+ if (key.startsWith("_") && !key.startsWith("_yoast") && !key.startsWith("_thumbnail")) {
343
+ return true;
344
+ }
345
+
346
+ return false;
347
+ }
348
+
349
+ function inferMetaType(key: string, value: string | undefined): MetaKeyInfo["inferredType"] {
350
+ // Known patterns
351
+ if (key.endsWith("_id") || key === "_thumbnail_id") return "reference";
352
+ if (key.endsWith("_date") || key.endsWith("_time")) return "date";
353
+ if (key.endsWith("_count") || key.endsWith("_number") || key === "price") return "number";
354
+
355
+ // Check value
356
+ if (!value) return "string";
357
+
358
+ // Serialized PHP
359
+ if (value.startsWith("a:") || value.startsWith("O:") || value.startsWith("s:")) {
360
+ return "json";
361
+ }
362
+
363
+ // JSON
364
+ if (
365
+ (value.startsWith("{") && value.endsWith("}")) ||
366
+ (value.startsWith("[") && value.endsWith("]"))
367
+ ) {
368
+ return "json";
369
+ }
370
+
371
+ // Number
372
+ if (NUMBER_PATTERN.test(value)) return "number";
373
+
374
+ // Boolean
375
+ if (value === "0" || value === "1" || value === "true" || value === "false") {
376
+ return "boolean";
377
+ }
378
+
379
+ return "string";
380
+ }
381
+
382
+ function generateMigrationConfig(wxr: WxrData, analysis: ContentAnalysis): MigrationConfig {
383
+ const collections: Record<string, CollectionMapping> = {};
384
+ const fields: Record<string, FieldMapping> = {};
385
+
386
+ // Map post types to collections
387
+ for (const [type, count] of analysis.postTypes) {
388
+ // Skip internal types (see INTERNAL_POST_TYPES in utils.ts)
389
+ const skip = [
390
+ "revision",
391
+ "nav_menu_item",
392
+ "custom_css",
393
+ "customize_changeset",
394
+ "oembed_cache",
395
+ "wp_global_styles",
396
+ "wp_navigation",
397
+ "wp_template",
398
+ "wp_template_part",
399
+ "attachment", // Handled separately as media
400
+ "wp_block", // Handled separately as sections (reusable blocks)
401
+ ].includes(type);
402
+
403
+ collections[type] = {
404
+ collection: mapPostTypeToCollection(type),
405
+ enabled: !skip,
406
+ count,
407
+ };
408
+ }
409
+
410
+ // Map meta keys to fields
411
+ for (const [key, info] of analysis.metaKeys) {
412
+ fields[key] = {
413
+ field: mapMetaKeyToField(key),
414
+ type: info.inferredType,
415
+ enabled: !info.isInternal,
416
+ count: info.count,
417
+ samples: info.samples,
418
+ };
419
+ }
420
+
421
+ return {
422
+ site: {
423
+ title: wxr.site.title || "WordPress Site",
424
+ url: wxr.site.link || "",
425
+ },
426
+ collections,
427
+ fields,
428
+ skipPostTypes: [
429
+ "revision",
430
+ "nav_menu_item",
431
+ "custom_css",
432
+ "customize_changeset",
433
+ "oembed_cache",
434
+ "wp_global_styles",
435
+ "wp_navigation",
436
+ "wp_template",
437
+ "wp_template_part",
438
+ "attachment",
439
+ "wp_block",
440
+ ],
441
+ skipMetaKeys: ["_edit_last", "_edit_lock", "_pingme", "_encloseme"],
442
+ };
443
+ }
444
+
445
+ function mapPostTypeToCollection(postType: string): string {
446
+ const mapping: Record<string, string> = {
447
+ post: "posts",
448
+ page: "pages",
449
+ attachment: "media",
450
+ product: "products",
451
+ portfolio: "portfolio",
452
+ testimonial: "testimonials",
453
+ team: "team",
454
+ event: "events",
455
+ faq: "faqs",
456
+ };
457
+ return mapping[postType] || postType;
458
+ }
459
+
460
+ function mapMetaKeyToField(key: string): string {
461
+ // SEO plugins
462
+ if (key === "_yoast_wpseo_title") return "seo.title";
463
+ if (key === "_yoast_wpseo_metadesc") return "seo.description";
464
+ if (key === "_yoast_wpseo_focuskw") return "seo.keywords";
465
+ if (key === "_rank_math_title") return "seo.title";
466
+ if (key === "_rank_math_description") return "seo.description";
467
+ if (key === "_aioseop_title") return "seo.title";
468
+ if (key === "_aioseop_description") return "seo.description";
469
+
470
+ // Featured image
471
+ if (key === "_thumbnail_id") return "featuredImage";
472
+
473
+ // Remove leading underscore for others
474
+ if (key.startsWith("_")) {
475
+ return key.slice(1);
476
+ }
477
+
478
+ return key;
479
+ }
480
+
481
+ function generateLiveConfig(config: MigrationConfig, _analysis: ContentAnalysis): string {
482
+ const lines: string[] = [
483
+ "/**",
484
+ " * Suggested EmDash collections",
485
+ ` * Generated from: ${config.site.title}`,
486
+ " *",
487
+ " * Create these collections in the EmDash admin UI:",
488
+ " * 1. Go to /_emdash/admin/content-types",
489
+ " * 2. Click 'New Content Type'",
490
+ " * 3. Create the collections listed below with their fields",
491
+ " */",
492
+ "",
493
+ ];
494
+
495
+ // Generate collection suggestions for each enabled post type
496
+ for (const [type, mapping] of Object.entries(config.collections)) {
497
+ if (!mapping.enabled) continue;
498
+
499
+ lines.push(`// ${type} → "${mapping.collection}" (${mapping.count} items)`);
500
+ lines.push(`// Label: "${capitalize(mapping.collection)}"`);
501
+ lines.push(`// Label Singular: "${capitalize(singularize(mapping.collection))}"`);
502
+ lines.push("// Suggested fields:");
503
+ lines.push("// - title (string)");
504
+ lines.push("// - content (portableText)");
505
+ lines.push("// - excerpt (string, optional)");
506
+
507
+ // Add fields for this collection
508
+ const collectionFields = Object.entries(config.fields)
509
+ .filter(([_, m]) => m.enabled)
510
+ .slice(0, 10); // Limit to avoid huge output
511
+
512
+ for (const [key, fieldMapping] of collectionFields) {
513
+ lines.push(
514
+ `// - ${sanitizeFieldName(fieldMapping.field)} (${fieldMapping.type}) // from: ${key}`,
515
+ );
516
+ }
517
+
518
+ lines.push("");
519
+ }
520
+
521
+ return lines.join("\n");
522
+ }
523
+
524
+ function sanitizeFieldName(name: string): string {
525
+ // Handle nested fields like seo.title → seo: { title }
526
+ // For now, just flatten with underscore
527
+ return name.replace(DOT_PATTERN, "_").replace(NON_ALPHANUMERIC_UNDERSCORE_PATTERN, "");
528
+ }
529
+
530
+ function capitalize(str: string): string {
531
+ return str.charAt(0).toUpperCase() + str.slice(1);
532
+ }
533
+
534
+ function singularize(str: string): string {
535
+ if (str.endsWith("ies")) return str.slice(0, -3) + "y";
536
+ if (str.endsWith("s")) return str.slice(0, -1);
537
+ return str;
538
+ }
539
+
540
+ // ============================================================================
541
+ // Phase 2: Execute
542
+ // ============================================================================
543
+
544
+ export async function executeWordPressImport(
545
+ filePath: string,
546
+ options: ExecuteOptions,
547
+ ): Promise<ImportResult> {
548
+ const result: ImportResult = {
549
+ success: true,
550
+ phase: "execute",
551
+ dryRun: options.dryRun,
552
+ summary: {
553
+ postsImported: 0,
554
+ postsSkipped: 0,
555
+ mediaDownloaded: 0,
556
+ mediaSkipped: 0,
557
+ errors: 0,
558
+ },
559
+ files: [],
560
+ errors: [],
561
+ nextSteps: [],
562
+ };
563
+
564
+ const log = (msg: string) => !options.json && console.log(msg);
565
+ const progressPath = join(options.outputDir, ".wp-migration-progress.json");
566
+
567
+ if (options.dryRun) {
568
+ log(pc.yellow("[DRY RUN] ") + pc.cyan("Importing WordPress content...\n"));
569
+ } else {
570
+ log(pc.cyan("Importing WordPress content...\n"));
571
+ }
572
+
573
+ // Load config
574
+ let config: MigrationConfig;
575
+ try {
576
+ const configContent = await readFile(options.configPath, "utf-8");
577
+ config = JSON.parse(configContent);
578
+ } catch (error) {
579
+ const msg = `Failed to load migration config: ${options.configPath}`;
580
+ log(pc.red(msg));
581
+ log(pc.dim("Run with --prepare first to generate the config."));
582
+ result.success = false;
583
+ result.errors.push({ message: msg });
584
+ if (options.json) console.log(JSON.stringify(result, null, 2));
585
+ throw error;
586
+ }
587
+
588
+ log(pc.dim(`Using config: ${options.configPath}`));
589
+ log(pc.dim(`File: ${filePath}`));
590
+ if (options.resume) {
591
+ log(pc.dim(`Resume mode: will skip already-imported items`));
592
+ }
593
+ log("");
594
+
595
+ // Load or initialize progress tracking
596
+ let progress: ImportProgress;
597
+ if (options.resume) {
598
+ try {
599
+ const progressContent = await readFile(progressPath, "utf-8");
600
+ progress = JSON.parse(progressContent);
601
+ log(
602
+ pc.dim(
603
+ `Resuming from previous run (${progress.stats.importedPosts} posts already imported)`,
604
+ ),
605
+ );
606
+ } catch {
607
+ progress = createFreshProgress(filePath, options.configPath);
608
+ }
609
+ } else {
610
+ progress = createFreshProgress(filePath, options.configPath);
611
+ }
612
+
613
+ const alreadyImported = new Set(progress.importedPosts);
614
+ const alreadyDownloaded = new Set(progress.downloadedMedia);
615
+
616
+ // Parse WXR
617
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
618
+ const wxr = await parseWxr(stream);
619
+
620
+ // Update totals in progress
621
+ progress.stats.totalPosts = wxr.posts.length;
622
+ progress.stats.totalMedia = wxr.attachments.length;
623
+
624
+ // Build media map
625
+ const mediaMap = new Map<number, string>();
626
+ for (const attachment of wxr.attachments) {
627
+ if (attachment.id) {
628
+ mediaMap.set(attachment.id, `media-${attachment.id}`);
629
+ }
630
+ }
631
+
632
+ // Stats
633
+ const stats = {
634
+ imported: 0,
635
+ skipped: 0,
636
+ resumed: 0,
637
+ errors: 0,
638
+ byCollection: new Map<string, number>(),
639
+ };
640
+ const redirects = new Map<string, string>();
641
+
642
+ // Process posts
643
+ for (const post of wxr.posts) {
644
+ const postType = post.postType || "post";
645
+ const mapping = config.collections[postType];
646
+
647
+ // Skip if not mapped or disabled
648
+ if (!mapping || !mapping.enabled) {
649
+ stats.skipped++;
650
+ continue;
651
+ }
652
+
653
+ // Skip if already imported (resume mode)
654
+ if (post.id && alreadyImported.has(post.id)) {
655
+ stats.resumed++;
656
+ if (options.verbose) {
657
+ log(pc.dim(` [skip] ${mapping.collection}/${post.title} (already imported)`));
658
+ }
659
+ continue;
660
+ }
661
+
662
+ try {
663
+ const converted = convertPostWithConfig(post, mapping.collection, config, mediaMap);
664
+
665
+ const outputPath = join(options.outputDir, converted.collection, `${converted.slug}.json`);
666
+
667
+ if (options.dryRun) {
668
+ if (options.verbose) {
669
+ log(pc.yellow(` [would create] ${outputPath}`));
670
+ }
671
+ result.files.push({ path: outputPath, action: "would_create" });
672
+ } else {
673
+ await mkdir(dirname(outputPath), { recursive: true });
674
+ await writeFile(outputPath, JSON.stringify(converted.data, null, 2));
675
+ result.files.push({ path: outputPath, action: "created" });
676
+
677
+ // Update progress
678
+ if (post.id) {
679
+ progress.importedPosts.push(post.id);
680
+ progress.stats.importedPosts++;
681
+ }
682
+
683
+ if (options.verbose) {
684
+ log(pc.green(` ${mapping.collection}/${converted.slug}`));
685
+ }
686
+ }
687
+
688
+ stats.imported++;
689
+ stats.byCollection.set(
690
+ mapping.collection,
691
+ (stats.byCollection.get(mapping.collection) || 0) + 1,
692
+ );
693
+
694
+ // Track redirect
695
+ if (post.link && converted.slug) {
696
+ redirects.set(post.link, `/${converted.collection}/${converted.slug}`);
697
+ }
698
+ } catch (error) {
699
+ stats.errors++;
700
+ const errorMsg = error instanceof Error ? error.message : String(error);
701
+ result.errors.push({
702
+ id: post.id,
703
+ message: `${post.title}: ${errorMsg}`,
704
+ });
705
+
706
+ if (post.id) {
707
+ progress.errors.push({ id: post.id, type: "post", error: errorMsg });
708
+ }
709
+
710
+ if (options.verbose) {
711
+ log(pc.red(` Failed: ${post.title} - ${errorMsg}`));
712
+ }
713
+ }
714
+
715
+ // Save progress periodically (every 50 items)
716
+ if (!options.dryRun && stats.imported % 50 === 0) {
717
+ progress.updatedAt = new Date().toISOString();
718
+ await writeFile(progressPath, JSON.stringify(progress, null, 2));
719
+ }
720
+ }
721
+
722
+ // Download media
723
+ let mediaDownloaded = 0;
724
+ let mediaSkipped = 0;
725
+ if (!options.skipMedia && options.mediaDir && wxr.attachments.length > 0) {
726
+ log(pc.dim("\nDownloading media..."));
727
+ for (const attachment of wxr.attachments) {
728
+ // Skip if already downloaded (resume mode)
729
+ if (attachment.id && alreadyDownloaded.has(attachment.id)) {
730
+ mediaSkipped++;
731
+ continue;
732
+ }
733
+
734
+ try {
735
+ if (options.dryRun) {
736
+ if (options.verbose) {
737
+ log(pc.yellow(` [would download] ${attachment.url}`));
738
+ }
739
+ } else {
740
+ await downloadMedia(attachment, options.mediaDir);
741
+
742
+ if (attachment.id) {
743
+ progress.downloadedMedia.push(attachment.id);
744
+ progress.stats.downloadedMedia++;
745
+ }
746
+
747
+ if (options.verbose) {
748
+ log(pc.green(` ${attachment.url}`));
749
+ }
750
+ }
751
+ mediaDownloaded++;
752
+ } catch (error) {
753
+ const errorMsg = error instanceof Error ? error.message : String(error);
754
+ result.errors.push({
755
+ id: attachment.id,
756
+ message: `Media ${attachment.url}: ${errorMsg}`,
757
+ });
758
+
759
+ if (attachment.id) {
760
+ progress.errors.push({
761
+ id: attachment.id,
762
+ type: "media",
763
+ error: errorMsg,
764
+ });
765
+ }
766
+
767
+ if (options.verbose) {
768
+ log(pc.red(` Failed: ${attachment.url}`));
769
+ }
770
+ }
771
+ }
772
+ log(pc.dim(`Downloaded ${mediaDownloaded} media files`));
773
+ }
774
+
775
+ // Write redirects
776
+ const redirectPath = join(options.outputDir, "_redirects.json");
777
+ if (redirects.size > 0) {
778
+ if (options.dryRun) {
779
+ result.files.push({ path: redirectPath, action: "would_create" });
780
+ } else {
781
+ await writeFile(redirectPath, JSON.stringify(Object.fromEntries(redirects), null, 2));
782
+ result.files.push({ path: redirectPath, action: "created" });
783
+ }
784
+ }
785
+
786
+ // Save final progress
787
+ if (!options.dryRun) {
788
+ progress.updatedAt = new Date().toISOString();
789
+ progress.stats.skippedPosts = stats.skipped;
790
+ progress.stats.errorCount = stats.errors;
791
+ await writeFile(progressPath, JSON.stringify(progress, null, 2));
792
+ }
793
+
794
+ // Update result summary
795
+ result.summary.postsImported = stats.imported;
796
+ result.summary.postsSkipped = stats.skipped + stats.resumed;
797
+ result.summary.mediaDownloaded = mediaDownloaded;
798
+ result.summary.mediaSkipped = mediaSkipped;
799
+ result.summary.errors = stats.errors + result.errors.length;
800
+
801
+ // Summary
802
+ const prefix = options.dryRun ? "[DRY RUN] " : "";
803
+ log(pc.cyan(`\n=== ${prefix}Import ${options.dryRun ? "Preview" : "Complete"} ===\n`));
804
+ log(`${options.dryRun ? "Would import" : "Imported"}: ${pc.green(stats.imported.toString())}`);
805
+ if (stats.resumed > 0) {
806
+ log(`Resumed (skipped): ${pc.blue(stats.resumed.toString())}`);
807
+ }
808
+ log(`Skipped (disabled): ${pc.yellow(stats.skipped.toString())}`);
809
+ if (stats.errors > 0) {
810
+ log(`Errors: ${pc.red(stats.errors.toString())}`);
811
+ }
812
+
813
+ log(pc.bold("\nBy collection:"));
814
+ for (const [collection, count] of stats.byCollection) {
815
+ log(` ${collection}: ${count}`);
816
+ }
817
+
818
+ if (redirects.size > 0 && !options.dryRun) {
819
+ log(pc.dim(`\nRedirect map written to: ${redirectPath}`));
820
+ }
821
+
822
+ // Next steps
823
+ if (options.dryRun) {
824
+ result.nextSteps = [
825
+ `Run without --dry-run to perform the import`,
826
+ `emdash import wordpress ${filePath} --execute`,
827
+ ];
828
+ } else if (stats.errors > 0) {
829
+ result.nextSteps = [
830
+ `Fix errors and run with --resume to continue`,
831
+ `emdash import wordpress ${filePath} --execute --resume`,
832
+ ];
833
+ } else {
834
+ result.nextSteps = [
835
+ `Verify import: emdash migrate:verify --source ${filePath}`,
836
+ `Progress saved to: ${progressPath}`,
837
+ ];
838
+ }
839
+
840
+ if (!options.dryRun) {
841
+ log(pc.dim(`\nProgress saved to: ${progressPath}`));
842
+ log(pc.dim(`Run with --resume to continue from where you left off.`));
843
+ }
844
+
845
+ // JSON output for agents
846
+ if (options.json) {
847
+ console.log(JSON.stringify(result, null, 2));
848
+ }
849
+
850
+ return result;
851
+ }
852
+
853
+ function createFreshProgress(sourceFile: string, configFile: string): ImportProgress {
854
+ return {
855
+ startedAt: new Date().toISOString(),
856
+ updatedAt: new Date().toISOString(),
857
+ sourceFile: resolve(sourceFile),
858
+ configFile: resolve(configFile),
859
+ importedPosts: [],
860
+ downloadedMedia: [],
861
+ errors: [],
862
+ stats: {
863
+ totalPosts: 0,
864
+ totalMedia: 0,
865
+ importedPosts: 0,
866
+ downloadedMedia: 0,
867
+ skippedPosts: 0,
868
+ errorCount: 0,
869
+ },
870
+ };
871
+ }
872
+
873
+ interface ConvertedContent {
874
+ slug: string;
875
+ collection: string;
876
+ data: Record<string, unknown>;
877
+ }
878
+
879
+ function convertPostWithConfig(
880
+ post: WxrPost,
881
+ collection: string,
882
+ config: MigrationConfig,
883
+ mediaMap: Map<number, string>,
884
+ ): ConvertedContent {
885
+ // Convert content to Portable Text
886
+ const content = gutenbergToPortableText(post.content || "", { mediaMap });
887
+
888
+ // Extract slug
889
+ const slug = extractSlug(post.link) || slugify(post.title || "untitled");
890
+
891
+ // Build data object
892
+ const data: Record<string, unknown> = {
893
+ title: post.title,
894
+ content,
895
+ status: mapStatus(post.status),
896
+ publishedAt: post.pubDate ? new Date(post.pubDate).toISOString() : null,
897
+ createdAt: post.postDate ? new Date(post.postDate).toISOString() : null,
898
+ author: post.creator,
899
+ excerpt: post.excerpt,
900
+ categories: post.categories,
901
+ tags: post.tags,
902
+ };
903
+
904
+ // Map custom fields
905
+ for (const [wpKey, value] of post.meta) {
906
+ const fieldConfig = config.fields[wpKey];
907
+ if (!fieldConfig || !fieldConfig.enabled) continue;
908
+
909
+ const fieldName = fieldConfig.field;
910
+ let fieldValue: unknown = value;
911
+
912
+ // Type conversion
913
+ switch (fieldConfig.type) {
914
+ case "number":
915
+ fieldValue = parseFloat(value) || 0;
916
+ break;
917
+ case "boolean":
918
+ fieldValue = value === "1" || value === "true";
919
+ break;
920
+ case "date":
921
+ fieldValue = new Date(value).toISOString();
922
+ break;
923
+ case "reference":
924
+ // Map WordPress ID to new reference
925
+ const wpId = parseInt(value, 10);
926
+ fieldValue = mediaMap.get(wpId) || value;
927
+ break;
928
+ case "json":
929
+ try {
930
+ // Try PHP unserialize first
931
+ if (value.startsWith("a:") || value.startsWith("O:")) {
932
+ fieldValue = unserializePhp(value);
933
+ } else {
934
+ fieldValue = JSON.parse(value);
935
+ }
936
+ } catch {
937
+ fieldValue = value;
938
+ }
939
+ break;
940
+ }
941
+
942
+ // Handle nested fields (e.g., seo.title)
943
+ if (fieldName.includes(".")) {
944
+ const parts = fieldName.split(".");
945
+ let obj: Record<string, unknown> = data;
946
+ for (let i = 0; i < parts.length - 1; i++) {
947
+ const part = parts[i];
948
+ const nested = obj[part];
949
+ if (isRecord(nested)) {
950
+ obj = nested;
951
+ } else {
952
+ const newObj: Record<string, unknown> = {};
953
+ obj[part] = newObj;
954
+ obj = newObj;
955
+ }
956
+ }
957
+ obj[parts.at(-1)!] = fieldValue;
958
+ } else {
959
+ data[fieldName] = fieldValue;
960
+ }
961
+ }
962
+
963
+ // Original WP metadata for reference
964
+ data._wp = {
965
+ id: post.id,
966
+ link: post.link,
967
+ guid: post.guid,
968
+ postType: post.postType,
969
+ };
970
+
971
+ return { slug, collection, data };
972
+ }
973
+
974
+ async function downloadMedia(attachment: WxrAttachment, mediaDir: string): Promise<void> {
975
+ if (!attachment.url) return;
976
+
977
+ // Validate URL is not targeting internal/private addresses
978
+ const parsed = validateExternalUrl(attachment.url);
979
+ const filename = parsed.pathname.split("/").pop() || `media-${attachment.id}`;
980
+ const filePath = join(mediaDir, filename);
981
+
982
+ await mkdir(dirname(filePath), { recursive: true });
983
+
984
+ const response = await ssrfSafeFetch(attachment.url);
985
+ if (!response.ok) {
986
+ throw new Error(`HTTP ${response.status}`);
987
+ }
988
+
989
+ const buffer = await response.arrayBuffer();
990
+ await writeFile(filePath, Buffer.from(buffer));
991
+ }
992
+
993
+ function extractSlug(link: string | undefined): string | undefined {
994
+ if (!link) return undefined;
995
+ try {
996
+ const url = new URL(link);
997
+ const path = url.pathname.replace(TRAILING_SLASH_PATTERN, "");
998
+ const segments = path.split("/").filter(Boolean);
999
+ return segments.pop();
1000
+ } catch {
1001
+ return undefined;
1002
+ }
1003
+ }
1004
+
1005
+ function mapStatus(wpStatus: string | undefined): string {
1006
+ switch (wpStatus) {
1007
+ case "publish":
1008
+ return "published";
1009
+ case "draft":
1010
+ return "draft";
1011
+ case "pending":
1012
+ return "pending";
1013
+ case "private":
1014
+ return "private";
1015
+ case "trash":
1016
+ return "archived";
1017
+ default:
1018
+ return "draft";
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Basic PHP unserialize for simple arrays/strings
1024
+ * Not a full implementation, but handles common cases
1025
+ */
1026
+ function unserializePhp(str: string): unknown {
1027
+ // This is a simplified parser - for production, use a proper library
1028
+ try {
1029
+ if (str.startsWith("a:")) {
1030
+ // Array - extract key/value pairs
1031
+ const result: Record<string, unknown> = {};
1032
+ // Match pattern: s:4:"key";s:5:"value";
1033
+ const matches = str.matchAll(PHP_ARRAY_PATTERN);
1034
+ for (const match of matches) {
1035
+ const key = match[2];
1036
+ const strVal = match[4];
1037
+ const intVal = match[5];
1038
+ const boolVal = match[6];
1039
+ if (key) {
1040
+ if (strVal !== undefined) result[key] = strVal;
1041
+ else if (intVal !== undefined) result[key] = parseInt(intVal, 10);
1042
+ else if (boolVal !== undefined) result[key] = boolVal === "1";
1043
+ }
1044
+ }
1045
+ return result;
1046
+ }
1047
+ if (str.startsWith("s:")) {
1048
+ // Simple string
1049
+ const match = str.match(PHP_STRING_PATTERN);
1050
+ return match?.[1] || str;
1051
+ }
1052
+ return str;
1053
+ } catch {
1054
+ return str;
1055
+ }
1056
+ }